cl-ds / data /repos /clack /src /handler /toot.lisp
j14i's picture
3375 CL macro transformation examples from 85 libraries
43203b4 verified
(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) ;; nothing to response
(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)))
;; there is no ";charset="
(values content-type toot::*default-charset*))))