Move SSD remote URL checks to typed Kotlin

ober

2edcccb057ceda43b5e4cca31f1ebabf44a53d70

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 36064b9..648f630 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1598,8 +1598,11 @@
                 textUtf8Bytes textUtf8SizeBytes textUtf8BytesWithinLimit
                 oldDestinationBytes urlEffectivePort
                 remoteBearerTokenPresent remoteBearerTokenValid
+                remoteEndpointIsHttps safeRemoteUrlParts
                 remotePinPresent remotePinOrNull remotePinSha256Length
-                remoteRelativePathSafe
+                remotePinBytesPresent remotePinBytesValid
+                remotePinAllowedForProtocol remoteConfigHasPin
+                remoteRelativePathSafe remoteOriginMatches
                 shouldCompareMoreCompleteTruth shouldWriteRemoteTruth
                 shouldReplaceLocalByTimes
                 zipEntryIsDirectory zipEntryDeclaresExcessiveSize)
@@ -1741,6 +1744,14 @@
         (def (urlEffectivePort (url : URL)) : Int32
           (let ((port (urlPort url)))
             (if (>= port (int32 0)) port (urlDefaultPort url))))
+        (def (remoteEndpointIsHttps (protocol : String) (host : String)) : Bool
+          (and (equal? protocol "https") (not (string-blank? host))))
+        (def (safeRemoteUrlParts (userInfo : (Nullable String))
+                                 (query : (Nullable String))
+                                 (fragment : (Nullable String))) : Bool
+          (and (nullable-null? userInfo)
+               (and (nullable-null? query)
+                    (nullable-null? fragment))))
         (def (remoteBearerTokenPresent (token : String)) : Bool
           (not (string-blank? token)))
         (def (remoteBearerTokenValid (token : String)) : Bool
@@ -1760,10 +1771,30 @@
               (nullable-none Bytes)))
         (def (remotePinSha256Length (pin : Bytes)) : Bool
           (= (bytesSize pin) (int32 32)))
+        (def (remotePinBytesPresent (pin : (Nullable Bytes))) : Bool
+          (not (nullable-null? pin)))
+        (def (remotePinBytesValid (pin : (Nullable Bytes))) : Bool
+          (if (remotePinBytesPresent pin)
+              (remotePinSha256Length (nullable-get pin))
+              #t))
+        (def (remotePinAllowedForProtocol (protocol : String)
+                                          (pin : (Nullable Bytes))) : Bool
+          (or (equal? protocol "https") (not (remotePinBytesPresent pin))))
+        (def (remoteConfigHasPin (config : RemoteConfig)) : Bool
+          (remotePinBytesPresent (RemoteConfig-spkiSha256 config)))
         (def (remoteRelativePathSafe (relative : String)) : Bool
           (and (not (string-blank? relative))
                (and (not (string-starts-with? relative "/"))
                     (not (string-contains? relative "..")))))
+        (def (remoteOriginMatches (candidateProtocol : String)
+                                  (candidateHost : String)
+                                  (candidatePort : Int32)
+                                  (baseProtocol : String)
+                                  (baseHost : String)
+                                  (basePort : Int32)) : Bool
+          (and (equal? candidateProtocol baseProtocol)
+               (and (equal? candidateHost baseHost)
+                    (= candidatePort basePort))))
         (def (shouldCompareMoreCompleteTruth (replaceIfMoreComplete : Bool)
                                              (destination : File)) : Bool
           (and replaceIfMoreComplete (fileExists destination)))
@@ -8253,16 +8284,16 @@
        "        val pinText = preferences.getString(\"spki_sha256\", null)?.trim().orEmpty()"
        "        val token = preferences.getString(\"bearer_token\", null)?.trim().orEmpty()"
        "        val url = URL(endpoint.trimEnd('/'))"
-       "        require(url.protocol == \"https\" && url.host.isNotBlank()) { \"HTTPS endpoint required\" }"
-       "        require(url.userInfo == null && url.query == null && url.ref == null) { \"Unsafe endpoint URL\" }"
+       "        require(remoteEndpointIsHttps(url.protocol, url.host)) { \"HTTPS endpoint required\" }"
+       "        require(safeRemoteUrlParts(url.userInfo, url.query, url.ref)) { \"Unsafe endpoint URL\" }"
        "        if (remoteBearerTokenPresent(token)) {"
        "            require(remoteBearerTokenValid(token)) {"
        "                \"Invalid remote authentication token\""
        "            }"
        "        }"
        "        val pin = remotePinOrNull(pinText, Base64.NO_WRAP)"
-       "        if (pin != null) require(remotePinSha256Length(pin)) { \"SPKI SHA-256 pin must decode to 32 bytes\" }"
-       "        require(url.protocol == \"https\" || pin == null) { \"SPKI pin requires HTTPS\" }"
+       "        require(remotePinBytesValid(pin)) { \"SPKI SHA-256 pin must decode to 32 bytes\" }"
+       "        require(remotePinAllowedForProtocol(url.protocol, pin)) { \"SPKI pin requires HTTPS\" }"
        "        return RemoteConfig(url, pin, token)"
        "    }"
        ""
@@ -8272,7 +8303,7 @@
        "        val candidate = URL(config.apiUrl.toString().trimEnd('/') + \"/\" + relative)"
        "        val basePort = urlEffectivePort(config.apiUrl)"
        "        val candidatePort = urlEffectivePort(candidate)"
-       "        require(candidate.protocol == config.apiUrl.protocol && candidate.host == config.apiUrl.host && candidatePort == basePort) {"
+       "        require(remoteOriginMatches(candidate.protocol, candidate.host, candidatePort, config.apiUrl.protocol, config.apiUrl.host, basePort)) {"
        "            \"Remote URL escaped configured HTTPS origin\""
        "        }"
        "        return candidate"
@@ -8287,7 +8318,7 @@
        "    ): HttpsURLConnection {"
        "        val config = remoteConfig ?: throw IllegalStateException(\"Remote SSD service is not locally configured\")"
        "        val connection = (remoteUrl(relative).openConnection() as HttpsURLConnection).apply {"
-       "            if (config.spkiSha256 != null) {"
+       "            if (remoteConfigHasPin(config)) {"
        "                sslSocketFactory = pinnedSocketFactory(config)"
        "            }"
        "            requestMethod = method"
@@ -8305,7 +8336,7 @@
        "            }"
        "        }"
        "        connection.connect()"
-       "        if (connection is HttpsURLConnection && config.spkiSha256 != null) {"
+       "        if (connection is HttpsURLConnection && remoteConfigHasPin(config)) {"
        "            val certificate = connection.serverCertificates.firstOrNull()"
        "                ?: throw SSLPeerUnverifiedException(\"Server provided no certificate\")"
        "            val actualPin = MessageDigest.getInstance(\"SHA-256\").digest(certificate.publicKey.encoded)"