source: project/release/4/spiffy/trunk/spiffy.scm @ 12024

Last change on this file since 12024 was 12024, checked in by sjamaan, 13 years ago

Make filename handling work the same way directory handling works

File size: 9.9 KB
Line 
1;;
2;; Spiffy the web server
3;;
4; Copyright (c) 2007-2008, Peter Bex
5; Copyright (c) 2000-2005, Felix L. Winkelmann
6; All rights reserved.
7;
8; Redistribution and use in source and binary forms, with or without
9; modification, are permitted provided that the following conditions
10; are met:
11;
12; 1. Redistributions of source code must retain the above copyright
13;    notice, this list of conditions and the following disclaimer.
14; 2. Redistributions in binary form must reproduce the above copyright
15;    notice, this list of conditions and the following disclaimer in the
16;    documentation and/or other materials provided with the distribution.
17; 3. Neither the name of the author nor the names of its
18;    contributors may be used to endorse or promote products derived
19;    from this software without specific prior written permission.
20;
21; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24; FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25; COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
26; INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
27; (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28; SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
29; HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
30; STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31; ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
32; OF THE POSSIBILITY OF SUCH DAMAGE.
33;
34; Please report bugs, suggestions and ideas to the Chicken Trac
35; ticket tracking system (assign tickets to user 'sjamaan'):
36; http://trac.callcc.org
37
38(module spiffy
39  (start-server with-headers send-status send-static-file
40   current-request current-response current-file current-pathinfo
41   root-path server-port index-files mime-type-map default-mime-type default-host
42   handle-directory handle-not-found
43   restart-request)
44
45(import chicken scheme extras ports files data-structures)
46(require-extension srfi-1 srfi-18 posix tcp
47                   intarweb uri-generic sendfile matchable)
48
49(define version 4)
50(define release 0)
51
52;;; Request processing information
53(define current-request  (make-parameter #f))
54(define current-response (make-parameter #f))
55(define current-file     (make-parameter #f))
56(define current-pathinfo (make-parameter #f))
57
58;;; Configuration
59(define root-path        (make-parameter "./web"))
60(define server-port      (make-parameter 8080))
61(define index-files      (make-parameter '("index.html" "index.htm")))
62(define mime-type-map
63  (make-parameter
64   '(("txt" . text/plain)
65     ("xml" . text/xml)
66     ("xul" . application/vnd.mozilla.xul+xml)
67     ("htm" . text/html)
68     ("html" . text/html)
69     ("js"  . text/javascript)
70     ("pdf" . application/pdf)
71     ("css" . text/css)
72     ("bmp" . image/bmp)
73     ("ico" . image/x-icon)
74     ("gif" . image/gif)
75     ("jpg" . image/jpeg)
76     ("jpeg" . image/jpeg)
77     ("png" . image/png))))
78(define default-mime-type (make-parameter 'application/octet-stream))
79(define default-host (make-parameter "localhost")) ;; XXX Can we do without?
80
81;;; Custom handlers
82(define handle-directory
83  (make-parameter
84   (lambda (path)
85     (send-status 403 "Forbidden"))))
86(define handle-file
87  (make-parameter
88   (lambda (path)
89     (with-headers `((content-type ,(extension->mime-type
90                                     (pathname-extension path))))
91       (lambda ()
92         (send-static-file path))))))
93(define handle-not-found
94  (make-parameter
95   (lambda ()
96     (send-status 404 "Not found"
97                  "The resource you requested could not be found"))))
98
99
100;;; Internal parameters
101(define request-restarter (make-parameter #f))
102
103;; A simple utility procedure to render a status code with message
104(define (send-status code reason #!optional text)
105  (parameterize ((current-response (update-response (current-response)
106                                                    code: code
107                                                    reason: reason)))
108    (write-response (current-response))
109    (with-output-to-port (response-port (current-response))
110      (lambda ()
111        (print "<?xml version=\"1.0\" encoding=\"iso-8859-1\" ?>")
112        (print "<!DOCTYPE html")
113        (print "  PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"")
114        (print "         \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">")
115        (print "<html xmlns=\"http://www.w3.org/1999/xhtml\" ")
116        (print "      xml:lang=\"en\" lang=\"en\">")
117        (print "  <head>")
118        (printf "    <title>~A - ~A</title>\n" code reason)
119        (print "  </head>")
120        (print "  <body>")
121        (printf "    <h1>~A - ~A</h1>\n" code reason)
122        (if text (printf "    <p>~A</p>" text))
123        (print "  </body>")
124        (print "</html>")))))
125
126(define (send-static-file filename)
127  (let ((path (make-pathname (root-path) filename)))
128    (with-headers `((content-length ,(file-size path)))
129      (lambda ()
130        (write-response (current-response))
131        (let ([fd (file-open path (+ open/binary open/rdonly))])
132          (handle-exceptions exn (begin
133                                   (file-close fd)
134                                   (signal exn))
135                             (sendfile fd (response-port (current-response))))
136          (file-close fd))))))
137
138(define (extension->mime-type ext)
139  (alist-ref (or ext "") (mime-type-map) string-ci=? (default-mime-type)))
140
141(define (with-headers new-headers thunk)
142  (parameterize ((current-response
143                  (update-response
144                   (current-response)
145                   headers: (headers new-headers
146                                     (response-headers (current-response))))))
147    (thunk)))
148
149(define (process-directory path)
150  (let ((index-page (find (lambda (ip)
151                            (file-exists?
152                             (make-pathname (list (root-path) path) ip)))
153                          (index-files))))
154    (if index-page
155        (process-entry (make-pathname path index-page) '())
156        ((handle-directory) path))))
157
158;; If an URL is missing a trailing slash, instead of directly serving
159;; its index-file, redirect to the URL _with_ trailing slash.  This
160;; prevents problems with relative references since the directory
161;; would be seen as the file component in the path and get replaced.
162(define (redirect-directory-root path)
163  (let* ((newloc (make-pathname path "/"))
164         (url (uri-relative-to (uri-reference newloc)
165                               (request-uri (current-request)))))
166    (with-headers `((location ,url))
167      (lambda () (send-status 301 "Moved permanently")))))
168
169(define (process-entry current-path remaining-path)
170  (let ((path (make-pathname (root-path) current-path)))
171    (cond
172     ;; TODO Check if there's a registered URI-handler first
173     ((directory? path)
174      (match remaining-path
175       (()    (redirect-directory-root current-path))
176       (("/") (process-directory current-path))
177       (else  (process-entry (make-pathname current-path (car remaining-path))
178                             (cdr remaining-path)))))
179     ((file-exists? path)
180      ((handle-file) current-path))
181     (else ((handle-not-found))))))
182
183;; Determine the vhost and port to use. This follows RFC 2616, section 5.2:
184;; If request URL is absolute, use that.  Otherwise, look at the Host header.
185;; In HTTP >= 1.1, a Host line is required, as per section 14.23 of
186;; RFC 2616.  If no host line is present, it returns the default host
187;; for HTTP/1.0.
188(define (determine-vhost/port)
189  (let* ((request-uri (request-uri (current-request)))
190         (request-host (uri-host request-uri))
191         (host-header (header-value 'host (request-headers (current-request)))))
192    (if request-host
193        (values request-host (or (uri-port request-uri) 80))
194        (if host-header
195            (values (car host-header) (cdr host-header))
196            (if (and (= (request-major (current-request)) 1)
197                     (>= (request-minor (current-request)) 1))
198                (values #f #f)
199                (values (default-host) 80))))))
200
201(define (normalize-current-request-uri)
202  (receive (host port) (determine-vhost/port)
203    (if host
204        (update-request (current-request)
205                        uri: (uri-relative-to
206                              (request-uri (current-request))
207                              ;; XXX
208                              (absolute-uri (conc "http://" host ":" port))))
209        (current-request))))
210
211(define (restart-request req)
212  ((request-restarter) req (request-restarter)))
213
214(define (handle-incoming-request in out)
215  (parameterize ((current-request (read-request in))
216                 (current-response
217                  (make-response port: out
218                                 headers: (headers
219                                           `((content-type text/html))))))
220    (let ((path (uri-path (request-uri (current-request)))))
221      (receive (req cont)
222        (call/cc (lambda (c) (values (normalize-current-request-uri) c)))
223        (parameterize ((current-request req)
224                       (request-restarter cont))
225          (if (and (uri-host (request-uri (current-request))) (pair? path))
226              (process-entry "" path)
227              ;; No host in the request? That's an error.
228              (send-status 400 "Bad request"
229                           "Your client sent a request that the server did not understand"))))
230      ;; For now, just close the ports and allow the thread to exit
231      (close-output-port out)
232      (close-input-port in))))
233
234(define (start-server #!key (port (server-port)))
235  (letrec ((listener (tcp-listen port))
236           (accept-loop (lambda ()
237                          (receive (in out)
238                            (tcp-accept listener)
239                            (thread-start!
240                             (make-thread (lambda ()
241                                            (handle-incoming-request in out))))
242                            (accept-loop)))))
243    (accept-loop)))
244
245)
Note: See TracBrowser for help on using the repository browser.