Skip to content

Commit 4135e33

Browse files
authored
Merge pull request #21 from LiberalArtist/rfc6265
Use RFC 6265 cookies from net-cookies
2 parents 219044f + 666889c commit 4135e33

File tree

7 files changed

+623
-325
lines changed

7 files changed

+623
-325
lines changed

web-server-doc/info.rkt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
(define collection 'multi)
44

55
(define build-deps '("net-doc"
6+
"net-cookies"
67
"rackunit-doc"
78
"compatibility-doc"
89
"db-doc"

web-server-doc/web-server/scribblings/http.scrbl

Lines changed: 111 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,10 @@ transmission that the server @bold{will not catch}.}
267267
@; ------------------------------------------------------------
268268
@section[#:tag "cookie"]{Placing Cookies}
269269

270-
@(require (for-label net/cookie
271-
web-server/servlet
270+
@(require (for-label (except-in net/cookies/server
271+
make-cookie)
272+
net/cookies/common
273+
web-server/servlet
272274
web-server/http/xexpr
273275
web-server/http/redirect
274276
web-server/http/request-structs
@@ -277,16 +279,26 @@ transmission that the server @bold{will not catch}.}
277279

278280
@defmodule[web-server/http/cookie]{
279281
This module provides functions to create cookies and responses that set them.
280-
281-
@defproc[(make-cookie [name cookie-name?] [value cookie-value?]
282-
[#:comment comment (or/c false/c string?) #f]
283-
[#:domain domain (or/c false/c valid-domain?) #f]
284-
[#:max-age max-age (or/c false/c exact-nonnegative-integer?) #f]
285-
[#:path path (or/c false/c string?) #f]
286-
[#:expires expires (or/c false/c string?) #f]
287-
[#:secure? secure? (or/c false/c boolean?) #f])
282+
283+
@defproc[(make-cookie [name cookie-name?]
284+
[value cookie-value?]
285+
[#:comment comment any/c #f]
286+
[#:domain domain (or/c domain-value? #f) #f]
287+
[#:max-age max-age (or/c (and/c integer? positive?) #f) #f]
288+
[#:path path (or/c path/extension-value? #f) #f]
289+
[#:expires expires (or/c date? string? #f) #f]
290+
[#:secure? secure? any/c #f]
291+
[#:http-only? http-only? any/c #f]
292+
[#:extension extension (or/c path/extension-value? #f) #f])
288293
cookie?]{
289-
Constructs a cookie with the appropriate fields.
294+
Constructs a cookie with the appropriate fields.
295+
296+
This is a wrapper around @racket[make-cookie] from @racketmodname[net/cookies/server]
297+
for backwards compatability. The @racket[comment] argument is ignored.
298+
If @racket[expires] is given as a string, it should match
299+
@link["https://tools.ietf.org/html/rfc7231#section-7.1.1.2"]{RFC 7231, Section 7.1.1.2},
300+
in which case it will be converted to a @racket[date?] value.
301+
If conversion fails, an @racket[exn:fail:contract?] is raised.
290302
}
291303

292304
@defproc[(cookie->header [c cookie?]) header?]{
@@ -348,50 +360,101 @@ FAQ} lists a few options. A convenient purely Racket-based option is
348360
available (@racket[make-secret-salt/file]),
349361
which is implemented using @racket[crypto-random-bytes].
350362

351-
@defproc[(make-id-cookie
352-
[name cookie-name?]
353-
[secret-salt bytes?]
354-
[value cookie-value?]
355-
[#:path path (or/c false/c string?) #f])
356-
cookie?]{
363+
@defproc*[([(make-id-cookie
364+
[name (and/c string? cookie-name?)]
365+
[value (and/c string? cookie-value?)]
366+
[#:key secret-salt bytes?]
367+
[#:path path (or/c path/extension-value? #f) #f]
368+
[#:expires expires (or/c date? #f) #f]
369+
[#:max-age max-age
370+
(or/c (and/c integer? positive?) #f) #f]
371+
[#:domain domain (or/c domain-value? #f) #f]
372+
[#:secure? secure? any/c #f]
373+
[#:http-only? http-only? any/c #f]
374+
[#:extension extension
375+
(or/c path/extension-value? #f) #f])
376+
cookie?]
377+
[(make-id-cookie
378+
[name (and/c string? cookie-name?)]
379+
[secret-salt bytes?]
380+
[value (and/c string? cookie-value?)]
381+
[#:path path (or/c path/extension-value? #f) #f]
382+
[#:expires expires (or/c date? #f) #f]
383+
[#:max-age max-age
384+
(or/c (and/c integer? positive?) #f) #f]
385+
[#:domain domain (or/c domain-value? #f) #f]
386+
[#:secure? secure? any/c #f]
387+
[#:http-only? http-only? any/c #t]
388+
[#:extension extension
389+
(or/c path/extension-value? #f) #f])
390+
cookie?])]{
357391
Generates an authenticated cookie named @racket[name] containing @racket[value], signed with @racket[secret-salt].
392+
393+
The calling conventions allow @racket[secret-salt] to be given either as a keyword
394+
argument (mirroring the style of @racket[make-cookie]) or a by-position argument
395+
(for compatability with older versions of this library).
396+
397+
The other arguments are passed to @racket[make-cookie]; however, note that the
398+
default value for @racket[http-only?] is @racket[#t]. Users will also likely
399+
want to set @racket[secure?] to @racket[#t] when using HTTPS.
358400
}
359401

360-
@defproc[(request-id-cookie
361-
[name cookie-name?]
362-
[secret-salt bytes?]
363-
[request request?]
364-
[#:timeout timeout +inf.0])
365-
(or/c false/c cookie-value?)]{
366-
Extracts the first authenticated cookie named @racket[name] that was previously signed with @racket[secret-salt] before @racket[timeout] from @racket[request]. If no valid cookie is available, returns @racket[#f].
402+
@defproc*[([(request-id-cookie [request request?]
403+
[#:name name (and/c string? cookie-name?)]
404+
[#:key secret-salt bytes?]
405+
[#:timeout timeout number? +inf.0])
406+
(or/c #f (and/c string? cookie-value?))]
407+
[(request-id-cookie [name (and/c string? cookie-name?)]
408+
[secret-salt bytes?]
409+
[request request?]
410+
[#:timeout timeout number? +inf.0])
411+
(or/c #f (and/c string? cookie-value?))])]{
412+
Extracts the first authenticated cookie named @racket[name]
413+
that was previously signed with @racket[secret-salt]
414+
before @racket[timeout] from @racket[request].
415+
If no valid cookie is available, returns @racket[#f].
367416
}
368417

369-
@defproc[(logout-id-cookie
370-
[name cookie-name?]
371-
[#:path path (or/c false/c string?) #f])
418+
@defproc[(valid-id-cookie? [cookie any/c]
419+
[#:name name (and/c string? cookie-name?)]
420+
[#:key secret-salt bytes?]
421+
[#:timeout timeout number? +inf.0])
422+
(or/c #f (and/c string? cookie-value?))]{
423+
Recognizes authenticated cookies named @racket[name] that were
424+
previously signed with @racket[secret-salt]
425+
before @racket[timeout]. Values satisfying either @racket[cookie?]
426+
or @racket[client-cookie?] can be recognized.
427+
428+
Specifically, @racket[valid-id-cookie?] tests that
429+
@racket[(authored . <= . timeout)], where @racket[authored] is the
430+
value returned by @racket[(current-seconds)] when the cookie was created.
431+
}
432+
433+
@defproc[(logout-id-cookie [name cookie-name?]
434+
[#:path path (or/c #f string?) #f]
435+
[#:domain domain (or/c domain-value? #f) #f])
372436
cookie?]{
373-
Generates a cookie named @racket[name] that is not validly authenticated.
437+
Generates a cookie named @racket[name] that is not validly authenticated
438+
and expires in the past.
374439

375440
This will cause non-malicious browsers to overwrite a previously set
376-
cookie. If you use authenticated cookies for login information, you
377-
could send this to cause a "logout". However, malicious browsers do
378-
not need to respect such an overwrite. Therefore, this is not an
379-
effective way to implement timeouts or protect users on
380-
public (i.e. possibly compromised) computers. The only way to securely
381-
logout on the compromised computer is to have server-side state
382-
keeping track of which cookies (sessions, etc.) are invalid. Depending
383-
on your application, it may be better to track live sessions or dead
384-
sessions, or never set cookies to begin with and just use
385-
continuations, which you can revoke with @racket[send/finish].
441+
cookie. If you use authenticated cookies for login information, you
442+
could send this to cause a "logout". However, malicious browsers do
443+
not need to respect such an overwrite. Therefore, this is not an
444+
effective way to implement timeouts or protect users on
445+
public (i.e. possibly compromised) computers. The only way to securely
446+
logout on the compromised computer is to have server-side state
447+
keeping track of which cookies (sessions, etc.) are invalid. Depending
448+
on your application, it may be better to track live sessions or dead
449+
sessions, or never set cookies to begin with and just use
450+
continuations, which you can revoke with @racket[send/finish].
386451
}
387452

388-
@defproc[(make-secret-salt/file
389-
[secret-salt-path path-string?])
390-
bytes?]{
391-
453+
@defproc[(make-secret-salt/file [secret-salt-path path-string?])
454+
bytes?]{
392455
Extracts the bytes from @racket[secret-salt-path]. If
393-
@racket[secret-salt-path] does not exist, then it is created and
394-
initialized with 128 random bytes.
456+
@racket[secret-salt-path] does not exist, then it is created and
457+
initialized with 128 random bytes.
395458
}
396459
}
397460

@@ -400,15 +463,14 @@ initialized with 128 random bytes.
400463

401464
@(require (for-label web-server/http/cookie-parse
402465
web-server/http/xexpr
403-
net/cookie
404466
net/url
405467
racket/list))
406468
@defmodule[web-server/http/cookie-parse]{
407469
@defstruct[client-cookie
408-
([name string?]
409-
[value string?]
410-
[domain (or/c false/c valid-domain?)]
411-
[path (or/c false/c string?)])]{
470+
([name (and/c string? cookie-name?)]
471+
[value (and/c string? cookie-value?)]
472+
[domain (or/c #f domain-value?)]
473+
[path (or/c #f path/extension-value?)])]{
412474

413475
While server cookies are represented with @racket[cookie?]s, cookies
414476
that come from the client are represented with a

web-server-lib/info.rkt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
(define deps '("srfi-lite-lib"
66
("base" #:version "6.2.900.15")
77
"net-lib"
8+
"net-cookies"
89
"compatibility-lib"
910
"scribble-text-lib"
1011
"parser-tools-lib"))
Lines changed: 30 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,41 @@
11
#lang racket/base
2-
(require racket/port
2+
3+
(require web-server/http/request-structs
4+
net/cookies/common
5+
net/cookies/server
6+
web-server/private/util
37
racket/match
4-
web-server/http/request-structs
5-
net/cookie
6-
web-server/private/util
78
racket/contract)
89

10+
(provide (contract-out
11+
[struct client-cookie
12+
([name (and/c string? cookie-name?)]
13+
[value (and/c string? cookie-value?)]
14+
[domain (or/c #f domain-value?)]
15+
[path (or/c #f path/extension-value?)])]
16+
[request-cookies (-> request?
17+
(listof client-cookie?))]
18+
))
19+
920
(define-struct client-cookie
1021
(name value domain path)
1122
#:prefab)
1223

13-
(provide/contract
14-
[struct client-cookie
15-
([name string?]
16-
[value string?]
17-
[domain (or/c false/c valid-domain?)]
18-
[path (or/c false/c string?)])]
19-
[request-cookies (request? . -> . (listof client-cookie?))])
20-
21-
;; ============================================================
22-
;; utilities for retrieving cookies
23-
24-
(require parser-tools/lex
25-
parser-tools/yacc
26-
(prefix-in : parser-tools/lex-sre))
27-
28-
#|
29-
cookie = "Cookie:" cookie-version
30-
1*((";" | ",") cookie-value)
31-
cookie-value = NAME "=" VALUE [";" path] [";" domain]
32-
cookie-version = "$Version" "=" value
33-
NAME = attr
34-
VALUE = value
35-
path = "$Path" "=" value
36-
domain = "$Domain" "=" value
37-
38-
value = token | quoted-string
39-
40-
token = 1*<any CHAR except CTLs or tspecials>
41-
42-
quoted-string = ( <"> *(qdtext) <"> )
43-
qdtext = <any TEXT except <">>
44-
|#
45-
(define-lex-abbrevs
46-
(illegal (char-set "()<>@:/[]?{}"))
47-
(tspecial (:or (char-set "()<>@,;\\\"/[]?={}") whitespace #\tab))
48-
(token-char (:- any-char tspecial iso-control)))
49-
50-
(define-tokens regular (TOKEN QUOTED-STRING ILLEGAL))
51-
(define-empty-tokens keywords (EQUALS SEMI COMMA PATH DOMAIN VERSION EOF))
52-
53-
(define lex-cookie
54-
(lexer-src-pos
55-
[(eof) (token-EOF)]
56-
[whitespace (return-without-pos (lex-cookie input-port))]
57-
["=" (token-EQUALS)]
58-
[";" (token-SEMI)]
59-
["," (token-COMMA)]
60-
[(:+ illegal) (token-ILLEGAL lexeme)]
61-
["$Path" (token-PATH)]
62-
["$Domain" (token-DOMAIN)]
63-
["$Version" (token-VERSION)]
64-
[(:: #\" (:* (:or (:~ #\") "\\\"")) #\")
65-
(token-QUOTED-STRING (substring lexeme 1 (- (string-length lexeme) 1)))]
66-
[(:+ token-char) (token-TOKEN lexeme)]))
67-
68-
(define current-source-name (make-parameter #f))
69-
70-
(define (make-srcloc start-pos end-pos)
71-
(list (current-source-name)
72-
(position-line start-pos)
73-
(position-col start-pos)
74-
(position-offset start-pos)
75-
(- (position-offset end-pos) (position-offset start-pos))))
76-
77-
(define parse-raw-cookies
78-
(parser (src-pos)
79-
(start items)
80-
(tokens regular keywords)
81-
(grammar (items [(item separator items) (cons $1 $3)]
82-
[(item) (list $1)])
83-
(separator [(COMMA) #t]
84-
[(SEMI) #t])
85-
(item [(lhs EQUALS rhs) (cons $1 $3)]
86-
; This is not part of the spec. It is illegal
87-
[(lhs EQUALS) (cons $1 "")])
88-
(lhs [(VERSION) "$Version"]
89-
[(DOMAIN) 'domain]
90-
[(PATH) 'path]
91-
[(TOKEN) $1])
92-
(rhs [(TOKEN) $1] ; This is legal, but is subsumed by the illegal rule
93-
[(QUOTED-STRING) (regexp-replace* (regexp-quote "\\\"") $1 "\"")]
94-
; This is not part of the spec. It is illegal
95-
[(illegal) $1])
96-
(illegal
97-
[(EQUALS) "="]
98-
[(ILLEGAL) $1]
99-
[(illegal illegal) (string-append $1 $2)]
100-
[(TOKEN) $1]))
101-
(suppress) ; The illegal rule creates many conflicts
102-
(end EOF)
103-
(error (lambda (tok-ok? tok-name tok-value start-pos end-pos)
104-
(raise-syntax-error
105-
'parse-cookies
106-
(format
107-
(if tok-ok?
108-
"Did not expect token ~a"
109-
"Invalid token ~a")
110-
tok-name)
111-
(datum->syntax #f tok-value (make-srcloc start-pos end-pos)))))))
112-
113-
(define (parse-cookie-likes ip)
114-
(parse-raw-cookies (λ () (lex-cookie ip))))
115-
116-
(define (parse-cookies str)
117-
(with-input-from-string
118-
str
119-
(λ ()
120-
(define ip (current-input-port))
121-
(port-count-lines! ip)
122-
(parameterize ([current-source-name (object-name ip)])
123-
(raw->cookies (parse-cookie-likes ip))))))
124-
125-
;; raw->cookies : flat-property-list -> (listof cookie)
126-
(define raw->cookies
24+
(define handle-quoted-value
12725
(match-lambda
128-
[(list-rest (cons (? string? key) val) l)
129-
(let loop ([l l] [c (make-client-cookie key val #f #f)])
130-
(match l
131-
[(list)
132-
(list c)]
133-
[(list-rest (cons (? string? key) val) l)
134-
(list* c (loop l (make-client-cookie key val #f #f)))]
135-
[(list-rest (cons 'domain val) l)
136-
(loop l (struct-copy client-cookie c [domain val]))]
137-
[(list-rest (cons 'path val) l)
138-
(loop l (struct-copy client-cookie c [path val]))]))]))
26+
[(regexp #rx"^\"(.*)\"$" (list _ inner))
27+
inner]
28+
[val val]))
13929

140-
;; request-cookies* : request -> (listof cookie)
14130
(define (request-cookies req)
142-
(define hdrs (request-headers/raw req))
143-
(apply append
144-
(map (compose parse-cookies bytes->string/utf-8 header-value)
145-
(filter (lambda (h)
146-
(bytes-ci=? #"Cookie" (header-field h)))
147-
hdrs))))
31+
(for/fold ([cookies-so-far null])
32+
([this-header (in-list (request-headers/raw req))]
33+
#:when (bytes-ci=? #"Cookie"
34+
(header-field this-header)))
35+
(append cookies-so-far
36+
(for/list ([pr (in-list (cookie-header->alist
37+
(header-value this-header)))])
38+
(client-cookie (bytes->string/utf-8 (car pr))
39+
(handle-quoted-value (bytes->string/utf-8 (cdr pr)))
40+
#f
41+
#f)))))

0 commit comments

Comments
 (0)