diff options
Diffstat (limited to 'web/socket/frame.scm')
-rw-r--r-- | web/socket/frame.scm | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/web/socket/frame.scm b/web/socket/frame.scm new file mode 100644 index 0000000..291c883 --- /dev/null +++ b/web/socket/frame.scm @@ -0,0 +1,263 @@ +(define-module (web socket frame) + #:use-module (ice-9 format) + #:use-module (ice-9 match) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-9) + #:use-module (srfi srfi-9 gnu) + #:use-module (srfi srfi-26) + #:use-module (rnrs bytevectors) + #:use-module (rnrs io ports) + #:export (make-frame + frame? + frame-final? + frame-type + frame-masking-key + frame-data + + continuation-frame? + text-frame? + binary-frame? + close-frame? + ping-frame? + pong-frame? + 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 (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 (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))) + + +;;; +;;; Frame reader +;;; + +(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)) + + ;; See section 5.3 - Client-to-Server Masking + (define (unmask-bytevector! bv masking-key) + (let loop ((i 0)) + (when (< i (bytevector-length bv)) + (let ((unmasked (logxor (u8vector-ref bv i) + (u8vector-ref masking-key (modulo i 4))))) + (u8vector-set! bv i unmasked) + (loop (1+ i)))))) + + (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 to large: " type length)) + + (let ((bv (get-bytevector-n port length))) + (when masking-key + (unmask-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))) + + +;;; +;;; 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)) + + (let ((length (frame-length frame)) + (mask (frame-masking-key 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. + (let ((mask (frame-masking-key frame))) + (when mask (put-bytevector port mask))) + + ;; Write data. + (put-bytevector port (frame-data frame)))) |