├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── echo-server.lisp ├── run-client-tests.sh ├── run-tests.sh ├── src ├── client.lisp ├── driver.lisp ├── server.lisp ├── util.lisp └── ws │ ├── base.lisp │ ├── client.lisp │ └── server.lisp ├── t ├── client.js ├── client.lisp ├── install-libfixposix.sh ├── install-libuv.sh └── server.js ├── websocket-driver-base.asd ├── websocket-driver-client.asd ├── websocket-driver-server.asd └── websocket-driver.asd /.gitignore: -------------------------------------------------------------------------------- 1 | *.fasl 2 | *.dx32fsl 3 | *.dx64fsl 4 | *.lx32fsl 5 | *.lx64fsl 6 | *.x86f 7 | *~ 8 | .#* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | 5 | sudo: false 6 | 7 | env: 8 | global: 9 | - PATH=~/.roswell/bin:$PATH 10 | - LD_LIBRARY_PATH=$HOME/libuv/lib:$HOME/libfixposix/lib:$LD_LIBRARY_PATH 11 | - C_INCLUDE_PATH=$HOME/libuv/include:$HOME/libfixposix/include:$C_INCLUDE_PATH 12 | - CPLUS_INCLUDE_PATH=$C_INCLUDE_PATH 13 | - ROSWELL_BRANCH=release 14 | - ROSWELL_INSTALL_DIR=$HOME/.roswell 15 | matrix: 16 | - LISP=sbcl-bin 17 | - LISP=ccl-bin 18 | 19 | install: 20 | # libuv for Wookie 21 | - if ! [ -f "$HOME/libuv/include/uv.h" ]; then ./t/install-libuv.sh; fi 22 | # libfixposix for IOLib 23 | - if ! [ -f "$HOME/libfixposix/include/lfp.h" ]; then ./t/install-libfixposix.sh; fi 24 | # Install Roswell 25 | - if ! $(which ros >/dev/null); then 26 | curl -L https://raw.githubusercontent.com/roswell/roswell/$ROSWELL_BRANCH/scripts/install-for-ci.sh | sh; 27 | fi 28 | - npm install ws 29 | - ros install fukamachi/clack 30 | 31 | script: 32 | - CLACK_HANDLER=wookie ./run-tests.sh 33 | - CLACK_HANDLER=hunchentoot ./run-tests.sh 34 | - ./run-client-tests.sh 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Eitaro Fukamachi 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Websocket Driver 2 | 3 | [](https://travis-ci.org/fukamachi/websocket-driver) 4 | [](http://quickdocs.org/websocket-driver/) 5 | 6 | This library provides WebSocket server & client implementation for Common Lisp. 7 | 8 | ## Supported Servers 9 | 10 | * [Hunchentoot](http://weitz.de/hunchentoot/) 11 | * [Wookie](http://wookie.lyonbros.com) 12 | * [Woo](https://github.com/fukamachi/woo) 13 | 14 | ## Usage 15 | 16 | ### Server-side with Clack 17 | 18 | WebSocket server implementation is designed to work with [Clack](https://github.com/fukamachi/clack), which is a abstraction layer for web servers. 19 | 20 | ```common-lisp 21 | (ql:quickload '(:websocket-driver-server :clack)) 22 | 23 | (use-package :websocket-driver) 24 | 25 | (defvar *echo-server* 26 | (lambda (env) 27 | (let ((ws (make-server env))) 28 | (on :message ws 29 | (lambda (message) 30 | (send ws message))) 31 | (lambda (responder) 32 | (declare (ignore responder)) 33 | (start-connection ws))))) 34 | 35 | ;; Start Wookie server 36 | (clack:clackup *echo-server* :server :wookie :port 5000) 37 | ``` 38 | 39 | The backend server can be changed by replacing `:wookie` by other servers. 40 | 41 | ### Client-side 42 | 43 | ```common-lisp 44 | (ql:quickload :websocket-driver-client) 45 | 46 | (defvar *client* (wsd:make-client "ws://localhost:5000/echo")) 47 | 48 | (wsd:start-connection *client*) 49 | (wsd:on :message *client* 50 | (lambda (message) 51 | (format t "~&Got: ~A~%" message))) 52 | (wsd:send *client* "Hi") 53 | (wsd:close-connection *client*) 54 | ``` 55 | 56 | ## APIs 57 | 58 | ### \[Function] make-server (env &key max-length accept-protocols additional-headers) 59 | 60 | Returns a new `SERVER` object. The `ENV` is a property list represents server information, which [Clack](http://clacklisp.org) provides. 61 | 62 | The `max-length` is the maximum message size allowed. The default is `#x3ffffff`. If at any time it stays bigger than this, the connection will be closed with code 1009 (too-large). 63 | 64 | The `accept-protocols` is a list of custom protocol names as strings. This will be used for checking `Sec-WebSocket-Protocol` header client sent. The default is an empty list. 65 | 66 | The `additional-headers` is an association list which represents HTTP headers to use in WebSocket handshake response. The default is an empty list. 67 | 68 | ### \[Function] make-client (url &key max-length accept-protocols additional-headers) 69 | 70 | Returns a new `CLIENT` object. The `URL` is a string to connect. 71 | 72 | Additional keyword arguments `max-length`, `accept-protocols` and `additional-headers` are shared with `make-server`. 73 | 74 | ### \[Class] ws 75 | 76 | The base class for `server` and `client`. 77 | 78 | As this inherits `event-emitter`, its object can be attached event listerners by `on`. 79 | 80 | #### \[Event] :open 81 | 82 | Called when the socket becomes open. 83 | 84 | ```common-lisp 85 | (on :open ws 86 | (lambda () 87 | (format t "Connected.~%"))) 88 | ``` 89 | 90 | #### \[Event] :message 91 | 92 | Called when a message is received. The callback function takes a `MESSAGE` as an argument which is either a string in the case of a text message or an `(UNSIGNED-BYTE 8)` vector in the case of a binary message. 93 | 94 | ```common-lisp 95 | (on :message ws 96 | (lambda (message) 97 | (format t "Received: ~S~%" message))) 98 | ``` 99 | 100 | #### \[Event] :error 101 | 102 | Called when a protocol error occurs due to the other peer sending an invalid byte sequence. The callback function takes a `PROTOCOL-ERROR` as an argument. 103 | 104 | ```common-lisp 105 | (on :error ws 106 | (lambda (error) 107 | (format t "Got an error: ~S~%" error))) 108 | ``` 109 | 110 | #### \[Event] :close 111 | 112 | Called when the socket becomes closed. The `CALLBACK` function takes `CODE` and `REASON` as arguments. 113 | 114 | ```common-lisp 115 | (on :close ws 116 | (lambda (&key code reason) 117 | (format t "Closed because '~A' (Code=~A)~%" reason code))) 118 | ``` 119 | 120 | ### \[Class] server 121 | 122 | The class for WebSocket (version 13) server implementation. 123 | 124 | ### \[Class] client 125 | 126 | The class for WebSocket client implementation. 127 | 128 | ### \[Method] `(start-connection ws)` 129 | 130 | Initiates the protocol by sending the handshake - either the response for a server-side driver or the request for a client-side one. This should be the first method you invoke. Returns `T` if a handshake was sent. 131 | 132 | ### \[Method] `(send ws data &key start end type code callback)` 133 | 134 | Sends `DATA` over the socket. 135 | 136 | ### \[Function] `(send-text ws message &key start end callback)` 137 | 138 | Sends a text message over the socket. 139 | 140 | ### \[Function] `(send-binary ws usb8-vector &key start end callback)` 141 | 142 | Takes an `(UNSIGNED-BYTE 8)` vector and sends them as a binary message. 143 | 144 | ### \[Method] `(send-ping ws &optional message callback)` 145 | 146 | Sends a ping frame over the socket, queueing it if necessary. 147 | 148 | ### \[Method] `(close-connection ws)` 149 | 150 | Initiates the closing handshake if the socket is still open. 151 | 152 | ### \[Method] `(version driver)` 153 | 154 | Returns the WebSocket version in use as a string (ex. "hybi-13"). 155 | 156 | ### \[Method] `(protocol driver)` 157 | 158 | Returns a string containing the selected subprotocol, if any was agreed upon using the `Sec-WebSocket-Protocol` mechanism. 159 | 160 | ### \[Method] `(ready-state ws)` 161 | 162 | Returns the connection state as a keyword, which is one of `:connecting`, `:open`, `:closing` and `:closed`. 163 | 164 | ## Author 165 | 166 | * Eitaro Fukamachi (e.arrows@gmail.com) 167 | 168 | ## Copyright 169 | 170 | Copyright (c) 2014 Eitaro Fukamachi (e.arrows@gmail.com) 171 | 172 | ## License 173 | 174 | Licensed under the BSD 2-Clause License. 175 | 176 | ## See Also 177 | 178 | * [Clack](http://clacklisp.org) 179 | * [Event Emitter](https://github.com/fukamachi/event-emitter) 180 | -------------------------------------------------------------------------------- /examples/echo-server.lisp: -------------------------------------------------------------------------------- 1 | (ql:quickload '(:websocket-driver :clack) :silent t) 2 | 3 | (in-package :cl-user) 4 | (defpackage websocket-test 5 | (:use :cl 6 | :websocket-driver)) 7 | (in-package :websocket-test) 8 | 9 | (defparameter *handler* 10 | (intern (string-upcase (or (asdf::getenv "CLACK_HANDLER") "hunchentoot")) :keyword)) 11 | 12 | (defvar *app* 13 | (lambda (env) 14 | (cond 15 | ((string= "/echo" (getf env :request-uri)) 16 | (let ((ws (make-server env))) 17 | (on :message ws 18 | (lambda (message) 19 | (send ws message))) 20 | (lambda (responder) 21 | (declare (ignore responder)) 22 | (start-connection ws)))) 23 | (T 24 | '(200 (:content-type "text/html") 25 | (" 26 |
27 | 37 | 38 | 39 | Open JavaScript console. 40 | 41 | 42 | ")))))) 43 | 44 | (clack:clackup *app* :server *handler* :use-thread nil) 45 | -------------------------------------------------------------------------------- /run-client-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | node t/server.js & 4 | SERVER_PID=$! 5 | 6 | while true; do 7 | nc -z 127.0.0.1 5000 >/dev/null 2>&1 && break 8 | sleep 1 9 | done 10 | 11 | RESULT=$(ros -l websocket-driver-client.asd -l t/client.lisp 2>/dev/null) 12 | echo "$RESULT" 13 | 14 | kill -HUP "$SERVER_PID" 15 | 16 | if [ "$RESULT" != "ok" ]; then 17 | exit 1; 18 | fi 19 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ros -l websocket-driver-server.asd -l examples/echo-server.lisp & 4 | SERVER_PID=$! 5 | 6 | count=0 7 | while true; do 8 | nc -z 127.0.0.1 5000 >/dev/null 2>&1 && break 9 | sleep 1 10 | count=$(( count + 1 )) 11 | if [ 180 -lt $count ]; then 12 | echo "Too long time took to start a server." 13 | exit 1; 14 | fi 15 | done 16 | 17 | RESULT=$(node t/client.js) 18 | echo "$RESULT" 19 | 20 | kill -HUP "$SERVER_PID" 21 | 22 | if [ "$RESULT" != "ok" ]; then 23 | exit 1; 24 | fi 25 | -------------------------------------------------------------------------------- /src/client.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | (defpackage websocket-driver-client 3 | (:use :cl) 4 | (:import-from :websocket-driver.ws.client 5 | #:client) 6 | (:export #:make-client)) 7 | (in-package :websocket-driver-client) 8 | 9 | (defun make-client (url &rest options &key max-length accept-protocols additional-headers) 10 | (declare (ignore max-length accept-protocols additional-headers)) 11 | (apply #'make-instance 'client 12 | :url url 13 | options)) 14 | 15 | (import 'make-client :websocket-driver) 16 | (export 'make-client :websocket-driver) 17 | -------------------------------------------------------------------------------- /src/driver.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | (uiop:define-package websocket-driver 3 | (:nicknames :wsd) 4 | (:use :cl) 5 | (:import-from :websocket-driver.ws.base 6 | #:ws 7 | #:socket 8 | #:additional-headers 9 | #:accept-protocols 10 | #:protocol 11 | #:version 12 | #:ready-state 13 | 14 | #:start-connection 15 | #:send 16 | #:send-text 17 | #:send-binary 18 | #:send-ping 19 | #:close-connection) 20 | (:import-from #:websocket-driver.util 21 | #:split-by-comma) 22 | (:import-from #:event-emitter 23 | #:listeners 24 | #:listener-count 25 | #:add-listener 26 | #:on 27 | #:once 28 | #:remove-listener 29 | #:remove-all-listeners 30 | #:emit) 31 | (:export #:websocket-p 32 | 33 | ;; from ws 34 | #:ws 35 | #:socket 36 | #:additional-headers 37 | #:accept-protocols 38 | #:protocol 39 | #:version 40 | #:ready-state 41 | 42 | #:start-connection 43 | #:send 44 | #:send-text 45 | #:send-binary 46 | #:send-ping 47 | #:close-connection 48 | 49 | ;; from event-emitter 50 | #:listeners 51 | #:listener-count 52 | #:add-listener 53 | #:on 54 | #:once 55 | #:remove-listener 56 | #:remove-all-listeners 57 | #:emit)) 58 | (in-package :websocket-driver) 59 | 60 | (defun websocket-p (env) 61 | (let ((headers (getf env :headers))) 62 | (and (eq (getf env :request-method) :get) 63 | (search "upgrade" (gethash "connection" headers "") :test 'equalp) 64 | (string-equal (gethash "upgrade" headers "") "websocket") 65 | (let ((ws-version (gethash "sec-websocket-version" headers))) 66 | (typecase ws-version 67 | (string (find "13" (split-by-comma ws-version) :test #'string=)) 68 | (integer (eql ws-version 13))))))) 69 | -------------------------------------------------------------------------------- /src/server.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | (defpackage websocket-driver.server 3 | (:use :cl) 4 | (:import-from :websocket-driver.ws.server 5 | #:server) 6 | (:export #:make-server)) 7 | (in-package :websocket-driver.server) 8 | 9 | (defun make-server (env &rest options &key max-length accept-protocols additional-headers) 10 | (declare (ignore max-length accept-protocols additional-headers)) 11 | (let ((socket (getf env :clack.io))) 12 | (unless socket 13 | (error ":clack.io doesn't exist in ENV. Probably this server is not supported.")) 14 | (apply #'make-instance 'server 15 | :socket socket 16 | :headers (getf env :headers) 17 | options))) 18 | 19 | (import 'make-server :websocket-driver) 20 | (export 'make-server :websocket-driver) 21 | -------------------------------------------------------------------------------- /src/util.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | (defpackage websocket-driver.util 3 | (:use :cl) 4 | (:import-from :split-sequence 5 | #:split-sequence) 6 | (:import-from :cl-base64 7 | #:string-to-base64-string) 8 | (:import-from :sha1 9 | #:sha1-base64) 10 | (:export #:split-by-comma 11 | #:generate-accept)) 12 | (in-package :websocket-driver.util) 13 | 14 | (defparameter +guid+ 15 | "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") 16 | 17 | (defun split-by-comma (string) 18 | (mapl (lambda (parts) 19 | (rplaca parts (string-trim '(#\Space) (car parts)))) 20 | (split-sequence #\, string))) 21 | 22 | (defun generate-accept (key) 23 | (declare (optimize (speed 3) (safety 0)) 24 | (type simple-string key)) 25 | (sha1:sha1-base64 (concatenate 'string key +guid+) 26 | #'string-to-base64-string)) 27 | -------------------------------------------------------------------------------- /src/ws/base.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | (defpackage websocket-driver.ws.base 3 | (:use :cl) 4 | (:import-from :fast-websocket 5 | #:make-ws 6 | #:ws-stage 7 | #:make-parser) 8 | (:import-from :event-emitter 9 | #:emit 10 | #:event-emitter) 11 | (:import-from :fast-io 12 | #:with-fast-output 13 | #:fast-write-sequence) 14 | (:export #:ws 15 | #:socket 16 | #:additional-headers 17 | #:accept-protocols 18 | #:protocol 19 | #:version 20 | #:ready-state 21 | #:require-masking 22 | #:ws 23 | 24 | #:start-connection 25 | #:parse 26 | #:send 27 | #:send-text 28 | #:send-binary 29 | #:send-ping 30 | #:close-connection 31 | #:open-connection 32 | #:send-handshake-response 33 | #:send-handshake-request 34 | 35 | #:read-websocket-frame)) 36 | (in-package :websocket-driver.ws.base) 37 | 38 | (defparameter +states+ 39 | #(:connecting :open :closing :closed)) 40 | 41 | (defclass ws (event-emitter) 42 | ((socket :initarg :socket 43 | :accessor socket) 44 | (accept-protocols :initarg :accept-protocols 45 | :initform '() 46 | :accessor accept-protocols) 47 | (protocol :type (or null string) 48 | :initform nil 49 | :accessor protocol) 50 | (version :accessor version) 51 | (max-length :initarg :max-length 52 | :initform #x3ffffff 53 | :accessor max-length) 54 | (ready-state :type fixnum 55 | :initform 0) 56 | (additional-headers :initarg :additional-headers 57 | :initform '() 58 | :accessor additional-headers) 59 | 60 | (queue :initform (make-array 0 :adjustable t :fill-pointer 0) 61 | :accessor queue) 62 | 63 | (require-masking :initarg :require-masking 64 | :accessor require-masking) 65 | (ws-parse :initform (fast-websocket:make-ws) 66 | :accessor ws-parse) 67 | (ping-callbacks :initform (make-hash-table :test 'equalp) 68 | :accessor ping-callbacks) 69 | (parser :accessor parser) 70 | (parse-lock :initform (bt2:make-recursive-lock) 71 | :reader parse-lock))) 72 | 73 | (defun send-close-frame (ws reason code) 74 | (setf (ready-state ws) :closing) 75 | (send ws reason :type :close :code code)) 76 | 77 | (defmethod initialize-instance :after ((ws ws) &key) 78 | (setf (parser ws) 79 | (make-parser (ws-parse ws) 80 | :require-masking (require-masking ws) 81 | :max-length (max-length ws) 82 | :message-callback 83 | (lambda (message) 84 | (emit :message ws message)) 85 | :ping-callback 86 | (lambda (payload) 87 | (send ws payload :type :pong)) 88 | :pong-callback 89 | (lambda (payload) 90 | (let ((callback (gethash payload (ping-callbacks ws)))) 91 | (when callback 92 | (remhash payload (ping-callbacks ws)) 93 | (funcall callback)))) 94 | :close-callback 95 | (lambda (data &key code) 96 | (case (ready-state ws) 97 | ;; closing request by the other peer 98 | (:open 99 | (send-close-frame ws data code)) 100 | ;; probably the response for a 'close' frame 101 | (otherwise 102 | (close-connection ws data code))) 103 | (setf (ws-stage (ws-parse ws)) 0)) 104 | :error-callback 105 | (lambda (code reason) 106 | (emit :error ws reason) 107 | (send-close-frame ws reason code) 108 | (setf (ws-stage (ws-parse ws)) 0))))) 109 | 110 | (defgeneric ready-state (ws) 111 | (:method ((ws ws)) 112 | (aref +states+ (slot-value ws 'ready-state)))) 113 | 114 | (defgeneric (setf ready-state) (state ws) 115 | (:method (state (ws ws)) 116 | (setf (slot-value ws 'ready-state) 117 | (ecase state 118 | (:connecting 0) 119 | (:open 1) 120 | (:closing 2) 121 | (:closed 3))))) 122 | 123 | (defgeneric start-connection (ws &key &allow-other-keys)) 124 | 125 | (defgeneric parse (ws data &key start end) 126 | (:method (ws data &key start end) 127 | (bt2:with-recursive-lock-held ((parse-lock ws)) 128 | (funcall (parser ws) data :start start :end end)))) 129 | 130 | (defgeneric send (ws data &key start end type code callback)) 131 | (defmethod send :around ((ws ws) data &rest args &key type callback &allow-other-keys) 132 | (when (eq (ready-state ws) :connecting) 133 | (return-from send 134 | (enqueue ws (cons data args)))) 135 | 136 | ; allow close packets through when we are trying to close the connection 137 | (unless (and (eq type :close) 138 | (eq (ready-state ws) :closing)) 139 | 140 | (when (and (eq (ready-state ws) :closing) callback) 141 | (funcall callback)) 142 | 143 | (unless (eq (ready-state ws) :open) 144 | (return-from send nil))) 145 | 146 | (call-next-method)) 147 | 148 | (defun send-text (ws message &rest args &key start end callback) 149 | (declare (ignore start end callback)) 150 | (apply #'send ws message :type :text args)) 151 | 152 | (defun send-binary (ws message &rest args &key start end callback) 153 | (declare (ignore start end callback)) 154 | (apply #'send ws message :type :binary args)) 155 | 156 | (defgeneric send-ping (ws &optional message callback) 157 | (:method ((ws ws) &optional message callback) 158 | (unless message 159 | (setq message #.(make-array 0 :element-type '(unsigned-byte 8)))) 160 | (when callback 161 | (setf (gethash message (ping-callbacks ws)) 162 | callback)) 163 | (send ws message :type :ping))) 164 | 165 | (defgeneric close-connection (ws &optional reason code)) 166 | 167 | (defmethod close-connection :around ((ws ws) &optional reason code) 168 | (case (ready-state ws) 169 | (:connecting 170 | (setf (ready-state ws) :closed) 171 | (emit :close ws :code code :reason reason) 172 | t) 173 | (:closing 174 | (call-next-method) 175 | (emit :close ws :code code :reason reason)) 176 | (:open 177 | (call-next-method)) 178 | (otherwise nil))) 179 | 180 | (defgeneric open-connection (ws) 181 | (:method ((ws ws)) 182 | (setf (ready-state ws) :open) 183 | 184 | (unless (= 0 (length (queue ws))) 185 | (map nil (lambda (message) 186 | (apply #'send ws message)) 187 | (queue ws)) 188 | 189 | (setf (queue ws) 190 | (make-array 0 :adjustable t :fill-pointer 0))) 191 | 192 | (emit :open ws))) 193 | 194 | (defun enqueue (ws message) 195 | (vector-push-extend message (queue ws)) 196 | t) 197 | 198 | (defgeneric send-handshake-response (ws &key callback)) 199 | 200 | (defgeneric send-handshake-request (ws &key callback)) 201 | 202 | (defun read-websocket-frame (stream) 203 | (let ((buf (make-array 2 :element-type '(unsigned-byte 8))) 204 | (extended-buf (make-array 8 :element-type '(unsigned-byte 8)))) 205 | (block nil 206 | (tagbody retry 207 | (let ((read-bytes (handler-case (read-sequence buf stream) 208 | (error () 209 | ;; Retry when I/O timeout error 210 | (go retry))))) 211 | (when (= read-bytes 0) 212 | (return nil)) 213 | 214 | (let ((maskp (plusp (ldb (byte 1 7) (aref buf 1)))) 215 | (data-length (ldb (byte 7 0) (aref buf 1)))) 216 | (cond 217 | ((<= 0 data-length 125)) 218 | (t 219 | (let ((end (if (= data-length 126) 2 8))) 220 | (read-sequence extended-buf stream :end end) 221 | (incf read-bytes end) 222 | (setf data-length 223 | (loop with length = 0 224 | for i from 0 below end 225 | do (setf length (+ (ash length 8) (aref extended-buf i))) 226 | finally (return length)))))) 227 | (when maskp 228 | (incf data-length 4)) 229 | (let ((data (make-array (+ read-bytes data-length) :element-type '(unsigned-byte 8)))) 230 | (replace data buf :end2 2) 231 | (unless (= read-bytes 2) 232 | (replace data extended-buf :start1 2 :end2 (- read-bytes 2))) 233 | (handler-case 234 | (read-sequence data stream :start read-bytes) 235 | (error () 236 | (return nil))) 237 | (return data)))))))) 238 | -------------------------------------------------------------------------------- /src/ws/client.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | (defpackage websocket-driver.ws.client 3 | (:use :cl 4 | #:websocket-driver.ws.base 5 | #:websocket-driver.util) 6 | (:import-from :event-emitter 7 | #:emit) 8 | (:import-from :fast-io 9 | #:with-fast-output 10 | #:fast-write-byte 11 | #:fast-write-sequence) 12 | (:import-from :fast-websocket 13 | #:compose-frame) 14 | (:import-from :fast-http 15 | #:make-http-response 16 | #:make-parser 17 | #:http-status) 18 | (:import-from :cl-base64 19 | #:usb8-array-to-base64-string) 20 | (:import-from :babel 21 | #:string-to-octets) 22 | (:import-from :quri 23 | #:uri 24 | #:uri-scheme 25 | #:uri-host 26 | #:uri-port) 27 | (:import-from :uiop) 28 | (:export #:client)) 29 | (in-package :websocket-driver.ws.client) 30 | 31 | (defclass client (ws) 32 | ((url :initarg :url 33 | :initform (error ":url is required") 34 | :accessor url) 35 | (key :initform (generate-key) 36 | :reader key) 37 | (accept :accessor accept) 38 | (version :initform "hybi-13") 39 | (require-masking :initarg :require-masking 40 | :initform nil 41 | :accessor require-masking) 42 | (read-thread :initform nil 43 | :accessor read-thread))) 44 | 45 | (defun generate-key () 46 | (let ((key (make-array 16 :element-type '(unsigned-byte 8)))) 47 | (dotimes (i 16) 48 | (setf (aref key i) (random 255))) 49 | (base64:usb8-array-to-base64-string key))) 50 | 51 | (defmethod initialize-instance :after ((client client) &key) 52 | (setf (accept client) (generate-accept (key client)))) 53 | 54 | (defun read-until-crlf*2 (stream) 55 | (declare (optimize (speed 3))) 56 | (with-fast-output (buf) 57 | (tagbody 58 | read-cr 59 | (loop for byte of-type (or (unsigned-byte 8) null) = (read-byte stream nil nil) 60 | if byte 61 | do (fast-write-byte byte buf) 62 | else 63 | do (go eof) 64 | until (= byte (char-code #\Return))) 65 | 66 | read-lf 67 | (let ((next-byte (read-byte stream nil nil))) 68 | (unless next-byte 69 | (go eof)) 70 | (locally (declare (type (unsigned-byte 8) next-byte)) 71 | (cond 72 | ((= next-byte (char-code #\Newline)) 73 | (fast-write-byte next-byte buf) 74 | (go read-cr2)) 75 | ((= next-byte (char-code #\Return)) 76 | (fast-write-byte next-byte buf) 77 | (go read-lf)) 78 | (T 79 | (fast-write-byte next-byte buf) 80 | (go read-cr))))) 81 | 82 | read-cr2 83 | (let ((next-byte (read-byte stream nil nil))) 84 | (unless next-byte 85 | (go eof)) 86 | (locally (declare (type (unsigned-byte 8) next-byte)) 87 | (cond 88 | ((= next-byte (char-code #\Return)) 89 | (fast-write-byte next-byte buf) 90 | (go read-lf2)) 91 | (T 92 | (fast-write-byte next-byte buf) 93 | (go read-cr))))) 94 | 95 | read-lf2 96 | (let ((next-byte (read-byte stream nil nil))) 97 | (unless next-byte 98 | (go eof)) 99 | (locally (declare (type (unsigned-byte 8) next-byte)) 100 | (cond 101 | ((= next-byte (char-code #\Newline)) 102 | (fast-write-byte next-byte buf)) 103 | ((= next-byte (char-code #\Return)) 104 | (fast-write-byte next-byte buf) 105 | (go read-lf)) 106 | (T 107 | (fast-write-byte next-byte buf) 108 | (go read-cr))))) 109 | 110 | eof))) 111 | 112 | (defmethod start-connection ((client client) &key (verify t) (ca-path nil)) 113 | (unless (eq (ready-state client) :connecting) 114 | (return-from start-connection)) 115 | 116 | (flet ((fail-handshake (format-control &rest format-arguments) 117 | (error (format nil "Error during WebSocket handshake:~% ~A" 118 | (apply #'format nil format-control format-arguments))))) 119 | (let* ((uri (quri:uri (url client))) 120 | (secure (cond ((string-equal (uri-scheme uri) "ws") 121 | nil) 122 | ((string-equal (uri-scheme uri) "wss") 123 | t) 124 | (t (error "Invalid URI scheme: ~S" (uri-scheme uri))))) 125 | (http (make-http-response)) 126 | (http-parser (make-parser http 127 | :first-line-callback 128 | (lambda () 129 | (unless (= (fast-http:http-status http) 101) 130 | (fail-handshake "Unexpected response code: ~S" 131 | (fast-http:http-status http)))) 132 | :header-callback 133 | (lambda (headers) 134 | (let ((upgrade (gethash "upgrade" headers))) 135 | (cond 136 | ((null upgrade) 137 | (fail-handshake "'Upgrade' header is missing")) 138 | ((not (string-equal upgrade "websocket")) 139 | (fail-handshake "'Upgrade' header value is not 'WebSocket'")))) 140 | (let ((connection (gethash "connection" headers))) 141 | (cond 142 | ((null connection) 143 | (fail-handshake "'Connection' header is missing")) 144 | ((not (string-equal connection "upgrade")) 145 | (fail-handshake "'Connection' header value is not 'Upgrade'")))) 146 | 147 | (unless (string= (accept client) 148 | (gethash "sec-websocket-accept" headers "")) 149 | (fail-handshake "Sec-WebSocket-Accept mismatch")) 150 | 151 | (let ((protocol (gethash "sec-websocket-protocol" headers))) 152 | (when (accept-protocols client) 153 | (unless (and protocol 154 | (find protocol (accept-protocols client) :test #'string=)) 155 | (fail-handshake "Sec-WebSocket-Protocol mismatch")) 156 | (setf (protocol client) protocol)))))) 157 | (stream (usocket:socket-stream 158 | (usocket:socket-connect (uri-host uri) (uri-port uri) 159 | :element-type '(unsigned-byte 8)))) 160 | (stream (if secure 161 | #+websocket-driver-no-ssl 162 | (error "SSL not supported. Remove :websocket-driver-no-ssl from *features* to enable SSL.") 163 | #-websocket-driver-no-ssl 164 | (progn 165 | (cl+ssl:ensure-initialized) 166 | (setf (cl+ssl:ssl-check-verify-p) t) 167 | (let ((ctx (cl+ssl:make-context :verify-mode (if verify 168 | cl+ssl:+ssl-verify-peer+ 169 | cl+ssl:+ssl-verify-none+) 170 | :verify-location (if ca-path 171 | (uiop:native-namestring ca-path) 172 | :default)))) 173 | ;; TODO: certificate files 174 | (cl+ssl:with-global-context (ctx :auto-free-p t) 175 | (cl+ssl:make-ssl-client-stream stream 176 | :hostname (uri-host uri) 177 | :verify (if verify :optional nil))))) 178 | stream))) 179 | 180 | (setf (socket client) stream) 181 | (send-handshake-request client) 182 | (funcall http-parser (read-until-crlf*2 stream)) 183 | (open-connection client) 184 | (setf (read-thread client) 185 | (bt2:make-thread 186 | (lambda () 187 | (unwind-protect 188 | (loop for frame = (read-websocket-frame stream) 189 | while frame 190 | do (parse client frame)) 191 | (close-connection client))) 192 | :name "websocket client read thread" 193 | :initial-bindings `((*standard-output* . ,*standard-output*) 194 | (*error-output* . ,*error-output*)))) 195 | client))) 196 | 197 | (defmethod send ((client client) data &key start end type code callback) 198 | (let ((frame (compose-frame data 199 | :start start 200 | :end end 201 | :type type 202 | :code code 203 | :masking t))) 204 | (handler-case (progn 205 | (write-sequence frame (socket client)) 206 | (force-output (socket client))) 207 | (error () 208 | (close-connection client))) 209 | (when callback 210 | (funcall callback)))) 211 | 212 | (defmethod send-handshake-request ((client client) &key callback) 213 | (let ((uri (quri:uri (url client))) 214 | (socket (socket client))) 215 | (write-sequence 216 | (with-fast-output (buffer) 217 | (labels ((octets (data) 218 | (fast-write-sequence data buffer)) 219 | (ascii-string (data) 220 | (octets (string-to-octets data :encoding :ascii))) 221 | (crlf () 222 | (octets #.(string-to-octets (format nil "~C~C" #\Return #\Newline) 223 | :encoding :ascii)))) 224 | (ascii-string 225 | (format nil "GET ~:[/~;~:*~A~]~:[~;~:*?~A~] HTTP/1.1~C~C" 226 | (quri:uri-path uri) 227 | (quri:uri-query uri) 228 | #\Return #\Newline)) 229 | (ascii-string 230 | (format nil "Host: ~A~C~C" 231 | (quri:uri-authority uri) 232 | #\Return #\Newline)) 233 | (octets 234 | #.(string-to-octets 235 | (with-output-to-string (s) 236 | (format s "Upgrade: websocket~C~C" #\Return #\Newline) 237 | (format s "Connection: Upgrade~C~C" #\Return #\Newline)) 238 | :encoding :ascii)) 239 | (ascii-string 240 | (format nil "Sec-WebSocket-Key: ~A~C~C" 241 | (key client) 242 | #\Return #\Newline)) 243 | (octets 244 | #.(string-to-octets 245 | (format nil "Sec-WebSocket-Version: 13~C~C" #\Return #\Newline) 246 | :encoding :ascii)) 247 | (when (accept-protocols client) 248 | (ascii-string 249 | (format nil "Sec-WebSocket-Protocol: ~{~A~^, ~}~C~C" 250 | (accept-protocols client) 251 | #\Return #\Newline))) 252 | 253 | (loop for (name . value) in (additional-headers client) 254 | do (ascii-string 255 | (string-capitalize name)) 256 | (octets 257 | #.(string-to-octets ": " :encoding :ascii)) 258 | (ascii-string value) 259 | (crlf)) 260 | 261 | (crlf))) 262 | socket) 263 | (force-output socket) 264 | (when callback 265 | (funcall callback)))) 266 | 267 | (defmethod close-connection ((client client) &optional reason code) 268 | (ignore-errors (close (socket client))) 269 | (setf (ready-state client) :closed) 270 | (let ((thread (read-thread client))) 271 | (when thread 272 | (if (and (bt2::threadp thread) 273 | (bt2::thread-alive-p thread) 274 | (not (eq (bt2:current-thread) thread))) 275 | (bt2::destroy-thread thread)) 276 | (setf (read-thread client) nil))) 277 | (emit :close client :code code :reason reason) 278 | t) 279 | -------------------------------------------------------------------------------- /src/ws/server.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | (defpackage websocket-driver.ws.server 3 | (:use :cl 4 | #:websocket-driver.ws.base 5 | #:websocket-driver.util) 6 | (:import-from :fast-websocket 7 | #:compose-frame 8 | #:error-code) 9 | (:import-from :clack.socket 10 | #:read-callback 11 | #:write-sequence-to-socket 12 | #:write-sequence-to-socket-buffer 13 | #:write-byte-to-socket-buffer 14 | #:flush-socket-buffer 15 | #:close-socket 16 | #:socket-async-p 17 | #:socket-stream) 18 | (:import-from :fast-io 19 | #:with-fast-output 20 | #:fast-write-sequence 21 | #:fast-write-byte) 22 | (:import-from :babel 23 | #:string-to-octets) 24 | (:export #:server)) 25 | (in-package :websocket-driver.ws.server) 26 | 27 | (defclass server (ws) 28 | ((headers :initarg :headers 29 | :initform (error ":headers is required") 30 | :type hash-table 31 | :accessor headers) 32 | (require-masking :initarg :require-masking 33 | :initform t 34 | :accessor require-masking))) 35 | 36 | (defmethod initialize-instance :after ((server server) &key) 37 | (let ((protocols (accept-protocols server)) 38 | (env-protocols (gethash "sec-websocket-protocol" (headers server)))) 39 | (when env-protocols 40 | (setq env-protocols (split-by-comma env-protocols))) 41 | (setf (protocol server) 42 | (find-if (lambda (proto) 43 | (find proto protocols :test #'string=)) 44 | env-protocols))) 45 | 46 | ;; Sec-Websocket-Version must be "13" 47 | (let ((ws-version (gethash "sec-websocket-version" 48 | (headers server)))) 49 | (etypecase ws-version 50 | (null 51 | (error "No Sec-WebSocket-Version header")) 52 | (string 53 | (unless (find "13" (split-by-comma ws-version) 54 | :test #'string=) 55 | (error "Unsupported WebSocket version: ~S" ws-version))) 56 | (integer 57 | (unless (= ws-version 13) 58 | (error "Unsupported WebSocket version: ~S" ws-version))))) 59 | (setf (version server) "hybi-13")) 60 | 61 | (defmethod start-connection ((server server) &key) 62 | (unless (eq (ready-state server) :connecting) 63 | (return-from start-connection)) 64 | 65 | (let ((socket (socket server))) 66 | (setf (read-callback socket) 67 | (lambda (data &key (start 0) end) 68 | (parse server data :start start :end end))) 69 | 70 | (send-handshake-response server 71 | :callback 72 | (lambda () 73 | (unless (eq (ready-state server) :closed) 74 | (open-connection server)))) 75 | 76 | (unless (clack.socket:socket-async-p socket) 77 | (unwind-protect 78 | (loop with stream = (socket-stream socket) 79 | while (open-stream-p stream) 80 | for frame = (read-websocket-frame stream) 81 | while frame 82 | do (funcall (read-callback socket) frame)) 83 | (close-connection server) 84 | (setf (ready-state server) :closed) 85 | (wsd:emit :close server :code 1006 :reason "websocket connection closed"))))) 86 | 87 | (defmethod close-connection ((server server) &optional (reason "") (code (error-code :normal-closure))) 88 | (setf (ready-state server) :closing) 89 | (send server reason :type :close :code code 90 | :callback 91 | (let ((socket (socket server))) 92 | (lambda () 93 | (setf (ready-state server) :closed) 94 | (close-socket socket)))) 95 | t) 96 | 97 | (defmethod send ((server server) data &key start end type code callback) 98 | (let ((frame (compose-frame data 99 | :start start 100 | :end end 101 | :type type 102 | :code code 103 | :masking nil))) 104 | (handler-case 105 | (write-sequence-to-socket (socket server) frame 106 | :callback callback) 107 | (error () 108 | (setf (ready-state server) :closed) 109 | (wsd:emit :close server :code 1006 :reason "websocket connection closed"))))) 110 | 111 | (defmethod send-handshake-response ((server server) &key callback) 112 | (let ((socket (socket server)) 113 | (sec-key (gethash "sec-websocket-key" (headers server)))) 114 | (unless (stringp sec-key) 115 | (when callback (funcall callback)) 116 | (return-from send-handshake-response)) 117 | 118 | (labels ((octets (data) 119 | (write-sequence-to-socket-buffer socket data)) 120 | (ascii-string (data) 121 | (octets (string-to-octets data :encoding :ascii))) 122 | (crlf () 123 | (octets #.(string-to-octets (format nil "~C~C" #\Return #\Newline) 124 | :encoding :ascii)))) 125 | (octets 126 | #.(string-to-octets 127 | (with-output-to-string (s) 128 | (format s "HTTP/1.1 101 Switching Protocols~C~C" #\Return #\Newline) 129 | (format s "Upgrade: websocket~C~C" #\Return #\Newline) 130 | (format s "Connection: Upgrade~C~C" #\Return #\Newline) 131 | (format s "Sec-WebSocket-Accept: ")) 132 | :encoding :ascii)) 133 | (ascii-string 134 | (generate-accept sec-key)) 135 | (crlf) 136 | 137 | (let ((protocol (protocol server))) 138 | (when protocol 139 | (octets 140 | #.(string-to-octets "Sec-WebSocket-Protocol: " 141 | :encoding :ascii)) 142 | (ascii-string protocol) 143 | (crlf))) 144 | 145 | (loop for (name . value) in (additional-headers server) 146 | do (ascii-string 147 | (string-capitalize name)) 148 | (octets 149 | #.(string-to-octets ": " :encoding :ascii)) 150 | (ascii-string value) 151 | (crlf)) 152 | 153 | (crlf)) 154 | 155 | (flush-socket-buffer socket :callback callback))) 156 | -------------------------------------------------------------------------------- /t/client.js: -------------------------------------------------------------------------------- 1 | var WebSocket = require('ws'), 2 | ws = new WebSocket('ws://localhost:5000/echo'); 3 | 4 | var _output = ''; 5 | ws.on('message', function(message) { 6 | console.error('received: %s', message); 7 | _output = _output + message; 8 | }); 9 | var getOutput = function() { 10 | var out = _output; 11 | _output = ''; 12 | return out; 13 | }; 14 | 15 | ws.on('open', function() { 16 | ws.send('Hi'); 17 | setTimeout(function() { 18 | var output = getOutput(); 19 | if (output === 'Hi') { 20 | console.log('ok'); 21 | process.exit(); 22 | } 23 | else { 24 | console.log('ng: "%s"', output); 25 | process.exit(1); 26 | } 27 | }, 300); 28 | }); 29 | 30 | setTimeout(function() { 31 | console.error('Timeout.'); 32 | process.exit(1); 33 | }, 3000); 34 | -------------------------------------------------------------------------------- /t/client.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | 3 | (ql:quickload '(:uiop :websocket-driver-client) :silent t) 4 | 5 | (defvar *client* 6 | (wsd:make-client "ws://localhost:5000/echo")) 7 | 8 | (wsd:on :open *client* 9 | (lambda () 10 | (format *error-output* "~&connected~%"))) 11 | 12 | (wsd:on :message *client* 13 | (lambda (message) 14 | (if (string= message "Hi") 15 | (progn 16 | (format t "~&ok~%") 17 | (uiop:quit)) 18 | (progn 19 | (format t "~&ng~%") 20 | (uiop:quit -1))))) 21 | 22 | (wsd:start-connection *client*) 23 | (wsd:send *client* "Hi") 24 | (sleep 1) 25 | -------------------------------------------------------------------------------- /t/install-libfixposix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | curl -L https://github.com/sionescu/libfixposix/archive/v0.3.0.tar.gz | tar xzf - 4 | 5 | cd libfixposix-0.3.0 6 | autoreconf -i -f 7 | 8 | mkdir build/ 9 | cd build/ 10 | ../configure --prefix=$HOME/libfixposix 11 | make 12 | make install 13 | -------------------------------------------------------------------------------- /t/install-libuv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | curl -L https://github.com/libuv/libuv/archive/v1.6.1.tar.gz | tar xzf - 4 | 5 | cd libuv-1.6.1 6 | ./autogen.sh 7 | ./configure --prefix=$HOME/libuv 8 | make 9 | make install 10 | -------------------------------------------------------------------------------- /t/server.js: -------------------------------------------------------------------------------- 1 | var WebSocketServer = require('ws').Server, 2 | wss = new WebSocketServer({ path: '/echo', port: 5000 }); 3 | 4 | wss.on('connection', function(ws) { 5 | console.log('connected'); 6 | ws.on('message', function(message) { 7 | console.log('received: %s', message); 8 | ws.send(message); 9 | console.log('send: %s', message); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /websocket-driver-base.asd: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | (defpackage websocket-driver-base-asd 3 | (:use :cl :asdf)) 4 | (in-package :websocket-driver-base-asd) 5 | 6 | (defsystem websocket-driver-base 7 | :version "0.2.0" 8 | :author "Eitaro Fukamachi" 9 | :license "BSD 2-Clause" 10 | :depends-on (:fast-websocket 11 | :fast-io 12 | :event-emitter 13 | :sha1 14 | :cl-base64 15 | :split-sequence 16 | :bordeaux-threads) 17 | :components ((:module "src" 18 | :components 19 | ((:file "driver" :depends-on ("ws/base")) 20 | (:file "ws/base" :depends-on ("util")) 21 | (:file "util")))) 22 | :description "WebSocket protocol handler") 23 | -------------------------------------------------------------------------------- /websocket-driver-client.asd: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | (defpackage websocket-driver-client-asd 3 | (:use :cl :asdf)) 4 | (in-package :websocket-driver-client-asd) 5 | 6 | (defsystem websocket-driver-client 7 | :version "0.2.0" 8 | :author "Eitaro Fukamachi" 9 | :license "BSD 2-Clause" 10 | :depends-on (:websocket-driver-base 11 | :usocket 12 | #-websocket-driver-no-ssl :cl+ssl 13 | :fast-io 14 | :fast-websocket 15 | :fast-http 16 | :cl-base64 17 | :babel 18 | :quri) 19 | :components ((:module "src" 20 | :components 21 | ((:file "ws/client") 22 | (:file "client" :depends-on ("ws/client")))))) 23 | -------------------------------------------------------------------------------- /websocket-driver-server.asd: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | (defpackage websocket-driver-server-asd 3 | (:use :cl :asdf)) 4 | (in-package :websocket-driver-server-asd) 5 | 6 | (defsystem websocket-driver-server 7 | :version "0.2.0" 8 | :author "Eitaro Fukamachi" 9 | :license "BSD 2-Clause" 10 | :depends-on (:websocket-driver-base 11 | :fast-websocket 12 | :fast-io 13 | :clack-socket 14 | :babel) 15 | :components ((:module "src" 16 | :components 17 | ((:file "server" :depends-on ("ws/server")) 18 | (:file "ws/server")))) 19 | :description "WebSocket protocol handler") 20 | -------------------------------------------------------------------------------- /websocket-driver.asd: -------------------------------------------------------------------------------- 1 | #| 2 | This file is a part of websocket-driver project. 3 | Copyright (c) 2014 Eitaro Fukamachi (e.arrows@gmail.com) 4 | |# 5 | 6 | #| 7 | Author: Eitaro Fukamachi (e.arrows@gmail.com) 8 | |# 9 | 10 | (in-package :cl-user) 11 | (defpackage websocket-driver-asd 12 | (:use :cl :asdf)) 13 | (in-package :websocket-driver-asd) 14 | 15 | (defsystem websocket-driver 16 | :version "0.2.0" 17 | :author "Eitaro Fukamachi" 18 | :license "BSD 2-Clause" 19 | :depends-on (:websocket-driver-server 20 | :websocket-driver-client) 21 | :description "WebSocket protocol handler") 22 | --------------------------------------------------------------------------------