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.
@@ -0,0 +1,39 @@
1
+ require 'time'
2
+ require 'faraday'
3
+ require 'awsraw/error'
4
+ require 'awsraw/s3/content_md5_header'
5
+ require 'awsraw/s3/signature'
6
+ require 'awsraw/s3/string_to_sign'
7
+
8
+ module AWSRaw
9
+ module S3
10
+ class FaradayMiddleware < Faraday::Middleware
11
+
12
+ def initialize(app, credentials = nil)
13
+ @app = app
14
+ @credentials = credentials
15
+ end
16
+
17
+ def call(env)
18
+ if env[:body] && env[:request_headers]['Content-Type'].nil?
19
+ raise AWSRaw::Error, "Can't make a request with a body but no Content-Type header"
20
+ end
21
+
22
+ env[:request_headers]['Date'] ||= Time.now.httpdate
23
+ env[:request_headers]['Content-MD5'] ||= ContentMD5Header.generate_content_md5(env[:body])
24
+
25
+ string_to_sign = StringToSign.string_to_sign(
26
+ :method => env[:method].to_s.upcase,
27
+ :uri => env[:url],
28
+ :content_md5 => env[:request_headers]['Content-MD5'],
29
+ :content_type => env[:request_headers]['Content-Type'],
30
+ :date => env[:request_headers]['Date'],
31
+ :amz_headers => env[:request_headers]
32
+ )
33
+
34
+ env[:request_headers]['Authorization'] = Signature.authorization_header(string_to_sign, @credentials)
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -1,50 +1,39 @@
1
- require 'awsraw/s3/signer'
2
- require 'cgi'
1
+ require 'awsraw/s3/string_to_sign'
2
+ require 'awsraw/s3/signature'
3
3
 
4
4
  module AWSRaw
5
5
  module S3
6
6
 
7
- # Generates a signed query string to make an authenticated S3 GET request
7
+ # Sign S3 URIs using the query string.
8
8
  #
9
- # See http://docs.amazonwebservices.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
10
- #
11
- # The Authorization header method is usually preferable, as implemented in
12
- # AWSRaw::S3::Signer. However, you may have occasions where you need a
13
- # simple "download URL", without having to tell your user-agent (browser,
14
- # curl, wget, etc) about all the special AWS headers. The query string
15
- # authentication method is useful in those cases.
16
- class QueryStringSigner < Signer
17
- def sign_with_query_string(url, expires, headers = {})
18
- query_string_hash = query_string_hash(url, expires, headers)
19
-
20
- uri = URI.parse(url)
21
- uri.query = query_string_hash.map { |k,v| "#{k}=#{v}" }.join("&")
22
- uri.to_s
23
- end
24
-
25
- def query_string_hash(url, expires, headers = {})
26
- string_to_sign = string_to_sign(url, expires, headers)
27
- signature = encoded_signature(string_to_sign)
9
+ # See http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
10
+ class QueryStringSigner
28
11
 
29
- {
30
- "AWSAccessKeyId" => @access_key_id,
31
- "Expires" => expires.to_s,
32
- "Signature" => CGI.escape(signature)
33
- }
12
+ def initialize(credentials)
13
+ @credentials = credentials
34
14
  end
35
15
 
36
- def string_to_sign(url, expires, headers)
37
- [
38
- "GET",
39
- headers["Content-MD5"],
40
- headers["Content-Type"],
41
- expires.to_s,
42
- canonicalized_amz_headers(headers),
43
- canonicalized_resource(URI.parse(url))
44
- ].flatten.join("\n")
16
+ def sign(uri, expires)
17
+ string_to_sign = StringToSign.string_to_sign(
18
+ :method => "GET",
19
+ :uri => uri,
20
+ :date => expires.to_i
21
+ )
22
+
23
+ signature = Signature.signature(string_to_sign, @credentials)
24
+
25
+ URI(uri).tap do |signed_uri|
26
+ signed_uri.query = URI.encode_www_form(
27
+ "AWSAccessKeyId" => @credentials.access_key_id,
28
+ "Signature" => signature,
29
+ "Expires" => expires.to_i
30
+ )
31
+ end
45
32
  end
