diff options
| -rw-r--r-- | src/papod.clj | 164 | ||||
| -rw-r--r-- | tests/integration.clj | 5 | ||||
| -rw-r--r-- | tests/unit.clj | 114 |
3 files changed, 249 insertions, 34 deletions
diff --git a/src/papod.clj b/src/papod.clj index ffcf1c2..2c05a0a 100644 --- a/src/papod.clj +++ b/src/papod.clj @@ -2030,11 +2030,39 @@ :else mn))) members))))) -(defn- join-one! - [handle client components] - (let [{:keys [conn clients channels]} components - nick (client-target client)] +(defn- chan-modes-for + [components handle] + (or (when (:chan-modes components) + (get @(:chan-modes components) handle)) + "+nt")) + +(defn- chan-symbol + [components handle] + (let [m (chan-modes-for components handle)] (cond + (string/includes? m "s") "@" + (string/includes? m "p") "*" + :else "="))) + +(defn- join-one! + ([handle client components] + (join-one! handle nil client components)) + ([handle key-arg client components] + (let [{:keys [conn clients channels]} components + nick (client-target client) + modes (chan-modes-for components handle) + existing? (and channels (seq (get @channels handle))) + invited? (when-let [invites (:invites components)] + (contains? + (get @invites handle #{}) nick)) + keys-map (some-> components :chan-keys deref) + limits (some-> components :chan-limits deref) + req-key (when keys-map (get keys-map handle)) + lim (when limits (get limits handle)) + full? (and lim + (>= (count (get @channels handle)) + lim))] + (cond ;; Validate channel name (not (channel-handle? handle)) [(numeric-reply client "403" @@ -2050,6 +2078,30 @@ [(numeric-reply client "403" (str handle " :No such channel"))] + ;; +i (invite-only): require invite + (and existing? + (not (contains? (get @channels handle) nick)) + (string/includes? modes "i") + (not invited?)) + [(numeric-reply client "473" + (str handle + " :Cannot join channel (+i)"))] + + ;; +k (key): require correct key + (and existing? + (not (contains? (get @channels handle) nick)) + req-key (not= req-key key-arg)) + [(numeric-reply client "475" + (str handle + " :Cannot join channel (+k)"))] + + ;; +l (limit): channel is full + (and existing? full? + (not (contains? (get @channels handle) nick))) + [(numeric-reply client "471" + (str handle + " :Cannot join channel (+l)"))] + :else (let [db (when conn (d/db conn)) channel-eid (when db (resolve-channel db handle)) @@ -2067,6 +2119,10 @@ :else (do + ;; Consume invite if any + (when-let [invites (:invites components)] + (swap! invites update handle + (fnil disj #{}) nick)) ;; PERSIST — single transaction (let [event-id (java.util.UUID/randomUUID) already? (and conn channel-eid @@ -2129,15 +2185,16 @@ "multi-prefix") members (names-for components handle :multi-prefix? mp?) + sym (chan-symbol components handle) w (:w @client)] (when (and w members) (deliver-to-client! w (str ":" +server-name+ " 353 " nick - " = " handle " :" members)) + " " sym " " handle " :" members)) (deliver-to-client! w (str ":" +server-name+ " 366 " nick " " handle " :End of /NAMES list")))) - []))))))) + [])))))))) (defn- handle-join @@ -2145,9 +2202,14 @@ (if (empty? params) [(numeric-reply client "461" "JOIN :Not enough parameters")] - (let [handles (string/split (first params) #",")] - (vec (mapcat #(join-one! % client components) - handles))))) + (let [handles (string/split (first params) #",") + keys (when (> (count params) 1) + (string/split (second params) #","))] + (vec + (mapcat + (fn [i h] + (join-one! h (get keys i) client components)) + (range) handles))))) (defn- handle-part [params client components] @@ -2875,19 +2937,29 @@ ;; +l/-l: limit (= \l mode-char) - (let [cur (get @(:chan-modes components) - target "+nt") + (let [cur (or (get @(:chan-modes components) + target) + "+nt") + flag "l" + new (if adding? + (if (string/includes? cur flag) + cur + (str cur flag)) + (string/replace cur flag "")) line (str ":" nick " MODE " target " " mode-str - (when mode-arg + (when (and adding? mode-arg) (str " " mode-arg)))] (when (:chan-modes components) - (if adding? - (swap! (:chan-modes components) - assoc target - (str "+nt" "l")) - (swap! (:chan-modes components) - assoc target "+nt"))) + (swap! (:chan-modes components) + assoc target new)) + (when-let [limits (:chan-limits components)] + (if (and adding? mode-arg) + (try + (swap! limits assoc target + (Integer/parseInt mode-arg)) + (catch NumberFormatException _ nil)) + (swap! limits dissoc target))) (when (and clients channels) (doseq [mn (get @channels target) :let [m (get @clients mn)] @@ -2922,10 +2994,26 @@ ;; +k/-k: channel key (= \k mode-char) - (let [line (str ":" nick " MODE " target + (let [cur (or (get @(:chan-modes components) + target) + "+nt") + flag "k" + new (if adding? + (if (string/includes? cur flag) + cur + (str cur flag)) + (string/replace cur flag "")) + line (str ":" nick " MODE " target " " mode-str - (when mode-arg + (when (and adding? mode-arg) (str " " mode-arg)))] + (when (:chan-modes components) + (swap! (:chan-modes components) + assoc target new)) + (when-let [keys (:chan-keys components)] + (if (and adding? mode-arg) + (swap! keys assoc target mode-arg) + (swap! keys dissoc target))) (when (and clients channels) (doseq [mn (get @channels target) :let [m (get @clients mn)] @@ -3092,6 +3180,9 @@ :else (let [nick (client-target client)] + (when-let [invites (:invites components)] + (swap! invites update handle + (fnil conj #{}) target)) (when-let [m (and clients (get @clients target))] (deliver-to-client! (:w m) @@ -3147,9 +3238,11 @@ (let [members (names-for components handle - :multi-prefix? mp?)] + :multi-prefix? mp?) + sym (chan-symbol + components handle)] [(numeric-reply client "353" - (str "= " handle + (str sym " " handle " :" members)) (numeric-reply client "366" (str handle @@ -3172,8 +3265,12 @@ mp?)] [(numeric-reply client "353" - (str "= " ch - " :" m))])) + (str + (chan-symbol + components + ch) + " " ch + " :" m))])) @chans))] (conj replies (numeric-reply client "366" @@ -3262,14 +3359,17 @@ :papod.process/hostname (.getHostName (java.net.InetAddress/getLocalHost)) :papod.process/started-at (java.util.Date.)}])] - {:conn conn - :cracha cracha-state - :process-id process-id - :clients (atom {}) - :channels (atom {}) - :ops (atom {}) - :voiced (atom {}) - :chan-modes (atom {})})) + {:conn conn + :cracha cracha-state + :process-id process-id + :clients (atom {}) + :channels (atom {}) + :ops (atom {}) + :voiced (atom {}) + :chan-modes (atom {}) + :chan-keys (atom {}) + :chan-limits (atom {}) + :invites (atom {})})) (defconst- +idle-timeout-ms+ (if-let [t (System/getenv "PAPOD_IDLE_TIMEOUT")] diff --git a/tests/integration.clj b/tests/integration.clj index 3e12833..04efc72 100644 --- a/tests/integration.clj +++ b/tests/integration.clj @@ -46,7 +46,10 @@ :channels (atom {}) :ops (atom {}) :voiced (atom {}) - :chan-modes (atom {})})) + :chan-modes (atom {}) + :chan-keys (atom {}) + :chan-limits (atom {}) + :invites (atom {})})) (defn- make-client "Creates a simulated client connection using piped streams. diff --git a/tests/unit.clj b/tests/unit.clj index 6b9e36b..5439c71 100644 --- a/tests/unit.clj +++ b/tests/unit.clj @@ -112,7 +112,10 @@ {:conn conn :cracha cracha-state :process-id proc-id :clients (atom {}) :channels (atom {}) :ops (atom {}) :voiced (atom {}) - :chan-modes (atom {})}))) + :chan-modes (atom {}) + :chan-keys (atom {}) + :chan-limits (atom {}) + :invites (atom {})}))) (defn test-network! [conn] @@ -1788,6 +1791,115 @@ (is (= 1 (count replies))) (is (string/includes? (first replies) "391")))))) +(deftest test_channel-modes + (testing "secret channel (+s) uses @ symbol in NAMES" + (let [{:keys [test-network-id] :as components} + (test-components-with-network) + out (java.io.ByteArrayOutputStream.) + c (registered-client "alice" out test-network-id) + comp (assoc components + :clients + (atom {"alice" + {:w out :client-atom c}}))] + (handle-join ["#sec"] c comp) + ;; Mark channel as secret + (swap! (:chan-modes comp) assoc "#sec" "+nts") + (let [replies (replies-for! + {:command "NAMES" :params ["" "#sec"]} + c comp)] + (is (string/includes? (first replies) "353")) + (is (string/includes? (first replies) "@ #sec"))))) + (testing "+i (invite-only) blocks JOIN with 473" + (let [{:keys [test-network-id] :as components} + (test-components-with-network) + a-out (java.io.ByteArrayOutputStream.) + b-out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" a-out test-network-id) + bob (registered-client "bob" b-out test-network-id) + comp (assoc components + :clients (atom + {"alice" {:w a-out + :client-atom alice} + "bob" {:w b-out + :client-atom bob}}) + :channels (atom {}))] + (handle-join ["#priv"] alice comp) + (swap! (:chan-modes comp) assoc "#priv" "+nti") + (let [replies (handle-join ["#priv"] bob comp)] + (is (= 1 (count replies))) + (is (string/includes? (first replies) "473"))))) + (testing "INVITE allows +i bypass" + (let [{:keys [test-network-id] :as components} + (test-components-with-network) + a-out (java.io.ByteArrayOutputStream.) + b-out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" a-out test-network-id) + bob (registered-client "bob" b-out test-network-id) + comp (assoc components + :clients (atom + {"alice" {:w a-out + :client-atom alice} + "bob" {:w b-out + :client-atom bob}}) + :channels (atom {}))] + (handle-join ["#priv"] alice comp) + (swap! (:chan-modes comp) assoc "#priv" "+nti") + (replies-for! + {:command "INVITE" :params ["" "bob" "#priv"]} + alice comp) + ;; Bob should now be able to join + (let [replies (handle-join ["#priv"] bob comp)] + (is (empty? + (filter #(re-find #" 473 " %) replies)))))) + (testing "+l limit blocks JOIN with 471" + (let [{:keys [test-network-id] :as components} + (test-components-with-network) + a-out (java.io.ByteArrayOutputStream.) + b-out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" a-out test-network-id) + bob (registered-client "bob" b-out test-network-id) + comp (assoc components + :clients (atom + {"alice" {:w a-out + :client-atom alice} + "bob" {:w b-out + :client-atom bob}}) + :channels (atom {}))] + (handle-join ["#small"] alice comp) + (swap! (:chan-modes comp) assoc "#small" "+ntl") + (swap! (:chan-limits comp) assoc "#small" 1) + (let [replies (handle-join ["#small"] bob comp)] + (is (string/includes? (first replies) "471"))))) + (testing "+k key blocks JOIN with 475" + (let [{:keys [test-network-id] :as components} + (test-components-with-network) + a-out (java.io.ByteArrayOutputStream.) + b-out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" a-out test-network-id) + bob (registered-client "bob" b-out test-network-id) + comp (assoc components + :clients (atom + {"alice" {:w a-out + :client-atom alice} + "bob" {:w b-out + :client-atom bob}}) + :channels (atom {}))] + (handle-join ["#locked"] alice comp) + (swap! (:chan-modes comp) assoc "#locked" "+ntk") + (swap! (:chan-keys comp) assoc "#locked" "secret") + ;; No key + (let [replies (handle-join ["#locked"] bob comp)] + (is (string/includes? (first replies) "475"))) + ;; Wrong key + (let [replies (handle-join ["#locked" "wrong"] + bob comp)] + (is (string/includes? (first replies) "475"))) + ;; Right key + (let [replies (handle-join ["#locked" "secret"] + bob comp)] + (is (empty? + (filter #(re-find #" 475 " %) replies))))))) + (defn -main [& _args] (binding [*out* *err*] |
