source: project/release/4/intarweb/trunk/header-parsers.scm @ 11914

Last change on this file since 11914 was 11914, checked in by sjamaan, 13 years ago

Implement improvement for entity-tag parser which makes it distinguish between quoted strong headers starting with 'W/' as value and regular weak headers

File size: 15.7 KB
Line 
1;; Change the accuracy of a number to 'digits' number of digits to the
2;; right of the decimal point.
3(define (chop-number num digits)
4  (let ((factor (expt 10 digits)))
5    (/ (round (* num factor)) factor)))
6
7(define (quality-parser str)
8  ;; Anything that's not a number is seen as if the value is missing, hence 1.0
9  (let* ((num       (or (string->number str) 1.0))
10         (imprecise (chop-number num 3)))
11    (max 0.0 (min 1.0 imprecise))))
12
13;; TODO: Make this use SRFI-19
14(define (rfc822-time-parser str)
15  0)
16
17;; Get the raw contents of a header
18(define (header-contents name headers #!optional (default #f))
19  (alist-ref name (headers-v headers) eq? default))
20
21;; Get all values of a header
22(define (header-values header-name headers)
23  (map (cut vector-ref <> 0) (header-contents header-name headers '())))
24
25;; Get the value of a header which is an alist
26;; Example: header = ((foo . bar) (qux . mooh)), header-list-ref foo
27;; will return bar.  (the header itself is encoded in a param+value
28(define (header-list-ref value headers #!optional (equal? eq?))
29  (find (lambda (h) (equal? (car (get-value h)) value)) headers))
30
31;; Get the value from one header entry
32(define get-value (cut vector-ref <> 0))
33
34;; Get all params from one header entry
35(define get-params (cut vector-ref <> 1))
36
37;; Get one specific parameter from one header entry
38(define (get-param param contents)
39  (alist-ref param (vector-ref contents 1) eq?))
40
41;; Get-param, except if no quality is present return 1
42(define (get-quality header-contents)
43  (or (get-param 'q header-contents) 1.0))
44
45;;;; Header parsers
46
47;; Find a matching endpoint for a token, ignoring escaped copies of the token
48(define (escaped-string-end str start stop-char-set)
49  (let ((len (string-length str)))
50    (let loop ((start start))
51      (let ((pos (string-index str (char-set-adjoin stop-char-set #\\) start)))
52        (if pos
53            (if (char=? #\\ (string-ref str pos))
54                ;; Escaped matching closing symbol
55                (if (= len (add1 pos))
56                    pos
57                    (loop (+ pos 2)))
58                ;; Reached the matching closing symbol
59                pos)
60            len))))) ; No matching closing symbol?  "Insert" it at the end
61
62;; RFC 2616 4.2 says multi-headers are a comma-separated list of tokens
63(define (split-multi-header value)
64  (let ((len (string-length value)))
65    (let loop ((result '())
66               (start-pos 0)   ; Where the current header value starts
67               (search-pos 0)) ; Where the searching starts
68      (or (and-let* (((< search-pos len))
69                     (pos (string-index value (char-set #\, #\") search-pos)))
70            (if (char=? #\, (string-ref value pos))
71                (loop (cons (string-copy value start-pos pos) result) (add1 pos) (add1 pos))
72                (let ((end-pos (escaped-string-end value (add1 pos) (char-set #\"))))
73                  (loop result start-pos (add1 end-pos)))))
74          (reverse (cons (string-drop value start-pos) result))))))
75
76;; Remove all escape characters from the input, recognising "escaped escapes"
77(define (unescape str)
78  (let ((last-char (sub1 (string-length str))))
79    (let loop ((result "")
80               (start-pos 0))
81      (or (and-let* ((pos (string-index str #\\ start-pos)))
82            (if (= pos last-char)
83                (string-append result (string-copy str start-pos))
84                (loop (string-append result (string-copy str start-pos pos)
85                                     (string-copy str (add1 pos) (+ pos 2)))
86                      (+ pos 2))))
87          (string-append result (string-copy str start-pos))))))
88
89;; Try to parse a token, starting at the provided offset, up until the
90;; char-set where we should stop.  Returns two values: the token or #f if
91;; there is no token left, and the position on which the token ends.
92(define (parse-token value start-pos stop-char-set)
93  (if (>= start-pos (string-length value))
94      (values #f start-pos)
95      (let ((stop (char-set-adjoin stop-char-set #\")))
96        (let ((pos (string-index value stop start-pos)))
97          (if pos
98              (if (not (char=? #\" (string-ref value pos)))
99                  (values (string-trim-both (string-copy value start-pos pos))
100                          pos) ; Stop-char found, but no quoting
101                  (let ((end-pos (escaped-string-end value (add1 pos)
102                                                     (char-set #\"))))
103                    ;; Found the double quote? Recurse on the remainder
104                    (receive (rest final-pos)
105                      (parse-token value (add1 end-pos) stop-char-set)
106                      (values (string-append
107                               (string-trim-both
108                                (string-copy value start-pos pos))
109                               (if (= pos end-pos)
110                                   (unescape (string-copy value (add1 pos)))
111                                   (unescape (string-copy value (add1 pos) end-pos)))
112                               (or rest ""))
113                              final-pos))))
114              ;; Nothing found?  Then the remainder of the string is the token
115              (values (string-trim-both (string-copy value start-pos))
116                      (string-length value)))))))
117
118;; Just put all header strings in a list, so we can pass it on
119;; Make no assumptions about the contents (only value, don't try to parse params)
120;; This is different from (multiple (without-params generic-header-parser))
121;; because this does not assume it can split up comma-separated values
122(define (unknown-header-parser name contents headers)
123  (update-header-contents! name (list (vector contents '())) headers))
124
125(define (parse-parameters string start-pos param-parsers)
126  (let loop ((start-pos start-pos)
127             (params '()))
128    (receive (key pos)
129      (parse-token string start-pos (char-set #\; #\=))
130      (if key
131          (if (and (< pos (string-length string)) (char=? (string-ref string pos) #\=))
132              (receive (value pos)
133                (parse-token string (add1 pos) (char-set #\;))
134                (let ((key (string->symbol (string-downcase key))))
135                  ;; In case of no value ("foo="), use the empty string as value
136                  (let ((value ((alist-ref key param-parsers eq? identity) (or value ""))))
137                    (loop (add1 pos) (cons (cons key value) params)))))
138              ;; Missing value is interpreted as "present",
139              ;; so #t. If not present, it's #f when looking it up
140              (loop (add1 pos) (cons (cons (string->symbol key) #t) params)))
141          (values (reverse params) pos)))))
142
143(define (parse-value+parameters string start-pos value-parser param-parsers)
144  (receive (value pos)
145    (parse-token string start-pos (char-set #\;))
146    (if (not value)
147        (values #f pos) ;; XXX this is wrong and not expected by the caller!
148        (receive (params pos)
149          (parse-parameters string (add1 pos) param-parsers)
150          (values (vector (value-parser value) params) pos)))))
151
152(define (with-params value-parser parameter-parsers)
153  (lambda (entry)
154    (receive (type+params pos)
155      (parse-value+parameters entry 0 value-parser parameter-parsers)
156      type+params)))
157
158(define (multiple other-parser #!optional (parameter-parsers '()))
159  (lambda (name entries headers)
160    (fold (lambda (entry headers)
161            (update-header-contents!
162             name
163             (list ((with-params other-parser parameter-parsers) entry))
164             headers))
165          headers
166          (split-multi-header entries))))
167
168(define (single other-parser #!optional (parameter-parsers '()))
169  (lambda (name contents headers)
170    (replace-header-contents!
171     name
172     (list ((with-params other-parser parameter-parsers) contents))
173     headers)))
174
175(define (key/values key/value-parsers)
176  (lambda (k/v)
177    ;; We're abusing parse-parameters here to read value
178    ;; instead of params.  This is weird, but it works :)
179    (receive (key+value pos)
180      (parse-parameters k/v 0 key/value-parsers)
181      (car key+value))))
182
183(define symbol-parser-ci
184  (compose string->symbol string-trim-both string-downcase))
185
186(define symbol-parser
187  (compose string->symbol string-trim-both))
188
189(define (natnum-parser contents)
190  (let ((num (string->number contents)))
191    (if num (inexact->exact (max 0 (round num))) 0)))
192
193; base64 of 128 bit hex digest as per RFC1864
194(define md5-parser base64:decode)
195
196;; bytes <start>-<end>/<total>
197(define (range-parser s)
198  (and-let* ((parts (string-match "bytes[ \t]+([0-9]+)-([0-9]+)/([0-9]+)" s)))
199            (map string->number (drop parts 1))))
200
201;; rfc1123-date | rfc850-date | asctime-date
202(define http-time-parser rfc822-time-parser)
203
204;; (W/)<string>
205(define (entity-tag-parser name contents headers)
206  (let ((contents (string-trim-both contents)))
207    (replace-header-contents!
208     name
209     (list (if (string-prefix? "W/" contents)
210               (vector `(weak . ,(parse-token contents 2 (char-set))) '())
211               (vector `(strong . ,(parse-token contents 0 (char-set))) '())))
212     headers)))
213
214;;;; MAJOR TODOs
215;; RFC822/1123 mailbox parser - just strings for now
216(define mailbox-parser identity)
217
218;; Either an entity-tag or a http-date
219(define if-range-parser identity)
220
221;; Either delta-seconds or RFC822 timestamp
222(define (retry-after-parser contents)
223  (if (string-match "[ \t]*[0-9]+[ \t]*" contents)
224      (natnum-parser contents)
225      (rfc822-time-parser contents)))
226
227;; Tricky - see 2616 14.45
228;; We probably shouldn't try to do too much parsing here
229(define via-parser (multiple identity))
230
231;; Tricky - see 2616 14.46
232(define warning-parser (multiple identity))
233;;;; END MAJOR TODOs
234
235(define (key/value-parser str)
236  (let ((idx (string-index str #\=)))
237    (cons (string-take str idx) (string-drop str (add1 idx)))))
238
239;; The 'expires' header defined by the Netscape cookie spec contains
240;; an embedded comma.  RFC 2109 cookies use Max-Age instead.
241(define (old-style-cookie? cookie)
242  (not (not (string-search (regexp "^[^=\"]+=[^;]*;.*expires[[:space:]]*=" #f) cookie))))
243
244(define set-cookie-parser
245  (let ((param-parsers `((expires . ,rfc822-time-parser)
246                         (max-age . ,string->number)
247                         (version . ,string->number))))
248    (lambda (name contents headers)
249      (if (old-style-cookie? contents)
250          (update-header-contents!
251           name
252           (list ((with-params key/value-parser param-parsers) contents))
253           headers)
254          ((multiple key/value-parser param-parsers) name contents headers)))))
255
256(define cache-control-parser
257  (let ((splitter (lambda (str) ;; Is this correct?
258                    (map (compose string->symbol string-trim-both)
259                         (string-split str ",")))))
260    (multiple
261     (key/values `((max-age . ,natnum-parser)
262                   (s-maxage . ,natnum-parser)
263                   (max-stale . ,natnum-parser)
264                   (min-fresh . ,natnum-parser)
265                   (private . ,splitter)
266                   (no-cache . ,splitter))))))
267
268;; This too
269(define (pragma-parser name value headers)
270  (update-header-contents! name (parse-parameters value 0 `()) headers))
271
272;; This one includes q parser
273(define (te-parser name value headers)
274  (update-header-contents! name (parse-parameters value 0 `((q . ,quality-parser))) headers))
275
276;; Cookie headers are also braindead: there can be several cookies in one header,
277;; separated by either commas or semicolons. The only way to distinguish a
278;; new cookie from a parameter of the current cookie is the dollar in front
279;; of all parameter names.
280;; Also, there's a $Version attribute that prepends all cookies, which is
281;; considered to apply to all cookies that follow.
282;;
283;; This code is a bit of a hack in the way it abuses parse-parameters
284(define (cookie-parser name value headers)
285  ;; Get all attributes (dollar-prefixed params) and remaining cookies & params
286  (define (split-attribs pairs)
287    (span (lambda (pair)
288            (string-prefix? "$" (symbol->string (car pair))))
289          pairs))
290  (receive (params pos)
291    (parse-parameters value 0 `(($version . ,string->number)
292                               ($port . ,string->number)))
293    (receive (global-attrs remaining)
294      (split-attribs params)
295      (let loop ((remaining remaining)
296                 (cookies '()))
297        (if (null? remaining)
298            (update-header-contents! name (reverse cookies) headers)
299            (let ((cookie (cons (symbol->string (caar remaining))
300                                (cdar remaining)))
301                  (params (cdr remaining)))
302              (receive (local-attrs rest)
303                (split-attribs params)
304                (let* ((all-attrs (append global-attrs local-attrs))
305                       (result (vector cookie all-attrs)))
306                  (loop rest (cons result cookies))))))))))
307
308;;; Unparsers ;;;
309(define (unparse-params params unparsers)
310  (let loop ((params params)
311             (results '()))
312    (if (null? params)
313        (string-join (reverse results) "; " 'prefix)
314        (let* ((unparser (alist-ref unparsers (caar params) eq?
315                                    (lambda (name value)
316                                      (case value
317                                        ;; #t means param is present (no value)
318                                        ((#t) (->string name))
319                                        ;; #f means param is missing
320                                        ((#f) #f)
321                                        (else (conc name "=" value))))))
322               (str (unparser (caar params) (cdar params))))
323          (loop (cdr params) (if str (cons str results) results))))))
324
325(define unparse-token
326  (let* ((must-be-quoted-chars (char-set-adjoin char-set:iso-control #\"))
327         (trigger-quoting-chars (char-set-union
328                                 (char-set-adjoin must-be-quoted-chars #\, #\; #\=)
329                                 char-set:blank))
330         (error-chars (char-set #\newline))
331         (smap (map (lambda (c)
332                      (cons (string c)
333                            (string-append "\\" (string c))))
334                    (char-set->list must-be-quoted-chars))))
335   (lambda (token)
336     (cond
337      ((string-any error-chars token)
338       (signal-http-condition "Unencoded newline in header contents! Please encode the newline in a way appropriate for this header"
339                              'unencoded-header 'value token))
340      ((string-any trigger-quoting-chars token)
341       (string-append "\"" (string-translate* token smap) "\""))
342      (else token)))))
343
344;; There's no need to make a specific header unparser for every header type.
345;; Usually, the Scheme value representing a header can unambiguously be
346;; unparsed into a header just by checking its type.
347(define (default-header-unparser header-name header-contents)
348  (let loop ((headers (reverse header-contents))
349             (result '()))
350    (if (null? headers)
351        (sprintf "~A: ~A\r\n"
352                 (header-name->string header-name)
353                 (string-join result ", "))
354        (let* ((contents (get-value (car headers)))
355               (value (cond
356                       ((pair? contents) ; alist?
357                        (if (eq? (cdr contents) #t)
358                            (unparse-token (->string (car contents)))
359                            (conc (unparse-token (->string (car contents))) "="
360                                  (unparse-token (->string (cdr contents))))))
361                       ((uri? contents) (unparse-token (uri->string contents)))
362                       (else (unparse-token (->string contents)))))
363               (parameter-unparsers '())) ; Maybe we want to make this a param
364         (loop (cdr headers)
365               (cons
366                (string-append
367                 value
368                 (unparse-params (get-params (car headers))
369                                 parameter-unparsers))
370                result))))))
Note: See TracBrowser for help on using the repository browser.