cl-rw

Layered streams for Common Lisp
git clone https://logand.com/git/cl-rw.git/
Log | Files | Refs

commit 6585617e506123f0c619f49ba302f2bd7cdf3213
parent 325318efb57fb60a7d134c9e95843c6ce14ca586
Author: Tomas Hlavaty <tom@logand.com>
Date:   Sun, 22 Sep 2013 01:05:32 +0200

added rw.socket, rw.uri, rw.http; other improvements and fixes

Diffstat:
Mcl-rw.asd | 3+++
Ahttp.lisp | 292+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrw.lisp | 25+++++++++++++++++++++++--
Asocket.lisp | 38++++++++++++++++++++++++++++++++++++++
Mui.lisp | 41++++++++++++++++++++++++++++-------------
Auri.lisp | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 436 insertions(+), 15 deletions(-)

diff --git a/cl-rw.asd b/cl-rw.asd @@ -45,5 +45,8 @@ (:file "concurrency") (:file "css") (:file "html") + (:file "socket") + (:file "uri") + (:file "http") (:file "calendar") (:file "ui"))) diff --git a/http.lisp b/http.lisp @@ -0,0 +1,292 @@ +(defpackage :rw.http + (:use :cl) + (:export :client + :server)) + +(in-package :rw.http) + +(defun next-eol (reader) + (ecase (rw:next reader) + (#\newline :lf) + (#\return (case (rw:peek reader) + (#\newline (rw:next reader) :crlf) + (t :lf))))) + +(defun next-protocol (reader) + (let ((x (cdr (assoc (rw:till reader '(#\H #\T #\P #\/ #\1 #\. #\0) t) + '(((#\H #\T #\T #\P #\/ #\1 #\. #\0) . :http-1.0) + ((#\H #\T #\T #\P #\/ #\1 #\. #\1) . :http-1.1)) + :test #'equal)))) + (assert x) + x)) + +(defun next-status (reader) + (unless (member (rw:peek reader) '(#\return #'\newline)) + (values (prog1 (next-protocol reader) + (rw:skip reader)) + (prog1 (rw:next-z0 reader) + (rw:skip reader)) + (prog1 (coerce (rw:till reader '(#\return #'\newline)) 'string) ;; TODO better + (next-eol reader))))) + +(defun header-reader (reader) + (lambda () + (let ((k (rw:till reader '(#\: #\return #\newline)))) + (when k + (assert (eql #\: (rw:peek reader))) + (rw:next reader) + (rw:skip reader) + (prog1 (cons (coerce k 'string) ;; TODO better + (coerce (rw:till reader '(#\return #\newline)) 'string)) ;; TODO better + (next-eol reader)))))) + +(defun next-headers (reader) + (prog1 (rw:till (rw:peek-reader (header-reader reader))) + (next-eol reader))) + +(defun next-body (reader) ;; TODO better, respect content-length! + (coerce (rw:till reader) 'string)) + +(defun write-crlf (stream) + (write-char (code-char 13) stream) + (write-char (code-char 10) stream)) + +(defun write-headers (headers stream) + (dolist (x headers) + (format stream "~a: ~a" (car x) (cdr x)) + (write-crlf stream))) + +(defun write-protocol (stream protocol) + (write-string (ecase protocol + (:http-1.0 "HTTP/1.0") + (:http-1.1 "HTTP/1.1")) + stream)) + +(defun write-query (stream method protocol path query-string) + (write-string (ecase method + (:get "GET") + (:post "POST")) + stream) + (write-char #\space stream) + (write-string (or path "/") stream) + (when query-string + (write-char #\? stream) + (write-string query-string stream)) + (write-char #\space stream) + (write-protocol stream protocol) + (write-crlf stream)) + +(defun client1 (url &optional headers) + (destructuring-bind (&key scheme host port path query-string fragment) + (etypecase url + (list url) + (string (rw.uri:parse url))) + (declare (ignore fragment)) + (assert (equal "http" scheme)) + (with-open-stream (s (rw.socket:make-active-tcp-socket host (or port 80))) + (write-query s :get :http-1.0 path query-string) + (write-headers (or headers + `(("Host" . ,(if port + (format nil "~a:~a" host port) + host)))) + s) + (write-crlf s) + (finish-output s) + (let ((r (rw:peek-reader (rw:char-reader s)))) + (multiple-value-bind (protocol code message) (next-status r) + (values protocol code message (next-headers r) (next-body r))))))) + +(defun client (url &key headers (redirect 5)) + (do (protocol code message headers2 body) + ((< (decf redirect) 0) + (unless (minusp redirect) + (list protocol code message headers2 body))) + (multiple-value-setq (protocol code message headers2 body) + (client1 url headers)) + (if (member code '(302)) + (setq url (cdr (assoc "Location" headers2 :test #'equal))) ;; TODO update "Host" header + (setq redirect 0)))) + +;;(client "http://127.0.0.1:1234/") +;;(client "http://logand.com") +;;(client "http://logand.com:2234") + + + + +;; HTTP/1.1 302 Moved Temporarily^M +;; Content-Length: 369 +;; Date: Sat, 21 Sep 2013 13:41:11 GMT +;; Server: Hunchentoot 1.2.3 +;; Connection: Close +;; Location: http://NIL/?s=24rb7pccnd&a=0&c= +;; Content-Type: text/html; charset=iso-8859-1 + +;; <html><head><title>302 Moved Temporarily</title></head><body><h1>Moved Temporarily</h1>The document has moved <a href='http://NIL/?s=24rb7pccnd&amp;a=0&amp;c='>here</a><p><hr><address><a href='http://weitz.de/hunchentoot/'>Hunchentoot 1.2.3</a> <a href='http://openmcl.clozure.com/'>(Clozure Common Lisp Version 1.9-r15767 (LinuxARM32))</a></address></p></body></html>Connection closed by foreign host. + +(defun next-method (reader) + (let ((x (cdr (assoc (rw:till reader '(#\G #\E #\T #\P #\O #\S) t) + '(((#\G #\E #\T) . :get) + ((#\P #\O #\S #\T) . :post)) + :test #'equal)))) + (assert x) + x)) + +(defun next-query (reader) + (unless (member (rw:peek reader) '(#\return #'\newline)) + (flet ((str (y) + (when y + (coerce y 'string)))) + (values (prog1 (next-method reader) + (rw:skip reader)) + (prog1 (str (rw:till reader '(#\space #\return #\newline))) + (unless (member (rw:peek reader) '(#\return #\newline)) + (rw:skip reader '(#\space)))) + (prog1 (next-protocol reader) + (next-eol reader)))))) + +(defun write-status (stream protocol code message) + (write-protocol stream protocol) + (write-char #\space stream) + (princ code stream) + (write-char #\space stream) + (write-string (or message + ;; https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + (ecase code + (200 "OK") + (201 "Created") + (202 "Accepted") + (203 "Non-Authoritative Information (since HTTP/1.1)") + (204 "No Content") + (205 "Reset Content") + (206 "Partial Content") + (207 "Multi-Status (WebDAV; RFC 4918)") + (208 "Already Reported (WebDAV; RFC 5842)") + (226 "IM Used (RFC 3229)") + (300 "Multiple Choices") + (301 "Moved Permanently") + (302 "Found") + (303 "See Other (since HTTP/1.1)") + (304 "Not Modified") + (305 "Use Proxy (since HTTP/1.1)") + (306 "Switch Proxy") + (307 "Temporary Redirect (since HTTP/1.1)") + (308 "Permanent Redirect (approved as experimental RFC)[12]") + (400 "Bad Request") + (401 "Unauthorized") + (402 "Payment Required") + (403 "Forbidden") + (404 "Not Found") + (405 "Method Not Allowed") + (406 "Not Acceptable") + (407 "Proxy Authentication Required") + (408 "Request Timeout") + (409 "Conflict") + (410 "Gone") + (411 "Length Required") + (412 "Precondition Failed") + (413 "Request Entity Too Large") + (414 "Request-URI Too Long") + (415 "Unsupported Media Type") + (416 "Requested Range Not Satisfiable") + (417 "Expectation Failed") + (418 "I'm a teapot (RFC 2324)") + (419 "Authentication Timeout (not in RFC 2616)") + ;;(420 "Method Failure (Spring Framework)") + ;;(420 "Enhance Your Calm (Twitter)") + (422 "Unprocessable Entity (WebDAV; RFC 4918)") + (423 "Locked (WebDAV; RFC 4918)") + ;;(424 "Failed Dependency (WebDAV; RFC 4918)") + ;;(424 "Method Failure (WebDAV)[14]") + (425 "Unordered Collection (Internet draft)") + (426 "Upgrade Required (RFC 2817)") + (428 "Precondition Required (RFC 6585)") + (429 "Too Many Requests (RFC 6585)") + (431 "Request Header Fields Too Large (RFC 6585)") + (444 "No Response (Nginx)") + (449 "Retry With (Microsoft)") + (450 "Blocked by Windows Parental Controls (Microsoft)") + ;;(451 "Unavailable For Legal Reasons (Internet draft)") + ;;(451 "Redirect (Microsoft)") + (494 "Request Header Too Large (Nginx)") + (495 "Cert Error (Nginx)") + (496 "No Cert (Nginx)") + (497 "HTTP to HTTPS (Nginx)") + (499 "Client Closed Request (Nginx)") + (500 "Internal Server Error") + (501 "Not Implemented") + (502 "Bad Gateway") + (503 "Service Unavailable") + (504 "Gateway Timeout") + (505 "HTTP Version Not Supported") + (506 "Variant Also Negotiates (RFC 2295)") + (507 "Insufficient Storage (WebDAV; RFC 4918)") + (508 "Loop Detected (WebDAV; RFC 5842)") + (509 "Bandwidth Limit Exceeded (Apache bw/limited extension)") + (510 "Not Extended (RFC 2774)") + (511 "Network Authentication Required (RFC 6585)") + (598 "Network read timeout error (Unknown)") + (599 "Network connect timeout error (Unknown)"))) + stream) + (write-crlf stream)) + +(defun server-handler (stream handler) + (let ((r (rw:peek-reader (rw:char-reader stream)))) + (multiple-value-bind (method query protocol) (next-query r) + (let ((headers (next-headers r))) + (multiple-value-bind (protocol2 code message headers2 body) + (funcall handler :write stream method query protocol headers + (when (eq :post method) + #+nil ;; TODO post + (let ((n (cdr (assoc "Content-Length" headers :test #'equal)))) + (when n + (funcall handler + :read + (rw:shorter-reader + (rw:byte-reader stream) + (rw:next-z0 (rw:peek-reader (rw:reader n)))) + method + query + protocol + headers))))) + (write-status stream protocol2 code message) + (write-headers (or headers2 + '(("Connection" . "close") + ;;("Date" . "") + ;;("Last-Modified" . "") + ("Server" . "CL-RW"))) + stream) + (write-crlf stream) + (etypecase body + (null) + (string (write-string body stream)) + (function (funcall body stream)))))))) + +(defun server-loop (socket quit handler host port) + (do ((q (or quit (rw:reader '(nil t))))) + ((funcall q)) + (let ((c (ccl:accept-connection socket))) + (rw.concurrency:make-thread + (format nil "RW.HTTP:SERVER-HANDLER ~s ~s" host port) + (lambda () + (with-open-stream (c c) + (server-handler c handler))))))) + +;; TODO also without threads +;; TODO also thread limit +;; TODO also thread pool +(defun server (host port handler &key quit) + (let ((s (rw.socket:make-passive-tcp-socket host port))) + (rw.concurrency:make-thread + (format nil "RW.HTTP:SERVER-LOOP ~s ~s" host port) + (lambda () + (with-open-stream (s s) + (server-loop s quit handler host port)))))) + +(defun my-handler (msg stream method query protocol headers &optional body) + (ecase msg + (:read (rw:till (rw:peek-reader stream))) + (:write (values :http-1.0 200 nil nil + (prin1-to-string (list method query protocol headers body)))))) + +;;(server "0.0.0.0" 1567 'my-handler :quit (lambda () nil)) diff --git a/rw.lisp b/rw.lisp @@ -35,10 +35,12 @@ :next-u16 :next-u32 :next-u8 + :next-z0 :peek :peek-reader :reader :search-reader + :shorter-reader :skip :till :write-octets @@ -96,12 +98,17 @@ do (next reader))) reader))) -(defun till (reader &optional items) +(defun till (reader &optional items good) (loop - while (let ((x (peek reader))) (and x (not (member x items)))) + while (let ((x (peek reader))) + (and x + (if good + (member x items) + (not (member x items))))) collect (next reader))) ;;(till (peek-reader (reader '(0 1 2 3 4))) '(3)) +;;(till (peek-reader (reader '(0 1 2 3 4))) '(0 1) t) ;;(till (skip (peek-reader (reader '(0 1 2 3 4))) 1) '(3)) ;;(till (skip (peek-reader (reader #(0 1 2 3 4))) 1) '(3)) ;;(with-open-file (s "/etc/passwd") (till (peek-reader (char-reader s)) '(#\:))) @@ -142,6 +149,11 @@ (assert x)) (setf (aref z i) x))))) +(defun next-z0 (reader) + (let ((x (rw:till reader '(#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 \9) t))) + (when x + (parse-integer (coerce x 'string))))) ;; TODO better + ;; TODO next-u64|128 ;; TODO next-s8|16|32|64|128 @@ -212,3 +224,12 @@ z2 z))))))) ;;(till (peek-reader (fibonacci-reader 10))) => 0 1 1 2 3 5 8 13 21 34 + +(defun shorter-reader (reader size) + (if size + (let ((offset 0)) + (lambda () + (when (< offset size) + (incf offset) + (funcall reader)))) + reader)) diff --git a/socket.lisp b/socket.lisp @@ -0,0 +1,38 @@ +(defpackage :rw.socket + (:use :cl) + (:export :make-passive-tcp-socket + :make-active-tcp-socket)) + +(in-package :rw.socket) + +(defun make-passive-tcp-socket (host port) + #-ccl + (error "TODO port RW.SOCKET:MAKE-PASSIVE-TCP-SOCKET") + #+ccl + (ccl:make-socket :connect :passive + :address-family :internet + :type :stream + :format :bivalent ;; TODO :binary + :local-host host + :local-port port + :reuse-address t)) + +(defun make-active-tcp-socket (host port) + #-ccl + (error "TODO port RW.SOCKET:MAKE-ACTIVE-TCP-SOCKET") + #+ccl + (ccl:make-socket :connect :active + :address-family :internet + :type :stream + :format :bivalent ;; TODO :binary + :remote-host host + :remote-port port)) + +;; eol +;; keepalive nodelay broadcast linger +;; backlog class out-of-band-inline +;; local-filename remote-filename +;; sharing basic +;; external-format (auto-close t) +;; connect-timeout input-timeout output-timeout deadline +;; fd diff --git a/ui.lisp b/ui.lisp @@ -1,6 +1,7 @@ (defpackage :rw.ui (:use :cl) - (:export :checkbox + (:export :*http-server* + :checkbox :choice-widget :combo-item1-widget :combo-item2-widget @@ -25,6 +26,20 @@ (in-package :rw.ui) +(defvar *http-server*) + +(defun http-method () + (funcall *http-server* :method)) + +(defun http-post-parameters () + (funcall *http-server* :post-parameters)) + +(defun set-http-header (k v) + (funcall *http-server* :set-parameter k v)) + +(defun http-redirect (url) + (funcall *http-server* :redirect url)) + (defvar *click-link*) (defvar *click-form*) @@ -40,10 +55,10 @@ (parse-nat0 (subseq x (1+ i)))))))) (defun html-reply (form) - (setf (hunchentoot:content-type*) "text/html;charset=utf-8" - (hunchentoot:header-out "cache-control") "no-cache, no-store" - (hunchentoot:header-out "pragma") "no-cache" - (hunchentoot:header-out "expires") "-1") + (set-http-header "Content-Type" "text/html;charset=utf-8") + (set-http-header "cache-control" "no-cache, no-store") + (set-http-header "pragma" "no-cache") + (set-http-header "expires" "-1") (with-output-to-string (*standard-output*) (rw.html:html form))) @@ -95,7 +110,7 @@ (ecase (car form) (:redirect (destructuring-bind (target) (cdr form) - (hunchentoot:redirect target))) + (http-redirect target))) (:html (html-reply form)))) (defun make-stepper (sid create construct) @@ -106,9 +121,9 @@ (with-state (state aid (lambda () actions2) dispatch clear) ;;(print (list :@@@ (hunchentoot:query-string*))) (handle-form - (ecase (hunchentoot:request-method*) + (ecase (http-method) (:post - (dolist (x (hunchentoot:post-parameters*)) + (dolist (x (http-post-parameters)) (destructuring-bind (k &rest v) x (let ((kk (when (char= #\z (char k 0)) (parse36 (subseq k 1))))) @@ -151,11 +166,11 @@ (defparameter *session-lifespan* (* 60 60)) (defun make-session (sid create construct) - (let ((lock (bt:make-lock "session ~s")) + (let ((lock (rw.concurrency:make-lock "session ~s")) (touched (get-universal-time)) (stepper (make-stepper sid create construct))) (lambda (aid) - (bt:with-lock-held (lock) + (rw.concurrency:with-lock (lock) (cond ((eq t aid) (< (- (get-universal-time) touched) *session-lifespan*)) @@ -194,12 +209,12 @@ (defun make-pool () (let ((sessions (make-hash-table :test #'equal)) - (lock (bt:make-lock "pool ~s"))) + (lock (rw.concurrency:make-lock "pool ~s"))) (lambda (create deconstruct construct) (multiple-value-bind (sid aid *renv*) (funcall deconstruct) (setq aid (parse36 aid)) (funcall - (bt:with-lock-held (lock) + (rw.concurrency:with-lock (lock) (maphash (lambda (k v) (unless (funcall v t) (remhash k sessions))) @@ -212,7 +227,7 @@ (setf (gethash sid sessions) (make-session sid create construct)) (lambda () - (hunchentoot:redirect + (http-redirect (funcall construct sid (pretty36 0) *renv*))))))))))))) (defparameter *pool* (make-pool)) diff --git a/uri.lisp b/uri.lisp @@ -0,0 +1,52 @@ +(defpackage :rw.uri + (:use :cl) + (:export :parse + :parse-query-string)) + +(in-package :rw.uri) + +;; TODO http://www.w3.org/Addressing/URL/url-spec.txt +(defun parse (x) + (flet ((str (y) + (when y + (coerce y 'string)))) + (let ((r (rw:peek-reader (rw:reader x)))) + ;;scheme://host:port/path?query-string#fragment + (list :scheme (str (prog1 (rw:till r '(#\:)) + (assert (eql #\: (rw:next r))) + (assert (eql #\/ (rw:next r))) + (assert (eql #\/ (rw:next r))))) + :host (str (rw:till r '(#\: #\/))) + :port (when (eql #\: (rw:peek r)) + (rw:next r) + (rw:next-z0 r)) + :path (str (rw:till r '(#\?))) + :query-string (when (eql #\? (rw:peek r)) + (rw:next r) + (str (rw:till r '(#\#)))) + :fragment (when (eql #\# (rw:peek r)) + (rw:next r) + (str (rw:till r))))))) + +;;(parse "https://en.wikipedia.org/wiki/Uniform_Resource_Locator") +;;(parse "http://panda:1234/?s=24rb7pccnd&a=0&c=#hello#there") + +(defun query-string-pair-reader (reader) + (let ((r (rw:peek-reader reader))) + (lambda () + (when (eql #\& (rw:peek r)) + (rw:next r)) + (let ((k (rw:till r '(#\= #\&)))) + (when k + (flet ((str (y) ;; TODO better + (when y + (coerce y 'string)))) + (cons (str k) + (when (eql #\= (rw:next r)) + (str (rw:till r '(#\&))))))))))) + +(defun parse-query-string (x) + (when x + (rw:till (rw:peek-reader (query-string-pair-reader (rw:reader x)))))) + +;;(parse-query-string "s=24rb7pccnd&a=0&c=")