]> code.delx.au - gnu-emacs-elpa/blobdiff - emacs-web-server.el
url-encoded parameters
[gnu-emacs-elpa] / emacs-web-server.el
index 461d01247e27d0721920f4b8b693e152c5f2ecaf..2d276d58ed7c50183430af0df3345311d0e958d1 100644 (file)
@@ -8,8 +8,11 @@
 
 ;;; Code:
 (require 'emacs-web-server-status-codes)
-(require 'mail-parse)
+(require 'mail-parse)             ; to parse multipart data in headers
+(require 'mm-encode)              ; to look-up mime types for files
+(require 'url-util)               ; to decode url-encoded params
 (require 'eieio)
+(eval-when-compile (require 'cl))
 (require 'cl-lib)
 
 (defclass ews-server ()
    (port    :initarg :port    :accessor port    :initform nil)
    (clients :initarg :clients :accessor clients :initform nil)))
 
+(defclass ews-client ()
+  ((leftover :initarg :leftover :accessor leftover :initform "")
+   (boundary :initarg :boundary :accessor boundary :initform nil)
+   (headers  :initarg :headers  :accessor headers  :initform (list nil))))
+
 (defvar ews-servers nil
   "List holding all ews servers.")
 
 (defvar ews-time-format "%Y.%m.%d.%H.%M.%S.%N"
   "Logging time format passed to `format-time-string'.")
 
-(defun ews-start (handler port &optional log-buffer host)
+(defun ews-start (handler port &optional log-buffer &rest network-args)
   "Start a server using HANDLER and return the server object.
 
 HANDLER should be a list of cons of the form (MATCH . ACTION),
@@ -35,6 +43,9 @@ specified by KEYWORD.  In either case when MATCH returns non-nil,
 then the function ACTION is called with two arguments, the
 process and the request object.
 
+Any supplied NETWORK-ARGS are assumed to be keyword arguments for
+`make-network-process' to which they are passed directly.
+
 For example, the following starts a simple hello-world server on
 port 8080.
 
@@ -59,26 +70,29 @@ function.
    8080)
 
 "
-  (let ((server (make-instance 'ews-server :handler handler :port port)))
+  (let ((server (make-instance 'ews-server :handler handler :port port))
+        (log (when log-buffer (get-buffer-create log-buffer))))
     (setf (process server)
-          (make-network-process
+          (apply
+           #'make-network-process
            :name "ews-server"
            :service (port server)
            :filter 'ews-filter
-           :server 't
-           :nowait 't
+           :server t
+           :nowait t
            :family 'ipv4
-           :host host
-           :plist (list :server server)
-           :log (when log-buffer
-                  (lexical-let ((buf log-buffer))
-                    (lambda (server client message)
-                      (let ((c (process-contact client)))
-                        (with-current-buffer buf
-                          (goto-char (point-max))
-                          (insert (format "%s\t%s\t%s\t%s"
-                                          (format-time-string ews-time-format)
-                                          (first c) (second c) message)))))))))
+           :plist (append (list :server server)
+                          (when log (list :log-buffer log)))
+           :log (when log
+                  (lambda (proc client message)
+                    (let ((c (process-contact client))
+                          (buf (plist-get (process-plist proc) :log-buffer)))
+                      (with-current-buffer buf
+                        (goto-char (point-max))
+                        (insert (format "%s\t%s\t%s\t%s"
+                                        (format-time-string ews-time-format)
+                                        (first c) (second c) message))))))
+           network-args))
     (push server ews-servers)
     server))
 
@@ -88,44 +102,108 @@ function.
   (mapc #'delete-process (append (mapcar #'car (clients server))
                                  (list (process server)))))
 
-(defun ews-parse (string)
-  (cond
-   ((string-match "^GET \\([^[:space:]]+\\) \\([^[:space:]]+\\)$" string)
-    (list (cons :GET (match-string 1 string))
-          (cons :TYPE (match-string 2 string))))
-   ((string-match "^\\([^[:space:]]+\\): \\(.*\\)$" string)
-    (list (cons (intern (concat ":" (upcase (match-string 1 string))))
-                (match-string 2 string))))
-   (:otherwise (message "[ews] bad header: %S" string) nil)))
+(defvar ews-http-common-methods '(GET HEAD POST PUT DELETE TRACE)
+  "HTTP methods from http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html.")
+
+(defvar ews-http-method-rx
+  (format "^\\(%s\\) \\([^[:space:]]+\\) \\([^[:space:]]+\\)$"
+          (mapconcat #'symbol-name ews-http-common-methods "\\|")))
+
+(defun ews-parse (proc string)
+  (cl-flet ((to-keyword (s) (intern (concat ":" (upcase (match-string 1 s))))))
+    (cond
+     ((string-match ews-http-method-rx string)
+      (let ((method (to-keyword (match-string 1 string)))
+            (url (match-string 2 string)))
+        (if (string-match "?" url)
+            (cons (cons method (substring url 0 (match-beginning 0)))
+                  (url-parse-query-string (url-unhex-string
+                                           (substring url (match-end 0))) ))
+          (list (cons method url)))))
+     ((string-match "^\\([^[:space:]]+\\): \\(.*\\)$" string)
+      (list (cons (to-keyword string) (match-string 2 string))))
+     (:otherwise (ews-error proc "bad header: %S" string) nil))))
+
+(defun ews-trim (string)
+  (while (and (> (length string) 0)
+              (or (and (string-match "[\r\n]" (substring string -1))
+                       (setq string (substring string 0 -1)))
+                  (and (string-match "[\r\n]" (substring string 0 1))
+                       (setq string (substring string 1))))))
+  string)
+
+(defun ews-parse-multipart/form (string)
+  ;; ignore empty and non-content blocks
+  (when (string-match "Content-Disposition:[[:space:]]*\\(.*\\)\r\n" string)
+    (let ((dp (mail-header-parse-content-disposition (match-string 1 string))))
+      (cons (cdr (assoc 'name (cdr dp)))
+            (ews-trim (substring string (match-end 0)))))))
 
 (defun ews-filter (proc string)
-  ;; TODO: parse post DATA, see the relevent test, and use these
-  ;;   - mail-header-parse-content-disposition
-  ;;   - mail-header-parse-content-type
   (with-slots (handler clients) (plist-get (process-plist proc) :server)
-    ;; register new client
-    (unless (assoc proc clients) (push (list proc "") clients))
-    (let* ((client (assoc proc clients)) ; clients are (proc pending headers)
-           (pending (concat (cadr client) string))
-           (last-index 0) index in-post)
+    (unless (assoc proc clients)
+      (push (cons proc (make-instance 'ews-client)) clients))
+    (let ((c (cdr (assoc proc clients))))
+      (when (not (eq (catch 'close-connection
+                       (if (ews-do-filter proc c string)
+                           (ews-call-handler proc (cdr (headers c)) handler)
+                         :keep-open))
+                     :keep-open))
+        (setq clients (assq-delete-all proc clients))
+        (delete-process proc)))))
+
+(defun ews-do-filter (proc client string)
+  "Return non-nil when finished and the client may be deleted."
+  (with-slots (leftover boundary headers) client
+    (let ((pending (concat leftover string))
+          (delimiter (if boundary
+                         (regexp-quote (concat "\r\n--" boundary))
+                       "\r\n"))
+          (last-index 0) index tmp-index)
       (catch 'finished-parsing-headers
         ;; parse headers and append to client
-        (while (setq index (string-match "\r\n" pending last-index))
-          ;; double \r\n outside of post data -> done w/headers, call handler
-          (when (and (not in-post) (= last-index index))
-            (throw 'finished-parsing-headers
-                   (when (ews-call-handler proc (cddr client) handler)
-                     (setq clients (assq-delete-all proc clients))
-                     (delete-process proc))))
-          (if in-post
-              ;; build up post data, maybe set in-post to boundary
-              (error "TODO: handle POST data")
-            (let ((this (ews-parse (substring pending last-index index))))
-              (if (eql (caar this) :CONTENT-TYPE)
-                  (error "TODO: handle POST data")
-                (setcdr (last client) this))))
-          (setq last-index (+ index 2)))
-        (setcar (cdr client) (substring pending last-index))))))
+        (while (setq index (string-match delimiter pending last-index))
+          (let ((tmp (+ index (length delimiter))))
+            (cond
+             ;; Double \r\n outside of post data means we are done
+             ;; w/headers and should call the handler.
+             ((and (not boundary) (= last-index index))
+              (throw 'finished-parsing-headers t))
+             ;; Parse a URL
+             ((eq boundary :application/x-www-form-urlencoded)
+              (mapc (lambda (pair) (setcdr (last headers) (list pair)))
+                    (url-parse-query-string
+                     (ews-trim (substring pending last-index))))
+              (throw 'finished-parsing-headers t))
+             ;; Build up multipart data.
+             (boundary
+              (setcdr (last headers)
+                      (list (ews-parse-multipart/form
+                             (ews-trim
+                              (substring pending last-index index)))))
+              ;; a boundary suffixed by "--" indicates the end of the headers
+              (when (and (> (length pending) (+ tmp 2))
+                         (string= (substring pending tmp (+ tmp 2)) "--"))
+                (throw 'finished-parsing-headers t)))
+             ;; Standard header parsing.
+             (:otherwise
+              (let ((this (ews-parse proc (substring pending last-index index))))
+                (if (and (caar this) (eql (caar this) :CONTENT-TYPE))
+                    (cl-destructuring-bind (type &rest data)
+                        (mail-header-parse-content-type (cdar this))
+                      (cond
+                       ((string= type "multipart/form-data")
+                        (when (assoc 'boundary data)
+                          (setq boundary (cdr (assoc 'boundary data)))
+                          (setq delimiter (concat "\r\n--" boundary))))
+                       ((string= type "application/x-www-form-urlencoded")
+                        (setq boundary (intern (concat ":" (downcase type)))))
+                       (:otherwise
+                        (ews-error proc "TODO: handle content type: %S" type))))
+                  (setcdr (last headers) this)))))
+            (setq last-index tmp)))
+        (setq leftover (ews-trim (substring pending last-index)))
+        nil))))
 
 (defun ews-call-handler (proc request handler)
   (catch 'matched-handler
@@ -137,9 +215,24 @@ function.
                              (string-match (cdr match)
                                            (cdr (assoc (car match) request))))
                         (and (functionp match) (funcall match request)))
-                (throw 'matched-handler (funcall function proc request)))))
+                (throw 'matched-handler
+                       (condition-case e
+                           (funcall function proc request)
+                         (error (ews-error proc "Caught Error: %S" e)))))))
           handler)
-    (error "[ews] no handler matched request:%S" request)))
+    (ews-error proc "no handler matched request: %S" request)))
+
+(defun ews-error (proc msg &rest args)
+  (let ((buf (plist-get (process-plist proc) :log-buffer))
+        (c (process-contact proc)))
+    (when buf
+      (with-current-buffer buf
+        (goto-char (point-max))
+        (insert (format "%s\t%s\t%s\tEWS-ERROR: %s"
+                        (format-time-string ews-time-format)
+                        (first c) (second c)
+                        (apply #'format msg args)))))
+    (apply #'ews-send-500 proc msg args)))
 
 \f
 ;;; Convenience functions to write responses
@@ -154,5 +247,36 @@ Currently CODE should be an HTTP status code, see
     (setcdr (last headers) (list "" ""))
     (process-send-string proc (mapconcat #'identity headers "\r\n"))))
 
+(defun ews-send-500 (proc &rest msg-and-args)
+  "Send 500 \"Internal Server Error\" to PROC with an optional message."
+  (ews-response-header proc 500
+    '("Content-type" . "text/plain"))
+  (process-send-string proc (if msg-and-args
+                                (apply #'format msg-and-args)
+                              "500 Internal Server Error"))
+  (throw 'close-connection nil))
+
+(defun ews-send-404 (proc &rest msg-and-args)
+  "Send 404 \"Not Found\" to PROC with an optional message."
+  (ews-response-header proc 404
+    '("Content-type" . "text/plain"))
+  (process-send-string proc (if msg-and-args
+                                (apply #'format msg-and-args)
+                              "404 Not Found"))
+  (throw 'close-connection nil))
+
+(defun ews-send-file (proc path &optional mime-type)
+  "Send PATH to PROC.
+Optionally explicitly set MIME-TYPE, otherwise it is guessed by
+`mm-default-file-encoding'."
+  (let ((mime (or mime-type
+                  (mm-default-file-encoding path)
+                  "application/octet-stream")))
+    (ews-response-header proc 200 (cons "Content-type" mime))
+    (process-send-string proc
+      (with-temp-buffer
+        (insert-file-contents-literally path)
+        (buffer-string)))))
+
 (provide 'emacs-web-server)
 ;;; emacs-web-server.el ends here