2-- Single-server implementation of the Skrepka protocol (v0.1)
3-- Binary data (keys, signatures, blobs) uses hex encoding (bytesToHex/bytesFromHex)
5-- ============================================
7-- ============================================
9serverHost = "localhost:8080"
10federationSecret = "" -- must be set to enable federation
11messageMaxAge = 86400000 -- 24 hours in milliseconds
12maxPresenceTtl = 3600 -- max presence TTL from gossip in seconds
14-- ============================================
16-- ============================================
18type PollEvent = {id: Text, eventType: Text, payload: Text}
20type GossipEvent = {eventType: Text, pubkey: Text, server: Text, timestamp: Int, ttl: Int}
22-- ============================================
24-- ============================================
26*challenges : [{pubkey: Text, challenge: Text, expiresAt: Int}]
28*sessions : [{token: Text, pubkey: Text, expiresAt: Int}]
30*messages : [{messageId: Text, toKey: Text, fromKey: Text, encryptedBlob: Text, timestamp: Int}]
32*presence : [{pubkey: Text, server: Text, expiresAt: Int}]
34-- ============================================
36-- ============================================
40 POST {pubkey: Text} /challenge
43 POST {pubkey: Text, challenge: Text, signature: Text} /verify
44 -> {token: Text, expiresAt: Int}
48 GET /poll headers {authorization: Text}
49 -> {events: [PollEvent], cursor: Text}
52 POST {to: Text, encryptedBlob: Text, timestamp: Int} / headers {authorization: Text}
53 -> {status: Text, messageId: Text}
55 POST {messageIds: [Text]} /ack headers {authorization: Text}
58 GET /lookup/{pubkey: Text} headers {authorization: Text}
59 -> {online: Bool, server: Text}
62route FederationApi where
64 POST {events: [GossipEvent], hopsRemaining: Int} /gossip headers {authorization: Text}
67 POST {toKey: Text, fromKey: Text, encryptedBlob: Text, timestamp: Int, originServer: Text, messageId: Text} /forward headers {authorization: Text}
70 POST {messageIds: [Text], originServer: Text} /ack headers {authorization: Text}
74route Api = AuthApi | ClientApi | FederationApi
76-- ============================================
78-- ============================================
81 a <- randomInt 2147483647
82 b <- randomInt 2147483647
83 c <- randomInt 2147483647
84 d <- randomInt 2147483647
85 yield (bytesToHex (textToBytes (show a ++ "." ++ show b ++ "." ++ show c ++ "." ++ show d)))
87lookupSession = \token t ->
90 where s.token == token
94extractToken = \authHeader ->
95 if take 7 authHeader == "Bearer "
96 then drop 7 authHeader
99verifyFederationAuth = \authorization ->
100 extractToken authorization == federationSecret && not (federationSecret == "")
102clampTtl = \ttl -> if ttl > maxPresenceTtl then maxPresenceTtl else ttl
104pruneExpired = \t -> do
107 where c.expiresAt > t
111 where s.expiresAt > t
115 where p.expiresAt > t
119 where m.timestamp > t - messageMaxAge
122-- ============================================
124-- ============================================
126serve = \req -> case req of
128 ReqChallenge {pubkey, respond} -> do
129 challenge <- genToken
132 set *challenges = union *challenges
133 [{pubkey: pubkey, challenge: challenge, expiresAt: t + 60000}]
134 respond {challenge: challenge}
136 VerifyAuth {pubkey, challenge, signature, respond} -> do
140 where c.pubkey == pubkey
141 where c.challenge == challenge
142 where c.expiresAt > t
145 respond {token: "", expiresAt: 0}
149 where not (c.pubkey == pubkey && c.challenge == challenge)
151 if verify (bytesFromHex pubkey) (textToBytes challenge) (bytesFromHex signature)
154 let expiry = t + 3600000
155 set *sessions = union *sessions
156 [{token: token, pubkey: pubkey, expiresAt: expiry}]
157 set *presence = union
160 where not (p.pubkey == pubkey && p.server == serverHost)
162 [{pubkey: pubkey, server: serverHost, expiresAt: expiry}]
163 respond {token: token, expiresAt: expiry}
165 respond {token: "", expiresAt: 0}
167 Poll {authorization, respond} -> do
170 case lookupSession (extractToken authorization) t of
172 respond {events: [], cursor: ""}
173 Just {value: session} -> do
176 where m.toKey == session.pubkey
177 yield {id: m.messageId, eventType: "message",
178 payload: toJson {toKey: m.toKey, fromKey: m.fromKey,
179 encryptedBlob: m.encryptedBlob,
180 timestamp: m.timestamp}}
181 respond {events: evts, cursor: ""}
183 SendMessage {to, encryptedBlob, timestamp, authorization, respond} -> do
185 case lookupSession (extractToken authorization) t of
187 respond {status: "unauthorized", messageId: ""}
188 Just {value: session} -> do
190 set *messages = union *messages
191 [{messageId: msgId, toKey: to, fromKey: session.pubkey,
192 encryptedBlob: encryptedBlob, timestamp: t}]
193 let online = count (do
196 where s.expiresAt > t
198 respond {status: (if online then "delivered" else "queued"),
201 AckMessages {messageIds, authorization, respond} -> do
203 case lookupSession (extractToken authorization) t of
205 respond {ok: (False {})}
206 Just {value: session} -> do
207 set *messages = diff *messages (do
210 where m.messageId == mid
211 where m.toKey == session.pubkey
213 respond {ok: (True {})}
215 LookupPresence {pubkey, authorization, respond} -> do
217 case lookupSession (extractToken authorization) t of
219 respond {online: (False {}), server: ""}
223 where p.pubkey == pubkey
224 where p.expiresAt > t
226 in if count valid == 0
227 then respond {online: (False {}), server: ""}
230 where p.server == serverHost
233 respond {online: (True {}), server: p.server}
237 respond {online: (True {}), server: p.server}
239 respond {online: (True {}), server: ""}
241 RecvGossip {events, hopsRemaining, authorization, respond} ->
242 if not (verifyFederationAuth authorization)
243 then respond {ok: (False {})}
244 else if hopsRemaining < 1
245 then respond {ok: (False {})}
248 let onlineEntries = do
250 where e.eventType == "online"
251 yield {pubkey: e.pubkey, server: e.server, expiresAt: t + clampTtl e.ttl * 1000}
252 let affectedKeys = do
254 yield {pubkey: e.pubkey, server: e.server}
255 let cleaned = diff *presence (do
258 where p.pubkey == k.pubkey
259 where p.server == k.server
265 where o.pubkey == e.pubkey
266 where o.server == e.server
267 where o.expiresAt > e.expiresAt
270 set *presence = union cleaned deduped
271 respond {ok: (True {})}
273 ForwardMessage {toKey, fromKey, encryptedBlob, timestamp, originServer, messageId, authorization, respond} ->
274 if not (verifyFederationAuth authorization)
275 then respond {ok: (False {})}
278 let clampedTs = if timestamp > t then t else timestamp
279 let exists = count (do
281 where m.messageId == messageId
282 where m.timestamp > t - messageMaxAge
285 then respond {ok: (True {})}
287 set *messages = union *messages
288 [{messageId: messageId, toKey: toKey, fromKey: fromKey,
289 encryptedBlob: encryptedBlob, timestamp: clampedTs}]
290 respond {ok: (True {})}
292 FederationAck {messageIds, originServer, authorization, respond} ->
293 if not (verifyFederationAuth authorization)
294 then respond {ok: (False {})}
296 set *messages = diff *messages (do
299 where m.messageId == mid
301 respond {ok: (True {})}
303-- ============================================
305-- ============================================
308 println "Skrepka relay server starting on port 8080..."
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list