Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f56b55708 | |||
| 35aa88199d | |||
| 949af5c092 | |||
| 44caad5ae9 | |||
| fa6ec7695a | |||
| 081209bd2f | |||
| 90b97f7d34 | |||
| fde9c289cc | |||
| 7f0b9d2873 | |||
| 5816661ebf | |||
| 946552f79c | |||
| e812e3cbbe | |||
| 23cecce39d | |||
| d8ee592823 | |||
| 221d2d0e6e | |||
| 3efccc88aa | |||
| 195d55732d | |||
| 91396e71e0 | |||
| 29561c4b71 | |||
| 9446f5992d | |||
| ecafa799d3 | |||
| 3692bbba77 | |||
| b1ed3de161 | |||
| b6fae3ef78 | |||
| 4bb341913f | |||
| 3fd9e266a1 | |||
| d194289740 | |||
| baf570ab65 | |||
| daef7b3ea4 | |||
| de406f1151 | |||
| fb9d35db77 | |||
| a6570f2659 | |||
| a79b61968f | |||
| 9312d6d5ca | |||
| c7c8de32f9 | |||
| b592bd1073 | |||
| 8044107bc7 |
25
test/echo-server.scm
Normal file
25
test/echo-server.scm
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
(import chicken scheme posix)
|
||||||
|
(use spiffy websockets)
|
||||||
|
|
||||||
|
(ping-interval 0)
|
||||||
|
(drop-incoming-pings #f)
|
||||||
|
(propagate-common-errors #f)
|
||||||
|
(max-message-size 20971520)
|
||||||
|
(max-frame-size 20971520)
|
||||||
|
|
||||||
|
(handle-not-found
|
||||||
|
(lambda (path)
|
||||||
|
(with-websocket
|
||||||
|
(lambda ()
|
||||||
|
(let loop ()
|
||||||
|
(receive (data type) (receive-message)
|
||||||
|
(unless (eq? type 'connection-close)
|
||||||
|
(send-message data type)
|
||||||
|
(loop))))))))
|
||||||
|
|
||||||
|
(debug-log (current-output-port))
|
||||||
|
(root-path ".")
|
||||||
|
(server-port 8080)
|
||||||
|
(start-server)
|
||||||
|
|
||||||
|
|
||||||
12
test/hello.scm
Normal file
12
test/hello.scm
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
(import chicken scheme)
|
||||||
|
(use spiffy websockets)
|
||||||
|
|
||||||
|
(handle-not-found
|
||||||
|
(lambda (path)
|
||||||
|
(when (string= path "/web-socket")
|
||||||
|
(with-websocket
|
||||||
|
(lambda ()
|
||||||
|
(send-message (string-append "you said: " (receive-message))))))))
|
||||||
|
|
||||||
|
(root-path ".")
|
||||||
|
(start-server port: 8080)
|
||||||
13
test/index.html
Normal file
13
test/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var ws = new WebSocket("ws://localhost:8080/web-socket");
|
||||||
|
ws.onmessage = function(evt) {
|
||||||
|
alert(evt.data);
|
||||||
|
};
|
||||||
|
ws.onopen = function() {
|
||||||
|
ws.send('Hello!');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
10
test/ws-test.spec
Normal file
10
test/ws-test.spec
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"servers": [
|
||||||
|
{"agent": "AutobahnServer",
|
||||||
|
"url": "ws://localhost:8080/web-socket",
|
||||||
|
"options": {"version": 13}}
|
||||||
|
],
|
||||||
|
"cases": ["1.*", "2.*", "3.*", "4.*", "5.*"],
|
||||||
|
"exclude-cases": [],
|
||||||
|
"exclude-agent-cases": {}
|
||||||
|
}
|
||||||
74
utf8-grammar.scm
Normal file
74
utf8-grammar.scm
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
(define (ucs-range->char-set/inclusive lower upper)
|
||||||
|
(ucs-range->char-set lower (add1 upper)))
|
||||||
|
|
||||||
|
(define utf8-tail
|
||||||
|
(in (ucs-range->char-set/inclusive #x80 #xBF)))
|
||||||
|
|
||||||
|
(define utf8-1
|
||||||
|
(satisfies (lambda (c) (or (< (char->integer c) 128)
|
||||||
|
(and (> (char->integer c) 128)
|
||||||
|
(< (char->integer c) 191))))))
|
||||||
|
|
||||||
|
(define utf8-2
|
||||||
|
(sequence
|
||||||
|
(in (ucs-range->char-set/inclusive #xC2 #xDF))
|
||||||
|
utf8-tail))
|
||||||
|
|
||||||
|
(define utf8-3
|
||||||
|
(any-of
|
||||||
|
(sequence
|
||||||
|
(is #\xE0)
|
||||||
|
(in (ucs-range->char-set/inclusive #xA0 #xBF))
|
||||||
|
utf8-tail)
|
||||||
|
(sequence
|
||||||
|
(in (ucs-range->char-set/inclusive #xE1 #xEC))
|
||||||
|
(repeated utf8-tail 2))
|
||||||
|
(sequence
|
||||||
|
(is #\xED)
|
||||||
|
(in (ucs-range->char-set/inclusive #x80 #x9F))
|
||||||
|
utf8-tail)
|
||||||
|
(sequence
|
||||||
|
(in (ucs-range->char-set/inclusive #xEE #xEF))
|
||||||
|
(repeated utf8-tail 2))))
|
||||||
|
|
||||||
|
(define utf8-4
|
||||||
|
(any-of
|
||||||
|
(sequence
|
||||||
|
(is #\xF0)
|
||||||
|
(in (ucs-range->char-set/inclusive #x90 #xBF))
|
||||||
|
(repeated utf8-tail 2))
|
||||||
|
(sequence
|
||||||
|
(in (ucs-range->char-set/inclusive #xF1 #xF3))
|
||||||
|
(repeated utf8-tail 3))
|
||||||
|
(sequence
|
||||||
|
(is #\xF4)
|
||||||
|
(in (ucs-range->char-set/inclusive #x80 #x8F))
|
||||||
|
(repeated utf8-tail 2))))
|
||||||
|
|
||||||
|
(define utf8-char
|
||||||
|
(any-of
|
||||||
|
utf8-1
|
||||||
|
utf8-2
|
||||||
|
utf8-3
|
||||||
|
utf8-4))
|
||||||
|
|
||||||
|
(define utf8-string
|
||||||
|
(followed-by (zero-or-more utf8-char) end-of-input))
|
||||||
|
|
||||||
|
;; (parse utf8-string (->parser-input "Hello-µ@ßöäüàá-UTF-8!!"))
|
||||||
|
;; (parse utf8-char (->parser-input #\a))
|
||||||
|
|
||||||
|
;; (define (valid-utf8? s)
|
||||||
|
;; (let ((len (string-length s)))
|
||||||
|
;; (let loop ((i 0))
|
||||||
|
;; (if (= i len)
|
||||||
|
;; #t
|
||||||
|
;; (let ((r (parse utf8-char (->parser-input (->string (string-ref s i))))))
|
||||||
|
;; (if r
|
||||||
|
;; (loop (+ i (length r)))
|
||||||
|
;; (string-ref s i)))))))
|
||||||
|
;; (valid-utf8? "Hello-µ@ßöäüàá-UTF-8!!")
|
||||||
|
;; (valid-utf8? "Hello")
|
||||||
|
;; (parse utf8-char (->parser-input (->string #\H)))
|
||||||
|
|
||||||
|
;; #\xC0
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
#include "utf8validator.h"
|
|
||||||
|
|
||||||
static const uint8_t UTF8VALIDATOR_DFA[] =
|
|
||||||
{
|
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
|
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
|
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
|
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
|
|
||||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
|
|
||||||
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
|
|
||||||
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
|
|
||||||
|
|
||||||
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
|
|
||||||
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
|
|
||||||
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
|
|
||||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
|
|
||||||
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
|
|
||||||
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
|
|
||||||
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1 // s7..s8
|
|
||||||
};
|
|
||||||
|
|
||||||
#define UTF8_ACCEPT 0
|
|
||||||
#define UTF8_REJECT 1
|
|
||||||
|
|
||||||
void utf8vld_reset (utf8_validator_t* validator) {
|
|
||||||
validator->state = UTF8_ACCEPT;
|
|
||||||
validator->current_index = 0;
|
|
||||||
validator->total_index = 0;
|
|
||||||
validator->is_valid = 1;
|
|
||||||
validator->ends_on_codepoint = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void utf8vld_validate (utf8_validator_t* validator, const uint8_t* data, size_t offset, size_t length) {
|
|
||||||
|
|
||||||
int state = validator->state;
|
|
||||||
|
|
||||||
for (size_t i = offset; i < length + offset; ++i) {
|
|
||||||
|
|
||||||
state = UTF8VALIDATOR_DFA[256 + (state << 4) + UTF8VALIDATOR_DFA[data[i]]];
|
|
||||||
|
|
||||||
if (state == UTF8_REJECT)
|
|
||||||
{
|
|
||||||
validator->state = state;
|
|
||||||
validator->current_index = i - offset;
|
|
||||||
validator->total_index += i - offset;
|
|
||||||
validator->is_valid = 0;
|
|
||||||
validator->ends_on_codepoint = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validator->state = state;
|
|
||||||
validator->current_index = length;
|
|
||||||
validator->total_index += length;
|
|
||||||
validator->is_valid = 1;
|
|
||||||
validator->ends_on_codepoint = validator->state == UTF8_ACCEPT;
|
|
||||||
}
|
|
||||||
|
|
||||||
int utf8_valid(const uint8_t* data, size_t len) {
|
|
||||||
utf8_validator_t validator;
|
|
||||||
utf8vld_reset(&validator);
|
|
||||||
utf8vld_validate(&validator, data, 0, len);
|
|
||||||
return validator.is_valid;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#ifndef UTF8_VALIDATOR_H
|
|
||||||
#define UTF8_VALIDATOR_H
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
size_t current_index;
|
|
||||||
size_t total_index;
|
|
||||||
int state;
|
|
||||||
int is_valid;
|
|
||||||
int ends_on_codepoint;
|
|
||||||
} utf8_validator_t;
|
|
||||||
|
|
||||||
extern void utf8vld_reset (utf8_validator_t* validator);
|
|
||||||
|
|
||||||
extern void utf8vld_validate (utf8_validator_t* validator, const uint8_t* data, size_t offset, size_t length);
|
|
||||||
|
|
||||||
extern int utf8_valid(const uint8_t* data, size_t len);
|
|
||||||
|
|
||||||
#endif // UTF8_VALIDATOR_H
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
(synopsis "websockets provides a websocket API.")
|
(synopsis "websockets provides a websocket API.")
|
||||||
(license "BSD")
|
(license "BSD")
|
||||||
(category web)
|
(category web)
|
||||||
(depends srfi-1 srfi-4 spiffy intarweb uri-common base64 simple-sha1 srfi-18
|
(author "Thomas Hintz")
|
||||||
srfi-13 miscmacros mailbox)
|
(depends (spiffy 5.3.1) intarweb uri-common base64 simple-sha1 mailbox comparse)
|
||||||
;(test-depends http-client test server-test regex)
|
(files "websockets.setup" "websockets.meta" "websockets.release-info" "LICENSE"
|
||||||
(files "websockets.setup" "websockets.meta" "websockets.release-info" "LICENSE"))
|
"utf8-grammar.scm"))
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
;; -*- scheme -*-
|
;; -*- scheme -*-
|
||||||
(repo git "git@bitbucket.org:thomashintz/{egg-name}.git")
|
(repo git "git@bitbucket.org:thomashintz/{egg-name}.git")
|
||||||
;(uri targz "https://github.com/mario-goulart/{egg-name}/tarball/{egg-release}")
|
(uri targz "https://bitbucket.org/thomashintz/{egg-name}/get/{egg-release}.tar.gz")
|
||||||
|
|
||||||
(release "0.0.1")
|
(release "0.0.1")
|
||||||
|
(release "0.1.0")
|
||||||
|
(release "0.1.1")
|
||||||
|
(release "0.1.2")
|
||||||
|
(release "0.1.3")
|
||||||
|
(release "0.1.4")
|
||||||
|
(release "0.1.5")
|
||||||
|
(release "0.1.6")
|
||||||
|
|||||||
313
websockets.scm
313
websockets.scm
@@ -8,50 +8,50 @@
|
|||||||
|
|
||||||
; high level API
|
; high level API
|
||||||
with-websocket with-concurrent-websocket
|
with-websocket with-concurrent-websocket
|
||||||
send-message receive-message
|
send-message receive-message current-websocket
|
||||||
|
|
||||||
; low level API
|
; low level API
|
||||||
send-frame read-frame read-frame-payload
|
;; send-frame read-frame read-frame-payload
|
||||||
receive-fragments valid-utf8?
|
;; receive-fragments valid-utf8?
|
||||||
control-frame? upgrade-to-websocket
|
;; control-frame? upgrade-to-websocket
|
||||||
current-websocket unmask close-websocket
|
;; current-websocket unmask close-websocket
|
||||||
process-fragments
|
;; process-fragments
|
||||||
|
|
||||||
; fragment
|
;; ; fragment
|
||||||
make-fragment fragment? fragment-payload fragment-length
|
;; make-fragment fragment? fragment-payload fragment-length
|
||||||
fragment-masked? fragment-masking-key fragment-last?
|
;; fragment-masked? fragment-masking-key fragment-last?
|
||||||
fragment-optype
|
;; fragment-optype
|
||||||
)
|
)
|
||||||
|
|
||||||
(import chicken scheme data-structures extras ports posix foreign)
|
(import chicken scheme data-structures extras ports posix foreign
|
||||||
(use srfi-1 srfi-4 spiffy intarweb uri-common base64 simple-sha1 srfi-18
|
srfi-13 srfi-14 srfi-18)
|
||||||
srfi-13 miscmacros mailbox)
|
(use srfi-1 srfi-4 spiffy intarweb uri-common base64 simple-sha1
|
||||||
|
mailbox comparse)
|
||||||
; TODO make sure all C operations check args to prevent overflows
|
|
||||||
(foreign-declare "#include \"utf8validator.c\"")
|
|
||||||
|
|
||||||
(define-inline (neq? obj1 obj2) (not (eq? obj1 obj2)))
|
(define-inline (neq? obj1 obj2) (not (eq? obj1 obj2)))
|
||||||
|
|
||||||
(define current-websocket (make-parameter #f))
|
(define current-websocket (make-parameter #f))
|
||||||
(define ping-interval (make-parameter 15))
|
(define ping-interval (make-parameter 15))
|
||||||
(define close-timeout (make-parameter 5))
|
(define close-timeout (make-parameter 5))
|
||||||
(define connection-timeout (make-parameter 58))
|
(define connection-timeout (make-parameter 58)) ; a little grace period from 60s
|
||||||
(define accept-connection (make-parameter (lambda (origin) #t)))
|
(define accept-connection (make-parameter (lambda (origin) #t)))
|
||||||
(define drop-incoming-pings (make-parameter #t))
|
(define drop-incoming-pings (make-parameter #t))
|
||||||
(define propagate-common-errors (make-parameter #f))
|
(define propagate-common-errors (make-parameter #f))
|
||||||
|
(define access-denied ; TODO test
|
||||||
|
(make-parameter (lambda () (send-status 'forbidden "<h1>Access denied</h1>"))))
|
||||||
|
|
||||||
(define max-frame-size (make-parameter 65536)) ; 64KiB
|
(define max-frame-size (make-parameter 1048576)) ; 1MiB
|
||||||
(define max-message-size (make-parameter 1048576)) ; 1MiB
|
(define max-message-size
|
||||||
|
(make-parameter 1048576 ; 1MiB
|
||||||
|
(lambda (v)
|
||||||
|
(if (> v 1073741823) ; max int size for unmask/utf8 check
|
||||||
|
(signal (make-property-condition 'out-of-range))
|
||||||
|
v))))
|
||||||
|
|
||||||
(define (make-websocket-exception . conditions)
|
(define (make-websocket-exception . conditions)
|
||||||
(apply make-composite-condition (append `(,(make-property-condition 'websocket))
|
(apply make-composite-condition (append `(,(make-property-condition 'websocket))
|
||||||
conditions)))
|
conditions)))
|
||||||
|
|
||||||
(define (make-invalid-header-exception type k v)
|
|
||||||
(make-composite-condition (make-websocket-exception
|
|
||||||
(make-property-condition type k v))
|
|
||||||
(make-property-condition 'invalid-header)))
|
|
||||||
|
|
||||||
(define (make-protocol-violation-exception msg)
|
(define (make-protocol-violation-exception msg)
|
||||||
(make-composite-condition (make-property-condition 'websocket)
|
(make-composite-condition (make-property-condition 'websocket)
|
||||||
(make-property-condition 'protocol-error 'msg msg)))
|
(make-property-condition 'protocol-error 'msg msg)))
|
||||||
@@ -74,7 +74,8 @@
|
|||||||
('connection-close 8)
|
('connection-close 8)
|
||||||
('ping 9)
|
('ping 9)
|
||||||
('pong 10)
|
('pong 10)
|
||||||
(else (error "bad optype")))) ; TODO
|
(else (signal (make-websocket-exception
|
||||||
|
(make-property-condition 'invalid-optype))))))
|
||||||
|
|
||||||
(define (control-frame? optype)
|
(define (control-frame? optype)
|
||||||
(or (eq? optype 'ping) (eq? optype 'pong) (eq? optype 'connection-close)))
|
(or (eq? optype 'ping) (eq? optype 'pong) (eq? optype 'connection-close)))
|
||||||
@@ -102,22 +103,11 @@
|
|||||||
fragment?
|
fragment?
|
||||||
(payload fragment-payload)
|
(payload fragment-payload)
|
||||||
(length fragment-length)
|
(length fragment-length)
|
||||||
(masked fragment-masked?)
|
(masked fragment-masked? set-fragment-masked!)
|
||||||
(masking-key fragment-masking-key)
|
(masking-key fragment-masking-key)
|
||||||
(fin fragment-last?)
|
(fin fragment-last?)
|
||||||
(optype fragment-optype))
|
(optype fragment-optype))
|
||||||
|
|
||||||
(define (string->bytes str)
|
|
||||||
(let* ((lst (map char->integer (string->list str)))
|
|
||||||
(bv (make-u8vector (length lst))))
|
|
||||||
(let loop ((lst lst)
|
|
||||||
(pos 0))
|
|
||||||
(if (null? lst) bv
|
|
||||||
(begin
|
|
||||||
(u8vector-set! bv pos (car lst))
|
|
||||||
(loop (cdr lst) (+ pos 1)))))))
|
|
||||||
|
|
||||||
|
|
||||||
(define (hex-string->string hexstr)
|
(define (hex-string->string hexstr)
|
||||||
;; convert a string like "a745ff12" to a string
|
;; convert a string like "a745ff12" to a string
|
||||||
(let ((result (make-string (/ (string-length hexstr) 2))))
|
(let ((result (make-string (/ (string-length hexstr) 2))))
|
||||||
@@ -176,10 +166,12 @@
|
|||||||
outbound-port)
|
outbound-port)
|
||||||
|
|
||||||
(write-string data len outbound-port)
|
(write-string data len outbound-port)
|
||||||
|
(flush-output outbound-port)
|
||||||
#t))
|
#t))
|
||||||
|
|
||||||
(define (send-message optype #!optional (data "") (ws (current-websocket)))
|
(define (send-message data #!optional (optype 'text) (ws (current-websocket)))
|
||||||
;; TODO break up large data into multiple frames?
|
;; TODO break up large data into multiple frames?
|
||||||
|
(optype->opcode optype) ; triggers error if invalid
|
||||||
(dynamic-wind
|
(dynamic-wind
|
||||||
(lambda () (mutex-lock! (websocket-send-mutex ws)))
|
(lambda () (mutex-lock! (websocket-send-mutex ws)))
|
||||||
(lambda () (send-frame ws optype data #t))
|
(lambda () (send-frame ws optype data #t))
|
||||||
@@ -191,84 +183,44 @@
|
|||||||
(u8vector-set! tmaskkey 1 (vector-ref frame-masking-key 1))
|
(u8vector-set! tmaskkey 1 (vector-ref frame-masking-key 1))
|
||||||
(u8vector-set! tmaskkey 2 (vector-ref frame-masking-key 2))
|
(u8vector-set! tmaskkey 2 (vector-ref frame-masking-key 2))
|
||||||
(u8vector-set! tmaskkey 3 (vector-ref frame-masking-key 3))
|
(u8vector-set! tmaskkey 3 (vector-ref frame-masking-key 3))
|
||||||
(define-external wsmaskkey blob (u8vector->blob/shared tmaskkey))
|
|
||||||
|
|
||||||
(define-external wslen int len)
|
((foreign-lambda* void ((blob wsmaskkey) (size_t wslen) (scheme-pointer wsv))
|
||||||
|
|
||||||
(define-external wsv scheme-pointer payload)
|
|
||||||
((foreign-lambda* void ()
|
|
||||||
"
|
"
|
||||||
const unsigned char* maskkey2 = wsmaskkey;
|
const unsigned char* maskkey2 = wsmaskkey;
|
||||||
const unsigned int kd = *(unsigned int*)maskkey2;
|
const unsigned int kd = *(unsigned int*)maskkey2;
|
||||||
const unsigned char* __restrict kb = maskkey2;
|
const unsigned char* __restrict kb = maskkey2;
|
||||||
|
|
||||||
|
|
||||||
for (int i = wslen >> 2; i != 0; --i)
|
size_t i;
|
||||||
|
for (i = wslen >> 2; i != 0; --i)
|
||||||
{
|
{
|
||||||
*((unsigned int*)wsv) ^= kd;
|
*((unsigned int*)wsv) ^= kd;
|
||||||
wsv += 4;
|
wsv += 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int rem = wslen & 3;
|
const size_t rem = wslen & 3;
|
||||||
for (int i = 0; i < rem; ++i)
|
for (i = 0; i < rem; ++i)
|
||||||
{
|
{
|
||||||
*((unsigned int*)wsv++) ^= kb[i];
|
*((unsigned int*)wsv++) ^= kb[i];
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
))
|
) (u8vector->blob/shared tmaskkey) len payload)
|
||||||
payload)
|
payload)
|
||||||
|
|
||||||
(define (unmask fragment)
|
(define (unmask fragment)
|
||||||
(if (fragment-masked? fragment)
|
(if (fragment-masked? fragment)
|
||||||
(websocket-unmask-frame-payload
|
(let ((r (websocket-unmask-frame-payload
|
||||||
(fragment-payload fragment)
|
(fragment-payload fragment)
|
||||||
(fragment-length fragment)
|
(fragment-length fragment)
|
||||||
(fragment-masking-key fragment))
|
(fragment-masking-key fragment))))
|
||||||
|
(set-fragment-masked! fragment #f)
|
||||||
|
r)
|
||||||
(fragment-payload fragment)))
|
(fragment-payload fragment)))
|
||||||
|
|
||||||
(define (read-frame-payload inbound-port frame-payload-length)
|
(define (read-frame-payload inbound-port frame-payload-length)
|
||||||
(let ((masked-data (make-string frame-payload-length)))
|
(let ((masked-data (make-string frame-payload-length)))
|
||||||
(read-string! frame-payload-length masked-data inbound-port)
|
(read-string! frame-payload-length masked-data inbound-port)
|
||||||
masked-data)
|
masked-data))
|
||||||
;; (let* ((masked-data (make-string frame-payload-length)))
|
|
||||||
;; (read-string! frame-payload-length masked-data inbound-port)
|
|
||||||
|
|
||||||
;; (define tmaskkey (make-u8vector 4 #f #t #t))
|
|
||||||
;; (u8vector-set! tmaskkey 0 (vector-ref frame-masking-key 0))
|
|
||||||
;; (u8vector-set! tmaskkey 1 (vector-ref frame-masking-key 1))
|
|
||||||
;; (u8vector-set! tmaskkey 2 (vector-ref frame-masking-key 2))
|
|
||||||
;; (u8vector-set! tmaskkey 3 (vector-ref frame-masking-key 3))
|
|
||||||
;; (define-external wsmaskkey blob (u8vector->blob/shared tmaskkey))
|
|
||||||
|
|
||||||
;; (define-external wslen int frame-payload-length)
|
|
||||||
|
|
||||||
;; (define-external wsv scheme-pointer masked-data)
|
|
||||||
|
|
||||||
;; (if frame-masked
|
|
||||||
;; (begin
|
|
||||||
;; ((foreign-lambda* void ()
|
|
||||||
;; "
|
|
||||||
;; const unsigned char* maskkey2 = wsmaskkey;
|
|
||||||
;; const unsigned int kd = *(unsigned int*)maskkey2;
|
|
||||||
;; const unsigned char* __restrict kb = maskkey2;
|
|
||||||
|
|
||||||
|
|
||||||
;; for (int i = wslen >> 2; i != 0; --i)
|
|
||||||
;; {
|
|
||||||
;; *((unsigned int*)wsv) ^= kd;
|
|
||||||
;; wsv += 4;
|
|
||||||
;; }
|
|
||||||
|
|
||||||
;; const int rem = wslen & 3;
|
|
||||||
;; for (int i = 0; i < rem; ++i)
|
|
||||||
;; {
|
|
||||||
;; *((unsigned int*)wsv++) ^= kb[i];
|
|
||||||
;; }
|
|
||||||
;; "
|
|
||||||
;; ))
|
|
||||||
;; masked-data)
|
|
||||||
;; masked-data))
|
|
||||||
)
|
|
||||||
|
|
||||||
(define (read-frame total-size ws)
|
(define (read-frame total-size ws)
|
||||||
(let* ((inbound-port (websocket-inbound-port ws))
|
(let* ((inbound-port (websocket-inbound-port ws))
|
||||||
@@ -322,67 +274,40 @@
|
|||||||
(read-frame-payload inbound-port frame-payload-length)
|
(read-frame-payload inbound-port frame-payload-length)
|
||||||
frame-payload-length frame-masked
|
frame-payload-length frame-masked
|
||||||
frame-masking-key frame-fin frame-optype))
|
frame-masking-key frame-fin frame-optype))
|
||||||
((eq? frame-optype 'connection-close)
|
((eq? frame-optype 'connection-close) ; TODO, same as above?
|
||||||
(make-fragment
|
(make-fragment
|
||||||
(read-frame-payload inbound-port frame-payload-length)
|
(read-frame-payload inbound-port frame-payload-length)
|
||||||
frame-payload-length frame-masked frame-masking-key
|
frame-payload-length frame-masked frame-masking-key
|
||||||
frame-fin frame-optype))
|
frame-fin frame-optype))
|
||||||
(else
|
(else
|
||||||
(thread-signal! (websocket-user-thread ws)
|
|
||||||
(make-websocket-exception
|
|
||||||
(make-property-condition 'unhandled-opcode
|
|
||||||
'optype frame-optype)))
|
|
||||||
(signal (make-websocket-exception
|
(signal (make-websocket-exception
|
||||||
(make-property-condition 'unhandled-opcode
|
(make-property-condition 'unhandled-optype
|
||||||
'optype frame-optype)))))))))))
|
'optype frame-optype)))))))))))
|
||||||
|
|
||||||
(define (valid-utf8-2? s)
|
(include "utf8-grammar.scm")
|
||||||
(define-external str c-string s)
|
|
||||||
(define-external len int (string-length s))
|
|
||||||
(zero?
|
|
||||||
((foreign-lambda* int ()
|
|
||||||
"
|
|
||||||
static const uint8_t utf8d[] = {
|
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
|
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
|
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
|
|
||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
|
|
||||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
|
|
||||||
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
|
|
||||||
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
|
|
||||||
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
|
|
||||||
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
|
|
||||||
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
|
|
||||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
|
|
||||||
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
|
|
||||||
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
|
|
||||||
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
|
|
||||||
};
|
|
||||||
|
|
||||||
uint32_t si;
|
|
||||||
uint32_t *state;
|
|
||||||
si = 0;
|
|
||||||
state = &si;
|
|
||||||
uint32_t type;
|
|
||||||
|
|
||||||
for (int i = 0; i < len; i++) {
|
|
||||||
// type = utf8d[(uint8_t)str[i]];
|
|
||||||
type = utf8d[*((uint8_t*)str)];
|
|
||||||
*state = utf8d[256 + (*state) * 16 + type];
|
|
||||||
|
|
||||||
if (*state != 0) // reject
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
C_return(*state);
|
|
||||||
"
|
|
||||||
))
|
|
||||||
))
|
|
||||||
|
|
||||||
(define (valid-utf8? s)
|
(define (valid-utf8? s)
|
||||||
(let ((len (string-length s)))
|
(or (let ((len (string-length s)))
|
||||||
((foreign-lambda int "utf8_valid" scheme-pointer int)
|
; Try to validate as an ascii string first. Its essentially
|
||||||
s len)))
|
; free, doesn't generate garbage and is many, many times
|
||||||
|
; faster than the general purpose validator.
|
||||||
|
(= 1
|
||||||
|
((foreign-lambda* int ((size_t ws_utlen) (scheme-pointer ws_uts))
|
||||||
|
"
|
||||||
|
if (ws_utlen > UINT_MAX) { return -1; }
|
||||||
|
|
||||||
|
int i;
|
||||||
|
for (i = ws_utlen; i != 0; --i)
|
||||||
|
{
|
||||||
|
if (*((unsigned char*)ws_uts++) > 127)
|
||||||
|
{
|
||||||
|
C_return(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
C_return(1);
|
||||||
|
") len s)))
|
||||||
|
(parse utf8-string (->parser-input s))))
|
||||||
|
|
||||||
(define (close-code->integer s)
|
(define (close-code->integer s)
|
||||||
(if (string-null? s)
|
(if (string-null? s)
|
||||||
@@ -452,7 +377,7 @@ static const uint8_t utf8d[] = {
|
|||||||
; immediate response
|
; immediate response
|
||||||
((and (eq? optype 'ping) last-frame (<= len 125))
|
((and (eq? optype 'ping) last-frame (<= len 125))
|
||||||
(unless (drop-incoming-pings)
|
(unless (drop-incoming-pings)
|
||||||
(send-message 'pong (unmask fragment)))
|
(send-message (unmask fragment) 'pong))
|
||||||
(loop fragments first type total-size))
|
(loop fragments first type total-size))
|
||||||
|
|
||||||
; protocol violation checks
|
; protocol violation checks
|
||||||
@@ -480,8 +405,13 @@ static const uint8_t utf8d[] = {
|
|||||||
(define (process-fragments fragments optype #!optional (ws (current-websocket)))
|
(define (process-fragments fragments optype #!optional (ws (current-websocket)))
|
||||||
(let ((message-body (string-concatenate/shared
|
(let ((message-body (string-concatenate/shared
|
||||||
(reverse (map unmask fragments)))))
|
(reverse (map unmask fragments)))))
|
||||||
(when (and (eq? optype 'text)
|
(when (and (or (eq? optype 'text) (eq? optype 'connection-close))
|
||||||
(not (valid-utf8? message-body)))
|
(not (valid-utf8?
|
||||||
|
(if (eq? optype 'text)
|
||||||
|
message-body
|
||||||
|
(if (> (string-length message-body) 2)
|
||||||
|
(substring message-body 2)
|
||||||
|
"")))))
|
||||||
(set-websocket-state! ws 'error)
|
(set-websocket-state! ws 'error)
|
||||||
(signal (make-websocket-exception
|
(signal (make-websocket-exception
|
||||||
(make-property-condition
|
(make-property-condition
|
||||||
@@ -497,6 +427,7 @@ static const uint8_t utf8d[] = {
|
|||||||
(values #!eof optype)
|
(values #!eof optype)
|
||||||
(process-fragments fragments optype)))))
|
(process-fragments fragments optype)))))
|
||||||
|
|
||||||
|
; TODO does #!optional and #!key work together?
|
||||||
(define (close-websocket #!optional (ws (current-websocket))
|
(define (close-websocket #!optional (ws (current-websocket))
|
||||||
#!key (close-reason 'normal) (data (make-u8vector 0)))
|
#!key (close-reason 'normal) (data (make-u8vector 0)))
|
||||||
(define invalid-close-reason #f)
|
(define invalid-close-reason #f)
|
||||||
@@ -525,11 +456,15 @@ static const uint8_t utf8d[] = {
|
|||||||
#t)
|
#t)
|
||||||
(let loop ()
|
(let loop ()
|
||||||
(receive (data type) (receive-message ws)
|
(receive (data type) (receive-message ws)
|
||||||
(unless (eq? type 'connection-close) (loop)))))
|
(if (eq? type 'connection-close)
|
||||||
|
(unless (valid-utf8? data)
|
||||||
|
(set! close-reason 'invalid-data))
|
||||||
|
(loop)))))
|
||||||
(begin
|
(begin
|
||||||
(send-frame ws 'connection-close
|
(send-frame ws 'connection-close
|
||||||
(u8vector 3 (close-reason->close-code close-reason))
|
(u8vector 3 (close-reason->close-code close-reason))
|
||||||
#t)))))))
|
#t))))
|
||||||
|
"close timeout thread")))
|
||||||
(thread-start! close-thread)
|
(thread-start! close-thread)
|
||||||
(if (> (close-timeout) 0)
|
(if (> (close-timeout) 0)
|
||||||
(unless (thread-join! close-thread (close-timeout) #f)
|
(unless (thread-join! close-thread (close-timeout) #f)
|
||||||
@@ -538,8 +473,7 @@ static const uint8_t utf8d[] = {
|
|||||||
;; (make-websocket-exception
|
;; (make-websocket-exception
|
||||||
;; (make-property-condition 'close-timeout)))
|
;; (make-property-condition 'close-timeout)))
|
||||||
)
|
)
|
||||||
(thread-join! close-thread))
|
(thread-join! close-thread))))
|
||||||
(log-to (error-log) "closed")))
|
|
||||||
|
|
||||||
|
|
||||||
(define (sha1-sum in-bv)
|
(define (sha1-sum in-bv)
|
||||||
@@ -547,7 +481,6 @@ static const uint8_t utf8d[] = {
|
|||||||
|
|
||||||
(define (websocket-compute-handshake client-key)
|
(define (websocket-compute-handshake client-key)
|
||||||
(let* ((key-and-magic
|
(let* ((key-and-magic
|
||||||
; TODO generate new, randome, secure key every time
|
|
||||||
(string-append client-key "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
|
(string-append client-key "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
|
||||||
(key-and-magic-sha1 (sha1-sum key-and-magic)))
|
(key-and-magic-sha1 (sha1-sum key-and-magic)))
|
||||||
(base64-encode key-and-magic-sha1)))
|
(base64-encode key-and-magic-sha1)))
|
||||||
@@ -583,21 +516,22 @@ static const uint8_t utf8d[] = {
|
|||||||
(lambda ()
|
(lambda ()
|
||||||
(let loop ()
|
(let loop ()
|
||||||
(thread-sleep! (ping-interval))
|
(thread-sleep! (ping-interval))
|
||||||
(send-message 'ping "" ws)
|
(when (eq? (websocket-state ws) 'open)
|
||||||
(loop))))))
|
(send-message "" 'ping ws)
|
||||||
|
(loop))))
|
||||||
|
"ping thread")))
|
||||||
|
|
||||||
; make sure the request meets the spec for websockets
|
; make sure the request meets the spec for websockets
|
||||||
(cond ((not (and (eq? (header-value 'connection headers #f) 'upgrade)
|
(cond ((not (and (member 'upgrade (header-values 'connection headers))
|
||||||
(string-ci= (car (header-value 'upgrade headers '(""))) "websocket")))
|
(string-ci= (car (header-value 'upgrade headers '(""))) "websocket")))
|
||||||
(signal (make-invalid-header-exception 'upgrade 'value
|
(signal (make-websocket-exception
|
||||||
(header-value 'upgrade headers #f))))
|
(make-property-condition 'missing-upgrade-header))))
|
||||||
((not (string= (header-value 'sec-websocket-version headers "") "13"))
|
((not (string= (header-value 'sec-websocket-version headers "") "13"))
|
||||||
(signal (make-invalid-header-exception
|
(with-headers ; TODO test
|
||||||
'websocket-version 'version
|
`((sec-websocket-version "13"))
|
||||||
(header-value 'sec-websocket-version headers #f))))
|
(lambda () (send-status 'upgrade-required))))
|
||||||
((not ((accept-connection) (header-value 'origin headers "")))
|
((not ((accept-connection) (header-value 'origin headers "")))
|
||||||
(signal (make-invalid-header-exception 'origin 'value
|
((access-denied))))
|
||||||
(header-value 'origin headers #f)))))
|
|
||||||
|
|
||||||
(with-headers
|
(with-headers
|
||||||
`((upgrade ("WebSocket" . #f))
|
`((upgrade ("WebSocket" . #f))
|
||||||
@@ -616,14 +550,16 @@ static const uint8_t utf8d[] = {
|
|||||||
; Add one to attempt to alleviate checking the timestamp
|
; Add one to attempt to alleviate checking the timestamp
|
||||||
; right before when the timeout should happen.
|
; right before when the timeout should happen.
|
||||||
(thread-sleep! (+ 1 (connection-timeout)))
|
(thread-sleep! (+ 1 (connection-timeout)))
|
||||||
|
(when (eq? (websocket-state ws) 'open)
|
||||||
(if (< (- (time->seconds (current-time))
|
(if (< (- (time->seconds (current-time))
|
||||||
(time->seconds (websocket-last-message-timestamp ws)))
|
(time->seconds (websocket-last-message-timestamp ws)))
|
||||||
(connection-timeout))
|
(connection-timeout))
|
||||||
(loop)
|
(loop)
|
||||||
(begin (thread-signal! (websocket-user-thread ws)
|
(begin (thread-signal!
|
||||||
|
(websocket-user-thread ws)
|
||||||
(make-websocket-exception
|
(make-websocket-exception
|
||||||
(make-property-condition 'connection-timeout)))
|
(make-property-condition 'connection-timeout)))
|
||||||
(close-websocket ws close-reason: 1001))))))))
|
(close-websocket ws close-reason: 'going-away)))))))))
|
||||||
|
|
||||||
(when (> (ping-interval) 0)
|
(when (> (ping-interval) 0)
|
||||||
(thread-start! ping-thread))
|
(thread-start! ping-thread))
|
||||||
@@ -631,6 +567,15 @@ static const uint8_t utf8d[] = {
|
|||||||
ws))
|
ws))
|
||||||
|
|
||||||
(define (with-websocket proc #!optional (concurrent #f))
|
(define (with-websocket proc #!optional (concurrent #f))
|
||||||
|
(define (handle-error close-reason exn)
|
||||||
|
(set-websocket-state! (current-websocket) 'closing)
|
||||||
|
(close-websocket (current-websocket) close-reason: close-reason)
|
||||||
|
(unless (port-closed? (request-port (current-request)))
|
||||||
|
(close-input-port (request-port (current-request))))
|
||||||
|
(unless (port-closed? (response-port (current-response)))
|
||||||
|
(close-output-port (response-port (current-response))))
|
||||||
|
(when (propagate-common-errors)
|
||||||
|
(signal exn)))
|
||||||
(parameterize
|
(parameterize
|
||||||
((current-websocket (websocket-accept concurrent)))
|
((current-websocket (websocket-accept concurrent)))
|
||||||
(condition-case
|
(condition-case
|
||||||
@@ -638,49 +583,11 @@ static const uint8_t utf8d[] = {
|
|||||||
(close-websocket)
|
(close-websocket)
|
||||||
(close-input-port (request-port (current-request)))
|
(close-input-port (request-port (current-request)))
|
||||||
(close-output-port (response-port (current-response))))
|
(close-output-port (response-port (current-response))))
|
||||||
(exn (websocket protocol-error)
|
(exn (websocket protocol-error) (handle-error 'protocol-error exn))
|
||||||
(set-websocket-state! (current-websocket) 'closing)
|
(exn (websocket invalid-data) (handle-error 'invalid-data exn))
|
||||||
(close-websocket (current-websocket) close-reason: 'protocol-error)
|
(exn (websocket connection-timeout) (handle-error 'going-away exn))
|
||||||
(unless (port-closed? (request-port (current-request)))
|
(exn (websocket message-too-large) (handle-error 'message-too-large exn))
|
||||||
(close-input-port (request-port (current-request))))
|
(exn () (handle-error 'unexpected-error exn)))))
|
||||||
(unless (port-closed? (response-port (current-response)))
|
|
||||||
(close-output-port (response-port (current-response))))
|
|
||||||
(when (propagate-common-errors)
|
|
||||||
(signal exn)))
|
|
||||||
(exn (websocket invalid-data)
|
|
||||||
(set-websocket-state! (current-websocket) 'closing)
|
|
||||||
(close-websocket (current-websocket) close-reason: 'invalid-data)
|
|
||||||
(unless (port-closed? (request-port (current-request)))
|
|
||||||
(close-input-port (request-port (current-request))))
|
|
||||||
(unless (port-closed? (response-port (current-response)))
|
|
||||||
(close-output-port (response-port (current-response))))
|
|
||||||
(when (propagate-common-errors)
|
|
||||||
(signal exn)))
|
|
||||||
(exn (websocket connection-timeout)
|
|
||||||
(set-websocket-state! (current-websocket) 'closing)
|
|
||||||
(close-websocket (current-websocket) close-reason: 'going-away)
|
|
||||||
(unless (port-closed? (request-port (current-request)))
|
|
||||||
(close-input-port (request-port (current-request))))
|
|
||||||
(unless (port-closed? (response-port (current-response)))
|
|
||||||
(close-output-port (response-port (current-response))))
|
|
||||||
(when (propagate-common-errors)
|
|
||||||
(signal exn)))
|
|
||||||
(exn (websocket message-too-large)
|
|
||||||
(set-websocket-state! (current-websocket) 'closing)
|
|
||||||
(close-websocket (current-websocket) close-reason: 'message-too-large)
|
|
||||||
(unless (port-closed? (request-port (current-request)))
|
|
||||||
(close-input-port (request-port (current-request))))
|
|
||||||
(unless (port-closed? (response-port (current-response)))
|
|
||||||
(close-output-port (response-port (current-response))))
|
|
||||||
(when (propagate-common-errors)
|
|
||||||
(signal exn)))
|
|
||||||
(exn ()
|
|
||||||
(close-websocket (current-websocket) close-reason: 1011)
|
|
||||||
(unless (port-closed? (request-port (current-request)))
|
|
||||||
(close-input-port (request-port (current-request))))
|
|
||||||
(unless (port-closed? (response-port (current-response)))
|
|
||||||
(close-output-port (response-port (current-response))))
|
|
||||||
(signal (make-websocket-exception (make-property-condition 'unexpected-error)))))))
|
|
||||||
|
|
||||||
(define (with-concurrent-websocket proc)
|
(define (with-concurrent-websocket proc)
|
||||||
(let ((parent-thread (current-thread)))
|
(let ((parent-thread (current-thread)))
|
||||||
|
|||||||
@@ -6,4 +6,4 @@
|
|||||||
|
|
||||||
(install-extension 'websockets
|
(install-extension 'websockets
|
||||||
'("websockets.so" "websockets.import.so")
|
'("websockets.so" "websockets.import.so")
|
||||||
`((version "0.0.1")))
|
`((version "0.1.6")))
|
||||||
|
|||||||
Reference in New Issue
Block a user