websocket: Preventing some eof-object related errors.
[8sync.git] / 8sync / systems / websocket / server.scm
1 ;;; guile-websocket --- WebSocket client/server
2 ;;; Copyright © 2015 David Thompson <davet@gnu.org>
3 ;;; Copyright © 2017 Christopher Allan Webber <cwebber@dustycloud.org>
4 ;;;
5 ;;; This file is part of guile-websocket.
6 ;;;
7 ;;; Guile-websocket is free software; you can redistribute it and/or modify
8 ;;; it under the terms of the GNU Lesser General Public License as
9 ;;; published by the Free Software Foundation; either version 3 of the
10 ;;; License, or (at your option) any later version.
11 ;;;
12 ;;; Guile-websocket is distributed in the hope that it will be useful,
13 ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 ;;; Lesser General Public License for more details.
16 ;;;
17 ;;; You should have received a copy of the GNU Lesser General Public
18 ;;; License along with guile-websocket.  If not, see
19 ;;; <http://www.gnu.org/licenses/>.
20
21 ;;; Commentary:
22 ;;
23 ;; WebSocket server.
24 ;;
25 ;;; Code:
26
27 (define-module (8sync systems websocket server)
28   #:use-module (ice-9 match)
29   #:use-module (rnrs bytevectors)
30   #:use-module (rnrs io ports)
31   #:use-module (web request)
32   #:use-module (web response)
33   #:use-module (web uri)
34   #:use-module (oop goops)
35   #:use-module (8sync)
36   #:use-module (8sync ports)
37   #:use-module (8sync systems web)
38   #:use-module (8sync systems websocket frame)
39   #:use-module (8sync systems websocket utils)
40   #:export (<websocket-server>
41             .websocket-handler))
42
43 ;; See section 4.2 for explanation of the handshake.
44 (define (read-handshake-request client-socket)
45   "Read HTTP request from CLIENT-SOCKET that should contain the
46 headers required for a WebSocket handshake."
47   ;; See section 4.2.1.
48   (read-request client-socket))
49
50 (define (make-handshake-response client-key)
51   "Return an HTTP response object for upgrading to a WebSocket
52 connection for the client whose key is CLIENT-KEY, a base64 encoded
53 string."
54   ;; See section 4.2.2.
55   (let ((accept-key (make-accept-key (string-trim-both client-key))))
56     (build-response #:code 101
57                     #:headers `((upgrade . ("websocket"))
58                                 (connection . (upgrade))
59                                 (sec-websocket-accept . ,accept-key)))))
60
61 (define no-op (const #f))
62
63 (define (make-simple-counter)
64   (let ((count 0))
65     (lambda ()
66       (set! count (1+ count))
67       count)))
68
69 (define-actor <websocket-server> (<web-server>)
70   ((ws-send websocket-server-send))
71   (upgrade-paths #:init-value `(("websocket" .
72                                  ,(wrap-apply websocket-client-loop)))
73                  #:allocation #:each-subclass
74                  #:accessor .upgrade-paths)
75
76   (gen-client-id #:init-thunk make-simple-counter)
77
78   ;; active websocket connections
79   (ws-clients #:init-thunk make-hash-table
80               #:accessor .ws-clients)
81
82   (on-ws-message #:init-keyword #:on-ws-message 
83                  #:getter .on-ws-message)
84   (on-ws-client-connect #:init-keyword #:on-ws-client-connect
85                         #:init-value no-op
86                         #:getter .on-ws-client-connect)
87   (on-ws-client-disconnect #:init-keyword #:on-ws-client-disconnect
88                            #:init-value no-op
89                            #:getter .on-ws-client-disconnect))
90
91 (define (web-server-gen-client-id websocket-server)
92   ((slot-ref websocket-server 'gen-client-id)))
93
94 (define (websocket-client-loop websocket-server client request body)
95   "Serve client connected via CLIENT by performing the HTTP
96 handshake and listening for control and data frames.  HANDLER is
97 called for each complete message that is received."
98   ;; TODO: We'll also want to handle stuff like the sub-protocol.
99   (define (handle-data-frame type data)
100     ((.on-ws-message websocket-server)
101      websocket-server client-id
102      (match type
103        ('text   (utf8->string data))
104        ('binary data))))
105
106   (define (read-frame-maybe)
107     (and (not (eof-object? (lookahead-u8 client)))
108          (read-frame client)))
109
110   ;; Allows other actors to send things to this specific client
111   ;; @@: We probably could just increment a counter...
112   (define client-id (web-server-gen-client-id websocket-server))
113
114   (define (close-down)
115     (close-port client)
116     (hash-remove! (.ws-clients websocket-server) client-id)
117     ((.on-ws-client-disconnect websocket-server)
118      websocket-server client-id))
119
120   (hash-set! (.ws-clients websocket-server) client-id client)
121
122   ;; Disable buffering for websockets
123   (setvbuf client 'none)
124
125   ((.on-ws-client-connect websocket-server)
126    websocket-server client-id)
127
128   ;; Perform the HTTP handshake and upgrade to WebSocket protocol.
129   (let* ((client-key (assoc-ref (request-headers request) 'sec-websocket-key))
130          (response (make-handshake-response client-key)))
131     (write-response response client)
132     (let loop ((fragments '())
133                (type #f))
134       (let ((frame (read-frame-maybe)))
135         (cond
136          ;; EOF - port is closed.
137          ;; @@: Sometimes the eof object appears here as opposed to
138          ;;   at lookahead, but I'm not sure why
139          ((or (not frame) (eof-object? frame))
140           (close-down))
141          ;; Per section 5.4, control frames may appear interspersed
142          ;; along with a fragmented message.
143          ((close-frame? frame)
144           ;; Per section 5.5.1, echo the close frame back to the
145           ;; client before closing the socket.  The client may no
146           ;; longer be listening.
147           (false-if-exception
148            (write-frame (make-close-frame (frame-data frame)) client))
149           (close-down))
150          ((ping-frame? frame)
151           ;; Per section 5.5.3, a pong frame must include the exact
152           ;; same data as the ping frame.
153           (write-frame (make-pong-frame (frame-data frame)) client)
154           (loop fragments type))
155          ((pong-frame? frame) ; silently ignore pongs
156           (loop fragments type))
157          ((first-fragment-frame? frame) ; begin accumulating fragments
158           (loop (list frame) (frame-type frame)))
159          ((final-fragment-frame? frame) ; concatenate all fragments
160           (handle-data-frame type (frame-concatenate (reverse fragments)))
161           (loop '() #f))
162          ((fragment-frame? frame) ; add a fragment
163           (loop (cons frame fragments) type))
164          ((data-frame? frame) ; unfragmented data frame
165           (handle-data-frame (frame-type frame) (frame-data frame))
166           (loop '() #f)))))))
167
168 (define (websocket-server-send websocket-server message client-id data)
169   (cond ((hash-ref (.ws-clients websocket-server) client-id) =>
170          (lambda (client)
171            (write-frame
172             (cond ((string? data)
173                    (make-text-frame data))
174                   ((bytevector? data)
175                    (make-binary-frame data)))
176             client)
177            ;; ok is like success, amirite
178            (<-reply message 'ok)))
179         (else
180          ;; No such client with that id.
181          ;; Either it closed, or it was never there.
182          (<-reply message 'client-gone))))