1
Fork 0
mirror of https://git.savannah.gnu.org/git/guile.git synced 2025-04-30 20:00:19 +02:00
guile/module/web/http.scm
Andy Wingo 9eed1010e7 leniency regarding quality values in http.scm
* module/web/http.scm: Add commentary.
  (parse-quality): Allow .NNN to be interpreted as 0.NNN.

* test-suite/tests/web-http.test ("request headers"): Add a test.
2010-12-06 13:52:56 +01:00

1495 lines
48 KiB
Scheme
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; 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 <header-decl>
(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)
(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)
(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)
(if (string? name)
(hash-ref *declared-headers-by-name* (string-downcase name))
(hashq-ref *declared-headers* name)))
(define (parse-header name val)
(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)
(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)
(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)
(let lp ((headers '()))
(call-with-values (lambda () (read-header port))
(lambda (k v)
(if k
(lp (acons k v headers))
(reverse! headers))))))
;; Doesn't write the final \r\n, as the user might want to add another
;; header.
(define (write-headers headers port)
(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)))
(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)
(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)))
(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)))
(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)
(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)
(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)
(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-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)
(cons k (if v-str (split-and-trim v-str) #t)))
(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 = <base64 of 128 bit MD5 digest as per RFC 1864>
;;
(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")