2 ;;;; Copyright (c) 2008, 2015 Zachary Beane, All Rights Reserved
4 ;;;; Redistribution and use in source and binary forms, with or without
5 ;;;; modification, are permitted provided that the following conditions
8 ;;;; * Redistributions of source code must retain the above copyright
9 ;;;; notice, this list of conditions and the following disclaimer.
11 ;;;; * Redistributions in binary form must reproduce the above
12 ;;;; copyright notice, this list of conditions and the following
13 ;;;; disclaimer in the documentation and/or other materials
14 ;;;; provided with the distribution.
16 ;;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
17 ;;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 ;;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 ;;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
20 ;;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 ;;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
22 ;;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 ;;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
24 ;;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25 ;;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 ;;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 (defvar *s3-endpoint
* "s3.amazonaws.com")
33 (defvar *s3-region
* "us-east-1")
34 (defvar *use-ssl
* nil
)
35 (defvar *use-content-md5
* t
)
36 (defvar *signed-payload
* nil
37 "When true, compute the SHA256 hash for the body of all requests
38 when submitting to AWS.")
40 (defvar *use-keep-alive
* nil
41 "When set to t, this library uses the drakma client with
42 keep alive enabled. This means that a stream will be reused for multiple
43 requests. The stream itself will be bound to *keep-alive-stream*")
46 (defvar *keep-alive-stream
* nil
47 "When using http keep-alive, this variable is bound to the stream
48 which is being kept open for repeated usage. It is up to client code
49 to ensure that only one thread at a time is making requests that
50 could use the same stream object concurrently. One way to achive
51 this would be to create a separate binding per thread. The
52 with-keep-alive macro can be useful here.")
55 (defmacro with-keep-alive
(&body body
)
56 "Create thread-local bindings of the zs3 keep-alive variables around a
57 body of code. Ensure the stream is closed at exit."
58 `(let ((*use-keep-alive
* t
)
59 (*keep-alive-stream
* nil
))
62 (when *keep-alive-stream
*
63 (ignore-errors (close *keep-alive-stream
*))))))
70 :documentation
"An object that has methods for ACCESS-KEY and
71 SECRET-KEY. A list of two strings (the keys) suffices.")
84 :documentation
"e.g. :GET, :PUT, :DELETE")
89 "A string naming the bucket to address in the request. If NIL,
90 request is not directed at a specific bucket.")
95 "A string naming the key to address in the request. If NIL,
96 request is not directed at a specific key.")
98 :initarg
:sub-resource
99 :accessor sub-resource
100 :documentation
"A sub-resource to address as part of the request,
101 without a leading \"?\", e.g. \"acl\", \"torrent\". If PARAMETERS
102 is set, this must be NIL.")
107 "An alist of string key/value pairs to send as CGI-style GET
108 parameters with the request. If SUB-RESOURCE is set, these must be
111 :initarg
:content-type
112 :accessor content-type
)
114 :initarg
:content-md5
115 :accessor content-md5
)
117 :initarg
:content-length
118 :accessor content-length
)
126 "An alist of Amazon metadata to attach to a request. These should
127 be straight string key/value pairs, WITHOUT any \"x-amz-meta-\"
130 :initarg
:amz-headers
131 :accessor amz-headers
133 "An alist of extra Amazon request headers. These should be
134 straight string key/value pairs, WITHOUT any \"x-amz-\" prefix.")
139 :initarg
:signed-string
140 :accessor signed-string
)
142 :initarg
:extra-http-headers
143 :accessor extra-http-headers
144 :documentation
"An alist of extra HTTP headers to include in the request."))
146 ;; :date and :content-md5 are specially treated, should not be nil
147 :credentials
*credentials
*
149 :endpoint
*s3-endpoint
*
161 :extra-http-headers nil
))
163 (defmethod slot-unbound ((class t
) (request request
) (slot (eql 'date
)))
164 (setf (date request
) (get-universal-time)))
166 (defmethod slot-unbound ((class t
) (request request
) (slot (eql 'content-md5
)))
167 (setf (content-md5 request
)
168 (and *use-content-md5
*
169 (pathnamep (content request
))
170 (file-md5/b64
(content request
)))))
172 (defmethod slot-unbound ((class t
) (request request
) (slot (eql 'signed-string
)))
173 (setf (signed-string request
)
174 (format nil
"~{~A~^~%~}" (string-to-sign-lines request
))))
176 (defgeneric amz-header-value
(request name
)
177 (:method
(request name
)
178 (cdr (assoc name
(amz-headers request
) :test
'string
=))))
180 (defgeneric ensure-amz-header
(request name value
)
181 (:method
(request name value
)
182 (unless (amz-header-value request name
)
183 (push (cons name value
) (amz-headers request
)))))
185 (defmethod initialize-instance :after
((request request
)
188 (declare (ignore initargs
))
189 (when (eql (method request
) :head
)
190 ;; https://forums.aws.amazon.com/thread.jspa?messageID=340398 -
191 ;; when using the bare endpoint, the 301 redirect for a HEAD
192 ;; request does not include enough info to actually redirect. Use
193 ;; the bucket endpoint pre-emptively instead
194 (setf (endpoint request
) (format nil
"~A.~A"
197 (ensure-amz-header request
"date"
198 (iso8601-basic-timestamp-string (date request
)))
199 (ensure-amz-header request
"content-sha256"
200 (payload-sha256 request
))
201 (let ((target-region (redirected-region (endpoint request
)
204 (setf (region request
) target-region
)))
205 (when (content-md5 request
)
206 (push (cons "Content-MD5" (content-md5 request
)) (extra-http-headers request
)))
207 (unless (integerp (content-length request
))
208 (let ((content (content request
)))
209 (setf (content-length request
)
212 (pathname (file-size content
))
213 (vector (length content
)))))))
215 (defgeneric host
(request)
216 (:method
((request request
))
217 (or (redirected-endpoint (endpoint request
) (bucket request
))
218 (endpoint request
))))
220 (defgeneric http-method
(request)
222 (string-upcase (method request
))))
224 (defun puri-canonicalized-path (path)
225 (let ((parsed (puri:parse-uri
(format nil
"http://dummy~A" path
))))
226 (with-output-to-string (stream)
227 (if (puri:uri-path parsed
)
228 (write-string (puri:uri-path parsed
) stream
)
229 (write-string "/" stream
))
230 (when (puri:uri-query parsed
)
231 (write-string "?" stream
)
232 (write-string (puri:uri-query parsed
) stream
)))))
234 (defgeneric signed-path
(request)
236 (let ((*print-pretty
* nil
))
237 (puri-canonicalized-path
238 (with-output-to-string (stream)
239 (write-char #\
/ stream
)
240 (when (bucket request
)
241 (write-string (url-encode (name (bucket request
))) stream
)
242 (write-char #\
/ stream
))
244 (write-string (url-encode (name (key request
)) :encode-slash nil
)
246 (when (sub-resource request
)
247 (write-string "?" stream
)
248 (write-string (url-encode (sub-resource request
)) stream
)))))))
250 (defgeneric request-path
(request)
252 (let ((*print-pretty
* nil
))
253 (with-output-to-string (stream)
254 (write-char #\
/ stream
)
255 (when (and (bucket request
)
256 (string= (endpoint request
)
257 (region-endpoint (region request
))))
258 (write-string (url-encode (name (bucket request
))) stream
)
259 (write-char #\
/ stream
))
261 (write-string (url-encode (name (key request
))
262 :encode-slash nil
) stream
))
263 (when (sub-resource request
)
264 (write-string "?" stream
)
265 (write-string (url-encode (sub-resource request
)) stream
))))))
267 (defgeneric all-amazon-headers
(request)
270 (loop for
((key . value
)) on
(amz-headers request
)
271 collect
(cons (format nil
"x-amz-~(~A~)" key
)
273 (loop for
((key . value
)) on
(metadata request
)
274 collect
(cons (format nil
"x-amz-meta-~(~A~)" key
)
277 (defgeneric date-string
(request)
279 (http-date-string (date request
))))
281 ;;; AWS 4 authorization
283 (defun headers-for-signing (request)
284 (append (all-amazon-headers request
)
285 (extra-http-headers request
)
286 (parameters-alist "host" (host request
)
287 "content-type" (content-type request
))))
289 (defun canonical-headers (headers)
290 (flet ((trim (string)
291 (string-trim " " string
)))
293 (loop for
(name . value
) in headers
294 collect
(cons (string-downcase name
)
296 (sort encoded
#'string
< :key
'car
))))
298 (defun signed-headers (request)
299 (mapcar 'first
(canonical-headers (headers-for-signing request
))))
301 (defun parameters-for-signing (request)
302 (cond ((sub-resource request
)
303 (list (cons (sub-resource request
) "")))
305 (parameters request
))))
307 (defun canonical-parameters (parameters)
309 (loop for
(name . value
) in parameters
312 (url-encode value
)))))
313 (sort encoded
#'string
< :key
'car
)))
315 (defun canonical-parameters-string (request)
316 (format nil
"~{~A=~A~^&~}"
317 (alist-plist (canonical-parameters
318 (parameters-for-signing request
)))))
320 (defun path-to-sign (request)
321 "Everything in the PATH of the request, up to the first ?"
322 (let ((path (request-path request
)))
323 (subseq path
0 (position #\? path
))))
325 (defun canonicalized-request-lines (request)
326 "Return a list of lines canonicalizing the request according to
327 http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html."
328 (let* ((headers (headers-for-signing request
))
329 (canonical-headers (canonical-headers headers
)))
331 (list (http-method request
)
332 (path-to-sign request
)
333 (canonical-parameters-string request
)
334 (loop for
(name . value
) in canonical-headers
335 collect
(format nil
"~A:~A" name value
))
337 (format nil
"~{~A~^;~}" (signed-headers request
))
338 (or (amz-header-value request
"content-sha256")
339 "UNSIGNED-PAYLOAD")))))
341 (defun string-to-sign-lines (request)
342 "Return a list of strings to sign to construct the Authorization header."
343 (list "AWS4-HMAC-SHA256"
344 (iso8601-basic-timestamp-string (date request
))
345 (with-output-to-string (s)
346 (format s
"~A/~A/s3/aws4_request"
347 (iso8601-basic-date-string (date request
))
349 (strings-sha256/hex
(canonicalized-request-lines request
))))
351 (defun make-signing-key (credentials &key region service
)
352 "The signing key is derived from the credentials, region, date, and
353 service. A signing key could be saved, shared, and reused, but ZS3 just recomputes it all the time instead."
354 (let* ((k1 (format nil
"AWS4~A" (secret-key credentials
)))
355 (date-key (hmac-sha256 k1
(iso8601-basic-date-string)))
356 (region-key (hmac-sha256 date-key region
))
357 (service-key (hmac-sha256 region-key service
)))
358 (hmac-sha256 service-key
"aws4_request")))
360 (defun payload-sha256 (request)
362 (let ((payload (content request
)))
364 ((or null empty-vector
)
365 *empty-string-sha256
*)
367 (vector-sha256/hex payload
))
369 (file-sha256/hex payload
))))
372 (defun request-signature (request)
373 (let ((key (make-signing-key *credentials
*
374 :region
(region request
)
376 (strings-hmac-sha256/hex key
(string-to-sign-lines request
) )))
378 (defmethod authorization-header-value ((request request
))
379 (let ((key (make-signing-key *credentials
*
380 :region
(region request
)
382 (lines (string-to-sign-lines request
)))
383 (with-output-to-string (s)
384 (write-string "AWS4-HMAC-SHA256" s
)
385 (format s
" Credential=~A/~A/~A/s3/aws4_request"
386 (access-key *credentials
*)
387 (iso8601-basic-date-string (date request
))
389 (format s
",SignedHeaders=~{~A~^;~}" (signed-headers request
))
390 (format s
",Signature=~A"
391 (strings-hmac-sha256/hex key lines
)))))
393 (defgeneric drakma-headers
(request)
396 (list* (cons "Date" (http-date-string (date request
)))
397 (cons "Authorization"
398 (authorization-header-value request
))
399 (all-amazon-headers request
))))
400 (append (extra-http-headers request
) base
))))
402 (defgeneric url
(request)
404 (format nil
"http~@[s~*~]://~A~A"
407 (request-path request
))))
409 (defun send-file-content (fun request
)
410 (with-open-file (stream (content request
)
411 :element-type
'(unsigned-byte 8))
412 (let* ((buffer-size 8000)
413 (buffer (make-octet-vector buffer-size
)))
414 (flet ((read-exactly (size)
415 (assert (= size
(read-sequence buffer stream
)))))
416 (multiple-value-bind (loops rest
)
417 (truncate (content-length request
) buffer-size
)
419 (read-exactly buffer-size
)
420 (funcall fun buffer t
))
422 (funcall fun
(subseq buffer
0 rest
) nil
))))))
424 (defgeneric send
(request &key want-stream stream
)
425 (:method
(request &key want-stream stream
)
427 (drakma:http-request
(url request
)
429 :want-stream want-stream
431 :keep-alive
*use-keep-alive
*
432 :close
(not *use-keep-alive
*)
433 :content-type
(content-type request
)
434 :additional-headers
(drakma-headers request
)
435 :method
(method request
)
437 :content-length
(content-length request
)
438 :parameters
(parameters request
)
439 :content
:continuation
)))
440 (let ((content (content request
)))
441 (if (pathnamep content
)
442 (send-file-content continuation request
)
443 (funcall continuation content nil
))))))
445 (defmethod access-key ((request request
))
446 (access-key (credentials request
)))
448 (defmethod secret-key ((request request
))
449 (secret-key (credentials request
)))
451 (defmethod security-token ((request request
))
452 (security-token (credentials request
)))