1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110 | (ns ird.p2p.descriptor
"The app's self-description at `GET https://<host>/.well-known/ap2p`:
endpoints, optional relay and mailbox blocks (present only when those
features are enabled — feature-detect from their presence), and the
node's limits, which the SDK paces against and never hardcodes.
Keys keep the service's snake_case names as keywords (:max_frame_bytes,
:mint_endpoint, ...); unknown keys and blocks are kept. When the fetch
fails the descriptor is synthesized from the documented default paths and
node-wide default limits, flagged :is-fallback true."
(:require [clojure.string :as str]
[cljs.core.async :as async :refer [go <!]]
[ird.p2p.async :as a]
[ird.p2p.errors :as err]
[ird.p2p.impl.http :as http]
[ird.p2p.impl.json :as json]))
(def default-limits
"The node-wide defaults, used only when the descriptor cannot be fetched."
{:max_rooms 500
:max_room_peers 16
:max_conns_per_ip 20
:max_bytes_per_sec 262144
:messages_per_sec 30
:message_burst 60
:max_frame_bytes 65536})
(defn descriptor-url [host] (str "https://" host "/.well-known/ap2p"))
(defn fallback
"What the descriptor of HOST would say if only the documented default
paths and node-wide limits were known. No mailbox (cannot be detected
offline)."
[host]
{:protocol "ap2p-signal"
:version "1"
:endpoints {:ws (str "wss://" host "/ws")
:turn_credentials (str "https://" host "/turn-credentials")}
:limits default-limits
:is-fallback true})
(defn- valid-limits? [l]
(and (map? l) (every? #(integer? (get l %)) (keys default-limits))))
(defn- valid-mailbox? [m]
(and (map? m) (string? (:endpoint m)) (integer? (:ttl_hours m))
(integer? (:max_mb m)) (integer? (:max_message_kb m)) (boolean? (:open_mint m))))
(defn- valid-relay? [r]
(and (map? r) (string? (:peer_id r)) (vector? (:multiaddrs r)) (every? string? (:multiaddrs r))))
(defn parse
"A descriptor map from the document TEXT (or an already-parsed map);
nil when it is not a usable descriptor (no endpoints). Missing
`protocol`/`version`/`limits` take their defaults; malformed optional
blocks are dropped, unknown keys kept."
[text-or-map]
(let [v (if (string? text-or-map) (json/parse text-or-map) text-or-map)]
(when (and (map? v) (map? (:endpoints v))
(string? (get-in v [:endpoints :ws]))
(string? (get-in v [:endpoints :turn_credentials])))
(cond-> (assoc v
:protocol (if (string? (:protocol v)) (:protocol v) "ap2p-signal")
:version (if (string? (:version v)) (:version v) "1")
:limits (if (valid-limits? (:limits v)) (:limits v) default-limits)
:is-fallback false)
(and (contains? v :relay) (not (valid-relay? (:relay v)))) (dissoc :relay)
(and (contains? v :mailbox) (not (valid-mailbox? (:mailbox v)))) (dissoc :mailbox)))))
(defn fetch
"Fetch the descriptor of HOST. OPTS: :origin (sent as the Origin header),
:http-fn (transport, default fetch over the global fetch), :timeout-ms
(default 3000). Channel of the descriptor — the fallback (never an error)
when the request fails or the body is not a descriptor."
[{:keys [host origin http-fn timeout-ms]}]
(go
(let [http (or http-fn http/fetch-http)
resp (<! (http {:method :get
:url (descriptor-url host)
:headers (cond-> {"Accept" "application/json"} origin (assoc "Origin" origin))
:timeout-ms (or timeout-ms 3000)}))]
(or (when (and (map? resp) (<= 200 (:status resp) 299))
(parse (:body resp)))
(fallback host)))))
(defn limits
"The limits block of DESCRIPTOR (always present)."
[descriptor]
(or (:limits descriptor) default-limits))
(defn mailbox-enabled?
"True when the app has its mailbox enabled (a mailbox block is present)."
[descriptor]
(some? (:mailbox descriptor)))
(defn open-mint?
"True when the mailbox mints tokens openly (with an allowed Origin)."
[descriptor]
(boolean (get-in descriptor [:mailbox :open_mint])))
(defn resolved-endpoints
"The URLs the SDK talks to: explicit OVERRIDES win, then the descriptor.
{:ws :turn-credentials :mailbox :mailbox-mint} (the last two nil when absent)."
([descriptor] (resolved-endpoints descriptor nil))
([descriptor overrides]
{:ws (or (:ws overrides) (get-in descriptor [:endpoints :ws]))
:turn-credentials (or (:turn-credentials overrides) (get-in descriptor [:endpoints :turn_credentials]))
:mailbox (or (:mailbox overrides) (get-in descriptor [:mailbox :endpoint]))
:mailbox-mint (or (:mailbox-mint overrides)
(when (open-mint? descriptor) (get-in descriptor [:mailbox :mint_endpoint])))}))
|