awsraw 0.1.9 → 1.0.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +121 -49
- data/Rakefile +0 -6
- data/awsraw.gemspec +6 -5
- data/lib/awsraw.rb +8 -3
- data/lib/awsraw/credentials.rb +7 -0
- data/lib/awsraw/error.rb +4 -0
- data/lib/awsraw/s3/canonicalized_resource.rb +50 -0
- data/lib/awsraw/s3/client.rb +50 -28
- data/lib/awsraw/s3/content_md5_header.rb +34 -0
- data/lib/awsraw/s3/faraday_middleware.rb +39 -0
- data/lib/awsraw/s3/query_string_signer.rb +26 -37
- data/lib/awsraw/s3/signature.rb +52 -0
- data/lib/awsraw/s3/string_to_sign.rb +49 -0
- data/lib/awsraw/version.rb +2 -2
- data/spec/s3/canonicalized_resource_spec.rb +51 -0
- data/spec/s3/content_md5_header_spec.rb +27 -0
- data/spec/s3/faraday_middleware_spec.rb +88 -0
- data/spec/s3/query_string_signer_spec.rb +13 -57
- data/spec/s3/signature_spec.rb +107 -0
- data/spec/s3/string_to_sign_spec.rb +94 -0
- metadata +59 -56
- checksums.yaml.gz.sig +0 -2
- data.tar.gz.sig +0 -0
- data/.travis.yml +0 -7
- data/lib/awsraw/s3.rb +0 -15
- data/lib/awsraw/s3/configuration.rb +0 -12
- data/lib/awsraw/s3/http_request_builder.rb +0 -51
- data/lib/awsraw/s3/md5_digester.rb +0 -19
- data/lib/awsraw/s3/request.rb +0 -65
- data/lib/awsraw/s3/response.rb +0 -26
- data/lib/awsraw/s3/signer.rb +0 -86
- data/spec/s3/client_spec.rb +0 -27
- data/spec/s3/configuration_spec.rb +0 -23
- data/spec/s3/http_request_builder_spec.rb +0 -45
- data/spec/s3/md5_digester_spec.rb +0 -21
- data/spec/s3/request_spec.rb +0 -23
- data/spec/s3/signer_spec.rb +0 -75
- metadata.gz.sig +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 74ab136d9a8688640905f2ffd967f828dc960f42
|
4
|
+
data.tar.gz: d961172c156fe8a271b94d33665597cea23e249b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2512ee73dde56b39b937258e0432eacd3b12a2056905ae28b921df5afb758d7d98cafe8225f3d96cefbcb9daf2833c42037724e8e9cc04ff57024f64b1c22d6d
|
7
|
+
data.tar.gz: c39c859a8b5516efaf9fd33dd0afc234c23efa70d79d802571b0baf68aebc81af417afcaae06a5886b830e46be35a9f9a6ea02e76f37f0dd92fe9882f559dab3
|
data/README.md
CHANGED
@@ -1,76 +1,148 @@
|
|
1
|
-
# AWSRaw
|
1
|
+
# AWSRaw
|
2
2
|
|
3
3
|
A client for [Amazon Web Services](http://www.amazonaws.com/) in the style of
|
4
|
-
[FlickRaw](http://hanklords.github.com/flickraw/)
|
4
|
+
[FlickRaw](http://hanklords.github.com/flickraw/).
|
5
5
|
|
6
6
|
## Background
|
7
7
|
|
8
|
-
AWSRaw
|
9
|
-
|
10
|
-
|
11
|
-
sending a `DELETE` request to the bucket/key URL will result in the file
|
12
|
-
being deleted.
|
8
|
+
AWSRaw has a simple goal: to let you follow the [AWS API
|
9
|
+
docs](http://aws.amazon.com/documentation/), and translate that into Ruby code
|
10
|
+
with the minimum of fuss.
|
13
11
|
|
14
|
-
|
15
|
-
|
16
|
-
able to use a new AWS feature by simply following the AWS docs' examples
|
17
|
-
directly is very nice, instead of having to dig deep into a higher-level
|
18
|
-
library to figure out how they've mapped that new feature into their
|
19
|
-
terminology and API.
|
12
|
+
This is the opposite of [fog](http://fog.io). AWSRaw tries to add as little
|
13
|
+
abstraction as possible on top of the AWS REST API.
|
20
14
|
|
21
|
-
|
15
|
+
You use a regular HTTP library to make requests, and AWSRaw provides useful
|
16
|
+
additions like request signing.
|
22
17
|
|
23
|
-
|
18
|
+
|
19
|
+
## Examples
|
20
|
+
|
21
|
+
### Credentials
|
22
|
+
|
23
|
+
For all the examples below, you'll need to set up your credentials like this:
|
24
24
|
|
25
25
|
```ruby
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
config.host = 'fake.s3.dev'
|
31
|
-
config.regional_hosts = {
|
32
|
-
'ap-southeast-2' => 'fake.s3.dev'
|
33
|
-
}
|
34
|
-
end
|
26
|
+
credentials = AWSRaw::Credentials.new(
|
27
|
+
:access_key_id => "...",
|
28
|
+
:secret_access_key => "..."
|
29
|
+
)
|
35
30
|
```
|
36
31
|
|
37
|
-
## Usage
|
38
|
-
|
39
32
|
### S3
|
40
33
|
|
41
|
-
|
34
|
+
Set up your Faraday connection something like this:
|
42
35
|
|
43
36
|
```ruby
|
44
|
-
|
37
|
+
connection = Faraday.new("http://s3.amazonaws.com") do |faraday|
|
38
|
+
faraday.use AWSRaw::S3::FaradayMiddleware, credentials
|
39
|
+
faraday.response :logger
|
40
|
+
faraday.adapter Faraday.default_adapter
|
41
|
+
end
|
42
|
+
```
|
45
43
|
|
46
|
-
|
47
|
-
ENV['AWS_ACCESS_KEY_ID'],
|
48
|
-
ENV['AWS_SECRET_ACCESS_KEY'])
|
44
|
+
A simple GET request:
|
49
45
|
|
50
|
-
|
46
|
+
```ruby
|
47
|
+
response = connection.get("/mah-sekret-buckit/reaction.gif")
|
48
|
+
```
|
51
49
|
|
52
|
-
|
53
|
-
:bucket => "mah-sekret-buckit",
|
54
|
-
:key => "/reaction.gif",
|
55
|
-
:content => file,
|
56
|
-
:headers => { "Content-Type" => "image/gif" })
|
50
|
+
A PUT request:
|
57
51
|
|
58
|
-
|
52
|
+
```ruby
|
53
|
+
connection.put do |request|
|
54
|
+
request.url "/mah-sekret-buckit/reaction.gif"
|
55
|
+
request.headers["Content-Type"] = "image/gif"
|
56
|
+
request.body = File.new("reaction.gif")
|
57
|
+
end
|
59
58
|
```
|
60
59
|
|
61
|
-
|
62
|
-
|
60
|
+
See the [AWS S3 REST API docs](http://docs.aws.amazon.com/AmazonS3/latest/API/APIRest.html)
|
61
|
+
for all the requests you can make.
|
62
|
+
|
63
|
+
|
64
|
+
#### On request bodies
|
65
|
+
|
66
|
+
If your request has a body and you don't provide a Content-MD5 header for it,
|
67
|
+
AWSRaw will try to calculate one. (The S3 API requires the Content-MD5 header
|
68
|
+
for correct request signing.)
|
69
|
+
|
70
|
+
It can handle the body behaving as either a String or a File. If you want to do
|
71
|
+
something different with the body, you'll need to set the Content-MD5 header
|
72
|
+
yourself.
|
73
|
+
|
74
|
+
You must also provide a Content-Type header for your request if there's a
|
75
|
+
request body. AWSRaw will raise an exception if you don't.
|
76
|
+
|
77
|
+
|
78
|
+
#### Signing query strings
|
79
|
+
|
80
|
+
If you need a signed URI with an expiry date, this is how to do it. See the
|
81
|
+
[AWS docs on the
|
82
|
+
subject](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth).
|
83
|
+
|
63
84
|
|
64
85
|
```ruby
|
65
|
-
|
86
|
+
signer = AWSRaw::S3::QueryStringSigner.new(credentials)
|
87
|
+
|
88
|
+
uri = signer.sign(
|
89
|
+
"https://s3.amazonaws.com/mah-sekret-buckit/reaction.gif",
|
90
|
+
Time.now + 600 # The URI will expire in 10 minutes.
|
91
|
+
)
|
92
|
+
```
|
93
|
+
|
94
|
+
|
95
|
+
#### HTML Form Uploads
|
66
96
|
|
67
|
-
|
68
|
-
|
69
|
-
|
97
|
+
You can use AWSRaw to generate signatures for browser-based uploads. See the
|
98
|
+
[AWS docs on the
|
99
|
+
topic](http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingHTTPPOST.html).
|
70
100
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
101
|
+
```ruby
|
102
|
+
policy = [
|
103
|
+
{ "bucket" => "mah-secret-buckit" }
|
104
|
+
]
|
105
|
+
|
106
|
+
policy_json = JSON.generate(policy)
|
107
|
+
|
108
|
+
http_post_variables = {
|
109
|
+
"AWSAccessKeyID" => credentials.access_key_id,
|
110
|
+
"key" => "reaction.gif",
|
111
|
+
"policy" => AWSRaw::S3::Signature.encode_form_policy(policy_json),
|
112
|
+
"signature" => AWSRaw::S3::Signature.form_signature(policy_json, credentials)
|
113
|
+
}
|
76
114
|
```
|
115
|
+
|
116
|
+
Then get your browser to do an XHR request using the http_post_variables, and
|
117
|
+
Bob's your aunty.
|
118
|
+
|
119
|
+
|
120
|
+
## Status
|
121
|
+
|
122
|
+
This is still a bit experimental, and is missing some key features, but what's
|
123
|
+
there is solid and well tested.
|
124
|
+
|
125
|
+
Right now AWSRaw only has direct support for
|
126
|
+
[Faraday](https://github.com/lostisland/faraday), but you could still use it
|
127
|
+
with other client libraries with a bit of work.
|
128
|
+
|
129
|
+
So far we've only built S3 support. We'd love to see pull requests for other
|
130
|
+
AWS services.
|
131
|
+
|
132
|
+
|
133
|
+
## Contributing
|
134
|
+
|
135
|
+
1. Fork it
|
136
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
137
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
138
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
139
|
+
5. Create new Pull Request
|
140
|
+
|
141
|
+
|
142
|
+
## To Do
|
143
|
+
|
144
|
+
- Add smart handling of errors
|
145
|
+
- Identify cases where string-to-sign doesn't match, and display something helpful
|
146
|
+
- Raise exceptions for errors?
|
147
|
+
- Add easy ways to nicely format XML responses
|
148
|
+
|
data/Rakefile
CHANGED
data/awsraw.gemspec
CHANGED
@@ -4,9 +4,9 @@ require "awsraw/version"
|
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = "awsraw"
|
7
|
-
s.version =
|
8
|
-
s.authors = ["Pete Yandell", "David Goodlad", "Jack 'chendo' Chen"
|
9
|
-
s.email = ["pete@notahat.com", "david@goodlad.net", "gems.awsraw@chen.do"
|
7
|
+
s.version = AWSRaw::VERSION
|
8
|
+
s.authors = ["Pete Yandell", "David Goodlad", "Jack 'chendo' Chen"]
|
9
|
+
s.email = ["pete@notahat.com", "david@goodlad.net", "gems.awsraw@chen.do"]
|
10
10
|
s.license = 'MIT'
|
11
11
|
s.homepage = "http://github.com/envato/awsraw"
|
12
12
|
s.summary = %q{Minimal AWS client}
|
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
|
|
21
21
|
|
22
22
|
# specify any dependencies here; for example:
|
23
23
|
s.add_development_dependency "rake"
|
24
|
-
s.add_development_dependency "rspec"
|
25
|
-
|
24
|
+
s.add_development_dependency "rspec", "~> 2.14"
|
25
|
+
s.add_runtime_dependency "finer_struct", "~> 0.0.5"
|
26
|
+
s.add_runtime_dependency "faraday", "~> 0.8.8"
|
26
27
|
end
|
data/lib/awsraw.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
|
+
require "awsraw/error"
|
1
2
|
require "awsraw/version"
|
3
|
+
require "awsraw/s3/canonicalized_resource"
|
4
|
+
require "awsraw/s3/client"
|
5
|
+
require "awsraw/s3/content_md5_header"
|
6
|
+
require "awsraw/s3/faraday_middleware"
|
7
|
+
require "awsraw/s3/query_string_signer"
|
8
|
+
require "awsraw/s3/signature"
|
9
|
+
require "awsraw/s3/string_to_sign"
|
2
10
|
|
3
|
-
module AWSRaw
|
4
|
-
|
5
|
-
end
|
data/lib/awsraw/error.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module AWSRaw
|
4
|
+
module S3
|
5
|
+
|
6
|
+
module CanonicalizedResource
|
7
|
+
|
8
|
+
def self.canonicalized_resource(uri)
|
9
|
+
uri = URI(uri)
|
10
|
+
bucket = bucket_from_hostname(uri.hostname)
|
11
|
+
|
12
|
+
[bucket && "/#{bucket}", uri.path, canonicalized_subresources(uri.query)].join
|
13
|
+
end
|
14
|
+
|
15
|
+
# Extract the bucket name from the hostname for virtual-host-style and
|
16
|
+
# cname-style S3 requests. Returns nil for path-style requests.
|
17
|
+
#
|
18
|
+
# See: http://docs.aws.amazon.com/AmazonS3/latest/dev/VirtualHosting.html
|
19
|
+
def self.bucket_from_hostname(hostname)
|
20
|
+
if hostname =~ %r{s3[-\w\d]*\.amazonaws\.com$}
|
21
|
+
components = hostname.split(".")
|
22
|
+
if components.length > 3
|
23
|
+
components[0..-4].join(".")
|
24
|
+
else
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
else
|
28
|
+
hostname
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
VALID_SUBRESOURCES = %w{acl lifecycle location logging notification partNumber policy requestPayment torrent uploadId uploads versionId versioning versions website}
|
33
|
+
|
34
|
+
# Generates the canonicalized subresources for a URI, as per:
|
35
|
+
#
|
36
|
+
# http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
|
37
|
+
#
|
38
|
+
# Note: This is incomplete, in that it doesn't handle header values
|
39
|
+
# that are overridden by parameters, nor does it handle the "delete"
|
40
|
+
# parameter for multi-object Delete requests.
|
41
|
+
def self.canonicalized_subresources(query)
|
42
|
+
query ||= ""
|
43
|
+
subresources = query.split("&") & VALID_SUBRESOURCES
|
44
|
+
"?#{subresources.sort.join('&')}" unless subresources.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
data/lib/awsraw/s3/client.rb
CHANGED
@@ -1,35 +1,42 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require 'awsraw/
|
4
|
-
require 'awsraw/s3/
|
5
|
-
|
6
|
-
require 'awsraw/s3/signer'
|
7
|
-
require 'awsraw/s3/md5_digester'
|
1
|
+
require 'uri'
|
2
|
+
require 'faraday'
|
3
|
+
require 'awsraw/credentials'
|
4
|
+
require 'awsraw/s3/faraday_middleware'
|
5
|
+
|
8
6
|
module AWSRaw
|
9
7
|
module S3
|
10
8
|
|
11
|
-
|
12
|
-
|
13
|
-
# A client for the AWS S3 rest API.
|
14
|
-
#
|
15
|
-
# http://docs.amazonwebservices.com/AmazonS3/latest/API/APIRest.html
|
9
|
+
# Legacy client, to support AWSRaw pre-1.0 requests. You shouldn't be using
|
10
|
+
# this anymore.
|
16
11
|
class Client
|
17
12
|
|
18
13
|
def initialize(access_key_id, secret_access_key)
|
19
|
-
@
|
20
|
-
|
14
|
+
@credentials = AWSRaw::Credentials.new(
|
15
|
+
:access_key_id => access_key_id,
|
16
|
+
:secret_access_key => secret_access_key
|
17
|
+
)
|
21
18
|
end
|
22
19
|
|
23
20
|
def request(params = {})
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
host = params[:region] ? "s3-#{params[:region]}.amazonaws.com" : "s3.amazonaws.com"
|
22
|
+
path = URI.escape("/#{params[:bucket]}#{params[:key]}")
|
23
|
+
url = URI::HTTP.build(
|
24
|
+
:host => host,
|
25
|
+
:path => path,
|
26
|
+
:query => params[:query]
|
27
|
+
)
|
27
28
|
|
28
|
-
|
29
|
-
|
29
|
+
faraday_response = connection.send(params[:method].downcase) do |request|
|
30
|
+
request.url(url)
|
31
|
+
request.headers = params[:headers] || {}
|
32
|
+
request.body = params[:content]
|
30
33
|
end
|
31
34
|
|
32
|
-
|
35
|
+
Response.new(
|
36
|
+
:code => faraday_response.status,
|
37
|
+
:headers => faraday_response.headers,
|
38
|
+
:content => faraday_response.body
|
39
|
+
)
|
33
40
|
end
|
34
41
|
|
35
42
|
def request!(params = {})
|
@@ -39,18 +46,33 @@ module AWSRaw
|
|
39
46
|
|
40
47
|
private
|
41
48
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
49
|
+
def connection
|
50
|
+
@connection ||= Faraday.new do |faraday|
|
51
|
+
faraday.use AWSRaw::S3::FaradayMiddleware, @credentials
|
52
|
+
faraday.adapter Faraday.default_adapter
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
class Response
|
59
|
+
def initialize(params = {})
|
60
|
+
@code = params[:code]
|
61
|
+
@headers = params[:headers]
|
62
|
+
@content = params[:content]
|
48
63
|
end
|
49
64
|
|
50
|
-
|
51
|
-
|
65
|
+
attr_accessor :code
|
66
|
+
attr_accessor :headers
|
67
|
+
attr_accessor :content
|
68
|
+
|
69
|
+
def success?
|
70
|
+
code =~ /^2\d\d$/
|
52
71
|
end
|
53
72
|
|
73
|
+
def failure?
|
74
|
+
!success?
|
75
|
+
end
|
54
76
|
end
|
55
77
|
|
56
78
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module AWSRaw
|
4
|
+
module S3
|
5
|
+
module ContentMD5Header
|
6
|
+
|
7
|
+
def self.generate_content_md5(body)
|
8
|
+
return nil if body.nil?
|
9
|
+
|
10
|
+
digest = Digest::MD5.new
|
11
|
+
if body.respond_to?(:read)
|
12
|
+
read_file_into_digest(digest, body)
|
13
|
+
else
|
14
|
+
digest << body
|
15
|
+
end
|
16
|
+
|
17
|
+
digest.base64digest
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# This mimics the behaviour of Ruby's Digest::Instance#file method.
|
23
|
+
# Unfortunately that takes a filename not a file, so we can't use it.
|
24
|
+
def self.read_file_into_digest(digest, file)
|
25
|
+
buffer = ""
|
26
|
+
while file.read(16384, buffer)
|
27
|
+
digest << buffer
|
28
|
+
end
|
29
|
+
file.rewind # ...so the HTTP client can read the body for sending.
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|