Merge remote-tracking branch 'origin/stable-2.0'
[bpt/guile.git] / module / web / server.scm
index 2e7ad0c..471bb98 100644 (file)
@@ -1,6 +1,6 @@
 ;;; Web server
 
-;; Copyright (C)  2010 Free Software Foundation, Inc.
+;; Copyright (C)  2010, 2011, 2012, 2013 Free Software Foundation, Inc.
 
 ;; This library is free software; you can redistribute it and/or
 ;; modify it under the terms of the GNU Lesser General Public
 ;;;     server socket object, or signals an error.
 ;;;
 ;;;   * The `read' hook is called, to read a request from a new client.
-;;;     The `read' hook takes two arguments: the server socket, and a
-;;;     list of keep-alive clients.  It should return four values:  the
-;;;     new list of keep-alive clients, an opaque client socket, the
+;;;     The `read' hook takes one arguments, the server socket.  It
+;;;     should return three values: an opaque client socket, the
 ;;;     request, and the request body. The request should be a
 ;;;     `<request>' object, from `(web request)'.  The body should be a
 ;;;     string or a bytevector, or `#f' if there is no body.
 ;;;
-;;;     The keep-alive list is used when selecting a new request.  You
-;;;     can either serve an old client or serve a new client; and some
-;;;     old clients might close their connections while you are waiting.
-;;;     The `read' hook returns a new keep-alive set to account for old
-;;;     clients going away, and for read errors on old clients.
-;;;
 ;;;     If the read failed, the `read' hook may return #f for the client
 ;;;     socket, request, and body.
 ;;;
 ;;;     constructed with those headers.
 ;;;
 ;;;   * The `write' hook is called with three arguments: the client
-;;;     socket, the response, and the body.  The `write' hook may return
-;;;     #f to indicate that the connection was closed.  If `write'
-;;;     returns a true value, it will be consed onto the keep-alive
-;;;     list.
+;;;     socket, the response, and the body.  The `write' hook returns no
+;;;     values.
 ;;;
 ;;;   * At this point the request handling is complete. For a loop, we
-;;;     loop back with the new keep-alive list, and try to read a new
-;;;     request.
+;;;     loop back and try to read a new request.
 ;;;
 ;;;   * If the user interrupts the loop, the `close' hook is called on
 ;;;     the server socket.
 
 (define-module (web server)
   #:use-module (srfi srfi-9)
+  #:use-module (srfi srfi-9 gnu)
   #:use-module (rnrs bytevectors)
+  #:use-module (ice-9 binary-ports)
   #:use-module (web request)
   #:use-module (web response)
   #:use-module (system repl error-handling)
   #:use-module (ice-9 control)
+  #:use-module (ice-9 iconv)
   #:export (define-server-impl
             lookup-server-impl
             open-server
             serve-one-client
             run-server))
 
