File: request.lisp

package info (click to toggle)
araneida 0.90.1-dfsg-2
  • links: PTS
  • area: main
  • in suites: etch, etch-m68k
  • size: 700 kB
  • ctags: 643
  • sloc: lisp: 4,878; perl: 166; sh: 109; makefile: 34
file content (287 lines) | stat: -rw-r--r-- 12,399 bytes parent folder | download | duplicates (2)
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))