46
33
 
47
- end
34
+ # For backwards-compatibility with pre-1.0 versions:
35
+ alias_method :sign_with_query_string, :sign
48
36
 
37
+ end
49
38
  end
50
39
  end
@@ -0,0 +1,52 @@
1
+ require 'digest/sha1'
2
+ require 'openssl'
3
+ require 'base64'
4
+
5
+ module AWSRaw
6
+ module S3
7
+
8
+ module Signature
9
+
10
+ # Given a string to sign and some AWS credentials, generate a signature
11
+ # for an S3 request.
12
+ def self.signature(string_to_sign, credentials)
13
+ base64_encode(hmac_sha1(string_to_sign, credentials))
14
+ end
15
+
16
+ # Given a string to sign and some AWS credentials, generate a value
17
+ # for the Authorization header of an S3 request.
18
+ def self.authorization_header(string_to_sign, credentials)
19
+ "AWS #{credentials.access_key_id}:#{signature(string_to_sign, credentials)}"
20
+ end
21
+
22
+ # Encode a HTML form upload policy. See:
23
+ # http://docs.aws.amazon.com/AmazonS3/latest/dev/HTTPPOSTForms.html
24
+ #
25
+ # The policy is expected to be a JSON document.
26
+ def self.encode_form_policy(policy)
27
+ base64_encode(policy)
28
+ end
29
+
30
+ # Sign a policy document for a HTML form upload.
31
+ #
32
+ # The policy document is expected to be base64 encoded JSON.
33
+ # See the .encode_form_policy method.
34
+ def self.form_signature(policy_base64, credentials)
35
+ signature(policy_base64, credentials)
36
+ end
37
+
38
+ private
39
+
40
+ def self.hmac_sha1(data, credentials)
41
+ digest = OpenSSL::Digest::Digest.new("sha1")
42
+ OpenSSL::HMAC.digest(digest, credentials.secret_access_key, data)
43
+ end
44
+
45
+ def self.base64_encode(data)
46
+ Base64.encode64(data).tr("\n", "")
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,49 @@
1
+ require 'awsraw/s3/canonicalized_resource'
2
+
3
+ module AWSRaw
4
+ module S3
5
+
6
+ module StringToSign
7
+
8
+ # Generate the string to sign for authentication headers or query string signing, as per:
9
+ #
10
+ # http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#ConstructingTheAuthenticationHeader
11
+ #
12
+ # Expects the following parameters:
13
+ #
14
+ # :method The HTTP method being used, e.g. GET, PUT, DELETE
15
+ # :uri The full URI for the request, hostname and everything
16
+ # :content_md5 The Content-MD5 header; required if there is body content
17
+ # :content_type The Content-Type header
18
+ # :date The Date (or X-Amz-Date) header
19
+ # :amz_headers A hash of all the X-Amz-* headers
20
+ #
21
+ # Headers in the :amz_headers hash that don't start with "X-Amz-" will be ignored.
22
+ #
23
+ # For query string signing, pass in the "Expires" timestamp in the :date parameter.
24
+ def self.string_to_sign(request_info = {})
25
+ [
26
+ request_info[:method],
27
+ request_info[:content_md5] || "",
28
+ request_info[:content_type] || "",
29
+ request_info[:date],
30
+ canonicalized_amz_headers(request_info[:amz_headers] || {}),
31
+ CanonicalizedResource.canonicalized_resource(request_info[:uri])
32
+ ].flatten.join("\n")
33
+ end
34
+
35
+ def self.canonicalized_amz_headers(headers)
36
+ header_names = headers.keys.
37
+ select {|name| name =~ /^x-amz-/i }.
38
+ sort_by {|name| name.downcase }
39
+
40
+ header_names.map do |name|
41
+ "#{name.downcase}:#{headers[name]}"
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+ end
49
+
@@ -1,3 +1,3 @@
1
- module Awsraw
2
- VERSION = "0.1.9"
1
+ module AWSRaw
2
+ VERSION = "1.0.0.alpha.1"
3
3
  end