+(define *timer* (gettimeofday))
+(define (print-elapsed who)
+  (let ((t (gettimeofday)))
+    (pk who (+ (* (- (car t) (car *timer*)) 1000000)
+               (- (cdr t) (cdr *timer*))))
+    (set! *timer* t)))
+
+(eval-when (expand)
+  (define *time-debug?* #f))
+
+(define-syntax debug-elapsed
+  (lambda (x)
+    (syntax-case x ()
+      ((_ who)
+       (if *time-debug?*
+           #'(print-elapsed who)
+           #'*unspecified*)))))
+
 (define-record-type server-impl
   (make-server-impl name open read write close)
   server-impl?
   (write server-impl-write)
   (close server-impl-close))
 
-(define-syntax define-server-impl
-  (syntax-rules ()
-    ((_ name open read write close)
-     (define name
-       (make-server-impl 'name open read write close)))))
+(define-syntax-rule (define-server-impl name open read write close)
+  (define name
+    (make-server-impl 'name open read write close)))
 
 (define (lookup-server-impl impl)
+  "Look up a server implementation.  If IMPL is a server
+implementation already, it is returned directly.  If it is a symbol, the
+binding named IMPL in the ‘(web server IMPL)’ module is
+looked up.  Otherwise an error is signaled.
+
+Currently a server implementation is a somewhat opaque type, useful only
+for passing to other procedures in this module, like
+‘read-client’."
   (cond
    ((server-impl? impl) impl)
    ((symbol? impl)
 
 ;; -> server
 (define (open-server impl open-params)
+  "Open a server for the given implementation.  Return one value, the
+new server object.  The implementation's ‘open’ procedure is
+applied to OPEN-PARAMS, which should be a list."
   (apply (server-impl-open impl) open-params))
 
-;; -> (keep-alive client request body | keep-alive #f #f #f)
-(define (read-client impl server keep-alive)
-  (call-with-error-handling
-   (lambda ()
-     ((server-impl-read impl) server keep-alive))
-   #:pass-keys '(quit interrupt)
-   #:on-error (if (batch-mode?) 'pass 'debug)
-   #:post-error
-   (lambda (k . args)
-     (warn "Error while accepting client" k args)
-     (values keep-alive #f #f #f #f))))
-
-;; -> response body state ...
-(define (handle-request handler request body . state)
+;; -> (client request body | #f #f #f)
+(define (read-client impl server)
+  "Read a new client from SERVER, by applying the implementation's
+‘read’ procedure to the server.  If successful, return three
+values: an object corresponding to the client, a request object, and the
+request body.  If any exception occurs, return ‘#f’ for all three
+values."
   (call-with-error-handling
    (lambda ()
-     (with-stack-and-prompt
-      (lambda ()
-        (apply handler request body state))))
+     ((server-impl-read impl) server))
    #:pass-keys '(quit interrupt)
-   #:on-error (if (batch-mode?) 'pass 'debug)
-   #:post-error
-   (lambda (k . args)
-     (warn "Error handling request" k args)
-     (apply values (build-response #:code 500) #f state))))
+   #:on-error (if (batch-mode?) 'backtrace 'debug)
+   #:post-error (lambda _ (values #f #f #f))))
+
+(define (extend-response r k v . additional)
+  (define (extend-alist alist k v)
+    (let ((pair (assq k alist)))
+      (acons k v (if pair (delq pair alist) alist))))
+  (let ((r (set-field r (response-headers)
+                      (extend-alist (response-headers r) k v))))
+    (if (null? additional)
+        r
+        (apply extend-response r additional))))
 
 ;; -> response body
 (define (sanitize-response request response body)
-  (values response body))
+  "\"Sanitize\" the given response and body, making them appropriate for
+the given request.
+
+As a convenience to web handler authors, RESPONSE may be given as
+an alist of headers, in which case it is used to construct a default
+response.  Ensures that the response version corresponds to the request
+version.  If BODY is a string, encodes the string to a bytevector,
+in an encoding appropriate for RESPONSE.  Adds a
+‘content-length’ and ‘content-type’ header, as necessary.
+
+If BODY is a procedure, it is called with a port as an argument,
+and the output collected as a bytevector.  In the future we might try to
+instead use a compressing, chunk-encoded port, and call this procedure
+later, in the write-client procedure.  Authors are advised not to rely
+on the procedure being called at any particular time."
+  (cond
+   ((list? response)
+    (sanitize-response request
+                       (build-response #:version (request-version request)
+                                       #:headers response)
+                       body))
+   ((not (equal? (request-version request) (response-version response)))
+    (sanitize-response request
+                       (adapt-response-version response
+                                               (request-version request))
+                       body))
+   ((not body)
+    (values response #vu8()))
+   ((string? body)
+    (let* ((type (response-content-type response
+                                        '(text/plain)))
+           (declared-charset (assq-ref (cdr type) 'charset))
+           (charset (or declared-charset "utf-8")))
+      (sanitize-response
+       request
+       (if declared-charset
+           response
+           (extend-response response 'content-type
+                            `(,@type (charset . ,charset))))
+       (string->bytevector body charset))))
+   ((procedure? body)
+    (let* ((type (response-content-type response
+                                        '(text/plain)))
+           (declared-charset (assq-ref (cdr type) 'charset))
+           (charset (or declared-charset "utf-8")))
+      (sanitize-response
+       request
+       (if declared-charset
+           response
+           (extend-response response 'content-type
+                            `(,@type (charset . ,charset))))
+       (call-with-encoded-output-string charset body))))
+   ((not (bytevector? body))
+    (error "unexpected body type"))
+   ((and (response-must-not-include-body? response)
+         body
+         ;; FIXME make this stricter: even an empty body should be prohibited.
+         (not (zero? (bytevector-length body))))
+    (error "response with this status code must not include body" response))
+   (else
+    ;; check length; assert type; add other required fields?
+    (values (let ((rlen (response-content-length response))
+                  (blen (bytevector-length body)))
+              (cond
+               (rlen (if (= rlen blen)
+                         response
+                         (error "bad content-length" rlen blen)))
+               (else (extend-response response 'content-length blen))))
+            (if (eq? (request-method request) 'HEAD)
+                ;; Responses to HEAD requests must not include bodies.
+                ;; We could raise an error here, but it seems more
+                ;; appropriate to just do something sensible.
+                #f
+                body)))))
 
-;; -> (#f | client)
+;; -> response body state
+(define (handle-request handler request body state)
+  "Handle a given request, returning the response and body.
+
+The response and response body are produced by calling the given
+HANDLER with REQUEST and BODY as arguments.
+
+The elements of STATE are also passed to HANDLER as
+arguments, and may be returned as additional values.  The new
+STATE, collected from the HANDLER's return values, is then
+returned as a list.  The idea is that a server loop receives a handler
+from the user, along with whatever state values the user is interested
+in, allowing the user's handler to explicitly manage its state."
+  (call-with-error-handling
+   (lambda ()
+     (call-with-values (lambda ()
+                         (with-stack-and-prompt
+                          (lambda ()
+                            (apply handler request body state))))
+       (lambda (response body . state)
+         (call-with-values (lambda ()
+                             (debug-elapsed 'handler)
+                             (sanitize-response request response body))
+           (lambda (response body)
+             (debug-elapsed 'sanitize)
+             (values response body state))))))
+   #:pass-keys '(quit interrupt)
+   #:on-error (if (batch-mode?) 'backtrace 'debug)
+   #:post-error (lambda _
+                  (values (build-response #:code 500) #f state))))
+
+;; -> unspecified values
 (define (write-client impl server client response body)
+  "Write an HTTP response and body to CLIENT.  If the server and
+client support persistent connections, it is the implementation's
+responsibility to keep track of the client thereafter, presumably by
+attaching it to the SERVER argument somehow."
   (call-with-error-handling
    (lambda ()
      ((server-impl-write impl) server client response body))
    #:pass-keys '(quit interrupt)
-   #:on-error (if (batch-mode?) 'pass 'debug)
-   #:post-error
-   (lambda (k . args)
-     (warn "Error while writing response" k args)
-     #f)))
+   #:on-error (if (batch-mode?) 'backtrace 'debug)
+   #:post-error (lambda _ (values))))
 
 ;; -> unspecified values
 (define (close-server impl server)
+  "Release resources allocated by a previous invocation of
+‘open-server’."
   ((server-impl-close impl) server))
 
 (define call-with-sigint
                     (lambda (k proc)
                       (with-stack-and-prompt (lambda () (proc k))))))
   
-(define (and-cons x xs)
-  (if x (cons x xs) xs))
-
-;; -> new keep-alive new-state
-(define (serve-one-client handler impl server keep-alive state)
+;; -> new-state
+(define (serve-one-client handler impl server state)
+  "Read one request from SERVER, call HANDLER on the request
+and body, and write the response to the client.  Return the new state
+produced by the handler procedure."
+  (debug-elapsed 'serve-again)
   (call-with-values
       (lambda ()
-        (read-client impl server keep-alive))
-    (lambda (keep-alive client request body)
+        (read-client impl server))
+    (lambda (client request body)
+      (debug-elapsed 'read-client)
       (if client
           (call-with-values
               (lambda ()
-                (apply handle-request handler request body state))
-            (lambda (response body . state)
-              (call-with-values (lambda ()
-                                  (sanitize-response request response body))
-                (lambda (response body)
-                  (values
-                   (and-cons (write-client impl server client response body)
-                             keep-alive)
-                   state)))))
-          (values keep-alive state)))))
+                (handle-request handler request body state))
+            (lambda (response body state)
+              (debug-elapsed 'handle-request)
+              (write-client impl server client response body)
+              (debug-elapsed 'write-client)
+              state))
+          state))))
 
 (define* (run-server handler #:optional (impl 'http) (open-params '())
                      . state)
+  "Run Guile's built-in web server.
+
+HANDLER should be a procedure that takes two or more arguments,
+the HTTP request and request body, and returns two or more values, the
+response and response body.
+
+For example, here is a simple \"Hello, World!\" server:
+
+@example
+ (define (handler request body)
+   (values '((content-type . (text/plain)))
+           \"Hello, World!\"))
+ (run-server handler)
+@end example
+
+The response and body will be run through ‘sanitize-response’
+before sending back to the client.
+
+Additional arguments to HANDLER are taken from
+STATE.  Additional return values are accumulated into a new
+STATE, which will be used for subsequent requests.  In this way a
+handler can explicitly manage its state.
+
+The default server implementation is ‘http’, which accepts
+OPEN-PARAMS like ‘(#:port 8081)’, among others.  See \"Web
+Server\" in the manual, for more information."
   (let* ((impl (lookup-server-impl impl))
          (server (open-server impl open-params)))
     (call-with-sigint
      (lambda ()
-       (let lp ((keep-alive '()) (state state))
-         (call-with-values
-             (lambda ()
-               (serve-one-client handler impl server keep-alive state))
-           (lambda (new-keep-alive new-state)
-             (lp new-keep-alive new-state)))))
+       (let lp ((state state))
+         (lp (serve-one-client handler impl server state))))
      (lambda ()
        (close-server impl server)
        (values)))))