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.
@@ -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