;;; HTTP messages ;; Copyright (C) 2010 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 ;; License as published by the Free Software Foundation; either ;; version 3 of the License, or (at your option) any later version. ;; ;; This library is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;; Lesser General Public License for more details. ;; ;; You should have received a copy of the GNU Lesser General Public ;; License along with this library; if not, write to the Free Software ;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA ;; 02110-1301 USA ;;; Commentary: ;;; ;;; This module has a number of routines to parse textual ;;; representations of HTTP data into native Scheme data structures. ;;; ;;; It tries to follow RFCs fairly strictly---the road to perdition ;;; being paved with compatibility hacks---though some allowances are ;;; made for not-too-divergent texts (like a quality of .2 which should ;;; be 0.2, etc). ;;; ;;; Code: (define-module (web http) #:use-module ((srfi srfi-1) #:select (append-map! map!)) #:use-module (srfi srfi-9) #:use-module (srfi srfi-19) #:use-module (ice-9 regex) #:use-module (ice-9 rdelim) #:use-module (web uri) #:export (header-decl? make-header-decl header-decl-sym header-decl-name header-decl-multiple? header-decl-parser header-decl-validator header-decl-writer lookup-header-decl declare-header! read-header parse-header valid-header? write-header read-headers write-headers parse-http-method parse-http-version parse-request-uri read-request-line write-request-line read-response-line write-response-line)) ;;; TODO ;;; ;;; Look at quality lists with more insight. ;;; Think about `accept' a bit more. ;;; (define-record-type (make-header-decl sym name multiple? parser validator writer) header-decl? (sym header-decl-sym) (name header-decl-name) (multiple? header-decl-multiple?) (parser header-decl-parser) (validator header-decl-validator) (writer header-decl-writer)) ;; sym -> header (define *declared-headers* (make-hash-table)) ;; downcased name -> header (define *declared-headers-by-name* (make-hash-table)) (define* (declare-header! sym name #:key multiple? parser validator writer) "Define a parser, validator, and writer for the HTTP header, @var{name}. @var{parser} should be a procedure that takes a string and returns a Scheme value. @var{validator} is a predicate for whether the given Scheme value is valid for this header. @var{writer} takes a value and a port, and writes the value to the port." (if (and (symbol? sym) (string? name) parser validator writer) (let ((decl (make-header-decl sym name multiple? parser validator writer))) (hashq-set! *declared-headers* sym decl) (hash-set! *declared-headers-by-name* (string-downcase name) decl) decl) (error "bad header decl" sym name multiple? parser validator writer))) (define (read-line* port) (let* ((pair (%read-line port)) (line (car pair)) (delim (cdr pair))) (if (and (string? line) (char? delim)) (let ((orig-len (string-length line))) (let lp ((len orig-len)) (if (and (> len 0) (char-whitespace? (string-ref line (1- len)))) (lp (1- len)) (if (= len orig-len) line (substring line 0 len))))) (bad-header '%read line)))) (define (read-continuation-line port val) (if (or (eqv? (peek-char port) #\space) (eqv? (peek-char port) #\tab)) (read-continuation-line port (string-append val (begin (read-line* port)))) val)) (define (read-header port) "Reads one HTTP header from @var{port}. Returns two values: the header name and the parsed Scheme value. May raise an exception if the header was known but the value was invalid. Returns @var{#f} for both values if the end of the message body was reached (i.e., a blank line)." (let ((line (read-line* port))) (if (or (string-null? line) (string=? line "\r")) (values #f #f) (let ((delim (or (string-index line #\:) (bad-header '%read line)))) (parse-header (substring line 0 delim) (read-continuation-line port (string-trim-both line char-whitespace? (1+ delim)))))))) (define (lookup-header-decl name) "Return the @var{header-decl} object registered for the given @var{name}. @var{name} may be a symbol or a string. Strings are mapped to headers in a case-insensitive fashion." (if (string? name) (hash-ref *declared-headers-by-name* (string-downcase name)) (hashq-ref *declared-headers* name))) (define (parse-header name val) "Parse @var{val}, a string, with the parser for the header named @var{name}. Returns two values, the header name and parsed value. If a parser was found, the header name will be returned as a symbol. If a parser was not found, both the header name and the value are returned as strings." (let* ((down (string-downcase name)) (decl (hash-ref *declared-headers-by-name* down))) (if decl (values (header-decl-sym decl) ((header-decl-parser decl) val)) (values down val)))) (define (valid-header? sym val) "Returns a true value iff @var{val} is a valid Scheme value for the header with name @var{sym}." (let ((decl (hashq-ref *declared-headers* sym))) (if (not decl) (error "Unknown header" sym) ((header-decl-validator decl) val)))) (define (write-header name val port) "Writes the given header name and value to @var{port}. If @var{name} is a symbol, looks up a declared header and uses that writer. Otherwise the value is written using @var{display}." (if (string? name) ;; assume that it's a header we don't know about... (begin (display name port) (display ": " port) (display val port) (display "\r\n" port)) (let ((decl (hashq-ref *declared-headers* name))) (if (not decl) (error "Unknown header" name) (begin (display (header-decl-name decl) port) (display ": " port) ((header-decl-writer decl) val port) (display "\r\n" port)))))) (define (read-headers port) "Read an HTTP message from @var{port}, returning the headers as an ordered alist." (let lp ((headers '())) (call-with-values (lambda () (read-header port)) (lambda (k v) (if k (lp (acons k v headers)) (reverse! headers)))))) (define (write-headers headers port) "Write the given header alist to @var{port}. Doesn't write the final \\r\\n, as the user might want to add another header." (let lp ((headers headers)) (if (pair? headers) (begin (write-header (caar headers) (cdar headers) port) (lp (cdr headers)))))) ;;; ;;; Utilities ;;; (define (bad-header sym val) (throw 'bad-header sym val)) (define (bad-header-component sym val) (throw 'bad-header sym val)) (define (parse-opaque-string str) str) (define (validate-opaque-string val) (string? val)) (define (write-opaque-string val port) (display val port)) (define separators-without-slash (string->char-set "[^][()<>@,;:\\\"?= \t]")) (define (validate-media-type str) (let ((idx (string-index str #\/))) (and idx (= idx (string-rindex str #\/)) (not (string-index str separators-without-slash))))) (define (parse-media-type str) (if (validate-media-type str) str (bad-header-component 'media-type str))) (define* (skip-whitespace str #:optional (start 0) (end (string-length str))) (let lp ((i start)) (if (and (< i end) (char-whitespace? (string-ref str i))) (lp (1+ i)) i))) (define* (trim-whitespace str #:optional (start 0) (end (string-length str))) (let lp ((i end)) (if (and (< start i) (char-whitespace? (string-ref str (1- i)))) (lp (1- i)) i))) (define* (split-and-trim str #:optional (delim #\,) (start 0) (end (string-length str))) (let lp ((i start)) (if (< i end) (let* ((idx (string-index str delim i end)) (tok (string-trim-both str char-whitespace? i (or idx end)))) (cons tok (split-and-trim str delim (if idx (1+ idx) end) end))) '()))) (define (collect-escaped-string from start len escapes) (let ((to (make-string len))) (let lp ((start start) (i 0) (escapes escapes)) (if (null? escapes) (begin (substring-move! from start (+ start (- len i)) to i) to) (let* ((e (car escapes)) (next-start (+ start (- e i) 2))) (substring-move! from start (- next-start 2) to i) (string-set! to e (string-ref from (- next-start 1))) (lp next-start (1+ e) (cdr escapes))))))) ;; in incremental mode, returns two values: the string, and the index at ;; which the string ended (define* (parse-qstring str #:optional (start 0) (end (trim-whitespace str start)) #:key incremental?) (if (and (< start end) (eqv? (string-ref str start) #\")) (let lp ((i (1+ start)) (qi 0) (escapes '())) (if (< i end) (case (string-ref str i) ((#\\) (lp (+ i 2) (1+ qi) (cons qi escapes))) ((#\") (let ((out (collect-escaped-string str (1+ start) qi escapes))) (if incremental? (values out (1+ i)) (if (= (1+ i) end) out (bad-header-component 'qstring str))))) (else (lp (1+ i) (1+ qi) escapes))) (bad-header-component 'qstring str))) (bad-header-component 'qstring str))) (define (write-list l port write-item delim) (if (pair? l) (let lp ((l l)) (write-item (car l) port) (if (pair? (cdr l)) (begin (display delim port) (lp (cdr l))))))) (define (write-qstring str port) (display #\" port) (if (string-index str #\") ;; optimize me (write-list (string-split str #\") port display "\\\"") (display str port)) (display #\" port)) (define* (parse-quality str #:optional (start 0) (end (string-length str))) (define (char->decimal c) (let ((i (- (char->integer c) (char->integer #\0)))) (if (and (<= 0 i) (< i 10)) i (bad-header-component 'quality str)))) (cond ((not (< start end)) (bad-header-component 'quality str)) ((eqv? (string-ref str start) #\1) (if (or (string= str "1" start end) (string= str "1." start end) (string= str "1.0" start end) (string= str "1.00" start end) (string= str "1.000" start end)) 1000 (bad-header-component 'quality str))) ((eqv? (string-ref str start) #\0) (if (or (string= str "0" start end) (string= str "0." start end)) 0 (if (< 2 (- end start) 6) (let lp ((place 1) (i (+ start 4)) (q 0)) (if (= i (1+ start)) (if (eqv? (string-ref str (1+ start)) #\.) q (bad-header-component 'quality str)) (lp (* 10 place) (1- i) (if (< i end) (+ q (* place (char->decimal (string-ref str i)))) q)))) (bad-header-component 'quality str)))) ;; Allow the nonstandard .2 instead of 0.2. ((and (eqv? (string-ref str start) #\.) (< 1 (- end start) 5)) (let lp ((place 1) (i (+ start 3)) (q 0)) (if (= i start) q (lp (* 10 place) (1- i) (if (< i end) (+ q (* place (char->decimal (string-ref str i)))) q))))) (else (bad-header-component 'quality str)))) (define (valid-quality? q) (and (non-negative-integer? q) (<= q 1000))) (define (write-quality q port) (define (digit->char d) (integer->char (+ (char->integer #\0) d))) (display (digit->char (modulo (quotient q 1000) 10)) port) (display #\. port) (display (digit->char (modulo (quotient q 100) 10)) port) (display (digit->char (modulo (quotient q 10) 10)) port) (display (digit->char (modulo q 10)) port)) (define (list-of? val pred) (or (null? val) (and (pair? val) (pred (car val)) (list-of? (cdr val) pred)))) (define* (parse-quality-list str) (map (lambda (part) (cond ((string-rindex part #\;) => (lambda (idx) (let ((qpart (string-trim-both part char-whitespace? (1+ idx)))) (if (string-prefix? "q=" qpart) (cons (parse-quality qpart 2) (string-trim-both part char-whitespace? 0 idx)) (bad-header-component 'quality qpart))))) (else (cons 1000 (string-trim-both part char-whitespace?))))) (string-split str #\,))) (define (validate-quality-list l) (list-of? l (lambda (elt) (and (pair? elt) (valid-quality? (car elt)) (string? (cdr elt)))))) (define (write-quality-list l port) (write-list l port (lambda (x port) (let ((q (car x)) (str (cdr x))) (display str port) (if (< q 1000) (begin (display ";q=" port) (write-quality q port))))) ",")) (define* (parse-non-negative-integer val #:optional (start 0) (end (string-length val))) (define (char->decimal c) (let ((i (- (char->integer c) (char->integer #\0)))) (if (and (<= 0 i) (< i 10)) i (bad-header-component 'non-negative-integer val)))) (if (not (< start end)) (bad-header-component 'non-negative-integer val) (let lp ((i start) (out 0)) (if (< i end) (lp (1+ i) (+ (* out 10) (char->decimal (string-ref val i)))) out)))) (define (non-negative-integer? code) (and (number? code) (>= code 0) (exact? code) (integer? code))) (define (default-kons k val) (if val (cons k val) k)) (define (default-kv-validator k val) #t) (define (default-val-writer k val port) (if (or (string-index val #\;) (string-index val #\,) (string-index val #\")) (write-qstring val port) (display val port))) (define* (parse-key-value-list str #:optional (kproc identity) (kons default-kons) (start 0) (end (string-length str))) (let lp ((i start) (out '())) (if (not (< i end)) (reverse! out) (let* ((i (skip-whitespace str i end)) (eq (string-index str #\= i end)) (comma (string-index str #\, i end)) (delim (min (or eq end) (or comma end))) (k (kproc (substring str i (trim-whitespace str i delim))))) (call-with-values (lambda () (if (and eq (or (not comma) (< eq comma))) (let ((i (skip-whitespace str (1+ eq) end))) (if (and (< i end) (eqv? (string-ref str i) #\")) (parse-qstring str i end #:incremental? #t) (values (substring str i (trim-whitespace str i (or comma end))) (or comma end)))) (values #f delim))) (lambda (v-str next-i) (let ((i (skip-whitespace str next-i end))) (if (or (= i end) (eqv? (string-ref str i) #\,)) (lp (1+ i) (cons (kons k v-str) out)) (bad-header-component 'key-value-list (substring str start end)))))))))) (define* (key-value-list? list #:optional (valid? default-kv-validator)) (list-of? list (lambda (elt) (cond ((pair? elt) (let ((k (car elt)) (v (cdr elt))) (and (or (string? k) (symbol? k)) (valid? k v)))) ((or (string? elt) (symbol? elt)) (valid? elt #f)) (else #f))))) (define* (write-key-value-list list port #:optional (val-writer default-val-writer) (delim ", ")) (write-list list port (lambda (x port) (let ((k (if (pair? x) (car x) x)) (v (if (pair? x) (cdr x) #f))) (display k port) (if v (begin (display #\= port) (val-writer k v port))))) delim)) ;; param-component = token [ "=" (token | quoted-string) ] \ ;; *(";" token [ "=" (token | quoted-string) ]) ;; (define* (parse-param-component str #:optional (kproc identity) (kons default-kons) (start 0) (end (string-length str))) (let lp ((i start) (out '())) (if (not (< i end)) (values (reverse! out) end) (let ((delim (string-index str (lambda (c) (memq c '(#\, #\; #\=))) i))) (let ((k (kproc (substring str i (trim-whitespace str i (or delim end))))) (delimc (and delim (string-ref str delim)))) (case delimc ((#\=) (call-with-values (lambda () (let ((i (skip-whitespace str (1+ delim) end))) (if (and (< i end) (eqv? (string-ref str i) #\")) (parse-qstring str i end #:incremental? #t) (let ((delim (or (string-index str (lambda (c) (or (eqv? c #\;) (eqv? c #\,) (char-whitespace? c))) i end) end))) (values (substring str i delim) delim))))) (lambda (v-str next-i) (let ((x (kons k v-str)) (i (skip-whitespace str next-i end))) (case (and (< i end) (string-ref str i)) ((#f) (values (reverse! (cons x out)) end)) ((#\;) (lp (skip-whitespace str (1+ i) end) (cons x out))) (else ; including #\, (values (reverse! (cons x out)) i))))))) ((#\;) (lp (skip-whitespace str (1+ delim) end) (cons (kons k #f) out))) (else ;; either the end of the string or a #\, (values (reverse! (cons (kons k #f) out)) (or delim end))))))))) (define* (parse-param-list str #:optional (kproc identity) (kons default-kons) (start 0) (end (string-length str))) (let lp ((i start) (out '())) (call-with-values (lambda () (parse-param-component str kproc kons i end)) (lambda (item i) (if (< i end) (if (eqv? (string-ref str i) #\,) (lp (skip-whitespace str (1+ i) end) (cons item out)) (bad-header-component 'param-list str)) (reverse! (cons item out))))))) (define* (validate-param-list list #:optional (valid? default-kv-validator)) (list-of? list (lambda (elt) (key-value-list? list valid?)))) (define* (write-param-list list port #:optional (val-writer default-val-writer)) (write-list list port (lambda (item port) (write-key-value-list item port val-writer ";")) ",")) (define (list-of-strings? val) (list-of? val string?)) (define (write-list-of-strings val port) (write-list val port display ", ")) (define (parse-date str) ;; Unfortunately, there is no way to make string->date parse out the ;; "GMT" bit, so we play string games to append a format it will ;; understand (the +0000 bit). (string->date (if (string-suffix? " GMT" str) (string-append (substring str 0 (- (string-length str) 4)) " +0000") (bad-header-component 'date str)) "~a, ~d ~b ~Y ~H:~M:~S ~z")) (define (write-date date port) (display (date->string date "~a, ~d ~b ~Y ~H:~M:~S GMT") port)) (define (write-uri uri port) (display (unparse-uri uri) port)) (define (parse-entity-tag val) (if (string-prefix? "W/" val) (cons (parse-qstring val 2) #f) (cons (parse-qstring val) #t))) (define (entity-tag? val) (and (pair? val) (string? (car val)))) (define (write-entity-tag val port) (if (not (cdr val)) (display "W/" port)) (write-qstring (car val) port)) (define* (parse-entity-tag-list val #:optional (start 0) (end (string-length val))) (let ((strong? (not (string-prefix? "W/" val 0 2 start end)))) (call-with-values (lambda () (parse-qstring val (if strong? start (+ start 2)) end #:incremental? #t)) (lambda (tag next) (acons tag strong? (let ((next (skip-whitespace val next end))) (if (< next end) (if (eqv? (string-ref val next) #\,) (parse-entity-tag-list val (skip-whitespace val (1+ next) end) end) (bad-header-component 'entity-tag-list val)) '()))))))) (define (entity-tag-list? val) (list-of? val entity-tag?)) (define (write-entity-tag-list val port) (write-list val port write-entity-tag ", ")) ;;; ;;; Request-Line and Response-Line ;;; ;; Hmm. (define (bad-request message . args) (throw 'bad-request message args)) (define (bad-response message . args) (throw 'bad-response message args)) (define *known-versions* '()) (define* (parse-http-version str #:optional (start 0) (end (string-length str))) "Parse an HTTP version from @var{str}, returning it as a major-minor pair. For example, @code{HTTP/1.1} parses as the pair of integers, @code{(1 . 1)}." (or (let lp ((known *known-versions*)) (and (pair? known) (if (string= str (caar known) start end) (cdar known) (lp (cdr known))))) (let ((dot-idx (string-index str #\. start end))) (if (and (string-prefix? "HTTP/" str 0 5 start end) dot-idx (= dot-idx (string-rindex str #\. start end))) (cons (parse-non-negative-integer str (+ start 5) dot-idx) (parse-non-negative-integer str (1+ dot-idx) end)) (bad-header-component 'http-version (substring str start end)))))) (define (write-http-version val port) "Write the given major-minor version pair to @var{port}." (display "HTTP/" port) (display (car val) port) (display #\. port) (display (cdr val) port)) (for-each (lambda (v) (set! *known-versions* (acons v (parse-http-version v 0 (string-length v)) *known-versions*))) '("HTTP/1.0" "HTTP/1.1")) ;; Request-URI = "*" | absoluteURI | abs_path | authority ;; ;; The `authority' form is only permissible for the CONNECT method, so ;; because we don't expect people to implement CONNECT, we save ;; ourselves the trouble of that case, and disallow the CONNECT method. ;; (define* (parse-http-method str #:optional (start 0) (end (string-length str))) "Parse an HTTP method from @var{str}. The result is an upper-case symbol, like @code{GET}." (cond ((string= str "GET" start end) 'GET) ((string= str "HEAD" start end) 'HEAD) ((string= str "POST" start end) 'POST) ((string= str "PUT" start end) 'PUT) ((string= str "DELETE" start end) 'DELETE) ((string= str "OPTIONS" start end) 'OPTIONS) ((string= str "TRACE" start end) 'TRACE) (else (bad-request "Invalid method: ~a" (substring str start end))))) (define* (parse-request-uri str #:optional (start 0) (end (string-length str))) "Parse a URI from an HTTP request line. Note that URIs in requests do not have to have a scheme or host name. The result is a URI object." (cond ((= start end) (bad-request "Missing Request-URI")) ((string= str "*" start end) #f) ((eq? (string-ref str start) #\/) (let* ((q (string-index str #\? start end)) (f (string-index str #\# start end)) (q (and q (or (not f) (< q f)) q))) (build-uri 'http #:path (substring str start (or q f end)) #:query (and q (substring str (1+ q) (or f end))) #:fragment (and f (substring str (1+ f) end))))) (else (or (parse-uri (substring str start end)) (bad-request "Invalid URI: ~a" (substring str start end)))))) (define (read-request-line port) "Read the first line of an HTTP request from @var{port}, returning three values: the method, the URI, and the version." (let* ((line (read-line* port)) (d0 (string-index line char-whitespace?)) ; "delimiter zero" (d1 (string-rindex line char-whitespace?))) (if (and d0 d1 (< d0 d1)) (values (parse-http-method line 0 d0) (parse-request-uri line (skip-whitespace line (1+ d0) d1) d1) (parse-http-version line (1+ d1) (string-length line))) (bad-request "Bad Request-Line: ~s" line)))) (define (write-uri uri port) (if (uri-host uri) (begin (display (uri-scheme uri) port) (display "://" port) (if (uri-userinfo uri) (begin (display (uri-userinfo uri) port) (display #\@ port))) (display (uri-host uri) port) (let ((p (uri-port uri))) (if (and p (not (eqv? p 80))) (begin (display #\: port) (display p port)))))) (let* ((path (uri-path uri)) (len (string-length path))) (cond ((and (> len 0) (not (eqv? (string-ref path 0) #\/))) (bad-request "Non-absolute URI path: ~s" path)) ((and (zero? len) (not (uri-host uri))) (bad-request "Empty path and no host for URI: ~s" uri)) (else (display path port)))) (if (uri-query uri) (begin (display #\? port) (display (uri-query uri) port)))) (define (write-request-line method uri version port) "Write the first line of an HTTP request to @var{port}." (display method port) (display #\space port) (write-uri uri port) (display #\space port) (write-http-version version port) (display "\r\n" port)) (define (read-response-line port) "Read the first line of an HTTP response from @var{port}, returning three values: the HTTP version, the response code, and the \"reason phrase\"." (let* ((line (read-line* port)) (d0 (string-index line char-whitespace?)) ; "delimiter zero" (d1 (and d0 (string-index line char-whitespace? (skip-whitespace line d0))))) (if (and d0 d1) (values (parse-http-version line 0 d0) (parse-non-negative-integer line (skip-whitespace line d0 d1) d1) (string-trim-both line char-whitespace? d1)) (bad-response "Bad Response-Line: ~s" line)))) (define (write-response-line version code reason-phrase port) "Write the first line of an HTTP response to @var{port}." (write-http-version version port) (display #\space port) (display code port) (display #\space port) (display reason-phrase port) (display "\r\n" port)) ;;; ;;; Syntax for declaring headers ;;; ;; emacs: (put 'declare-header 'scheme-indent-function 1) (define-syntax declare-header (syntax-rules () ((_ sym name parser validator writer arg ...) (declare-header! 'sym name #:parser parser #:validator validator #:writer writer arg ...)))) ;; emacs: (put 'declare-opaque-header 'scheme-indent-function 1) (define-syntax declare-opaque-header (syntax-rules () ((_ sym name) (declare-header sym name parse-opaque-string validate-opaque-string write-opaque-string)))) ;; emacs: (put 'declare-date-header 'scheme-indent-function 1) (define-syntax declare-date-header (syntax-rules () ((_ sym name) (declare-header sym name parse-date date? write-date)))) ;; emacs: (put 'declare-string-list-header 'scheme-indent-function 1) (define-syntax declare-string-list-header (syntax-rules () ((_ sym name) (declare-header sym name split-and-trim list-of-strings? write-list-of-strings)))) ;; emacs: (put 'declare-integer-header 'scheme-indent-function 1) (define-syntax declare-integer-header (syntax-rules () ((_ sym name) (declare-header sym name parse-non-negative-integer non-negative-integer? display)))) ;; emacs: (put 'declare-uri-header 'scheme-indent-function 1) (define-syntax declare-uri-header (syntax-rules () ((_ sym name) (declare-header sym name (lambda (str) (or (parse-uri str) (bad-header-component 'uri str))) uri? write-uri)))) ;; emacs: (put 'declare-quality-list-header 'scheme-indent-function 1) (define-syntax declare-quality-list-header (syntax-rules () ((_ sym name) (declare-header sym name parse-quality-list validate-quality-list write-quality-list)))) ;; emacs: (put 'declare-param-list-header 'scheme-indent-function 1) (define-syntax declare-param-list-header (syntax-rules () ((_ sym name) (declare-param-list-header sym name identity default-kons default-kv-validator default-val-writer)) ((_ sym name kproc) (declare-param-list-header sym name kproc default-kons default-kv-validator default-val-writer)) ((_ sym name kproc kons val-validator val-writer) (declare-header sym name (lambda (str) (parse-param-list str kproc kons)) (lambda (val) (validate-param-list val val-validator)) (lambda (val port) (write-param-list val port val-writer)))))) ;; emacs: (put 'declare-key-value-list-header 'scheme-indent-function 1) (define-syntax declare-key-value-list-header (syntax-rules () ((_ sym name) (declare-key-value-list-header sym name identity default-kons default-kv-validator default-val-writer)) ((_ sym name kproc) (declare-key-value-list-header sym name kproc default-kons default-kv-validator default-val-writer)) ((_ sym name kproc kons val-validator val-writer) (declare-header sym name (lambda (str) (parse-key-value-list str kproc kons)) (lambda (val) (key-value-list? val val-validator)) (lambda (val port) (write-key-value-list val port val-writer)))))) ;; emacs: (put 'declare-entity-tag-list-header 'scheme-indent-function 1) (define-syntax declare-entity-tag-list-header (syntax-rules () ((_ sym name) (declare-header sym name (lambda (str) (if (string=? str "*") '* (parse-entity-tag-list str))) (lambda (val) (or (eq? val '*) (entity-tag-list? val))) (lambda (val port) (if (eq? val '*) (display "*" port) (write-entity-tag-list val port))))))) ;;; ;;; General headers ;;; ;; Cache-Control = 1#(cache-directive) ;; cache-directive = cache-request-directive | cache-response-directive ;; cache-request-directive = ;; "no-cache" ; Section 14.9.1 ;; | "no-store" ; Section 14.9.2 ;; | "max-age" "=" delta-seconds ; Section 14.9.3, 14.9.4 ;; | "max-stale" [ "=" delta-seconds ] ; Section 14.9.3 ;; | "min-fresh" "=" delta-seconds ; Section 14.9.3 ;; | "no-transform" ; Section 14.9.5 ;; | "only-if-cached" ; Section 14.9.4 ;; | cache-extension ; Section 14.9.6 ;; cache-response-directive = ;; "public" ; Section 14.9.1 ;; | "private" [ "=" <"> 1#field-name <"> ] ; Section 14.9.1 ;; | "no-cache" [ "=" <"> 1#field-name <"> ]; Section 14.9.1 ;; | "no-store" ; Section 14.9.2 ;; | "no-transform" ; Section 14.9.5 ;; | "must-revalidate" ; Section 14.9.4 ;; | "proxy-revalidate" ; Section 14.9.4 ;; | "max-age" "=" delta-seconds ; Section 14.9.3 ;; | "s-maxage" "=" delta-seconds ; Section 14.9.3 ;; | cache-extension ; Section 14.9.6 ;; cache-extension = token [ "=" ( token | quoted-string ) ] ;; (declare-key-value-list-header cache-control "Cache-Control" (let ((known-directives (make-hash-table))) (for-each (lambda (s) (hash-set! known-directives s (string->symbol s))) '("no-cache" "no-store" "max-age" "max-stale" "min-fresh" "no-transform" "only-if-cached" "public" "private" "must-revalidate" "proxy-revalidate" "s-maxage")) (lambda (k-str) (hash-ref known-directives k-str k-str))) (lambda (k v-str) (case k ((max-age max-stale min-fresh s-maxage) (cons k (parse-non-negative-integer v-str))) ((private no-cache) (if v-str (cons k (map (lambda (f) (or (and=> (lookup-header-decl f) header-decl-sym) f)) (split-and-trim v-str))) k)) (else (if v-str (cons k v-str) k)))) default-kv-validator (lambda (k v port) (cond ((string? v) (display v port)) ((pair? v) (write-qstring (string-join v ", ") port)) ((integer? v) (display v port)) (else (bad-header-component 'cache-control v))))) ;; Connection = "Connection" ":" 1#(connection-token) ;; connection-token = token ;; e.g. ;; Connection: close, foo-header ;; (declare-string-list-header connection "Connection") ;; Date = "Date" ":" HTTP-date ;; e.g. ;; Date: Tue, 15 Nov 1994 08:12:31 GMT ;; (declare-date-header date "Date") ;; Pragma = "Pragma" ":" 1#pragma-directive ;; pragma-directive = "no-cache" | extension-pragma ;; extension-pragma = token [ "=" ( token | quoted-string ) ] ;; (declare-key-value-list-header pragma "Pragma" (lambda (k) (if (equal? k "no-cache") 'no-cache k))) ;; Trailer = "Trailer" ":" 1#field-name ;; (declare-string-list-header trailer "Trailer") ;; Transfer-Encoding = "Transfer-Encoding" ":" 1#transfer-coding ;; (declare-param-list-header transfer-encoding "Transfer-Encoding" (lambda (k) (if (equal? k "chunked") 'chunked k))) ;; Upgrade = "Upgrade" ":" 1#product ;; (declare-string-list-header upgrade "Upgrade") ;; Via = "Via" ":" 1#( received-protocol received-by [ comment ] ) ;; received-protocol = [ protocol-name "/" ] protocol-version ;; protocol-name = token ;; protocol-version = token ;; received-by = ( host [ ":" port ] ) | pseudonym ;; pseudonym = token ;; (declare-header via "Via" split-and-trim list-of-strings? write-list-of-strings #:multiple? #t) ;; Warning = "Warning" ":" 1#warning-value ;; ;; warning-value = warn-code SP warn-agent SP warn-text ;; [SP warn-date] ;; ;; warn-code = 3DIGIT ;; warn-agent = ( host [ ":" port ] ) | pseudonym ;; ; the name or pseudonym of the server adding ;; ; the Warning header, for use in debugging ;; warn-text = quoted-string ;; warn-date = <"> HTTP-date <"> (declare-header warning "Warning" (lambda (str) (let ((len (string-length str))) (let lp ((i (skip-whitespace str 0))) (let* ((idx1 (string-index str #\space i)) (idx2 (string-index str #\space (1+ idx1)))) (if (and idx1 idx2) (let ((code (parse-non-negative-integer str i idx1)) (agent (substring str (1+ idx1) idx2))) (call-with-values (lambda () (parse-qstring str (1+ idx2) #:incremental? #t)) (lambda (text i) (call-with-values (lambda () (let ((c (and (< i len) (string-ref str i)))) (case c ((#\space) ;; we have a date. (call-with-values (lambda () (parse-qstring str (1+ i) #:incremental? #t)) (lambda (date i) (values text (parse-date date) i)))) (else (values text #f i))))) (lambda (text date i) (let ((w (list code agent text date)) (c (and (< i len) (string-ref str i)))) (case c ((#f) (list w)) ((#\,) (cons w (lp (skip-whitespace str (1+ i))))) (else (bad-header 'warning str)))))))))))))) (lambda (val) (list-of? val (lambda (elt) (and (list? elt) (= (length elt) 4) (apply (lambda (code host text date) (and (non-negative-integer? code) (< code 1000) (string? host) (string? text) (or (not date) (date? date)))) elt))))) (lambda (val port) (write-list val port (lambda (w port) (apply (lambda (code host text date) (display code port) (display #\space port) (display host port) (display #\space port) (write-qstring text port) (if date (begin (display #\space port) (write-date date port)))) w)) ", ")) #:multiple? #t) ;;; ;;; Entity headers ;;; ;; Allow = #Method ;; (declare-string-list-header allow "Allow") ;; Content-Encoding = 1#content-coding ;; (declare-string-list-header content-encoding "Content-Encoding") ;; Content-Language = 1#language-tag ;; (declare-string-list-header content-language "Content-Language") ;; Content-Length = 1*DIGIT ;; (declare-integer-header content-length "Content-Length") ;; Content-Location = ( absoluteURI | relativeURI ) ;; (declare-uri-header content-location "Content-Location") ;; Content-MD5 = ;; (declare-opaque-header content-md5 "Content-MD5") ;; Content-Range = content-range-spec ;; content-range-spec = byte-content-range-spec ;; byte-content-range-spec = bytes-unit SP ;; byte-range-resp-spec "/" ;; ( instance-length | "*" ) ;; byte-range-resp-spec = (first-byte-pos "-" last-byte-pos) ;; | "*" ;; instance-length = 1*DIGIT ;; (declare-header content-range "Content-Range" (lambda (str) (let ((dash (string-index str #\-)) (slash (string-index str #\/))) (if (and (string-prefix? "bytes " str) slash) (list 'bytes (cond (dash (cons (parse-non-negative-integer str 6 dash) (parse-non-negative-integer str (1+ dash) slash))) ((string= str "*" 6 slash) '*) (else (bad-header 'content-range str))) (if (string= str "*" (1+ slash)) '* (parse-non-negative-integer str (1+ slash)))) (bad-header 'content-range str)))) (lambda (val) (and (list? val) (= (length val) 3) (symbol? (car val)) (let ((x (cadr val))) (or (eq? x '*) (and (pair? x) (non-negative-integer? (car x)) (non-negative-integer? (cdr x))))) (let ((x (caddr val))) (or (eq? x '*) (non-negative-integer? x))))) (lambda (val port) (display (car val) port) (display #\space port) (if (eq? (cadr val) '*) (display #\* port) (begin (display (caadr val) port) (display #\- port) (display (caadr val) port))) (if (eq? (caddr val) '*) (display #\* port) (display (caddr val) port)))) ;; Content-Type = media-type ;; (declare-header content-type "Content-Type" (lambda (str) (let ((parts (string-split str #\;))) (cons (parse-media-type (car parts)) (map (lambda (x) (let ((eq (string-index x #\=))) (if (and eq (= eq (string-rindex x #\=))) (cons (string-trim x char-whitespace? 0 eq) (string-trim-right x char-whitespace? (1+ eq))) (bad-header 'content-type str)))) (cdr parts))))) (lambda (val) (and (pair? val) (string? (car val)) (list-of? (cdr val) (lambda (x) (and (pair? x) (string? (car x)) (string? (cdr x))))))) (lambda (val port) (display (car val) port) (if (pair? (cdr val)) (begin (display ";" port) (write-list (cdr val) port (lambda (pair port) (display (car pair) port) (display #\= port) (display (cdr pair) port)) ";"))))) ;; Expires = HTTP-date ;; (declare-date-header expires "Expires") ;; Last-Modified = HTTP-date ;; (declare-date-header last-modified "Last-Modified") ;;; ;;; Request headers ;;; ;; Accept = #( media-range [ accept-params ] ) ;; media-range = ( "*/*" | ( type "/" "*" ) | ( type "/" subtype ) ) ;; *( ";" parameter ) ;; accept-params = ";" "q" "=" qvalue *( accept-extension ) ;; accept-extension = ";" token [ "=" ( token | quoted-string ) ] ;; (declare-param-list-header accept "Accept" ;; -> ("type/subtype" (str-prop . str-val) ...) ...) ;; ;; with the exception of prop = "q", in which case the prop will be ;; the symbol 'q, and the val will be a valid quality value ;; (lambda (k) (if (string=? k "q") 'q k)) (lambda (k v) (if (eq? k 'q) (cons k (parse-quality v)) (default-kons k v))) (lambda (k v) (if (eq? k 'q) (valid-quality? v) (default-kv-validator k v))) (lambda (k v port) (if (eq? k 'q) (write-quality v port) (default-val-writer k v port)))) ;; Accept-Charset = 1#( ( charset | "*" )[ ";" "q" "=" qvalue ] ) ;; (declare-quality-list-header accept-charset "Accept-Charset") ;; Accept-Encoding = 1#( codings [ ";" "q" "=" qvalue ] ) ;; codings = ( content-coding | "*" ) ;; (declare-quality-list-header accept-encoding "Accept-Encoding") ;; Accept-Language = 1#( language-range [ ";" "q" "=" qvalue ] ) ;; language-range = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" ) ;; (declare-quality-list-header accept-language "Accept-Language") ;; Authorization = credentials ;; ;; Authorization is basically opaque to this HTTP stack, we just pass ;; the string value through. ;; (declare-opaque-header authorization "Authorization") ;; Expect = 1#expectation ;; expectation = "100-continue" | expectation-extension ;; expectation-extension = token [ "=" ( token | quoted-string ) ;; *expect-params ] ;; expect-params = ";" token [ "=" ( token | quoted-string ) ] ;; (declare-param-list-header expect "Expect" (lambda (k) (if (equal? k "100-continue") '100-continue k))) ;; From = mailbox ;; ;; Should be an email address; we just pass on the string as-is. ;; (declare-opaque-header from "From") ;; Host = host [ ":" port ] ;; (declare-header host "Host" (lambda (str) (let ((colon (string-index str #\:))) (if colon (cons (substring str 0 colon) (parse-non-negative-integer str (1+ colon))) (cons str #f)))) (lambda (val) (and (pair? val) (string? (car val)) (or (not (cdr val)) (non-negative-integer? (cdr val))))) (lambda (val port) (display (car val) port) (if (cdr val) (begin (display #\: port) (display (cdr val) port))))) ;; If-Match = ( "*" | 1#entity-tag ) ;; (declare-entity-tag-list-header if-match "If-Match") ;; If-Modified-Since = HTTP-date ;; (declare-date-header if-modified-since "If-Modified-Since") ;; If-None-Match = ( "*" | 1#entity-tag ) ;; (declare-entity-tag-list-header if-none-match "If-None-Match") ;; If-Range = ( entity-tag | HTTP-date ) ;; (declare-header if-range "If-Range" (lambda (str) (if (or (string-prefix? "\"" str) (string-prefix? "W/" str)) (parse-entity-tag str) (parse-date str))) (lambda (val) (or (date? val) (entity-tag? val))) (lambda (val port) (if (date? val) (write-date val port) (write-entity-tag val port)))) ;; If-Unmodified-Since = HTTP-date ;; (declare-date-header if-unmodified-since "If-Unmodified-Since") ;; Max-Forwards = 1*DIGIT ;; (declare-integer-header max-forwards "Max-Forwards") ;; Proxy-Authorization = credentials ;; (declare-opaque-header proxy-authorization "Proxy-Authorization") ;; Range = "Range" ":" ranges-specifier ;; ranges-specifier = byte-ranges-specifier ;; byte-ranges-specifier = bytes-unit "=" byte-range-set ;; byte-range-set = 1#( byte-range-spec | suffix-byte-range-spec ) ;; byte-range-spec = first-byte-pos "-" [last-byte-pos] ;; first-byte-pos = 1*DIGIT ;; last-byte-pos = 1*DIGIT ;; suffix-byte-range-spec = "-" suffix-length ;; suffix-length = 1*DIGIT ;; (declare-header range "Range" (lambda (str) (if (string-prefix? "bytes=" str) (cons 'bytes (map (lambda (x) (let ((dash (string-index x #\-))) (cond ((not dash) (bad-header 'range str)) ((zero? dash) (cons #f (parse-non-negative-integer x 1))) ((= dash (1- (string-length x))) (cons (parse-non-negative-integer x 0 dash) #f)) (else (cons (parse-non-negative-integer x 0 dash) (parse-non-negative-integer x (1+ dash))))))) (string-split (substring str 6) #\,))) (bad-header 'range str))) (lambda (val) (and (pair? val) (symbol? (car val)) (list-of? (cdr val) (lambda (elt) (and (pair? elt) (let ((x (car elt)) (y (cdr elt))) (and (or x y) (or (not x) (non-negative-integer? x)) (or (not y) (non-negative-integer? y))))))))) (lambda (val port) (display (car val) port) (display #\= port) (write-list (cdr val) port (lambda (pair port) (if (car pair) (display (car pair) port)) (display #\- port) (if (cdr pair) (display (cdr pair) port))) ","))) ;; Referer = ( absoluteURI | relativeURI ) ;; (declare-uri-header referer "Referer") ;; TE = #( t-codings ) ;; t-codings = "trailers" | ( transfer-extension [ accept-params ] ) ;; (declare-param-list-header te "TE" (lambda (k) (if (equal? k "trailers") 'trailers k))) ;; User-Agent = 1*( product | comment ) ;; (declare-opaque-header user-agent "User-Agent") ;;; ;;; Reponse headers ;;; ;; Accept-Ranges = acceptable-ranges ;; acceptable-ranges = 1#range-unit | "none" ;; (declare-string-list-header accept-ranges "Accept-Ranges") ;; Age = age-value ;; age-value = delta-seconds ;; (declare-integer-header age "Age") ;; ETag = entity-tag ;; (declare-header etag "ETag" parse-entity-tag entity-tag? write-entity-tag) ;; Location = absoluteURI ;; (declare-uri-header location "Location") ;; Proxy-Authenticate = 1#challenge ;; ;; FIXME: split challenges ? (declare-opaque-header proxy-authenticate "Proxy-Authenticate") ;; Retry-After = ( HTTP-date | delta-seconds ) ;; (declare-header retry-after "Retry-After" (lambda (str) (if (and (not (string-null? str)) (char-numeric? (string-ref str 0))) (parse-non-negative-integer str) (parse-date str))) (lambda (val) (or (date? val) (non-negative-integer? val))) (lambda (val port) (if (date? val) (write-date val port) (display val port)))) ;; Server = 1*( product | comment ) ;; (declare-opaque-header server "Server") ;; Vary = ( "*" | 1#field-name ) ;; (declare-header vary "Vary" (lambda (str) (if (equal? str "*") '* (split-and-trim str))) (lambda (val) (or (eq? val '*) (list-of-strings? val))) (lambda (val port) (if (eq? val '*) (display "*" port) (write-list-of-strings val port)))) ;; WWW-Authenticate = 1#challenge ;; ;; Hum. (declare-opaque-header www-authenticate "WWW-Authenticate")