1 ;;; http-cookies.el --- simple HTTP cookies implementation
3 ;; Copyright (C) 2004, David Hansen
5 ;; Author: David Hansen <david.hansen@physik.fu-berlin.de>
6 ;; Maintainer: David Hansen <david.hansen@physik.fu-berlin.de>
8 ;; Keywords: hypermedia
10 ;; This file is not part of GNU Emacs.
12 ;; This is free software; you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation; either version 2, or (at your option)
17 ;; This is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with GNU Emacs; see the file COPYING. If not, write to the
24 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
25 ;; Boston, MA 02111-1307, USA.
30 ;; Implementation of old netscape cookies (used by maybe all servers) and
33 ;; See http://www.faqs.org/rfcs/rfc2109.html and
34 ;; http://wp.netscape.com/newsref/std/cookie_spec.html
42 ;; - reading from file, saving to file
49 (defconst http-cookies-version
"1.0.0")
51 (defgroup http-emacs
()
52 "Simple HTTP client implementation in elisp.")
54 (defcustom http-emacs-use-cookies nil
55 "Use cookies in the http-emacs package. *EXPERIMENTAL*"
59 (defcustom http-emacs-cookie-file
"~/.emacs-cookies"
60 "*File where to store the cookies."
64 (defconst http-token-value-regexp
65 "^[ \t]*\\(.*?\\)[ \t]*=[ \t]*\"?\\(.*?\\)\"?[ \t]*;?[ \t]*$"
66 "Regexp to match a token=\"value\"; in a cookie.")
68 (defvar http-cookies-accept-functions
69 '(http-cookie-check-path
70 http-cookie-check-domain
71 http-cookie-check-hostname
)
72 "*List of functions used to determine if we accept a cookie or not.
73 If one of these function returns nil the cookie will be rejected. Each
74 function can access the free variables `cookie', `host' (from the url)
75 `path' (from the URL) and `url' to make its decision.")
77 (defvar http-cookies-host-hash
78 (make-hash-table :test
'equal
)
79 "Hash to look up cookies by host name.")
81 (defvar http-cookies-domain-hash
82 (make-hash-table :test
'equal
)
83 "Hash to look up cookies by domain.")
87 ;; functions for parsing the header
89 (defun http-cookies-ns-to-rfc (line)
90 "Make the header value LINE a bit more RFC compatible.
91 Make old netscape cookies a bit more RFC 2109 compatible by quoting
92 the \"expires\" value. We need this to be able to properly split
93 the header value if there is more than one cookie."
95 (while (string-match "expires[ \t]*=[ \t]*\\([^\";]+?\\)\\(;\\|$\\)"
97 (setq start
(match-end 0))
98 (setq line
(replace-match "\"\\1\"" t nil line
1)))
101 (defun http-cookies-find-char-in-string (char string
&optional start
)
102 "Return the first position of CHAR in STRING.
103 If START is non-nil start at position START."
106 (let ((i start
) (len (length string
)) pos
)
107 (while (and (not pos
) (< i len
))
108 (when (= (aref string i
) char
)
113 (defun http-cookies-find-quoted-strings (header-value)
114 "Return list of positions of quoted strings in HEADER_VALUE.
115 Return a list of pairs with the beginning and end of quoted strings
116 in a \"Set-cookie: \" header value."
117 (let ((start 0) qstring-pos
)
118 (while (string-match "=[ \t]*\\(\".*?[^\\]\"\\)" header-value start
)
119 (add-to-list 'qstring-pos
(cons (match-beginning 1) (1- (match-end 1))))
120 (setq start
(match-end 1)))
123 (defun http-cookies-split-string (header-value sep-char
)
124 "Split the HEADER-VALUE at the character SEP-CHAR.
125 Ignores SEP-CHAR if it is in a quoted string. Return a list of the
127 (let ((qstrings (http-cookies-find-quoted-strings header-value
))
128 (start 0) (beg 0) pos in-qstring strings
)
129 (while (setq pos
(http-cookies-find-char-in-string
130 sep-char header-value start
))
131 (unless (= pos start
) ; ignore empty strings
132 ;; check if pos is in a quoted string
133 (dolist (qstring-pos qstrings
)
135 (when (and (> pos
(car qstring-pos
)) (< pos
(cdr qstring-pos
)))
136 (setq in-qstring t
))))
138 (setq in-qstring nil
)
139 (add-to-list 'strings
(substring header-value beg pos
))
140 (setq beg
(1+ pos
))))
141 (setq start
(1+ pos
)))
142 ;; add the last token
143 (add-to-list 'strings
(substring header-value beg
))
146 (defun http-cookies-parse-cookie (string)
148 Return an alist ((NAME . VALUE) (attr1 . value1) (attr2 . value2) ...)
151 (dolist (attr (http-cookies-split-string string ?\
;))
152 (if (string-match http-token-value-regexp attr
)
153 (add-to-list 'attrs
(cons (match-string 1 attr
)
154 (match-string 2 attr
)))
155 ;; match the secure attribute
156 (if (string-match "[ \t]*\\([a-zA-Z]+\\)[ \t]*" attr
)
157 (add-to-list 'attrs
(cons (match-string 1 attr
) t
))
159 (message "Cannot parse cookie %s" string
))))
163 (defun http-cookies-set (url headers
)
164 "Set the cookies from the response to a request of URL.
165 Set HEADERS to the headers of the response."
166 (let ((host (http-cookies-url-host url
)) (path (http-cookies-url-path url
))
168 ;; The server may send several "Set-Cookie:" headers.
169 (dolist (line headers
)
170 (when (equal (car line
) "set-cookie")
171 (setq header-value
(http-cookies-ns-to-rfc (cdr line
)))
172 ;; there may be several cookies separated by ","
173 (dolist (raw-cookie (http-cookies-split-string header-value ?\
,))
174 (setq cookie
(http-cookies-parse-cookie raw-cookie
))
175 ;; (message "%s" raw-cookie)
176 (when (http-cookies-accept)
177 ;; (message "accepted")
178 (http-cookies-store host cookie
)))))))
184 (defun http-cookies-name (cookie)
185 "Return the name of the COOKIE."
188 (defun http-cookies-path (cookie)
189 "Return the value of the path attribute of the COOKIE."
190 (let ((attr (or (assoc "path" cookie
) (assoc "Path" cookie
))))
194 (defun http-cookies-domain (cookie)
195 "Return the value of the domain attribute of the COOKIE."
196 (let ((attr (or (assoc "domain" cookie
) (assoc "Domain" cookie
))))
200 (defun http-cookies-expires (cookie)
201 "Return the value of the expires attribute of the COOKIE."
202 (let ((attr (assoc "expires" cookie
)))
206 (defun http-cookies-max-age (cookie)
207 "Return the value of the Max-Age attribute of the COOKIE."
208 (let ((attr (assoc "Max-Age" cookie
)))
212 (defun http-cookies-version (cookie)
213 "Return the value of the version attribute of the COOKIE."
214 (let ((version (assoc "Version" cookie
)))
216 (if (equal version
"1")
218 (message "Cookie version %s not supported." version
)
221 (defun http-cookies-equal (c1 c2
)
222 "Return non nil if the given cookies are equal.
223 Old netscape cookies are equal if the name and path attributes are equal.
224 Version 1 cookies are equal if name path and domain are equal."
225 (if (and (http-cookies-version c1
) (http-cookies-version c2
))
227 (and (equal (http-cookies-name c1
) (http-cookies-name c2
))
228 (equal (http-cookies-path c1
) (http-cookies-path c2
))
229 (equal (http-cookies-domain c1
) (http-cookies-domain c2
)))
231 (and (equal (http-cookies-name c1
) (http-cookies-name c2
))
232 (equal (http-cookies-path c1
) (http-cookies-path c2
)))))
234 (defun http-cookies-expired (expire-string)
235 "Return non nil if EXPIRE-STRING is in the past."
236 (> (time-to-seconds (time-since expire-string
)) 0.0))
238 (defun http-cookies-remove (cookie key table
)
239 "Remove cookies \"equal\" to COOKIE from the list stored with KEY in TABLE."
240 (let ((cookie-list (gethash key table
)) new-list
)
241 (dolist (entry cookie-list
)
242 (unless (http-cookies-equal entry cookie
)
243 (add-to-list 'new-list entry
)))
246 (puthash key new-list table
))))
248 (defun http-cookies-store (host cookie
)
249 "Store the given COOKIE from HOST in the hash tables.
250 Remove cookie from the tables if the given COOKIE expires in the past or
251 has an \"Max-Age\" of 0."
252 (let ((domain (http-cookies-domain cookie
))
253 (max-age (http-cookies-max-age cookie
))
254 (expires (http-cookies-expires cookie
))
256 ;; remove an possible "equal" old cookie
257 (http-cookies-remove cookie host http-cookies-host-hash
)
259 (http-cookies-remove cookie domain http-cookies-domain-hash
))
260 ;; check if expires is in the past or Max-Age is zero
261 (unless (or (and max-age
(= (string-to-number max-age
) 0))
262 (and expires
(http-cookies-expired expires
)))
263 ;; convert "Max-Age" to "expire"
265 ;; this value does not have to be in the "right" format
266 ;; it's enough if `parse-time-string' can parse it
267 (setq expires
(format-time-string
269 (time-add (current-time) (seconds-to-time max-age
))
271 (setcdr (assoc "Max-Age" cookie
) expires
)
272 (setcar (assoc "Max-Age" cookie
) "expires"))
273 (setq cookie-list
(gethash host http-cookies-host-hash
))
274 (add-to-list 'cookie-list cookie
)
275 (puthash host cookie-list http-cookies-host-hash
)
277 (setq cookie-list
(gethash domain http-cookies-domain-hash
))
278 (add-to-list 'cookie-list cookie
)
279 (puthash domain cookie-list http-cookies-domain-hash
)))))
283 ;; building the header to send back the cookie
285 (defun http-cookies-cookie-to-string (cookie)
286 "Return the cookie as a string to be used as a header value."
287 (let* ((name (http-cookies-name cookie
))
288 (value (cdr (assoc name cookie
)))
289 (path (http-cookies-path cookie
))
290 (domain (http-cookies-domain cookie
))
292 (if (http-cookies-version cookie
)
295 (setq string
(concat "$Version = \"1\"; " name
" = \"" value
"\""))
297 (setq string
(concat string
"; $Path = \"" path
"\"")))
299 (setq string
(concat string
"; $Domain = \"" domain
"\""))))
301 (setq string
(concat name
"=" value
)))))
303 (defun http-cookies-cookie-in-list (cookie list
)
304 "Return non-nil if a cookie \"equal\" to the given COOKIE is in LIST."
306 (dolist (element list
)
308 (setq in-list
(http-cookies-equal cookie element
))))
311 (defun http-cookies-path-depth (cookie)
312 "Return the number of dashes in the path attribute of the cookie."
313 (let ((patch http-cookies-path cookie
) (n 0) (start 0))
314 (while (setq start
(http-cookies-find-char-in-string ?\
/ path start
))
318 (defun http-cookie-path-depth-less (c1 c2
)
319 "Return non nil if the path depth of cookie C1 is less than C2."
320 (< (http-cookies-path-depth c1
) (http-cookies-path-depth c2
)))
322 (defun http-cookies-build-header (url)
323 "Return a pair (\"Cookie\" . <header value>).
324 Use this to send back cookies to the given URL."
325 (let ((host (http-cookies-url-host url
)) (domain) (cookie-list) (string))
326 (when (string-match "^[^.]+\\(\\..+\\)" host
)
327 (setq domain
(match-string 1 host
))
328 (dolist (cookie (gethash host http-cookies-host-hash
))
329 (unless (http-cookies-expired (http-cookies-expires cookie
))
330 (add-to-list 'cookie-list cookie
)))
331 (dolist (cookie (gethash domain http-cookies-domain-hash
))
332 (unless (or (http-cookies-cookie-in-list cookie cookie-list
)
333 (http-cookies-expired (http-cookies-expires cookie
)))
334 (add-to-list 'cookie-list cookie
)))
335 (setq cookie-list
(sort cookie-list
'http-cookies-path-depth-less
))
336 (dolist (cookie cookie-list
)
338 (setq string
(concat string
"; "
339 (http-cookies-cookie-to-string cookie
)))
340 (setq string
(http-cookies-cookie-to-string cookie
)))))
341 (cons "Cookie" string
)))
345 ;; extract parts of the url
347 (defun http-cookies-url-host (url)
348 "Return the hostname of URL"
349 (unless (string-match
350 "http://\\([^/:]+\\)\\(:\\([0-9]+\\)\\)?/\\(.*/\\)?\\([^:]*\\)"
352 (error "Cannot parse URL %s." url
))
353 (match-string 1 url
))
355 (defun http-cookies-url-path (url)
356 "Return the path of the URL."
357 (unless (string-match
358 "http://\\([^/:]+\\)\\(:\\([0-9]+\\)\\)?/\\(.*/\\)?\\([^:]*\\)"
360 (error "Cannot parse URL %s." url
))
361 (concat "/" (or (match-string 4 url
) "")))
365 ;; functions to check the cookie (implementation of 4.3.2 of RFC 2109)
367 (defun http-cookies-accept ()
368 "Return non nil if the cookie should be accepted.
369 The tests are based on the functions in `http-cookies-accept-functions'."
371 (dolist (fun http-cookies-accept-functions
)
373 (setq accept
(funcall fun
))))
376 (defun http-cookie-check-path ()
377 "Return nil if the \"path\" attribute is not a prefix of th URL."
378 (let ((cookie-path (cdr (assoc "path" cookie
))))
380 (if (string-match (concat "^" cookie-path
) path
)
382 (message "Rejecting cookie: path attribute \"%s\" is not a prefix\
383 of the URL %s." cookie-path url
)
387 (defun http-cookie-check-domain ()
388 "Return nil if the domain is bogus.
389 Return nil if the domain does not start with a \".\" or does not contain
391 (let ((domain (cdr (assoc "domain" cookie
))))
393 (if (string-match "^\\.[^.]+\\.[^.]+" domain
)
395 (message "Rejection cookie: domain \"%s\" does not start with a dot\
396 or does not contain an embedded dot." domain
)
400 (defun http-cookie-check-hostname ()
401 "Return nil if the domain doesn't match the host.
402 Return nil if the domain attribute does not match the host name or the
403 host name without the domain attribute still contains one or more dots."
404 ;; FIXME: hostname might be an IP address
405 (let ((domain (cdr (assoc "domain" cookie
))))
408 (when (string-match (concat domain
"$") host
)
409 (not (http-cookies-find-char-in-string
410 ?\.
(substring host
0 (match-beginning 0))))))))
414 (provide 'http-cookies
)
416 ;;; http-cookies.el ends here