@@ -8,6 +8,7 @@
88
99serverHost = "localhost:8080"
1010federationSecret = "" -- must be set to enable federation
11+maintenanceSecret = "" -- set to enable /maintenance/prune independently of federation
1112federationScheme = "https://" -- scheme for federation HTTP calls
1213messageMaxAge = 86400000 -- 24 hours in milliseconds
1314maxPresenceTtl = 3600 -- max presence TTL from gossip in seconds
@@ -20,6 +21,7 @@ maxPollEvents = 50
2021maxPresenceEntries = 10000
2122maxMessagesPerRecipient = 500
2223clockSkewTolerance = 5000 -- 5 seconds tolerance for clock skew between federated servers
24+maxForwardsEntries = 10000
2325
2426-- ============================================
2527-- Types
@@ -102,11 +104,27 @@ genToken = do
102104 c <- randomInt 2147483647
103105 d <- randomInt 2147483647
104106 e <- randomInt 2147483647
105− yield (bytesToHex (textToBytes (show a ++ "." ++ show b ++ "." ++ show c ++ "." ++ show d ++ "." ++ show e)))
107+ f <- randomInt 2147483647
108+ g <- randomInt 2147483647
109+ h <- randomInt 2147483647
110+ yield (bytesToHex (textToBytes (show a ++ show b ++ show c ++ show d ++ show e ++ show f ++ show g ++ show h)))
106111
107112tooLong = \s maxLen -> not (drop maxLen s == "")
108113
109−isValidHex = \s -> not (s == "") && bytesToHex (bytesFromHex s) == s
114+isHexChar = \c ->
115+ c == "0" || c == "1" || c == "2" || c == "3" || c == "4" || c == "5" || c == "6" || c == "7" || c == "8" || c == "9" ||
116+ c == "a" || c == "b" || c == "c" || c == "d" || c == "e" || c == "f" ||
117+ c == "A" || c == "B" || c == "C" || c == "D" || c == "E" || c == "F"
118+
119+isValidHexPairs = \s ->
120+ if s == "" then True {}
121+ else if drop 1 s == "" then False {}
122+ else if isHexChar (take 1 s) && isHexChar (take 1 (drop 1 s)) then isValidHexPairs (drop 2 s)
123+ else False {}
124+
125+isValidHex = \s -> not (s == "") && isValidHexPairs s
126+
127+normalizeHex = \s -> bytesToHex (bytesFromHex s)
110128
111129containsBadUrlChar = \s ->
112130 if s == "" then False {}
@@ -115,6 +133,47 @@ containsBadUrlChar = \s ->
115133 then True {}
116134 else containsBadUrlChar (drop 1 s)
117135
136+isDigitChar = \c -> c == "0" || c == "1" || c == "2" || c == "3" || c == "4" || c == "5" || c == "6" || c == "7" || c == "8" || c == "9"
137+
138+isCgnatRange = \s ->
139+ if not (take 4 s == "100.") then False {}
140+ else if (take 1 (drop 4 s) == "7" || take 1 (drop 4 s) == "8" || take 1 (drop 4 s) == "9")
141+ && isDigitChar (take 1 (drop 5 s)) && take 1 (drop 6 s) == "."
142+ then True {}
143+ else if take 1 (drop 4 s) == "6"
144+ && (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")
145+ && take 1 (drop 6 s) == "."
146+ then True {}
147+ else if take 1 (drop 4 s) == "1" && (take 1 (drop 5 s) == "0" || take 1 (drop 5 s) == "1")
148+ && isDigitChar (take 1 (drop 6 s)) && take 1 (drop 7 s) == "."
149+ then True {}
150+ else if take 1 (drop 4 s) == "1" && take 1 (drop 5 s) == "2"
151+ && (take 1 (drop 6 s) == "0" || take 1 (drop 6 s) == "1" || take 1 (drop 6 s) == "2" || take 1 (drop 6 s) == "3"
152+ || take 1 (drop 6 s) == "4" || take 1 (drop 6 s) == "5" || take 1 (drop 6 s) == "6" || take 1 (drop 6 s) == "7")
153+ && take 1 (drop 7 s) == "."
154+ then True {}
155+ else False {}
156+
157+containsDoubleColon = \s ->
158+ if s == "" then False {}
159+ else if take 1 s == ":" && take 1 (drop 1 s) == ":" then True {}
160+ else containsDoubleColon (drop 1 s)
161+
162+isBadServerName = \s ->
163+ containsBadUrlChar s
164+ || s == "localhost" || take 10 s == "localhost:"
165+ || take 4 s == "127." || take 3 s == "10."
166+ || take 8 s == "192.168." || take 7 s == "169.254."
167+ || take 2 s == "0." || take 1 s == "["
168+ || take 7 s == "172.16." || take 7 s == "172.17." || take 7 s == "172.18." || take 7 s == "172.19."
169+ || take 7 s == "172.20." || take 7 s == "172.21." || take 7 s == "172.22." || take 7 s == "172.23."
170+ || take 7 s == "172.24." || take 7 s == "172.25." || take 7 s == "172.26." || take 7 s == "172.27."
171+ || take 7 s == "172.28." || take 7 s == "172.29." || take 7 s == "172.30." || take 7 s == "172.31."
172+ || take 7 s == "198.18." || take 7 s == "198.19."
173+ || take 2 s == "::"
174+ || isCgnatRange s
175+ || containsDoubleColon s
176+
118177lookupSession = \sessions token t ->
119178 single (do
120179 s <- sessions
@@ -136,11 +195,15 @@ extractToken = \authHeader ->
136195verifyFederationAuth = \authorization ->
137196 not (federationSecret == "") && safeCompare (extractToken authorization) federationSecret
138197
198+verifyMaintenanceAuth = \authorization ->
199+ verifyFederationAuth authorization
200+ || (not (maintenanceSecret == "") && safeCompare (extractToken authorization) maintenanceSecret)
201+
139202clampTtl = \ttl -> if ttl < 1 then 1 else if ttl > maxPresenceTtl then maxPresenceTtl else ttl
140203
141204safeCompareLoop = \s1 s2 result ->
142− if s2 == ""
143− then result + (if s1 == "" then 0 else 1)
205+ if s1 == ""
206+ then result + (if s2 == "" then 0 else 1)
144207 else safeCompareLoop (drop 1 s1) (drop 1 s2) (result + (if take 1 s1 == take 1 s2 then 0 else 1))
145208
146209safeCompare = \a b -> safeCompareLoop a b 0 == 0
@@ -155,8 +218,12 @@ sendToEach = \servers action ->
155218 then yield {}
156219 else do
157220 catch (action server) (\err -> do
158− println ("federation: error reaching " ++ server ++ ": " ++ show err)
159− yield {})
221+ println ("federation: error reaching " ++ server ++ ", retrying (1/2): " ++ show err)
222+ catch (action server) (\err2 -> do
223+ println ("federation: retry 1 failed for " ++ server ++ ", retrying (2/2): " ++ show err2)
224+ catch (action server) (\err3 -> do
225+ println ("federation: all retries exhausted for " ++ server ++ ": " ++ show err3)
226+ yield {})))
160227 sendToEach rest action
161228
162229pruneExpired = \t -> do
@@ -201,6 +268,23 @@ pruneExpired = \t -> do
201268 yield q) == 0
202269 yield p), peerServers: peerServersBefore}
203270
271+sendBatchedGossip = \peerServers allEvents ->
272+ if count allEvents == 0
273+ then yield {}
274+ else do
275+ let batch = fold (\acc e ->
276+ if count acc >= maxGossipEvents then acc
277+ else union acc [e]) [] allEvents
278+ let rest = (fold (\acc e ->
279+ if acc.taken < maxGossipEvents
280+ then {taken: acc.taken + 1, items: acc.items}
281+ else {taken: acc.taken, items: union acc.items [e]}) {taken: 0, items: []} allEvents).items
282+ sendToEach peerServers (\server ->
283+ fetch (federationScheme ++ server) (RecvGossip {
284+ authorization: "Bearer " ++ federationSecret,
285+ events: batch, hopsRemaining: 2, fromServer: serverHost}))
286+ sendBatchedGossip peerServers rest
287+
204288broadcastOfflineGossip = \t peerServers expiredLocal -> do
205289 let expiredPubkeys = fold (\acc p ->
206290 if count (do a <- acc; where a == p.pubkey; yield a) > 0 then acc
@@ -210,10 +294,7 @@ broadcastOfflineGossip = \t peerServers expiredLocal -> do
210294 let gossipEvents = do
211295 pk <- expiredPubkeys
212296 yield {eventType: "offline", pubkey: pk, server: serverHost, timestamp: t, ttl: 3600}
213− sendToEach peerServers (\server ->
214− fetch (federationScheme ++ server) (RecvGossip {
215− authorization: "Bearer " ++ federationSecret,
216− events: gossipEvents, hopsRemaining: 2, fromServer: serverHost}))
297+ sendBatchedGossip peerServers gossipEvents
217298 else yield {}
218299
219300-- ============================================
@@ -224,6 +305,7 @@ serve = \req -> case req of
224305
225306 ReqChallenge {pubkey, respond} -> do
226307 let valid = not (pubkey == "") && not (tooLong pubkey maxPubkeyLen) && isValidHex pubkey
308+ let pubkey = if isValidHex pubkey then normalizeHex pubkey else pubkey
227309 challenge <- genToken
228310 t <- now
229311 if valid
@@ -248,6 +330,7 @@ serve = \req -> case req of
248330 else respond {challenge: ""}
249331
250332 VerifyAuth {pubkey, challenge, signature, respond} -> do
333+ let pubkey = if isValidHex pubkey then normalizeHex pubkey else pubkey
251334 t <- now
252335 token <- genToken
253336 let expiry = t + 3600000
@@ -368,10 +451,12 @@ serve = \req -> case req of
368451 broadcastOfflineGossip t result.peerServers result.expiredLocal
369452
370453 SendMessage {to, encryptedBlob, timestamp, authorization, respond} -> do
454+ let validInput = not (to == "") && not (encryptedBlob == "") && not (tooLong to maxPubkeyLen) && not (tooLong encryptedBlob maxBlobLen) && isValidHex to && isValidHex encryptedBlob
455+ let to = if isValidHex to then normalizeHex to else to
456+ let encryptedBlob = if isValidHex encryptedBlob then normalizeHex encryptedBlob else encryptedBlob
371457 t <- now
372458 msgId <- genToken
373459 let clampedTs = if timestamp > t then t else if timestamp < t - messageMaxAge then t else timestamp
374− let validInput = not (to == "") && not (encryptedBlob == "") && not (tooLong to maxPubkeyLen) && not (tooLong encryptedBlob maxBlobLen) && isValidHex to && isValidHex encryptedBlob
375460 result <- atomic do
376461 pr <- pruneExpired t
377462 sessions <- *sessions
@@ -422,10 +507,11 @@ serve = \req -> case req of
422507 else if count remoteServers > 0 then "federated"
423508 else "queued"
424509 forwards <- *forwards
425− set *forwards = if shouldInsert
510+ let forwardsAtCapacity = count forwards >= maxForwardsEntries
511+ set *forwards = if shouldInsert && not forwardsAtCapacity
426512 then union forwards (do s <- remoteServers; yield {messageId: msgId, toServer: s, createdAt: t})
427513 else forwards
428− yield {status: status, remoteServers: remoteServers, fromKey: fromKey, expiredLocal: pr.expiredLocal, peerServers: pr.peerServers}
514+ yield {status: status, remoteServers: remoteServers, fromKey: fromKey, expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, forwardsAtCapacity: forwardsAtCapacity}
429515 let finalStatus = if result.status == "federated" && federationSecret == "" then "queued" else result.status
430516 respond {status: finalStatus, messageId: (if result.status == "unauthorized" then "" else if result.status == "invalid" then "" else msgId)}
431517 if result.status == "federated" && not (federationSecret == "")
@@ -435,6 +521,9 @@ serve = \req -> case req of
435521 timestamp: clampedTs, originServer: serverHost, messageId: msgId,
436522 authorization: "Bearer " ++ federationSecret}))
437523 else yield {}
524+ if result.forwardsAtCapacity && result.status == "federated"
525+ then println ("sendMessage: forwards at capacity (" ++ show maxForwardsEntries ++ "), skipping forward tracking")
526+ else yield {}
438527 broadcastOfflineGossip t result.peerServers result.expiredLocal
439528
440529 AckMessages {messageIds, authorization, respond} -> do
@@ -444,8 +533,8 @@ serve = \req -> case req of
444533 else if count acc >= maxMessageIds then acc
445534 else if tooLong mid maxPubkeyLen then acc
446535 else if not (isValidHex mid) then acc
447− else if count (do a <- acc; where a == mid; yield a) > 0 then acc
448− else union acc [mid]) [] messageIds
536+ else if count (do a <- acc; where a == normalizeHex mid; yield a) > 0 then acc
537+ else union acc [normalizeHex mid]) [] messageIds
449538 result <- atomic do
450539 pr <- pruneExpired t
451540 sessions <- *sessions
@@ -485,6 +574,8 @@ serve = \req -> case req of
485574 broadcastOfflineGossip t result.peerServers result.expiredLocal
486575
487576 LookupPresence {pubkey, authorization, respond} -> do
577+ let validPubkey = not (pubkey == "") && not (tooLong pubkey maxPubkeyLen) && isValidHex pubkey
578+ let pubkey = normalizeHex pubkey
488579 t <- now
489580 result <- atomic do
490581 pr <- pruneExpired t
@@ -502,7 +593,7 @@ serve = \req -> case req of
502593 let foundServer = case session of
503594 Nothing {} -> ""
504595 Just {value: _} ->
505− if pubkey == "" || tooLong pubkey maxPubkeyLen || not (isValidHex pubkey) || count valid == 0
596+ if not validPubkey || count valid == 0
506597 then ""
507598 else case single (do
508599 p <- valid
@@ -518,7 +609,7 @@ serve = \req -> case req of
518609 t <- now
519610 let authorized = verifyFederationAuth authorization
520611 let cappedHops = if hopsRemaining > 5 then 5 else hopsRemaining
521− let validRequest = authorized && cappedHops >= 1 && not (fromServer == "") && not (fromServer == serverHost) && not (tooLong fromServer maxPubkeyLen) && not (containsBadUrlChar fromServer) && count events <= maxGossipEvents
612+ let validRequest = authorized && cappedHops >= 1 && not (fromServer == "") && not (fromServer == serverHost) && not (tooLong fromServer maxPubkeyLen) && not (isBadServerName fromServer) && count events <= maxGossipEvents
522613 let onlineEntries = do
523614 e <- events
524615 where validRequest
@@ -527,12 +618,12 @@ serve = \req -> case req of
527618 where not (e.pubkey == "")
528619 where not (tooLong e.pubkey maxPubkeyLen)
529620 where not (tooLong e.server maxPubkeyLen)
530− where not (containsBadUrlChar e.server)
621+ where not (isBadServerName e.server)
531622 where isValidHex e.pubkey
532623 where not (e.server == serverHost)
533624 where e.timestamp <= t + clockSkewTolerance
534625 where e.timestamp + (clampTtl e.ttl) * 1000 > t
535− yield {pubkey: e.pubkey, server: e.server, expiresAt: e.timestamp + (clampTtl e.ttl) * 1000, timestamp: e.timestamp}
626+ yield {pubkey: normalizeHex e.pubkey, server: e.server, expiresAt: e.timestamp + (clampTtl e.ttl) * 1000, timestamp: e.timestamp}
536627 let offlineEntries = do
537628 e <- events
538629 where validRequest
@@ -541,12 +632,12 @@ serve = \req -> case req of
541632 where not (e.pubkey == "")
542633 where not (tooLong e.pubkey maxPubkeyLen)
543634 where not (tooLong e.server maxPubkeyLen)
544− where not (containsBadUrlChar e.server)
635+ where not (isBadServerName e.server)
545636 where isValidHex e.pubkey
546637 where not (e.server == serverHost)
547638 where e.timestamp <= t + clockSkewTolerance
548639 where t - e.timestamp < (clampTtl e.ttl) * 1000
549− yield {pubkey: e.pubkey, server: e.server, timestamp: e.timestamp}
640+ yield {pubkey: normalizeHex e.pubkey, server: e.server, timestamp: e.timestamp}
550641 let filteredOnline = do
551642 e <- onlineEntries
552643 where count (do
@@ -580,13 +671,14 @@ serve = \req -> case req of
580671 gossipResult <- atomic do
581672 pr <- pruneExpired t
582673 presence <- *presence
674+ let fromServerTrusted = count (do p <- presence; where p.server == fromServer; yield p) > 0
583675 let trustedDeduped = do
584676 e <- deduped
585− where e.server == fromServer || count (do p <- presence; where p.pubkey == e.pubkey; where p.server == e.server; yield p) > 0
677+ where e.server == fromServer || fromServerTrusted || count (do p <- presence; where p.server == e.server; yield p) > 0
586678 yield e
587679 let trustedOffline = do
588680 e <- offlineEntries
589− where e.server == fromServer || count (do p <- presence; where p.pubkey == e.pubkey; where p.server == e.server; yield p) > 0
681+ where e.server == fromServer || fromServerTrusted || count (do p <- presence; where p.server == e.server; yield p) > 0
590682 yield e
591683 let appliedOnline = do
592684 e <- trustedDeduped
@@ -672,19 +764,36 @@ serve = \req -> case req of
672764 where o.timestamp >= p.timestamp
673765 yield o) > 0
674766 yield p)
675− let appliedOnlineUpdates = if withinCapacity then appliedOnline
767+ let reclaimCapacity = if withinCapacity then 0 else maxPresenceEntries - count updatesOnlyAfterOffline
768+ let newEntries = if withinCapacity then []
676769 else do
677770 e <- trustedDeduped
678− where count (do p <- presence; where p.pubkey == e.pubkey; where p.server == e.server; yield p) > 0
679− where count (do
680− p <- presence
681− where p.pubkey == e.pubkey
682− where p.server == e.server
683− where p.expiresAt >= e.expiresAt
684− yield p) == 0
685− yield {pubkey: e.pubkey, server: e.server}
771+ where count (do p <- presence; where p.pubkey == e.pubkey; where p.server == e.server; yield p) == 0
772+ where count (do p <- updatesOnlyAfterOffline; where p.pubkey == e.pubkey; where p.server == e.server; yield p) == 0
773+ yield e
774+ let reclaimedEntries = if withinCapacity then []
775+ else fold (\acc e ->
776+ if count acc >= reclaimCapacity then acc
777+ else union acc [e]) [] newEntries
778+ let afterReclaim = if withinCapacity then afterOffline
779+ else union updatesOnlyAfterOffline reclaimedEntries
780+ let appliedOnlineUpdates = if withinCapacity then appliedOnline
781+ else union
782+ (do
783+ e <- trustedDeduped
784+ where count (do p <- presence; where p.pubkey == e.pubkey; where p.server == e.server; yield p) > 0
785+ where count (do
786+ p <- presence
787+ where p.pubkey == e.pubkey
788+ where p.server == e.server
789+ where p.expiresAt >= e.expiresAt
790+ yield p) == 0
791+ yield {pubkey: e.pubkey, server: e.server})
792+ (do
793+ e <- reclaimedEntries
794+ yield {pubkey: e.pubkey, server: e.server})
686795 let finalPresence = if validRequest
687− then if withinCapacity then afterOffline else updatesOnlyAfterOffline
796+ then if withinCapacity then afterOffline else afterReclaim
688797 else presence
689798 set *presence = finalPresence
690799 let gossipPeerServers = fold (\acc p ->
@@ -700,25 +809,25 @@ serve = \req -> case req of
700809 where not (e.pubkey == "")
701810 where not (tooLong e.pubkey maxPubkeyLen)
702811 where not (tooLong e.server maxPubkeyLen)
703− where not (containsBadUrlChar e.server)
812+ where not (isBadServerName e.server)
704813 where isValidHex e.pubkey
705814 where not (e.server == serverHost)
706815 where e.timestamp <= t + clockSkewTolerance
707816 where e.timestamp + (clampTtl e.ttl) * 1000 > t
708817 where not (e.eventType == "online" && count (do
709818 o <- offlineEntries
710− where o.pubkey == e.pubkey
819+ where o.pubkey == normalizeHex e.pubkey
711820 where o.server == e.server
712821 where o.timestamp >= e.timestamp
713822 yield o) > 0)
714823 yield e
715824 let effectiveEvents = do
716825 e <- validEvents
717− where count (do a <- gossipResult.appliedKeys; where a.pubkey == e.pubkey; where a.server == e.server; yield a) > 0
826+ where count (do a <- gossipResult.appliedKeys; where a.pubkey == normalizeHex e.pubkey; where a.server == e.server; yield a) > 0
718827 yield e
719828 respond {ok: validRequest}
720829 if not gossipResult.withinCapacity
721− then println ("gossip: presence at capacity (" ++ show maxPresenceEntries ++ "), dropping new entries from " ++ fromServer)
830+ then println ("gossip: presence at capacity (" ++ show maxPresenceEntries ++ "), limiting new entries from " ++ fromServer)
722831 else yield {}
723832 if validRequest && cappedHops > 1 && not (federationSecret == "") && count effectiveEvents > 0
724833 then do
@@ -737,10 +846,14 @@ serve = \req -> case req of
737846 broadcastOfflineGossip t gossipResult.peerServers gossipResult.expiredLocal
738847
739848 ForwardMessage {toKey, fromKey, encryptedBlob, timestamp, originServer, messageId, authorization, respond} -> do
849+ let validFields = not (toKey == "") && not (fromKey == "") && not (encryptedBlob == "") && not (messageId == "") && not (tooLong toKey maxPubkeyLen) && not (tooLong fromKey maxPubkeyLen) && not (tooLong encryptedBlob maxBlobLen) && not (tooLong messageId maxPubkeyLen) && isValidHex toKey && isValidHex fromKey && isValidHex encryptedBlob && isValidHex messageId
850+ let toKey = if isValidHex toKey then normalizeHex toKey else toKey
851+ let fromKey = if isValidHex fromKey then normalizeHex fromKey else fromKey
852+ let encryptedBlob = if isValidHex encryptedBlob then normalizeHex encryptedBlob else encryptedBlob
853+ let messageId = if isValidHex messageId then normalizeHex messageId else messageId
740854 t <- now
741855 let authorized = verifyFederationAuth authorization
742− let validOrigin = not (originServer == "") && not (originServer == serverHost) && not (tooLong originServer maxPubkeyLen) && not (containsBadUrlChar originServer)
743− let validFields = not (toKey == "") && not (fromKey == "") && not (encryptedBlob == "") && not (messageId == "") && not (tooLong toKey maxPubkeyLen) && not (tooLong fromKey maxPubkeyLen) && not (tooLong encryptedBlob maxBlobLen) && not (tooLong messageId maxPubkeyLen) && isValidHex toKey && isValidHex fromKey && isValidHex encryptedBlob && isValidHex messageId
856+ let validOrigin = not (originServer == "") && not (originServer == serverHost) && not (tooLong originServer maxPubkeyLen) && not (isBadServerName originServer)
744857 let clampedTs = if timestamp > t then t else if timestamp < t - messageMaxAge then t else timestamp
745858 result <- atomic do
746859 pr <- pruneExpired t
@@ -765,8 +878,8 @@ serve = \req -> case req of
765878 [{messageId: messageId, toKey: toKey, fromKey: fromKey,
766879 encryptedBlob: encryptedBlob, timestamp: clampedTs, originServer: originServer, receivedAt: seqTs}]
767880 else messages
768− yield {peerServers: pr.peerServers, expiredLocal: pr.expiredLocal}
769− respond {ok: authorized && validOrigin && validFields}
881+ yield {peerServers: pr.peerServers, expiredLocal: pr.expiredLocal, inserted: shouldInsert, alreadyExists: exists}
882+ respond {ok: result.inserted || (authorized && validOrigin && validFields && result.alreadyExists)}
770883 broadcastOfflineGossip t result.peerServers result.expiredLocal
771884
772885 FederationAck {messageIds, originServer, fromServer, authorization, respond} -> do
@@ -777,9 +890,9 @@ serve = \req -> case req of
777890 else if count acc >= maxMessageIds then acc
778891 else if tooLong mid maxPubkeyLen then acc
779892 else if not (isValidHex mid) then acc
780− else if count (do a <- acc; where a == mid; yield a) > 0 then acc
781− else union acc [mid]) [] messageIds
782− let validAckFields = authorized && originServer == serverHost && not (fromServer == "") && not (fromServer == serverHost) && not (tooLong fromServer maxPubkeyLen) && not (containsBadUrlChar fromServer)
893+ else if count (do a <- acc; where a == normalizeHex mid; yield a) > 0 then acc
894+ else union acc [normalizeHex mid]) [] messageIds
895+ let validAckFields = authorized && originServer == serverHost && not (fromServer == "") && not (fromServer == serverHost) && not (tooLong fromServer maxPubkeyLen) && not (isBadServerName fromServer)
783896 pruneResult <- atomic do
784897 pr <- pruneExpired t
785898 forwards <- *forwards
@@ -798,7 +911,7 @@ serve = \req -> case req of
798911
799912 Prune {authorization, respond} -> do
800913 t <- now
801− let authorized = verifyFederationAuth authorization
914+ let authorized = verifyMaintenanceAuth authorization
802915 if authorized
803916 then do
804917 result <- atomic do