ServerUrlPolicy.swift 63 lignes · 2905 octets
import Foundation

/// Server URL policy: HTTPS required by default.
/// Cleartext HTTP is only accepted when the user checked
/// « Autoriser HTTP (clair) » (dev / internal network / demo).
enum ServerUrlPolicy {
    static let ERROR_HTTPS_REQUIRED =
        "L'URL doit commencer par https:// (cochez « Autoriser HTTP (clair) » pour le développement)."
    static let ERROR_INVALID_URL =
        "URL invalide : utilisez https://… (ou http://… si HTTP clair autorisé)."
    static let ERROR_LAN_HTTPS_NO_PORT =
        "URL HTTPS sans port sur une IP locale : Projectiaon écoute en HTTP sur le port 3000. " +
        "Utilisez http://192.168.x.x:3000 et cochez « Autoriser HTTP (clair) »."
    static let ERROR_LAN_HTTP_NO_PORT =
        "URL sans port : Projectiaon écoute sur le port 3000 (ex. http://192.168.x.x:3000)."

    static func isHttps(_ url: String) -> Bool {
        url.trimmingCharacters(in: .whitespacesAndNewlines).lowercased().hasPrefix("https://")
    }

    static func isHttp(_ url: String) -> Bool {
        url.trimmingCharacters(in: .whitespacesAndNewlines).lowercased().hasPrefix("http://")
    }

    /// API root: no trailing slash nor browser-test path (`/version`).
    static func normalizeBaseUrl(_ url: String) -> String {
        var trimmed = url.trimmingCharacters(in: .whitespacesAndNewlines)
        while trimmed.hasSuffix("/") { trimmed.removeLast() }
        if trimmed.hasSuffix("/version") { trimmed.removeLast("/version".count) }
        return trimmed
    }

    /// `nil` when the URL is acceptable, otherwise the error message to display.
    static func validate(_ url: String, allowCleartext: Bool) -> String? {
        let trimmed = normalizeBaseUrl(url)
        if trimmed.isEmpty { return ERROR_INVALID_URL }
        if isHttps(trimmed) {
            return lanHostWithoutPortError(trimmed)
        }
        if isHttp(trimmed) {
            if !allowCleartext { return ERROR_HTTPS_REQUIRED }
            return lanHostWithoutPortError(trimmed)
        }
        return ERROR_INVALID_URL
    }

    private static func lanHostWithoutPortError(_ url: String) -> String? {
        guard let components = URLComponents(string: url) else { return nil }
        if components.port != nil { return nil }
        guard let host = components.host else { return nil }
        if !isPrivateLanHost(host) { return nil }
        if isHttps(url) { return ERROR_LAN_HTTPS_NO_PORT }
        if isHttp(url) { return ERROR_LAN_HTTP_NO_PORT }
        return nil
    }

    private static func isPrivateLanHost(_ host: String) -> Bool {
        if host.caseInsensitiveCompare("localhost") == .orderedSame || host == "127.0.0.1" { return true }
        if host.hasPrefix("192.168.") { return true }
        if host.hasPrefix("10.") { return true }
        return host.range(of: #"^172\.(1[6-9]|2\d|3[01])\..+$"#, options: .regularExpression) != nil
    }
}