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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c2162640827a7ebe53d58fd6f53c8ff5c515f80f
4
- data.tar.gz: 9b0725c26c0c27465a3a606094ea7e2beafa5def
3
+ metadata.gz: 74ab136d9a8688640905f2ffd967f828dc960f42
4
+ data.tar.gz: d961172c156fe8a271b94d33665597cea23e249b
5
5
  SHA512:
6
- metadata.gz: d8b3bf71d146932f0def424d13105286833ee2635387d6fb4ae968a02a902e0b050773e404c77ebbf5a9a54e2a8202cfab5b8edf4934c5614ab793b7c496f236
7
- data.tar.gz: 68fb736100f5693ced2f453e311fd7d8ca96edab5c124364a45be46db6969319bb3937c7fa9015db55450dafdfe9558654dae6d2019857e907310eac85d438b8
6
+ metadata.gz: 2512ee73dde56b39b937258e0432eacd3b12a2056905ae28b921df5afb758d7d98cafe8225f3d96cefbcb9daf2833c42037724e8e9cc04ff57024f64b1c22d6d
7
+ data.tar.gz: c39c859a8b5516efaf9fd33dd0afc234c23efa70d79d802571b0baf68aebc81af417afcaae06a5886b830e46be35a9f9a6ea02e76f37f0dd92fe9882f559dab3
data/README.md CHANGED
@@ -1,76 +1,148 @@
1
- # AWSRaw [![Build Status](https://travis-ci.org/envato/awsraw.svg?branch=0.1-maintenance)](https://travis-ci.org/envato/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 helps you make authenticated requests to AWS's various services. It
9
- doesn't provide any higher-level concepts like, for example, "delete this
10
- file from S3". Instead, you should understand S3's http API and know that
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
- While these higher-level concepts can be useful (see, e.g.,
15
- [fog](https://github.com/fog/fog)), they can also get in the way. Being
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
- ## Configuration
15
+ You use a regular HTTP library to make requests, and AWSRaw provides useful
16
+ additions like request signing.
22
17
 
23
- If you need to override the AWS hostname for development/testing purposes, you can do so as follows:
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
- require 'awsraw/s3/client'
27
-
28
- # Assuming we have a fake S3 service listening on `fake.s3.dev`
29
- AWSRaw::S3.configure do |config|
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
- Standard requests:
34
+ Set up your Faraday connection something like this:
42
35
 
43
36
  ```ruby
44
- require 'awsraw/s3/client'
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
- s3 = AWSRaw::S3::Client.new(
47
- ENV['AWS_ACCESS_KEY_ID'],
48
- ENV['AWS_SECRET_ACCESS_KEY'])
44
+ A simple GET request:
49
45
 
50
- file = File.open("reaction.gif", "rb")
46
+ ```ruby
47
+ response = connection.get("/mah-sekret-buckit/reaction.gif")
48
+ ```
51
49
 
52
- s3.request(:method => "PUT",
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
- f.close
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
- Signed query-string requests, to allow authorized clients to get protected
62
- resources:
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
- require 'awsraw/s3/query_string_signer'
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
- signer = AWSRaw::S3::QueryStringSigner.new(
68
- ENV['AWS_ACCESS_KEY_ID'],
69
- ENV['AWS_SECRET_ACCESS_KEY'])
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
- url = "http://s3.amazonaws.com/mah-sekret-bucket/reaction.gif"
72
- expiry = Time.now.utc + 60 # 1 minute from now
73
- temporary_url = signer.sign_with_query_string(url, expiry.to_i)
74
- puts temporary_url
75
- # => "http://s3.amazonaws.com/mah-sekret-bucket/reaction.gif?Signature=..."
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
@@ -1,7 +1 @@
1
1
  require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- desc "Run tests"
5
- RSpec::Core::RakeTask.new(:spec)
6
-
7
- task :default => [:spec]
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 = Awsraw::VERSION
8
- s.authors = ["Pete Yandell", "David Goodlad", "Jack 'chendo' Chen", "Warren Seen"]
9
- s.email = ["pete@notahat.com", "david@goodlad.net", "gems.awsraw@chen.do", "warren@warrenseen.com"]
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
- # s.add_runtime_dependency "rest-client"
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
@@ -0,0 +1,7 @@
1
+ require 'finer_struct'
2
+
3
+ module AWSRaw
4
+ class Credentials < FinerStruct::Immutable(:access_key_id, :secret_access_key)
5
+ end
6
+ end
7
+
@@ -0,0 +1,4 @@
1
+ module AWSRaw
2
+ class Error < RuntimeError
3
+ end
4
+ end
@@ -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
@@ -1,35 +1,42 @@
1
- require 'net/http'
2
- require 'awsraw/s3'
3
- require 'awsraw/s3/request'
4
- require 'awsraw/s3/http_request_builder'
5
- require 'awsraw/s3/response'
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
- class ConnectionError < StandardError; end
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
- @access_key_id = access_key_id
20
- @secret_access_key = secret_access_key
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
- request = Request.new(params, signer)
25
-
26
- http_request = HTTPRequestBuilder.new(request).build
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
- http_response = Net::HTTP.start(request.uri.host, request.uri.port) do |http|
29
- http.request(http_request)
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
- construct_response(http_response)
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 construct_response(http_response)
43
- Response.new(
44
- :code => http_response.code,
45
- :headers => http_response.to_hash,
46
- :content => http_response.body
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
- def signer
51
- @signer ||= Signer.new(@access_key_id, @secret_access_key)
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