@@ -0,0 +1,51 @@
1
+ require 'awsraw/s3/canonicalized_resource'
2
+
3
+ describe AWSRaw::S3::CanonicalizedResource do
4
+
5
+ context ".canonicalized_resource" do
6
+ it "works for a virtual-host-style request" do
7
+ expect(subject.canonicalized_resource("http://johnsmith.s3.amazonaws.com/puppies.jpg")).to eq("/johnsmith/puppies.jpg")
8
+ end
9
+
10
+ it "works for a path-style request" do
11
+ expect(subject.canonicalized_resource("http://s3.amazonaws.com/johnsmith/puppies.jpg")).to eq("/johnsmith/puppies.jpg")
12
+ end
13
+ end
14
+
15
+ context ".bucket_from_hostname" do
16
+ it "gets the bucket from virtual-host-style requests in the default region" do
17
+ expect(subject.bucket_from_hostname("johnsmith.net.s3.amazonaws.com")).to eq("johnsmith.net")
18
+ end
19
+
20
+ it "gets the bucket from virtual-host-style requests in other regions" do
21
+ expect(subject.bucket_from_hostname("johnsmith.net.s3-eu-west-1.amazonaws.com")).to eq("johnsmith.net")
22
+ end
23
+
24
+ it "gets the bucket from cname-style requests" do
25
+ expect(subject.bucket_from_hostname("johnsmith.net")).to eq("johnsmith.net")
26
+ end
27
+
28
+ it "doesn't get the bucket for path-style requests in the default region" do
29
+ expect(subject.bucket_from_hostname("s3.amazonaws.com")).to be_nil
30
+ end
31
+
32
+ it "doesn't get the bucket for path-style requests in other regions" do
33
+ expect(subject.bucket_from_hostname("s3-eu-west-1.amazonaws.com")).to be_nil
34
+ end
35
+ end
36
+
37
+ context ".canonicalized_subresources" do
38
+ it "includes valid subresources" do
39
+ expect(subject.canonicalized_subresources("acl")).to eq("?acl")
40
+ end
41
+
42
+ it "excludes invalid subresources" do
43
+ expect(subject.canonicalized_subresources("rhubarb")).to be_nil
44
+ end
45
+
46
+ it "sorts the subresources" do
47
+ expect(subject.canonicalized_subresources("website&acl")).to eq("?acl&website")
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,27 @@
1
+ require 'stringio'
2
+ require 'awsraw/s3/content_md5_header'
3
+
4
+ describe AWSRaw::S3::ContentMD5Header do
5
+
6
+ context ".generate_content_md5" do
7
+ it "returns nil if the body is nil" do
8
+ expect(subject.generate_content_md5(nil)).to be_nil
9
+ end
10
+
11
+ it "generates the correct digest for a string" do
12
+ expect(subject.generate_content_md5("rhubarb")).to eq("lBxzvNO0KqwdCwPVMx2IYQ==")
13
+ end
14
+
15
+ it "generates the correct digest for a file" do
16
+ file = StringIO.new("rhubarb")
17
+ expect(subject.generate_content_md5(file)).to eq("lBxzvNO0KqwdCwPVMx2IYQ==")
18
+ end
19
+
20
+ it "rewinds a file after reading it" do
21
+ file = StringIO.new("rhubarb")
22
+ subject.generate_content_md5(file)
23
+ expect(file.pos).to eq(0)
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,88 @@
1
+ require 'awsraw/s3/faraday_middleware'
2
+ require 'ostruct'
3
+
4
+ describe AWSRaw::S3::FaradayMiddleware do
5
+
6
+ let(:credentials) do
7
+ OpenStruct.new(
8
+ :access_key_id => "AKIAIOSFODNN7EXAMPLE",
9
+ :secret_access_key => "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
10
+ )
11
+ end
12
+
13
+ let(:app) { double("app") }
14
+ let(:time) { Time.parse("2013-09-19 19:22:13 +1000") }
15
+
16
+ subject { described_class.new(app, credentials) }
17
+
18
+ it "signs the request" do
19
+ env = {
20
+ :method => :put,
21
+ :url => "http://static.johnsmith.net:8080/db-backup.dat.gz",
22
+ :request_headers => {
23
+ "Date" => "Tue, 27 Mar 2007 21:06:08 +0000",
24
+ "Content-Type" => "application/x-download",
25
+ "Content-MD5" => "4gJE4saaMU4BqNR0kLY+lw==",
26
+ "x-amz-acl" => "public-read",
27
+ "X-Amz-Meta-ReviewedBy" => "joe@johnsmith.net,jane@johnsmith.net",
28
+ "X-Amz-Meta-FileChecksum" => "0x02661779",
29
+ "X-Amz-Meta-ChecksumAlgorithm" => "crc32"
30
+ }
31
+ }
32
+ subject.call(env)
33
+ expect(env[:request_headers]["Authorization"]).to eq(
34
+ "AWS AKIAIOSFODNN7EXAMPLE:ilyl83RwaSoYIEdixDQcA4OnAnc="
35
+ )
36
+ end
37
+
38
+ it "sets the Date header if there isn't one" do
39
+ Time.stub(:now => time) # Freeze time for the duration of the test.
40
+
41
+ env = {
42
+ :method => :get,
43
+ :url => "http://s3.amazonaws.com/",
44
+ :request_headers => {}
45
+ }
46
+ subject.call(env)
47
+ expect(env[:request_headers]["Date"]).to eq(time.httpdate)
48
+ end
49
+
50
+ it "calculates the Content-MD5 header from the body if there isn't one" do
51
+ env = {
52
+ :method => :put,
53
+ :url => "http://s3.amazonaws.com/johnsmith/my-file.txt",
54
+ :body => "rhubarb",
55
+ :request_headers => {
56
+ "Content-Type" => "text/plain"
57
+ }
58
+ }
59
+ subject.call(env)
60
+ expect(env[:request_headers]["Content-MD5"]).to eq("lBxzvNO0KqwdCwPVMx2IYQ==")
61
+ end
62
+
63
+ it "lets you manually set the Content-MD5 header" do
64
+ env = {
65
+ :method => :put,
66
+ :url => "http://s3.amazonaws.com/johnsmith/my-file.txt",
67
+ :body => "rhubarb",
68
+ :request_headers => {
69
+ "Content-Type" => "text/plain",
70
+ "Content-MD5" => "test-content-md5"
71
+ }
72
+ }
73
+ subject.call(env)
74
+ expect(env[:request_headers]["Content-MD5"]).to eq("test-content-md5")
75
+
76
+ end
77
+
78
+ it "blows up if you have a request body, but no content type" do
79
+ env = {
80
+ :method => :put,
81
+ :url => "http://s3.amazonaws.com/johnsmith/my-file.txt",
82
+ :body => "rhubarb",
83
+ :request_headers => { }
84
+ }
85
+ expect { subject.call(env) }.to raise_error(AWSRaw::Error, "Can't make a request with a body but no Content-Type header")
86
+ end
87
+
88
+ end
@@ -1,67 +1,23 @@
1
1
  require 'awsraw/s3/query_string_signer'
