1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
|
(in-package :araneida)
(defgeneric request-path-info (request)
(:documentation "Returns portion of the requested URL after the base-url"))
(defmethod request-path-info ((r request))
(let ((path (url-path (request-url r)))
(ppath (url-path (request-base-url r))))
(subseq path (length ppath) nil)))
(defgeneric request-unhandled-part (request)
(:documentation "Returns portion of request unhandled (??)"))
(defmethod request-unhandled-part ((request request))
(let* ((handled-by (request-handled-by request))
(offset (or (second (first handled-by)) 0))
(urlstring (request-urlstring request)))
(subseq urlstring offset)))
(defgeneric request-header (request name)
(:documentation "Returns a list containing the values of all header lines in REQUEST given by the keyword NAME"))
(defmethod request-header ((r request) name)
(cdr (assoc name (request-headers r) :test #'string=)))
(defgeneric request-if-modified-since (request &optional default)
(:documentation "Retrieve and parse the date in the If-Modified-Since header field. Return DEFAULT if the header is absent or unparseable"))
(defmethod request-if-modified-since ((request request)
&optional (default nil))
(let ((if-mod-since (car (request-header request :if-modified-since))))
(if if-mod-since
(or (date:parse-time (car (split if-mod-since 2 '(#\;))))
default)
default)))
(define-condition cookie-not-found (serious-condition)
((cookie-name :initarg :cookie-name
:reader cookie-not-found-cookie-name))
(:report (lambda (condition stream)
(format stream "Cookie ~A was not found"
(cookie-not-found-cookie-name condition)))))
(defgeneric request-cookie (request cookie-name &key on-fail)
(:documentation "Returns the value of the cookie named COOKIE-NAME
If the cookie is not found, the default is to return nil. :on-fail can also
be set to :signal-condition, whereupon it will signal a cookie-not-found condition.
Valid values for on-fail:
:return-nil
:signal-condition"))
(defmethod request-cookie ((request request) name &key (on-fail :return-nil))
(let ((cookie (find-if (lambda (c) (string-equal (rfc2109:cookie-name c) name))
(request-cookies request))))
(if cookie
(rfc2109:cookie-value cookie)
(ecase on-fail
(:return-nil nil)
(:signal-condition (error 'cookie-not-found :cookie-name name))))))
(defgeneric request-safe-cookie (request cookie-name domain-restrict &key on-fail)
(:documentation "Returns the value of the cookie named COOKIE-NAME, guarding against spoofing attacks.
The cookie will only be used if its domain matches domain-restrict (a string), or if its domain is blank.
If the cookie is not found, the default is to return nil. :on-fail can also
be set to :signal-condition, whereupon it will signal a cookie-not-found condition.
Valid values for on-fail:
:return-nil
:signal-condition"))
(defmethod request-safe-cookie ((request request) name domain-restrict &key (on-fail :return-nil))
(let ((cookie (find-if (lambda (c) (string-equal (rfc2109:cookie-name c) name))
(request-safe-cookies request domain-restrict))))
(if cookie
(rfc2109:cookie-value cookie)
(ecase on-fail
(:return-nil nil)
(:signal-condition (error 'cookie-not-found :cookie-name name))))))
(defgeneric request-cookies (request)
(:documentation "Returns cookie-structs for all cookies returned (see rfc2109 package for details)
This is rarely used directly. REQUEST-COOKIE is the better choice for most uses."))
(defmethod request-cookies ((request request))
(loop for cookie-string in (request-header request :cookie)
appending (rfc2109:parse-cookies cookie-string)))
(defgeneric request-safe-cookies (request domain-restriction)
(:documentation "Returns cookie-structs for all cookies returned, avoiding spoofing attacks
domain-restriction is a string such as 'my.test.domain' which should match your website's domain
See the RFC2109 package for details
This is rarely used directly. REQUEST-SAFE-COOKIE is the better choice for most uses."))
(defmethod request-safe-cookies ((request request) domain-restriction)
(loop for cookie-string in (request-header request :cookie)
appending (rfc2109:safe-parse-cookies cookie-string domain-restriction)))
(defun cookie-string (name value &key comment domain max-age path secure)
"Returns a cookie string suitable for setting
See documentation for RFC2109:COOKIE-STRING for details."
(rfc2109:cookie-string name value :comment comment :domain domain :max-age max-age :path path :secure secure))
;; this takes an alist not a request, hence the name
(defun body-param (name alist)
"Look in the request body ALIST for the value of the parameter NAME"
(cadr (assoc name alist :test #'string=)))
(defun body-params (name alist &key (case-sensitive nil))
"Look in the request body ALIST for the values of the parameters starting NAME, returning a list of KEY VALUE pairs"
;; find all parameters starting NAME
(let ((equal (if case-sensitive #'string= #'string-equal))
(len (length name)))
(flet ((starts-with-name (string)
(and (>= (length string) len)
(funcall equal string name :end1 len))))
(remove-if-not #'starts-with-name alist :key #'car))))
(defgeneric dispatch-request (request handlers &optional discriminator)
(:documentation "Find the best match for REQUEST in the list HANDLERS"))
(defmethod dispatch-request ((request request) handlers &optional discriminator)
(unless discriminator (setf discriminator (request-url request)))
(destructuring-bind
(method match prefix func &optional needs-discriminator)
(find-export (urlstring discriminator) handlers (request-method request))
(declare (ignore match))
(unless method (return-from dispatch-request nil))
(setf (request-base-url request) (parse-urlstring prefix))
(let ((rest-of-url
(subseq (urlstring discriminator)
(length (urlstring (request-base-url request)))
nil)))
(cond ((and needs-discriminator (consp func))
(apply (car func) request handlers discriminator
rest-of-url (cdr func)))
((consp func)
(apply (car func) request rest-of-url (cdr func)))
(needs-discriminator
(funcall func request handlers discriminator rest-of-url))
(t
(funcall func request rest-of-url))))))
;;; rfc 1945 p26
(defvar *http-error-codes*
'((400 . "Bad Request")
(401 . "Unauthorized")
(402 . "Payment required")
(403 . "Forbidden")
(404 . "Not Found")
(500 . "Internal Server Error")
(501 . "Not Implemented")
(502 . "Bad Gateway")
(503 . "Service Unavailable")))
(defgeneric request-send-headers (request &key
content-type
content-length
expires
cache-control
location
refresh
pragma
set-cookie
conditional
www-authenticate
extra-http-headers
last-modified
response-text
response-code)
(:documentation "Send HTTP/1.0 headers in response to REQUEST. If the request HTTP version
is less than 1.0, do nothing. If CONDITIONAL is true, may signal RESPONSE-SENT
instead of returning normally."))
(defmethod request-send-headers ((request request) &key
(content-type "text/html")
content-length
expires
cache-control
location
refresh
pragma
set-cookie
conditional
www-authenticate
extra-http-headers
(last-modified (get-universal-time))
(response-text "OK")
(response-code 200))
(when (< (request-http-version request) 1.0)
(return-from request-send-headers response-code))
(let ((stream (request-stream request))
(cr (code-char 13))
(lf (code-char 10)))
(labels ((perhaps (if name &optional then)
(if if (princ (s. name ": " (or then if) (format nil "~C~C" cr lf))
stream)))
(date (d)
(if (numberp d) (date:universal-time-to-http-date d) d)))
(when (and conditional
(<= last-modified
(request-if-modified-since request 0)))
(setf response-code 304 response-text "Not modified"))
(when (eql response-code 304)
;; "the response {SHOULD,MUST} NOT include other
;; entity-headers; this prevents inconsistencies between
;; cached entity-bodies and updated headers.
(setf content-length nil content-type nil))
(format stream "HTTP/1.0 ~D ~A~C~C~
Date: ~A~C~C~
Server: ~A~C~C~
Connection: close~C~C"
response-code response-text cr lf
(date:universal-time-to-http-date (get-universal-time)) cr lf
*araneida-product-tokens* cr lf cr lf)
(perhaps content-type "Content-Type")
(perhaps last-modified "Last-Modified" (date last-modified))
(perhaps content-length "Content-Length")
(if set-cookie
(let ((cookies (if (listp set-cookie) set-cookie (list set-cookie))))
(dolist (cookie cookies) (perhaps cookie "Set-Cookie"))))
(perhaps cache-control "Cache-Control" )
(perhaps refresh "Refresh" )
(perhaps location "Location" )
(perhaps pragma "Pragma" )
(perhaps expires "Expires" (date expires))
(perhaps www-authenticate "WWW-Authenticate")
(mapc #'(lambda (header)
(format stream "~A: ~A~C~C" (car header) (cdr header) cr lf))
extra-http-headers)
(format stream "~C~C" cr lf)
(when (eql response-code 304)
;; "The 304 response MUST NOT contain a message-body" (rfc2616)
(signal 'response-sent)))
response-code))
(defgeneric request-send-error (request error-code &key log-message client-message)
(:documentation "Send the client HTTP headers and HTML body for an error message
with numeric code ERROR-CODE. LOG-MESSAGE is sent to *log-stream*, while CLIENT-MESSAGE
is sent on to the user - replacing the default text. CLIENT-MESSAGE is passed to format,
so it should not be an HTML list"))
(defmethod request-send-error ((request request) error-code &key log-message client-message)
(let ((stream (request-stream request))
(error-text (cdr (assoc error-code *http-error-codes*))))
(when *log-stream*
(format *log-stream* "~&Logged error: ~A ~A ~@[~A~] while processing URL <~A>~%"
error-code error-text log-message (urlstring (request-url request))))
(request-send-headers request
:response-code error-code :response-text error-text)
(html-stream stream
`(html (head (title ,(s. error-code) " " ,error-text))
(body
(h2 ,error-text)
(p "Was asked for URL "
(tt ,(urlstring (request-url request)))
", but it didn't happen for us. Sorry")
,@(when client-message
`((H3 "Additional information: ")
(pre ,(html-escape (format nil "~a" client-message))))))))
(signal 'response-sent)))
(defgeneric request-redirect (request new-url &rest headers)
(:documentation "Redirects request to NEW-URL, appending HEADERS to the redirect"))
(defmethod request-redirect ((request request) new-url &rest headers)
(let ((urlstring (urlstring
(if (typep new-url 'url)
new-url
(merge-url (request-url request) new-url)))))
(apply #'request-send-headers
request
:location urlstring
:expires "Fri, 30 Oct 1998 14:19:41 GMT"
:pragma "no-cache"
:response-code 302 :response-text "Redirected"
headers)
(format (request-stream request)
"~%<h1>Redirected</h1><p>Continue <a href=\"~A\">~A</a>"
urlstring urlstring)
(signal 'response-sent)))
(defun copy-request (from)
(let ((to (make-instance 'request)))
(labels ((set-slot (name)
(if (slot-boundp from name)
(setf (slot-value to name) (slot-value from name))
(slot-makunbound to name))))
(dolist (i '(url urlstring http-version handled-by user
method stream headers body unparsed-body condition))
(set-slot i)))
to))
|