racket/collects/web-server/web-server-unit.ss
2005-08-23 16:52:40 +00:00

684 lines
32 KiB
Scheme

(module web-server-unit mzscheme
(require "sig.ss"
"connection-manager.ss"
(all-except "request-parsing.ss" request-bindings)
"configuration-structures.ss"
"util.ss"
"response.ss"
"servlet-tables.ss"
"servlet.ss"
"servlet-helpers.ss"
"timer.ss"
(lib "tcp-sig.ss" "net")
(lib "unitsig.ss")
(lib "string.ss")
(lib "url.ss" "net")
(lib "xml.ss" "xml")
(lib "list.ss"))
(provide web-server@)
(define myprint
(lambda args
(apply fprintf (cons (current-error-port) args))))
;; ****************************************
;; stick this auxilliary outside the unit so
;; I can get at it with require/expose
;; get-host : Url (listof (cons Symbol String)) -> String
;; host names are case insesitive---Internet RFC 1034
(define DEFAULT-HOST-NAME "<none>")
(define (get-host uri headers)
(let ([lower!
(lambda (s)
(string-lowercase! s)
s)])
(cond
[(url-host uri) => lower!]
[(assq 'host headers)
=>
(lambda (h) (lower! (bytes->string/utf-8 (cdr h))))]
[else DEFAULT-HOST-NAME])))
;; ****************************************
(define web-server@
(unit/sig web-server^
(import net:tcp^ (config : web-config^))
(define current-server-custodian (make-parameter #f))
;; make-servlet-custodian: -> custodian
;; create a custodian for the dynamic extent of a servlet continuation
(define (make-servlet-custodian)
(make-custodian (current-server-custodian)))
;; serve: -> -> void
;; start the server and return a thunk to shut it down
(define (serve)
(let ([the-server-custodian (make-custodian)])
(start-connection-manager the-server-custodian)
(parameterize ([current-custodian the-server-custodian]
[current-server-custodian the-server-custodian])
(let ([get-ports
(let ([listener (tcp-listen config:port config:max-waiting
#t config:listen-ip)])
(lambda ()
(let-values ([(ip op) (tcp-accept listener)])
;; Try to set buffer mode, and if it can't be set,
;; assume that it doesn't matter. (Only happens
;; when tcp-accept is not MzScheme's version.)
(with-handlers ([exn:fail? void])
(file-stream-buffer-mode op 'none))
(values ip op))))])
(thread
(lambda ()
(server-loop get-ports)))))
(lambda ()
(custodian-shutdown-all the-server-custodian))))
;; server-loop: (-> i-port o-port) -> void
;; start a thread to handle each incoming connection
(define (server-loop get-ports)
(let loop ()
(let ([connection-cust (make-custodian)])
(parameterize ([current-custodian connection-cust])
(let-values ([(ip op) (get-ports)])
(serve-ports/inner ip op))))
(loop)))
;; serve-ports : input-port output-port -> void
;; returns immediately, spawning a thread to handle
;; the connection
;; NOTE: this doesn't use a connection manager since
;; connection managers don't do anything anyways. -robby
;; NOTE: (GregP) should allow the user to pass in a connection-custodian
(define (serve-ports ip op)
(let ([connection-cust (make-custodian)]
[server-cust (make-custodian)])
(parameterize ([current-custodian connection-cust]
[current-server-custodian server-cust])
(serve-ports/inner ip op))))
;; serve-ports/inner : input-port output-port -> void
;; returns immediately, spawning a thread to handle
(define (serve-ports/inner ip op)
(thread
(lambda ()
(let ([conn (new-connection config:initial-connection-timeout
ip op (current-custodian) #f)])
(with-handlers ([exn:fail:network?
(lambda (e)
(set-connection-close?! conn #t)
; XXX: Can this block on the mutex?
(kill-connection! conn)
(raise e))])
(serve-connection conn))))))
;; serve-connection: connection -> void
;; respond to all requests on this connection
(define (serve-connection conn)
(let connection-loop ()
(let-values ([(req close?) (read-request conn)])
(let* ([host (get-host (request-uri req) (request-headers req))]
[host-conf (config:virtual-hosts host)])
((host-log-message host-conf) (request-host-ip req)
(request-client-ip req) (request-method req) (request-uri req) host)
(set-connection-close?! conn close?)
(adjust-connection-timeout! conn config:initial-connection-timeout)
(dispatch conn req host-conf)
(cond
[(connection-close? conn) (kill-connection! conn)]
[else (connection-loop)])))))
;; dispatch: connection request host -> void
;; NOTE: (GregP) I'm going to use the dispatch logic out of v208 for now.
;; I will move the other dispatch logic out of the prototype
;; at a later time.
(define (dispatch conn req host-info)
(let* ([uri (request-uri req)]
[method (request-method req)]
[path (translate-escapes (url-path->string (url-path uri)))])
(cond
[(access-denied? method path (request-headers req) host-info
config:access)
=> (lambda (realm)
(adjust-connection-timeout! conn (timeouts-password
(host-timeouts host-info)))
(request-authentication conn method uri host-info realm))]
[(conf-prefix? path)
(cond
[(string=? "/conf/refresh-servlets" path)
;; more here - this is broken - only out of date or specifically mentioned
;; scripts should be flushed. This destroys persistent state!
(call-with-semaphore config:scripts-lock
(lambda ()
(set-box! config:scripts (make-hash-table 'equal))))
(output-response/method
conn
((responders-servlets-refreshed (host-responders host-info)))
method)]
[(string=? "/conf/refresh-passwords" path)
;; more here - send a nice error page
(hash-table-put! config:access host-info
(read-passwords host-info))
(output-response/method
conn
((responders-passwords-refreshed (host-responders host-info)))
method)]
[else
(output-response/method
conn
((responders-file-not-found (host-responders host-info)) uri)
method)])]
[(servlet-bin? path)
(adjust-connection-timeout!
conn
(timeouts-servlet-connection (host-timeouts host-info)))
;; more here - make timeouts proportional to size of bindings
(servlet-content-producer conn req host-info)]
[else (file-content-producer conn req host-info)])))
;; conf-prefix?: string -> (union (listof string) #f)
;; does the path string have "/conf/" as a prefix?
(define conf-prefix?
(let ([conf-re (regexp "^/conf/.*")])
(lambda (str)
(regexp-match conf-re str))))
(define servlet-bin?
(let ([svt-bin-re (regexp "^/servlets/.*")])
(lambda (str)
(regexp-match svt-bin-re str))))
;; ripped this off from url-unit.ss
(define (url-path->string strs)
(apply
string-append
(let loop ([strs strs])
(cond
[(null? strs) '()]
[else (list* "/"
(maybe-join-params (car strs))
(loop (cdr strs)))]))))
;; needs to unquote things!
(define (maybe-join-params s)
(cond
[(string? s) s]
[else (path/param-path s)]))
;; ****************************************
;; ****************************************
;; ACCESS CONTROL
;; pass-entry = (make-pass-entry str regexp (list sym str))
(define-struct pass-entry (domain pattern users))
;; access-denied? : Method string x-table host Access-table -> (+ false str)
;; the return string is the prompt for authentication
(define (access-denied? method uri-str headers host-info access-table)
;; denied?: str sym str -> (U str #f)
;; a function to authenticate the user
(let ([denied?
;; GregP lookup the authenticator function, if you can't find it, then try to load the
;; passwords file for this host.
(hash-table-get
access-table host-info
(lambda ()
; more here - a malformed password file will kill the connection
(let ([f (read-passwords host-info)])
(hash-table-put! access-table host-info f)
f)))])
(let ([user-pass (extract-user-pass headers)])
(if user-pass
(denied? uri-str (lowercase-symbol! (car user-pass)) (cdr user-pass))
(denied? uri-str fake-user "")))))
(define-struct (exn:password-file exn) ())
;; : host -> (str sym str -> (U str #f))
;; to produce a function that checks if a given url path is accessible by a given user with a given
;; password. If not, the produced function returns a string, prompting for the password.
;; If the password file does not exist, all accesses are allowed. If the file is malformed, an
;; exn:password-file is raised.
(define (read-passwords host-info)
(let ([password-path (host-passwords host-info)])
(with-handlers ([void (lambda (exn)
(raise (make-exn:password-file (format "could not load password file ~a" password-path)
(current-continuation-marks))))])
(if (and (file-exists? password-path) (memq 'read (file-or-directory-permissions password-path)))
(let ([passwords
(let ([raw (load password-path)])
(unless (password-list? raw)
(raise "malformed passwords"))
(map (lambda (x) (make-pass-entry (car x) (regexp (cadr x)) (cddr x)))
raw))])
;; string symbol bytes -> (union #f string)
(lambda (request-path user-name password)
(ormap (lambda (x)
(and (regexp-match (pass-entry-pattern x) request-path)
(let ([name-pass (assq user-name (pass-entry-users x))])
(if (and name-pass
(string=?
(cadr name-pass)
(bytes->string/utf-8 password)))
#f
(pass-entry-domain x)))))
passwords)))
(lambda (req user pass) #f)))))
(define fake-user (gensym))
;; password-list? : TST -> bool
;; Note: andmap fails for dotted pairs at end.
;; This is okay, since #f ends up raising a caught exception anyway.
(define (password-list? passwords)
(and (list? passwords)
(andmap (lambda (domain)
(and (pair? domain) (pair? (cdr domain)) (list (cddr domain))
(string? (car domain))
(string? (cadr domain))
(andmap (lambda (x)
(and (pair? x) (pair? (cdr x)) (null? (cddr x))
(symbol? (car x)) (string? (cadr x))))
(cddr domain))))
passwords)))
;; request-authentication : connection Method URL iport oport host str bool -> bool
;; GregP: at first look, it seems that this gets called when the user
;; has supplied bad authentication credentials.
(define (request-authentication conn method uri host-info realm)
(output-response/method
conn
((responders-authentication (host-responders host-info))
uri `(WWW-Authenticate . ,(string-append " Basic
realm=\"" realm "\"")))
method))
;; ************************************************************
;; ************************************************************
;; SERVING FILES
;; file-content-producer: connection request host -> void
(define (file-content-producer conn req host-info)
(serve-file conn (request-method req) (request-uri req) host-info))
;; serve-file : connection symbol uri host -> void
;; to find the file, including searching for implicit index files, and serve it out
(define (serve-file conn method uri host-info)
(let ([path (url-path->path (paths-htdocs (host-paths host-info))
(translate-escapes (url-path->string (url-path uri))))])
(cond
[(file-exists? path)
(output-file conn path method (get-mime-type path))]
[(directory-exists? path)
(let loop ([dir-defaults (host-indices host-info)])
(cond
[(pair? dir-defaults)
(let ([full-name (build-path path (car dir-defaults))])
(if (file-exists? full-name)
(cond
[(looks-like-directory? (url-path->string (url-path uri)))
(output-file conn full-name method (get-mime-type full-name))]
[else
(output-slash-message conn method (url-path->string (url-path uri)))])
(loop (cdr dir-defaults))))]
[else
(output-response/method
conn
((responders-file-not-found
(host-responders host-info)) uri)
method)]))]
[else
(output-response/method
conn ((responders-file-not-found (host-responders host-info))
uri)
method)])))
;; looks-like-directory : str -> bool
;; to determine if is url style path looks like it refers to a directory
(define (looks-like-directory? path)
(eq? #\/ (string-ref path (sub1 (string-length path)))))
;; output-slash-message: connection symbol string -> void
;; basically this is just a special error response
(define (output-slash-message conn method url-path-str)
(output-response/method
conn
(make-response/full
301 "Moved Permanently"
(current-seconds)
TEXT/HTML-MIME-TYPE
`([Location . ,(string-append url-path-str "/")])
(list
(xml->string
(xexpr->xml
`(html
(head (title "Add a Slash"))
(body "Please use "
(a ([href ,(string-append
url-path-str "/")])
"this url") " instead."))))))
method))
;; xml->string: xml -> string
(define (xml->string some-xml)
(let ([o-port (open-output-string)])
(write-xml/content some-xml o-port)
(get-output-string o-port)))
;; ************************************************************
;; ************************************************************
;; SERVING SERVLETS
;; servlet-content-producer: connection request host -> void
(define (servlet-content-producer conn req host-info)
(let ([meth (request-method req)])
(if (eq? meth 'head)
(output-response/method
conn
(make-response/full
200 "Okay" (current-seconds) TEXT/HTML-MIME-TYPE
'() (list "ignored"))
meth)
(let ([uri (request-uri req)])
(set-request-bindings!
req
(read-bindings/handled conn meth uri (request-headers req)
host-info))
(cond
[(continuation-url? uri)
=> (lambda (k-ref)
(invoke-servlet-continuation conn req k-ref host-info))]
[else
(servlet-content-producer/path conn req host-info uri)])))))
;; read-bindings/handled: connection symbol url headers host -> (listof (list (symbol string))
;; read the bindings and handle any exceptions
(define (read-bindings/handled conn meth uri headers host-info)
(with-handlers ([exn? (lambda (e)
(output-response/method
conn
;((responders-protocol (host-responders host-info))
; (exn-message e))
((responders-servlet-loading (host-responders
host-info))
uri e)
meth)
'())])
(read-bindings conn meth uri headers)))
;; servlet-content-producer/path: connection request host url -> void
;; This is not a continuation url so the loading behavior is determined
;; by the url path. Build the servlet path and then load the servlet
(define (servlet-content-producer/path conn req host-info uri)
(with-handlers (;; couldn't find the servlet
[exn:fail:filesystem:exists:servlet?
(lambda (the-exn)
(output-response/method
conn
((responders-file-not-found (host-responders
host-info))
(request-uri req))
(request-method req)))]
;; servlet won't load (e.g. syntax error)
[(lambda (x) #t)
(lambda (the-exn)
(output-response/method
conn
((responders-servlet-loading (host-responders
host-info)) uri
the-exn)
(request-method req)))])
(let ([sema (make-semaphore 0)])
(let/cc suspend
(let* ([servlet-custodian (make-servlet-custodian)]
[inst (create-new-instance!
config:instances servlet-custodian
(make-execution-context
conn req (lambda () (suspend #t)))
sema
(start-timer 0 (lambda () (void))))]
[real-servlet-path (url-path->path
(paths-servlet (host-paths host-info))
(url-path->string (url-path uri)))]
[servlet-exit-handler (make-servlet-exit-handler inst)]
)
(parameterize ([current-directory (get-servlet-base-dir real-servlet-path)]
[current-custodian servlet-custodian]
[current-servlet-instance inst]
[exit-handler servlet-exit-handler])
(let (;; timer thread must be within the dynamic extent of
;; servlet custodian
[time-bomb (start-timer (timeouts-default-servlet
(host-timeouts host-info))
(lambda ()
(servlet-exit-handler #f)))]
;; any resources (e.g. threads) created when the
;; servlet is loaded should be within the dynamic
;; extent of the servlet custodian
[servlet-program (cached-load real-servlet-path)])
(set-servlet-instance-timer! inst time-bomb)
(with-handlers ([(lambda (x) #t)
(make-servlet-exception-handler inst
host-info)])
;; The following bindings need to be in scope for the
;; invoke-unit/sig
(let ([adjust-timeout!
(lambda (secs) (reset-timer time-bomb secs))]
[initial-request req])
;; Two possibilities:
;; - module servlet. start : Request -> Void handles
;; output-response via send/finish, etc.
;; - unit/sig or simple xexpr servlet. These must produce a
;; response, which is then output by the server.
;; Here, we do not know if the servlet was a module,
;; unit/sig, or Xexpr; we do know whether it produces a
;; response.
(let ([r (invoke-unit/sig servlet-program servlet^)])
(when (response? r)
(send/back r)))))))))
(semaphore-post sema))))
;; make-servlet-exit-handler: servlet-instance -> alpha -> void
;; exit handler for a servlet
(define (make-servlet-exit-handler inst)
(lambda (x)
(remove-instance! config:instances inst)
(kill-connection!
(execution-context-connection
(servlet-instance-context inst)))
(custodian-shutdown-all (servlet-instance-custodian inst))))
;; make-servlet-exception-handler: host -> exn -> void
;; This exception handler traps all unhandled servlet exceptions
;; * Must occur within the dynamic extent of the servlet
;; custodian since several connection custodians will typically
;; be shutdown during the dynamic extent of a continuation
;; * Use the connection from the current-servlet-context in case
;; the exception is raised while invoking a continuation.
;; * Use the suspend from the servlet-instanct-context which is
;; closed over the current tcp ports which may need to be
;; closed for an http 1.0 request.
;; * Also, suspend will post to the semaphore so that future
;; requests won't be blocked.
;; * This fixes PR# 7066
(define (make-servlet-exception-handler inst host-info)
(lambda (the-exn)
(let* ([ctxt (servlet-instance-context inst)]
[req (execution-context-request ctxt)]
[resp ((responders-servlet (host-responders
host-info))
(request-uri req)
the-exn)])
;; Don't handle twice
(with-handlers ([exn:fail? (lambda (exn) (void))])
(output-response/method
(execution-context-connection ctxt)
resp (request-method req)))
((execution-context-suspend ctxt)))))
;; path -> path
;; The actual servlet's parent directory.
(define (get-servlet-base-dir servlet-path)
(let loop ((path servlet-path))
(let-values ([(base name must-be-dir?) (split-path path)])
(if must-be-dir?
(or (and (directory-exists? path) path)
(loop base))
(or (and (directory-exists? base) base)
(loop base))))))
;; invoke-servlet-continuation: connection request continuation-reference
;; host -> void
;; pull the continuation out of the table and apply it
(define (invoke-servlet-continuation conn req k-ref host-info)
(with-handlers ([exn:servlet-instance?
(lambda (the-exn)
(output-response/method
conn
((responders-file-not-found (host-responders
host-info))
(request-uri req))
(request-method req)))]
[exn:servlet-continuation?
(lambda (the-exn)
(output-response/method
conn
((responders-file-not-found (host-responders
host-info))
(request-uri req))
(request-method req)))])
(let* ([inst
(hash-table-get config:instances (first k-ref)
(lambda ()
(raise
(make-exn:servlet-instance
"" (current-continuation-marks)))))]
[k-table
(servlet-instance-k-table inst)])
(let/cc suspend
; We don't use call-with-semaphore or dynamic-wind because we
; always call a continuation. The exit-handler above ensures that
; the post is done.
(semaphore-wait (servlet-instance-mutex inst))
(set-servlet-instance-context!
inst
(make-execution-context
conn req (lambda () (suspend #t))))
(increment-timer (servlet-instance-timer inst)
(timeouts-default-servlet
(host-timeouts host-info)))
(let ([k*salt
(hash-table-get k-table (second k-ref)
(lambda ()
(raise
(make-exn:servlet-continuation
"" (current-continuation-marks)))))])
(if (= (second k*salt) (third k-ref))
((first k*salt) req)
(raise
(make-exn:servlet-continuation
"" (current-continuation-marks))))))
(semaphore-post (servlet-instance-mutex inst))
)))
;; ************************************************************
;; ************************************************************
;; Paul's ugly loading code:
;; cached-load : str -> script
;; timestamps are no longer checked for performance. The cache must be explicitly
;; refreshed (see dispatch).
(define (cached-load name)
(call-with-semaphore config:scripts-lock
(lambda ()
(hash-table-get (unbox config:scripts)
name
(lambda () (reload-servlet-script name))))))
;; exn:i/o:filesystem:servlet-not-found =
;; (make-exn:fail:filesystem:exists:servlet str continuation-marks str sym)
(define-struct (exn:fail:filesystem:exists:servlet
exn:fail:filesystem:exists) ())
;; reload-servlet-script : str -> script
;; The servlet is not cached in the servlet-table, so reload it from the filesystem.
(define (reload-servlet-script servlet-filename)
(cond
[(load-servlet/path servlet-filename)
=> (lambda (svlt)
; This is only called from cached-load, so config:scripts is locked
(hash-table-put! (unbox config:scripts) servlet-filename svlt)
svlt)]
[else
(raise (make-exn:fail:filesystem:exists:servlet
(string->immutable-string (format "Couldn't find ~a" servlet-filename))
(current-continuation-marks) ))]))
;; load-servlet/path path -> (union #f signed-unit)
;; given a string path to a filename attempt to load a servlet
;; A servlet-file will contain either
;;;; A signed-unit-servlet
;;;; A module servlet
;;;;;; (two versions, 'v1 and I don't know what 'typed-model-split-store0 is)
;;;; A response
(define (load-servlet/path a-path)
(and (file-exists? a-path)
(let ([s (load/use-compiled a-path)])
(cond
;; signed-unit servlet
; MF: I'd also like to test that s has the correct import signature.
[(unit/sig? s) s]
; FIX - reason about exceptions from dynamic require (catch and report if not already)
;; module servlet
[(void? s)
(parameterize ([current-namespace (config:make-servlet-namespace)])
(let* ([module-name `(file ,(path->string a-path))]
[version (dynamic-require module-name 'interface-version)])
(case version
[(v1)
(let ([timeout (dynamic-require module-name 'timeout)]
[start (dynamic-require module-name 'start)])
(unit/sig ()
(import servlet^)
(adjust-timeout! timeout)
(start initial-request)))]
[(typed-model-split-store-0)
(let ([constrained (dynamic-require module-name 'type)]
[the-servlet (dynamic-require module-name 'servlet)])
; more here - check constraints
the-servlet)]
[else
(raise (format "unknown sevlet version ~e" version))])))]
;; response
[(response? s)
(letrec ([go (lambda ()
(begin
(set! go (lambda () (load/use-compiled a-path)))
s))])
(unit/sig () (import servlet^) (go)))]
[else
(raise (format "Loading ~e produced ~n~e~n instead of a servlet." a-path s))]))))
)))