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 | ) |
---|