| (in-package :cl-user) |
| (defpackage clack.handler.toot |
| (:use :cl |
| :toot |
| :split-sequence |
| :ppcre) |
| (:shadow :handle-request) |
| (:import-from :toot |
| :shutdown-p |
| :listen-socket |
| :listen-backlog |
| :acceptor-process |
| :accept-connections) |
| (:import-from :flexi-streams |
| :octets-to-string |
| :*substitution-char*) |
| (:import-from :alexandria |
| :if-let) |
| (:export :run)) |
| (in-package :clack.handler.toot) |
|
|
| (defun run (app &rest args |
| &key debug (address "127.0.0.1") (port 5000) |
| ssl ssl-key-file ssl-cert-file ssl-key-password) |
| "Start Toot server." |
| (cond |
| ((asdf::getenv "SERVER_STARTER_PORT") |
| (error "Toot handler doesn't work with Server::Starter.")) |
| ((getf args :fd) |
| (error ":fd is specified though Toot handler cannot listen on fd"))) |
|
|
| (let* ((stdout *standard-output*) |
| (errout *error-output*) |
| (acceptor (apply #'make-instance 'toot:acceptor |
| :handler (lambda (req) |
| (let ((env (handle-request req :ssl ssl)) |
| (*standard-output* stdout) |
| (*error-output* errout)) |
| (handle-response |
| req |
| (if debug |
| (restart-case |
| (funcall app env) |
| (throw-internal-server-error () |
| '(500 () ("Internal Server Error")))) |
| (handler-case (funcall app env) |
| (error (error) |
| (princ error *error-output*) |
| '(500 () ("Internal Server Error")))))))) |
| :address address |
| :port port |
| :access-logger nil |
| (if ssl |
| (list :ssl-certificate-file ssl-cert-file |
| :ssl-private-key-file ssl-key-file |
| :ssl-private-key-password ssl-key-password) |
| '())))) |
| (setf (shutdown-p acceptor) nil) |
| (setf (listen-socket acceptor) |
| (usocket:socket-listen |
| (or (address acceptor) usocket:*wildcard-host*) port |
| :reuseaddress t |
| :backlog (listen-backlog acceptor) |
| :element-type '(unsigned-byte 8))) |
| (setf (acceptor-process (toot:taskmaster acceptor)) (bt2:current-thread)) |
| (unwind-protect |
| (accept-connections acceptor) |
| (toot:stop-acceptor acceptor)))) |
|
|
| (defun handle-request (req &key ssl) |
| "Convert Request from server into a plist |
| before pass to Clack application." |
| (let ((content-length (if-let (content-length (request-header :content-length req)) |
| (parse-integer content-length :junk-allowed t) |
| (progn |
| (setf (slot-value req 'request-headers) (acons :content-length "" (slot-value req 'request-headers))) |
| nil)))) |
| (destructuring-bind (server-name &optional server-port) |
| (split-sequence #\: (cdr (assoc :host (request-headers req)))) |
| (list |
| :request-method (request-method req) |
| :script-name "" |
| :path-info (let ((flex:*substitution-char* #-abcl #\Replacement_Character |
| #+abcl #\?)) |
| (url-decode (request-path req))) |
| :server-name server-name |
| :server-port (if server-port |
| (parse-integer server-port) |
| 80) |
| :server-protocol (server-protocol req) |
| :request-uri (request-uri req) |
| :url-scheme (if ssl "https" "http") |
| :remote-addr (remote-addr req) |
| :remote-port (remote-port req) |
| :query-string (request-query req) |
| :content-length content-length |
| :content-type (request-header :content-type req) |
| :raw-body (toot::request-body-stream req) |
| :clack.streaming t |
| :clack.handler :toot |
| :headers (loop with headers = (make-hash-table :test 'equal) |
| for (k . v) in (toot::request-headers req) |
| unless (or (eq k :content-length) |
| (eq k :content-type)) |
| do (setf (gethash (string-downcase k) headers) v) |
| finally (return headers)))))) |
|
|
| (defun handle-response (req res) |
| (let ((no-body '#:no-body)) |
| (flet ((handle-normal-response (req res) |
| (destructuring-bind (status headers &optional (body no-body)) res |
| (when (pathnamep body) |
| (multiple-value-call #'serve-file |
| (values req body (parse-charset (getf headers :content-type)))) |
| (return-from handle-normal-response)) |
|
|
| (setf (status-code req) status) |
| (loop for (k v) on headers by #'cddr |
| if (eq k :set-cookie) |
| do (rplacd (last (toot::response-headers req)) |
| (list (cons k v))) |
| else if (eq k :content-type) do |
| (multiple-value-bind (v charset) |
| (parse-charset v) |
| (setf (response-header k req) v) |
| (setf (toot::response-charset req) charset)) |
| else if (response-header k req) do |
| (setf (response-header k req) |
| (format nil "~A, ~A" (response-header k req) v)) |
| else do |
| (setf (response-header k req) v)) |
| (toot::send-response-headers |
| req |
| (getf headers :content-length) |
| nil |
| (toot::response-charset req)) |
| (let ((out (toot::content-stream req))) |
| (when (eq body no-body) |
| (return-from handle-normal-response |
| (lambda (body &key (start 0) (end (length body)) (close nil)) |
| (declare (ignore close)) |
| (etypecase body |
| (null) |
| (string |
| (write-sequence (flex:string-to-octets body |
| :start start :end end |
| :external-format toot::*default-charset*) |
| out)) |
| ((vector (unsigned-byte 8)) |
| (write-sequence body out :start start :end end)))))) |
|
|
| (etypecase body |
| (null) |
| (list |
| (write-sequence (flex:string-to-octets (format nil "~{~A~}" body) |
| :external-format toot::*default-charset*) |
| out))))))) |
| (etypecase res |
| (list (handle-normal-response req res)) |
| (function (funcall res (lambda (res) |
| (handle-normal-response req res)))))))) |
|
|
| (defun parse-charset (content-type) |
| (multiple-value-bind (start end reg1 reg2) |
| (ppcre:scan "(;\\s*?charset=([-_a-zA-Z0-9]+))" content-type) |
| (declare (ignore end)) |
| (if start |
| (values (subseq content-type 0 (aref reg1 0)) |
| (subseq content-type (aref reg1 1) (aref reg2 1))) |
| |
| (values content-type toot::*default-charset*)))) |
|
|