#!/usr/bin/env openlisp -shell
(require "setf")
(require "defstruct")
(require "datetime")
(require "logger")
(require "sysinfo")
(require "sort")
(require "url")
(require "http")
(defpackage #:httpd
(:use #:openlisp #:http #:logger)
(:export
#:*QUERY-LIST*
#:*QUERY-FORM*
#:*BODY-CONTENT*
#:*HEADER-LIST*
#:httpd
#:httpd-echo-binary-file
#:httpd-send-binary-file
#:httpd-send-file
#:httpd-shell)
)
(in-package #:httpd)
(defglobal *QUERY-LIST* nil)
(defglobal *QUERY-FORM* nil)
(defglobal *BODY-CONTENT* nil)
(defglobal *HEADER-LIST* nil)
(defmacro check-def (name value)
`(if (gboundp ',name) ,name ,value))
(defglobal *httpd-version* (check-def *httpd-version* "HTTP/1.1"))
(defglobal *httpd-root* (check-def *httpd-root* "."))
(defglobal *httpd-port* (check-def *httpd-port* 8080))
(defglobal *httpd-default* (check-def *httpd-default* "index.html"))
(defglobal *httpd-config* (check-def *httpd-config* "httpd.cnf"))
(defglobal *httpd-logfile* (check-def *httpd-logfile* t))
(defglobal *httpd-max-fd* (check-def *httpd-max-fd* 8))
(defglobal *httpd-timeout* (check-def *httpd-timeout* 1.0))
(defglobal *httpd-running* (check-def *httpd-running* nil))
(defglobal *httpd-pid-list* (check-def *httpd-pid-list* nil))
(defglobal *httpd-max-len* (check-def *httpd-max-len* #xFFFF))
(defglobal *httpd-try-fork* (check-def *httpd-try-fork* t))
(defglobal *httpd-nodelay* (check-def *httpd-nodelay* nil))
(defglobal *httpd-read-buffer* (check-def *httpd-read-buffer* nil))
(defglobal *httpd-debug-level* (check-def *httpd-debug-level* :info))
(defglobal *httpd-date* (check-def *httpd-date* (make-date)))
(defglobal *httpd-server* (check-def *httpd-server* nil))
(defglobal *httpd-force-close* (check-def *httpd-force-close* t))
(defglobal *httpd-bufsize* (check-def *httpd-bufsize* stdc:|OLMAXIOBUFLEN|))
(defglobal *httpd-loop* (check-def *httpd-loop* t))
(defun httpd-server-name ()
(or *httpd-server*
(let ((info (system-info)))
(setf *httpd-server*
(format () "Server: OpenLisp/~G (~A, ~A)"
(version)
(system-name)
(sysinfo-character-set info))))))
(defun httpd-get-debug-level ()
(if (getenv "OLHTTPDEBUG")
(let ((level (read-from-string (getenv "OLHTTPDEBUG"))))
(log-set-level level)
level)
*httpd-debug-level*))
(defun httpd-log (level fmt &rest args)
(when (and *httpd-logfile* (loggable-p (current-logger) level))
(apply #'logger-add-entry (current-logger) level fmt args)))
(defglobal *httpd-mime-types*
'(("txt" . "text/plain")
("html" . "text/html")
("htm" . "text/html")
("csv" . "text/csv")
("shtml" . "text/html")
("gif" . "image/gif")
("jpg" . "image/jpeg")
("jpeg" . "image/jpeg")
("png" . "image/png")
("pdf" . "application/pdf")
("lsp" . "application/lisp")
("json" . "application/json")
("doc" . "application/msword")
("rss" . "application/rss+xml")
("rtf" . "application/rtf")
("exe" . "application/octet-stream")
("odsp" . "application/odsp")
("xml" . "application/xml")
("dtd" . "application/dtd")
("tgz" . "application/x-tar")
("tar" . "application/x-tar")
("gz" . "application/x-gzip")
("gzip" . "application/x-gzip")
("zip" . "application/x-zip")
("bz2" . "application/x-bzip2")
("css" . "application/x-pointplus")
("cgi" . "server/cgi") ))
(defstruct (<request>
(:copier nil)
(:predicate nil))
header
body
type
version
connection)
(defglobal *httpd-client-list* ())
(defglobal *httpd-client-max* 0)
(defun httpd-add-socket (so)
(push (cons (get-universal-time) so) *httpd-client-list*))
(defun httpd-close-old-socket (sec)
(let ((len (length *httpd-client-list*)))
(when (> len *httpd-client-max*)
(setf *httpd-client-max* len))
(httpd-log :debug "Checking for dead sockets (size=~d/~d)"
len
*httpd-client-max*))
(let ((old (- (get-universal-time) sec))
(l (nreverse *httpd-client-list*)))
(while (and l (< (caar l) old))
(httpd-log :debug "Closing ~s (~ds)." (cdar l) sec)
(close (cdar l))
(setf l (cdr l)))
(setf *httpd-client-list* (nreverse l))))
(defun httpd-error-handler (error)
(when (eq (class-of error) (class <user-interruption>))
(httpd-log :notice "^C received, shutdown HTTPD server.")
(let ((pidfile (httpd-pid-file *httpd-port*)))
(when (probe-file pidfile)
(delete-file pidfile)))
(end))
(httpd-log :err "caught: ~s" error)
(when (log-loggable-p :debug)
(httpd-log :debug "Stack-Trace:")
(let ((stack -1))
(dolist (frame (stack-trace 10))
(httpd-log :debug
" [~2,'0d] (~s ...)" (incf stack) frame)))))
(defun httpd-server-handler (error)
(httpd-error-handler error)
(throw 'httpd-server-handler nil))
(defun httpd-client-handler (error)
(httpd-error-handler error)
(throw 'httpd-client-handler 500))
(defun httpd (port rootdir &rest kill-prev)
(when (probe-file *httpd-config*)
(load *httpd-config*))
(with-logger (if (eq *httpd-logfile* t)
(create (class <console-logger>)
:level (httpd-get-debug-level))
(create (class <file-logger>)
:level (httpd-get-debug-level)
:file *httpd-logfile*))
(setf *httpd-root* (or rootdir "."))
(setf *httpd-port* port)
(when *httpd-config*
(httpd-log :notice "Using config: ~s." *httpd-config*))
(let ((pidfile (httpd-pid-file *httpd-port*))
(pid 0))
(when (and kill-prev (probe-file pidfile))
(with-open-input-file (st pidfile)
(setf pid (read st))
(httpd-log :warning "Kill previous server, PID=~s." pid)
(kill pid 0)
(sleep 500)))
(with-open-output-file (st pidfile)
(format st "~a" (getpid))))
(while *httpd-loop*
(catch 'httpd-server-handler
(with-handler #'httpd-server-handler
(httpd-server))))))
(defun httpd-pid-file (port)
(format () "httpd-~4,'0d.pid" port))
(defun httpd-running-p ()
*httpd-running*)
(defun httpd-get-root ()
*httpd-root*)
(defun httpd-set-root (root)
(setf *httpd-root* root))
(defun httpd-get-port ()
*httpd-port*)
(defun httpd-set-port (port)
(setf *httpd-port* port))
(defun httpd-server ()
(setf *httpd-read-buffer* (create-string *httpd-bufsize* #\space))
(unless (directoryp "/tmp")
(create-directory "/tmp"))
(with-server-socket (server *httpd-port* :tcp)
(let ((fdsock (create-vector *httpd-max-fd* nil))
(fdwrite (create-vector *httpd-max-fd* nil))
(fdread (create-vector *httpd-max-fd* nil))
(timeout *httpd-timeout*))
(declare (ignore fdwrite)) (if (dynamic *quiet-mode*)
(unless (log-loggable-p :debug)
(log-set-level :err))
(httpd-welcome))
(setf *httpd-running* t)
(select-clear fdsock)
(select-add server fdsock)
(while *httpd-running*
(when *httpd-pid-list*
(httpd-check-exit))
(when *httpd-force-close*
(httpd-close-old-socket 2))
(case (select *httpd-max-fd* fdsock fdread () () timeout)
((-1)
(httpd-log :err "Server error on SELECT .."))
((0)
t)
(t
#|
(when (and (log-loggable-p :debug)
(probe-file "httpd.lsp"))
(httpd-log :notice "reload httpd.lsp ..")
(load "httpd.lsp"))
|#
(httpd-loop-connect server fdread))))
(setf *httpd-read-buffer* ())
(select-remove server fdsock))))
(defun httpd-make-temp ()
(format () "/tmp/httpd~d.tmp" (getpid)))
(defun httpd-loop-connect (server fdread)
(for ((i 0 (1+ i)))
((>= i *httpd-max-fd*))
(when (elt fdread i)
(httpd-fork-request server))))
(defun httpd-welcome ()
(httpd-log :info "OpenLisp ~a HTTPD Server." (version))
(httpd-log :info "Hostname: ~a" (get-host-name))
(httpd-log :info "Address: ~a" (get-host-address (get-host-name)))
(httpd-log :info "Port: ~a" *httpd-port*)
(httpd-log :info "Root dir: ~s" *httpd-root*)
(httpd-log :info "Charset: ~a" (if (= (system::character-size) 1)
"ANSI"
"UNICODE"))
(httpd-log :info "CWD: '~a'" (current-directory))
(unless (or (dynamic *quiet-mode*) (null *httpd-logfile*))
(format (standard-output) ";; OpenLisp ~a HTTPD on '~a'~%"
(version)
(get-host-name)))
(unless (dynamic *quiet-mode*)
(when (stringp *httpd-logfile*)
(format (standard-output)
";; Log file: '~a'~%" *httpd-logfile*))
(format (standard-output) ";; Hit ^C (Ctrl-C) to stop.~%")))
(defun httpd-fork-request (server)
(let ((pid (if *httpd-try-fork* (fork) -1)))
(case pid
((-1) (httpd-request server))
((0) (httpd-log :debug "child, pid=~d" (getpid))
(httpd-request server)
(end 0))
(t (push pid *httpd-pid-list*)
(httpd-log :debug "pid ~d, forked process id=~d"
(getpid) pid)
pid))))
(defun httpd-check-exit ()
(dolist (pid *httpd-pid-list*)
(let ((res (waitpid pid 'WNOHANG))
(errno 0))
(cond
((> res 0)
(httpd-log :debug
"process pid=~d exited res=~d." pid res)
(setf *httpd-pid-list* (remove pid *httpd-pid-list*)))
((= res 0)
t)
(t
(setf *httpd-pid-list* (remove pid *httpd-pid-list*))
(setf errno (system-errno))
(httpd-log :debug
"process pid=~d exited with code ~d"
pid
errno))))))
(defun httpd-request (server)
(let ((client (accept server))
(time (get-internal-run-time)))
(if client
(unwind-protect
(progn
(socket-nonblocking client nil)
(socket-receive-timeout client *httpd-timeout*)
(socket-send-timeout client *httpd-timeout*)
(when (and *httpd-nodelay*
(null (socket-nodelay client t)))
(httpd-log :warning
"NODELAY not supported!!"))
(catch 'httpd-exit-status
(httpd-process-request
client
(httpd-get-request client))))
(httpd-log :debug "Elapsed time: ~s."
(quotient (- (get-internal-run-time) time)
(internal-time-units-per-second)))
(cond
(*httpd-force-close*
(httpd-log :debug "Close socket")
(close client))
(t
(httpd-log :debug "Add socket to pool")
(httpd-add-socket client)))
t)
(httpd-log :err "Error, can't accept connection"))))
(defun httpd-file-type (file)
(let ((start 0)
(stop nil)
(len (length file)))
(for ((i 0 (+ i 1)))
((= i len))
(when (or (char= (elt file i) #\.)
(char= (elt file i) #\/))
(setf start i))
(when (char= (elt file i) #\?)
(setf stop i)
(setf i (- len 1))))
(if stop
(subseq file (+ start 1) stop)
(subseq file (+ start 1)))))
(defun httpd-get-request (client)
(let ((request (httpd-read-request-header client))
(header nil)
(type nil))
(setf header (request-header request))
(when (consp header)
(setf type (car header)))
(cond
((null type)
(httpd-send-status client 400))
((not (member (car type) '(:GET :POST :HEAD)))
(httpd-log :err "Unsupported ~a method " (car type))
(httpd-send-status client 400))
(t
(setf (request-type request) type)
request))))
(defun httpd-read-request-header (client)
(let ((header nil)
(body nil)
(connection nil)
(line nil)
(version nil)
(len 0))
(while (and (setf line (read-line client nil nil))
(> (length line) 0))
(push line header))
(setf header (http-decode-header (nreverse header)))
(setf len (assoc :content-length header))
(cond
((and (consp len) (cdr len) (consp (cdr len)))
(setf len (read-from-string (cadr len)))
(when (or (not (integerp len)) (> len *httpd-max-len*))
(httpd-send-status client 413))
(socket-nonblocking client t)
(setf body (create-string len #\space))
(receive client body)
(socket-nonblocking client nil))
((assoc :POST header)
(httpd-send-status client 411)))
(setf connection (cassoc :connection header))
(setf line (nth 0 header))
(setf version (or (member "HTTP/1.1" line :test #'equal)
(member "HTTP/1.0" line :test #'equal)))
(if (consp version)
(setf version (car version))
(setf version "HTTP/1.0"))
(when (and (string-equal *httpd-version* "HTTP/1.1")
(string-equal version "HTTP/1.1")
(null (assoc :host header)))
(httpd-log :warning "Host: was missing from HTTP/1.1 request")
t)
(make-request :header header
:body body
:version version
:connection connection)))
(defun httpd-send-header (client mime-type file)
(with-output-to-string (so)
(http-format-nl so "~a 200 OK" *httpd-version*)
(http-format-nl so "~a" (httpd-server-name))
(http-format-nl so "Date: ~A" (get-internal-date *httpd-date* :gmt))
(http-format-nl so "Connection: close")
(http-format-nl so "Content-Type: ~A" mime-type)
(cond
((null file)
(http-format-nl so "Last-Modified: ~A~%"
(get-internal-date *httpd-date* :gmt)))
((probe-file file)
(http-format-nl so "Last-Modified: ~A"
(file-date file *httpd-date* :gmt))
(http-format-nl so "Content-Length: ~D"
(file-length file 8)))
((stringp file)
(http-format-nl so "Content-Length: ~D" (length file)))
(t
(http-format-nl so "Last-Modified: ~A~%"
(get-internal-date *httpd-date* :gmt))))
(http-format-nl so)
(send client (get-output-stream-string so))))
(defun httpd-reply-header (client status msg)
(with-output-to-string (so)
(http-format-nl so "~a ~d ~a~%" *httpd-version* status msg)
(http-format-nl so "~a" (httpd-server-name))
(http-format-nl so "Date: ~A~%" (get-internal-date *httpd-date* :gmt))
(http-format-nl so "Connection: close")
(http-format-nl so)
(send client (get-output-stream-string so))))
(defun httpd-send-status (client status)
(httpd-reply-header client status (http-status-message status))
(httpd-log :debug "Status = ~a." status)
(unless (< status 400)
(throw 'httpd-exit-status nil)))
(defun httpd-check-valid-mime-type (client file)
(if (null file)
(httpd-send-status client 404)
(or
(cassoc (httpd-file-type file) *httpd-mime-types* :test #'equal)
"text/plain")))
(defun httpd-query-to-alist (query)
(mapcar (lambda (x)
(setf x (string-split "=" x))
(cons (first x) (second x)))
(string-split "&" (url:url-decoder query))))
(defun httpd-process-request (client request)
(let* ((type (request-type request))
(header (request-header request))
(method nil)
(file nil)
(content nil)
(mdate nil)
(udate nil)
(args nil)
(mimetype nil))
(setf method (nth 0 type))
(setf file (nth 1 type))
(setf *BODY-CONTENT* nil)
(setf *HEADER-LIST* nil)
(setf *QUERY-FORM* nil)
(setf *QUERY-LIST* nil)
(httpd-log :debug "QUERY: ~a" type)
(httpd-log :debug "HEADER: ~a" request)
(setf args (string-split "?" (string-append *httpd-root* file)))
(setf file (pop args))
(when (directoryp file)
(let ((index file))
(unless (member (elt file (1- (length file)))
'(#\/ #\/))
(setf index (string-append file "/")))
(setf index (string-append index *httpd-default*))
(when (probe-file index)
(setf file index))))
(httpd-log :debug "URI: ~a" file)
(when args
(setf *QUERY-LIST* (httpd-query-to-alist (car args)))
(httpd-log :debug "Args: ~s" *QUERY-LIST*))
(setf mimetype (httpd-check-valid-mime-type client file))
(if *QUERY-LIST*
(httpd-log :debug "~a ~s ~s" method file *QUERY-LIST*)
(httpd-log :debug "~a ~s" method file))
(case (and mimetype method)
((:GET)
(httpd-set-connection-options client request)
(httpd-log :info "Process 'GET ~a' (~a)" file mimetype)
(cond
((string-index ".." file)
(httpd-send-status client 403))
((or (equal mimetype "server/cgi")
(eq (string-index "application/" mimetype) 0))
(httpd-send-file client file mimetype))
((eq (string-index "application/" mimetype) 0)
(httpd-send-file client file mimetype))
((directoryp file)
(let ((html (httpd-directory file)))
(httpd-send-header client "text/html" html)
(send client html)))
((probe-file file)
(setf mdate (cassoc :If-Modified-Since header))
(setf udate (cassoc :If-Unmodified-Since header))
(cond
((and mdate
(date<= (file-date file nil :gmt) mdate))
#|
(httpd-log :debug "~s m ~s" file mdate)
(httpd-log :debug "~s f ~s" file
(file-date file nil :gmt))
|#
(httpd-send-status client 304))
((and udate
(date> (file-date file nil :gmt) udate))
(httpd-send-status client 304))
(t
(httpd-send-file client file mimetype))))
((or (regmatch "eval-hook.html$" file)
(regmatch "eval-hook.asp$" file))
(socket-nonblocking client nil)
(httpd-send-header client "text/plain" nil)
(format client "~%~%GC: ~S~%~%" (gc t))
(pretty 'httpd-process-request client)
t)
(t
(httpd-send-status client 404))))
((:POST)
(setf *HEADER-LIST* (request-header request))
(setf *BODY-CONTENT* (request-body request))
(setf content (cassoc :Content-Type *HEADER-LIST*))
(httpd-log :debug "Process POST ~a type: ~a" file mimetype)
(when (consp content)
(when (equal "application/x-www-form-urlencoded"
(car content))
(ignore-errors
(setq *QUERY-FORM*
(httpd-query-to-alist *BODY-CONTENT*))))
(httpd-log :debug "Query form: ~s" *QUERY-FORM*))
(when (log-loggable-p :debug)
(httpd-log :debug "Header:")
(dolist (x *HEADER-LIST*)
(httpd-log :debug "~a" x))
(httpd-log :debug "Body:~%~a" *BODY-CONTENT*))
#| Should we send 100 (Continue) ?
(httpd-send-status client 100)
|#
(cond
((probe-file file)
(httpd-send-file client file mimetype))
(t
(httpd-send-status client 404))))
((:HEAD)
(httpd-log :info "Process HEAD ~a type: ~a" file mimetype)
(setf file (string-append *httpd-root* file))
(if (probe-file file)
(httpd-send-header client mimetype file)
(httpd-send-status client 404)))
((:CONNECT :DELETE :OPTIONS :PUT :TRACE)
(httpd-send-status client 501))
(t
(httpd-send-status client 405)))
t))
(defun httpd-set-connection-options (client request)
(case-using #'string-equal (request-connection request)
(("close")
(httpd-log :debug "'Connection: close' option not handled.")
nil)
(("Keep-Alive")
(socket-keepalive client t))
(t
nil)))
(defun httpd-send-binary-file (socket file type)
(let ((fd (stdc:fopen file "rb"))
(buf (or *httpd-read-buffer* (create-string *httpd-bufsize* #\Space)))
(loop t)
(sum 0)
(len 0))
(cond
(fd
(httpd-send-header socket type file)
(unwind-protect
(while (and loop
(> (setf len
(stdc:fread buf 1 *httpd-bufsize* fd))
0))
(setf sum (+ sum len))
(unless (send socket buf len)
(httpd-log :err
"SEND error: err=~a, len=~a"
(system-errno)
len)
(setf loop nil)))
(stdc:fclose fd)))
(t
(httpd-send-status socket 404)))))
(defun httpd-echo-binary-file (file)
(let ((fd (stdc:fopen file "rb"))
(buf (or *httpd-read-buffer* (create-string *httpd-bufsize* #\Space)))
(out (convert (standard-output) <external>))
(len 0))
(cond
(fd
(unwind-protect
(while (> (setf len
(stdc:fread buf 1 *httpd-bufsize* fd))
0)
(stdc:fwrite buf 1 len out))
(stdc:fclose fd)))
(t
(httpd-log :err "Can't echo: ~s." file)))))
(defun httpd-send-file (client file type)
(let ((tmp nil))
(case-using #'string-equal type
(("image/gif" "image/jpeg" "image/png")
(httpd-send-binary-file client file type))
(("application/lisp" "server/cgi")
(setf tmp (httpd-make-temp))
(with-open-output-file (strm tmp)
(with-standard-output strm
(ignore-errors (load file))))
(httpd-send-binary-file client tmp "text/html"))
(("application/odsp")
(setf tmp (httpd-make-temp))
(with-open-output-file (strm tmp)
(with-standard-output strm
(ignore-errors (odsp file))))
(httpd-send-binary-file client tmp "text/html"))
(t
(httpd-send-binary-file client file type)))
(when (and tmp
(probe-file tmp)
(not (log-loggable-p :debug)))
(delete-file tmp))))
(defun httpd-directory (dir)
(let ((dir-list ())
(path ()))
(dolist (name (expand-pathname dir))
(unless (directoryp (string-append dir "/" name))
(push name dir-list)))
(setf dir-list (sortl dir-list))
(with-output-to-string (so)
(format so "<html>~%")
(format so " <head>~%")
(format so " <title>Index of ~a</title>~%" dir)
(format so " </head>~%")
(format so "<body>~%")
(format so " <h1>Index of ~a</h1>~%" dir)
(format so " <pre>~%")
(format so "Size Name~%")
(format so "<hr />~%")
(dolist (name dir-list)
(setf path (string-append dir "/" name))
(format so "~9d <a href=\"~a\">~a</a>~%"
(file-length path 8)
path
name))
(format so "</pre>~%")
(format so "<hr />~%")
(format so "<i>OpenLisp v~g [~a]"
(version)
(get-internal-date nil :gmt))
(format so "</body>~%")
(format so "</html>~%")
(get-output-stream-string so))))
(defun httpd-shell ()
(when (and (not *httpd-running*) (regmatch "http" $0))
(let ((port *httpd-port*)
(root *httpd-root*)
(toplevel nil)
(cmd $0))
(while (and $1 (eql #\- (elt $1 0)))
(case-using #'string-equal $1
(("-quiet")
(shift))
(("-toplevel")
(setf toplevel t)
(shift))
(("-port")
(setf port $2)
(shift)
(shift))
(("-root")
(setf root $2)
(shift)
(shift))
(t (error "HTTPD: unsupported option ~s" $1))))
(when (stringp port)
(setf port (ignore-errors (parse-number port)))
(unless port
(format (error-output) "~a: Invalid port.~%" cmd)))
(unless toplevel
(httpd port root)))))
(provide "httpd")
(httpd-shell)