changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / lisp/lib/net/cookie.lisp

changeset 698: 96958d3eb5b0
parent: d1d64b856fae
author: Richard Westhaver <ellis@rwest.io>
date: Fri, 04 Oct 2024 22:04:59 -0400
permissions: -rw-r--r--
description: fixes
1 ;;; net/cookie.lisp --- HTTP Cookies
2 
3 ;; Based on Fukamachi's CL-COOKIE
4 
5 ;;; Code:
6 (in-package :net/cookie)
7 
8 (defun cookie-domain-p (domain cookie-domain)
9  (unless cookie-domain
10  (return-from cookie-domain-p t))
11  (if (ip-addr-p domain)
12  (ip-addr= domain cookie-domain)
13  (progn
14  ;; ignore the preceding "."
15  (when (char= (aref cookie-domain 0) #\.)
16  (setq cookie-domain (subseq cookie-domain 1)))
17  (when-let (registered-domain (parse-domain domain))
18  (cond
19  ((= (length registered-domain) (length cookie-domain))
20  (string= registered-domain cookie-domain))
21  ((= (length domain) (length cookie-domain))
22  (string= domain cookie-domain))
23  (t (and (std/seq:ends-with-subseq domain cookie-domain)
24  (char= #\.
25  (aref cookie-domain (- (length cookie-domain)
26  (length registered-domain)))))))))))
27 
28 (defun same-site-p (same-site)
29  "Predicate for allowed values of same-site attribute"
30  (member same-site (list "Strict" "Lax" "None") :test #'string-equal))
31 
32 (deftype same-site nil
33  '(satisfies same-site-p))
34 
35 (defstruct cookie
36  (name nil :type (or null string))
37  (value nil :type (or null string))
38  (path nil :type (or null string))
39  (domain nil :type (or null string))
40  (origin-host nil :type (or null string))
41  (expires nil :type (or null integer))
42  (max-age nil :type (or null integer))
43  (same-site nil :type (or null same-site))
44  (partitioned nil :type boolean)
45  (secure-p nil :type boolean)
46  (httponly-p nil :type boolean)
47  (creation-timestamp (get-universal-time) :type integer :read-only t))
48 
49 (defstruct cookie-jar
50  cookies)
51 
52 (defun cookie= (cookie1 cookie2)
53  "Equality check for the attributes name, domain, host and path."
54  (and (string= (cookie-name cookie1)
55  (cookie-name cookie2))
56  (if (cookie-domain cookie1)
57  (equalp (cookie-domain cookie1)
58  (cookie-domain cookie2))
59  (equalp (cookie-origin-host cookie1)
60  (cookie-origin-host cookie2)))
61  (equal (cookie-path cookie1)
62  (cookie-path cookie2))))
63 
64 (defun cookie-equal (cookie1 cookie2)
65  "Equality check as in cookie= plus also secure-p, same-site, partitioned, as well as httponly-p."
66  (and (cookie= cookie1 cookie2)
67  (eq (cookie-secure-p cookie1) (cookie-secure-p cookie2))
68  (string= (cookie-same-site cookie1)
69  (cookie-same-site cookie2))
70  (eq (cookie-partitioned cookie1)
71  (cookie-partitioned cookie2))
72  (eq (cookie-httponly-p cookie1) (cookie-httponly-p cookie2))))
73 
74 (defun expired-cookie-p (cookie)
75  "Check if cookie is expired, whereas max-age has priority over expires."
76  (if-let (max-age
77  (cookie-max-age cookie))
78  (< (+ max-age
79  (cookie-creation-timestamp cookie))
80  (get-universal-time))
81  (when-let (expires
82  (cookie-expires cookie))
83  (< expires (get-universal-time)))))
84 
85 (defun delete-old-cookies (cookie-jar)
86  (setf (cookie-jar-cookies cookie-jar)
87  (delete-if #'expired-cookie-p
88  (cookie-jar-cookies cookie-jar))))
89 
90 (defun match-cookie-path (request-path cookie-path)
91  (flet ((last-char (str)
92  (aref str (1- (length str)))))
93  (when (= 0 (length request-path))
94  (setf request-path "/"))
95  (when (= 0 (length cookie-path))
96  (setf cookie-path "/"))
97  (or (string= request-path cookie-path)
98  (and (starts-with-subseq cookie-path request-path)
99  (or (char= (last-char cookie-path) #\/)
100  (char= (aref request-path (length cookie-path)) #\/))))))
101 
102 (defun match-cookie (cookie host path &key securep)
103  "Get all available cookies for a specific host and path."
104  (and (if (cookie-secure-p cookie)
105  securep
106  t)
107  (match-cookie-path path (cookie-path cookie))
108  (if (cookie-domain cookie)
109  (cookie-domain-p host (cookie-domain cookie))
110  (equalp host (cookie-origin-host cookie)))))
111 
112 (defun cookie-jar-host-cookies (cookie-jar host path &key securep)
113  (delete-old-cookies cookie-jar)
114  (remove-if-not (lambda (cookie)
115  (match-cookie cookie host path :securep securep))
116  (cookie-jar-cookies cookie-jar)))
117 
118 (defun write-cookie-header (cookies &optional stream)
119  (labels ((write-cookie (cookie s)
120  (format s "~A=~A"
121  (cookie-name cookie)
122  (cookie-value cookie)))
123  (main (cookies stream)
124  (write-cookie (pop cookies) stream)
125  (dolist (cookie cookies)
126  (write-string "; " stream)
127  (write-cookie cookie stream))))
128  (when cookies
129  (if stream
130  (main (ensure-cons cookies) stream)
131  (with-output-to-string (s)
132  (main (ensure-cons cookies) s))))))
133 
134 (defparameter +set-cookie-date-format+
135  '(:short-weekday ", " (:day 2) #\space :short-month #\space (:year 4) #\space
136  (:hour 2) #\: (:min 2) #\: (:sec 2) #\space "GMT")
137  "The date format used in RFC 6265. For example: Wed, 09 Jun 2021 10:18:14 GMT.")
138 
139 (defun write-set-cookie-header (cookie &optional stream)
140  "Writes full header in conformance with RFC 6265 plus some additional attributes."
141  (labels ((format-cookie-date (universal-time s)
142  (when universal-time
143  (format-timestring s (universal-to-timestamp universal-time)
144  :format +set-cookie-date-format+ :timezone obj/time:+gmt-zone+))))
145  (format stream
146  "~A=~A~@[; Expires=~A~]~@[; Max-age=~A~]~@[; Path=~A~]~@[; Domain=~A~]~@[; SameSite=~A~]~:[~;; Partitioned~]~:[~;; Secure~]~:[~;; HttpOnly~]"
147  (cookie-name cookie)
148  (cookie-value cookie)
149  (format-cookie-date (cookie-expires cookie) stream)
150  (cookie-max-age cookie)
151  (cookie-path cookie)
152  (cookie-domain cookie)
153  (cookie-same-site cookie)
154  (cookie-partitioned cookie)
155  (cookie-secure-p cookie)
156  (cookie-httponly-p cookie))))
157 
158 (defun merge-cookies (cookie-jar cookies)
159  (setf (cookie-jar-cookies cookie-jar)
160  (delete-duplicates
161  (nconc (cookie-jar-cookies cookie-jar)
162  cookies)
163  :test #'cookie=)))
164 
165 (define-condition invalid-set-cookie (error)
166  ((header :initarg :header))
167  (:report (lambda (condition stream)
168  (format stream "Invalid Set-Cookie header: ~S"
169  (slot-value condition 'header)))))
170 
171 (define-condition invalid-expires-date (error)
172  ((expires :initarg :expires))
173  (:report (lambda (condition stream)
174  (format stream "Invalid expires date: ~S. Ignoring."
175  (slot-value condition 'expires)))))
176 
177 (defun integer-char-p (char)
178  (char<= #\0 char #\9))
179 
180 (defun get-tz-offset (tz-abbrev)
181  (symbol-macrolet ((timezones obj/time::*abbreviated-subzone-name->timezone-list*))
182  (let* ((tz (gethash tz-abbrev timezones nil))
183  (tz (if tz
184  (car tz)
185  (when (zerop (hash-table-count timezones))
186  (obj/time::reread-timezone-repository
187  :timezone-repository (asdf:system-relative-pathname :local-time #P"zoneinfo/"))
188  (first (gethash tz-abbrev timezones nil))))))
189  (when tz
190  (loop for sub across (obj/time::timezone-subzones tz)
191  when (equal tz-abbrev (obj/time::subzone-abbrev sub))
192  do (return (obj/time::subzone-offset sub)))))))
193 
194 (defparameter *current-century-offset*
195  (* (1- (timestamp-century (today)))
196  100))
197 
198 (defun parse-cookie-date (cookie-date)
199  (let (year month day hour min sec offset)
200  (handler-case
201  (with-vector-parsing (cookie-date)
202  (labels ((parse-month ()
203  (if (integer-char-p (current))
204  (parse-int)
205  (match-case
206  ("Jan" (match? "uary") 1)
207  ("Feb" (match? "ruary") 2)
208  ("Mar" (match? "ch") 3)
209  ("Apr" (match? "il") 4)
210  ("May" 5)
211  ("Jun" (match? "e") 6)
212  ("Jul" (match? "y") 7)
213  ("Aug" (match? "ust") 8)
214  ("Sep" (match? "tember") 9)
215  ("Oct" (match? "ober") 10)
216  ("Nov" (match? "ember") 11)
217  ("Dec" (match? "ember") 12))))
218  (parse-int ()
219  (bind (int (skip-while integer-char-p))
220  (parse-integer int))))
221  (skip? #\")
222  (match-case
223  ("Sun" (match? "day"))
224  ("Mon" (match? "day"))
225  ("Tue" (match? "sday"))
226  ("Wed" (match? "nesday"))
227  ("Thu" (match? "rsday"))
228  ("Fri" (match? "day"))
229  ("Sat" (match? "urday")))
230  (skip? #\,)
231  (skip #\Space)
232  (if (integer-char-p (current))
233  (progn
234  (setq day (parse-int))
235  (skip #\Space #\-)
236  (setq month (parse-month))
237  (skip #\Space #\-)
238  (setq year (parse-int))
239  (skip #\Space)
240  (setq hour (parse-int))
241  (skip #\:)
242  (setq min (parse-int))
243  (skip #\:)
244  (setq sec (parse-int)))
245  (progn
246  (setq month (parse-month))
247  (skip #\Space #\-)
248  (setq day (parse-int))
249  (skip #\Space)
250  (setq hour (parse-int))
251  (skip #\:)
252  (setq min (parse-int))
253  (skip #\:)
254  (setq sec (parse-int))
255  (skip #\Space)
256  (setq year (parse-int))))
257  (skip #\Space)
258  (bind (tz-abbrev (skip-while alpha-char-p))
259  (setq offset (get-tz-offset tz-abbrev))
260  (skip? #\")
261  ;; Shorthand year, default to current century
262  (when (< year 100)
263  (incf year *current-century-offset*))
264  (return-from parse-cookie-date
265  (obj/time:timestamp-to-universal
266  (obj/time:encode-timestamp 0 sec min hour day month year :timezone obj/time:+gmt-zone+
267  :offset offset))))))
268  (error ()
269  (error 'invalid-expires-date
270  :expires cookie-date)))))
271 
272 (defun parse-set-cookie-header (set-cookie-string origin-host origin-path)
273  "Parse cookie header string and return a cookie struct instance populated with
274 the respective slots."
275  (check-type origin-host string)
276  (let ((cookie (make-cookie :origin-host origin-host :path origin-path)))
277  (handler-case
278  (with-vector-parsing (set-cookie-string)
279  (bind (name (skip+ (not #\=)))
280  (setf (cookie-name cookie) name))
281  (skip #\=)
282  (bind (value (skip* (not #\;)))
283  (setf (cookie-value cookie) value))
284  (skip #\;)
285  (loop
286  (skip* #\Space)
287  (match-i-case
288  ("expires" (skip #\=)
289  ;; Assume there're both the Max-Age and the Expires attribute if cookie-expires has already set.
290  ;; In that case, just ignores Expires header.
291  (if (cookie-expires cookie)
292  (skip* (not #\;))
293  (bind (expires (skip* (not #\;)))
294  (setf (cookie-expires cookie)
295  (parse-cookie-date expires)))))
296  ("max-age" (skip #\=)
297  (bind (max-age (skip* (not #\;)))
298  (setf (cookie-max-age cookie)
299  (parse-integer max-age))))
300  ("path" (skip #\=)
301  (bind (path (skip* (not #\;)))
302  (setf (cookie-path cookie) path)))
303  ("domain" (skip #\=)
304  (bind (domain (skip* (not #\;)))
305  (setf (cookie-domain cookie) domain)))
306  ("samesite" (skip #\=)
307  (bind (samesite (skip* (not #\;)))
308  (setf (cookie-same-site cookie) samesite)))
309  ("partitioned" (setf (cookie-partitioned cookie) t))
310  ("secure" (setf (cookie-secure-p cookie) t))
311  ("httponly" (setf (cookie-httponly-p cookie) t))
312  (otherwise ;; Ignore unknown attributes
313  (skip* (not #\=))
314  (skip #\=)
315  (skip* (not #\;))))
316  (skip? #\;)))
317  (match-failed ()
318  (error 'invalid-set-cookie :header set-cookie-string))
319  (invalid-expires-date (e)
320  (warn (princ-to-string e))
321  (return-from parse-set-cookie-header nil)))
322  cookie))