summaryrefslogtreecommitdiff
path: root/xep-0114.scm
blob: 9b9440819be449ef85b6575a24a9e6b9f1c6f662 (about) (plain)
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
(define-module (gnubba xep-0114)
  #:use-module (gnubba sha1)
  #:use-module (gnubba sugar)
  #:use-module (ice-9 rdelim)
  #:use-module (ice-9 regex)
  #:use-module (sxml simple)
  #:export (with-component))


(define component-port 5347)


(define (stream-header component-name)
  (string-concatenate (list "<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' to='" component-name "'>")))


(define (generate-handshake stream-id shared-secret)
  (sha1 (string-concatenate (list stream-id shared-secret))))


;; Establish a component connection and execute actions.
(define-syntax with-component
  (syntax-rules ()
    ((with-component domain shared-secret exp ...)
     (begin
       (define addr (make-socket-address AF_INET INADDR_LOOPBACK component-port))
       (define sock (socket AF_INET SOCK_STREAM 0))

       (define (get-attribute name response)
         (define (get-attributes)
           "Extract key value pairs from a string and return as alist."
           (map (lambda (res)
                  (cons (match:substring res 1)
                        (match:substring res 3)))
                (list-matches "([^= ]+)=(['\"])([^ ]+)\\2" response)))
         (let ((found (assoc name (get-attributes))))
           (unless found
             (throw 'stream-error 'attribute-not-found name response))
           found))

       (define (close-stream)
         ;; TODO: make sure this is understood as "</stream:stream>"
         (display "</stream>" sock)
         ;; TODO: wait for other side to also send a closing tag before terminating the connection
         (close-port sock))

       (define (write-to-stream msg)
         "Write msg to sock and return a single response tag.  Ignore <?xml..> tag."
         (when msg
           (display msg sock))
         (let ((response-string (read-delimited ">" sock)))
           (if (string-match "<?xml" response-string)
               (let ((response-string (read-delimited ">" sock)))
                 response-string)
               response-string)))

       (catch #t
         (lambda ()
           (connect sock addr)
           (let ((response (write-to-stream (stream-header domain))))
             ;; server must reply with either an error or a stream header
             ;; TODO: should not match against literal string
             (unless (string-match "<stream:stream" response)
               (throw 'stream-error 'stream response))
             (let ((stream-id (cdr (get-attribute "id" response))))
               ;; send handshake
               (sxml->xml `(handshake ,(generate-handshake stream-id shared-secret)) sock)
               (let* ((ns '((stream . "http://etherx.jabber.org/streams")))
                      (response (xml->sxml sock #:namespaces ns #:trim-whitespace? #t)))
                 ;; expect empty handshake element as acknowlegement
                 (unless (equal? 'handshake (caadr response))
                   (throw 'stream-error 'handshake response))

                 (display "Connected to XMPP server!\n")

                 ;; Handlers are passed as expressions that
                 ;; are evaluated in the xmpp monad.
                 (display "Registering handlers...\n")
                 (begin
                   (xmpp-> sock exp ...))

                 ;; Blocking wait for stanzas.  At this
                 ;; point, new handler can only be
                 ;; registered by previously registered
                 ;; handlers.
                 (display "Starting listener loop.\n")
                 (run-handler-loop sock)

                 ;; close connection at the end
                 (display "Closing stream.\n")
                 (close-stream)))))
         (lambda (key . args)
           (if (and (equal? 'system-error key)
                    (string-prefix? "connect" (car args)))
               (begin
                 ;; connection refused or something
                 (display (format #t "ERROR: ~S\n" (cdr args))))
               (begin
                 (display (format #t "ERROR: ~S.  Closing stream. (~S)\n" key args))
                 (close-stream)))))))))