websockets: Initial websocket support.
authorChristopher Allan Webber <cwebber@dustycloud.org>
Sun, 15 Jan 2017 00:24:34 +0000 (18:24 -0600)
committerChristopher Allan Webber <cwebber@dustycloud.org>
Sun, 22 Jan 2017 04:16:05 +0000 (22:16 -0600)
* 8sync/contrib/base64.scm:
* 8sync/contrib/sha-1.scm:
* 8sync/ports.scm:
* 8sync/systems/websocket.scm:
* 8sync/systems/websocket/client.scm:
* 8sync/systems/websocket/frame.scm:
* 8sync/systems/websocket/server.scm:
* 8sync/systems/websocket/utils.scm: New files.

* Makefile.am (SOURCES): Add them.

* 8sync/systems/web.scm (<web-server>): Rename upgrade slot to
upgrade-paths and use it.
(.upgrade-paths, maybe-upgrade-request): New variables.
(web-server-client-loop): Upgrade protocol if appropriate.

8sync/contrib/base64.scm [new file with mode: 0644]
8sync/contrib/sha-1.scm [new file with mode: 0644]
8sync/ports.scm [new file with mode: 0644]
8sync/systems/web.scm
8sync/systems/websocket.scm [new file with mode: 0644]
8sync/systems/websocket/client.scm [new file with mode: 0644]
8sync/systems/websocket/frame.scm [new file with mode: 0644]
8sync/systems/websocket/server.scm [new file with mode: 0644]
8sync/systems/websocket/utils.scm [new file with mode: 0644]
Makefile.am

