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 3 ;; Based on Fukamachi's CL-COOKIE 6 (in-package :net/cookie) 8 (defun cookie-domain-p (domain cookie-domain) 10 (return-from cookie-domain-p t)) 11 (if (ip-addr-p domain) 12 (ip-addr= domain cookie-domain) 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)) 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) 25 (aref cookie-domain (- (length cookie-domain) 26 (length registered-domain))))))))))) 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)) 32 (deftype same-site nil 33 '(satisfies same-site-p)) 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)) 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)))) 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)))) 74 (defun expired-cookie-p (cookie) 75 "Check if cookie is expired, whereas max-age has priority over expires." 77 (cookie-max-age cookie)) 79 (cookie-creation-timestamp cookie)) 82 (cookie-expires cookie)) 83 (< expires (get-universal-time))))) 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)))) 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)) #\/)))))) 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) 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))))) 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))) 118 (defun write-cookie-header (cookies &optional stream) 119 (labels ((write-cookie (cookie s) 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)))) 130 (main (ensure-cons cookies) stream) 131 (with-output-to-string (s) 132 (main (ensure-cons cookies) s)))))) 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.") 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) 143 (format-timestring s (universal-to-timestamp universal-time) 144 :format +set-cookie-date-format+ :timezone obj/time:+gmt-zone+)))) 146 "~A=~A~@[; Expires=~A~]~@[; Max-age=~A~]~@[; Path=~A~]~@[; Domain=~A~]~@[; SameSite=~A~]~:[~;; Partitioned~]~:[~;; Secure~]~:[~;; HttpOnly~]" 148 (cookie-value cookie) 149 (format-cookie-date (cookie-expires cookie) stream) 150 (cookie-max-age cookie) 152 (cookie-domain cookie) 153 (cookie-same-site cookie) 154 (cookie-partitioned cookie) 155 (cookie-secure-p cookie) 156 (cookie-httponly-p cookie)))) 158 (defun merge-cookies (cookie-jar cookies) 159 (setf (cookie-jar-cookies cookie-jar) 161 (nconc (cookie-jar-cookies cookie-jar) 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))))) 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))))) 177 (defun integer-char-p (char) 178 (char<= #\0 char #\9)) 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)) 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)))))) 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))))))) 194 (defparameter *current-century-offset* 195 (* (1- (timestamp-century (today))) 198 (defun parse-cookie-date (cookie-date) 199 (let (year month day hour min sec offset) 201 (with-vector-parsing (cookie-date) 202 (labels ((parse-month () 203 (if (integer-char-p (current)) 206 ("Jan" (match? "uary") 1) 207 ("Feb" (match? "ruary") 2) 208 ("Mar" (match? "ch") 3) 209 ("Apr" (match? "il") 4) 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)))) 219 (bind (int (skip-while integer-char-p)) 220 (parse-integer int)))) 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"))) 232 (if (integer-char-p (current)) 234 (setq day (parse-int)) 236 (setq month (parse-month)) 238 (setq year (parse-int)) 240 (setq hour (parse-int)) 242 (setq min (parse-int)) 244 (setq sec (parse-int))) 246 (setq month (parse-month)) 248 (setq day (parse-int)) 250 (setq hour (parse-int)) 252 (setq min (parse-int)) 254 (setq sec (parse-int)) 256 (setq year (parse-int)))) 258 (bind (tz-abbrev (skip-while alpha-char-p)) 259 (setq offset (get-tz-offset tz-abbrev)) 261 ;; Shorthand year, default to current century 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+ 269 (error 'invalid-expires-date 270 :expires cookie-date))))) 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))) 278 (with-vector-parsing (set-cookie-string) 279 (bind (name (skip+ (not #\=))) 280 (setf (cookie-name cookie) name)) 282 (bind (value (skip* (not #\;))) 283 (setf (cookie-value cookie) value)) 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) 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)))) 301 (bind (path (skip* (not #\;))) 302 (setf (cookie-path cookie) path))) 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 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)))