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:
M | cl-rw.asd | | | 3 | +++ |
A | http.lisp | | | 292 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | rw.lisp | | | 25 | +++++++++++++++++++++++-- |
A | socket.lisp | | | 38 | ++++++++++++++++++++++++++++++++++++++ |
M | ui.lisp | | | 41 | ++++++++++++++++++++++++++++------------- |
A | uri.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&a=0&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=")