diff --git a/8sync/contrib/base64.scm b/8sync/contrib/base64.scm
new file mode 100644 (file)
index 0000000..0eb489e
--- /dev/null
@@ -0,0 +1,285 @@
+;; -*- mode: scheme; coding: utf-8 -*-
+;; Copyright © 2009, 2010, 2012, 2013 Göran Weinholt <goran@weinholt.se>
+
+;; Permission is hereby granted, free of charge, to any person obtaining a
+;; copy of this software and associated documentation files (the "Software"),
+;; to deal in the Software without restriction, including without limitation
+;; the rights to use, copy, modify, merge, publish, distribute, sublicense,
+;; and/or sell copies of the Software, and to permit persons to whom the
+;; Software is furnished to do so, subject to the following conditions:
+
+;; The above copyright notice and this permission notice shall be included in
+;; all copies or substantial portions of the Software.
+
+;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+;; THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+;; DEALINGS IN THE SOFTWARE.
+#!r6rs
+
+;; RFC 4648 Base-N Encodings
+
+(library (8sync contrib base64)
+  (export base64-encode
+          base64-decode
+          base64-alphabet
+          base64url-alphabet
+          get-delimited-base64
+          put-delimited-base64)
+  (import (rnrs)
+          (only (srfi :13 strings)
+                string-index
+                string-prefix? string-suffix?
+                string-concatenate string-trim-both))
+
+  (define base64-alphabet
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
+
+  (define base64url-alphabet
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")
+
+  (define base64-encode
+    (case-lambda
+      ;; Simple interface. Returns a string containing the canonical
+      ;; base64 representation of the given bytevector.
+      ((bv)
+       (base64-encode bv 0 (bytevector-length bv) #f #f base64-alphabet #f))
+      ((bv start)
+       (base64-encode bv start (bytevector-length bv) #f #f base64-alphabet #f))
+      ((bv start end)
+       (base64-encode bv start end #f #f base64-alphabet #f))
+      ((bv start end line-length)
+       (base64-encode bv start end line-length #f base64-alphabet #f))
+      ((bv start end line-length no-padding)
+       (base64-encode bv start end line-length no-padding base64-alphabet #f))
+      ((bv start end line-length no-padding alphabet)
+       (base64-encode bv start end line-length no-padding alphabet #f))
+      ;; Base64 encodes the bytes [start,end[ in the given bytevector.
+      ;; Lines are limited to line-length characters (unless #f),
+      ;; which must be a multiple of four. To omit the padding
+      ;; characters (#\=) set no-padding to a true value. If port is
+      ;; #f, returns a string.
+      ((bv start end line-length no-padding alphabet port)
+       (assert (or (not line-length) (zero? (mod line-length 4))))
+       (let-values (((p extract) (if port
+                                     (values port (lambda () (values)))
+                                     (open-string-output-port))))
+         (letrec ((put (if line-length
+                           (let ((chars 0))
+                             (lambda (p c)
+                               (when (fx=? chars line-length)
+                                 (set! chars 0)
+                                 (put-char p #\linefeed))
+                               (set! chars (fx+ chars 1))
+                               (put-char p c)))
+                           put-char)))
+           (let lp ((i start))
+             (cond ((= i end))
+                   ((<= (+ i 3) end)
+                    (let ((x (bytevector-uint-ref bv i (endianness big) 3)))
+                      (put p (string-ref alphabet (fxbit-field x 18 24)))
+                      (put p (string-ref alphabet (fxbit-field x 12 18)))
+                      (put p (string-ref alphabet (fxbit-field x 6 12)))
+                      (put p (string-ref alphabet (fxbit-field x 0 6)))
+                      (lp (+ i 3))))
+                   ((<= (+ i 2) end)
+                    (let ((x (fxarithmetic-shift-left (bytevector-u16-ref bv i (endianness big)) 8)))
+                      (put p (string-ref alphabet (fxbit-field x 18 24)))
+                      (put p (string-ref alphabet (fxbit-field x 12 18)))
+                      (put p (string-ref alphabet (fxbit-field x 6 12)))
+                      (unless no-padding
+                        (put p #\=))))
+                   (else
+                    (let ((x (fxarithmetic-shift-left (bytevector-u8-ref bv i) 16)))
+                      (put p (string-ref alphabet (fxbit-field x 18 24)))
+                      (put p (string-ref alphabet (fxbit-field x 12 18)))
+                      (unless no-padding
+                        (put p #\=)
+                        (put p #\=)))))))
+         (extract)))))
+
+  ;; Create a lookup table for the alphabet and remember the latest table.
+  (define get-decode-table
+    (let ((ascii-table #f)
+          (extra-table '())     ;in the unlikely case of unicode chars
+          (table-alphabet #f))
+      (lambda (alphabet)
+        (unless (eq? alphabet table-alphabet)
+          ;; Rebuild the table.
+          (do ((ascii (make-vector 128 #f))
+               (extra '())
+               (i 0 (+ i 1)))
+              ((= i (string-length alphabet))
+               (set! ascii-table ascii)
+               (set! extra-table extra))
+            (let ((c (char->integer (string-ref alphabet i))))
+              (if (fx<=? c 127)
+                  (vector-set! ascii c i)
+                  (set! extra (cons (cons c i) extra)))))
+          (set! table-alphabet alphabet))
+        (values ascii-table extra-table))))
+
+  ;; Decodes a correctly padded base64 string, optionally ignoring
+  ;; non-alphabet characters.
+  (define base64-decode
+    (case-lambda
+      ((str)
+       (base64-decode str base64-alphabet #f))
+      ((str alphabet)
+       (base64-decode str alphabet #f))
+      ((str alphabet port)
+       (base64-decode str alphabet port #t))
+      ((str alphabet port strict?)
+       (define (pad? c) (eqv? c (char->integer #\=)))
+       (let-values (((p extract) (if port
+                                     (values port (lambda () (values)))
+                                     (open-bytevector-output-port)))
+                    ((ascii extra) (get-decode-table alphabet)))
+         (define-syntax lookup
+           (syntax-rules ()
+             ((_ c) (or (and (fx<=? c 127) (vector-ref ascii c))
+                        (cond ((assv c extra) => cdr)
+                              (else #f))))))
+         (let* ((len (if strict?
+                         (string-length str)
+                         (let lp ((i (fx- (string-length str) 1)))
+                           ;; Skip trailing invalid chars.
+                           (cond ((fxzero? i) 0)
+                                 ((let ((c (char->integer (string-ref str i))))
+                                    (or (lookup c) (pad? c)))
+                                  (fx+ i 1))
+                                 (else (lp (fx- i 1))))))))
+           (let lp ((i 0))
+             (cond
+               ((fx=? i len)
+                (extract))
+               ((fx<=? i (fx- len 4))
+                (let lp* ((c1 (char->integer (string-ref str i)))
+                          (c2 (char->integer (string-ref str (fx+ i 1))))
+                          (c3 (char->integer (string-ref str (fx+ i 2))))
+                          (c4 (char->integer (string-ref str (fx+ i 3))))
+                          (i i))
+                  (let ((i1 (lookup c1)) (i2 (lookup c2))
+                        (i3 (lookup c3)) (i4 (lookup c4)))
+                    (cond
+                      ((and i1 i2 i3 i4)
+                       ;; All characters present and accounted for.
+                       ;; The most common case.
+                       (let ((x (fxior (fxarithmetic-shift-left i1 18)
+                                       (fxarithmetic-shift-left i2 12)
+                                       (fxarithmetic-shift-left i3 6)
+                                       i4)))
+                         (put-u8 p (fxbit-field x 16 24))
+                         (put-u8 p (fxbit-field x 8 16))
+                         (put-u8 p (fxbit-field x 0 8))
+                         (lp (fx+ i 4))))
+                      ((and i1 i2 i3 (pad? c4) (= i (- len 4)))
+                       ;; One padding character at the end of the input.
+                       (let ((x (fxior (fxarithmetic-shift-left i1 18)
+                                       (fxarithmetic-shift-left i2 12)
+                                       (fxarithmetic-shift-left i3 6))))
+                         (put-u8 p (fxbit-field x 16 24))
+                         (put-u8 p (fxbit-field x 8 16))
+                         (lp (fx+ i 4))))
+                      ((and i1 i2 (pad? c3) (pad? c4) (= i (- len 4)))
+                       ;; Two padding characters.
+                       (let ((x (fxior (fxarithmetic-shift-left i1 18)
+                                       (fxarithmetic-shift-left i2 12))))
+                         (put-u8 p (fxbit-field x 16 24))
+                         (lp (fx+ i 4))))
+                      ((not strict?)
+                       ;; Non-alphabet characters.
+                       (let lp ((i i) (c* '()) (n 4))
+                         (cond ((fxzero? n)
+                                ;; Found four valid characters.
+                                (lp* (cadddr c*) (caddr c*) (cadr c*) (car c*)
+                                     (fx- i 4)))
+                               ((fx=? i len)
+                                (error 'base64-decode
+                                       "Invalid input in non-strict mode."
+                                       i c*))
+                               (else
+                                ;; Gather alphabetic (or valid
+                                ;; padding) characters.
+                                (let ((c (char->integer (string-ref str i))))
+                                  (cond ((or (lookup c)
+                                             (and (pad? c)
+                                                  (fx<=? n 2)
+                                                  (fx=? i (fx- len n))))
+                                         (lp (fx+ i 1) (cons c c*) (fx- n 1)))
+                                        (else
+                                         (lp (fx+ i 1) c* n))))))))
+                      (else
+                       (error 'base64-decode
+                              "Invalid input in strict mode."
+                              c1 c2 c3 c4))))))
+               (else
+                (error 'base64-decode
+                       "The input is too short, it may be missing padding."
+                       i)))))))))
+
+  (define (get-line-comp f port)
+    (if (port-eof? port)
+        (eof-object)
+        (f (get-line port))))
+
+  ;; Reads the common -----BEGIN/END type----- delimited format from
+  ;; the given port. Returns two values: a string with the type and a
+  ;; bytevector containing the base64 decoded data. The second value
+  ;; is the eof object if there is an eof before the BEGIN delimiter.
+  (define get-delimited-base64
+    (case-lambda
+      ((port)
+       (get-delimited-base64 port #t))
+      ((port strict)
+       (define (get-first-data-line port)
+         ;; Some MIME data has header fields in the same format as mail
+         ;; or http. These are ignored.
+         (let ((line (get-line-comp string-trim-both port)))
+           (cond ((eof-object? line) line)
+                 ((string-index line #\:)
+                  (let lp ()               ;read until empty line
+                    (let ((line (get-line-comp string-trim-both port)))
+                      (if (string=? line "")
+                          (get-line-comp string-trim-both port)
+                          (lp)))))
+                 (else line))))
+       (let ((line (get-line-comp string-trim-both port)))
+         (cond ((eof-object? line)
+                (values "" (eof-object)))
+               ((string=? line "")
+                (get-delimited-base64 port))
+               ((and (string-prefix? "-----BEGIN " line)
+                     (string-suffix? "-----" line))
+                (let* ((type (substring line 11 (- (string-length line) 5)))
+                       (endline (string-append "-----END " type "-----")))
+                  (let-values (((outp extract) (open-bytevector-output-port)))
+                    (let lp ((line (get-first-data-line port)))
+                      (cond ((eof-object? line)
+                             (error 'get-delimited-base64
+                                    "unexpected end of file"))
+                            ((string-prefix? "-" line)
+                             (unless (string=? line endline)
+                               (error 'get-delimited-base64
+                                      "bad end delimiter" type line))
+                             (values type (extract)))
+                            (else
+                             (unless (and (= (string-length line) 5)
+                                          (string-prefix? "=" line)) ;Skip Radix-64 checksum
+                               (base64-decode line base64-alphabet outp))
+                             (lp (get-line-comp string-trim-both port))))))))
+               (else ;skip garbage (like in openssl x509 -in foo -text output).
+                (get-delimited-base64 port)))))))
+
+  (define put-delimited-base64
+    (case-lambda
+      ((port type bv line-length)
+       (display (string-append "-----BEGIN " type "-----\n") port)
+       (base64-encode bv 0 (bytevector-length bv)
+                      line-length #f base64-alphabet port)
+       (display (string-append "\n-----END " type "-----\n") port))
+      ((port type bv)
+       (put-delimited-base64 port type bv 76)))))
diff --git a/8sync/contrib/sha-1.scm b/8sync/contrib/sha-1.scm
new file mode 100644 (file)
index 0000000..6400094
--- /dev/null
@@ -0,0 +1,300 @@
+;; -*- mode: scheme; coding: utf-8 -*-
+;; Copyright © 2009, 2010, 2012 Göran Weinholt <goran@weinholt.se>
+
+;; Permission is hereby granted, free of charge, to any person obtaining a
+;; copy of this software and associated documentation files (the "Software"),
+;; to deal in the Software without restriction, including without limitation
+;; the rights to use, copy, modify, merge, publish, distribute, sublicense,
+;; and/or sell copies of the Software, and to permit persons to whom the
+;; Software is furnished to do so, subject to the following conditions:
+
+;; The above copyright notice and this permission notice shall be included in
+;; all copies or substantial portions of the Software.
+
+;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+;; THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+;; DEALINGS IN THE SOFTWARE.
+#!r6rs
+
+;; Byte-oriented SHA-1 from FIPS 180-3 and RFC 3174.
+
+;; The data being hashed will never be modified here.
+
+;; TODO: give an error if more than 2^64 bits are processed?
+;; TODO: Optimize. Should be simple enough with the help of a profiler.
+
+(library (8sync contrib sha-1)
+  (export make-sha-1 sha-1-update! sha-1-finish! sha-1-clear!
+          sha-1 sha-1-copy sha-1-finish
+          sha-1-transform!              ;for interested parties only
+          sha-1-length
+          sha-1-copy-hash! sha-1-96-copy-hash!
+          sha-1->bytevector sha-1->string
+          sha-1-hash=? sha-1-96-hash=?
+          hmac-sha-1)
+  (import (except (rnrs) bitwise-rotate-bit-field))
+
+  (define (sha-1-length) 20)
+
+  (define (vector-copy x) (vector-map (lambda (i) i) x))
+
+  (define (rol32 n count)
+    (let ((field1 (bitwise-and #xffffffff (bitwise-arithmetic-shift-left n count)))
+          (field2 (bitwise-arithmetic-shift-right n (- 32 count))))
+      (bitwise-ior field1 field2)))
+
+  (define-record-type sha1state
+    (fields (immutable H)               ;Hash
+            (immutable W)               ;temporary data
+            (immutable m)               ;unprocessed data
+            (mutable pending)           ;length of unprocessed data
+            (mutable processed)))       ;length of processed data
+
+  (define (make-sha-1)
+    (let ((H (list->vector initial-hash))
+          (W (make-bytevector (* 4 80)))
+          (m (make-bytevector (* 4 16))))
+      (make-sha1state H W m 0 0)))
+
+  (define (sha-1-copy state)
+    (let ((H (vector-copy (sha1state-H state)))
+          (W (make-bytevector (* 4 80)))
+          (m (bytevector-copy (sha1state-m state))))
+      (make-sha1state H W m
+                      (sha1state-pending state)
+                      (sha1state-processed state))))
+
+  (define (sha-1-clear! state)
+    (for-each (lambda (i v)
+                (vector-set! (sha1state-H state) i v))
+              '(0 1 2 3 4)
+              initial-hash)
+    (bytevector-fill! (sha1state-W state) 0)
+    (bytevector-fill! (sha1state-m state) 0)
+    (sha1state-pending-set! state 0)
+    (sha1state-processed-set! state 0))
+
+  (define initial-hash '(#x67452301 #xefcdab89 #x98badcfe #x10325476 #xc3d2e1f0))
+
+  (define (Ch x y z)
+    (bitwise-xor (bitwise-and x y)
+                 (bitwise-and (bitwise-not x) z)))
+
+  (define Parity bitwise-xor)
+
+  (define (Maj x y z)
+    (bitwise-xor (bitwise-and x y)
+                 (bitwise-and x z)
+                 (bitwise-and y z)))
+
+  (define k1 #x5a827999)
+  (define k2 #x6ed9eba1)
+  (define k3 #x8f1bbcdc)
+  (define k4 #xca62c1d6)
+
+  (define (f t B C D)
+    ((cond ((<= 0 t 19) Ch)
+           ((<= 20 t 39) Parity)
+           ((<= 40 t 59) Maj)
+           (else Parity))
+     B C D))
+
+  (define (K t)
+    (cond ((<= 0 t 19) k1)
+          ((<= 20 t 39) k2)
+          ((<= 40 t 59) k3)
+          (else k4)))
+
+  ;; This function transforms a whole 512 bit block.
+  (define (sha-1-transform! H W m offset)
+    ;; Copy the message block
+    (do ((t 0 (+ t 4)))
+        ((= t (* 4 16)))
+      (bytevector-u32-native-set! W t (bytevector-u32-ref m (+ t offset) (endianness big))))
+    ;; Initialize W[16..79]
+    (do ((t (* 4 16) (+ t 4)))
+        ((= t (* 4 80)))
+      (bytevector-u32-native-set! W t (rol32
+                                       (bitwise-xor (bytevector-u32-native-ref W (- t (* 4 3)))
+                                                    (bytevector-u32-native-ref W (- t (* 4 8)))
+                                                    (bytevector-u32-native-ref W (- t (* 4 14)))
+                                                    (bytevector-u32-native-ref W (- t (* 4 16))))
+                                       1)))
+    ;; Do the hokey pokey
+    (let lp ((A (vector-ref H 0))
+             (B (vector-ref H 1))
+             (C (vector-ref H 2))
+             (D (vector-ref H 3))
+             (E (vector-ref H 4))
+             (t 0))
+      (cond ((= t 80)
+             (vector-set! H 0 (bitwise-and #xffffffff (+ A (vector-ref H 0))))
+             (vector-set! H 1 (bitwise-and #xffffffff (+ B (vector-ref H 1))))
+             (vector-set! H 2 (bitwise-and #xffffffff (+ C (vector-ref H 2))))
+             (vector-set! H 3 (bitwise-and #xffffffff (+ D (vector-ref H 3))))
+             (vector-set! H 4 (bitwise-and #xffffffff (+ E (vector-ref H 4)))))
+            (else
+             (lp (bitwise-and #xffffffff
+                              (+ (rol32 A 5)
+                                 (f t B C D)
+                                 E
+                                 (bytevector-u32-native-ref W (* 4 t))
+                                 (K t)))
+                 A
+                 (rol32 B 30)
+                 C
+                 D
+                 (+ t 1))))))
+
+  ;; Add a bytevector to the state. Align your data to whole blocks if
+  ;; you want this to go a little faster.
+  (define sha-1-update!
+    (case-lambda
+      ((state data start end)
+       (let ((m (sha1state-m state))    ;unprocessed data
+             (H (sha1state-H state))
+             (W (sha1state-W state)))
+         (let lp ((offset start))
+           (cond ((= (sha1state-pending state) 64)
+                  ;; A whole block is pending
+                  (sha-1-transform! H W m 0)
+                  (sha1state-pending-set! state 0)
+                  (sha1state-processed-set! state (+ 64 (sha1state-processed state)))
+                  (lp offset))
+                 ((= offset end)
+                  (values))
+                 ((or (> (sha1state-pending state) 0)
+                      (> (+ offset 64) end))
+                  ;; Pending data exists or less than a block remains.
+                  ;; Add more pending data.
+                  (let ((added (min (- 64 (sha1state-pending state))
+                                    (- end offset))))
+                    (bytevector-copy! data offset
+                                      m (sha1state-pending state)
+                                      added)
+                    (sha1state-pending-set! state (+ added (sha1state-pending state)))
+                    (lp (+ offset added))))
+                 (else
+                  ;; Consume a whole block
+                  (sha-1-transform! H W data offset)
+                  (sha1state-processed-set! state (+ 64 (sha1state-processed state)))
+                  (lp (+ offset 64)))))))
+      ((state data)
+       (sha-1-update! state data 0 (bytevector-length data)))))
+
+  (define zero-block (make-bytevector 64 0))
+
+  ;; Finish the state by adding a 1, zeros and the counter.
+  (define (sha-1-finish! state)
+    (let ((m (sha1state-m state))
+          (pending (+ (sha1state-pending state) 1)))
+      (bytevector-u8-set! m (sha1state-pending state) #x80)
+      (cond ((> pending 56)
+             (bytevector-copy! zero-block 0
+                               m pending
+                               (- 64 pending))
+             (sha-1-transform! (sha1state-H state)
+                               (sha1state-W state)
+                               m
+                               0)
+             (bytevector-fill! m 0))
+            (else
+             (bytevector-copy! zero-block 0
+                               m pending
+                               (- 64 pending))))
+      ;; Number of bits in the data
+      (bytevector-u64-set! m 56
+                           (* (+ (sha1state-processed state)
+                                 (- pending 1))
+                              8)
+                           (endianness big))
+      (sha-1-transform! (sha1state-H state)
+                        (sha1state-W state)
+                        m
+                        0)))
+
+  (define (sha-1-finish state)
+    (let ((copy (sha-1-copy state)))
+      (sha-1-finish! copy)
+      copy))
+
+  ;; Find the SHA-1 of the concatenation of the given bytevectors.
+  (define (sha-1 . data)
+    (let ((state (make-sha-1)))
+      (for-each (lambda (d) (sha-1-update! state d))
+                data)
+      (sha-1-finish! state)
+      state))
+
+  (define (copy-hash! state bv off len)
+    (do ((i 0 (+ i 1)))
+        ((= i len))
+      (bytevector-u32-set! bv (+ off (* 4 i))
+                           (vector-ref (sha1state-H state) i)
+                           (endianness big))))
+
+  (define (sha-1-copy-hash! state bv off)
+    (copy-hash! state bv off 5))
+
+  (define (sha-1-96-copy-hash! state bv off)
+    (copy-hash! state bv off 3))
+
+  (define (sha-1->bytevector state)
+    (let ((ret (make-bytevector (* 4 5))))
+      (sha-1-copy-hash! state ret 0)
+      ret))
+
+  (define (sha-1->string state)
+    (apply string-append
+           (map (lambda (x)
+                  (if (< x #x10)
+                      (string-append "0" (number->string x 16))
+                      (number->string x 16)))
+                (bytevector->u8-list (sha-1->bytevector state)))))
+
+  ;; Compare an SHA-1 state with a bytevector. It is supposed to not
+  ;; terminate early in order to not leak timing information. Assumes
+  ;; that the bytevector's length is ok.
+  (define (cmp state bv len)
+    (do ((i 0 (fx+ i 1))
+         (diff 0 (+ diff
+                    (bitwise-xor
+                     (bytevector-u32-ref bv (* 4 i) (endianness big))
+                     (vector-ref (sha1state-H state) i)))))
+        ((fx=? i len)
+         (zero? diff))))
+
+  (define (sha-1-hash=? state bv) (cmp state bv 5))
+
+  (define (sha-1-96-hash=? state bv) (cmp state bv 3))
+
+;;; HMAC-SHA-1. RFC 2104.
+
+  ;; TODO: an API with make, update!, finish!, finish, clear!, copy, etc
+
+  (define (hmac-sha-1 secret . data)
+    ;; RFC 2104.
+    (if (> (bytevector-length secret) 64)
+        (apply hmac-sha-1 (sha-1->bytevector (sha-1 secret)) data)
+        (let ((k-ipad (make-bytevector 64 0))
+              (k-opad (make-bytevector 64 0)))
+          (bytevector-copy! secret 0 k-ipad 0 (bytevector-length secret))
+          (bytevector-copy! secret 0 k-opad 0 (bytevector-length secret))
+          (do ((i 0 (fx+ i 1)))
+              ((fx=? i 64))
+            (bytevector-u8-set! k-ipad i (fxxor #x36 (bytevector-u8-ref k-ipad i)))
+            (bytevector-u8-set! k-opad i (fxxor #x5c (bytevector-u8-ref k-opad i))))
+          (let ((state (make-sha-1)))
+            (sha-1-update! state k-ipad)
+            (for-each (lambda (d) (sha-1-update! state d)) data)
+            (sha-1-finish! state)
+            (let ((digest (sha-1->bytevector state)))
+              (sha-1-clear! state)
+              (sha-1-update! state k-opad)
+              (sha-1-update! state digest)
+              (sha-1-finish! state)
+              state))))))
diff --git a/8sync/ports.scm b/8sync/ports.scm
new file mode 100644 (file)
index 0000000..d7b82b8
--- /dev/null
@@ -0,0 +1,26 @@
+;;; 8sync --- Asynchronous programming for Guile
+;;; Copyright © 2017 Christopher Allan Webber <cwebber@dustycloud.org>
+;;;
+;;; This file is part of 8sync.
+;;;
+;;; 8sync is free software: you can redistribute it and/or modify it
+;;; under the terms of the GNU Lesser General Public License as
+;;; published by the Free Software Foundation, either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; 8sync is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU Lesser General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with 8sync.  If not, see <http://www.gnu.org/licenses/>.
+
+
+(define-module (8sync ports)
+  #:export (set-port-nonblocking!))
+
+(define (set-port-nonblocking! port)
+  "Do the usual song and dance to make this port nonblocking."
+  (let ((flags (fcntl port F_GETFL)))
+    (fcntl port F_SETFL (logior O_NONBLOCK flags))))
index 82ed4d20985b9c80deab018bdc556866ca424681..a6ee696e571a4826d4af20b52fdd0b1129b8187c 100644 (file)
   #:use-module (web server)
   #:use-module (rnrs io ports)
   #:use-module (8sync)
-  #:export (<web-server>))
+  #:export (<web-server>
+            ;; @@: If we don't want to import these because of
+            ;;   "conflicts" with other objects, we could just
+            ;;   select <web-server> only.
+            ;;   Alternately we could move the crux of this into
+            ;;   another module and just export <web-server>, though
+            ;;   that does feel a bit like overkill.
+            .host .family .port-num .addr .socket
+            .upgrade-paths .http-handler))
 
 (define-actor <web-server> (<actor>)
   ((*init* web-server-socket-loop)
         #:accessor .addr)
   (socket #:init-value #f
           #:accessor .socket)
-  (upgrade #:init-value '()
-           #:allocation #:each-subclass)
+  (upgrade-paths #:init-value '()
+                 #:allocation #:each-subclass)
   (http-handler #:init-keyword #:http-handler
                 #:getter .http-handler))
 
+;; Define getter externally so it works even if we subclass
+(define-method (.upgrade-paths (web-server <web-server>))
+  (slot-ref web-server 'upgrade-paths))
+
 (define-method (initialize (web-server <web-server>) init-args)
   (next-method)
   ;; Make sure the addr is set up
@@ -127,6 +139,23 @@ as we're alive."
               ((0) (memq 'keep-alive (response-connection response)))))
            (else #f)))))
 
+(define (maybe-upgrade-request web-server request body)
+  (define upgrade-paths (.upgrade-paths web-server))
+  ;; A request can specify multiple values to the "Upgrade"
+  ;; field, so we slook to see if we have an applicable option,
+  ;; in order.
+  ;; Note that we'll only handle one... we *don't* "compose"
+  ;; upgrades.
+  (let loop ((upgrades (request-upgrade request)))
+    (if (eq? upgrades '())
+        #f ; Shouldn't upgrade
+        (match (assoc (car upgrades) upgrade-paths)
+          ;; Yes, upgrade with this method
+          ((_ . upgrade-proc)
+           upgrade-proc)
+          ;; Keep looking...
+          (#f (loop (cdr upgrades)))))))
+
 (define (web-server-client-loop web-server message client)
   "Read request(s) from a client and pass off to the handler."
   (with-throw-handler #t
@@ -149,12 +178,22 @@ as we're alive."
             (lambda ()
               (let* ((request (read-request client))
                      (body (read-request-body request)))
-                (call-with-message
-                 ;; TODO: Add error handling in case we get an error
-                 ;;   response
-                 (<-wait (actor-id web-server) 'handle-request
-                         request body)
-                 respond-and-maybe-continue)))
+                (cond
+                 ;; Should we "upgrade" the protocol?
+                 ;; Doing so "breaks out" of this loop, possibly into a new one
+                 ((maybe-upgrade-request web-server request body) =>
+                  (lambda (upgrade)
+                    ;; TODO: this isn't great because we're in this catch,
+                    ;;   which doesn't make sense once we've "upgraded"
+                    ;;   since we might not "respond" in the same way anymore.
+                    (upgrade web-server client request body)))
+                 (else
+                  (call-with-message
+                   ;; TODO: Add error handling in case we get an error
+                   ;;   response
+                   (<-wait (actor-id web-server) 'handle-request
+                           request body)
+                   respond-and-maybe-continue)))))
             (lambda (key . args)
               (display "While reading request:\n" (current-error-port))
               (print-exception (current-error-port) #f key args)
diff --git a/8sync/systems/websocket.scm b/8sync/systems/websocket.scm
new file mode 100644 (file)
index 0000000..93bfdac
--- /dev/null
@@ -0,0 +1,31 @@
+;;; 8sync --- Asynchronous programming for Guile
+;;; Copyright © 2015 David Thompson <davet@gnu.org>
+;;; Copyright © 2017 Christopher Allan Webber <cwebber@dustycloud.org>
+;;;
+;;; This file is part of 8sync.
+;;;
+;;; 8sync is free software: you can redistribute it and/or modify it
+;;; under the terms of the GNU Lesser General Public License as
+;;; published by the Free Software Foundation, either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; 8sync is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU Lesser General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with 8sync.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; WebSocket server.
+;; Adapted from guile-websocket.
+;;
+;;; Code:
+
+(define-module (8sync systems websocket)
+  #:use-module (8sync systems websocket client)
+  #:use-module (8sync systems websocket server)
+  #:export (<websocket-server>
+            <websocket-client>))
diff --git a/8sync/systems/websocket/client.scm b/8sync/systems/websocket/client.scm
new file mode 100644 (file)
index 0000000..86f82ef
--- /dev/null
@@ -0,0 +1,210 @@
+;;; guile-websocket --- WebSocket client/server
+;;; Copyright © 2016 David Thompson <davet@gnu.org>
+;;;
+;;; This file is part of guile-websocket.
+;;;
+;;; Guile-websocket is free software; you can redistribute it and/or modify
+;;; it under the terms of the GNU Lesser General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; Guile-websocket is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with guile-websocket.  If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; WebSocket client.
+;;
+;;; Code:
+
+(define-module (8sync systems websocket client)
+  #:use-module (ice-9 match)
+  #:use-module (rnrs bytevectors)
+  #:use-module (rnrs io ports)
+  #:use-module (srfi srfi-9)
+  #:use-module (srfi srfi-9 gnu)
+  #:use-module (web request)
+  #:use-module (web response)
+  #:use-module (web uri)
+  #:use-module (8sync contrib base64)
+  #:use-module (8sync systems websocket frame)
+  #:use-module (8sync systems websocket utils)
+  #:export (make-websocket
+            websocket?
+            websocket-uri
+            websocket-state
+            websocket-connecting?
+            websocket-open?
+            websocket-closing?
+            websocket-closed?
+            close-websocket
+            websocket-send
+            websocket-receive))
+
+;; See Section 3 - WebSocket URIs
+(define (encrypted-websocket-scheme? uri)
+  "Return #t if the scheme for URI is 'wss', the secure WebSocket
+scheme."
+  (eq? (uri-scheme uri) 'wss))
+
+(define (unencrypted-websocket-scheme? uri)
+  "Return #t if the scheme for URI is 'ws', the insecure WebSocket
+scheme."
+  (eq? (uri-scheme uri) 'ws))
+
+(define (websocket-uri? uri)
+  "Return #t if URI is a valid WebSocket URI."
+  (and (or (encrypted-websocket-scheme? uri)
+           (unencrypted-websocket-scheme? uri))
+       (not (uri-fragment uri))))
+
+(define (make-client-socket uri)
+  "Connect a socket to the remote resource described by URI."
+  (let* ((port (uri-port uri))
+         (info (car (getaddrinfo (uri-host uri)
+                                 (if port
+                                     (number->string port)
+                                     (symbol->string (uri-scheme uri)))
+                                 (if port
+                                     AI_NUMERICSERV
+                                     0))))
+         (s (with-fluids ((%default-port-encoding #f))
+              (socket (addrinfo:fam info) SOCK_STREAM IPPROTO_IP))))
+    ;; TODO: Configure I/O buffering?
+    (connect s (addrinfo:addr info))
+    s))
+
+(define-record-type <websocket>
+  (%make-websocket uri socket entropy-port state)
+  websocket?
+  (uri websocket-uri)
+  (socket websocket-socket)
+  (entropy-port websocket-entropy-port)
+  (state websocket-state set-websocket-state!))
+
+(define (display-websocket ws port)
+  (format port "#<websocket ~a ~a>"
+          (uri->string (websocket-uri ws))
+          (websocket-state ws)))
+
+(set-record-type-printer! <websocket> display-websocket)
+
+(define (websocket-connecting? ws)
+  "Return #t if the WebSocket WS is in the connecting state."
+  (eq? (websocket-state ws) 'connecting))
+
+(define (websocket-open? ws)
+  "Return #t if the WebSocket WS is in the open state."
+  (eq? (websocket-state ws) 'open))
+
+(define (websocket-closing? ws)
+  "Return #t if the WebSocket WS is in the closing state."
+  (eq? (websocket-state ws) 'closing))
+
+(define (websocket-closed? ws)
+  "Return #t if the WebSocket WS is in the closed state."
+  (eq? (websocket-state ws) 'closed))
+
+(define (generate-client-key ws)
+  "Return a random, base64 encoded nonce using the entropy source of
+WS."
+  (base64-encode
+   (get-bytevector-n (websocket-entropy-port ws) 16)))
+
+;; See Section 4.1 - Client Requirements
+(define (make-handshake-request uri key)
+  "Create an HTTP request for initiating a WebSocket connection with
+the remote resource described by URI, using a randomly generated nonce
+KEY."
+  (let ((headers `((host . (,(uri-host uri) . #f))
+                   (upgrade . ("WebSocket"))
+                   (connection . (upgrade))
+                   (sec-websocket-key . ,key)
+                   (sec-websocket-version . "13"))))
+    (build-request uri #:method 'GET #:headers headers)))
+
+(define (handshake ws)
+  "Perform the WebSocket handshake for the client WS."
+  (let ((key (generate-client-key ws)))
+    (write-request (make-handshake-request (websocket-uri ws) key)
+                   (websocket-socket ws))
+    (let* ((response (read-response (websocket-socket ws)))
+           (headers (response-headers response))
+           (upgrade (assoc-ref headers 'upgrade))
+           (connection (assoc-ref headers 'connection))
+           (accept (assoc-ref headers 'sec-websocket-accept)))
+      ;; Validate the handshake.
+      (if (and (= (response-code response) 101)
+               (string-ci=? (car upgrade) "websocket")
+               (equal? connection '(upgrade))
+               (string=? (string-trim-both accept) (make-accept-key key)))
+          (set-websocket-state! ws 'open)
+          (begin
+            (close-websocket ws)
+            (error "websocket handshake failed" (websocket-uri ws)))))))
+
+(define (open-entropy-port)
+  "Return an open input port to a reliable source of entropy for the
+current system."
+  ;; XXX: This works on GNU/Linux and OS X systems, but this isn't
+  ;; exactly portable.
+  (open-input-file "/dev/urandom"))
+
+(define (make-websocket uri-or-string)
+  "Create and establish a new WebSocket connection for the remote
+resource described by URI-OR-STRING."
+  (let ((uri (match uri-or-string
+               ((? uri? uri) uri)
+               ((? string? str) (string->uri str)))))
+    (if (websocket-uri? uri)
+        (let ((ws (%make-websocket uri
+                                   (make-client-socket uri)
+                                   (open-entropy-port)
+                                   'connecting)))
+          (handshake ws)
+          ws)
+        (error "not a websocket uri" uri))))
+
+(define (close-websocket ws)
+  "Close the WebSocket connection for the client WS."
+  (let ((socket (websocket-socket ws)))
+    (set-websocket-state! ws 'closing)
+    (write-frame (make-close-frame (make-bytevector 0)) socket)
+    ;; Per section 5.5.1 , wait for the server to close the connection
+    ;; for a reasonable amount of time.
+    (let loop ()
+      (match (select #() (vector socket) #() 1) ; 1 second timeout
+        ((#() #(socket) #()) ; there is output to read
+         (unless (port-eof? socket)
+           (read-frame socket) ; throw it away
+           (loop)))))
+    (close-port socket)
+    (close-port (websocket-entropy-port ws))
+    (set-websocket-state! ws 'closed)
+    *unspecified*))
+
+(define (generate-masking-key ws)
+  "Create a new masking key using the entropy source of WS."
+  ;; Masking keys are 32 bits long.
+  (get-bytevector-n (websocket-entropy-port ws) 4))
+
+(define (websocket-send ws data)
+  "Send DATA, a string or bytevector, to the server that WS is
+connected to."
+  ;; TODO: Send frames over some threshold in fragments.
+  (write-frame (make-text-frame data (generate-masking-key ws))
+               (websocket-socket ws)))
+
+(define (websocket-receive ws)
+  "Read a response from the server that WS is connected to."
+  ;; TODO: Handle fragmented frames and control frames.
+  (let ((frame (read-frame (websocket-socket ws))))
+    (if (binary-frame? frame)
+        (frame-data frame)
+        (text-frame->string frame))))
diff --git a/8sync/systems/websocket/frame.scm b/8sync/systems/websocket/frame.scm
new file mode 100644 (file)
index 0000000..7a71dba
--- /dev/null
@@ -0,0 +1,375 @@
+;;; guile-websocket --- WebSocket client/server
+;;; Copyright © 2015 David Thompson <davet@gnu.org>
+;;;
+;;; This file is part of guile-websocket.
+;;;
+;;; Guile-websocket is free software; you can redistribute it and/or modify
+;;; it under the terms of the GNU Lesser General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; Guile-websocket is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with guile-websocket.  If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; WebSocket frame abstraction.
+;;
+;;; Code:
+
+(define-module (8sync systems websocket frame)
+  #:use-module (rnrs bytevectors)
+  #:use-module (rnrs io ports)
+  #:use-module (ice-9 format)
+  #:use-module (ice-9 match)
+  #:use-module (ice-9 rdelim)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-9)
+  #:use-module (srfi srfi-9 gnu)
+  #:use-module (srfi srfi-26)
+  #:export (make-frame
+            frame?
+            frame-final?
+            frame-type
+            frame-masking-key
+            frame-data
+
+            make-ping-frame
+            make-pong-frame
+            make-close-frame
+            make-text-frame
+            make-binary-frame
+
+            continuation-frame?
+            text-frame?
+            binary-frame?
+            close-frame?
+            ping-frame?
+            pong-frame?
+            fragment-frame?
+            first-fragment-frame?
+            final-fragment-frame?
+            control-frame?
+            data-frame?
+
+            frame-length
+            frame-concatenate
+            text-frame->string
+            text-frames->string
+
+            read-frame
+            write-frame))
+
+;;;
+;;; WebSocket frames
+;;;
+
+(define-record-type <frame>
+  (make-frame final? type masking-key data)
+  frame?
+  (final? frame-final?)
+  (type frame-type)
+  (masking-key frame-masking-key)
+  (data frame-data))
+
+(define (display-frame frame port)
+  (format port "#<frame final?: ~a type: ~a masking-key: ~a length: ~d>"
+          (frame-final? frame)
+          (frame-type frame)
+          (frame-masking-key frame)
+          (frame-length frame)))
+
+(set-record-type-printer! <frame> display-frame)
+
+(define* (make-ping-frame bv #:optional masking-key)
+  "Return a \"ping\" control frame containing the contents of the
+bytevector BV, masked with MASKING-KEY.  By default, the data is
+unmasked."
+  (make-frame #t 'ping masking-key bv))
+
+(define* (make-pong-frame bv #:optional masking-key)
+  "Return a \"pong\" control frame containing the contents of the
+bytevector BV, masked with MASKING-KEY.  By default, the data is
+unmasked."
+  (make-frame #t 'pong masking-key bv))
+
+(define* (make-close-frame bv #:optional masking-key)
+  "Return a \"close\" control frame containing the contents of the
+bytevector BV, masked with MASKING-KEY.  By default, the data is
+unmasked."
+  (make-frame #t 'close masking-key bv))
+
+(define* (make-text-frame text #:optional masking-key)
+  "Return a text data frame containing the string TEXT, masked with MASKING-KEY.
+By default, the text is unmasked."
+  (make-frame #t 'text masking-key (string->utf8 text)))
+
+(define* (make-binary-frame bv #:optional masking-key)
+  "Return a binary data frame containing the contents of the
+bytevector BV, masked with MASKING-KEY.  By default, the data is
+unmasked."
+  (make-frame #t 'binary masking-key bv))
+
+(define (continuation-frame? frame)
+  "Return #t if FRAME is a continuation frame."
+  (eq? (frame-type frame) 'continuation))
+
+(define (text-frame? frame)
+  "Return #t if FRAME is a text frame."
+  (eq? (frame-type frame) 'text))
+
+(define (binary-frame? frame)
+  "Return #t if FRAME is a binary frame."
+  (eq? (frame-type frame) 'binary))
+
+(define (close-frame? frame)
+  "Return #t if FRAME is a close frame."
+  (eq? (frame-type frame) 'close))
+
+(define (ping-frame? frame)
+  "Return #t if FRAME is a ping frame."
+  (eq? (frame-type frame) 'ping))
+
+(define (pong-frame? frame)
+  "Return #t if FRAME is a pong frame."
+  (eq? (frame-type frame) 'pong))
+
+;; See section 5.4 - Fragmentation
+(define (fragment-frame? frame)
+  "Return #t if FRAME is an incomplete message."
+  (or (continuation-frame? frame)
+      (not (frame-final? frame))))
+
+(define (first-fragment-frame? frame)
+  "Return #t if FRAME is the first piece of a fragmented message."
+  (and (not (frame-final? frame))
+       (data-frame? frame)))
+
+(define (final-fragment-frame? frame)
+  "Return #t if FRAME is the final piece of a fragmented message."
+  (and (frame-final? frame)
+       (continuation-frame? frame)))
+
+;; See section 5.5 - Control Frames
+(define (control-frame? frame)
+  "Return #t if FRAME is a control frame."
+  (or (close-frame? frame)
+      (ping-frame? frame)
+      (pong-frame? frame)))
+
+;; See section 5.6 - Data Frames
+(define (data-frame? frame)
+  "Return #t if FRAME is a data frame."
+  (or (text-frame? frame)
+      (binary-frame? frame)))
+
+(define (frame-length frame)
+  "Return the length of the data bytevector in FRAME."
+  (bytevector-length (frame-data frame)))
+
+(define (text-frame->string frame)
+  "Convert FRAME, an unfragmented text frame, into a string."
+  (utf8->string (frame-data frame)))
+
+(define (frame-concatenate frames)
+  "Concatenate the data in FRAMES, a list of fragmented frames, into a
+single bytevector."
+  (let ((bv (make-bytevector (reduce + 0 (map frame-length frames)))))
+    (let loop ((frames frames)
+               (offset 0))
+      (match frames
+        (() bv)
+        ((frame . rest)
+         (let ((length (frame-length frame)))
+           (bytevector-copy! (frame-data frame) 0 bv offset length)
+           (loop rest (+ offset length))))))))
+
+(define (text-frames->string frames)
+  "Convert FRAMES, a list of fragmented text frames, into a single
+concatenated string."
+  (utf8->string (frame-concatenate frames)))
+
+(define (call-with-input-bytevector bv proc)
+  "Call PROC with one argument: an open input port that reads from the
+bytevector BV."
+  (let ((port (open-bytevector-input-port bv)))
+    (dynamic-wind
+      (const #t)
+      (lambda ()
+        (proc port))
+      (lambda ()
+        (close-port port)))))
+
+(define (close-frame->status frame)
+  "Convert FRAME, a close frame, into a pair.  The \"car\" of the pair
+is a positive integer status code, and the \"cdr\" is a string
+containing the explanation, if present."
+  (define (read-status-code)
+    (match (bytevector-sint-ref (frame-data frame) 0 (endianness big) 2)
+      ;; See section 7.4
+      ((and (or 1005 1006 1015) status)
+       (websocket-error "invalid use of reserved status code: " status))
+      (status status)))
+
+  (let ((length (frame-length frame)))
+    (cond
+     ((zero? length) ; unspecified closing status
+      '(1005 . ""))
+     ((= length 2) ; status code only
+      (cons (read-status-code) ""))
+     (else ; status + reason
+      (cons (read-status-code)
+            (call-with-input-bytevector (frame-data frame)
+              (lambda (port)
+                ;; Throw away the status code.
+                (get-u8 port)
+                (get-u8 port)
+                ;; Now read the reason.
+                (read-string port))))))))
+
+\f
+;;;
+;;; Frame reader
+;;;
+
+;; See section 5.3 - Client-to-Server Masking
+(define (mask-bytevector! bv masking-key)
+  "Apply the WebSocket masking algorithm to the bytevector BV using
+MASKING-KEY."
+  (let loop ((i 0))
+    (when (< i (bytevector-length bv))
+      (let ((masked (logxor (u8vector-ref bv i)
+                            (u8vector-ref masking-key (modulo i 4)))))
+        (u8vector-set! bv i masked)
+        (loop (1+ i))))))
+
+(define (websocket-error message . args)
+  (apply error message args))
+
+;; See section 5.2 - Base Framing Protocol
+(define (read-frame port)
+  (define (opcode->frame-type opcode)
+    (match opcode
+      (#x0 'continuation)
+      (#x1 'text)
+      (#x2 'binary)
+      (#x8 'close)
+      (#x9 'ping)
+      (#xA 'pong)
+      (else (websocket-error "invalid opcode: " opcode))))
+
+  (define (control-frame? type)
+    (memq type '(close ping pong)))
+
+  (define (parse-fin-bit octet)
+    ;; Test the first bit of the octet.
+    (not (zero? (logand #x80 octet))))
+
+  (define (parse-opcode octet final?)
+    ;; The opcode is stored in the least significant nibble of the
+    ;; octet.
+    (let ((type (opcode->frame-type (logand #x0f octet))))
+      ;; Section 5.5 specifies that control frames must not be
+      ;; fragmented.
+      (when (and (not final?) (control-frame? type))
+        (websocket-error "fragmented control frame: " type))
+      type))
+
+  (define (parse-mask-bit octet)
+    (not (zero? (logand #x80 octet))))
+
+  (define (parse-length octet)
+    ;; For lengths <= 125, the frame length is encoded in the last 7
+    ;; bits of the octet.  If this number is 126, then the true length
+    ;; is encoded in the following 16 bits.  If the number is 127,
+    ;; then the true length is encoded in the following 64 bits.
+    (match (logand #x7f octet)
+      (126
+       (bytevector-u16-ref (get-bytevector-n port 2) 0 (endianness big)))
+      (127
+       (bytevector-u64-ref (get-bytevector-n port 8) 0 (endianness big)))
+      (length length)))
+
+  (define (parse-masking-key)
+    ;; Masking keys are always 32 bits.
+    (get-bytevector-n port 4))
+
+  (define (read-data type masking-key length)
+    ;; Section 5.5 specifies that control frame bodies may not exceed
+    ;; 125 bytes.
+    (when (and (> length 125)
+               (control-frame? type))
+      (websocket-error "control frame too large: " type length))
+
+    (let ((bv (get-bytevector-n port length)))
+      (when masking-key
+        (mask-bytevector! bv masking-key))
+      bv))
+
+  (let* ((type-byte (get-u8 port))
+         (length-byte (get-u8 port))
+         (final? (parse-fin-bit type-byte))
+         (type (parse-opcode type-byte final?))
+         (mask? (parse-mask-bit length-byte))
+         (length (parse-length length-byte))
+         (masking-key (and mask? (parse-masking-key)))
+         (data (read-data type masking-key length)))
+    (make-frame final? type masking-key data)))
+
+\f
+;;;
+;;; Frame writer
+;;;
+
+(define* (write-frame frame #:optional (port (current-output-port)))
+  ;; Packs an unsigned integer into a bytevector in network byte
+  ;; order.
+  (define (uint->bytevector n size)
+    (uint-list->bytevector (list n) (endianness big) size))
+
+  (define (masked-data mask data)
+    (let ((bv (bytevector-copy data)))
+      (mask-bytevector! bv mask)
+      bv))
+
+  (let ((length (frame-length frame))
+        (mask   (frame-masking-key frame))
+        (data   (frame-data frame)))
+    ;; Write FIN bit and opcode.
+    (put-u8 port
+            (logior (if (frame-final? frame) #x80 #x00)
+                    (match (frame-type frame)
+                      ('continuation #x00)
+                      ('text         #x01)
+                      ('binary       #x02)
+                      ('close        #x08)
+                      ('ping         #x09)
+                      ('pong         #x0A))))
+
+    ;; Write mask bit and length.
+    (put-u8 port
+            (logior (if mask #x80 #x00)
+                    (cond
+                     ((< length 126) length)
+                     ((< length (expt 2 16)) 126)
+                     (else 127))))
+
+    ;; Write true size when size is greater than 125.
+    (cond
+     ((< length 126) #f)
+     ((< length (expt 2 16))
+      (put-bytevector port (uint->bytevector length 2)))
+     (else
+      (put-bytevector port (uint->bytevector length 8))))
+
+    ;; Write masking key, if present.
+    (when mask (put-bytevector port mask))
+
+    ;; Write data, potentially masked.
+    (put-bytevector port (if mask (masked-data mask data) data))))
diff --git a/8sync/systems/websocket/server.scm b/8sync/systems/websocket/server.scm
new file mode 100644 (file)
index 0000000..97645bb
--- /dev/null
@@ -0,0 +1,179 @@
+;;; guile-websocket --- WebSocket client/server
+;;; Copyright © 2015 David Thompson <davet@gnu.org>
+;;;
+;;; This file is part of guile-websocket.
+;;;
+;;; Guile-websocket is free software; you can redistribute it and/or modify
+;;; it under the terms of the GNU Lesser General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; Guile-websocket is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with guile-websocket.  If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; WebSocket server.
+;;
+;;; Code:
+
+(define-module (8sync systems websocket server)
+  #:use-module (ice-9 match)
+  #:use-module (rnrs bytevectors)
+  #:use-module (rnrs io ports)
+  #:use-module (web request)
+  #:use-module (web response)
+  #:use-module (web uri)
+  #:use-module (oop goops)
+  #:use-module (8sync)
+  #:use-module (8sync ports)
+  #:use-module (8sync systems web)
+  #:use-module (8sync systems websocket frame)
+  #:use-module (8sync systems websocket utils)
+  #:export (<websocket-server>
+            .websocket-handler))
+
+;; See section 4.2 for explanation of the handshake.
+(define (read-handshake-request client-socket)
+  "Read HTTP request from CLIENT-SOCKET that should contain the
+headers required for a WebSocket handshake."
+  ;; See section 4.2.1.
+  (read-request client-socket))
+
+(define (make-handshake-response client-key)
+  "Return an HTTP response object for upgrading to a WebSocket
+connection for the client whose key is CLIENT-KEY, a base64 encoded
+string."
+  ;; See section 4.2.2.
+  (let ((accept-key (make-accept-key (string-trim-both client-key))))
+    (build-response #:code 101
+                    #:headers `((upgrade . ("websocket"))
+                                (connection . (upgrade))
+                                (sec-websocket-accept . ,accept-key)))))
+
+(define no-op (const #f))
+
+(define (make-simple-counter)
+  (let ((count 0))
+    (lambda ()
+      (set! count (1+ count))
+      count)))
+
+(define-actor <websocket-server> (<web-server>)
+  ((ws-send websocket-server-send))
+  (upgrade-paths #:init-value `(("websocket" .
+                                 ,(wrap-apply websocket-client-loop)))
+                 #:allocation #:each-subclass
+                 #:accessor .upgrade-paths)
+
+  (gen-client-id #:init-thunk make-simple-counter)
+
+  ;; active websocket connections
+  (ws-clients #:init-thunk make-hash-table
+              #:accessor .ws-clients)
+
+  (on-ws-message #:init-keyword #:on-ws-message 
+                 #:getter .on-ws-message)
+  (on-ws-client-connect #:init-keyword #:on-ws-client-connect
+                        #:init-value no-op
+                        #:getter .on-ws-client-connect)
+  (on-ws-client-disconnect #:init-keyword #:on-ws-client-disconnect
+                           #:init-value no-op
+                           #:getter .on-ws-client-disconnect))
+
+(define (web-server-gen-client-id websocket-server)
+  ((slot-ref websocket-server 'gen-client-id)))
+
+(define (websocket-client-loop websocket-server client request body)
+  "Serve client connected via CLIENT by performing the HTTP
+handshake and listening for control and data frames.  HANDLER is
+called for each complete message that is received."
+  ;; TODO: We'll also want to handle stuff like the sub-protocol.
+  (define (handle-data-frame type data)
+    ((.on-ws-message websocket-server)
+     websocket-server client-id
+     (match type
+       ('text   (utf8->string data))
+       ('binary data))))
+
+  (define (read-frame-maybe)
+    (and (not (eof-object? (lookahead-u8 client)))
+         (read-frame client)))
+
+  ;; Allows other actors to send things to this specific client
+  ;; @@: We probably could just increment a counter...
+  (define client-id (web-server-gen-client-id websocket-server))
+
+  (define (close-down)
+    (close-port client)
+    (hash-remove! (.ws-clients websocket-server) client-id)
+    ((.on-ws-client-disconnect websocket-server)
+     websocket-server client-id))
+
+  (hash-set! (.ws-clients websocket-server) client-id client)
+
+  ;; Disable buffering for websockets
+  (setvbuf client 'none)
+
+  ((.on-ws-client-connect websocket-server)
+   websocket-server client-id)
+
+  ;; Perform the HTTP handshake and upgrade to WebSocket protocol.
+  (let* ((client-key (assoc-ref (request-headers request) 'sec-websocket-key))
+         (response (make-handshake-response client-key)))
+    (write-response response client)
+    (let loop ((fragments '())
+               (type #f))
+      (let ((frame (read-frame-maybe)))
+        (cond
+         ;; EOF - port is closed.
+         ((not frame)
+          (close-down))
+         ;; Per section 5.4, control frames may appear interspersed
+         ;; along with a fragmented message.
+         ((close-frame? frame)
+          ;; Per section 5.5.1, echo the close frame back to the
+          ;; client before closing the socket.  The client may no
+          ;; longer be listening.
+          (false-if-exception
+           (write-frame (make-close-frame (frame-data frame)) client))
+          (close-down))
+         ((ping-frame? frame)
+          ;; Per section 5.5.3, a pong frame must include the exact
+          ;; same data as the ping frame.
+          (write-frame (make-pong-frame (frame-data frame)) client)
+          (loop fragments type))
+         ((pong-frame? frame) ; silently ignore pongs
+          (loop fragments type))
+         ((first-fragment-frame? frame) ; begin accumulating fragments
+          (loop (list frame) (frame-type frame)))
+         ((final-fragment-frame? frame) ; concatenate all fragments
+          (handle-data-frame type (frame-concatenate (reverse fragments)))
+          (loop '() #f))
+         ((fragment-frame? frame) ; add a fragment
+          (loop (cons frame fragments) type))
+         ((data-frame? frame) ; unfragmented data frame
+          (handle-data-frame (frame-type frame) (frame-data frame))
+          (loop '() #f)))))))
+
+(define (websocket-server-send websocket-server message client-id data)
+  (cond ((hash-ref (.ws-clients websocket-server) client-id) =>
+         (lambda (client)
+           (write-frame
+            (cond ((string? data)
+                   (make-text-frame data))
+                  ((bytevector? data)
+                   (make-binary-frame data)))
+            client)
+           ;; ok is like success, amirite
+           (<-reply message 'ok)))
+        (else
+         ;; No such client with that id.
+         ;; Either it closed, or it was never there.
+         (<-reply message 'client-gone))))
diff --git a/8sync/systems/websocket/utils.scm b/8sync/systems/websocket/utils.scm
new file mode 100644 (file)
index 0000000..e316245
--- /dev/null
@@ -0,0 +1,43 @@
+;;; guile-websocket --- WebSocket client/server
+;;; Copyright © 2016 David Thompson <davet@gnu.org>
+;;;
+;;; This file is part of guile-websocket.
+;;;
+;;; Guile-websocket is free software; you can redistribute it and/or modify
+;;; it under the terms of the GNU Lesser General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; Guile-websocket is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; Lesser General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with guile-websocket.  If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; WebSocket utilities.
+;;
+;;; Code:
+
+(define-module (8sync systems websocket utils)
+  #:use-module (rnrs bytevectors)
+  #:use-module (8sync contrib base64)
+  #:use-module (8sync contrib sha-1)
+  #:export (%handshake-guid
+            make-accept-key))
+
+;; See section 1.3 - Opening Handshake
+(define %handshake-guid "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
+
+(define (make-accept-key client-key)
+  "Return a WebSocket accept key based on CLIENT-KEY, a base64 encoded
+string."
+  (base64-encode
+   (sha-1->bytevector
+    (sha-1
+     (string->utf8
+      (string-append client-key %handshake-guid))))))
index c93361f81101783f75c77df6c53c2d3afb3aa787..d0dd1a41251723a4c4c50af5c7af746c6bbcf790 100644 (file)
@@ -45,13 +45,21 @@ moddir=$(prefix)/share/guile/site/2.2
 godir=$(libdir)/guile/2.2/ccache
 
 SOURCES =  \
-       8sync.scm \
-       8sync/agenda.scm \
-       8sync/repl.scm \
-       8sync/systems/irc.scm \
-       8sync/systems/web.scm \
-       8sync/actors.scm \
-       8sync/debug.scm
+       8sync.scm                                       \
+       8sync/agenda.scm                                \
+       8sync/repl.scm                                  \
+       8sync/actors.scm                                \
+       8sync/debug.scm                                 \
+       8sync/ports.scm                                 \
+       8sync/contrib/base64.scm                        \
+       8sync/contrib/sha-1.scm                         \
+       8sync/systems/irc.scm                           \
+       8sync/systems/web.scm                           \
+       8sync/systems/websocket.scm                     \
+       8sync/systems/websocket/client.scm              \
+       8sync/systems/websocket/frame.scm               \
+       8sync/systems/websocket/server.scm              \
+       8sync/systems/websocket/utils.scm
 
 TESTS =                                                        \
        tests/test-agenda.scm                           \