AtlatestRepositorysigil-xmpp
sigil-xmpp / tree / src / sigil / xmppconnection.sgl
1
;;; (sigil xmpp connection) - XMPP Connection Lifecycle2
;;;3
;;; Manages XMPP connections with STARTTLS, SASL authentication,4
;;; resource binding, stanza dispatch, and cooperative I/O.6
(define-library (sigil xmpp connection)7
(import (sigil core)8
(sigil string)9
(sigil struct)10
(sigil socket)11
(sigil tls)12
(sigil crypto)13
(sigil channels)14
(sigil async)15
(sigil sxml)16
(sigil sxml reader)17
(sigil xmpp stanza)18
(sigil xmpp sasl)19
(scheme base))21
(export22
;; Connection record23
xmpp-connection24
make-xmpp-connection25
xmpp-connection?26
xmpp-connection-server27
xmpp-connection-port28
xmpp-connection-jid29
xmpp-connection-password30
xmpp-connection-state31
xmpp-connection-bound-jid33
;; Connection lifecycle34
xmpp-connect35
xmpp-disconnect36
xmpp-connected?38
;; Event handling (callback interface)39
xmpp-on-stanza40
xmpp-on41
xmpp-send-iq43
;; Channel interface44
xmpp-channel46
;; I/O47
xmpp-send48
xmpp-send-raw49
xmpp-process-input51
;; Event loop52
xmpp-run53
xmpp-tick55
;; Presence56
xmpp-send-presence58
;; Internal (for feature modules)59
xmpp-connection-event-handlers60
set-xmpp-connection-event-handlers!61
xmpp-connection-iq-callbacks62
set-xmpp-connection-iq-callbacks!)64
(begin66
;; ============================================================67
;; Connection Record68
;; ============================================================70
(define-struct xmpp-connection71
;; Configuration (immutable)72
(server)73
(port default: 5222)74
(jid)75
(password)76
(resource default: "sigil")78
;; State (mutable)79
(state default: 'disconnected mutable: #t)80
(socket default: #f mutable: #t)81
(tls-conn default: #f mutable: #t)82
(reader default: #f mutable: #t)83
(bound-jid default: #f mutable: #t)84
(sasl-state default: #f mutable: #t)86
;; Handlers (mutable)87
(stanza-handlers default: '() mutable: #t)88
(event-handlers default: #{} mutable: #t)89
(iq-callbacks default: #{} mutable: #t)91
;; Channels (mutable)92
(broadcasts default: #{} mutable: #t))95
;; ============================================================96
;; Connection Constructor97
;; ============================================================99
;;; Create a new XMPP connection (does not connect yet).100
;;;101
;;; ```scheme102
;;; (make-xmpp-connection103
;;; server: "example.com"104
;;; jid: "[email protected]"105
;;; password: "secret"106
;;; resource: "bot")107
;;; ```108
(define (make-xmpp-connection (keys: (server #f)109
(port 5222)110
(jid #f)111
(password #f)112
(resource "sigil")))113
(: (server: any?) (port: integer?) (jid: any?) (password: any?) (resource: string?) -> xmpp-connection?)114
(unless server115
(error "make-xmpp-connection: server: is required"))116
(unless jid117
(error "make-xmpp-connection: jid: is required"))118
(unless password119
(error "make-xmpp-connection: password: is required"))120
(xmpp-connection121
server: server122
port: port123
jid: jid124
password: password125
resource: resource))127
;;; Check if connection is in connected state.128
(define (xmpp-connected? conn)129
(: xmpp-connection? -> boolean?)130
(eq? (xmpp-connection-state conn) 'connected))133
;; ============================================================134
;; I/O Abstraction135
;; ============================================================137
;; Write to the connection (dispatches to socket or TLS)138
(define (conn-write conn data)139
(let ((tls (xmpp-connection-tls-conn conn))140
(sock (xmpp-connection-socket conn)))141
(if tls142
(tls-write tls data)143
(when sock (socket-write sock data)))))145
;; Read from the connection146
(define (conn-read conn)147
(let ((tls (xmpp-connection-tls-conn conn))148
(sock (xmpp-connection-socket conn)))149
(if tls150
(tls-read tls)151
(when sock (socket-read sock)))))153
;; Get the socket object for select/await (even after TLS upgrade)154
(define (conn-socket conn)155
(or (xmpp-connection-tls-conn conn)156
(xmpp-connection-socket conn)))159
;; ============================================================160
;; Stream Management161
;; ============================================================163
;; Open an XML stream to the server164
(define (open-stream conn)165
(let ((jid-obj (parse-jid (xmpp-connection-jid conn))))166
(conn-write conn167
(string-append168
"<?xml version='1.0'?>"169
"<stream:stream"170
" xmlns='jabber:client'"171
" xmlns:stream='http://etherx.jabber.org/streams'"172
" to='" (jid-domain jid-obj) "'"173
" version='1.0'>"))))175
;; Reset the stanza reader for a new stream176
(define (reset-stream conn)177
(let ((reader (xmpp-connection-reader conn)))178
(when reader179
(stanza-reader-reset! reader)))180
(set-xmpp-connection-reader! conn (make-stanza-reader)))183
;; ============================================================184
;; Event Handling185
;; ============================================================187
;;; Register a handler for all incoming stanzas.188
(define (xmpp-on-stanza conn handler)189
(: xmpp-connection? procedure? -> void?)190
(set-xmpp-connection-stanza-handlers!191
conn192
(cons handler (xmpp-connection-stanza-handlers conn))))194
;;; Register a handler for a specific event type.195
;;;196
;;; Event types: 'message, 'presence, 'iq, 'connected,197
;;; 'disconnected, 'error198
(define (xmpp-on conn event handler)199
(: xmpp-connection? symbol? procedure? -> void?)200
(let* ((handlers (xmpp-connection-event-handlers conn))201
(existing (dict-ref handlers event '())))202
(set-xmpp-connection-event-handlers!203
conn204
(dict-set handlers event (cons handler existing)))))206
;; Fire an event to registered handlers207
(define (fire-event conn event . args)208
(let ((handlers (dict-ref (xmpp-connection-event-handlers conn) event '())))209
(for-each (lambda (handler) (apply handler args))210
handlers))211
;; Also send to broadcast channel if one exists212
(let ((bc (dict-ref (xmpp-connection-broadcasts conn) event #f)))213
(when (and bc (pair? args))214
(broadcast-send bc (car args)))))216
;;; Get a channel for receiving events of a given type.217
;;;218
;;; Creates a broadcast subscription. Use with `channel-receive`219
;;; or `for-channel`.220
;;;221
;;; ```scheme222
;;; (let ((msgs (xmpp-channel conn 'message)))223
;;; (for-channel msgs224
;;; (lambda (stanza)225
;;; (display (message-body stanza)))))226
;;; ```227
(define (xmpp-channel conn event)228
(: xmpp-connection? symbol? -> any?)229
(let* ((broadcasts (xmpp-connection-broadcasts conn))230
(bc (dict-ref broadcasts event #f)))231
;; Create broadcast if it doesn't exist yet232
(let ((bc (or bc (let ((new-bc (make-broadcast)))233
(set-xmpp-connection-broadcasts!234
conn235
(dict-set broadcasts event new-bc))236
new-bc))))237
(broadcast-subscribe bc))))239
;; Dispatch a stanza to handlers240
(define (dispatch-stanza conn stanza)241
;; Call general stanza handlers242
(for-each (lambda (handler) (handler stanza))243
(xmpp-connection-stanza-handlers conn))245
;; Fire event based on stanza type246
(let ((type (stanza-type stanza)))247
(when type248
(fire-event conn type stanza)249
(fire-event conn 'stanza stanza)))251
;; Handle IQ callbacks252
(when (eq? (stanza-type stanza) 'iq)253
(let* ((id (stanza-id stanza))254
(callbacks (xmpp-connection-iq-callbacks conn))255
(cb (and id (dict-ref callbacks id #f))))256
(when cb257
(set-xmpp-connection-iq-callbacks!258
conn259
(dict-remove callbacks id))260
(cb stanza)))))263
;; ============================================================264
;; Sending265
;; ============================================================267
;;; Send a stanza (SXML) to the server.268
;;;269
;;; ```scheme270
;;; (xmpp-send conn (xmpp-message to: "[email protected]" body: "Hi"))271
;;; ```272
(define (xmpp-send conn stanza)273
(: xmpp-connection? list? -> void?)274
(conn-write conn (stanza->xml stanza)))276
;;; Send raw XML string to the server.277
(define (xmpp-send-raw conn data)278
(: xmpp-connection? string? -> void?)279
(conn-write conn data))281
;;; Send an IQ stanza and register a callback for the response.282
;;;283
;;; The callback receives the response stanza.284
;;;285
;;; ```scheme286
;;; (xmpp-send-iq conn287
;;; (xmpp-iq type: "get" children: '((query (@ (xmlns "jabber:iq:roster")))))288
;;; (lambda (response) (display "Got roster\n")))289
;;; ```290
(define (xmpp-send-iq conn stanza callback)291
(: xmpp-connection? list? procedure? -> void?)292
(let ((id (stanza-id stanza)))293
(when id294
(set-xmpp-connection-iq-callbacks!295
conn296
(dict-set (xmpp-connection-iq-callbacks conn)297
id callback)))298
(xmpp-send conn stanza)))300
;;; Send initial presence to indicate availability.301
(define (xmpp-send-presence conn . args)302
(: xmpp-connection? any? ... -> void?)303
(xmpp-send conn (apply xmpp-presence args)))306
;; ============================================================307
;; Connection Flow308
;; ============================================================310
;;; Connect to the XMPP server.311
;;;312
;;; Performs the full XMPP connection sequence:313
;;; TCP connect -> stream open -> STARTTLS -> SASL auth -> bind314
;;;315
;;; Returns #t on success, #f on failure.316
(define (xmpp-connect conn)317
(: xmpp-connection? -> boolean?)318
(when (not (eq? (xmpp-connection-state conn) 'disconnected))319
(error "xmpp-connect: already connected or connecting"))321
(set-xmpp-connection-state! conn 'connecting)323
;; TCP connect324
(let ((sock (tcp-connect (xmpp-connection-server conn)325
(xmpp-connection-port conn))))326
(if (not sock)327
(begin328
(set-xmpp-connection-state! conn 'disconnected)329
#f)330
(begin331
(set-xmpp-connection-socket! conn sock)332
(set-xmpp-connection-reader! conn (make-stanza-reader))333
(set-xmpp-connection-state! conn 'stream-open)335
;; Open initial stream336
(open-stream conn)338
;; Process the connection handshake synchronously339
(run-handshake conn)))))341
;; Run the XMPP handshake (blocking until connected or failure)342
(define (run-handshake conn)343
(let loop ()344
(let ((state (xmpp-connection-state conn)))345
(cond346
((eq? state 'connected) #t)347
((eq? state 'disconnected) #f)348
(else349
;; Read and process data350
(let ((data (conn-read conn)))351
(cond352
((or (not data) (eof-object? data))353
(set-xmpp-connection-state! conn 'disconnected)354
#f)355
((string=? data "") (loop))356
(else357
(process-handshake-data conn data)358
(loop)))))))))360
;; Process incoming data during handshake361
(define (process-handshake-data conn data)362
(let ((reader (xmpp-connection-reader conn)))363
(stanza-reader-feed! reader data)364
(let ((stanzas (stanza-reader-stanzas! reader)))365
(for-each (lambda (stanza) (handle-handshake-stanza conn stanza))366
stanzas))))368
;; Handle stanzas during the handshake phase369
(define (handle-handshake-stanza conn stanza)370
(let ((state (xmpp-connection-state conn))371
(type (stanza-type stanza)))372
(cond373
;; Waiting for features after stream open374
((and (eq? state 'stream-open)375
(eq? type 'stream:features))376
(handle-features conn stanza))378
;; STARTTLS negotiation379
((and (eq? state 'starttls-negotiating)380
(eq? type 'proceed))381
(handle-starttls-proceed conn))383
;; SASL challenge384
((and (eq? state 'authenticating)385
(eq? type 'challenge))386
(handle-sasl-challenge conn stanza))388
;; SASL success389
((and (eq? state 'authenticating)390
(eq? type 'success))391
(handle-sasl-success conn stanza))393
;; SASL failure394
((and (eq? state 'authenticating)395
(eq? type 'failure))396
(handle-sasl-failure conn stanza))398
;; Bind response399
((and (eq? state 'binding)400
(eq? type 'iq))401
(handle-bind-response conn stanza))403
;; Features after auth (look for bind)404
((and (eq? state 'authenticated)405
(eq? type 'stream:features))406
(handle-post-auth-features conn stanza)))))409
;; ============================================================410
;; Feature Negotiation411
;; ============================================================413
;; Handle stream features414
(define (handle-features conn features)415
(cond416
;; STARTTLS available417
((stanza-child features 'starttls)418
(set-xmpp-connection-state! conn 'starttls-negotiating)419
(conn-write conn "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"))421
;; SASL mechanisms available422
((stanza-child features 'mechanisms)423
(begin-sasl conn features))425
;; Bind available426
((stanza-child features 'bind)427
(begin-bind conn))))430
;; ============================================================431
;; STARTTLS432
;; ============================================================434
(define (handle-starttls-proceed conn)435
;; Upgrade socket to TLS436
(let* ((sock (xmpp-connection-socket conn))437
(server (xmpp-connection-server conn))438
(tls (tls-upgrade sock server)))439
(if (not tls)440
(begin441
(set-xmpp-connection-state! conn 'disconnected)442
(fire-event conn 'error "STARTTLS handshake failed"))443
(begin444
(set-xmpp-connection-tls-conn! conn tls)445
;; Restart stream446
(reset-stream conn)447
(set-xmpp-connection-state! conn 'stream-open)448
(open-stream conn)))))451
;; ============================================================452
;; SASL Authentication453
;; ============================================================455
(define (begin-sasl conn features)456
(let* ((mechanisms-el (stanza-child features 'mechanisms))457
(mechanism-els (if mechanisms-el458
(stanza-children mechanisms-el 'mechanism)459
'()))460
(mechanism-names (map (lambda (el) (sxml-text el))461
mechanism-els))462
(selected (select-sasl-mechanism mechanism-names)))463
(cond464
((eq? selected 'scram-sha-1)465
(let* ((jid-obj (parse-jid (xmpp-connection-jid conn)))466
(username (jid-local jid-obj))467
(scram (make-scram-sha1 username (xmpp-connection-password conn)))468
(initial (scram-initial-message scram)))469
(set-xmpp-connection-sasl-state! conn scram)470
(set-xmpp-connection-state! conn 'authenticating)471
(conn-write conn472
(string-append473
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'"474
" mechanism='SCRAM-SHA-1'>"475
initial "</auth>"))))477
((eq? selected 'plain)478
(let* ((jid-obj (parse-jid (xmpp-connection-jid conn)))479
(username (jid-local jid-obj))480
(response (sasl-plain-response username481
(xmpp-connection-password conn))))482
(set-xmpp-connection-state! conn 'authenticating)483
(conn-write conn484
(string-append485
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'"486
" mechanism='PLAIN'>"487
response "</auth>"))))489
(else490
(set-xmpp-connection-state! conn 'disconnected)491
(fire-event conn 'error "No supported SASL mechanism")))))493
(define (handle-sasl-challenge conn stanza)494
(let ((scram (xmpp-connection-sasl-state conn))495
(challenge-text (sxml-text stanza)))496
(if (and scram challenge-text)497
(let ((response (scram-challenge-response scram challenge-text)))498
(conn-write conn499
(string-append "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>"500
response "</response>")))501
(begin502
(set-xmpp-connection-state! conn 'disconnected)503
(fire-event conn 'error "SASL challenge failed")))))505
(define (handle-sasl-success conn stanza)506
(let ((scram (xmpp-connection-sasl-state conn))507
(server-data (sxml-text stanza)))508
;; Verify server signature if SCRAM509
(when (and scram server-data)510
(unless (scram-verify-server scram server-data)511
(fire-event conn 'error "SCRAM server verification failed")))513
(set-xmpp-connection-sasl-state! conn #f)514
(set-xmpp-connection-state! conn 'authenticated)516
;; Restart stream after auth517
(reset-stream conn)518
(open-stream conn)))520
(define (handle-sasl-failure conn stanza)521
(set-xmpp-connection-state! conn 'disconnected)522
(fire-event conn 'error523
(string-append "SASL authentication failed: "524
(or (sxml-text stanza) "unknown error"))))527
;; ============================================================528
;; Resource Binding529
;; ============================================================531
(define (handle-post-auth-features conn features)532
(when (stanza-child features 'bind)533
(begin-bind conn)))535
(define (begin-bind conn)536
(set-xmpp-connection-state! conn 'binding)537
(let ((resource (xmpp-connection-resource conn)))538
(xmpp-send-raw conn539
(stanza->xml540
(xmpp-iq type: "set" id: "bind-1"541
children: (list542
(if resource543
`(bind (@ (xmlns "urn:ietf:params:xml:ns:xmpp-bind"))544
(resource ,resource))545
'(bind (@ (xmlns "urn:ietf:params:xml:ns:xmpp-bind"))))))))))547
(define (handle-bind-response conn stanza)548
(let ((type (stanza-attr stanza 'type)))549
(cond550
((equal? type "result")551
(let* ((bind-el (stanza-child stanza 'bind))552
(jid-el (and bind-el (stanza-child bind-el 'jid)))553
(bound-jid (and jid-el (sxml-text jid-el))))554
(set-xmpp-connection-bound-jid! conn bound-jid)555
(set-xmpp-connection-state! conn 'connected)556
(fire-event conn 'connected conn)))558
(else559
(set-xmpp-connection-state! conn 'disconnected)560
(fire-event conn 'error "Resource binding failed")))))563
;; ============================================================564
;; Disconnection565
;; ============================================================567
;;; Disconnect from the XMPP server.568
(define (xmpp-disconnect conn)569
(: xmpp-connection? -> void?)570
(when (conn-socket conn)571
;; Send stream close572
(when (memq (xmpp-connection-state conn) '(connected authenticated binding))573
(conn-write conn "</stream:stream>"))575
;; Close connections576
(let ((tls (xmpp-connection-tls-conn conn)))577
(when tls (tls-close tls)))578
(let ((sock (xmpp-connection-socket conn)))579
(when (and sock (not (xmpp-connection-tls-conn conn)))580
(socket-close sock))))582
(set-xmpp-connection-tls-conn! conn #f)583
(set-xmpp-connection-socket! conn #f)584
(set-xmpp-connection-state! conn 'disconnected)585
(fire-event conn 'disconnected conn))588
;; ============================================================589
;; Input Processing590
;; ============================================================592
;;; Process available input from the XMPP connection.593
;;;594
;;; Returns #t if connection is alive, #f if disconnected.595
(define (xmpp-process-input conn)596
(: xmpp-connection? -> boolean?)597
(let ((data (conn-read conn)))598
(cond599
((or (not data) (eof-object? data))600
(set-xmpp-connection-state! conn 'disconnected)601
(fire-event conn 'disconnected conn)602
#f)604
((string=? data "") #t)606
(else607
(let ((reader (xmpp-connection-reader conn)))608
(stanza-reader-feed! reader data)609
(let ((stanzas (stanza-reader-stanzas! reader)))610
(for-each (lambda (stanza) (dispatch-stanza conn stanza))611
stanzas)))612
#t))))615
;; ============================================================616
;; Event Loop617
;; ============================================================619
;;; Process one tick of the XMPP connection (non-blocking).620
;;;621
;;; Returns #t if connection is alive, #f if disconnected.622
(define (xmpp-tick conn)623
(: xmpp-connection? -> boolean?)624
(let ((sock (conn-socket conn)))625
(if (not sock)626
#f627
(let ((ready (socket-select (list sock) '() 0)))628
(if (and ready (pair? (car ready)))629
(xmpp-process-input conn)630
#t)))))632
;;; Run the XMPP connection event loop.633
;;;634
;;; Processes stanzas until disconnected. When running inside a635
;;; `with-async` context, cooperates with other tasks via `await-readable`.636
;;; Otherwise blocks in a traditional event loop.637
(define (xmpp-run conn)638
(: xmpp-connection? -> void?)639
(let ((sock (conn-socket conn)))640
(when sock641
(if (current-scheduler)642
;; Cooperative mode643
(let loop ()644
(await-readable sock)645
(when (xmpp-process-input conn)646
(when (conn-socket conn)647
(loop))))648
;; Blocking mode649
(let loop ()650
(let ((ready (socket-select (list sock) '() 1000)))651
(when (and ready (pair? (car ready)))652
(xmpp-process-input conn))653
(when (conn-socket conn)654
(loop))))))))656
))