summaryrefslogtreecommitdiff
path: root/elpa/simple-httpd-20191103.1446/simple-httpd.el
diff options
context:
space:
mode:
Diffstat (limited to 'elpa/simple-httpd-20191103.1446/simple-httpd.el')
-rw-r--r--elpa/simple-httpd-20191103.1446/simple-httpd.el904
1 files changed, 904 insertions, 0 deletions
diff --git a/elpa/simple-httpd-20191103.1446/simple-httpd.el b/elpa/simple-httpd-20191103.1446/simple-httpd.el
new file mode 100644
index 0000000..fcdc33f
--- /dev/null
+++ b/elpa/simple-httpd-20191103.1446/simple-httpd.el
@@ -0,0 +1,904 @@
+;;; simple-httpd.el --- pure elisp HTTP server
+
+;; This is free and unencumbered software released into the public domain.
+
+;; Author: Christopher Wellons <wellons@nullprogram.com>
+;; URL: https://github.com/skeeto/emacs-http-server
+;; Package-Version: 20191103.1446
+;; Package-Commit: 22ce66ea43e0eadb9ec1d691a35d9695fc29cee6
+;; Version: 1.5.1
+;; Package-Requires: ((cl-lib "0.3"))
+
+;;; Commentary:
+
+;; Use `httpd-start' to start the web server. Files are served from
+;; `httpd-root' on port `httpd-port' using `httpd-ip-family' at host
+;; `httpd-host'. While the root can be changed at any time, the server
+;; needs to be restarted in order for a port change to take effect.
+
+;; Everything is performed by servlets, including serving
+;; files. Servlets are enabled by setting `httpd-servlets' to true
+;; (default). Servlets are four-parameter functions that begin with
+;; "httpd/" where the trailing component specifies the initial path on
+;; the server. For example, the function `httpd/hello-world' will be
+;; called for the request "/hello-world" and "/hello-world/foo".
+
+;; The default servlet `httpd/' is the one that serves files from
+;; `httpd-root' and can be turned off through redefinition or setting
+;; `httpd-serve-files' to nil. It is used even when `httpd-servlets'
+;; is nil.
+
+;; The four parameters for a servlet are process, URI path, GET/POST
+;; arguments (alist), and the full request object (header
+;; alist). These are ordered by general importance so that some can be
+;; ignored. Two macros are provided to help with writing servlets.
+
+;; * `with-httpd-buffer' -- Creates a temporary buffer that is
+;; automatically served to the client at the end of the body.
+;; Additionally, `standard-output' is set to this output
+;; buffer. For example, this servlet says hello,
+
+;; (defun httpd/hello-world (proc path &rest args)
+;; (with-httpd-buffer proc "text/plain"
+;; (insert "hello, " (file-name-nondirectory path))))
+
+;; This servlet be viewed at http://localhost:8080/hello-world/Emacs
+
+;; * `defservlet' -- Similar to the above macro but totally hides the
+;; process object from the servlet itself. The above servlet can be
+;; re-written identically like so,
+
+;; (defservlet hello-world text/plain (path)
+;; (insert "hello, " (file-name-nondirectory path)))
+
+;; Note that `defservlet' automatically sets `httpd-current-proc'. See
+;; below.
+
+;; The "function parameters" part can be left empty or contain up to
+;; three parameters corresponding to the final three servlet
+;; parameters. For example, a servlet that shows *scratch* and doesn't
+;; need parameters,
+
+;; (defservlet scratch text/plain ()
+;; (insert-buffer-substring (get-buffer-create "*scratch*")))
+
+;; A higher level macro `defservlet*' wraps this lower-level
+;; `defservlet' macro, automatically binding variables to components
+;; of the request. For example, this binds parts of the request path
+;; and one query parameter. Request components not provided by the
+;; client are bound to nil.
+
+;; (defservlet* packages/:package/:version text/plain (verbose)
+;; (insert (format "%s\n%s\n" package version))
+;; (princ (get-description package version))
+;; (when verbose
+;; (insert (format "%S" (get-dependencies package version)))))
+
+;; It would be accessed like so,
+
+;; http://example.com/packages/foobar/1.0?verbose=1
+
+;; Some support functions are available for servlets for more
+;; customized responses.
+
+;; * `httpd-send-file' -- serve a file with proper caching
+;; * `httpd-redirect' -- redirect the browser to another url
+;; * `httpd-send-header' -- send custom headers
+;; * `httpd-error' -- report an error to the client
+;; * `httpd-log' -- log an object to *httpd*
+
+;; Some of these functions require a process object, which isn't
+;; passed to `defservlet' servlets. Use t in place of the process
+;; argument to use `httpd-current-proc' (like `standard-output').
+
+;; If you just need to serve static from some location under some
+;; route on the server, use `httpd-def-file-servlet'. It expands into
+;; a `defservlet' that serves files.
+
+;;; History:
+
+;; Version 1.5.1: improvements
+;; * Add `httpd-running-p'
+;; * Properly handle "Connection: close" and HTTP/1.0
+;; Version 1.5.0: improvements
+;; * Drastically improved performance for large requests
+;; * More HTTP status codes
+;; Version 1.4.6: fixes
+;; * Added httpd-serve-directory
+;; * Fix some encoding issues
+;; Version 1.4.5: fixes
+;; * Update to cl-lib from cl
+;; Version 1.4.4: features
+;; * Common Lisp &key-like defservlet* argument support
+;; * Fix up some defservlet* usage warnings.
+;; Version 1.4.3: features
+;; * Add `httpd-discard-buffer'
+;; * Add `httpd-def-file-servlet'
+;; * Be more careful about not sending extra headers
+;; Version 1.4.2: features, fixes
+;; * `defservlet*' macro
+;; Version 1.4.1: small bug fixes, one feature
+;; * All mime-type parameters now accept string designators
+;; * Documentation update
+;; Version 1.4.0: features, API change, and fixes
+;; * Removed httpd-send-buffer; httpd-send-header now does this implicitly
+;; * httpd-send-header now accepts keywords instead
+;; * Fix httpd-clean-path in Windows
+;; * Fix a content-length bug
+;; * defservlet fontification
+;; Version 1.3.1: features and fixes
+;; * Set `standard-output' in `with-httpd-buffer'
+;; Version 1.3.0: security fix
+;; * Fix path expansion security issue
+;; * Fix coding system (don't default)
+;; Version 1.2.4: fixes
+;; * Handle large POSTs
+;; * Fix date strings
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'pp)
+(require 'url-util)
+
+(defgroup simple-httpd nil
+ "A simple web server."
+ :group 'comm)
+
+(defcustom httpd-ip-family 'ipv4
+ "Web server IP family used by `make-network-process'."
+ :group 'simple-httpd
+ :type 'symbol)
+
+(defcustom httpd-host nil
+ "Web server host name used by `make-network-process'."
+ :group 'simple-httpd
+ :type '(choice (const nil) (const local) string))
+
+(defcustom httpd-port 8080
+ "Web server port."
+ :group 'simple-httpd
+ :type 'integer)
+
+(defcustom httpd-root "~/public_html"
+ "Web server file root."
+ :group 'simple-httpd
+ :type 'directory)
+
+(defcustom httpd-serve-files t
+ "Enable serving files from `httpd-root'."
+ :group 'simple-httpd
+ :type 'boolean)
+
+(defcustom httpd-listings t
+ "If true, serve directory listings."
+ :group 'simple-httpd
+ :type 'boolean)
+
+(defcustom httpd-servlets t
+ "Enable servlets."
+ :group 'simple-httpd
+ :type 'boolean)
+
+(defcustom httpd-show-backtrace-when-error nil
+ "If true, show backtrace on error page."
+ :group 'simple-httpd
+ :type 'boolean)
+
+(defcustom httpd-start-hook nil
+ "Hook to run when the server has started."
+ :group 'simple-httpd
+ :type 'hook)
+
+(defcustom httpd-stop-hook nil
+ "Hook to run when the server has stopped."
+ :group 'simple-httpd
+ :type 'hook)
+
+(defvar httpd-server-name (format "simple-httpd (Emacs %s)" emacs-version)
+ "String to use in the Server header.")
+
+(defvar httpd-mime-types
+ '(("png" . "image/png")
+ ("gif" . "image/gif")
+ ("jpg" . "image/jpeg")
+ ("jpeg" . "image/jpeg")
+ ("tif" . "image/tif")
+ ("tiff" . "image/tiff")
+ ("ico" . "image/x-icon")
+ ("svg" . "image/svg+xml")
+ ("css" . "text/css; charset=utf-8")
+ ("htm" . "text/html; charset=utf-8")
+ ("html" . "text/html; charset=utf-8")
+ ("xml" . "text/xml; charset=utf-8")
+ ("rss" . "text/xml; charset=utf-8")
+ ("atom" . "text/xml; charset=utf-8")
+ ("txt" . "text/plain; charset=utf-8")
+ ("el" . "text/plain; charset=utf-8")
+ ("js" . "text/javascript; charset=utf-8")
+ ("md" . "text/x-markdown; charset=utf-8")
+ ("gz" . "application/octet-stream")
+ ("ps" . "application/postscript")
+ ("eps" . "application/postscript")
+ ("pdf" . "application/pdf")
+ ("tar" . "application/x-tar")
+ ("zip" . "application/zip")
+ ("mp3" . "audio/mpeg")
+ ("wav" . "audio/x-wav")
+ ("flac" . "audio/flac")
+ ("spx" . "audio/ogg")
+ ("oga" . "audio/ogg")
+ ("ogg" . "audio/ogg")
+ ("ogv" . "video/ogg")
+ ("mp4" . "video/mp4")
+ ("mkv" . "video/x-matroska")
+ ("webm" . "video/webm"))
+ "MIME types for headers.")
+
+(defvar httpd-indexes
+ '("index.html"
+ "index.htm"
+ "index.xml")
+ "File served by default when accessing a directory.")
+
+(defvar httpd-status-codes
+ '((100 . "Continue")
+ (101 . "Switching Protocols")
+ (102 . "Processing")
+ (200 . "OK")
+ (201 . "Created")
+ (202 . "Accepted")
+ (203 . "Non-authoritative Information")
+ (204 . "No Content")
+ (205 . "Reset Content")
+ (206 . "Partial Content")
+ (207 . "Multi-Status")
+ (208 . "Already Reported")
+ (226 . "IM Used")
+ (300 . "Multiple Choices")
+ (301 . "Moved Permanently")
+ (302 . "Found")
+ (303 . "See Other")
+ (304 . "Not Modified")
+ (305 . "Use Proxy")
+ (307 . "Temporary Redirect")
+ (308 . "Permanent Redirect")
+ (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 . "Payload Too Large")
+ (414 . "Request-URI Too Long")
+ (415 . "Unsupported Media Type")
+ (416 . "Requested Range Not Satisfiable")
+ (417 . "Expectation Failed")
+ (418 . "I'm a teapot")
+ (421 . "Misdirected Request")
+ (422 . "Unprocessable Entity")
+ (423 . "Locked")
+ (424 . "Failed Dependency")
+ (426 . "Upgrade Required")
+ (428 . "Precondition Required")
+ (429 . "Too Many Requests")
+ (431 . "Request Header Fields Too Large")
+ (444 . "Connection Closed Without Response")
+ (451 . "Unavailable For Legal Reasons")
+ (499 . "Client Closed Request")
+ (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")
+ (507 . "Insufficient Storage")
+ (508 . "Loop Detected")
+ (510 . "Not Extended")
+ (511 . "Network Authentication Required")
+ (599 . "Network Connect Timeout Error"))
+ "HTTP status codes.")
+
+(defvar httpd-html
+ '((403 . "<!DOCTYPE html>
+<html><head>
+<title>403 Forbidden</title>
+</head><body>
+<h1>Forbidden</h1>
+<p>The requested URL is forbidden.</p>
+<pre>%s</pre>
+</body></html>")
+ (404 . "<!DOCTYPE html>
+<html><head>
+<title>404 Not Found</title>
+</head><body>
+<h1>Not Found</h1>
+<p>The requested URL was not found on this server.</p>
+<pre>%s</pre>
+</body></html>")
+ (500 . "<!DOCTYPE html>
+<html><head>
+<title>500 Internal Error</title>
+</head><body>
+<h1>500 Internal Error</h1>
+<p>Internal error when handling this request.</p>
+<pre>%s</pre>
+</body></html>"))
+ "HTML for various errors.")
+
+;; User interface
+
+;;;###autoload
+(defun httpd-start ()
+ "Start the web server process. If the server is already
+running, this will restart the server. There is only one server
+instance per Emacs instance."
+ (interactive)
+ (httpd-stop)
+ (httpd-log `(start ,(current-time-string)))
+ (make-network-process
+ :name "httpd"
+ :service httpd-port
+ :server t
+ :host httpd-host
+ :family httpd-ip-family
+ :filter 'httpd--filter
+ :coding 'binary
+ :log 'httpd--log)
+ (run-hooks 'httpd-start-hook))
+
+;;;###autoload
+(defun httpd-stop ()
+ "Stop the web server if it is currently running, otherwise do nothing."
+ (interactive)
+ (when (process-status "httpd")
+ (delete-process "httpd")
+ (httpd-log `(stop ,(current-time-string)))
+ (run-hooks 'httpd-stop-hook)))
+
+;;;###autoload
+(defun httpd-running-p ()
+ "Return non-nil if the simple-httpd server is running."
+ (not (null (process-status "httpd"))))
+
+;;;###autoload
+(defun httpd-serve-directory (directory)
+ "Start the web server with given `directory' as `httpd-root'."
+ (interactive "DServe directory: \n")
+ (setf httpd-root directory)
+ (httpd-start)
+ (message "Started simple-httpd on %s:%d, serving: %s"
+ (cl-case httpd-host
+ ((nil) "0.0.0.0")
+ ((local) "localhost")
+ (otherwise httpd-host)) httpd-port directory))
+
+(defun httpd-batch-start ()
+ "Never returns, holding the server open indefinitely for batch mode.
+Logs are redirected to stdout. To use, invoke Emacs like this:
+emacs -Q -batch -l simple-httpd.elc -f httpd-batch-start"
+ (if (not noninteractive)
+ (error "Only use `httpd-batch-start' in batch mode!")
+ (httpd-start)
+ (defalias 'httpd-log 'pp)
+ (while t (sleep-for 60))))
+
+;; Utility
+
+(defun httpd-date-string (&optional date)
+ "Return an HTTP date string (RFC 1123)."
+ (format-time-string "%a, %e %b %Y %T GMT" date t))
+
+(defun httpd-etag (file)
+ "Compute the ETag for FILE."
+ (concat "\"" (substring (sha1 (prin1-to-string (file-attributes file))) -16)
+ "\""))
+
+(defun httpd--stringify (designator)
+ "Turn a string designator into a string."
+ (let ((string (format "%s" designator)))
+ (if (keywordp designator)
+ (substring string 1)
+ string)))
+
+;; Networking code
+
+(defun httpd--connection-close-p (request)
+ "Return non-nil if the client requested \"connection: close\"."
+ (or (equal '("close") (cdr (assoc "Connection" request)))
+ (equal '("HTTP/1.0") (cddr (assoc "GET" request)))))
+
+(defun httpd--filter (proc chunk)
+ "Runs each time client makes a request."
+ (with-current-buffer (process-get proc :request-buffer)
+ (setf (point) (point-max))
+ (insert chunk)
+ (let ((request (process-get proc :request)))
+ (unless request
+ (when (setf request (httpd-parse))
+ (delete-region (point-min) (point))
+ (process-put proc :request request)))
+ (when request
+ (let ((content-length (cadr (assoc "Content-Length" request))))
+ (when (or (null content-length)
+ (= (buffer-size) (string-to-number content-length)))
+ (let* ((content (buffer-string))
+ (uri (cl-cadar request))
+ (parsed-uri (httpd-parse-uri (concat uri)))
+ (uri-path (httpd-unhex (nth 0 parsed-uri)))
+ (uri-query (append (nth 1 parsed-uri)
+ (httpd-parse-args content)))
+ (servlet (httpd-get-servlet uri-path)))
+ (erase-buffer)
+ (process-put proc :request nil)
+ (setf request (nreverse (cons (list "Content" content)
+ (nreverse request))))
+ (httpd-log `(request (date ,(httpd-date-string))
+ (address ,(car (process-contact proc)))
+ (get ,uri-path)
+ ,(cons 'headers request)))
+ (if (null servlet)
+ (httpd--error-safe proc 404)
+ (condition-case error-case
+ (funcall servlet proc uri-path uri-query request)
+ (error (httpd--error-safe proc 500 error-case))))
+ (when (httpd--connection-close-p request)
+ (process-send-eof proc)))))))))
+
+(defun httpd--log (server proc message)
+ "Runs each time a new client connects."
+ (with-current-buffer (generate-new-buffer " *httpd-client*")
+ (process-put proc :request-buffer (current-buffer)))
+ (set-process-sentinel proc #'httpd--sentinel)
+ (httpd-log (list 'connection (car (process-contact proc)))))
+
+(defun httpd--sentinel (proc message)
+ "Runs when a client closes the connection."
+ (unless (string-match-p "^open " message)
+ (let ((buffer (process-get proc :request-buffer)))
+ (when buffer
+ (kill-buffer buffer)))))
+
+;; Logging
+
+(defun httpd-log (item)
+ "Pretty print a lisp object to the log."
+ (with-current-buffer (get-buffer-create "*httpd*")
+ (setf buffer-read-only nil)
+ (let ((follow (= (point) (point-max))))
+ (save-excursion
+ (goto-char (point-max))
+ (pp item (current-buffer)))
+ (if follow (goto-char (point-max))))
+ (setf truncate-lines t
+ buffer-read-only t)
+ (set-buffer-modified-p nil)))
+
+;; Servlets
+
+(defvar httpd-current-proc nil
+ "The process object currently in use.")
+
+(defvar httpd--header-sent nil
+ "Buffer-local variable indicating if the header has been sent.")
+(make-variable-buffer-local 'httpd--header-sent)
+
+(defun httpd-resolve-proc (proc)
+ "Return the correct process to use. This handles `httpd-current-proc'."
+ (if (eq t proc) httpd-current-proc proc))
+
+(defmacro with-httpd-buffer (proc mime &rest body)
+ "Create a temporary buffer, set it as the current buffer, and,
+at the end of body, automatically serve it to an HTTP client with
+an HTTP header indicating the specified MIME type. Additionally,
+`standard-output' is set to this output buffer and
+`httpd-current-proc' is set to PROC."
+ (declare (indent defun))
+ (let ((proc-sym (make-symbol "--proc--")))
+ `(let ((,proc-sym ,proc))
+ (with-temp-buffer
+ (setf major-mode 'httpd-buffer)
+ (let ((standard-output (current-buffer))
+ (httpd-current-proc ,proc-sym))
+ ,@body)
+ (unless httpd--header-sent
+ (httpd-send-header ,proc-sym ,mime 200))))))
+
+(defun httpd-discard-buffer ()
+ "Don't respond using current server buffer (`with-httpd-buffer').
+Returns a process for future response."
+ (when (eq major-mode 'httpd-buffer) (setf httpd--header-sent t))
+ httpd-current-proc)
+
+(defmacro defservlet (name mime path-query-request &rest body)
+ "Defines a simple httpd servelet. The servlet runs in a
+temporary buffer which is automatically served to the client
+along with a header.
+
+A servlet that serves the contents of *scratch*,
+
+ (defservlet scratch text/plain ()
+ (insert-buffer-substring (get-buffer-create \"*scratch*\")))
+
+A servlet that says hello,
+
+ (defservlet hello-world text/plain (path)
+ (insert \"hello, \" (file-name-nondirectory path))))"
+ (declare (indent defun))
+ (let ((proc-sym (make-symbol "proc"))
+ (fname (intern (concat "httpd/" (symbol-name name)))))
+ `(defun ,fname (,proc-sym ,@path-query-request &rest ,(cl-gensym))
+ (with-httpd-buffer ,proc-sym ,(httpd--stringify mime)
+ ,@body))))
+
+(defun httpd-parse-endpoint (symbol)
+ "Parse an endpoint definition template for use with `defservlet*'."
+ (cl-loop for item in (split-string (symbol-name symbol) "/")
+ for n upfrom 0
+ when (and (> (length item) 0) (eql (aref item 0) ?:))
+ collect (cons (intern (substring item 1)) n) into vars
+ else collect item into path
+ finally
+ (cl-return
+ (cl-values (intern (mapconcat #'identity path "/")) vars))))
+
+(defvar httpd-path nil
+ "Anaphoric variable for `defservlet*'.")
+
+(defvar httpd-query nil
+ "Anaphoric variable for `defservlet*'.")
+
+(defvar httpd-request nil
+ "Anaphoric variable for `defservlet*'.")
+
+(defvar httpd-split-path nil
+ "Anaphoric variable for `defservlet*'.")
+
+(defmacro defservlet* (endpoint mime args &rest body)
+ "Like `defservlet', but automatically bind variables/arguments
+to the request. Trailing components of the ENDPOINT can be bound
+by prefixing these components with a colon, acting like a template.
+
+ (defservlet* packages/:package/:version text/plain (verbose)
+ (insert (format \"%s\\n%s\\n\" package version))
+ (princ (get-description package version))
+ (when verbose
+ (insert (format \"%S\" (get-dependencies package version)))))
+
+When accessed from this URL,
+
+ http://example.com/packages/foobar/1.0?verbose=1
+
+the variables package, version, and verbose will be bound to the
+associated components of the URL. Components not provided are
+bound to nil. The query arguments can use the Common Lisp &key
+form (variable default provided-p).
+
+ (defservlet* greeting/:name text/plain ((greeting \"hi\" greeting-p))
+ (princ (format \"%s, %s (provided: %s)\" greeting name greeting-p)))
+
+The original path, query, and request can be accessed by the
+anaphoric special variables `httpd-path', `httpd-query', and
+`httpd-request'."
+ (declare (indent defun))
+ (let ((path-lexical (cl-gensym))
+ (query-lexical (cl-gensym))
+ (request-lexical (cl-gensym)))
+ (cl-multiple-value-bind (path vars) (httpd-parse-endpoint endpoint)
+ `(defservlet ,path ,mime (,path-lexical ,query-lexical ,request-lexical)
+ (let ((httpd-path ,path-lexical)
+ (httpd-query ,query-lexical)
+ (httpd-request ,request-lexical)
+ (httpd-split-path (split-string
+ (substring ,path-lexical 1) "/")))
+ (let ,(cl-loop for (var . pos) in vars
+ for extract =
+ `(httpd-unhex (nth ,pos httpd-split-path))
+ collect (list var extract))
+ (let ,(cl-loop for arg in args
+ for has-default = (listp arg)
+ for has-default-p = (and has-default
+ (= 3 (length arg)))
+ for arg-name = (symbol-name
+ (if has-default (cl-first arg) arg))
+ when has-default collect
+ (list (cl-first arg)
+ `(let ((value (assoc ,arg-name httpd-query)))
+ (if value
+ (cl-second value)
+ ,(cl-second arg))))
+ else collect
+ (list arg `(cl-second
+ (assoc ,arg-name httpd-query)))
+ when has-default-p collect
+ (list (cl-third arg)
+ `(not (null (assoc ,arg-name httpd-query)))))
+ ,@body)))))))
+
+(font-lock-add-keywords
+ 'emacs-lisp-mode
+ '(("(\\<\\(defservlet\\*?\\)\\> +\\([^ ()]+\\) +\\([^ ()]+\\)"
+ (1 'font-lock-keyword-face)
+ (2 'font-lock-function-name-face)
+ (3 'font-lock-type-face))))
+
+(defmacro httpd-def-file-servlet (name root)
+ "Defines a servlet that serves files from ROOT under the route NAME.
+
+ (httpd-def-file-servlet my/www \"/var/www/\")
+
+Automatically handles redirects and uses `httpd-serve-root' to
+actually serve up files."
+ (let* ((short-root (directory-file-name (symbol-name name)))
+ (path-root (concat short-root "/"))
+ (chop (length path-root)))
+ `(defservlet ,name nil (uri-path query request)
+ (setf httpd--header-sent t) ; Don't actually use this temp buffer
+ (if (= (length uri-path) ,chop)
+ (httpd-redirect t ,path-root)
+ (let ((path (substring uri-path ,chop)))
+ (httpd-serve-root t ,root path request))))))
+
+;; Request parsing
+
+(defun httpd--normalize-header (header)
+ "Destructively capitalize the components of HEADER."
+ (mapconcat #'capitalize (split-string header "-") "-"))
+
+(defun httpd-parse ()
+ "Parse HTTP header in current buffer into association list.
+Leaves the point at the start of the request content. Returns nil
+if it failed to parse a complete HTTP header."
+ (setf (point) (point-min))
+ (when (looking-at "\\([^ ]+\\) +\\([^ ]+\\) +\\([^\r]+\\)\r\n")
+ (let ((method (match-string 1))
+ (path (decode-coding-string (match-string 2) 'iso-8859-1))
+ (version (match-string 3))
+ (headers ()))
+ (setf (point) (match-end 0))
+ (while (looking-at "\\([-!#-'*+.0-9A-Z^_`a-z|~]+\\): *\\([^\r]+\\)\r\n")
+ (setf (point) (match-end 0))
+ (let ((name (match-string 1))
+ (value (match-string 2)))
+ (push (list (httpd--normalize-header name)
+ (decode-coding-string value 'iso-8859-1)) headers)))
+ (when (looking-at "\r\n")
+ (setf (point) (match-end 0))
+ (cons (list method path version) (nreverse headers))))))
+
+(defun httpd-unhex (str)
+ "Fully decode the URL encoding in STR (including +'s)."
+ (when str
+ (let ((nonplussed (replace-regexp-in-string (regexp-quote "+") " " str)))
+ (decode-coding-string (url-unhex-string nonplussed t) 'utf-8))))
+
+(defun httpd-parse-args (argstr)
+ "Parse a string containing URL encoded arguments."
+ (unless (zerop (length argstr))
+ (mapcar (lambda (str)
+ (mapcar 'httpd-unhex (split-string str "=")))
+ (split-string argstr "&"))))
+
+(defun httpd-parse-uri (uri)
+ "Split a URI into its components.
+The first element of the return value is the script path, the
+second element is an alist of variable/value pairs, and the third
+element is the fragment."
+ (let ((p1 (string-match (regexp-quote "?") uri))
+ (p2 (string-match (regexp-quote "#") uri))
+ retval)
+ (push (if p2 (httpd-unhex (substring uri (1+ p2)))) retval)
+ (push (if p1 (httpd-parse-args (substring uri (1+ p1) p2))) retval)
+ (push (substring uri 0 (or p1 p2)) retval)))
+
+(defun httpd-escape-html-buffer ()
+ "Escape current buffer contents to be safe for inserting into HTML."
+ (setf (point) (point-min))
+ (while (search-forward-regexp "[<>&]" nil t)
+ (replace-match
+ (cl-case (aref (match-string 0) 0)
+ (?< "&lt;")
+ (?> "&gt;")
+ (?& "&amp;")))))
+
+(defun httpd-escape-html (string)
+ "Escape STRING so that it's safe to insert into an HTML document."
+ (with-temp-buffer
+ (insert string)
+ (httpd-escape-html-buffer)
+ (buffer-string)))
+
+;; Path handling
+
+(defun httpd-status (path)
+ "Determine status code for PATH."
+ (cond
+ ((not (file-exists-p path)) 404)
+ ((not (file-readable-p path)) 403)
+ ((and (file-directory-p path) (not httpd-listings)) 403)
+ (200)))
+
+(defun httpd-clean-path (path)
+ "Clean dangerous .. from PATH and remove the leading slash."
+ (let* ((sep (if (member system-type '(windows-nt ms-dos)) "[/\\]" "/"))
+ (split (delete ".." (split-string path sep)))
+ (unsplit (mapconcat 'identity (delete "" split) "/")))
+ (concat "./" unsplit)))
+
+(defun httpd-gen-path (path &optional root)
+ "Translate GET to secure path in ROOT (`httpd-root')."
+ (let ((clean (expand-file-name (httpd-clean-path path) (or root httpd-root))))
+ (if (file-directory-p clean)
+ (let* ((dir (file-name-as-directory clean))
+ (indexes (cl-mapcar (apply-partially 'concat dir) httpd-indexes))
+ (existing (cl-remove-if-not 'file-exists-p indexes)))
+ (or (car existing) dir))
+ clean)))
+
+(defun httpd-get-servlet (uri-path)
+ "Determine the servlet to be executed for URI-PATH."
+ (if (not httpd-servlets)
+ 'httpd/
+ (cl-labels ((cat (x)
+ (concat "httpd/" (mapconcat 'identity (reverse x) "/"))))
+ (let ((parts (cdr (split-string (directory-file-name uri-path) "/"))))
+ (or
+ (cl-find-if 'fboundp (mapcar 'intern-soft
+ (cl-maplist #'cat (reverse parts))))
+ 'httpd/)))))
+
+(defun httpd-serve-root (proc root uri-path &optional request)
+ "Securely serve a file from ROOT from under PATH."
+ (let* ((path (httpd-gen-path uri-path root))
+ (status (httpd-status path)))
+ (cond
+ ((not (= status 200)) (httpd-error proc status))
+ ((file-directory-p path) (httpd-send-directory proc path uri-path))
+ (t (httpd-send-file proc path request)))))
+
+(defun httpd/ (proc uri-path query request)
+ "Default root servlet which serves files when httpd-serve-files is T."
+ (if (and httpd-serve-files httpd-root)
+ (httpd-serve-root proc httpd-root uri-path request)
+ (httpd-error proc 403)))
+
+(defun httpd-get-mime (ext)
+ "Fetch MIME type given the file extention."
+ (or (and ext (cdr (assoc (downcase ext) httpd-mime-types)))
+ "application/octet-stream"))
+
+;; Data sending functions
+
+(defun httpd-send-header (proc mime status &rest header-keys)
+ "Send an HTTP header with given MIME type and STATUS, followed
+by the current buffer. If PROC is T use the `httpd-current-proc'
+as the process.
+
+Extra headers can be sent by supplying them like keywords, i.e.
+
+ (httpd-send-header t \"text/plain\" 200 :X-Powered-By \"simple-httpd\")"
+ (let ((status-str (cdr (assq status httpd-status-codes)))
+ (headers `(("Server" . ,httpd-server-name)
+ ("Date" . ,(httpd-date-string))
+ ("Connection" . "keep-alive")
+ ("Content-Type" . ,(httpd--stringify mime))
+ ("Content-Length" . ,(httpd--buffer-size)))))
+ (unless httpd--header-sent
+ (setf httpd--header-sent t)
+ (with-temp-buffer
+ (insert (format "HTTP/1.1 %d %s\r\n" status status-str))
+ (cl-loop for (header value) on header-keys by #'cddr
+ for header-name = (substring (symbol-name header) 1)
+ for value-name = (format "%s" value)
+ collect (cons header-name value-name) into extras
+ finally (setf headers (nconc headers extras)))
+ (dolist (header headers)
+ (insert (format "%s: %s\r\n" (car header) (cdr header))))
+ (insert "\r\n")
+ (process-send-region (httpd-resolve-proc proc)
+ (point-min) (point-max)))
+ (process-send-region (httpd-resolve-proc proc)
+ (point-min) (point-max)))))
+
+(defun httpd-redirect (proc path &optional code)
+ "Redirect the client to PATH (default 301). If PROC is T use
+the `httpd-current-proc' as the process."
+ (httpd-log (list 'redirect path))
+ (httpd-discard-buffer)
+ (with-temp-buffer
+ (httpd-send-header proc "text/plain" (or code 301) :Location path)))
+
+(defun httpd-send-file (proc path &optional req)
+ "Serve file to the given client. If PROC is T use the
+`httpd-current-proc' as the process."
+ (httpd-discard-buffer)
+ (let ((req-etag (cadr (assoc "If-None-Match" req)))
+ (etag (httpd-etag path))
+ (mtime (httpd-date-string (nth 4 (file-attributes path)))))
+ (if (equal req-etag etag)
+ (with-temp-buffer
+ (httpd-log `(file ,path not-modified))
+ (httpd-send-header proc "text/plain" 304))
+ (httpd-log `(file ,path))
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally path)
+ (httpd-send-header proc (httpd-get-mime (file-name-extension path))
+ 200 :Last-Modified mtime :ETag etag)))))
+
+(defun httpd-send-directory (proc path uri-path)
+ "Serve a file listing to the client. If PROC is T use the
+`httpd-current-proc' as the process."
+ (httpd-discard-buffer)
+ (let ((title (concat "Directory listing for "
+ (url-insert-entities-in-string uri-path))))
+ (if (equal "/" (substring uri-path -1))
+ (with-temp-buffer
+ (httpd-log `(directory ,path))
+ (insert "<!DOCTYPE html>\n")
+ (insert "<html>\n<head><title>" title "</title></head>\n")
+ (insert "<body>\n<h2>" title "</h2>\n<hr/>\n<ul>")
+ (dolist (file (directory-files path))
+ (unless (eq ?. (aref file 0))
+ (let* ((full (expand-file-name file path))
+ (tail (if (file-directory-p full) "/" ""))
+ (f (url-insert-entities-in-string file))
+ (l (url-hexify-string file)))
+ (insert (format "<li><a href=\"%s%s\">%s%s</a></li>\n"
+ l tail f tail)))))
+ (insert "</ul>\n<hr/>\n</body>\n</html>")
+ (httpd-send-header proc "text/html; charset=utf-8" 200))
+ (httpd-redirect proc (concat uri-path "/")))))
+
+(defun httpd--buffer-size (&optional buffer)
+ "Get the buffer size in bytes."
+ (let ((orig enable-multibyte-characters)
+ (size 0))
+ (with-current-buffer (or buffer (current-buffer))
+ (set-buffer-multibyte nil)
+ (setf size (buffer-size))
+ (if orig (set-buffer-multibyte orig)))
+ size))
+
+(defun httpd-error (proc status &optional info)
+ "Send an error page appropriate for STATUS to the client,
+optionally inserting object INFO into page. If PROC is T use the
+`httpd-current-proc' as the process."
+ (httpd-discard-buffer)
+ (httpd-log `(error ,status ,info))
+ (with-temp-buffer
+ (let ((html (or (cdr (assq status httpd-html)) ""))
+ (contents
+ (if (not info)
+ ""
+ (with-temp-buffer
+ (let ((standard-output (current-buffer)))
+ (insert "error: ")
+ (princ info)
+ (insert "\n")
+ (when httpd-show-backtrace-when-error
+ (insert "backtrace: ")
+ (princ (backtrace))
+ (insert "\n"))
+ (httpd-escape-html-buffer)
+ (buffer-string))))))
+ (insert (format html contents)))
+ (httpd-send-header proc "text/html" status)))
+
+(defun httpd--error-safe (&rest args)
+ "Call httpd-error and report failures to *httpd*."
+ (condition-case error-case
+ (apply #'httpd-error args)
+ (error (httpd-log `(hard-error ,error-case)))))
+
+(provide 'simple-httpd)
+
+;;; simple-httpd.el ends here