awsraw 0.1.9 → 1.0.0.alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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