diff --git a/collects/net/cookie-sig.ss b/collects/net/cookie-sig.ss new file mode 100644 index 0000000..ef216c6 --- /dev/null +++ b/collects/net/cookie-sig.ss @@ -0,0 +1,19 @@ +(module cookie-sig mzscheme + (require (lib "unitsig.ss")) + (provide net:cookie^) + + (define-signature net:cookie^ + (set-cookie + cookie:add-comment + cookie:add-domain + cookie:add-max-age + cookie:add-path + cookie:secure + cookie:version + ;; To actually return a cookie (string formated as a cookie): + print-cookie + ;; To parse the Cookies header: + get-cookie + get-cookie/single + ;; exceptions + (struct cookie-error ())))) diff --git a/collects/net/cookie-unit.ss b/collects/net/cookie-unit.ss new file mode 100644 index 0000000..ca88967 --- /dev/null +++ b/collects/net/cookie-unit.ss @@ -0,0 +1,297 @@ +;;; +;;; ---- HTTP cookies library +;;; Time-stamp: <03/04/25 10:50:05 noel> +;;; +;;; Copyright (C) 2002 by Francisco Solsona. +;;; +;;; This file is part of net. + +;;; net 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 2.1 of the License, or (at your option) any later version. + +;;; net 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 net; if not, write to the Free Software +;;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +;;; Author: Francisco Solsona +;; +;; +;; Commentary: +;; +;; The PLT Web server offers functionality to keep state beyond the +;; capacity of HTTP cookies. Believe or not, sometimes you may want +;; to keep stuff on the client end, like the theme the (anonymous) +;; user likes, a session id, etc., so I wrote this wanna be library. +;; +;; It uses some of the surfies you see on the require section below. +;; It doesn't actually "send" the Set-Cookie header to the browser, it +;; doesn't even add the string "Set-Cookie: " to the will-be cookie it +;; generates. Why? well because you may want to use this code with +;; very different Web browsers, and the way to send them may vary +;; drastically. For instance, if you are writing a CGI to be executed +;; from a non-cooperative (from the PLT Scheme point of view) web +;; server, like Apache, then you will `display' de cookie right to the +;; standard output. If you use FastCGI, you will `display' it trhough +;; the `output' communication stream between your CGI, and the Web +;; browser, but if you are using PLT Scheme Web browser, you will use +;; `make-response/full', or similar, chances are you will add cookies +;; to the `extras' parameter (extra headers), as cons pairs... as in +;; `(("Set-Cookie" . "cookie=\"as_returned_by_this_library";etc"))'. +;; +;; You should think of this procedures as a `format' for cookies. + +(module cookie-unit mzscheme + (require (lib "unitsig.ss") + (lib "etc.ss") + (lib "list.ss") + (lib "string.ss" "srfi" "13") + (lib "char-set.ss" "srfi" "14") + "cookie-sig.ss") + + (provide cookie@) + + + (define cookie@ + (unit/sig net:cookie^ + (import) + + (define-struct cookie (name value comment domain max-age path secure version)) + (define-struct (cookie-error exn) ()) + + ;; The syntax for the Set-Cookie response header is + ;; set-cookie = "Set-Cookie:" cookies + ;; cookies = 1#cookie + ;; cookie = NAME "=" VALUE *(";" cookie-av) + ;; NAME = attr + ;; VALUE = value + ;; cookie-av = "Comment" "=" value + ;; | "Domain" "=" value + ;; | "Max-Age" "=" value + ;; | "Path" "=" value + ;; | "Secure" + ;; | "Version" "=" 1*DIGIT + (define set-cookie + (lambda (name value) + (unless (and (cookie-string? name #f) + (cookie-string? value)) + (raise (make-cookie-error (format "Invalid NAME/VALUE pair: ~a / ~a" name value) (current-continuation-marks)))) + (make-cookie name value + #f;; comment + #f;; current domain + #f;; at the end of session + #f;; current path + #f;; normal (non SSL) + #f;; default version + ))) + + ;;! + ;; + ;; (function (print-cookie cookie)) + ;; + ;; (param cookie Cookie-structure "The cookie to return as a string") + ;; + ;; Formats the cookie contents in a string ready to be appended to a + ;; "Set-Cookie: " header, and sent to a client (browser). + (define print-cookie + (lambda (cookie) + (unless (cookie? cookie) + (raise (make-cookie-error (format "Cookie expected, received: ~a" cookie) (current-continuation-marks)))) + (string-join + (filter (lambda (s) + (not (string-null? s))) + (list (format "~a=~a" (cookie-name cookie) (cookie-value cookie)) + (let ((c (cookie-comment cookie))) (if c (format "Comment=~a" c) "")) + (let ((d (cookie-domain cookie))) (if d (format "Domain=~a" d) "")) + (let ((age (cookie-max-age cookie))) (if age (format "Max-Age=~a" age) "")) + (let ((p (cookie-path cookie))) (if p (format "Path=~a" p) "")) + (let ((s (cookie-secure cookie))) (if s "Secure" "")) + (let ((v (cookie-version cookie))) (format "Version=~a" (if v v 1))))) + "; "))) + + (define cookie:add-comment + (lambda (cookie comment) + (unless (cookie-string? comment) + (raise (make-cookie-error (format "Invalid comment: ~a" comment) (current-continuation-marks)))) + (unless (cookie? cookie) + (raise (make-cookie-error (format "Cookie expected, received: ~a" cookie) (current-continuation-marks)))) + (set-cookie-comment! cookie comment) + cookie)) + + (define cookie:add-domain + (lambda (cookie domain) + (unless (valid-domain? domain) + (raise (make-cookie-error (format "Invalid domain: ~a" domain) (current-continuation-marks)))) + (unless (cookie? cookie) + (raise (make-cookie-error (format "Cookie expected, received: ~a" cookie) (current-continuation-marks)))) + (set-cookie-domain! cookie domain) + cookie)) + + (define cookie:add-max-age + (lambda (cookie seconds) + (unless (and (integer? seconds) (not (negative? seconds))) + (raise (make-cookie-error (format "Invalid Max-Age for cookie: ~a" seconds) (current-continuation-marks)))) + (unless (cookie? cookie) + (raise (make-cookie-error (format "Cookie expected, received: ~a" cookie) (current-continuation-marks)))) + (set-cookie-max-age! cookie seconds) + cookie)) + + (define cookie:add-path + (lambda (cookie path) + (unless (string? path) + (raise (make-cookie-error (format "Invalid path: ~a" path) (current-continuation-marks)))) + (unless (cookie? cookie) + (raise (make-cookie-error (format "Cookie expected, received: ~a" cookie) (current-continuation-marks)))) + (set-cookie-path! cookie path) + cookie)) + + (define cookie:secure + (lambda (cookie secure?) + (unless (boolean? secure?) + (raise (make-cookie-error (format "Invalid argument (boolean expected), received: ~a" secure?) (current-continuation-marks)))) + (unless (cookie? cookie) + (raise (make-cookie-error (format "Cookie expected, received: ~a" cookie) (current-continuation-marks)))) + (set-cookie-secure! cookie secure?) + cookie)) + + (define cookie:version + (lambda (cookie version) + (unless (integer? version) + (raise (make-cookie-error (format "Unsupported version: ~a" version) (current-continuation-marks)))) + (unless (cookie? cookie) + (raise (make-cookie-error (format "Cookie expected, received: ~a" cookie) (current-continuation-marks)))) + (set-cookie-version! cookie version) + cookie)) + + + ;; Parsing the Cookie header: + + + (define char-set:all-but= + (char-set-difference char-set:full (string->char-set "="))) + + (define char-set:all-but-semicolon + (char-set-difference char-set:full (string->char-set ";"))) + + ;;! + ;; + ;; (function (get-all-results name cookies)) + ;; + ;; Auxiliar procedure that returns all values associated with + ;; `name' in the association list (cookies). + (define get-all-results + (lambda (name cookies) + (let loop ((c cookies)) + (cond ((null? c) ()) + (else + (let ((pair (car c))) + (if (string=? name (car pair)) + ;; found an instance of cookie named `name' + (cons (cadr pair) (loop (cdr c))) + (loop (cdr c))))))))) + + ;; which tipically looks like: (cookie . "test5=\"5\"; test1=\"1\"; test0=\"0\"; test1=\"20\"") + ;; note that it can be multi-valued: `test1' has values: "1", and "20". + ;; Of course, in the same spirit, we only receive the "string content". + (define get-cookie + (lambda (name cookies) + (let ((cookies (map (lambda (p) + (map string-trim-both + (string-tokenize p char-set:all-but=))) + (string-tokenize cookies char-set:all-but-semicolon)))) + (get-all-results name cookies)))) + + ;;! + ;; + ;; (function (get-cookie/single name cookies)) + ;; + ;; (param name String "The name of the cookie we are looking for") + ;; (param cookies String "The string (from the environment) with the content of the cookie header.") + ;; + ;; Returns the first name associated with the cookie named `name', if any, or #f. + (define get-cookie/single + (lambda (name cookies) + (let ((cookies (get-cookie name cookies))) + (and (not (null? cookies)) + (car cookies))))) + + + + ;;;;; + ;; Auxiliar procedures + ;;;;; + + + ;; token = 1* + ;; + ;; tspecials = "(" | ")" | "<" | ">" | "@" + ;; | "," | ";" | ":" | "\" | <"> + ;; | "/" | "[" | "]" | "?" | "=" + ;; | "{" | "}" | SP | HT + (define char-set:tspecials + (char-set-union + (char-set-difference char-set:punctuation (string->char-set "_")) + char-set:whitespace)) + (define char-set:control (char-set-union char-set:iso-control + (char-set (latin-1-integer->char 127))));; DEL + (define char-set:token (char-set-difference char-set:ascii char-set:tspecials char-set:control)) + + ;;! + ;; + ;; (function (quoted-string? s)) + ;; + ;; (param s String "The string to check") + ;; + ;; Returns #t only if the string is surrounded by double quotes. As in: + ;; quoted-string = ( <"> *(qdtext) <"> ) + ;; qdtext = > + (define quoted-string? + (lambda (s) + (and (string=? (string-take s 1) "\"") + (string=? (string-take-right s 1) "\"")))) + + ;;! + ;; + ;; (function (cookie-string? s)) + ;; + ;; (param s String "String to check") + ;; + ;; Returns whether this is a valid string to use as the value or the + ;; name (depending on value?) of an HTTP cookie. + (define cookie-string? + (opt-lambda (s (value? #t)) + (unless (string? s) + (raise (make-cookie-error (format "String expected, received: ~a" s) (current-continuation-marks)))) + (if value? + ;; value: token | quoted-string + (or (string-every char-set:token s) + (quoted-string? s)) + ;; name: token + (string-every char-set:token s)))) + + ;; Host names as per RFC 1123 and RFC952, more or less, anyway. :-) + (define char-set:hostname + (let ((a-z-lowercase (ucs-range->char-set #x61 #x7B)) + (a-z-uppercase (ucs-range->char-set #x41 #x5B))) + (char-set-adjoin! + (char-set-union char-set:digit a-z-lowercase a-z-uppercase) + #\. ))) + + (define valid-domain? + (lambda (dom) + (and + ;; Domain must start with a dot (.) + (string=? (string-take dom 1) ".") + ;; The rest are tokens-like strings separated by dots + (string-every char-set:hostname dom) + (<= (string-length dom) 76)))) + )) + ) + +;;; cookie-unit.ss ends here \ No newline at end of file diff --git a/collects/net/uri-codec-sig.ss b/collects/net/uri-codec-sig.ss new file mode 100644 index 0000000..338ac93 --- /dev/null +++ b/collects/net/uri-codec-sig.ss @@ -0,0 +1,11 @@ +(module uri-codec-sig mzscheme + (require (lib "unitsig.ss")) + (provide net:uri-codec^) + + (define-signature net:uri-codec^ + (uri-encode + uri-decode + form-urlencoded-encode + form-urlencoded-decode + alist->form-urlencoded + form-urlencoded->alist))) \ No newline at end of file diff --git a/collects/net/uri-codec-unit.ss b/collects/net/uri-codec-unit.ss new file mode 100644 index 0000000..948a99c --- /dev/null +++ b/collects/net/uri-codec-unit.ss @@ -0,0 +1,260 @@ +;;; +;;; ---- En/Decode URLs and form-urlencoded data +;;; Time-stamp: <03/04/25 10:31:31 noel> +;;; +;;; Copyright (C) 2002 by Noel Welsh. +;;; +;;; This file is part of Net. + +;;; Net 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 2.1 of the License, or (at your option) any later version. + +;;; Net 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 Net; if not, write to the Free Software +;;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +;;; Author: Noel Welsh +;; +;; +;; Commentary: + +;; The module provides functions to encode and decode strings using +;; the URI encoding rules given in RFC 2396, and to encode and decode +;; name/value pairs using the application/x-www-form-urlencoded +;; mimetype given the in HTML 4.0 specification. There are minor +;; differences between the two encodings. + +;; The URI encoding uses allows a few characters to be represented `as +;; is': a-Z, A-Z, 0-9, -, _, ., !, ~, *, ', ( and ). The remaining +;; characters are encoded as %xx, where xx is the hex representation +;; of the integer value of the character (where the mapping +;; character<->integer is determined by US-ASCII if the integer is +;; <128). + +;; The encoding, inline with RFC 2396's recommendation, represents a +;; character as is, if possible. The decoding allows any characters +;; to be represented by their hex values, and allows characters to be +;; incorrectly represented `as is'. + +;; The rules for the application/x-www-form-urlencoded mimetype given +;; in the HTML 4.0 spec are: + +;; 1. Control names and values are escaped. Space characters are +;; replaced by `+', and then reserved characters are escaped as +;; described in [RFC1738], section 2.2: Non-alphanumeric characters +;; are replaced by `%HH', a percent sign and two hexadecimal digits +;; representing the ASCII code of the character. Line breaks are +;; represented as "CR LF" pairs (i.e., `%0D%0A'). + +;; 2. The control names/values are listed in the order they appear +;; in the document. The name is separated from the value by `=' and +;; name/value pairs are separated from each other by `&'. + +;; NB: RFC 2396 supersedes RFC 1738. + +;; This differs slightly from the straight encoding in RFC 2396 in +;; that `+' is allowed, and represents a space. We follow this +;; convention, encoding a space as `+' and decoding `+' as a space. +;; There appear to be some brain-dead decoders on the web, so we also +;; encode `!', `~', `'', `(' and ')' using their hex representation. +;; This is the same choice as made by the Java URLEncoder. + +;; Draws inspiration from encode-decode.scm by Kurt Normark and a code +;; sample provided by Eli Barzilay + +(module uri-codec-unit mzscheme + + (require (lib "unitsig.ss") + (lib "match.ss") + "uri-codec-sig.ss") + + (provide uri-codec@) + + ;; Macro to loop over integers from n (inclusive) to m (exclusive) + ;; Extracted from iteration.ss in macro package at Schematics + (define-syntax for + (syntax-rules () + ((for (i n m) forms ...) + (let ((fixed-m m)) + (let loop ((i n)) + (if (< i fixed-m) + (begin + forms ... + (loop (+ i 1))))))))) + + (define uri-codec@ + (unit/sig net:uri-codec^ + (import) + + ;; The characters that always map to themselves + (define alphanumeric-mapping + (map (lambda (char) + (cons char char)) + '(#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9 + #\A #\B #\C #\D #\E #\F #\G #\H #\I #\J + #\K #\L #\M #\N #\O #\P #\Q #\R #\S #\T + #\U #\V #\W #\X #\Y #\Z #\a #\b #\c #\d + #\e #\f #\g #\h #\i #\j #\k #\l #\m #\n + #\o #\p #\q #\r #\s #\t #\u #\v #\w #\x + #\y #\z))) + + ;; Characters that sometimes map to themselves + (define safe-mapping + (map (lambda (char) + (cons char char)) + '(#\- #\_ #\. #\! #\~ #\* #\' #\( #\)))) + + ;; The strict URI mapping + (define uri-mapping + (append alphanumeric-mapping safe-mapping)) + + ;; The form-urlencoded mapping + (define form-urlencoded-mapping + (append '((#\. . #\.) + (#\- . #\-) + (#\* . #\*) + (#\_ . #\_) + (#\space . #\+)) + alphanumeric-mapping)) + + (define (number->hex-string number) + (let ((hex (number->string number 16))) + (string-append "%" + (if (= (string-length hex) 1) + (string-append "0" hex) + hex)))) + + (define (hex-string->number hex-string) + (string->number (substring hex-string 1 3) 16)) + + ;; (listof (cons char char)) -> (values (vectorof string) (vectorof string)) + (define (make-codec-tables alist) + (let ((encoding-table (make-vector 256 "")) + (decoding-table (make-vector 256 ""))) + (for (i 0 256) + (vector-set! encoding-table i (number->hex-string i)) + (vector-set! decoding-table i (string (integer->char i)))) + (for-each (match-lambda + [(orig . enc) + (vector-set! encoding-table + (char->integer orig) + (string enc)) + (vector-set! decoding-table + (char->integer enc) + (string orig))]) + alist) + (values encoding-table decoding-table))) + + (define-values (uri-encoding-vector uri-decoding-vector) + (make-codec-tables uri-mapping)) + + (define-values (form-urlencoded-encoding-vector + form-urlencoded-decoding-vector) + (make-codec-tables form-urlencoded-mapping)) + + ;; vector string -> string + (define (encode table str) + (apply string-append + (map (lambda (char) + (vector-ref table (char->integer char))) + (string->list str)))) + + ;; vector string -> string + (define (decode table str) + (define internal-decode + (match-lambda + [() (list)] + [(#\% char1 char2 . rest) + (cons + (vector-ref table + (string->number (string char1 char2) 16)) + (internal-decode rest))] + [(char . rest) + (cons + (vector-ref table + (char->integer char)) + (internal-decode rest))])) + (apply string-append (internal-decode (string->list str)))) + + ;; string -> string + (define (uri-encode str) + (encode uri-encoding-vector str)) + + ;; string -> string + (define (uri-decode str) + (decode uri-decoding-vector str)) + + ;; string -> string + (define (form-urlencoded-encode str) + (encode form-urlencoded-encoding-vector str)) + + ;; string -> string + (define (form-urlencoded-decode str) + (decode form-urlencoded-decoding-vector str)) + + ;; listof (cons string string) -> string + (define alist->form-urlencoded + (match-lambda + [() ""] + [((name . value)) + (string-append (form-urlencoded-encode name) + "=" + (form-urlencoded-encode value))] + [((name . value) . rest) + (string-append (form-urlencoded-encode name) + "=" + (form-urlencoded-encode value) + "&" + (alist->form-urlencoded rest))])) + + ;; string -> listof (cons string string) + (define (form-urlencoded->alist str) + (define key-regexp (regexp "[^=]*")) + (define value-regexp (regexp "[^&]*")) + (define (next-key str start) + (if (>= start (string-length str)) + #f + (match (regexp-match-positions key-regexp str start) + [((start . end)) + (vector (form-urlencoded-decode (substring str start end)) + (add1 end))] + [#f #f]))) + (define (next-value str start) + (if (>= start (string-length str)) + #f + (match (regexp-match-positions value-regexp str start) + [((start . end)) + (vector (form-urlencoded-decode (substring str start end)) + (add1 end))] + [#f #f]))) + (define (next-pair str start) + (match (next-key str start) + [#(key start) + (match (next-value str start) + [#(value start) + (vector (cons key value) start)] + [#f + (vector (cons key "") (string-length str))])] + [#f #f])) + (let loop ((start 0) + (end (string-length str)) + (alist '())) + (cond + [(>= start end) alist] + [else + (match (next-pair str start) + [#(pair next-start) + (loop next-start end (cons pair alist))] + [#f alist])]))) + + )) + ) + +;;; uri-codec-unit.ss ends here \ No newline at end of file