2
+ require 'ostruct'
2
3
 
3
4
  describe AWSRaw::S3::QueryStringSigner do
4
- let(:access_key_id) { "AKIAIOSFODNN7EXAMPLE" }
5
- let(:secret_access_key) { "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" }
6
5
 
7
- subject { AWSRaw::S3::QueryStringSigner.new(access_key_id, secret_access_key) }
8
-
9
- context "examples from Amazon docs" do
10
- it "signs a get request correctly" do
11
- url = "http://s3.amazonaws.com/johnsmith/photos/puppy.jpg"
12
- expiry = 1175139620
13
- headers = {}
14
-
15
- subject.string_to_sign(url, expiry, {}).should ==
16
- "GET\n\n\n#{expiry}\n/johnsmith/photos/puppy.jpg"
17
-
18
- subject.query_string_hash(url, expiry).should == {
19
- "AWSAccessKeyId" => access_key_id,
20
- "Expires" => expiry.to_s,
21
- "Signature" => "NpgCjnDzrM%2BWFzoENXmpNDUsSn8%3D"
22
- }
23
-
24
- subject.sign_with_query_string(url, expiry).to_s.should ==
25
- "http://s3.amazonaws.com/johnsmith/photos/puppy.jpg?AWSAccessKeyId=#{access_key_id}&Expires=#{expiry}&Signature=NpgCjnDzrM%2BWFzoENXmpNDUsSn8%3D"
26
- end
27
-
28
- it "signs a get request to a non-us-east bucket" do
29
- url = "http://johnsmith.s3.amazonaws.com/photos/puppy.jpg"
30
- expiry = 1175139620
31
- headers = {}
32
-
33
- subject.string_to_sign(url, expiry, headers).should ==
34
- "GET\n\n\n#{expiry}\n/johnsmith/photos/puppy.jpg"
35
-
36
- subject.query_string_hash(url, expiry).should == {
37
- "AWSAccessKeyId" => access_key_id,
38
- "Expires" => expiry.to_s,
39
- "Signature" => "NpgCjnDzrM%2BWFzoENXmpNDUsSn8%3D"
40
- }
41
-
42
- subject.sign_with_query_string(url, expiry).to_s.should ==
43
- "http://johnsmith.s3.amazonaws.com/photos/puppy.jpg?AWSAccessKeyId=#{access_key_id}&Expires=#{expiry}&Signature=NpgCjnDzrM%2BWFzoENXmpNDUsSn8%3D"
44
- end
6
+ let(:credentials) do
7
+ OpenStruct.new(
8
+ :access_key_id => "AKIAIOSFODNN7EXAMPLE",
9
+ :secret_access_key => "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
10
+ )
45
11
  end
