582 lines
20 KiB
Racket
582 lines
20 KiB
Racket
#lang scribble/doc
|
|
@(require "web-server.rkt")
|
|
|
|
@title[#:tag "http"]{HTTP: Hypertext Transfer Protocol}
|
|
|
|
@defmodule[web-server/http]
|
|
|
|
The @web-server implements many HTTP libraries that are provided by this module.
|
|
|
|
@; ------------------------------------------------------------
|
|
@section[#:tag "request-structs"]{Requests}
|
|
@(require (for-label web-server/http/request-structs
|
|
xml
|
|
racket/promise
|
|
racket/match))
|
|
|
|
@defmodule[web-server/http/request-structs]{
|
|
|
|
@defstruct[header ([field bytes?]
|
|
[value bytes?])]{
|
|
Represents a header of @racket[field] to @racket[value].
|
|
}
|
|
|
|
@defproc[(headers-assq [id bytes?] [heads (listof header?)])
|
|
(or/c false/c header?)]{
|
|
Returns the header with a field equal to @racket[id] from @racket[heads] or @racket[#f].
|
|
}
|
|
|
|
@defproc[(headers-assq* [id bytes?] [heads (listof header?)])
|
|
(or/c false/c header?)]{
|
|
Returns the header with a field case-insensitively equal to @racket[id] from @racket[heads] or @racket[#f].
|
|
|
|
You almost @bold{always} want to use this, rather than @racket[headers-assq] because Web browsers may send headers with arbitrary casing.
|
|
}
|
|
|
|
@defstruct[binding ([id bytes?])]{Represents a binding of @racket[id].}
|
|
|
|
@defstruct[(binding:form binding) ([value bytes?])]{
|
|
Represents a form binding of @racket[id] to @racket[value].
|
|
}
|
|
|
|
@defstruct[(binding:file binding) ([filename bytes?]
|
|
[headers (listof header?)]
|
|
[content bytes?])]{
|
|
Represents the uploading of the file @racket[filename] with the id @racket[id]
|
|
and the content @racket[content], where @racket[headers] are the additional headers from
|
|
the MIME envelope the file was in. (For example, the @racket[#"Content-Type"] header may
|
|
be included by some browsers.)
|
|
}
|
|
|
|
@defproc[(bindings-assq [id bytes?]
|
|
[binds (listof binding?)])
|
|
(or/c false/c binding?)]{
|
|
Returns the binding with an id equal to @racket[id] from @racket[binds] or @racket[#f].
|
|
}
|
|
|
|
@defproc[(bindings-assq-all [id bytes?]
|
|
[binds (listof binding?)])
|
|
(listof binding?)]{
|
|
Like @racket[bindings-assq], but returns a list of all bindings matching @racket[id].
|
|
}
|
|
|
|
|
|
@defstruct[request ([method bytes?]
|
|
[uri url?]
|
|
[headers/raw (listof header?)]
|
|
[bindings/raw-promise (promise/c (listof binding?))]
|
|
[post-data/raw (or/c false/c bytes?)]
|
|
[host-ip string?]
|
|
[host-port number?]
|
|
[client-ip string?])]{
|
|
An HTTP @racket[method] request to @racket[uri] from @racket[client-ip]
|
|
to the server at @racket[host-ip]:@racket[host-port] with @racket[headers/raw]
|
|
headers, @racket[bindings/raw] GET and POST queries and @racket[post-data/raw]
|
|
POST data.
|
|
|
|
You are @bold{unlikely to need to construct} a request struct.
|
|
}
|
|
|
|
@defproc[(request-bindings/raw [r request?])
|
|
(listof binding?)]{
|
|
Forces @racket[(request-bindings/raw-promise r)].
|
|
}
|
|
|
|
Here is an example typical of what you will find in many applications:
|
|
@racketblock[
|
|
(define (get-number req)
|
|
(match
|
|
(bindings-assq
|
|
#"number"
|
|
(request-bindings/raw req))
|
|
[(? binding:form? b)
|
|
(string->number
|
|
(bytes->string/utf-8
|
|
(binding:form-value b)))]
|
|
[_
|
|
(get-number (request-number))]))
|
|
]
|
|
|
|
}
|
|
|
|
@; ------------------------------------------------------------
|
|
@section[#:tag "bindings"]{Bindings}
|
|
@(require (for-label web-server/http/bindings))
|
|
|
|
@defmodule[web-server/http/bindings]{
|
|
|
|
These functions, while convenient, could introduce subtle bugs into your
|
|
application. Examples: that they are case-insensitive could introduce
|
|
a bug; if the data submitted is not in UTF-8 format, then the conversion
|
|
to a string will fail; if an attacker submits a form field as if it were
|
|
a file, when it is not, then the @racket[request-bindings] will hold a
|
|
@racket[bytes?] object and your program will error; and, for file uploads
|
|
you lose the filename. @bold{Therefore, we recommend against their use, but
|
|
they are provided for compatibility with old code.}
|
|
|
|
@defproc[(request-bindings [req request?])
|
|
(listof (or/c (cons/c symbol? string?)
|
|
(cons/c symbol? bytes?)))]{
|
|
Translates the @racket[request-bindings/raw] of @racket[req] by
|
|
interpreting @racket[bytes?] as @racket[string?]s, except in the case
|
|
of @racket[binding:file] bindings, which are left as is. Ids are then
|
|
translated into lowercase symbols.
|
|
}
|
|
|
|
@defproc[(request-headers [req request?])
|
|
(listof (cons/c symbol? string?))]{
|
|
Translates the @racket[request-headers/raw] of @racket[req] by
|
|
interpreting @racket[bytes?] as @racket[string?]s. Ids are then
|
|
translated into lowercase symbols.
|
|
}
|
|
|
|
@defproc[(extract-binding/single [id symbol?]
|
|
[binds (listof (cons/c symbol? string?))])
|
|
string?]{
|
|
Returns the single binding associated with @racket[id] in the a-list @racket[binds]
|
|
if there is exactly one binding. Otherwise raises @racket[exn:fail].
|
|
}
|
|
|
|
@defproc[(extract-bindings [id symbol?]
|
|
[binds (listof (cons/c symbol? string?))])
|
|
(listof string?)]{
|
|
Returns a list of all the bindings of @racket[id] in the a-list @racket[binds].
|
|
}
|
|
|
|
@defproc[(exists-binding? [id symbol?]
|
|
[binds (listof (cons/c symbol? string))])
|
|
boolean?]{
|
|
Returns @racket[#t] if @racket[binds] contains a binding for @racket[id].
|
|
Otherwise, @racket[#f].
|
|
}
|
|
|
|
Here is an example typical of what you will find in many applications:
|
|
@racketblock[
|
|
(define (get-number req)
|
|
(string->number
|
|
(extract-binding/single
|
|
'number
|
|
(request-bindings req))))
|
|
]
|
|
|
|
}
|
|
|
|
@; ------------------------------------------------------------
|
|
@section[#:tag "response-structs"]{Responses}
|
|
@(require (for-label web-server/http/response-structs))
|
|
|
|
@defmodule[web-server/http/response-structs]{
|
|
|
|
@defstruct*[response
|
|
([code number?]
|
|
[message bytes?]
|
|
[seconds number?]
|
|
[mime (or/c false/c bytes?)]
|
|
[headers (listof header?)]
|
|
[output (output-port? . -> . void)])]{
|
|
An HTTP response where @racket[output] produces the body. @racket[code] is the response code,
|
|
@racket[message] the message, @racket[seconds] the generation time, @racket[mime]
|
|
the MIME type of the file, and @racket[headers] are the headers. If @racket[headers] does not include @litchar{Date}, @litchar{Last-Modified}, @litchar{Server}, or @litchar{Content-Type} headers, then the server will automatically add them. The server will always replace your @litchar{Connection} header if it needs to ensure the connection will be closed. (Typically with an HTTP/1.0 client.)
|
|
|
|
Example:
|
|
@racketblock[
|
|
(response
|
|
301 #"Moved Permanently"
|
|
(current-seconds) TEXT/HTML-MIME-TYPE
|
|
(list (make-header #"Location"
|
|
#"http://racket-lang.org/download"))
|
|
(λ (op) (write-bytes #"Moved" op)))
|
|
]
|
|
}
|
|
|
|
@defproc[(response/full [code number?] [message bytes?] [seconds number?] [mime (or/c false/c bytes?)]
|
|
[headers (listof header?)] [body (listof bytes?)])
|
|
response?]{
|
|
A constructor for responses where @racket[body] is the response body.
|
|
|
|
Example:
|
|
@racketblock[
|
|
(response/full
|
|
301 #"Moved Permanently"
|
|
(current-seconds) TEXT/HTML-MIME-TYPE
|
|
(list (make-header #"Location"
|
|
#"http://racket-lang.org/download"))
|
|
(list #"<html><body><p>"
|
|
#"Please go to <a href=\""
|
|
#"http://racket-lang.org/download"
|
|
#"\">here</a> instead."
|
|
#"</p></body></html>"))
|
|
]
|
|
}
|
|
|
|
@defthing[TEXT/HTML-MIME-TYPE bytes?]{Equivalent to @racket[#"text/html; charset=utf-8"].}
|
|
|
|
@warning{If you include a Content-Length header in a response that is inaccurate, there @bold{will be an error} in
|
|
transmission that the server @bold{will not catch}.}
|
|
|
|
}
|
|
|
|
@; ------------------------------------------------------------
|
|
@section[#:tag "cookie"]{Placing Cookies}
|
|
|
|
@(require (for-label net/cookie
|
|
web-server/servlet
|
|
web-server/http/xexpr
|
|
web-server/http/redirect
|
|
web-server/http/request-structs
|
|
web-server/http/response-structs
|
|
web-server/http/cookie))
|
|
|
|
@defmodule[web-server/http/cookie]{
|
|
This module provides functions to create cookies and responses that set them.
|
|
|
|
@defproc[(make-cookie [name cookie-name?] [value cookie-value?]
|
|
[#:comment comment (or/c false/c string?) #f]
|
|
[#:domain domain (or/c false/c valid-domain?) #f]
|
|
[#:max-age max-age (or/c false/c exact-nonnegative-integer?) #f]
|
|
[#:path path (or/c false/c string?) #f]
|
|
[#:secure? secure? (or/c false/c boolean?) #f])
|
|
cookie?]{
|
|
Constructs a cookie with the appropriate fields.
|
|
}
|
|
|
|
@defproc[(cookie->header [c cookie?]) header?]{
|
|
Constructs a header that sets the cookie.
|
|
}
|
|
|
|
Examples:
|
|
@racketblock[
|
|
(define time-cookie
|
|
(make-cookie "time" (number->string (current-seconds))))
|
|
(define id-cookie
|
|
(make-cookie "id" "joseph" #:secure? #t))
|
|
|
|
(redirect-to
|
|
"http://localhost/logged-in"
|
|
see-other
|
|
#:headers
|
|
(map cookie->header
|
|
(list time-cookie id-cookie)))
|
|
|
|
(send/suspend
|
|
(lambda (k-url)
|
|
(response/xexpr
|
|
#:cookies (list time-cookie id-cookie)
|
|
`(html (head (title "Cookie Example"))
|
|
(body (h1 "You're cookie'd!"))))))
|
|
]
|
|
}
|
|
|
|
@; ------------------------------------------------------------
|
|
@section[#:tag "id-cookie"]{Authenticated Cookies}
|
|
|
|
@(require (for-label web-server/http/id-cookie))
|
|
@defmodule[web-server/http/id-cookie]{
|
|
|
|
Cookies are useful for storing information of user's browsers and
|
|
particularly useful for storing identifying information for
|
|
authentication, sessions, etc. However, there are inherent
|
|
difficulties when using cookies as authenticators, because cookie data
|
|
is fully controlled by the user, and thus cannot be trusted.
|
|
|
|
This module provides functions for creating and verifying
|
|
authenticated cookies that are intrinsically timestamped. It is based
|
|
on the algorithm proposed by the
|
|
@link["http://cookies.lcs.mit.edu/"]{MIT Cookie Eaters}: if you store
|
|
the data @racket[_data] at thime @racket[_authored-seconds], then the
|
|
user will receive @litchar{digest&authored-seconds&data}, where
|
|
@racket[_digest] is an HMAC-SHA1 digest of @racket[_authored-seconds]
|
|
and @racket[_data], using an arbitrary secret key. When you receive a
|
|
cookie, it will reverify this digest and check that the cookie's
|
|
@racket[_authored-seconds] is not after a timeout period, and only
|
|
then return the cookie data to the program.
|
|
|
|
The interface represents the secret key as a byte string. The best way
|
|
to generate this is by using random bytes from something like OpenSSL
|
|
or
|
|
@tt{/dev/random}. @link["http://www.madboa.com/geek/openssl/#random-generate"]{This
|
|
FAQ} lists a few options. A convenient purely Racket-based option is
|
|
available (@racket[make-secret-salt/file]), but it will not have as
|
|
good entropy, if you care about that sort of thing.
|
|
|
|
@defproc[(make-id-cookie
|
|
[name cookie-name?]
|
|
[secret-salt bytes?]
|
|
[value cookie-value?])
|
|
cookie?]{
|
|
Generates an authenticated cookie named @racket[name] containing @racket[value], signed with @racket[secret-salt].
|
|
}
|
|
|
|
@defproc[(request-id-cookie
|
|
[name cookie-name?]
|
|
[secret-salt bytes?]
|
|
[request request?]
|
|
[#:timeout timeout +inf.0])
|
|
(or/c false/c cookie-value?)]{
|
|
Extracts the first authenticated cookie named @racket[name] that was previously signed with @racket[secret-salt] before @racket[timeout] from @racket[request]. If no valid cookie is available, returns @racket[#f].
|
|
}
|
|
|
|
@defproc[(logout-id-cookie
|
|
[name cookie-name?])
|
|
cookie?]{
|
|
Generates a cookie named @racket[name] that is not validly authenticated.
|
|
|
|
This will cause non-malicious browsers to overwrite a previously set
|
|
cookie. If you use authenticated cookies for login information, you
|
|
could send this to cause a "logout". However, malicious browsers do
|
|
not need to respect such an overwrite. Therefore, this is not an
|
|
effective way to implement timeouts or protect users on
|
|
public (i.e. possibly compromised) computers. The only way to securely
|
|
logout on the compromised computer is to have server-side state
|
|
keeping track of which cookies (sessions, etc.) are invalid. Depending
|
|
on your application, it may be better to track live sessions or dead
|
|
sessions, or never set cookies to begin with and just use
|
|
continuations, which you can revoke with @racket[send/finish].
|
|
}
|
|
|
|
@defproc[(make-secret-salt/file
|
|
[secret-salt-path path-string?])
|
|
bytes?]{
|
|
|
|
Extracts the bytes from @racket[secret-salt-path]. If
|
|
@racket[secret-salt-path] does not exist, then it is created and
|
|
initialized with 128 random bytes.
|
|
}
|
|
}
|
|
|
|
@; ------------------------------------------------------------
|
|
@section[#:tag "cookie-parse"]{Extracting Cookies}
|
|
|
|
@(require (for-label web-server/http/cookie-parse
|
|
web-server/http/xexpr
|
|
net/cookie
|
|
net/url
|
|
racket/list))
|
|
@defmodule[web-server/http/cookie-parse]{
|
|
@defstruct[client-cookie
|
|
([name string?]
|
|
[value string?]
|
|
[domain (or/c false/c valid-domain?)]
|
|
[path (or/c false/c string?)])]{
|
|
|
|
While server cookies are represented with @racket[cookie?]s, cookies
|
|
that come from the client are represented with a
|
|
@racket[client-cookie] structure.
|
|
}
|
|
|
|
@defproc[(request-cookies [req request?])
|
|
(listof client-cookie?)]{
|
|
Extracts the cookies from @racket[req]'s headers.
|
|
}
|
|
|
|
Examples:
|
|
@racketblock[
|
|
(define (start req)
|
|
(define cookies (request-cookies req))
|
|
(define id-cookie
|
|
(findf (lambda (c)
|
|
(string=? "id" (client-cookie-name c)))
|
|
cookies))
|
|
(if id-cookie
|
|
(hello (client-cookie-value id-cookie))
|
|
(redirect-to
|
|
(url->string (request-uri req))
|
|
see-other
|
|
#:headers
|
|
(list
|
|
(cookie->header (make-cookie "id" "joseph"))))))
|
|
|
|
(define (hello who)
|
|
(response/xexpr
|
|
`(html (head (title "Hello!"))
|
|
(body
|
|
(h1 "Hello "
|
|
,who)))))
|
|
]
|
|
}
|
|
|
|
@; ------------------------------------------------------------
|
|
@section[#:tag "redirect"]{Redirect}
|
|
@(require (for-label web-server/http/redirect
|
|
web-server/private/util))
|
|
|
|
@defmodule[web-server/http/redirect]{
|
|
|
|
@defproc[(redirect-to [uri non-empty-string/c]
|
|
[perm/temp redirection-status? temporarily]
|
|
[#:headers headers (listof header?) (list)])
|
|
response?]{
|
|
Generates an HTTP response that redirects the browser to @racket[uri],
|
|
while including the @racket[headers] in the response.
|
|
|
|
Example:
|
|
@racket[(redirect-to "http://www.add-three-numbers.com" permanently)]
|
|
}
|
|
|
|
@defproc[(redirection-status? [v any/c])
|
|
boolean?]{
|
|
Determines if @racket[v] is one of the following values.
|
|
}
|
|
|
|
@defthing[permanently redirection-status?]{A @racket[redirection-status?] for permanent redirections.}
|
|
|
|
@defthing[temporarily redirection-status?]{A @racket[redirection-status?] for temporary redirections.}
|
|
|
|
@defthing[see-other redirection-status?]{A @racket[redirection-status?] for "see-other" redirections.}
|
|
|
|
}
|
|
|
|
@; ------------------------------------------------------------
|
|
@section[#:tag "basic-auth"]{Basic Authentication}
|
|
@(require (for-label web-server/http/response-structs
|
|
web-server/http/basic-auth))
|
|
|
|
@defmodule[web-server/http/basic-auth]{
|
|
|
|
An implementation of HTTP Basic Authentication.
|
|
|
|
@defproc[(make-basic-auth-header [realm string?])
|
|
header?]{
|
|
Returns a header that instructs the Web browser to request a username and password from the client using
|
|
Basic authentication with @racket[realm] as the realm.
|
|
}
|
|
|
|
@defproc[(request->basic-credentials [req request?])
|
|
(or/c false/c (cons/c bytes? bytes?))]{
|
|
Returns a pair of the username and password from the authentication
|
|
header in @racket[req] if they are present, or @racket[#f].
|
|
}
|
|
|
|
Example:
|
|
@racketmod[
|
|
web-server/insta
|
|
|
|
(define (start req)
|
|
(match (request->basic-credentials req)
|
|
[(cons user pass)
|
|
(response/xexpr
|
|
`(html (head (title "Basic Auth Test"))
|
|
(body (h1 "User: " ,(bytes->string/utf-8 user))
|
|
(h1 "Pass: " ,(bytes->string/utf-8 pass)))))]
|
|
[else
|
|
(response
|
|
401 #"Unauthorized" (current-seconds) TEXT/HTML-MIME-TYPE
|
|
(list
|
|
(make-basic-auth-header
|
|
(format "Basic Auth Test: ~a" (gensym))))
|
|
void)]))
|
|
]
|
|
}
|
|
|
|
@; ------------------------------------------------------------
|
|
@section[#:tag "digest-auth"]{Digest Authentication}
|
|
@(require (for-label web-server/http/digest-auth
|
|
web-server/http/xexpr
|
|
web-server/http/response-structs
|
|
racket/pretty))
|
|
|
|
@defmodule[web-server/http/digest-auth]{
|
|
|
|
An implementation of HTTP Digest Authentication.
|
|
|
|
@defproc[(make-digest-auth-header [realm string?] [private-key string?] [opaque string?])
|
|
header?]{
|
|
Returns a header that instructs the Web browser to request a username and password from the client
|
|
using Digest authentication with @racket[realm] as the realm, @racket[private-key] as the server's
|
|
contribution to the nonce, and @racket[opaque] as the opaque data passed through the client.
|
|
}
|
|
|
|
@defproc[(request->digest-credentials [req request?])
|
|
(or/c false/c (listof (cons/c symbol? string?)))]{
|
|
Returns the Digest credentials from @racket[req] (if they appear) as an association list.
|
|
}
|
|
|
|
@defthing[username*realm->password/c contract?]{
|
|
Used to look up the password for a user is a realm.
|
|
|
|
Equivalent to @racket[(string? string? . -> . string?)].
|
|
}
|
|
|
|
@defthing[username*realm->digest-HA1/c contract?]{
|
|
Used to compute the user's secret hash.
|
|
|
|
Equivalent to @racket[(string? string? . -> . bytes?)].
|
|
}
|
|
|
|
@defproc[(password->digest-HA1 [lookup-password username*realm->password/c])
|
|
username*realm->digest-HA1/c]{
|
|
Uses @racket[lookup-password] to find the password, then computes the
|
|
secret hash of it.
|
|
}
|
|
|
|
@defproc[(make-check-digest-credentials [lookup-HA1 username*realm->digest-HA1/c])
|
|
(string? (listof (cons/c symbol? string?)) . -> . boolean?)]{
|
|
Constructs a function that checks whether particular Digest credentials
|
|
(the second argument of the returned function) are correct given the
|
|
HTTP method provided as the first argument and the secret hash computed
|
|
by @racket[lookup-HA1].
|
|
|
|
This is will result in an exception if the Digest credentials are
|
|
missing portions.
|
|
}
|
|
|
|
Example:
|
|
@racketmod[
|
|
web-server/insta
|
|
(require racket/pretty)
|
|
|
|
(define private-key "private-key")
|
|
(define opaque "opaque")
|
|
|
|
(define (start req)
|
|
(match (request->digest-credentials req)
|
|
[#f
|
|
(response
|
|
401 #"Unauthorized" (current-seconds) TEXT/HTML-MIME-TYPE
|
|
(list (make-digest-auth-header
|
|
(format "Digest Auth Test: ~a" (gensym))
|
|
private-key opaque))
|
|
void)]
|
|
[alist
|
|
(define check
|
|
(make-check-digest-credentials
|
|
(password->digest-HA1 (lambda (username realm) "pass"))))
|
|
(define pass?
|
|
(check "GET" alist))
|
|
(response/xexpr
|
|
`(html (head (title "Digest Auth Test"))
|
|
(body
|
|
(h1 ,(if pass? "Pass!" "No Pass!"))
|
|
(pre ,(pretty-format alist)))))]))
|
|
]
|
|
}
|
|
|
|
@; ------------------------------------------------------------
|
|
@section[#:tag "xexpr"]{X-expression Support}
|
|
@(require (for-label web-server/http/xexpr
|
|
xml))
|
|
|
|
@defmodule*/no-declare[(web-server/http/xexpr)]{}
|
|
|
|
@declare-exporting[web-server/http/xexpr web-server]
|
|
|
|
@defproc[(response/xexpr [xexpr xexpr/c]
|
|
[#:code code number? 200]
|
|
[#:message message bytes? #"Okay"]
|
|
[#:seconds seconds number? (current-seconds)]
|
|
[#:mime-type mime-type (or/c false/c bytes?) TEXT/HTML-MIME-TYPE]
|
|
[#:headers headers (listof header?) empty]
|
|
[#:cookies cookies (listof cookie?) empty]
|
|
[#:preamble preamble bytes? #""])
|
|
response?]{
|
|
Equivalent to
|
|
@racketblock[
|
|
(response/full
|
|
code message seconds mime-type
|
|
(append headers (map cookie->header cookies))
|
|
(list preamble (string->bytes/utf-8 (xexpr->string xexpr))))
|
|
]
|
|
|
|
This is a viable function to pass to @racket[set-any->response!].
|
|
}
|