summaryrefslogtreecommitdiff
path: root/web/socket/frame.scm
blob: 9319c181116110ed3e7f7bb8e5b89c010e2e57d9 (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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
;;; guile-websocket --- WebSocket client/server
;;; Copyright © 2015 David Thompson <davet@gnu.org>
;;; Copyright © 2023 Andrew Whatson <whatson@tailcall.au>
;;;
;;; 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 (web socket frame)
  #:use-module (rnrs bytevectors)
  #:use-module (rnrs io ports)
  #:use-module (ice-9 format)
  #:use-module (ice-9 match)
  #:use-module (ice-9 ports internal)
  #: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
            read-data-frame
            read-close-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))))))))


;;;
;;; 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)))

(define* (port-readable? port #:optional timeout)
  "Wait for PORT to become readable or TIMEOUT seconds to elapse.
Returns #t if the port is readable, or #f on timeout."
  ;; TODO: Integrate with suspendable-ports & fibers.
  (match (select (vector (port-read-wait-fd port)) #() #() timeout)
    ((#(port) #() #()) #t)
    (else #f)))

(define* (port-writable? port #:optional timeout)
  "Wait for PORT to become writable, or TIMEOUT seconds to elapse.
Returns #t if the port is writable, or #f on timeout."
  ;; TODO: Integrate with suspendable-ports & fibers.
  (match (select #() (vector (port-write-wait-fd port)) #() timeout)
    ((#() #(port) #()) #t)
    (else #f)))

(define* (frame-reader port #:key timeout)
  "Return a generator which will read a frame from PORT on each call,
until TIMEOUT seconds have elapsed.  This is used to implement timeouts
over composed read operations."
  (define timeout-at-real-time
    (and timeout
         (exact->inexact
          (floor (+ (get-internal-real-time)
                    (* internal-time-units-per-second
                       timeout))))))
  (define (timeout-remaining)
    (and timeout
         (/ (- timeout-at-real-time
               (get-internal-real-time))
            internal-time-units-per-second)))
  (lambda ()
    (cond ((port-closed? port)
           #f)
          ((not (port-readable? port (timeout-remaining)))
           #f)
          ((port-eof? port)
           (eof-object))
          (else
           (read-frame port)))))

(define* (read-data-frame port #:key echo-close? timeout)
  "Read frames from PORT until a complete data frame is received or
TIMEOUT seconds have elapsed, handling any control frames transparently.
ECHO-CLOSE? should be #t if PORT represents the server end of a
WebSocket connection."
  (define next-frame
    (frame-reader port #:timeout timeout))
  (let loop ((fragments '())
             (type #f))
    (let ((frame (next-frame)))
      (cond
       ;; EOF - port is closed.
       ((eof-object? frame)
        (close-port port)
        #f)
       ;; Read timeout or already closed.
       ((not frame)
        #f)
       ;; Per section 5.4, control frames may appear interspersed
       ;; along with a fragmented message.
       ((close-frame? frame)
        (when echo-close?
          ;; 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)) port)))
        (close-port port)
        #f)
       ((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)) port)
        (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
        (make-frame #t type #f (frame-concatenate (reverse fragments))))
       ((fragment-frame? frame) ; add a fragment
        (loop (cons frame fragments) type))
       ((data-frame? frame) ; unfragmented data frame
        frame)
       (else
        (websocket-error "unexpected frame: " frame))))))

(define* (read-close-frame port #:key timeout)
  "Read frames from PORT until a close frame is received or TIMEOUT
seconds have elapsed, ignoring control and data frames."
  (define next-frame
    (frame-reader port #:timeout timeout))
  (let loop ()
    (let ((frame (next-frame)))
      (cond ((not (frame? frame))
             #f)
            ((close-frame? frame)
             #t)
            (else
             (loop))))))


;;;
;;; 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))
    (force-output port)))