a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Networking/GitbayInstance.swift

ui-smoke
gitbay-ios/gitbay/Networking/GitbayInstance.swift history · blame · raw

71 lines · 2784 bytes

 1import Foundation
 2
 3/// A gitbay deployment. gitbay is self-hosted, so an account is only
 4/// meaningful paired with the instance it lives on.
 5nonisolated struct GitbayInstance: Sendable, Hashable, Codable {
 6
 7    /// Origin of the instance, e.g. `https://gitbay.org`. Path, query and
 8    /// fragment are stripped on init so endpoint construction cannot be
 9    /// steered by whatever the user pasted.
10    let baseURL: URL
11
12    enum InvalidURL: LocalizedError, Sendable {
13        case notAURL(String)
14        case insecureScheme(String)
15
16        var errorDescription: String? {
17            switch self {
18            case .notAURL(let text):
19                "\(text) is not a server address."
20            case .insecureScheme(let scheme):
21                "\(scheme):// would send your token in the clear. Use https."
22            }
23        }
24    }
25
26    init(url text: String) throws {
27        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
28        let withScheme = trimmed.contains("://") ? trimmed : "https://\(trimmed)"
29        guard var components = URLComponents(string: withScheme),
30              let host = components.host, !host.isEmpty else {
31            throw InvalidURL.notAURL(trimmed)
32        }
33        let scheme = (components.scheme ?? "https").lowercased()
34        // A bearer token over plaintext is a token given away. localhost is
35        // exempt so a self-hoster can point the app at their own machine.
36        guard scheme == "https" || (scheme == "http" && Self.isLoopback(host)) else {
37            throw InvalidURL.insecureScheme(scheme)
38        }
39        components.scheme = scheme
40        components.path = ""
41        components.query = nil
42        components.fragment = nil
43        guard let url = components.url else { throw InvalidURL.notAURL(trimmed) }
44        self.baseURL = url
45    }
46
47    private static func isLoopback(_ host: String) -> Bool {
48        host == "localhost" || host == "127.0.0.1" || host == "::1"
49    }
50
51    /// `GET /api/v1/read`  argv arrives as repeated query parameters.
52    func readURL(argv: [String]) -> URL {
53        var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
54        components.path = "/api/v1/read"
55        components.queryItems = argv.map { URLQueryItem(name: "argv", value: $0) }
56        return components.url!
57    }
58
59    /// `POST /api/v1/cmd`
60    var cmdURL: URL {
61        baseURL.appending(path: "/api/v1/cmd")
62    }
63
64    /// Whether a URL is on this instance. Checked before the Authorization
65    /// header goes on a request, and again on every redirect.
66    func isOwn(_ url: URL) -> Bool {
67        url.scheme?.lowercased() == baseURL.scheme
68            && url.host()?.lowercased() == baseURL.host()?.lowercased()
69            && url.port == baseURL.port
70    }
71}