changelog shortlog graph tags branches changeset files revisions annotate raw help

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

changeset 359: 0e00dec3de03
child: 5b6a2a8ba83e
author: Richard Westhaver <ellis@rwest.io>
date: Wed, 22 May 2024 22:16:26 -0400
permissions: -rw-r--r--
description: macs/control macros, seq functions, ported cl-cookie, added uri/domain.lisp, fully ported http! next we should remove dependence on cl+ssl
1 ;;; net/req.lisp --- HTTP Request API
2 
3 ;; based on Fukamachi's DEXADOR
4 
5 ;;; Code:
6 (in-package :net/req)
7 
8 ;;; errors
9 (define-condition http-request-failed (error)
10  ((body :initarg :body
11  :reader response-body)
12  (status :initarg :status
13  :reader response-status)
14  (headers :initarg :headers
15  :reader response-headers)
16  (uri :initarg :uri
17  :reader request-uri)
18  (method :initarg :method
19  :reader request-method))
20  (:report (lambda (condition stream)
21  (with-slots (uri status) condition
22  (format stream "An HTTP request to ~S has failed (status=~D)."
23  (render-uri uri nil)
24  status)))))
25 
26 (defmacro define-request-failed-condition (name code)
27  `(define-condition ,(intern (format nil "~A-~A" :http-request name)) (http-request-failed)
28  ()
29  (:report (lambda (condition stream)
30  (with-slots (body uri) condition
31  (format stream ,(format nil "An HTTP request to ~~S returned ~D ~A.~~2%~~A"
32  code
33  (substitute #\Space #\- (string-downcase name)))
34  (render-uri uri nil)
35  body))))))
36 
37 
38 (defvar *request-failed-error* (make-hash-table :test 'eql))
39 
40 #.`(progn
41  ,@(loop for (name . code) in '(;; 4xx (Client Errors)
42  (bad-request . 400)
43  (unauthorized . 401)
44  (payment-required . 402)
45  (forbidden . 403)
46  (not-found . 404)
47  (method-not-allowed . 405)
48  (not-acceptable . 406)
49  (proxy-authentication-required . 407)
50  (request-timeout . 408)
51  (conflict . 409)
52  (gone . 410)
53  (length-required . 411)
54  (precondition-failed . 412)
55  (payload-too-large . 413)
56  (uri-too-long . 414)
57  (unsupported-media-type . 415)
58  (range-not-satisfiable . 416)
59  (expectation-failed . 417)
60  (misdirected-request . 421)
61  (upgrade-required . 426)
62  (too-many-requests . 429)
63 
64  ;; 5xx (Server Errors)
65  (internal-server-error . 500)
66  (not-implemented . 501)
67  (bad-gateway . 502)
68  (service-unavailable . 503)
69  (gateway-timeout . 504)
70  (http-version-not-supported . 505))
71  collect `(define-request-failed-condition ,name ,code)
72  collect `(setf (gethash ,code *request-failed-error*)
73  ',(intern (format nil "~A-~A" :http-request name)))))
74 
75 (defun http-request-failed (status &key body headers uri method)
76  (cerror
77  "Ignore and continue"
78  (gethash status *request-failed-error* 'http-request-failed)
79  :body body
80  :status status
81  :headers headers
82  :uri uri
83  :method method))
84 
85 (define-condition socks5-proxy-request-failed (http-request-failed)
86  ((reason :initarg :reason))
87  (:report (lambda (condition stream)
88  (with-slots (uri reason) condition
89  (format stream "An HTTP request to ~S via SOCKS5 has failed (reason=~S)."
90  (render-uri uri nil)
91  reason)))))
92 
93 ;;; utils
94 (defvar *default-connect-timeout* 10)
95 (defvar *default-read-timeout* 10)
96 (defvar *verbose* nil)
97 (defvar *no-ssl* nil)
98 
99 (defvar *default-proxy* (or #-windows (uiop:getenv "HTTPS_PROXY")
100  #-windows (uiop:getenv "HTTP_PROXY"))
101  "If specified will be used as the default value of PROXY in calls to dexador. Defaults to
102  the value of the environment variable HTTPS_PROXY or HTTP_PROXY if not on Windows.")
103 
104 (define-constant +crlf+ (string-to-octets (format nil "~C~C" #\Return #\Newline)) :test 'equalp)
105 
106 (eval-always
107  (defparameter *default-user-agent*
108  (format nil "cc/req (~A~@[ ~A~]); ~A;~@[ ~A~]"
109  (lisp-implementation-type)
110  (lisp-implementation-version)
111  (software-type)
112  (software-version))))
113 
114 (defparameter *header-buffer* nil)
115 
116 (defun write-first-line (method uri version &optional (buffer *header-buffer*))
117  (fast-write-sequence (string-to-octets (string method)) buffer)
118  (fast-write-byte #.(char-code #\Space) buffer)
119  (fast-write-sequence (string-to-octets
120  (format nil "~A~:[~;~:*?~A~]"
121  (or (uri-path uri) "/")
122  (uri-query uri)))
123  buffer)
124  (fast-write-byte #.(char-code #\Space) buffer)
125  (fast-write-sequence (ecase version
126  (1.1 (string-to-octets "HTTP/1.1"))
127  (1.0 (string-to-octets "HTTP/1.0")))
128  buffer)
129  (fast-write-sequence +crlf+ buffer))
130 
131 (defun write-header-field (name buffer)
132  (fast-write-sequence (if (typep name 'octet-vector)
133  name
134  (string-to-octets (string-capitalize name)))
135  buffer))
136 
137 (defun write-header-value (value buffer)
138  (fast-write-sequence (if (typep value 'octet-vector)
139  value
140  (string-to-octets (princ-to-string value)))
141  buffer))
142 
143 (defun write-header (name value &optional (buffer *header-buffer*))
144  (write-header-field name buffer)
145  (fast-write-sequence (string-to-octets ": ") buffer)
146  (write-header-value value buffer)
147  (fast-write-sequence +crlf+ buffer))
148 
149 (define-compiler-macro write-header (name value &optional (buffer '*header-buffer*))
150  `(progn
151  ,(if (and (constantp name)
152  (typep name '(or keyword string)))
153  `(fast-write-sequence (string-to-octets ,(string-capitalize name)) ,buffer)
154  `(write-header-field ,name ,buffer))
155  (fast-write-sequence (string-to-octets ": ") ,buffer)
156  ,(if (constantp value)
157  `(fast-write-sequence (string-to-octets ,(string value)) ,buffer)
158  `(write-header-value ,value ,buffer))
159  (fast-write-sequence +crlf+ ,buffer)))
160 
161 (defmacro with-header-output ((buffer &optional output) &body body)
162  `(fast-io:with-fast-output (,buffer ,output)
163  (declare (ignorable ,buffer))
164  (let ((*header-buffer* ,buffer))
165  ,@body)))
166 
167 (defun write-connect-header (uri version buffer &optional proxy-auth)
168  (fast-write-sequence (string-to-octets "CONNECT") buffer)
169  (fast-write-byte #.(char-code #\Space) buffer)
170  (fast-write-sequence (string-to-octets (format nil "~A:~A"
171  (uri-host uri)
172  (uri-port uri)))
173  buffer)
174  (fast-write-byte #.(char-code #\Space) buffer)
175  (fast-write-sequence (ecase version
176  (1.1 (string-to-octets "HTTP/1.1"))
177  (1.0 (string-to-octets "HTTP/1.0")))
178  buffer)
179  (fast-write-sequence +crlf+ buffer)
180  (fast-write-sequence (string-to-octets "Host:") buffer)
181  (fast-write-byte #.(char-code #\Space) buffer)
182  (fast-write-sequence (string-to-octets (format nil "~A:~A"
183  (uri-host uri)
184  (uri-port uri)))
185  buffer)
186  (when proxy-auth
187  (fast-write-sequence +crlf+ buffer)
188  (fast-write-sequence (string-to-octets "Proxy-Authorization:") buffer)
189  (fast-write-byte #.(char-code #\Space) buffer)
190  (fast-write-sequence (string-to-octets proxy-auth) buffer))
191  (fast-write-sequence +crlf+ buffer)
192  (fast-write-sequence +crlf+ buffer))
193 
194 (defun make-random-string (&optional (length 12))
195  (declare (type fixnum length))
196  (let ((result (make-string length)))
197  (declare (type simple-string result))
198  (dotimes (i length result)
199  (setf (aref result i)
200  (ecase (random 5)
201  ((0 1) (code-char (+ #.(char-code #\a) (random 26))))
202  ((2 3) (code-char (+ #.(char-code #\A) (random 26))))
203  ((4) (code-char (+ #.(char-code #\0) (random 10)))))))))
204 
205 ;;; encoding
206 (defun parse-content-type (content-type)
207  (let ((types
208  (nth-value 1
209  (ppcre:scan-to-strings "^\\s*?(\\w+)/([^;\\s]+)(?:\\s*;\\s*charset=([A-Za-z0-9_-]+))?"
210  content-type))))
211  (when types
212  (values (aref types 0)
213  (aref types 1)
214  (aref types 2)))))
215 
216 (defun charset-to-encoding (charset &optional
217  (default *default-external-format*))
218  (cond
219  ((null charset)
220  default)
221  ((string-equal charset "utf-8")
222  :utf-8)
223  ((string-equal charset "euc-jp")
224  :eucjp)
225  ((or (string-equal charset "shift_jis")
226  (string-equal charset "shift-jis"))
227  :cp932)
228  ((string-equal charset "windows-31j")
229  :cp932)
230  (t (or (when (sb-impl::get-external-format (keywordicate charset)) charset)
231  default))))
232 
233 (defun detect-charset (content-type body)
234  (multiple-value-bind (type subtype charset)
235  (parse-content-type content-type)
236  (cond
237  ((charset-to-encoding charset nil))
238  ((string-equal type "text")
239  (or (charset-to-encoding charset nil)
240  (if (and (string-equal subtype "html")
241  (typep body '(array (unsigned-byte 8) (*))))
242  (charset-to-encoding (detect-charset-from-html body) nil)
243  nil)
244  :utf-8))
245  ((and (string-equal type "application")
246  (or (string-equal subtype "json")
247  (string-equal subtype "javascript")))
248  ;; According to RFC4627 (http://www.ietf.org/rfc/rfc4627.txt),
249  ;; JSON text SHALL be encoded in Unicode. The default encoding is UTF-8.
250  ;; It's possible to determine if the encoding is UTF-16 or UTF-36
251  ;; by looking at the first four octets, however, I leave it to the future.
252  ;;
253  ;; According to RFC4329 (https://datatracker.ietf.org/doc/html/rfc4329),
254  ;; javascript also is specified by charset, or defaults to UTF-8
255  ;; It's also possible to specify in the first four octets, but
256  ;; like application/json I leave it to the future.
257  (charset-to-encoding charset :utf-8))
258  ((and (string-equal type "application")
259  (ppcre:scan "(?:[^+]+\\+)?xml" subtype))
260  (charset-to-encoding charset)))))
261 
262 (defun detect-charset-from-html (body)
263  "Detect the body's charset by (roughly) searching meta tags which has \"charset\" attribute."
264  (labels ((find-meta (start)
265  (search #.(string-to-octets "<meta ") body :start2 start))
266  (main (start)
267  (let ((start (find-meta start)))
268  (unless start
269  (return-from main nil))
270  (let ((end (position (char-code #\>) body :start start :test #'=)))
271  (unless end
272  (return-from main nil))
273  (incf end)
274  (let ((match (nth-value 1 (ppcre:scan-to-strings
275  "charset=[\"']?([^\\s\"'>]+)[\"']?"
276  (octets-to-string body :start start :end end)))))
277  (if match
278  (aref match 0)
279  (main end)))))))
280  (main 0)))
281 
282 ;;; keep-alive-stream
283 (defclass keep-alive-stream (fundamental-input-stream)
284  ((stream :type (or null stream)
285  :initarg :stream
286  :initform (error ":stream is required")
287  :accessor keep-alive-stream-stream
288  :documentation "A stream; when we read END elements from it, we call CLOSE-ACTION on it and
289  set this slot to nil.")
290  (end :initarg :end
291  :initform nil
292  :accessor keep-alive-stream-end)
293  (close-action :initarg :on-close-or-eof :reader close-action
294  :documentation "A (lambda (stream abort)) which will be called with keep-alive-stream-stream
295  when the stream is either closed or we hit end of file or we hit end")))
296 
297 (defun keep-alive-stream-close-underlying-stream (underlying-stream abort)
298  (when (and underlying-stream (open-stream-p underlying-stream))
299  (close underlying-stream :abort abort)))
300 
301 (defclass keep-alive-chunked-stream (keep-alive-stream)
302  ((chunga-stream :initarg :chunga-stream :accessor chunga-stream)))
303 
304 (defun make-keep-alive-stream (stream &key end chunked-stream (on-close-or-eof #'keep-alive-stream-close-underlying-stream))
305  "ON-CLOSE-OR-EOF takes a single parameter, STREAM (the stream passed in here, not the
306 keep-alive-stream), and should handle clean-up of it"
307  (assert (xor end chunked-stream))
308  (if chunked-stream
309  (make-instance 'keep-alive-chunked-stream :stream stream :chunga-stream chunked-stream :on-close-or-eof on-close-or-eof)
310  (make-instance 'keep-alive-stream :stream stream :end end :on-close-or-eof on-close-or-eof)))
311 
312 (defun maybe-close (stream &optional (close-if nil))
313  "Will close the underlying stream if close-if is T (unless it is already closed).
314  If the stream is already closed or we closed it returns :EOF otherwise NIL."
315  (let ((underlying-stream (keep-alive-stream-stream stream)))
316  (cond
317  ((not underlying-stream)
318  :eof)
319  (close-if
320  (funcall (close-action stream) underlying-stream nil)
321  (setf (keep-alive-stream-stream stream) nil)
322  :eof)
323  (t nil))))
324 
325 (defmethod stream-read-byte ((stream keep-alive-stream))
326  "Return :EOF or byte read. When we hit EOF or finish reading our allowed content,
327  call the close-action on our underlying-stream and return EOF."
328  (let ((byte :eof)
329  (underlying-stream (keep-alive-stream-stream stream)))
330  (or (maybe-close stream (<= (keep-alive-stream-end stream) 0))
331  (progn
332  (setf byte (read-byte underlying-stream nil :eof))
333  (decf (keep-alive-stream-end stream) 1)
334  (maybe-close stream (or (<= (keep-alive-stream-end stream) 0) (eql byte :eof)))
335  byte))))
336 
337 (defmethod stream-read-byte ((stream keep-alive-chunked-stream))
338  "Return :EOF or byte read. When we hit :EOF or finish reading our chunk,
339  call the close-action on our underlying-stream and return :EOF"
340  (or (maybe-close stream)
341  (if (chunga:chunked-stream-input-chunking-p (chunga-stream stream))
342  (let ((byte (read-byte (chunga-stream stream) nil :eof)))
343  (if (eql byte :eof)
344  (prog1
345  byte
346  (maybe-close stream t))
347  byte))
348  (or (maybe-close stream t) :eof))))
349 
350 (defmethod stream-read-sequence ((stream keep-alive-stream) sequence start end &key)
351  (declare (optimize speed))
352  (if (null (keep-alive-stream-stream stream)) ;; we already closed it
353  start
354  (let* ((to-read (min (- end start) (keep-alive-stream-end stream)))
355  (n (read-sequence sequence (keep-alive-stream-stream stream)
356  :start start
357  :end (+ start to-read))))
358  (decf (keep-alive-stream-end stream) (- n start))
359  (maybe-close stream (<= (keep-alive-stream-end stream) 0))
360  n)))
361 
362 (defmethod stream-read-sequence ((stream keep-alive-chunked-stream) sequence start end &key)
363  (declare (optimize speed))
364  (if (null (keep-alive-stream-stream stream)) ;; we already closed it
365  start
366  (if (chunga:chunked-stream-input-chunking-p (chunga-stream stream))
367  (prog1
368  (let ((num-read (read-sequence sequence (chunga-stream stream) :start start :end end)))
369  num-read)
370  (maybe-close stream (not (chunga:chunked-stream-input-chunking-p (chunga-stream stream)))))
371  start)))
372 
373 (defmethod stream-element-type ((stream keep-alive-chunked-stream))
374  (stream-element-type (chunga-stream stream)))
375 
376 (defmethod stream-element-type ((stream keep-alive-stream))
377  '(unsigned-byte 8))
378 
379 (defmethod open-stream-p ((stream keep-alive-stream))
380  (let ((underlying-stream (keep-alive-stream-stream stream)))
381  (and underlying-stream (open-stream-p underlying-stream))))
382 
383 (defmethod close ((stream keep-alive-stream) &key abort)
384  (funcall (close-action stream) (keep-alive-stream-stream stream) abort)
385  (setf (keep-alive-stream-stream stream) nil))
386 
387 ;;; decoding-stream
388 (declaim (type fixnum +buffer-size+))
389 (eval-when (:compile-toplevel :load-toplevel :execute)
390  (defconstant +buffer-size+ 128))
391 
392 (defclass decoding-stream (fundamental-character-input-stream)
393  ((stream :type stream
394  :initarg :stream
395  :initform (error ":stream is required")
396  :accessor decoding-stream-stream)
397  (encoding :initarg :encoding
398  :initform (error ":encoding is required")
399  :accessor decoding-stream-encoding)
400  (buffer :type (simple-array (unsigned-byte 8) (#.+buffer-size+))
401  :initform (make-array +buffer-size+ :element-type '(unsigned-byte 8))
402  :accessor decoding-stream-buffer)
403  (buffer-position :type fixnum
404  :initform +buffer-size+
405  :accessor decoding-stream-buffer-position)
406  (buffer-end-position :type fixnum
407  :initform -1
408  :accessor decoding-stream-buffer-end-position)
409  (last-char :type character
410  :initform #\Nul
411  :accessor decoding-stream-last-char)
412  (last-char-size :type fixnum
413  :initform 0
414  :accessor decoding-stream-last-char-size)
415  (on-close :type (or null function) :initform nil :initarg :on-close)))
416 
417 (defmethod initialize-instance :after ((stream decoding-stream) &rest initargs)
418  (declare (ignore initargs))
419  (with-slots (encoding) stream
420  (when (keywordp encoding)
421  (setf encoding (babel-encodings:get-character-encoding encoding)))))
422 
423 (defun make-decoding-stream (stream &key (encoding babel-encodings:*default-character-encoding*)
424  (on-close))
425  (let ((decoding-stream (make-instance 'decoding-stream
426  :stream stream
427  :encoding encoding
428  :on-close on-close)))
429  (fill-buffer decoding-stream)
430  decoding-stream))
431 
432 (defun fill-buffer (stream)
433  (declare (optimize speed))
434  (with-slots (stream buffer buffer-position buffer-end-position) stream
435  (declare (type (simple-array (unsigned-byte 8) (#.+buffer-size+)) buffer)
436  (type fixnum buffer-position))
437  (let ((to-read (- +buffer-size+ buffer-position)))
438  (declare (type fixnum to-read))
439  (replace buffer buffer
440  :start1 0
441  :start2 buffer-position
442  :end2 +buffer-size+)
443  (setf buffer-position 0)
444  (let ((n (read-sequence buffer stream :start to-read)))
445  (declare (type fixnum n))
446  (unless (= n +buffer-size+)
447  (setf buffer-end-position n))))))
448 
449 (defun needs-to-fill-buffer-p (stream)
450  (declare (optimize speed))
451  (when (/= -1 (the fixnum (decoding-stream-buffer-end-position stream)))
452  (return-from needs-to-fill-buffer-p nil))
453 
454  (with-slots (buffer-position encoding) stream
455  (< (- +buffer-size+ (the fixnum buffer-position))
456  (the fixnum (babel-encodings:enc-max-units-per-char encoding)))))
457 
458 
459 (defmethod stream-read-char ((stream decoding-stream))
460  (declare (optimize speed))
461  (when (needs-to-fill-buffer-p stream)
462  (fill-buffer stream))
463 
464  (when (= (the fixnum (decoding-stream-buffer-end-position stream))
465  (the fixnum (decoding-stream-buffer-position stream)))
466  (return-from stream-read-char :eof))
467 
468  (with-slots (buffer buffer-position encoding last-char last-char-size)
469  stream
470  (declare (fixnum buffer-position))
471  (let* ((mapping (babel-encodings:lookup-mapping babel::*string-vector-mappings* encoding))
472  (counter (babel-encodings:code-point-counter mapping)))
473  (declare (type function counter))
474  (multiple-value-bind (chars new-end)
475  (funcall counter buffer buffer-position +buffer-size+ 1)
476  (declare (ignore chars) (fixnum new-end))
477  (let ((string (make-string 1 :element-type 'babel:unicode-char))
478  (size (the fixnum (- new-end buffer-position))))
479  (funcall (the function (babel-encodings:decoder mapping))
480  buffer buffer-position new-end string 0)
481  (setf buffer-position new-end
482  last-char (aref string 0)
483  last-char-size size)
484  (aref string 0))))))
485 
486 (defmethod stream-unread-char ((stream decoding-stream) char)
487  (let ((last-char (decoding-stream-last-char stream)))
488  (when (char= last-char #\Nul)
489  (error "No character to unread from this stream"))
490  (unless (char= char last-char)
491  (error "Last character read (~S) was different from ~S"
492  last-char char))
493  (with-slots (buffer-position last-char-size) stream
494  (decf buffer-position last-char-size))
495  (with-slots (last-char last-char-size) stream
496  (setf last-char #\Nul
497  last-char-size 0))
498  nil))
499 
500 #+(or abcl clasp ecl)
501 (defmethod stream-read-sequence ((stream decoding-stream) sequence start end &key)
502  (loop for i from start to end
503  for char = (stream-read-char stream)
504  if (eq char :eof)
505  do (return i)
506  else do (setf (aref sequence i) char)
507  finally (return end)))
508 
509 #+(or clasp ecl)
510 (defmethod stream-read-byte ((stream decoding-stream))
511  (with-slots (last-char last-char-size) stream
512  (setf last-char #\Nul
513  last-char-size 0))
514  (read-byte (decoding-stream-stream stream) nil :eof))
515 
516 (defmethod open-stream-p ((stream decoding-stream))
517  (open-stream-p (decoding-stream-stream stream)))
518 
519 (defmethod stream-element-type ((stream decoding-stream))
520  'unicode-char)
521 
522 (defmethod close ((stream decoding-stream) &key abort)
523  ;; TODO: modify me to return the connection to the connection pool
524  (with-slots (stream) stream
525  (when (open-stream-p stream)
526  (close stream :abort abort))))
527 
528 ;;; body
529 (defun decode-body (content-type body &key default-charset on-close)
530  (let ((charset (or (and content-type
531  (detect-charset content-type body))
532  default-charset))
533  (babel-encodings:*suppress-character-coding-errors* t))
534  (if charset
535  (handler-case
536  (if (streamp body)
537  (make-decoding-stream body :encoding charset :on-close on-close)
538  (babel:octets-to-string body :encoding (keywordicate charset)))
539  (babel:character-decoding-error (e)
540  (warn (format nil "Failed to decode the body to ~S due to the following error (falling back to binary):~% ~A"
541  charset
542  e))
543  (return-from decode-body body)))
544  body)))
545 
546 (defun content-disposition (key val)
547  (typecase val
548  (cons (content-disposition key (first val)))
549  (pathname
550  (let* ((filename (file-namestring val))
551  (utf8-filename-p (find-if (lambda (char)
552  (< 127 (char-code char)))
553  filename)))
554  (format nil "Content-Disposition: form-data; name=\"~A\"; ~:[filename=\"~A\"~;filename*=UTF-8''~A~]~C~C"
555  key
556  utf8-filename-p
557  (if utf8-filename-p
558  (obj/uri:parse-uri filename)
559  filename)
560  #\Return #\Newline)))
561  (otherwise
562  (format nil "Content-Disposition: form-data; name=\"~A\"~C~C"
563  key
564  #\Return #\Newline))))
565 
566 (defmacro define-alist-cache (cache-name)
567  (let ((var (intern (format nil "*~A*" cache-name))))
568  `(progn
569  (defvar ,var)
570  (defun ,(intern (format nil "LOOKUP-IN-~A" cache-name)) (elt)
571  (when (boundp ',var)
572  (alexandria:assoc-value ,var elt)))
573  (defun (setf ,(intern (format nil "LOOKUP-IN-~A" cache-name))) (val elt)
574  (when (boundp ',var)
575  (setf (alexandria:assoc-value ,var elt) val))
576  val))))
577 
578 ;; If bound, an alist mapping content to content-type,
579 ;; used to avoid determining content type multiple times
580 (define-alist-cache content-type-cache)
581 ;; If bound, an alist mapping content to encoded content, to avoid
582 ;; double converting content when we must calculate its length first
583 (define-alist-cache content-encoding-cache)
584 
585 (defmacro with-content-caches (&body body)
586  `(let ((*content-type-cache* nil)
587  (*content-encoding-cache* nil))
588  ,@body))
589 
590 (defun content-type (value)
591  (typecase value
592  (pathname (or (lookup-in-content-type-cache value)
593  (setf (lookup-in-content-type-cache value) (mimes:mime value))))
594  (otherwise nil)))
595 
596 (defun multipart-value-content-type (value)
597  (typecase value
598  (cons
599  (destructuring-bind (val &key content-type)
600  value
601  (or content-type (content-type val))))
602  (otherwise (content-type value))))
603 
604 (defun convert-to-octets (val)
605  (or (lookup-in-content-encoding-cache val)
606  (setf (lookup-in-content-encoding-cache val)
607  (typecase val
608  (string (babel:string-to-octets val))
609  ((array (unsigned-byte 8) (*)) val)
610  (symbol (babel:string-to-octets (princ-to-string val)))
611  (cons (convert-to-octets (first val)))
612  (otherwise (babel:string-to-octets (princ-to-string val)))))))
613 
614 (defun write-as-octets (stream val)
615  (typecase val
616  ((array (unsigned-byte 8) (*)) (write-sequence val stream))
617  (pathname
618  (with-open-file (in val :element-type '(unsigned-byte 8))
619  (copy-stream in stream)))
620  (string
621  (write-sequence (convert-to-octets val) stream))
622  (cons (write-as-octets stream (first val)))
623  (otherwise (fast-write-sequence (convert-to-octets val) stream))))
624 
625 (defun content-length (val)
626  (typecase val
627  (pathname (with-open-file (in val)
628  (file-length in)))
629  (cons (content-length (first val)))
630  (otherwise (length (convert-to-octets val)))))
631 
632 (defun multipart-content-length (content boundary)
633  (declare (type simple-string boundary))
634  (let ((boundary-length (length boundary)))
635  (+ (loop for (key . val) in content
636  sum (+ 2 ;; --
637  boundary-length
638  2 ;; CR LF
639  (length (the simple-string (content-disposition key val)))
640  (let ((content-type (multipart-value-content-type val)))
641  (if content-type
642  (+ #.(length "Content-Type: ") (length content-type) 2)
643  0))
644  2
645  (content-length val)
646  2)
647  into total-length
648  finally (return total-length))
649  2 boundary-length 2 2)))
650 
651 (defun write-multipart-content (content boundary stream)
652  (let ((boundary (string-to-octets boundary)))
653  (labels ((boundary-line (&optional endp)
654  (fast-write-sequence (string-to-octets "--") stream)
655  (fast-write-sequence boundary stream)
656  (when endp
657  (fast-write-sequence (string-to-octets "--") stream))
658  (crlf))
659  (crlf () (fast-write-sequence +crlf+ stream)))
660  (loop for (key . val) in content
661  do (boundary-line)
662  (fast-write-sequence (string-to-octets (content-disposition key val)) stream)
663  (let ((content-type (multipart-value-content-type val)))
664  (when content-type
665  (fast-write-sequence
666  (string-to-octets
667  (format nil "Content-Type: ~A~C~C" content-type #\Return #\Newline))
668  stream)))
669  (crlf)
670  (write-as-octets stream val)
671  (crlf)
672  finally
673  (boundary-line t)))))
674 
675 (defun decompress-body (content-encoding body)
676  (unless content-encoding
677  (return-from decompress-body body))
678 
679  (cond
680  ((string= content-encoding "gzip")
681  (if (streamp body)
682  (chipz:make-decompressing-stream :gzip body)
683  (chipz:decompress nil (chipz:make-dstate :gzip) body)))
684  ((string= content-encoding "deflate")
685  (if (streamp body)
686  (chipz:make-decompressing-stream :zlib body)
687  (chipz:decompress nil (chipz:make-dstate :zlib) body)))
688  (t body)))
689 
690 ;;; connection-cache
691 (defvar *use-connection-pool* t)
692 (defvar *max-active-connections* 8
693  "Allowed number of active connections to all hosts. If you change this,
694  then call (make-new-connection-pool).")
695 
696 (defstruct lru-pool-elt
697  (prev nil :type (or null lru-pool-elt))
698  (next nil :type (or null lru-pool-elt))
699  (elt nil :type t)
700  (key nil :type t)
701  (eviction-callback nil :type (or null function)))
702 
703 ;; An LRU-POOL can have multiple entries for the same key
704 (defstruct lru-pool
705  (lock #+sb-thread (sb-thread:make-mutex :name "connection pool lock")
706  #-sb-thread nil)
707  (hash-table nil :type (or null hash-table)) ;; hash table entries are lists of elements
708  (head nil :type (or null lru-pool-elt)) ;; most recently used is here and it's a doubly-linked-list
709  (tail nil :type (or null lru-pool-elt)) ;; least recently used is here
710  (num-elts 0 :type fixnum)
711  (max-elts 8 :type fixnum))
712 
713 (defun make-connection-pool (&optional (max-active-connections *max-active-connections*))
714  (make-lru-pool :hash-table (make-hash-table :test 'equal) :max-elts max-active-connections))
715 
716 (defvar *connection-pool* nil)
717 
718 (defun make-new-connection-pool (&optional (max-active-connections *max-active-connections*))
719  (clear-connection-pool)
720  (setf *connection-pool* (make-connection-pool max-active-connections)))
721 
722 (defun get-from-lru-pool (lru-pool key)
723  "Takes an element from the LRU-POOL matching KEY. Must be called with LRU-POOL-LOCK held.
724  The element is removed from the pool."
725  (let* ((hash-table (lru-pool-hash-table lru-pool))
726  (possible-elts (gethash key (lru-pool-hash-table lru-pool))))
727  (when possible-elts
728  (let ((remaining-elts (cdr possible-elts)))
729  (if remaining-elts
730  (setf (gethash key hash-table) remaining-elts)
731  (remhash key hash-table)))
732  (let ((elt (car possible-elts)))
733  (let ((prev (lru-pool-elt-prev elt))
734  (next (lru-pool-elt-next elt)))
735  (if prev
736  (setf (lru-pool-elt-next prev) next)
737  (setf (lru-pool-head lru-pool) next))
738  (if next
739  (setf (lru-pool-elt-prev next) prev)
740  (setf (lru-pool-tail lru-pool) prev)))
741  (decf (lru-pool-num-elts lru-pool))
742  (lru-pool-elt-elt elt)))))
743 
744 (defun evict-tail (lru-pool)
745  "Removes the least recently used element of the LRU-POOL and returns
746  (values evicted-element eviction-callback t) if there was
747  an element to remove, otherwise nil. Must be called with LRU-POOL-LOCK held.
748 
749  Outside the LRU-POOL-LOCK you must call the returned EVICTION-CALLBACK with the EVICTED-ELEMENT."
750  ;; slightly different from get-from-lru-pool because we want to get rid of the
751  ;; actual oldest element (one could in principle call get-from-lru-pool on
752  ;; (lru-pool-elt-key (lru-pool-tail lru-pool)) if you didn't care
753  (let* ((tail (lru-pool-tail lru-pool)))
754  (when tail
755  (let ((prev (lru-pool-elt-prev tail)))
756  (if prev
757  (setf (lru-pool-elt-next prev) nil)
758  (setf (lru-pool-head lru-pool) nil))
759  (setf (lru-pool-tail lru-pool) prev)
760  (let* ((hash-table (lru-pool-hash-table lru-pool))
761  (key (lru-pool-elt-key tail))
762  (remaining (cl:delete tail (gethash key hash-table))))
763  (if remaining
764  (setf (gethash key hash-table) remaining)
765  (remhash key hash-table))))
766  (decf (lru-pool-num-elts lru-pool))
767  (values (lru-pool-elt-elt tail) (lru-pool-elt-eviction-callback tail) t))))
768 
769 (defun add-to-lru-pool (lru-pool key elt eviction-callback)
770  "Adds ELT to an LRU-POOL with potentially non-unique KEY, potentially evicting another element to
771  make room. EVICTION-CALLBACK will be called with one parameter ELT, when ELT is evicted from the
772  LRU-POOL. ADD-TO-LRU-POOL must be called with LRU-POOL-LOCK held.
773 
774  If an element was evicted to make space, returns (values evicted-elt eviction-callback t)
775  otherwise nil. The EVICTION-CALLBACK should take one parameter, the evicted element."
776  (declare (type lru-pool lru-pool))
777  (let* ((old-head (lru-pool-head lru-pool))
778  (lru-pool-elt (make-lru-pool-elt :prev nil :next old-head :elt elt :key key :eviction-callback eviction-callback))
779  (hash-table (lru-pool-hash-table lru-pool)))
780  (setf (lru-pool-head lru-pool) lru-pool-elt)
781  (push lru-pool-elt (gethash key hash-table))
782  (when old-head
783  (setf (lru-pool-elt-prev old-head) lru-pool-elt))
784  (unless (lru-pool-tail lru-pool)
785  (setf (lru-pool-tail lru-pool) lru-pool-elt))
786  (when (> (incf (lru-pool-num-elts lru-pool)) (lru-pool-max-elts lru-pool))
787  (evict-tail lru-pool))))
788 
789 (defmethod print-object ((obj lru-pool-elt) str) ;; avoid printing loops
790  (print-unreadable-object (obj str :type "LRU-POOL-ELT")
791  (format str "~A NEXT ~A" (lru-pool-elt-key obj) (lru-pool-elt-next obj))))
792 
793 (defmethod print-object ((obj lru-pool) str) ;; avoid printing loops
794  (print-unreadable-object (obj str :type "LRU-POOL")
795  (let (objs)
796  (loop with lru-pool-elt = (lru-pool-head obj)
797  while lru-pool-elt
798  do (push (list (lru-pool-elt-key lru-pool-elt) (lru-pool-elt-elt lru-pool-elt)) objs)
799  do (setf lru-pool-elt (lru-pool-elt-next lru-pool-elt)))
800  (if objs
801  (format str "~A/~A elts~%~{ ~{~A~^: ~}~^~%~}" (lru-pool-num-elts obj) (lru-pool-max-elts obj) objs)
802  (format str "empty")))))
803 
804 (defmacro with-lock (lock &body body)
805  #+thread-support `(sb-thread:with-mutex (,lock)
806  ,@body)
807  #-thread-support `(progn ,@body))
808 
809 (defun push-connection (host-port stream &optional eviction-callback)
810  "Add STREAM back to connection pool with key HOST-PORT. EVICTION-CALLBACK
811  must be a function of a single parameter, and will be called with STREAM
812  if the HOST-PORT/SOCKET pair is evicted from the connection pool."
813  (when *use-connection-pool*
814  (let ((pool *connection-pool*))
815  (multiple-value-bind (evicted-elt eviction-callback)
816  (with-lock (lru-pool-lock pool)
817  (add-to-lru-pool pool host-port stream eviction-callback))
818  (and eviction-callback (funcall eviction-callback evicted-elt))
819  (values)))))
820 
821 (defun steal-connection (host-port)
822  "Return the STREAM associated with key HOST-PORT"
823  (when *use-connection-pool*
824  (let ((pool *connection-pool*))
825  (with-lock (lru-pool-lock pool)
826  (get-from-lru-pool pool host-port)))))
827 
828 (defun clear-connection-pool ()
829  "Remove all elements from the connection pool, calling their eviction-callbacks."
830  (when *use-connection-pool*
831  (let ((pool *connection-pool*)
832  evicted-element eviction-callback element-was-evicted)
833  (when pool
834  (loop for count from 0
835  do (setf (values evicted-element eviction-callback element-was-evicted)
836  (with-lock (lru-pool-lock pool)
837  (evict-tail pool)))
838  do (when eviction-callback (funcall eviction-callback evicted-element))
839  while element-was-evicted)))))
840 
841 (make-new-connection-pool)
842 
843 ;;; backend
844 (with-compilation-unit ()
845 (defparameter *ca-bundle*
846  (uiop:native-namestring #P"/etc/ssl/cacert.pem")
847  "The default public root certificates used in requests.")
848 
849 
850 (defun read-until-crlf*2 (stream)
851  (fast-io:with-fast-output (buf)
852  (tagbody
853  read-cr
854  (loop for byte of-type (or (unsigned-byte 8) null) = (read-byte stream nil nil)
855  if byte
856  do (fast-io:fast-write-byte byte buf)
857  else
858  do (go eof)
859  until (= byte (char-code #\Return)))
860 
861  read-lf
862  (let ((next-byte (read-byte stream nil nil)))
863  (unless next-byte
864  (go eof))
865  (locally (declare (type (unsigned-byte 8) next-byte))
866  (cond
867  ((= next-byte (char-code #\Newline))
868  (fast-io:fast-write-byte next-byte buf)
869  (go read-cr2))
870  ((= next-byte (char-code #\Return))
871  (fast-io:fast-write-byte next-byte buf)
872  (go read-lf))
873  (T
874  (fast-io:fast-write-byte next-byte buf)
875  (go read-cr)))))
876 
877  read-cr2
878  (let ((next-byte (read-byte stream nil nil)))
879  (unless next-byte
880  (go eof))
881  (locally (declare (type (unsigned-byte 8) next-byte))
882  (cond
883  ((= next-byte (char-code #\Return))
884  (fast-io:fast-write-byte next-byte buf)
885  (go read-lf2))
886  (T
887  (fast-io:fast-write-byte next-byte buf)
888  (go read-cr)))))
889 
890  read-lf2
891  (let ((next-byte (read-byte stream nil nil)))
892  (unless next-byte
893  (go eof))
894  (locally (declare (type (unsigned-byte 8) next-byte))
895  (cond
896  ((= next-byte (char-code #\Newline))
897  (fast-io:fast-write-byte next-byte buf))
898  ((= next-byte (char-code #\Return))
899  (fast-io:fast-write-byte next-byte buf)
900  (go read-lf))
901  (T
902  (fast-io:fast-write-byte next-byte buf)
903  (go read-cr)))))
904 
905  eof)))
906 
907 (defvar +empty-body+
908  (make-array 0 :element-type '(unsigned-byte 8)))
909 
910 (defun read-response (stream has-body collect-headers read-body)
911  (let* ((http (make-http-response))
912  body
913  body-data
914  (headers-data (and collect-headers
915  (fast-io:make-output-buffer)))
916  (header-finished-p nil)
917  (finishedp nil)
918  (content-length nil)
919  (transfer-encoding-p)
920  (parser (make-parser http
921  :header-callback
922  (lambda (headers)
923  (setq header-finished-p t
924  content-length (gethash "content-length" headers)
925  transfer-encoding-p (gethash "transfer-encoding" headers))
926  (unless (and has-body
927  (or content-length
928  transfer-encoding-p))
929  (setq finishedp t)))
930  :body-callback
931  (lambda (data start end)
932  (when body-data
933  (fast-io:fast-write-sequence data body-data start end)))
934  :finish-callback
935  (lambda ()
936  (setq finishedp t)))))
937  (let ((buf (read-until-crlf*2 stream)))
938  (declare (type octet-vector buf))
939  (when collect-headers
940  (fast-io:fast-write-sequence buf headers-data))
941  (funcall parser buf))
942  (unless header-finished-p
943  (error "maybe invalid header"))
944  (cond
945  ((not read-body)
946  (setq body stream))
947  ((not has-body)
948  (setq body +empty-body+))
949  ((and content-length (not transfer-encoding-p))
950  (let ((buf (make-array (etypecase content-length
951  (integer content-length)
952  (string (parse-integer content-length)))
953  :element-type '(unsigned-byte 8))))
954  (read-sequence buf stream)
955  (setq body buf)))
956  ((let ((status (http-status http)))
957  (or (= status 100) ;; Continue
958  (= status 101) ;; Switching Protocols
959  (= status 204) ;; No Content
960  (= status 304))) ;; Not Modified
961  (setq body +empty-body+))
962  (T
963  (setq body-data (fast-io:make-output-buffer))
964  (loop for buf of-type octet-vector = (read-until-crlf*2 stream)
965  do (funcall parser buf)
966  until (or finishedp
967  (zerop (length buf)))
968  finally
969  (setq body (fast-io:finish-output-buffer body-data)))))
970  (values http
971  body
972  (and collect-headers
973  (fast-io:finish-output-buffer headers-data))
974  transfer-encoding-p)))
975 
976 (defun print-verbose-data (direction &rest data)
977  (flet ((boundary-line ()
978  (let ((char (ecase direction
979  (:incoming #\<)
980  (:outgoing #\>))))
981  (fresh-line)
982  (dotimes (i 50)
983  (write-char char))
984  (fresh-line))))
985  (boundary-line)
986  (dolist (d data)
987  (map nil (lambda (byte)
988  (princ (code-char byte)))
989  d))
990  (boundary-line)))
991 
992 (defun convert-body (body content-encoding content-type content-length chunkedp force-binary force-string keep-alive-p on-close)
993  (when (streamp body)
994  (cond
995  ((and keep-alive-p chunkedp)
996  (setf body (make-keep-alive-stream body :chunked-stream
997  (let ((chunked-stream (chunga:make-chunked-stream body)))
998  (setf (chunga:chunked-stream-input-chunking-p chunked-stream) t)
999  chunked-stream) :on-close-or-eof on-close)))
1000  ((and keep-alive-p content-length)
1001  (setf body (make-keep-alive-stream body :end content-length :on-close-or-eof on-close)))
1002  (chunkedp
1003  (let ((chunked-stream (chunga:make-chunked-stream body)))
1004  (setf (chunga:chunked-stream-input-chunking-p chunked-stream) t)
1005  (setf body chunked-stream)))))
1006  (let ((body (decompress-body content-encoding body)))
1007  (if force-binary
1008  body
1009  (decode-body content-type body
1010  :default-charset (if force-string
1011  babel:*default-character-encoding*
1012  nil)))))
1013 
1014 (defun content-disposition (key val)
1015  (if (pathnamep val)
1016  (let* ((filename (file-namestring val))
1017  (utf8-filename-p (find-if (lambda (char)
1018  (< 127 (char-code char)))
1019  filename)))
1020  (format nil "Content-Disposition: form-data; name=\"~A\"; ~:[filename=\"~A\"~;filename*=UTF-8''~A~]~C~C"
1021  key
1022  utf8-filename-p
1023  (if utf8-filename-p
1024  (obj/uri:parse-uri filename)
1025  filename)
1026  #\Return #\Newline))
1027  (format nil "Content-Disposition: form-data; name=\"~A\"~C~C"
1028  key
1029  #\Return #\Newline)))
1030 
1031 (defun build-cookie-headers (uri cookie-jar)
1032  (with-header-output (buffer)
1033  (let ((cookies (cookie-jar-host-cookies cookie-jar (uri-host uri) (or (uri-path uri) "/")
1034  :securep (string= (uri-scheme uri) "https"))))
1035  (when cookies
1036  (fast-io:fast-write-sequence (string-to-octets "Cookie: ") buffer)
1037  (fast-io:fast-write-sequence
1038  (string-to-octets (write-cookie-header cookies))
1039  buffer)
1040  (fast-io:fast-write-sequence +crlf+ buffer)))))
1041 
1042 (defun make-connect-stream (uri version stream &optional proxy-auth)
1043  (let ((header (fast-io:with-fast-output (buffer)
1044  (write-connect-header uri version buffer proxy-auth))))
1045  (write-sequence header stream)
1046  (force-output stream)
1047  (read-until-crlf*2 stream)
1048  stream))
1049 
1050 (defun make-proxy-authorization (uri)
1051  (let ((proxy-auth (quri:uri-userinfo uri)))
1052  (when proxy-auth
1053  (format nil "Basic ~A"
1054  (cl-base64:string-to-base64-string proxy-auth)))))
1055 
1056 (defconstant +socks5-version+ 5)
1057 (defconstant +socks5-reserved+ 0)
1058 (defconstant +socks5-no-auth+ 0)
1059 (defconstant +socks5-connect+ 1)
1060 (defconstant +socks5-domainname+ 3)
1061 (defconstant +socks5-succeeded+ 0)
1062 (defconstant +socks5-ipv4+ 1)
1063 (defconstant +socks5-ipv6+ 4)
1064 
1065 (defun ensure-socks5-connected (input output uri http-method)
1066  (labels ((fail (condition &key reason)
1067  (error (make-condition condition
1068  :body nil :status nil :headers nil
1069  :uri uri
1070  :method http-method
1071  :reason reason)))
1072  (exact (n reason)
1073  (unless (eql n (read-byte input nil 'eof))
1074  (fail 'socks5-proxy-request-failed :reason reason)))
1075  (drop (n reason)
1076  (dotimes (i n)
1077  (when (eq (read-byte input nil 'eof) 'eof)
1078  (fail 'socks5-proxy-request-failed :reason reason)))))
1079  ;; Send Version + Auth Method
1080  ;; Currently, only supports no-auth method.
1081  (write-byte +socks5-version+ output)
1082  (write-byte 1 output)
1083  (write-byte +socks5-no-auth+ output)
1084  (finish-output output)
1085 
1086  ;; Receive Auth Method
1087  (exact +socks5-version+ "Unexpected version")
1088  (exact +socks5-no-auth+ "Unsupported auth method")
1089 
1090  ;; Send domainname Request
1091  (let* ((host (babel:string-to-octets (uri-host uri)))
1092  (hostlen (length host))
1093  (port (uri-port uri)))
1094  (unless (<= 1 hostlen 255)
1095  (fail 'socks5-proxy-request-failed :reason "domainname too long"))
1096  (unless (<= 1 port 65535)
1097  (fail 'socks5-proxy-request-failed :reason "Invalid port"))
1098  (write-byte +socks5-version+ output)
1099  (write-byte +socks5-connect+ output)
1100  (write-byte +socks5-reserved+ output)
1101  (write-byte +socks5-domainname+ output)
1102  (write-byte hostlen output)
1103  (write-sequence host output)
1104  (write-byte (ldb (byte 8 8) port) output)
1105  (write-byte (ldb (byte 8 0) port) output)
1106  (finish-output output)
1107 
1108  ;; Receive reply
1109  (exact +socks5-version+ "Unexpected version")
1110  (exact +socks5-succeeded+ "Unexpected result code")
1111  (drop 1 "Should be reserved byte")
1112  (let ((atyp (read-byte input nil 'eof)))
1113  (cond
1114  ((eql atyp +socks5-ipv4+)
1115  (drop 6 "Should be IPv4 address and port"))
1116  ((eql atyp +socks5-ipv6+)
1117  (drop 18 "Should be IPv6 address and port"))
1118  ((eql atyp +socks5-domainname+)
1119  (let ((n (read-byte input nil 'eof)))
1120  (when (eq n 'eof)
1121  (fail 'socks5-proxy-request-failed :reason "Invalid domainname length"))
1122  (drop n "Should be domainname and port")))
1123  (t
1124  (fail 'socks5-proxy-request-failed :reason "Unknown address")))))))
1125 
1126 (defun make-ssl-stream (stream ca-path ssl-key-file ssl-cert-file ssl-key-password hostname insecure)
1127  #+(not ssl) (declare (ignore stream ca-path ssl-key-file ssl-cert-file ssl-key-password hostname insecure))
1128  #+(not ssl) (error "SSL not supported. Remove :dexador-no-ssl from *features* to enable SSL.")
1129  #+ssl
1130  (progn
1131  (cl+ssl:ensure-initialized)
1132  (let ((ctx (cl+ssl:make-context :verify-mode
1133  (if insecure
1134  cl+ssl:+ssl-verify-none+
1135  cl+ssl:+ssl-verify-peer+)
1136  :verify-location
1137  ;; TODO 2024-05-22:
1138  (cond
1139  (ca-path (uiop:native-namestring ca-path))
1140  ((probe-file *ca-bundle*) *ca-bundle*)
1141  ;; In executable environment, perhaps *ca-bundle* doesn't exist.
1142  (t :default))))
1143  (ssl-cert-pem-p (and ssl-cert-file
1144  (std/seq:ends-with-subseq ".pem" ssl-cert-file))))
1145  (cl+ssl:with-global-context (ctx :auto-free-p t)
1146  (when ssl-cert-pem-p
1147  (cl+ssl:use-certificate-chain-file ssl-cert-file))
1148  (cl+ssl:make-ssl-client-stream stream
1149  :hostname hostname
1150  :verify (not insecure)
1151  :key ssl-key-file
1152  :certificate (and (not ssl-cert-pem-p)
1153  ssl-cert-file)
1154  :password ssl-key-password)))))
1155 
1156 (defstruct usocket-wrapped-stream
1157  stream)
1158 
1159 ;; Forward methods the user might want to use on this.
1160 ;; User is not meant to interact with this object except
1161 ;; potentially to close it when they decide they don't
1162 ;; need the :keep-alive connection anymore.
1163 (defmethod close ((u usocket-wrapped-stream) &key abort)
1164  (close (usocket-wrapped-stream-stream u) :abort abort))
1165 
1166 (defmethod open-stream-p ((u usocket-wrapped-stream))
1167  (open-stream-p (usocket-wrapped-stream-stream u)))
1168 
1169 (defun request (uri &rest args
1170  &key (method :get) (version 1.1)
1171  content headers
1172  basic-auth bearer-auth
1173  cookie-jar
1174  (connect-timeout *default-connect-timeout*) (read-timeout *default-read-timeout*)
1175  (keep-alive t) (use-connection-pool t)
1176  (max-redirects 5)
1177  ssl-key-file ssl-cert-file ssl-key-password
1178  stream (verbose *verbose*)
1179  force-binary
1180  force-string
1181  want-stream
1182  (proxy *default-proxy*)
1183  (insecure *no-ssl*)
1184  ca-path
1185  &aux
1186  (proxy-uri (and proxy (quri:uri proxy)))
1187  (original-user-supplied-stream stream)
1188  (user-supplied-stream (if (usocket-wrapped-stream-p stream) (usocket-wrapped-stream-stream stream) stream)))
1189  (declare (ignorable ssl-key-file ssl-cert-file ssl-key-password
1190  connect-timeout)
1191  (type real version)
1192  (type fixnum max-redirects))
1193  (with-content-caches
1194  (labels ((make-new-connection (uri)
1195  (restart-case
1196  (let* ((con-uri (uri (or proxy uri)))
1197  (connection (usocket:socket-connect (uri-host con-uri)
1198  (or (uri-port con-uri) 80)
1199  #-(or ecl clasp clisp allegro) :timeout #-(or ecl clasp clisp allegro) connect-timeout
1200  :element-type '(unsigned-byte 8)))
1201  (stream
1202  (usocket:socket-stream connection))
1203  (scheme (uri-scheme uri)))
1204  (declare (type keyword scheme))
1205  (when read-timeout
1206  #+lispworks(setf (stream:stream-read-timeout stream) read-timeout)
1207  #-lispworks(setf (usocket:socket-option connection :receive-timeout) read-timeout))
1208  (when (socks5-proxy-p proxy-uri)
1209  (ensure-socks5-connected stream stream uri method))
1210  (if (string= (symbol-name scheme) "https")
1211  (make-ssl-stream (if (http-proxy-p proxy-uri)
1212  (make-connect-stream uri version stream (make-proxy-authorization con-uri))
1213  stream) ca-path ssl-key-file ssl-cert-file ssl-key-password (uri-host uri) insecure)
1214  stream))
1215  (retry-request ()
1216  :report "Retry the same request."
1217  (return-from request
1218  (apply #'request uri :use-connection-pool nil args)))
1219  (retry-insecure ()
1220  :report "Retry the same request without checking for SSL certificate validity."
1221  (return-from request
1222  (apply #'request uri :use-connection-pool nil :insecure t args)))))
1223  (http-proxy-p (uri)
1224  (and uri
1225  (let ((scheme (uri-scheme uri)))
1226  (and (stringp scheme)
1227  (or (string= scheme "http")
1228  (string= scheme "https"))))))
1229  (socks5-proxy-p (uri)
1230  (and uri
1231  (let ((scheme (uri-scheme uri)))
1232  (and (stringp scheme)
1233  (string= scheme "socks5")))))
1234  (connection-keep-alive-p (connection-header)
1235  (and keep-alive
1236  (or (and (= (the real version) 1.0)
1237  (equalp connection-header "keep-alive"))
1238  (not (equalp connection-header "close")))))
1239  (return-stream-to-pool (stream uri)
1240  (push-connection (format nil "~A://~A"
1241  (uri-scheme uri)
1242  (uri-authority uri)) stream #'close))
1243  (return-stream-to-pool-or-close (stream connection-header uri)
1244  (if (and (not user-supplied-stream) use-connection-pool (connection-keep-alive-p connection-header))
1245  (return-stream-to-pool stream uri)
1246  (when (open-stream-p stream)
1247  (close stream))))
1248  (finalize-connection (stream connection-header uri)
1249  "If KEEP-ALIVE is in the connection-header and the user is not requesting a stream,
1250  we will push the connection to our connection pool if allowed, otherwise we return
1251  the stream back to the user who must close it."
1252  (unless want-stream
1253  (cond
1254  ((and use-connection-pool (connection-keep-alive-p connection-header) (not user-supplied-stream))
1255  (return-stream-to-pool stream uri))
1256  ((not (connection-keep-alive-p connection-header))
1257  (when (open-stream-p stream)
1258  (close stream)))))))
1259  (let* ((uri (uri uri))
1260  (proxy (when (http-proxy-p proxy-uri) proxy))
1261  (content-type (cdr (find :content-type headers :key #'car :test #'string-equal)))
1262  (multipart-p (or (and content-type
1263  (>= (length content-type) 10)
1264  (string= content-type "multipart/" :end1 10))
1265  (and (not content-type)
1266  (consp content)
1267  (find-if #'pathnamep content :key #'cdr))))
1268  (form-urlencoded-p (or (string= content-type "application/x-www-form-urlencoded")
1269  (and (not content-type)
1270  (consp content)
1271  (not multipart-p))))
1272  (boundary (and multipart-p
1273  (make-random-string 12)))
1274  (content (if (and form-urlencoded-p (not (stringp content))) ;; user can provide already encoded content, trust them.
1275  (quri:url-encode-params content)
1276  content))
1277  (stream (or user-supplied-stream
1278  (and use-connection-pool
1279  (steal-connection (format nil "~A://~A"
1280  (uri-scheme uri)
1281  (uri-authority uri))))))
1282  (reusing-stream-p (not (null stream))) ;; user provided or from connection-pool
1283  (stream (or stream
1284  (make-new-connection uri)))
1285  (content-length
1286  (assoc :content-length headers :test #'string-equal))
1287  (transfer-encoding
1288  (assoc :transfer-encoding headers :test #'string-equal))
1289  (chunkedp (or (and transfer-encoding
1290  (equalp (cdr transfer-encoding) "chunked"))
1291  (and content-length
1292  (null (cdr content-length)))))
1293  (first-line-data
1294  (fast-io:with-fast-output (buffer)
1295  (write-first-line method uri version buffer)))
1296  (headers-data
1297  (flet ((write-header* (name value)
1298  (let ((header (assoc name headers :test #'string-equal)))
1299  (if header
1300  (when (cdr header)
1301  (write-header name (cdr header)))
1302  (write-header name value)))
1303  (values)))
1304  (with-header-output (buffer)
1305  (write-header* :user-agent #.*default-user-agent*)
1306  (write-header* :host (uri-authority uri))
1307  (write-header* :accept "*/*")
1308  (cond
1309  ((and keep-alive
1310  (= (the real version) 1.0))
1311  (write-header* :connection "keep-alive"))
1312  ((and (not keep-alive)
1313  (= (the real version) 1.1))
1314  (write-header* :connection "close")))
1315  (cond ((and bearer-auth basic-auth)
1316  (error "You should only use one Authorization header."))
1317  (basic-auth
1318  (write-header* :authorization
1319  (format nil "Basic ~A"
1320  (dat/base64::string-to-base64-string
1321  (format nil "~A:~A"
1322  (car basic-auth)
1323  (cdr basic-auth))))))
1324  (bearer-auth
1325  (write-header* :authorization
1326  (format nil "Bearer ~A" bearer-auth))))
1327  (when proxy
1328  (let ((scheme (uri-scheme uri)))
1329  (when (string= scheme "http")
1330  (let* ((uri (uri proxy))
1331  (proxy-authorization (make-proxy-authorization uri)))
1332  (when proxy-authorization
1333  (write-header* :proxy-authorization proxy-authorization))))))
1334  (cond
1335  (multipart-p
1336  (write-header :content-type (format nil "~A; boundary=~A"
1337  (or content-type "multipart/form-data")
1338  boundary))
1339  (unless chunkedp
1340  (write-header :content-length
1341  (multipart-content-length content boundary))))
1342  (form-urlencoded-p
1343  (write-header* :content-type "application/x-www-form-urlencoded")
1344  (unless chunkedp
1345  (write-header* :content-length (length (the string content)))))
1346  (t
1347  (etypecase content
1348  (null
1349  (unless chunkedp
1350  (write-header* :content-length 0)))
1351  (string
1352  (write-header* :content-type (or content-type "text/plain"))
1353  (unless chunkedp
1354  (write-header* :content-length (content-length content))))
1355  ((array (unsigned-byte 8) *)
1356  (write-header* :content-type (or content-type "text/plain"))
1357  (unless chunkedp
1358  (write-header* :content-length (length content))))
1359  (pathname
1360  (write-header* :content-type (or content-type (content-type content)))
1361  (unless chunkedp
1362  (write-header :content-length
1363  (or (cdr (assoc :content-length headers :test #'string-equal))
1364  (content-length content))))))))
1365  ;; Transfer-Encoding: chunked
1366  (when (and chunkedp
1367  (not transfer-encoding))
1368  (write-header* :transfer-encoding "chunked"))
1369 
1370  ;; Custom headers
1371  (loop for (name . value) in headers
1372  unless (member name '(:user-agent :host :accept
1373  :connection
1374  :content-type :content-length) :test #'string-equal)
1375  do (write-header name value)))))
1376  (cookie-headers (and cookie-jar
1377  (build-cookie-headers uri cookie-jar))))
1378  (macrolet ((maybe-try-again-without-reusing-stream (&optional (force nil))
1379  `(progn ;; retrying by go retry avoids generating the header, parsing, etc.
1380  (when (open-stream-p stream)
1381  (close stream :abort t)
1382  (setf stream nil))
1383 
1384  (when ,(or force 'reusing-stream-p)
1385  (setf reusing-stream-p nil
1386  user-supplied-stream nil
1387  stream (make-new-connection uri))
1388  (go retry))))
1389  (try-again-without-reusing-stream ()
1390  `(maybe-try-again-without-reusing-stream t))
1391  (with-retrying (&body body)
1392  `(restart-case
1393  (handler-bind (((and error
1394  ;; We should not retry errors received from the server.
1395  ;; Only technical errors such as disconnection or some
1396  ;; problems with the protocol should be retried automatically.
1397  ;; This solves https://github.com/fukamachi/dexador/issues/137 issue.
1398  (not http-request-failed))
1399  (lambda (e)
1400  (declare (ignorable e))
1401  (maybe-try-again-without-reusing-stream))))
1402  ,@body)
1403  (retry-request () :report "Retry the same request."
1404  (return-from request (apply #'request uri args)))
1405  (ignore-and-continue () :report "Ignore the error and continue."))))
1406  (tagbody
1407  retry
1408 
1409  (unless (open-stream-p stream)
1410  (try-again-without-reusing-stream))
1411 
1412  (with-retrying
1413  (write-sequence first-line-data stream)
1414  (write-sequence headers-data stream)
1415  (when cookie-headers
1416  (write-sequence cookie-headers stream))
1417  (write-sequence +crlf+ stream)
1418  (force-output stream))
1419 
1420  ;; Sending the content
1421  (when content
1422  (let ((stream (if chunkedp
1423  (chunga:make-chunked-stream stream)
1424  stream)))
1425  (when chunkedp
1426  (setf (chunga:chunked-stream-output-chunking-p stream) t))
1427  (with-retrying
1428  (if (consp content)
1429  (write-multipart-content content boundary stream)
1430  (write-as-octets stream content))
1431  (when chunkedp
1432  (setf (chunga:chunked-stream-output-chunking-p stream) nil))
1433  (finish-output stream))))
1434 
1435  start-reading
1436  (multiple-value-bind (http body response-headers-data transfer-encoding-p)
1437  (with-retrying
1438  (read-response stream (not (eq method :head)) verbose (not want-stream)))
1439  (let* ((status (http-status http))
1440  (response-headers (http-headers http))
1441  (content-length (gethash "content-length" response-headers))
1442  (content-length (etypecase content-length
1443  (null content-length)
1444  (string (parse-integer content-length))
1445  (integer content-length))))
1446  (when (= status 0)
1447  (with-retrying
1448  (http-request-failed status
1449  :body body
1450  :headers headers
1451  :uri uri
1452  :method method)))
1453  (when verbose
1454  (print-verbose-data :outgoing first-line-data headers-data cookie-headers +crlf+)
1455  (print-verbose-data :incoming response-headers-data))
1456  (when cookie-jar
1457  (when-let ((set-cookies (append (gethash "set-cookie" response-headers)
1458  (ensure-list (gethash "set-cookie2" response-headers)))))
1459  (net/cookie::merge-cookies cookie-jar
1460  (remove nil (mapcar (lambda (cookie)
1461  (declare (type string cookie))
1462  (unless (= (length cookie) 0)
1463  (net/cookie:parse-set-cookie-header cookie
1464  (uri-host uri)
1465  (uri-path uri))))
1466  set-cookies)))))
1467  (when (and (member status '(301 302 303 307 308) :test #'=)
1468  (gethash "location" response-headers)
1469  (/= max-redirects 0))
1470  ;; Need to read the response body
1471  (when (and want-stream
1472  (not (eq method :head)))
1473  (cond
1474  ((integerp content-length)
1475  (dotimes (i content-length)
1476  (loop until (read-byte body nil nil))))
1477  (transfer-encoding-p
1478  (read-until-crlf*2 body))))
1479 
1480  (let* ((location-uri (uri (gethash "location" response-headers)))
1481  (same-server-p (or (null (uri-host location-uri))
1482  (and (string= (uri-scheme location-uri)
1483  (uri-scheme uri))
1484  (string= (uri-host location-uri)
1485  (uri-host uri))
1486  (eql (uri-port location-uri)
1487  (uri-port uri))))))
1488  (if (and same-server-p
1489  (or (= status 307) (= status 308)
1490  (member method '(:get :head) :test #'eq)))
1491  (progn ;; redirection to the same host
1492  (setq uri (merge-uris location-uri uri))
1493  (setq first-line-data
1494  (fast-io:with-fast-output (buffer)
1495  (write-first-line method uri version buffer)))
1496  (when cookie-jar
1497  ;; Rebuild cookie-headers.
1498  (setq cookie-headers (build-cookie-headers uri cookie-jar)))
1499  (decf max-redirects)
1500  (if (equalp (gethash "connection" response-headers) "close")
1501  (try-again-without-reusing-stream)
1502  (progn
1503  (setq reusing-stream-p t)
1504  (go retry))))
1505  (progn ;; this is a redirection to a different host
1506  (setf location-uri (merge-uris location-uri uri))
1507  ;; Close connection if it isn't from our connection pool or from the user and we aren't going to
1508  ;; pass it to our new call.
1509  (when (not same-server-p) (return-stream-to-pool-or-close stream (gethash "connection" response-headers) uri))
1510  (setf (getf args :headers)
1511  (nconc `((:host . ,(uri-host location-uri))) headers))
1512  (setf (getf args :max-redirects)
1513  (1- max-redirects))
1514  ;; Redirect as GET if it's 301, 302, 303
1515  (unless (or (= status 307) (= status 308)
1516  (member method '(:get :head) :test #'eq))
1517  (setf (getf args :method) :get))
1518  (return-from request
1519  (apply #'request location-uri (if same-server-p
1520  args
1521  (progn (remf args :stream) args))))))))
1522  (unwind-protect
1523  (let* ((keep-connection-alive (connection-keep-alive-p
1524  (gethash "connection" response-headers)))
1525  (body (convert-body body
1526  (gethash "content-encoding" response-headers)
1527  (gethash "content-type" response-headers)
1528  content-length
1529  transfer-encoding-p
1530  force-binary
1531  force-string
1532  keep-connection-alive
1533  (if (and use-connection-pool keep-connection-alive (not user-supplied-stream) (streamp body))
1534  (lambda (underlying-stream abort)
1535  (declare (ignore abort))
1536  (when (and underlying-stream (open-stream-p underlying-stream))
1537  ;; read any left overs the user may have not read (in case of errors on user side?)
1538  (loop while (ignore-errors (listen underlying-stream)) ;; ssl streams may close
1539  do (read-byte underlying-stream nil nil))
1540  (when (open-stream-p underlying-stream)
1541  (push-connection (format nil "~A://~A"
1542  (uri-scheme uri)
1543  (uri-authority uri)) underlying-stream #'close))))
1544  #'keep-alive-stream-close-underlying-stream))))
1545  ;; Raise an error when the HTTP response status code is 4xx or 50x.
1546  (when (<= 400 status)
1547  (with-retrying
1548  (http-request-failed status
1549  :body body
1550  :headers response-headers
1551  :uri uri
1552  :method method)))
1553  ;; Have to be a little careful with the fifth value stream we return --
1554  ;; the user may be not aware that keep-alive t without use-connection-pool can leak
1555  ;; sockets, so we wrap the returned last value so when it is garbage
1556  ;; collected it gets closed. If the user is getting a stream back as BODY,
1557  ;; then we instead add a finalizer to that stream to close it when garbage collected
1558  (return-from request
1559  (values body
1560  status
1561  response-headers
1562  uri
1563  (when (and keep-alive
1564  (not (equalp (gethash "connection" response-headers) "close"))
1565  (or (not use-connection-pool) user-supplied-stream))
1566  (or (and original-user-supplied-stream ;; user provided a stream
1567  (if (usocket-wrapped-stream-p original-user-supplied-stream) ;; but, it came from us
1568  (eql (usocket-wrapped-stream-stream original-user-supplied-stream) stream) ;; and we used it
1569  (eql original-user-supplied-stream stream)) ;; user provided a bare stream
1570  original-user-supplied-stream) ;; return what the user sent without wrapping it
1571  (if want-stream ;; add a finalizer to the body to close the stream
1572  (progn
1573  (trivial-garbage:finalize body (lambda () (close stream)))
1574  stream)
1575  (let ((wrapped-stream (make-usocket-wrapped-stream :stream stream)))
1576  (trivial-garbage:finalize wrapped-stream (lambda () (close stream)))
1577  wrapped-stream)))))))
1578  (finalize-connection stream (gethash "connection" response-headers) uri)))))))))))
1579 
1580 ;;; API
1581 (defun get (uri &rest args
1582  &key version headers basic-auth bearer-auth cookie-jar keep-alive use-connection-pool
1583  connect-timeout read-timeout max-redirects
1584  force-binary force-string want-stream content
1585  ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path)
1586  "Make a GET request to URI and return
1587  (values body-or-stream status response-headers uri &optional opaque-socket-stream)
1588 
1589  You may pass a real stream in as STREAM if you want us to communicate with the server via it --
1590  though if any errors occur, we will open a new connection to the server. If you have a previous
1591  OPAQUE-SOCKET-STREAM you can pass that in as STREAM as well and we will re-use that connection.
1592 
1593  OPAQUE-SOCKET-STREAM is not returned if USE-CONNECTION-POOL is T, instead we keep track of it and
1594  re-use it when needed.
1595 
1596  If WANT-STREAM is T, then a STREAM is returned as the first value. You may read this as needed to
1597  get the body of the response. If KEEP-ALIVE and USE-CONNECTION-POOL are T, then the stream will be
1598  returned to the connection pool when you have read all the data or closed the stream. If KEEP-ALIVE
1599  is NIL then you are responsible for closing the stream when done.
1600 
1601  If KEEP-ALIVE is T and USE-CONNECTION-POOL is NIL, then the fifth value returned is a stream which
1602  you can then pass in again using the STREAM option to re-use the active connection. If you ignore
1603  the stream, it will get closed during garbage collection.
1604 
1605  If KEEP-ALIVE is T and USE-CONNECTION-POOL is T, then there is no fifth
1606  value (OPAQUE-SOCKET-STREAM) returned, but the active connection to the host/port may be reused in
1607  subsequent calls. This removes the need for the caller to keep track of the active socket-stream
1608  for subsequent calls.
1609 
1610  While CONTENT is allowed in a GET request the results are ill-defined and not advised."
1611  (declare (ignore version headers basic-auth bearer-auth cookie-jar keep-alive use-connection-pool
1612  connect-timeout read-timeout max-redirects force-binary force-string want-stream
1613  ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path content))
1614  (apply #'request uri :method :get args))
1615 
1616 (defun post (uri &rest args
1617  &key version content headers basic-auth bearer-auth cookie-jar keep-alive
1618  use-connection-pool connect-timeout read-timeout
1619  force-binary force-string want-stream
1620  ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path)
1621  (declare (ignore version content headers basic-auth bearer-auth cookie-jar keep-alive
1622  use-connection-pool connect-timeout read-timeout force-binary force-string
1623  want-stream ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy
1624  insecure ca-path))
1625  (apply #'request uri :method :post args))
1626 
1627 (defun head (uri &rest args
1628  &key version headers basic-auth bearer-auth cookie-jar connect-timeout read-timeout max-redirects
1629  ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path)
1630  (declare (ignore version headers basic-auth bearer-auth cookie-jar connect-timeout read-timeout
1631  max-redirects ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path))
1632  (apply #'request uri :method :head :use-connection-pool nil args))
1633 
1634 (defun put (uri &rest args
1635  &key version content headers basic-auth bearer-auth cookie-jar keep-alive
1636  use-connection-pool connect-timeout read-timeout
1637  force-binary force-string want-stream
1638  ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path)
1639  (declare (ignore version content headers basic-auth bearer-auth cookie-jar keep-alive
1640  use-connection-pool connect-timeout read-timeout force-binary force-string
1641  want-stream ssl-key-file ssl-cert-file ssl-key-password stream verbose
1642  proxy insecure ca-path))
1643  (apply #'request uri :method :put args))
1644 
1645 (defun patch (uri &rest args
1646  &key version content headers basic-auth bearer-auth cookie-jar keep-alive
1647  use-connection-pool connect-timeout read-timeout
1648  force-binary force-string want-stream
1649  ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path)
1650  (declare (ignore version content headers basic-auth bearer-auth cookie-jar keep-alive
1651  use-connection-pool connect-timeout read-timeout force-binary force-string
1652  want-stream ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy
1653  insecure ca-path))
1654  (apply #'request uri :method :patch args))
1655 
1656 (defun delete (uri &rest args
1657  &key version headers basic-auth bearer-auth cookie-jar keep-alive
1658  use-connection-pool connect-timeout read-timeout
1659  force-binary force-string want-stream content
1660  ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path)
1661  (declare (ignore version headers basic-auth bearer-auth cookie-jar keep-alive use-connection-pool
1662  connect-timeout read-timeout force-binary force-string want-stream ssl-key-file
1663  ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path content))
1664  (apply #'request uri :method :delete args))
1665 
1666 (defun fetch (uri destination &rest args
1667  &key (if-exists :error)
1668  version headers basic-auth bearer-auth cookie-jar keep-alive use-connection-pool
1669  connect-timeout read-timeout max-redirects
1670  ssl-key-file ssl-cert-file ssl-key-password stream verbose proxy insecure ca-path)
1671  (declare (ignore version headers basic-auth bearer-auth cookie-jar keep-alive use-connection-pool
1672  connect-timeout read-timeout max-redirects ssl-key-file ssl-cert-file
1673  ssl-key-password stream verbose proxy insecure ca-path))
1674  (unless (and (eql if-exists nil)
1675  (probe-file destination))
1676  (with-open-file (out destination
1677  :direction :output :element-type '(unsigned-byte 8)
1678  :if-exists if-exists
1679  :if-does-not-exist :create)
1680  (remf args :if-exists)
1681  (let ((body (apply #'dex:get uri :want-stream t :force-binary t
1682  args)))
1683  (alexandria:copy-stream body out)
1684  ;; Nominally the body gets closed, but if keep-alive is nil we need to explicitly do it.
1685  (when (open-stream-p body)
1686  (close body))))))
1687 
1688 (defun ignore-and-continue (e)
1689  (let ((restart (find-restart 'ignore-and-continue e)))
1690  (when restart
1691  (invoke-restart restart))))
1692 
1693 (defun retry-request (times &key (interval 3))
1694  (declare (type (or function integer) interval))
1695  (etypecase times
1696  (condition
1697  (let ((restart (find-restart 'retry-request times)))
1698  (when restart
1699  (invoke-restart restart))))
1700  (integer
1701  (retry-request-ntimes times :interval interval))))
1702 
1703 (defun retry-request-ntimes (n &key (interval 3))
1704  (declare (type integer n)
1705  (type (or function integer) interval))
1706  (let ((retries 0))
1707  (declare (type integer retries))
1708  (lambda (e)
1709  (declare (type condition e))
1710  (let ((restart (find-restart 'retry-request e)))
1711  (when restart
1712  (when (< retries n)
1713  (incf retries)
1714  (etypecase interval
1715  (function (funcall interval retries))
1716  (integer (sleep interval)))
1717  (invoke-restart restart)))))))