[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] 80/119: helpers for handling web socket connections
From: |
Eric Schulte |
Subject: |
[elpa] 80/119: helpers for handling web socket connections |
Date: |
Mon, 10 Mar 2014 16:57:43 +0000 |
eschulte pushed a commit to branch master
in repository elpa.
commit 6e9f8168f25660609385b6436fa7de84f2bb032b
Author: Eric Schulte <address@hidden>
Date: Tue Jan 7 09:26:42 2014 -0700
helpers for handling web socket connections
---
.gitignore | 1 +
examples/9-web-socket.el | 15 ++--
web-server.el | 198 ++++++++++++++++++++++++----------------------
3 files changed, 113 insertions(+), 101 deletions(-)
diff --git a/.gitignore b/.gitignore
index c531d98..65b8fcb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
*.elc
+stuff
diff --git a/examples/9-web-socket.el b/examples/9-web-socket.el
index 3b39568..1ad9276 100644
--- a/examples/9-web-socket.el
+++ b/examples/9-web-socket.el
@@ -1,11 +1,14 @@
;;; web-sockets.el --- communicate via web-sockets
-(defvar web-socket-page "<html>
+(defvar web-socket-port 8888)
+
+(defvar web-socket-page
+ (format "<html>
<head>
<script type=\"text/javascript\">
var ws;
function connect(){
- ws = new WebSocket(\"ws://localhost:9999/\");
+ ws = new WebSocket(\"ws://localhost:%d/\");
ws.onopen = function() { alert(\"connected\"); };
ws.onmessage = function(msg) { alert(\"Server: \" + msg.data); };
@@ -22,14 +25,12 @@ function close(){ ws.close(); };
<a href=\"javascript:message()\">message</a>
<a href=\"javascript:close()\">close</a>
</body>
-</html>")
+</html>" web-socket-port))
(defun web-socket-server (request)
(with-slots (process headers) request
- (ws/web-socket-messages-do headers message
- ;; (ws/web-socket-send message)
- (message "GOT:%S" message))
+ (ws-web-socket-connect request 'ws-web-socket-send)
(ws-response-header process 200 '("Content-type" . "text/html"))
(process-send-string process web-socket-page)))
-(ws-start '(((:GET . ".*") . web-socket-server)) 9999)
+(ws-start '(((:GET . ".*") . web-socket-server)) web-socket-port)
diff --git a/web-server.el b/web-server.el
index cbb68fc..b0cbfa6 100644
--- a/web-server.el
+++ b/web-server.el
@@ -305,8 +305,51 @@ Return non-nil only when parsing is complete."
;;; Web Socket
-(defvar ws/web-socket-handler nil
- "Function to handle web-socket messages, should take a single argument.")
+(defclass ws-message () ; web socket message object
+ ((process :initarg :process :accessor process :initform "")
+ (pending :initarg :pending :accessor pending :initform "")
+ (active :initarg :active :accessor active :initform nil)
+ (new :initarg :new :accessor new :initform nil)
+ (data :initarg :data :accessor data :initform "")
+ (handler :initarg :handler :accessor handler :initform "")))
+
+(defun ws-web-socket-connect (request handler)
+ "Establish a web socket connection with request.
+If the connection is successful this function will throw
+`:keep-alive' to `close-connection' skipping any remaining code
+in the request handler. If no web-socket connection is
+established (e.g., because REQUEST is not attempting to establish
+a connection) then no actions are taken and nil is returned.
+
+Second argument HANDLER should be a function of one argument
+which will be called on all complete messages as they are
+received and parsed from the network."
+ (with-slots (process headers) request
+ (when (assoc :SEC-WEBSOCKET-KEY headers)
+ ;; Accept the connection
+ (ws-response-header process 101
+ (cons "Upgrade" "websocket")
+ (cons "Connection" "upgrade")
+ (cons "Sec-WebSocket-Accept"
+ (ws-web-socket-handshake
+ (cdr (assoc :SEC-WEBSOCKET-KEY headers)))))
+ ;; Setup the process filter
+ (set-process-coding-system process 'binary)
+ (set-process-plist
+ process (list :message (make-instance 'ws-message
+ :handler handler :process process)))
+ (set-process-filter process 'ws-web-socket-filter)
+ (throw 'close-connection :keep-alive))))
+
+(defun ws-web-socket-filter (process string)
+ (let ((message (plist-get (process-plist process) :message)))
+ ;; don't re-start if message is being parsed
+ (if (active message)
+ (setf (new message) string)
+ (setf (pending message) (concat (pending message) string))
+ (setf (active message) t)
+ (ws-web-socket-parse-messages message))
+ (setf (active message) nil)))
(defun int-to-bits (int size)
(let ((result (make-bool-vector size nil)))
@@ -324,7 +367,7 @@ Return non-nil only when parsing is complete."
(prog1 (if bit (expt 2 place) 0) (incf place)))
(reverse bits)))))
-(defun ws/web-socket-mask (masking-key data)
+(defun ws-web-socket-mask (masking-key data)
(let ((masking-data (apply #'concat (make-list (+ 1 (/ (length data) 4))
masking-key))))
(apply #'string (cl-mapcar #'logxor masking-data data))))
@@ -351,99 +394,66 @@ Return non-nil only when parsing is complete."
;; | Payload Data continued ... |
;; +---------------------------------------------------------------+
;;
-(defun ws-make-web-socket-filter (handler)
+(defun ws-web-socket-parse-messages (message)
"Web socket filter to pass whole frames to the client.
See RFC6455."
- (lexical-let ((my-handler handler))
- (lambda proc string
- (catch 'wait ; TODO: this needs more complete partial input handling
- (when (plist-get (process-plist proc) :active)
- (let ((pending (plist-get (process-plist proc) :pending)))
- (set-process-plist proc
- (plist-put (process-plist proc)
- :pending (concat pending string))))
- (throw 'wait nil))
- ;; set to active
- (set-process-plist proc (plist-put (process-plist proc) :active t))
- (let ((index 0))
- (cl-flet ((bits (length)
- (apply #'append
- (mapcar (lambda (int) (int-to-bits int 8))
- (subseq string index (incf index
length))))))
- (let ((data (plist-get (process-plist proc) :parsed))
- fin rsvs opcode mask pl mask-key)
- (let ((byte (bits 1)))
- (setq fin (car byte)
- rsvs (subseq byte 1 4)
- opcode
- (let ((it (bits-to-int (subseq byte 4))))
- (case it
- (0 :CONTINUATION)
- (1 :TEXT)
- (2 :BINARY)
- ((3 4 5 6 7) :NON-CONTROL)
- (9 :PING)
- (10 :PONG)
- ((11 12 13 14 15) :CONTROL)
- ;; If an unknown opcode is received, the receiving
- ;; endpoint MUST _Fail the WebSocket Connection_.
- (t (ws-error proc "Web Socket Fail: bad opcode %d"
it))))))
- (unless (cl-every #'null rsvs)
- ;; MUST be 0 unless an extension is negotiated that defines
- ;; meanings for non-zero values.
- (ws-error proc "Web Socket Fail: non-zero RSV 1 2 or 3"))
- (let ((byte (bits 1)))
- (setq mask (car byte)
- pl (bits-to-int (subseq byte 1))))
- (unless (eq mask t)
- ;; All frames sent from client to server have this bit set to
1.
- (ws-error proc "Web Socket Fail: client must mask data"))
- (cond
- ((= pl 126) (setq pl (bits-to-int (bits 2))))
- ((= pl 127) (setq pl (bits-to-int (bits 8)))))
- (when mask (setq mask-key (subseq string index (incf index 4))))
- (setq data (concat data
- (ws/web-socket-mask
- mask-key (subseq string index (+ index
pl)))))
- ;; set to inactive
- (set-process-plist proc (plist-put (process-plist proc) :active
nil))
- (if fin
- (funcall my-handler data) ; call the web-socket handler
- ;; add parsed data to the process plist
- (let ((plist (process-plist proc)))
- (set-process-plist
- (plist-put plist :parsed (concat (plist-get plist :parsed)
- data))))
- ;; add any remaining un-parsed network data to pending
- (when (< (+ index pl) (length string))
- (let ((plist (process-plist proc)))
- (set-process-plist
- (plist-put plist :pending
- (concat (substring string (+ index pl))
- (or (plist-get plist :pending) ""))))))
- ;; possibly re-parse any pending input
- (when (plist-get (process-plist proc) :pending)
- (set-process-plist (plist-put (process-plist proc) :pending
nil))
- (ws-web-socket-filter
- proc (plist-get (process-plist proc) :pending)))))))))))
-
-(defmacro ws/web-socket-messages-do (headers variable body)
- "Helper macro to set the `ws-web-socket-filter' appropriately."
- `(when (assoc :SEC-WEBSOCKET-KEY ,(identity headers))
- (ws-response-header process 101
- (cons "Upgrade" "websocket")
- (cons "Connection" "upgrade")
- (cons "Sec-WebSocket-Accept"
- (ws-web-socket-handshake
- (cdr (assoc :SEC-WEBSOCKET-KEY headers)))))
- (set-process-plist process (list :parsed "" :pending nil :active nil))
- (set-process-coding-system process 'binary)
- (set-process-filter process (ws-make-web-socket-filter
- (lambda ,(list variable) ,@body)))
- (throw 'close-connection :keep-alive)))
-
-(defun ws/web-socket-send (string)
- )
+ (let ((index 0))
+ (cl-flet ((bits (length)
+ (apply #'append
+ (mapcar (lambda (int) (int-to-bits int 8))
+ (subseq string index (incf index
length))))))
+ (with-slots (process pending data handler new) message
+ (let (fin rsvs opcode mask pl mask-key)
+ ;; Parse fin bit, rsvs bits and opcode
+ (let ((byte (bits 1)))
+ (setq fin (car byte)
+ rsvs (subseq byte 1 4)
+ opcode
+ (let ((it (bits-to-int (subseq byte 4))))
+ (case it
+ (0 :CONTINUATION)
+ (1 :TEXT)
+ (2 :BINARY)
+ ((3 4 5 6 7) :NON-CONTROL)
+ (9 :PING)
+ (10 :PONG)
+ ((11 12 13 14 15) :CONTROL)
+ ;; If an unknown opcode is received, the receiving
+ ;; endpoint MUST _Fail the WebSocket Connection_.
+ (t (ws-error process
+ "Web Socket Fail: bad opcode %d" it))))))
+ (unless (cl-every #'null rsvs)
+ ;; MUST be 0 unless an extension is negotiated that defines
+ ;; meanings for non-zero values.
+ (ws-error process "Web Socket Fail: non-zero RSV 1 2 or 3"))
+ ;; Parse mask and payload length
+ (let ((byte (bits 1)))
+ (setq mask (car byte)
+ pl (bits-to-int (subseq byte 1))))
+ (unless (eq mask t)
+ ;; All frames sent from client to server have this bit set to 1.
+ (ws-error process "Web Socket Fail: client must mask data"))
+ (cond
+ ((= pl 126) (setq pl (bits-to-int (bits 2))))
+ ((= pl 127) (setq pl (bits-to-int (bits 8)))))
+ ;; unmask data
+ (when mask (setq mask-key (subseq string index (incf index 4))))
+ (setq data (concat data
+ (ws-web-socket-mask
+ mask-key (subseq string index (+ index pl)))))
+ (if fin
+ ;; wipe the message state and call the handler
+ (let ((it data))
+ (setq data "" active nil pending "" new nil)
+ (funcall handler it))
+ ;; add any remaining un-parsed network data to pending
+ (when (< (+ index pl) (length pending))
+ (setq pending (substring pending (+ index pl)))))))
+ ;; possibly re-parse any pending input
+ (when (new message) (ws-web-socket-parse-messages message)))))
+
+(defun ws-web-socket-send (string)
+ (message "TODO: send %S" string))
;;; Convenience functions to write responses
- [elpa] 71/119: updating notes and README text, (continued)
- [elpa] 71/119: updating notes and README text, Eric Schulte, 2014/03/10
- [elpa] 72/119: notes for running behind an Apache HTTPS proxy, Eric Schulte, 2014/03/10
- [elpa] 73/119: some more examples to implement, Eric Schulte, 2014/03/10
- [elpa] 68/119: no multiple concurrent entry of ws-parse-request, Eric Schulte, 2014/03/10
- [elpa] 76/119: and more web-socket progress, Eric Schulte, 2014/03/10
- [elpa] 74/119: beginning to implement web-socket support, Eric Schulte, 2014/03/10
- [elpa] 77/119: handle chunked receipt of web-socket messages, Eric Schulte, 2014/03/10
- [elpa] 78/119: hold, Eric Schulte, 2014/03/10
- [elpa] 79/119: beginning to add convenience macro for web sockets, Eric Schulte, 2014/03/10
- [elpa] 81/119: implemented ws-web-socket-frame to send replies, Eric Schulte, 2014/03/10
- [elpa] 80/119: helpers for handling web socket connections,
Eric Schulte <=
- [elpa] 82/119: web-sockets are working, Eric Schulte, 2014/03/10
- [elpa] 84/119: more examples, Eric Schulte, 2014/03/10
- [elpa] 85/119: renaming example files, Eric Schulte, 2014/03/10
- [elpa] 86/119: another example idea -- org export service, Eric Schulte, 2014/03/10
- [elpa] 87/119: update server stopping w/requests process field, Eric Schulte, 2014/03/10
- [elpa] 83/119: supports web sockets, Eric Schulte, 2014/03/10
- [elpa] 75/119: more web-socket implementation, Eric Schulte, 2014/03/10
- [elpa] 91/119: more tutorial, Eric Schulte, 2014/03/10
- [elpa] 94/119: example serving Org-mode files as JSON, Eric Schulte, 2014/03/10
- [elpa] 93/119: helper function to serve directory listings, Eric Schulte, 2014/03/10