2 ;;;; Copyright (c) 2008 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.")
44 :documentation
"An object that has methods for ACCESS-KEY and
45 SECRET-KEY. A list of two strings (the keys) suffices.")
58 :documentation
"e.g. :GET, :PUT, :DELETE")
63 "A string naming the bucket to address in the request. If NIL,
64 request is not directed at a specific bucket.")
69 "A string naming the key to address in the request. If NIL,
70 request is not directed at a specific key.")
72 :initarg
:sub-resource
73 :accessor sub-resource
74 :documentation
"A sub-resource to address as part of the request,
75 without a leading \"?\", e.g. \"acl\", \"torrent\". If PARAMETERS
76 is set, this must be NIL.")
81 "An alist of string key/value pairs to send as CGI-style GET
82 parameters with the request. If SUB-RESOURCE is set, these must be
85 :initarg
:content-type
86 :accessor content-type
)
89 :accessor content-md5
)
91 :initarg
:content-length
92 :accessor content-length
)
100 "An alist of Amazon metadata to attach to a request. These should
101 be straight string key/value pairs, WITHOUT any \"x-amz-meta-\"
104 :initarg
:amz-headers
105 :accessor amz-headers
107 "An alist of extra Amazon request headers. These should be
108 straight string key/value pairs, WITHOUT any \"x-amz-\" prefix.")
113 :initarg
:signed-string
114 :accessor signed-string
)
116 :initarg
:extra-http-headers
117 :accessor extra-http-headers
118 :documentation
"An alist of extra HTTP headers to include in the request."))
120 ;; :date and :content-md5 are specially treated, should not be nil
121 :credentials
*credentials
*
123 :endpoint
*s3-endpoint
*
135 :extra-http-headers nil
))
137 (defmethod slot-unbound ((class t
) (request request
) (slot (eql 'date
)))
138 (setf (date request
) (get-universal-time)))
140 (defmethod slot-unbound ((class t
) (request request
) (slot (eql 'content-md5
)))
141 (setf (content-md5 request
)
142 (and *use-content-md5
*
143 (pathnamep (content request
))
144 (file-md5/b64
(content request
)))))
146 (defmethod slot-unbound ((class t
) (request request
) (slot (eql 'signed-string
)))
147 (setf (signed-string request
)
148 (format nil
"~{~A~^~%~}" (string-to-sign-lines request
))))
150 (defgeneric amz-header-value
(request name
)
151 (:method
(request name
)
152 (cdr (assoc name
(amz-headers request
) :test
'string
=))))
154 (defgeneric ensure-amz-header
(request name value
)
155 (:method
(request name value
)
156 (unless (amz-header-value request name
)
157 (push (cons name value
) (amz-headers request
)))))
159 (defmethod initialize-instance :after
((request request
)
162 (declare (ignore initargs
))
163 (when (eql (method request
) :head
)
164 ;; https://forums.aws.amazon.com/thread.jspa?messageID=340398 -
165 ;; when using the bare endpoint, the 301 redirect for a HEAD
166 ;; request does not include enough info to actually redirect. Use
167 ;; the bucket endpoint pre-emptively instead
168 (setf (endpoint request
) (format nil
"~A.~A"
171 (ensure-amz-header request
"date"
172 (iso8601-basic-timestamp-string (date request
)))
173 (ensure-amz-header request
"content-sha256"
174 (payload-sha256 request
))
175 (let ((target-region (redirected-region (endpoint request
)
178 (setf (region request
) target-region
)))
179 (when (content-md5 request
)
180 (push (cons "Content-MD5" (content-md5 request
)) (extra-http-headers request
)))
181 (unless (integerp (content-length request
))
182 (let ((content (content request
)))
183 (setf (content-length request
)
186 (pathname (file-size content
))
187 (vector (length content
)))))))
189 (defgeneric host
(request)
190 (:method
((request request
))
191 (or (redirected-endpoint (endpoint request
) (bucket request
))
192 (endpoint request
))))
194 (defgeneric http-method
(request)
196 (string-upcase (method request
))))
198 (defun puri-canonicalized-path (path)
199 (let ((parsed (puri:parse-uri
(format nil
"http://dummy~A" path
))))
200 (with-output-to-string (stream)
201 (if (puri:uri-path parsed
)
202 (write-string (puri:uri-path parsed
) stream
)
203 (write-string "/" stream
))
204 (when (puri:uri-query parsed
)
205 (write-string "?" stream
)
206 (write-string (puri:uri-query parsed
) stream
)))))
208 (defgeneric signed-path
(request)
210 (let ((*print-pretty
* nil
))
211 (puri-canonicalized-path
212 (with-output-to-string (stream)
213 (write-char #\
/ stream
)
214 (when (bucket request
)
215 (write-string (url-encode (name (bucket request
))) stream
)
216 (write-char #\
/ stream
))
218 (write-string (url-encode (name (key request
)) :encode-slash nil
)
220 (when (sub-resource request
)
221 (write-string "?" stream
)
222 (write-string (url-encode (sub-resource request
)) stream
)))))))
224 (defgeneric request-path
(request)
226 (let ((*print-pretty
* nil
))
227 (with-output-to-string (stream)
228 (write-char #\
/ stream
)
229 (when (and (bucket request
)
230 (string= (endpoint request
)
231 (region-endpoint (region request
))))
232 (write-string (url-encode (name (bucket request
))) stream
)
233 (write-char #\
/ stream
))
235 (write-string (url-encode (name (key request
))
236 :encode-slash nil
) stream
))
237 (when (sub-resource request
)
238 (write-string "?" stream
)
239 (write-string (url-encode (sub-resource request
)) stream
))))))
241 (defgeneric all-amazon-headers
(request)
244 (loop for
((key . value
)) on
(amz-headers request
)
245 collect
(cons (format nil
"x-amz-~(~A~)" key
)
247 (loop for
((key . value
)) on
(metadata request
)
248 collect
(cons (format nil
"x-amz-meta-~(~A~)" key
)
251 (defgeneric date-string
(request)
253 (http-date-string (date request
))))
255 ;;; AWS 4 authorization
257 (defun headers-for-signing (request)
258 (append (all-amazon-headers request
)
259 (extra-http-headers request
)
260 (parameters-alist "host" (host request
)
261 "content-type" (content-type request
))))
263 (defun canonical-headers (headers)
264 (flet ((trim (string)
265 (string-trim " " string
)))
267 (loop for
(name . value
) in headers
268 collect
(cons (string-downcase name
)
270 (sort encoded
#'string
< :key
'car
))))
272 (defun signed-headers (request)
273 (mapcar 'first
(canonical-headers (headers-for-signing request
))))
275 (defun parameters-for-signing (request)
276 (cond ((sub-resource request
)
277 (list (cons (sub-resource request
) "")))
279 (parameters request
))))
281 (defun canonical-parameters (parameters)
283 (loop for
(name . value
) in parameters
286 (url-encode value
)))))
287 (sort encoded
#'string
< :key
'car
)))
289 (defun canonical-parameters-string (request)
290 (format nil
"~{~A=~A~^&~}"
291 (alist-plist (canonical-parameters
292 (parameters-for-signing request
)))))
294 (defun path-to-sign (request)
295 "Everything in the PATH of the request, up to the first ?"
296 (let ((path (request-path request
)))
297 (subseq path
0 (position #\? path
))))
299 (defun canonicalized-request-lines (request)
300 "Return a list of lines canonicalizing the request according to
301 http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html."
302 (let* ((headers (headers-for-signing request
))
303 (canonical-headers (canonical-headers headers
)))
305 (list (http-method request
)
306 (path-to-sign request
)
307 (canonical-parameters-string request
)
308 (loop for
(name . value
) in canonical-headers
309 collect
(format nil
"~A:~A" name value
))
311 (format nil
"~{~A~^;~}" (signed-headers request
))
312 (or (amz-header-value request
"content-sha256")
313 "UNSIGNED-PAYLOAD")))))
315 (defun string-to-sign-lines (request)
316 "Return a list of strings to sign to construct the Authorization header."
317 (list "AWS4-HMAC-SHA256"
318 (iso8601-basic-timestamp-string (date request
))
319 (with-output-to-string (s)
320 (format s
"~A/~A/s3/aws4_request"
321 (iso8601-basic-date-string (date request
))
323 (strings-sha256/hex
(canonicalized-request-lines request
))))
325 (defun make-signing-key (credentials &key region service
)
326 "The signing key is derived from the credentials, region, date, and
327 service. A signing key could be saved, shared, and reused, but ZS3 just recomputes it all the time instead."
328 (let* ((k1 (format nil
"AWS4~A" (secret-key credentials
)))
329 (date-key (hmac-sha256 k1
(iso8601-basic-date-string)))
330 (region-key (hmac-sha256 date-key region
))
331 (service-key (hmac-sha256 region-key service
)))
332 (hmac-sha256 service-key
"aws4_request")))
334 (defun payload-sha256 (request)
336 (let ((payload (content request
)))
338 ((or null empty-vector
)
339 *empty-string-sha256
*)
341 (vector-sha256/hex payload
))
343 (file-sha256/hex payload
))))
346 (defun request-signature (request)
347 (let ((key (make-signing-key *credentials
*
348 :region
(region request
)
350 (strings-hmac-sha256/hex key
(string-to-sign-lines request
) )))
352 (defmethod authorization-header-value ((request request
))
353 (let ((key (make-signing-key *credentials
*
354 :region
(region request
)
356 (lines (string-to-sign-lines request
)))
357 (with-output-to-string (s)
358 (write-string "AWS4-HMAC-SHA256" s
)
359 (format s
" Credential=~A/~A/~A/s3/aws4_request"
360 (access-key *credentials
*)
361 (iso8601-basic-date-string (date request
))
363 (format s
",SignedHeaders=~{~A~^;~}" (signed-headers request
))
364 (format s
",Signature=~A"
365 (strings-hmac-sha256/hex key lines
)))))
367 (defgeneric drakma-headers
(request)
370 (list* (cons "Date" (http-date-string (date request
)))
371 (cons "Authorization"
372 (authorization-header-value request
))
373 (all-amazon-headers request
))))
374 (append (extra-http-headers request
) base
))))
376 (defgeneric url
(request)
378 (format nil
"http~@[s~*~]://~A~A"
381 (request-path request
))))
383 (defun send-file-content (fun request
)
384 (with-open-file (stream (content request
)
385 :element-type
'(unsigned-byte 8))
386 (let* ((buffer-size 8000)
387 (buffer (make-octet-vector buffer-size
)))
388 (flet ((read-exactly (size)
389 (assert (= size
(read-sequence buffer stream
)))))
390 (multiple-value-bind (loops rest
)
391 (truncate (content-length request
) buffer-size
)
393 (read-exactly buffer-size
)
394 (funcall fun buffer t
))
396 (funcall fun
(subseq buffer
0 rest
) nil
))))))
398 (defgeneric send
(request &key want-stream
)
399 (:method
(request &key want-stream
)
401 (drakma:http-request
(url request
)
404 :want-stream want-stream
405 :content-type
(content-type request
)
406 :additional-headers
(drakma-headers request
)
407 :method
(method request
)
409 :content-length
(content-length request
)
410 :parameters
(parameters request
)
411 :content
:continuation
)))
412 (let ((content (content request
)))
413 (if (pathnamep content
)
414 (send-file-content continuation request
)
415 (funcall continuation content nil
))))))
417 (defmethod access-key ((request request
))
418 (access-key (credentials request
)))
420 (defmethod secret-key ((request request
))
421 (secret-key (credentials request
)))