diff options
Diffstat (limited to 'src/papod.clj')
| -rw-r--r-- | src/papod.clj | 167 |
1 files changed, 149 insertions, 18 deletions
diff --git a/src/papod.clj b/src/papod.clj index 245d3b7..edbedf5 100644 --- a/src/papod.clj +++ b/src/papod.clj @@ -755,6 +755,112 @@ (defconst- +charset+ "UTF-8") +;; PROXY protocol v2: untls injects a header on every accepted +;; connection carrying the SNI as PP2_TYPE_AUTHORITY. We use that +;; authority string to resolve the network the client is joining. +;; Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt + +(def- +proxy-v2-signature+ + (byte-array + [(unchecked-byte 0x0D) (unchecked-byte 0x0A) + (unchecked-byte 0x0D) (unchecked-byte 0x0A) + (unchecked-byte 0x00) (unchecked-byte 0x0D) + (unchecked-byte 0x0A) (unchecked-byte 0x51) + (unchecked-byte 0x55) (unchecked-byte 0x49) + (unchecked-byte 0x54) (unchecked-byte 0x0A)])) + +(defconst- +pp2-type-authority+ 0x02) + +(def- +network-name-fallback+ + (System/getenv "PAPOD_NETWORK_NAME")) + +(defn- read-fully! + [^java.io.InputStream is n] + (let [buf (byte-array n)] + (loop [off 0] + (cond + (= off n) buf + + :else + (let [r (.read is buf off (- n off))] + (if (pos? r) (recur (+ off r)) nil)))))) + +(defn- parse-proxy-v2-tlvs + "Walk a PROXY v2 TLV body looking for AUTHORITY (0x02). Returns + the value as a String, or empty string if absent / malformed." + [^bytes body] + (let [n (alength body)] + (loop [i 0] + (cond + (> (+ i 3) n) "" + + :else + (let [t (bit-and (aget body i) 0xFF) + vl (bit-or (bit-shift-left + (bit-and (aget body (+ i 1)) 0xFF) 8) + (bit-and (aget body (+ i 2)) 0xFF)) + vs (+ i 3)] + (cond + (> (+ vs vl) n) "" + + (= t +pp2-type-authority+) + (String. body vs vl ^String +charset+) + + :else (recur (+ vs vl)))))))) + +(defn- read-proxy-v2-authority! + "If the next bytes on `bis` are a PROXY v2 header, consume the + full header and return the AUTHORITY TLV value (or empty string + if absent). If the bytes are not a PROXY v2 header, reset the + stream so they can be read normally, and return nil." + [^java.io.BufferedInputStream bis] + (.mark bis 16) + (let [first12 (read-fully! bis 12)] + (cond + (nil? first12) + (do (try (.reset bis) (catch Exception _)) nil) + + (not (java.util.Arrays/equals + ^bytes first12 ^bytes +proxy-v2-signature+)) + (do (.reset bis) nil) + + :else + (let [hdr (read-fully! bis 4)] + (when hdr + (let [ver-cmd (bit-and (aget hdr 0) 0xFF) + len (bit-or + (bit-shift-left + (bit-and (aget hdr 2) 0xFF) 8) + (bit-and (aget hdr 3) 0xFF)) + body (if (pos? len) + (read-fully! bis len) + (byte-array 0))] + (when (and body (= ver-cmd 0x21)) + (parse-proxy-v2-tlvs body)))))))) + +(defn- find-or-create-network! + [conn authority] + (let [lookup #(ffirst + (d/q '{:find [?id] + :in [$ ?n] + :where [[?e :papod.network/name ?n] + [?e :papod.network/id ?id]]} + (d/db conn) %))] + (or (lookup authority) + (let [new-id (java.util.UUID/randomUUID)] + (try + @(d/transact conn + [{:db/ensure :papod.network/attrs + :papod.network/id new-id + :papod.network/name authority + :papod.network/description "" + :papod.network/type "public" + :papod.network/created-at (java.util.Date.)}]) + new-id + (catch Exception _ + ;; Race: another connection just created it. + (lookup authority))))))) + (defn- client-target [client] (or (:nick @client) "*")) @@ -1129,11 +1235,10 @@ "ERROR :Closing link: (Bad Password)"]) :else - (do ;; Assign default network if none selected via PASS - (when (and (not (:network-id @client)) - (:default-network-id components)) - (swap! client assoc :network-id - (:default-network-id components))) + (do ;; network-id is set at connect time from the PROXY v2 + ;; AUTHORITY (or PAPOD_NETWORK_NAME fallback); PASS may + ;; have overridden it explicitly. Either way, we assume + ;; it's set by now. (swap! client assoc :registered? true) (when-let [n (:n-unreg components)] (swap! n (fn [v] (max 0 (dec v))))) @@ -6687,6 +6792,8 @@ (Long/parseLong t) 300000)) +(declare client-loop-body!) + (defn- client-loop! [socket components] (let [_ (try @@ -6694,15 +6801,45 @@ (when s (.setSoTimeout s (int +idle-timeout-ms+)))) (catch Exception _)) - r (java.io.InputStreamReader. + bis (java.io.BufferedInputStream. (java.nio.channels.Channels/newInputStream socket) - +charset+) + 4096) + proxy-authority (try (read-proxy-v2-authority! bis) + (catch Exception _ nil)) + authority (or (and (string? proxy-authority) + (not (string/blank? proxy-authority)) + proxy-authority) + (and (not (string/blank? +network-name-fallback+)) + +network-name-fallback+)) + network-id (when authority + (try (find-or-create-network! + (:conn components) authority) + (catch Exception _ nil))) + r (java.io.InputStreamReader. bis ^String +charset+) w (java.nio.channels.Channels/newOutputStream socket) b (char-array +buffer-size+) conn-id (java.util.UUID/randomUUID) client (atom {:nick nil :user nil :pass nil :registered? false :w w :connection-id conn-id + :network-id network-id :socket socket})] + ;; No authority resolved (no PROXY header and no PAPOD_NETWORK_NAME + ;; fallback): refuse the connection. papod no longer carries a + ;; default network — every client must declare which network they + ;; are connecting to via the SNI surfaced by untls. + (if-not network-id + (do (try + (.write w (.getBytes + "ERROR :Closing link: (No network)\r\n" + ^String +charset+)) + (.flush w) + (catch Exception _)) + (try (.close socket) (catch Exception _))) + (client-loop-body! r w b conn-id client components)))) + +(defn- client-loop-body! + [^java.io.Reader r w b conn-id client components] + (let [socket (:socket @client)] ;; Record connection start (when-let [n (:n-unreg components)] (swap! n inc)) @@ -6828,14 +6965,8 @@ (str "datomic:mem://cracha-" (java.util.UUID/randomUUID))) fiinha-state) - components (init! db-uri cracha-state) - ;; Create default network for clients that don't send PASS - default-net (java.util.UUID/randomUUID) - _ @(d/transact (:conn components) - [{:db/ensure :papod.network/attrs - :papod.network/id default-net - :papod.network/name +server-name+ - :papod.network/description "" - :papod.network/type "public" - :papod.network/created-at (java.util.Date.)}])] - (start (assoc components :default-network-id default-net)))) + components (init! db-uri cracha-state)] + ;; Networks are no longer pre-created. The authority surfaced by + ;; untls (or the PAPOD_NETWORK_NAME fallback) is looked up — and + ;; created on first use — at connect time inside client-loop!. + (start components))) |
