@@ -50,19 +50,33 @@ federationEnabled = federationSecret != ""
51type PollEvent = {id: MessageIdHex, eventType: Text, payload: {encryptedBlob: BlobHex, fromKey: PubkeyHex, timestamp: Timestamp, toKey: PubkeyHex}}
51type PollEvent = {id: MessageIdHex, eventType: Text, payload: {encryptedBlob: BlobHex, fromKey: PubkeyHex, timestamp: Timestamp, toKey: PubkeyHex}}
53−type GossipEventType = Text where \s -> s == "online" || s == "offline"
53+type GossipEventType = GossipOnline {} | GossipOffline {}
55+impl ToJSON GossipEventType where
56+ toJson et = toJson (case et of
57+ GossipOnline {} -> "online"
58+ GossipOffline {} -> "offline")
60+impl FromJSON GossipEventType where
62+ let s : Text = parseJson t in
63+ if s == "online" then GossipOnline {} else GossipOffline {}
65+isGossipOnline = \et -> case et of GossipOnline {} -> True {}; _ -> False {}
66+isGossipOffline = \et -> case et of GossipOffline {} -> True {}; _ -> False {}
55type GossipEvent = {eventType: GossipEventType, pubkey: PubkeyHex, server: ServerName, timestamp: Timestamp, ttl: Int<S>}
68type GossipEvent = {eventType: GossipEventType, pubkey: PubkeyHex, server: ServerName, timestamp: Timestamp, ttl: Int<S>}
57type ValidGossipEntry = {eventType: GossipEventType, pubkey: PubkeyHex, server: ServerName, expiresAt: Timestamp, timestamp: Timestamp, ttl: Int<S>}
70type ValidGossipEntry = {eventType: GossipEventType, pubkey: PubkeyHex, server: ServerName, expiresAt: Timestamp, timestamp: Timestamp, ttl: Int<S>}
59−isDeliveryStatus = \s -> s == "delivered" || s == "federated" || s == "queued"
60−type MessageStatus = Text where \s -> s == "unauthorized" || s == "rejected" || isDeliveryStatus s
72+type MessageStatus = Text where \s -> s == "unauthorized" || s == "rejected" || s == "delivered" || s == "federated" || s == "queued"
62type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen) && s == normalizeHex s
74type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen) && s == normalizeHex s
63type BlobHex = Text where \s -> isValidHex s && not (tooLong s maxBlobLen) && s == normalizeHex s
75type BlobHex = Text where \s -> isValidHex s && not (tooLong s maxBlobLen) && s == normalizeHex s
64type SignatureHex = Text where \s -> isValidHex s && not (tooLong s maxSignatureLen) && s == normalizeHex s
76type SignatureHex = Text where \s -> isValidHex s && not (tooLong s maxSignatureLen) && s == normalizeHex s
65−type MessageIdHex = Text where \s -> isValidHex s && not (tooLong s maxMessageIdLen) && s == normalizeHex s
77+isValidMessageId = \s -> isValidHex s && not (tooLong s maxMessageIdLen) && s == normalizeHex s
78+type MessageIdHex = Text where isValidMessageId
79+type OptionalMessageId = Text where \s -> s == "" || isValidMessageId s
66isValidServerName = \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s && not (isBadServerName s)
80isValidServerName = \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s && not (isBadServerName s)
67type ServerName = Text where isValidServerName
81type ServerName = Text where isValidServerName
68type OptionalServerName = Text where \s -> s == "" || isValidServerName s
82type OptionalServerName = Text where \s -> s == "" || isValidServerName s
@@ -78,7 +92,11 @@ type ForwardEntry = {messageId: MessageIdHex, toServer: ServerName, createdAt: T
78type AckedEntry = {messageId: MessageIdHex, toKey: PubkeyHex, ackedAt: Timestamp, originServer: ServerName}
92type AckedEntry = {messageId: MessageIdHex, toKey: PubkeyHex, ackedAt: Timestamp, originServer: ServerName}
79type AuthResult = Authorized {pubkey: PubkeyHex} | Unauthorized {}
93type AuthResult = Authorized {pubkey: PubkeyHex} | Unauthorized {}
80type PruneResult = {expiredLocal: [PresenceEntry], peerServers: [ServerName]}
94type PruneResult = {expiredLocal: [PresenceEntry], peerServers: [ServerName]}
81−type DeliveryStatus = Text where isDeliveryStatus
95+type DeliveryStatus = Delivered {} | Federated {} | Queued {}
96+deliveryStatusText = \s -> case s of
97+ Delivered {} -> "delivered"
98+ Federated {} -> "federated"
99+ Queued {} -> "queued"
82type SendResult = SendUnauthorized {} | SendRejected {} | SendDone {status: DeliveryStatus, remoteServers: [ServerName], fromKey: PubkeyHex}
100type SendResult = SendUnauthorized {} | SendRejected {} | SendDone {status: DeliveryStatus, remoteServers: [ServerName], fromKey: PubkeyHex}
@@ -121,13 +139,13 @@ route ClientApi where
122 /messages
140 /messages
123 POST {to: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp} / headers {authorization: Text}
141 POST {to: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp} / headers {authorization: Text}
124− -> {status: MessageStatus, messageId: Text}
142+ -> {status: MessageStatus, messageId: OptionalMessageId}
125 = SendMessage
143 = SendMessage
126 POST {messageIds: [MessageIdHex]} /ack headers {authorization: Text}
144 POST {messageIds: [MessageIdHex]} /ack headers {authorization: Text}
127 -> {ok: Bool}
145 -> {ok: Bool}
128 = AckMessages
146 = AckMessages
129 GET /lookup/{pubkey: PubkeyHex} headers {authorization: Text}
147 GET /lookup/{pubkey: PubkeyHex} headers {authorization: Text}
130− -> {online: Bool, server: Text, authorized: Bool}
148+ -> {online: Bool, server: AnyServerName, authorized: Bool}
131 = LookupPresence
149 = LookupPresence
133route FederationApi where
151route FederationApi where
@@ -204,28 +222,25 @@ containsBadUrlChar = \s -> any (chars s) (\c -> not (isAllowedServerChar c))
205isCgnatRange = \s ->
223isCgnatRange = \s ->
206 if take 4 s != "100." then False {}
224 if take 4 s != "100." then False {}
207− else if (take 1 (drop 4 s) == "7" || take 1 (drop 4 s) == "8" || take 1 (drop 4 s) == "9")
225+ else let d4 = take 1 (drop 4 s) in
208− && isDigitChar (take 1 (drop 5 s)) && take 1 (drop 6 s) == "."
226+ let d5 = take 1 (drop 5 s) in
227+ let d6 = take 1 (drop 6 s) in
228+ let d7 = take 1 (drop 7 s) in
229+ if contains d4 "789" && isDigitChar d5 && d6 == "."
209 && isAllDigitsOrDotsUntilColonOrEnd (drop 7 s)
230 && isAllDigitsOrDotsUntilColonOrEnd (drop 7 s)
210 then True {}
231 then True {}
211− else if take 1 (drop 4 s) == "6"
232+ else if d4 == "6" && contains d5 "456789" && d6 == "."
212− && (take 1 (drop 5 s) == "4" || take 1 (drop 5 s) == "5" || take 1 (drop 5 s) == "6" || take 1 (drop 5 s) == "7" || take 1 (drop 5 s) == "8" || take 1 (drop 5 s) == "9")
213− && take 1 (drop 6 s) == "."
214 && isAllDigitsOrDotsUntilColonOrEnd (drop 7 s)
233 && isAllDigitsOrDotsUntilColonOrEnd (drop 7 s)
215 then True {}
234 then True {}
216− else if take 1 (drop 4 s) == "1" && (take 1 (drop 5 s) == "0" || take 1 (drop 5 s) == "1")
235+ else if d4 == "1" && contains d5 "01" && isDigitChar d6 && d7 == "."
217− && isDigitChar (take 1 (drop 6 s)) && take 1 (drop 7 s) == "."
218 && isAllDigitsOrDotsUntilColonOrEnd (drop 8 s)
236 && isAllDigitsOrDotsUntilColonOrEnd (drop 8 s)
219 then True {}
237 then True {}
220− else if take 1 (drop 4 s) == "1" && take 1 (drop 5 s) == "2"
238+ else if d4 == "1" && d5 == "2" && contains d6 "01234567" && d7 == "."
221− && (take 1 (drop 6 s) == "0" || take 1 (drop 6 s) == "1" || take 1 (drop 6 s) == "2" || take 1 (drop 6 s) == "3"
222− || take 1 (drop 6 s) == "4" || take 1 (drop 6 s) == "5" || take 1 (drop 6 s) == "6" || take 1 (drop 6 s) == "7")
223− && take 1 (drop 7 s) == "."
224 && isAllDigitsOrDotsUntilColonOrEnd (drop 8 s)
239 && isAllDigitsOrDotsUntilColonOrEnd (drop 8 s)
225 then True {}
240 then True {}
226 else False {}
241 else False {}
228−containsMultipleColons = \s -> count (do c <- chars s; where c == ":"; yield c) > 1
243+containsMultipleColons = \s -> fold (\n c -> if c == ":" then n + 1 else n) 0 (chars s) > 1
230hasOctalOctet = \s ->
245hasOctalOctet = \s ->
231 if take 1 s == "0" && isDigitChar (take 1 (drop 1 s)) && isAllDigitsOrDotsUntilColonOrEnd (drop 2 s) then True {}
246 if take 1 s == "0" && isDigitChar (take 1 (drop 1 s)) && isAllDigitsOrDotsUntilColonOrEnd (drop 2 s) then True {}
@@ -273,30 +288,26 @@ containsSuffixN = \suffix suffixLen s ->
274containsSuffix = \suffix s -> containsSuffixN suffix (count (chars suffix)) s
289containsSuffix = \suffix s -> containsSuffixN suffix (count (chars suffix)) s
276−containsLocalhostSuffix = \s -> containsSuffix ".localhost" s
277−containsLocalSuffix = \s -> containsSuffix ".local" s
278−containsInternalSuffix = \s -> containsSuffix ".internal" s
280is172Private = \sn ->
291is172Private = \sn ->
281 take 4 sn == "172."
292 take 4 sn == "172."
282 && let d1 = take 1 (drop 4 sn) in
293 && let d1 = take 1 (drop 4 sn) in
283 let d2 = take 1 (drop 5 sn) in
294 let d2 = take 1 (drop 5 sn) in
284 take 1 (drop 6 sn) == "."
295 take 1 (drop 6 sn) == "."
285− && ((d1 == "1" && (d2 == "6" || d2 == "7" || d2 == "8" || d2 == "9"))
296+ && ((d1 == "1" && contains d2 "6789")
286 || (d1 == "2" && isDigitChar d2)
297 || (d1 == "2" && isDigitChar d2)
287− || (d1 == "3" && (d2 == "0" || d2 == "1")))
298+ || (d1 == "3" && contains d2 "01"))
289isHighOctetPrefix = \sn ->
300isHighOctetPrefix = \sn ->
290 take 1 sn == "2"
301 take 1 sn == "2"
291 && let d2 = take 1 (drop 1 sn) in
302 && let d2 = take 1 (drop 1 sn) in
292 let d3 = take 1 (drop 2 sn) in
303 let d3 = take 1 (drop 2 sn) in
293 take 1 (drop 3 sn) == "."
304 take 1 (drop 3 sn) == "."
294− && ((d2 == "2" && (d3 == "4" || d3 == "5" || d3 == "6" || d3 == "7" || d3 == "8" || d3 == "9"))
305+ && ((d2 == "2" && contains d3 "456789")
295− || (d2 == "3" && isDigitChar d3)
296− || (d2 == "4" && isDigitChar d3)
306+ || (contains d2 "34" && isDigitChar d3)
297− || (d2 == "5" && (d3 == "0" || d3 == "1" || d3 == "2" || d3 == "3" || d3 == "4" || d3 == "5")))
307+ || (d2 == "5" && contains d3 "012345"))
299−matchesHostname = \sn name nameLen ->
309+matchesHostname = \sn name ->
310+ let nameLen = count (chars name) in
300 take nameLen sn == name && (drop nameLen sn == "" || take 1 (drop nameLen sn) == ":")
311 take nameLen sn == name && (drop nameLen sn == "" || take 1 (drop nameLen sn) == ":")
302isBadServerName = \sn ->
313isBadServerName = \sn ->
@@ -304,14 +315,14 @@ isBadServerName = \sn ->
304 || take 1 sn == "."
315 || take 1 sn == "."
305 || take 1 sn == ":"
316 || take 1 sn == ":"
306 || containsBadUrlChar sn
317 || containsBadUrlChar sn
307− || matchesHostname sn "localhost" 9
318+ || matchesHostname sn "localhost"
308− || matchesHostname sn "localhost.localdomain" 21
319+ || matchesHostname sn "localhost.localdomain"
309− || containsLocalhostSuffix sn
320+ || containsSuffix ".localhost" sn
310− || matchesHostname sn "local" 5
321+ || matchesHostname sn "local"
311− || containsLocalSuffix sn
322+ || containsSuffix ".local" sn
312− || matchesHostname sn "internal" 8
323+ || matchesHostname sn "internal"
313− || containsInternalSuffix sn
324+ || containsSuffix ".internal" sn
314− || matchesHostname sn "metadata" 8
325+ || matchesHostname sn "metadata"
315 || take 4 sn == "127." || take 3 sn == "10."
326 || take 4 sn == "127." || take 3 sn == "10."
316 || take 8 sn == "192.168." || take 8 sn == "169.254."
327 || take 8 sn == "192.168." || take 8 sn == "169.254."
317 || take 2 sn == "0." || take 1 sn == "["
328 || take 2 sn == "0." || take 1 sn == "["
@@ -573,7 +584,7 @@ broadcastOfflineGossip = \t peerServers expiredLocal ->
573 let gossipEvents = mergeByKey (\a b -> a.pubkey == b.pubkey) (\x -> x.timestamp) [] (do
584 let gossipEvents = mergeByKey (\a b -> a.pubkey == b.pubkey) (\x -> x.timestamp) [] (do
574 p <- expiredLocal
585 p <- expiredLocal
575 let ttl = clamp offlineGossipMinTtl maxPresenceTtl ((p.expiresAt - p.timestamp) / 1000)
586 let ttl = clamp offlineGossipMinTtl maxPresenceTtl ((p.expiresAt - p.timestamp) / 1000)
576− yield {eventType: "offline", pubkey: p.pubkey, server: serverHostN, timestamp: p.expiresAt, ttl: ttl})
587+ yield {eventType: GossipOffline {}, pubkey: p.pubkey, server: serverHostN, timestamp: p.expiresAt, ttl: ttl})
577 sendBatchedGossip peerServers gossipEvents 2)
588 sendBatchedGossip peerServers gossipEvents 2)
579forkBroadcastOffline = \t result ->
590forkBroadcastOffline = \t result ->
@@ -764,7 +775,7 @@ serve = \req -> case req of
764 else result.pr.expiredLocal
775 else result.pr.expiredLocal
766 when (result.verified && federationEnabled)
777 when (result.verified && federationEnabled)
767− (sendBatchedGossip result.peerServers [{eventType: "online", pubkey: pubkey, server: serverHostN, timestamp: t, ttl: onlineGossipTtl}] 2)
778+ (sendBatchedGossip result.peerServers [{eventType: GossipOnline {}, pubkey: pubkey, server: serverHostN, timestamp: t, ttl: onlineGossipTtl}] 2)
768 broadcastOfflineGossip t result.pr.peerServers expiredLocal)
779 broadcastOfflineGossip t result.pr.peerServers expiredLocal)
769 yield (if result.verified
780 yield (if result.verified
770 then respond {token: token, expiresAt: expiry}
781 then respond {token: token, expiresAt: expiry}
@@ -808,9 +819,9 @@ serve = \req -> case req of
808 forwards <- *forwards
819 forwards <- *forwards
809 let forwardServers = takeN remoteServers (maxForwardsEntries - count forwards)
820 let forwardServers = takeN remoteServers (maxForwardsEntries - count forwards)
810 let shouldForward = federationEnabled && count forwardServers > 0
821 let shouldForward = federationEnabled && count forwardServers > 0
811− let status = if localOnline then "delivered"
822+ let status = if localOnline then Delivered {}
812− else if shouldForward then "federated"
823+ else if shouldForward then Federated {}
813− else "queued"
824+ else Queued {}
814 set *forwards = if shouldForward
825 set *forwards = if shouldForward
815 then union forwards (do s <- forwardServers; yield {messageId: msgId, toServer: s, createdAt: t, toKey: to, fromKey: fromKey, encryptedBlob: encryptedBlob, timestamp: clampedTs})
826 then union forwards (do s <- forwardServers; yield {messageId: msgId, toServer: s, createdAt: t, toKey: to, fromKey: fromKey, encryptedBlob: encryptedBlob, timestamp: clampedTs})
816 else forwards
827 else forwards
@@ -825,14 +836,15 @@ serve = \req -> case req of
825 timestamp: clampedTs, originServer: serverHostN, messageId: msgId,
836 timestamp: clampedTs, originServer: serverHostN, messageId: msgId,
826 authorization: "Bearer " ++ federationSecret})
837 authorization: "Bearer " ++ federationSecret})
827 whenOk fwdResult (removeForward msgId server))
838 whenOk fwdResult (removeForward msgId server))
828− when (status == "queued")
839+ case status of
829− (println ("sendMessage: forwards at capacity (" ++ show maxForwardsEntries ++ "), skipping forward tracking")))
840+ Queued {} -> println ("sendMessage: forwards at capacity (" ++ show maxForwardsEntries ++ "), skipping forward tracking")
830 _ -> yield {}
842 _ -> yield {}
831 broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
843 broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
832 yield (case result.send of
844 yield (case result.send of
833 SendUnauthorized _ -> respond {status: "unauthorized", messageId: ""}
845 SendUnauthorized _ -> respond {status: "unauthorized", messageId: ""}
834 SendRejected _ -> respond {status: "rejected", messageId: ""}
846 SendRejected _ -> respond {status: "rejected", messageId: ""}
835− SendDone {status} -> respond {status: status, messageId: msgId})
847+ SendDone {status} -> respond {status: deliveryStatusText status, messageId: msgId})
837 AckMessages {messageIds, authorization, respond} -> do
849 AckMessages {messageIds, authorization, respond} -> do
@@ -901,8 +913,8 @@ serve = \req -> case req of
901 yield (respond {ok: False {}})
913 yield (respond {ok: False {}})
903 let allValidEntries = validGossipEvents events t
915 let allValidEntries = validGossipEvents events t
904− let onlineEntries = do e <- allValidEntries; where e.eventType == "online"; yield e
916+ let onlineEntries = do e <- allValidEntries; where isGossipOnline e.eventType; yield e
905− let offlineEntries = do e <- allValidEntries; where e.eventType == "offline"; yield e
917+ let offlineEntries = do e <- allValidEntries; where isGossipOffline e.eventType; yield e
906 let filteredOnline = do
918 let filteredOnline = do
907 e <- onlineEntries
919 e <- onlineEntries
908 where none offlineEntries (\o -> o.server == fromServer && samePubkeyServer o e && o.timestamp > e.timestamp)
920 where none offlineEntries (\o -> o.server == fromServer && samePubkeyServer o e && o.timestamp > e.timestamp)