import Foundation /// A gitbay deployment. gitbay is self-hosted, so an account is only /// meaningful paired with the instance it lives on. nonisolated struct GitbayInstance: Sendable, Hashable, Codable { /// Origin of the instance, e.g. `https://gitbay.org`. Path, query and /// fragment are stripped on init so endpoint construction cannot be /// steered by whatever the user pasted. let baseURL: URL enum InvalidURL: LocalizedError, Sendable { case notAURL(String) case insecureScheme(String) var errorDescription: String? { switch self { case .notAURL(let text): "\(text) is not a server address." case .insecureScheme(let scheme): "\(scheme):// would send your token in the clear. Use https." } } } init(url text: String) throws { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) let withScheme = trimmed.contains("://") ? trimmed : "https://\(trimmed)" guard var components = URLComponents(string: withScheme), let host = components.host, !host.isEmpty else { throw InvalidURL.notAURL(trimmed) } let scheme = (components.scheme ?? "https").lowercased() // A bearer token over plaintext is a token given away. localhost is // exempt so a self-hoster can point the app at their own machine. guard scheme == "https" || (scheme == "http" && Self.isLoopback(host)) else { throw InvalidURL.insecureScheme(scheme) } components.scheme = scheme components.path = "" components.query = nil components.fragment = nil guard let url = components.url else { throw InvalidURL.notAURL(trimmed) } self.baseURL = url } private static func isLoopback(_ host: String) -> Bool { host == "localhost" || host == "127.0.0.1" || host == "::1" } /// `GET /api/v1/read` — argv arrives as repeated query parameters. func readURL(argv: [String]) -> URL { var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)! components.path = "/api/v1/read" components.queryItems = argv.map { URLQueryItem(name: "argv", value: $0) } return components.url! } /// `POST /api/v1/cmd` var cmdURL: URL { baseURL.appending(path: "/api/v1/cmd") } /// Whether a URL is on this instance. Checked before the Authorization /// header goes on a request, and again on every redirect. func isOwn(_ url: URL) -> Bool { url.scheme?.lowercased() == baseURL.scheme && url.host()?.lowercased() == baseURL.host()?.lowercased() && url.port == baseURL.port } }