(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 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-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 (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? frame) (frame-type frame) (frame-masking-key frame) (frame-length frame))) (set-record-type-printer! display-frame) (define* (make-pong-frame bv #:optional (masking-key #f)) (make-frame #t 'pong masking-key bv)) (define* (make-fclose-frame bv #:optional (masking-key #f)) (make-frame #t 'close masking-key bv)) (define* (make-text-frame str #:optional (masking-key #f)) (make-frame #t 'text masking-key (string->utf8 str))) (define* (make-binary-frame bv #:optional (masking-key #f)) (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 to 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))) ;;; ;;; 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* ((length (bytevector-length data)) (bv (make-bytevector length))) (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))))