46
12
 
47
- context "custom headers" do
48
- let(:url) { "http://s3.amazonaws.com/johnsmith/" }
49
- let(:expiry) { 1175139620 }
50
-
51
- it "changes the signature based on the Content-MD5 header" do
52
- subject.string_to_sign(url, expiry, "Content-MD5" => "deadbeef").should ==
53
- "GET\ndeadbeef\n\n#{expiry}\n/johnsmith/"
54
- end
13
+ subject { described_class.new(credentials) }
55
14
 
56
- it "changes the signature based on the Content-Type header" do
57
- subject.string_to_sign(url, expiry, "Content-Type" => "image/png").should ==
58
- "GET\n\nimage/png\n#{expiry}\n/johnsmith/"
59
- end
15
+ # See the example in the AWS docs:
16
+ # http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
17
+ it "signs Amazon's example URI correctly" do
18
+ uri = subject.sign("http://johnsmith.s3.amazonaws.com/photos/puppy.jpg", Time.at(1175139620))
60
19
 
61
- it "changes the signature based on x-amz-* headers" do
62
- subject.string_to_sign(url, expiry, "x-amz-acl" => "public-read").should ==
63
- "GET\n\n\n#{expiry}\nx-amz-acl:public-read\n/johnsmith/"
64
- end
20
+ expect(uri.to_s).to eq("http://johnsmith.s3.amazonaws.com/photos/puppy.jpg?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Signature=NpgCjnDzrM%2BWFzoENXmpNDUsSn8%3D&Expires=1175139620")
65
21
  end
66
- end
67
22
 
23
+ end