]> code.delx.au - gnu-emacs-elpa/blob - emacs-web-server.el
fleshed out some more tests
[gnu-emacs-elpa] / emacs-web-server.el
1 ;;; emacs-web-server.el --- Emacs Web Server
2
3 ;; Copyright (C) 2013 Eric Schulte <schulte.eric@gmail.com>
4
5 ;; Author: Eric Schulte <schulte.eric@gmail.com>
6 ;; Keywords: http
7 ;; License: GPLV3 (see the COPYING file in this directory)
8
9 ;;; Code:
10 (require 'emacs-web-server-status-codes)
11 (require 'mail-parse) ; to parse multipart data in headers
12 (require 'mm-encode) ; to look-up mime types for files
13 (require 'eieio)
14 (eval-when-compile (require 'cl))
15 (require 'cl-lib)
16
17 (defclass ews-server ()
18 ((handler :initarg :handler :accessor handler :initform nil)
19 (process :initarg :process :accessor process :initform nil)
20 (port :initarg :port :accessor port :initform nil)
21 (clients :initarg :clients :accessor clients :initform nil)))
22
23 (defclass ews-client ()
24 ((leftover :initarg :leftover :accessor leftover :initform "")
25 (boundary :initarg :boundary :accessor boundary :initform nil)
26 (headers :initarg :headers :accessor headers :initform (list nil))))
27
28 (defvar ews-servers nil
29 "List holding all ews servers.")
30
31 (defvar ews-time-format "%Y.%m.%d.%H.%M.%S.%N"
32 "Logging time format passed to `format-time-string'.")
33
34 (defun ews-start (handler port &optional log-buffer &rest network-args)
35 "Start a server using HANDLER and return the server object.
36
37 HANDLER should be a list of cons of the form (MATCH . ACTION),
38 where MATCH is either a function (in which case it is called on
39 the request object) or a cons cell of the form (KEYWORD . STRING)
40 in which case STRING is matched against the value of the header
41 specified by KEYWORD. In either case when MATCH returns non-nil,
42 then the function ACTION is called with two arguments, the
43 process and the request object.
44
45 Any supplied NETWORK-ARGS are assumed to be keyword arguments for
46 `make-network-process' to which they are passed directly.
47
48 For example, the following starts a simple hello-world server on
49 port 8080.
50
51 (ews-start
52 '(((:GET . \".*\") .
53 (lambda (proc request)
54 (process-send-string proc
55 \"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello world\r\n\")
56 t)))
57 8080)
58
59 Equivalently, the following starts an identical server using a
60 function MATCH and the `ews-response-header' convenience
61 function.
62
63 (ews-start
64 '(((lambda (_) t) .
65 (lambda (proc request)
66 (ews-response-header proc 200 '(\"Content-type\" . \"text/plain\"))
67 (process-send-string proc \"hello world\")
68 t)))
69 8080)
70
71 "
72 (let ((server (make-instance 'ews-server :handler handler :port port))
73 (log (when log-buffer (get-buffer-create log-buffer))))
74 (setf (process server)
75 (apply
76 #'make-network-process
77 :name "ews-server"
78 :service (port server)
79 :filter 'ews-filter
80 :server t
81 :nowait t
82 :family 'ipv4
83 :plist (append (list :server server)
84 (when log (list :log-buffer log)))
85 :log (when log
86 (lambda (proc client message)
87 (let ((c (process-contact client))
88 (buf (plist-get (process-plist proc) :log-buffer)))
89 (with-current-buffer buf
90 (goto-char (point-max))
91 (insert (format "%s\t%s\t%s\t%s"
92 (format-time-string ews-time-format)
93 (first c) (second c) message))))))
94 network-args))
95 (push server ews-servers)
96 server))
97
98 (defun ews-stop (server)
99 "Stop SERVER."
100 (setq ews-servers (remove server ews-servers))
101 (mapc #'delete-process (append (mapcar #'car (clients server))
102 (list (process server)))))
103
104 (defun ews-parse (proc string)
105 (cl-flet ((to-keyword (s) (intern (concat ":" (upcase (match-string 1 s))))))
106 (cond
107 ((string-match
108 "^\\(GET\\|POST\\) \\([^[:space:]]+\\) \\([^[:space:]]+\\)$" string)
109 (list (cons (to-keyword (match-string 1 string)) (match-string 2 string))
110 (cons :TYPE (match-string 3 string))))
111 ((string-match "^\\([^[:space:]]+\\): \\(.*\\)$" string)
112 (list (cons (to-keyword string) (match-string 2 string))))
113 (:otherwise (ews-error proc "bad header: %S" string) nil))))
114
115 (defun ews-trim (string)
116 (while (and (> (length string) 0)
117 (or (and (string-match "[\r\n]" (substring string -1))
118 (setq string (substring string 0 -1)))
119 (and (string-match "[\r\n]" (substring string 0 1))
120 (setq string (substring string 1))))))
121 string)
122
123 (defun ews-parse-multipart/form (string)
124 ;; ignore empty and non-content blocks
125 (when (string-match "Content-Disposition:[[:space:]]*\\(.*\\)\r\n" string)
126 (let ((dp (mail-header-parse-content-disposition (match-string 1 string))))
127 (cons (cdr (assoc 'name (cdr dp)))
128 (ews-trim (substring string (match-end 0)))))))
129
130 (defun ews-filter (proc string)
131 (with-slots (handler clients) (plist-get (process-plist proc) :server)
132 (unless (assoc proc clients)
133 (push (cons proc (make-instance 'ews-client)) clients))
134 (let ((c (cdr (assoc proc clients))))
135 (when (not (eq (catch 'close-connection
136 (if (ews-do-filter proc c string)
137 (ews-call-handler proc (cdr (headers c)) handler)
138 :keep-open))
139 :keep-open))
140 (setq clients (assq-delete-all proc clients))
141 (delete-process proc)))))
142
143 (defun ews-do-filter (proc client string)
144 "Return non-nil when finished and the client may be deleted."
145 (with-slots (leftover boundary headers) client
146 (let ((pending (concat leftover string))
147 (delimiter (if boundary
148 (regexp-quote (concat "\r\n--" boundary))
149 "\r\n"))
150 (last-index 0) index tmp-index)
151 (catch 'finished-parsing-headers
152 ;; parse headers and append to client
153 (while (setq index (string-match delimiter pending last-index))
154 (let ((tmp (+ index (length delimiter))))
155 (cond
156 ;; Double \r\n outside of post data means we are done
157 ;; w/headers and should call the handler.
158 ((and (not boundary) (= last-index index))
159 (throw 'finished-parsing-headers t))
160 ;; Parse a URL
161 ((eq boundary :application/x-www-form-urlencoded)
162 (mapc (lambda (p)
163 (when (string-match "=" p)
164 (setcdr (last headers)
165 (list (cons (substring p 0 (match-beginning 0))
166 (substring p (match-end 0)))))))
167 (split-string (ews-trim (substring pending last-index))
168 "&" 'omit-nulls))
169 (throw 'finished-parsing-headers t))
170 ;; Build up multipart data.
171 (boundary
172 (setcdr (last headers)
173 (list (ews-parse-multipart/form
174 (ews-trim
175 (substring pending last-index index)))))
176 ;; a boundary suffixed by "--" indicates the end of the headers
177 (when (and (> (length pending) (+ tmp 2))
178 (string= (substring pending tmp (+ tmp 2)) "--"))
179 (throw 'finished-parsing-headers t)))
180 ;; Standard header parsing.
181 (:otherwise
182 (let ((this (ews-parse proc (substring pending last-index index))))
183 (if (and (caar this) (eql (caar this) :CONTENT-TYPE))
184 (cl-destructuring-bind (type &rest data)
185 (mail-header-parse-content-type (cdar this))
186 (cond
187 ((string= type "multipart/form-data")
188 (when (assoc 'boundary data)
189 (setq boundary (cdr (assoc 'boundary data)))
190 (setq delimiter (concat "\r\n--" boundary))))
191 ((string= type "application/x-www-form-urlencoded")
192 (setq boundary (intern (concat ":" (downcase type)))))
193 (:otherwise
194 (ews-error proc "TODO: handle content type: %S" type))))
195 (setcdr (last headers) this)))))
196 (setq last-index tmp)))
197 (setq leftover (ews-trim (substring pending last-index)))
198 nil))))
199
200 (defun ews-call-handler (proc request handler)
201 (catch 'matched-handler
202 (mapc (lambda (handler)
203 (let ((match (car handler))
204 (function (cdr handler)))
205 (when (or (and (consp match)
206 (assoc (car match) request)
207 (string-match (cdr match)
208 (cdr (assoc (car match) request))))
209 (and (functionp match) (funcall match request)))
210 (throw 'matched-handler
211 (condition-case e
212 (funcall function proc request)
213 (error (ews-error proc "Caught Error: %S" e)))))))
214 handler)
215 (ews-error proc "no handler matched request: %S" request)))
216
217 (defun ews-error (proc msg &rest args)
218 (let ((buf (plist-get (process-plist proc) :log-buffer))
219 (c (process-contact proc)))
220 (when buf
221 (with-current-buffer buf
222 (goto-char (point-max))
223 (insert (format "%s\t%s\t%s\tEWS-ERROR: %s"
224 (format-time-string ews-time-format)
225 (first c) (second c)
226 (apply #'format msg args)))))
227 (apply #'ews-send-500 proc msg args)))
228
229 \f
230 ;;; Convenience functions to write responses
231 (defun ews-response-header (proc code &rest header)
232 "Send the headers for an HTTP response to PROC.
233 Currently CODE should be an HTTP status code, see
234 `ews-status-codes' for a list of known codes."
235 (let ((headers
236 (cons
237 (format "HTTP/1.1 %d %s" code (cdr (assoc code ews-status-codes)))
238 (mapcar (lambda (h) (format "%s: %s" (car h) (cdr h))) header))))
239 (setcdr (last headers) (list "" ""))
240 (process-send-string proc (mapconcat #'identity headers "\r\n"))))
241
242 (defun ews-send-500 (proc &rest msg-and-args)
243 "Send 500 \"Internal Server Error\" to PROC with an optional message."
244 (ews-response-header proc 500
245 '("Content-type" . "text/plain"))
246 (process-send-string proc (if msg-and-args
247 (apply #'format msg-and-args)
248 "500 Internal Server Error"))
249 (throw 'close-connection nil))
250
251 (defun ews-send-404 (proc &rest msg-and-args)
252 "Send 404 \"Not Found\" to PROC with an optional message."
253 (ews-response-header proc 404
254 '("Content-type" . "text/plain"))
255 (process-send-string proc (if msg-and-args
256 (apply #'format msg-and-args)
257 "404 Not Found"))
258 (throw 'close-connection nil))
259
260 (defun ews-send-file (proc path &optional mime-type)
261 "Send PATH to PROC.
262 Optionally explicitly set MIME-TYPE, otherwise it is guessed by
263 `mm-default-file-encoding'."
264 (let ((mime (or mime-type
265 (mm-default-file-encoding path)
266 "application/octet-stream")))
267 (ews-response-header proc 200 (cons "Content-type" mime))
268 (process-send-string proc
269 (with-temp-buffer
270 (insert-file-contents-literally path)
271 (buffer-string)))))
272
273 (provide 'emacs-web-server)
274 ;;; emacs-web-server.el ends here