From 508b77e290ff46606ac54064d56cf35ca174ea97 Mon Sep 17 00:00:00 2001 From: chejingy Date: Tue, 7 Mar 2017 16:04:00 -0800 Subject: [PATCH 1/7] Add cross-region presign support for #copy_db_cluster_snapshot and #create_db_cluster --- aws-sdk-core/lib/aws-sdk-core/api/customizations.rb | 12 ++++++++++++ .../aws-sdk-core/plugins/rds_cross_region_copying.rb | 7 ++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/aws-sdk-core/lib/aws-sdk-core/api/customizations.rb b/aws-sdk-core/lib/aws-sdk-core/api/customizations.rb index 9c4f0652325..bd29585f223 100644 --- a/aws-sdk-core/lib/aws-sdk-core/api/customizations.rb +++ b/aws-sdk-core/lib/aws-sdk-core/api/customizations.rb @@ -152,6 +152,10 @@ def apply_plugins(client_class) {"shape" => "String"} api['shapes']['CreateDBInstanceReadReplicaMessage']['members']['DestinationRegion'] = {"shape" => "String"} + api['shapes']['CopyDBClusterSnapshotMessage']['members']['DestinationRegion'] = + {"shape" => "String"} + api['shapes']['CreateDBClusterMessage']['members']['DestinationRegion'] = + {"shape" => "String"} end doc('rds') do |docs| @@ -163,6 +167,14 @@ def apply_plugins(client_class) "

The region which you are copying an encrypted snapshot from.

" + "

This is a required paramter that allows SDK to compute a pre-signed Url and" + " populate PreSignedURL parameter on your behalf.

" + docs['shapes']['String']['refs']['CopyDBClusterSnapshotMessage$SourceRegion'] = + "

The region which you are copying an encrypted snapshot from.

" + + "

This is a required paramter that allows SDK to compute a pre-signed Url and" + + " populate PreSignedURL parameter on your behalf.

" + docs['shapes']['String']['refs']['CreateDBClusterMessage$SourceRegion'] = + "

The region which you are copying an encrypted snapshot from.

" + + "

This is a required paramter that allows SDK to compute a pre-signed Url and" + + " populate PreSignedURL parameter on your behalf.

" end api('s3') do |api| diff --git a/aws-sdk-core/lib/aws-sdk-core/plugins/rds_cross_region_copying.rb b/aws-sdk-core/lib/aws-sdk-core/plugins/rds_cross_region_copying.rb index 7c9b9bc6562..6c61c0b6ec8 100644 --- a/aws-sdk-core/lib/aws-sdk-core/plugins/rds_cross_region_copying.rb +++ b/aws-sdk-core/lib/aws-sdk-core/plugins/rds_cross_region_copying.rb @@ -57,7 +57,12 @@ def presigned_url(context, params) handler( Handler, step: :initialize, - operations: [:copy_db_snapshot, :create_db_instance_read_replica] + operations: [ + :copy_db_snapshot, + :create_db_instance_read_replica, + :copy_db_cluster_snapshot, + :create_db_cluster + ] ) end end From baf05e61dc2b428c215067f22313ea6f7172903a Mon Sep 17 00:00:00 2001 From: chejingy Date: Wed, 5 Apr 2017 11:07:53 -0700 Subject: [PATCH 2/7] Add enhanced presign request feature --- .../features/s3/presigned_request.feature | 51 +++++ aws-sdk-core/features/s3/step_definitions.rb | 31 +++ aws-sdk-core/lib/aws-sdk-core/s3.rb | 1 + .../lib/aws-sdk-core/s3/presigned_request.rb | 177 ++++++++++++++++ .../spec/aws/s3/presigned_request_spec.rb | 189 ++++++++++++++++++ .../aws-sdk-resources/services/s3/object.rb | 45 +++++ 6 files changed, 494 insertions(+) create mode 100644 aws-sdk-core/features/s3/presigned_request.feature create mode 100644 aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb create mode 100644 aws-sdk-core/spec/aws/s3/presigned_request_spec.rb diff --git a/aws-sdk-core/features/s3/presigned_request.feature b/aws-sdk-core/features/s3/presigned_request.feature new file mode 100644 index 00000000000..d1cb61cba3f --- /dev/null +++ b/aws-sdk-core/features/s3/presigned_request.feature @@ -0,0 +1,51 @@ +# language: en +@s3 @presigned_request +Feature: Enhanced presigned request interface + + Background: + Given I create a bucket + + Scenario: Presigning a put object request + When I create a presigned request for "put_object" with: + | key | test | + | acl | public-read | + And I send an HTTP put request with uri headers and body "hello" + And I make an unauthenticated HTTPS GET request for key "test" + Then the response should be "hello" + + Scenario: Presigning a HTTPS get object request + When I put "signed" to the key "retrieve_me" + And I create a presigned request for "get_object" with: + | key | retrieve_me | + And I send an HTTP "get" request with uri and headers + Then the response should be "signed" + + Scenario: Presigning a HTTP get object request + When I put "signed" to the key "retrieve_me" + And I create a non-secure presigned request for "get_object" with: + | key | retrieve_me | + And I send an HTTP "get" request with uri and headers + Then the response should be "signed" + + @headers + Scenario: Presigning a put object request with custom headers + Given I have a custom header hash to sign: + | foo | bar | + When I create a presigned request for "put_object" with the hash and: + | key | test | + | acl | public-read | + And I send an HTTP "put" request with uri and headers + Then Signed headers should be: + | x-amz-acl | public-read | + | x-amz-foo | bar | + Then the response should have a 200 status code + + @headers + Scenario: Presigning a put object request with whitelist headers + When I create a presigned request for "put_object" with: + | key | test | + | cache-control | max-age=20000 | + And I send an HTTP "put" request with uri and headers + Then Signed headers should be: + | x-amz-cache-control | max-age=20000 | + Then the response should have a 200 status code diff --git a/aws-sdk-core/features/s3/step_definitions.rb b/aws-sdk-core/features/s3/step_definitions.rb index 2d9d8a572fb..11a1b5f66b9 100644 --- a/aws-sdk-core/features/s3/step_definitions.rb +++ b/aws-sdk-core/features/s3/step_definitions.rb @@ -280,3 +280,34 @@ def create_bucket(options = {}) Then(/^the location constraint should be "([^"]*)"$/) do |lc| expect(@response.location_constraint).to eq(lc) end + +When(/^I create a (non-secure )?presigned request for "([^"]*)" with( the hash and)?:$/) do |non_secure, method, hash, params| + params = symbolized_params(params) + params[:bucket] = @bucket_name + params[:secure] = false if non_secure + params[:headers] = @custom_headers if hash + presigned_request = Aws::S3::PresignedRequest.new(method.to_sym, params) + @uri = presigned_request.uri + @headers = presigned_request.headers +end + +When(/^I send an HTTP put request with uri headers and body "([^"]*)"$/) do |body| + http = Net::HTTP.new(@uri.host) + req = Net::HTTP::Put.new(@uri.request_uri, @headers) + req.body = body + @resp = http.request(req) +end + +When(/^I send an HTTP "([^"]*)" request with uri and headers$/) do |method| + http = Net::HTTP.new(@uri.host) + req = Net::HTTP.const_get(method.capitalize).new(@uri.request_uri, @headers) + @resp = http.request(req) +end + +Then(/^Signed headers should be:$/) do |table| + table.rows_hash.each {|k, v| expect(@headers[k]).to eq(v)} +end + +Given(/^I have a custom header hash to sign:$/) do |hash| + @custom_headers = hash.rows_hash.inject({}) {|h, (k, v)| h[k] = v; h} +end diff --git a/aws-sdk-core/lib/aws-sdk-core/s3.rb b/aws-sdk-core/lib/aws-sdk-core/s3.rb index bac2ac62f7f..c7485254f31 100644 --- a/aws-sdk-core/lib/aws-sdk-core/s3.rb +++ b/aws-sdk-core/lib/aws-sdk-core/s3.rb @@ -11,6 +11,7 @@ module Aws module S3 autoload :Presigner, 'aws-sdk-core/s3/presigner' + autoload :PresignedRequest, 'aws-sdk-core/s3/presigned_request' autoload :BucketRegionCache, 'aws-sdk-core/s3/bucket_region_cache' # A cache of discovered bucket regions. You can call `#bucket_added` diff --git a/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb b/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb new file mode 100644 index 00000000000..4ff76b45cf4 --- /dev/null +++ b/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb @@ -0,0 +1,177 @@ +require 'aws-sigv4' + +module Aws + module S3 + + # Utility class for creating a presigned request with presigned URL and signed + # headers hash. + # + # Example Use: + # + # presigned_request = Aws::S3::PresignedRequest.new( + # :put_object, + # bucket: "bucket", + # key: "key", + # acl: "private", + # ... + # headers: { + # foo: "bar" + # } + # ) + # + # presigned_request.uri + # # => # {"x-amz-acl"=>"private", "x-amz-foo"=>'bar', ...} + # + class PresignedRequest + + # @api private + ONE_WEEK = 60 * 60 * 24 * 7 + + # @param [Symbol] method Symbolized method name of the operation you + # want to presign. + # + # @option params [Client] :client Optionally provide an existing + # S3 client + # + # @option params [Integer] :expires_in (900) + # How long the presigned URL should be valid for. Defaults + # to 15 minutes (900 seconds). + # + # @option params [Boolean] :secure (true) When `false`, a HTTP URL + # is returned instead of the default HTTPS URL. + # + # @option params [Boolean] :virtual_host (false) When + # `true`, the bucket name will be used as the hostname. + # This will cause the returned URL to be 'http' and not + # 'https'. + # + # @option options [Hash] :headers ({}) Customized headers to + # be signed and sent along with the request. + # + # @option options [Time] :time (Time.now) Time of the signature. + # You should only set this value for testing. + def initialize(method, params = {}) + @bucket, @key = bucket_and_key(params) + @client = params.delete(:client) || Aws::S3::Client.new + @http_method = method[/[^_]+/].upcase + @time = params.delete(:time) || Time.now + @expires = expires_in(params) + @virtual_host = !!params.delete(:virtual_host) + @secure = params.delete(:secure) + @headers = build_headers(params) + end + + # Returns the presigned URL for the S3 operation + # + # @return [HTTPS::URI, HTTP::URI] + def uri + v4_signer.presign_url( + http_method: @http_method, + url: build_url(@client.config.endpoint), + headers: @headers, + body_digest: 'UNSIGNED-PAYLOAD', + expires_in: @expires, + time: @time + ) + end + + # Returns signed headers + # + # @return [Hash] headers + attr_reader :headers + + private + + def v4_signer + Aws::Sigv4::Signer.new( + service: 's3', + region: @client.config.region, + credentials_provider: @client.config.credentials, + apply_checksum_header: @client.config.compute_checksums, + uri_escape_path: false, + unsigned_headers: Aws::Signers::V4::BLACKLIST_HEADERS - @headers.keys + ) + end + + def bucket_and_key(params) + if params[:bucket].nil? or params[:bucket] == '' + raise ArgumentError, ":bucket must not be blank" + end + if params[:key].nil? or params[:key] == '' + raise ArgumentError, ":key must not be blank" + end + [ params.delete(:bucket), params.delete(:key) ] + end + + def expires_in(params) + if expires = params.delete(:expires_in) + if expires > ONE_WEEK + msg = "expires_in value of #{expires} exceeds one-week maximum" + raise ArgumentError, msg + end + expires + end + end + + def build_headers(params) + h = params.delete(:headers) || {} + headers = h.inject({}) {|h, (k, v)| h["x-amz-#{k.to_s.gsub(/_/, '-')}"] = v; h} + params.inject(headers) {|h, (k, v)| h["x-amz-#{k.to_s.gsub(/_/, '-')}"] = v; h} + end + + def build_url(uri) + if @virtual_host + uri = virtual_host(uri) + uri.path = "/#{@key}" + elsif @client.config.force_path_style + uri = scheme(uri) + uri.path = "/#{@bucket}/#{@key}" + else + uri = dns(uri) + uri.path = "/#{@key}" + end + uri + end + + def virtual_host(uri) + uri = http_scheme(uri) + uri.host = @bucket + uri + end + + def dns(endpoint) + return endpoint unless Aws::Plugins::S3BucketDns.dns_compatible?(@bucket, @secure) + endpoint.host = "#{@bucket}.#{endpoint.host}" + scheme(endpoint) + end + + def scheme(url) + return url if @secure.nil? + if @secure + https_scheme(url) + else + http_scheme(url) + end + end + + def http_scheme(uri) + return uri if uri.scheme == 'http' + uri.scheme = 'http' + uri.port = 80 + uri + end + + def https_scheme(uri) + return uri if uri.scheme == 'https' + uri.scheme = 'https' + uri.port = 443 + uri + end + + end + + end +end diff --git a/aws-sdk-core/spec/aws/s3/presigned_request_spec.rb b/aws-sdk-core/spec/aws/s3/presigned_request_spec.rb new file mode 100644 index 00000000000..4a9f868ec9c --- /dev/null +++ b/aws-sdk-core/spec/aws/s3/presigned_request_spec.rb @@ -0,0 +1,189 @@ +require 'spec_helper' + +module Aws + module S3 + describe PresignedRequest do + + before (:each) do + Aws.config[:s3] = { + region: 'us-east-1', + credentials: Credentials.new( + "AKIAIOSFODNN7EXAMPLE", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), + retry_limit: 0 + } + + allow(Time).to receive(:now).and_return(now) + allow(now).to receive(:utc).and_return(utc) + allow(utc).to receive(:strftime).and_return(datetime) + end + + after(:each) do + Aws.config = {} + end + + let(:client) { Aws::S3::Client.new } + + let(:now) { double('now') } + let(:utc) { double('utc-time') } + let(:datetime) { '20130524T000000Z' } + + describe '#initialize' do + + it 'accepts an injected S3 client' do + req = PresignedRequest.new( + :put_object, + bucket: 'bucket', + key: 'key', + client: client + ) + expect(req.class).to eq(Aws::S3::PresignedRequest) + end + + it 'can be constructed without a client' do + req = PresignedRequest.new( + :put_object, + bucket: 'bucket', + key: 'key' + ) + expect(req.class).to eq(Aws::S3::PresignedRequest) + end + + it 'raises error when missing valid :bucket or :key' do + expect { + req = PresignedRequest.new( + :put_object, + client: client, + key: 'key' + ) + }.to raise_error(ArgumentError, /bucket must not be blank/) + expect { + req = PresignedRequest.new( + :put_object, + client: client, + bucket: 'bucket', + key: '' + ) + }.to raise_error(ArgumentError, /key must not be blank/) + end + + it 'raises when expires_in length is over 1 week' do + expect { + req = PresignedRequest.new( + :get_bucket, + client: client, + bucket: 'bucket', + key: 'key', + expires_in: (7 * 86400) + 1 + ) + }.to raise_error(ArgumentError) + end + + end + + describe "#uri" do + + it 'can presign #get_object to spec' do + bucket = "examplebucket" + key = "test.txt" + expected_url = "https://examplebucket.s3.amazonaws.com/test.txt"\ + "?X-Amz-Algorithm=AWS4-HMAC-SHA256"\ + "&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2F"\ + "us-east-1%2Fs3%2Faws4_request"\ + "&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400"\ + "&X-Amz-SignedHeaders=host"\ + "&X-Amz-Signature=aeeed9bbccd4d02ee5c0109b86d86835f995330da4c26595"\ + "7d157751f604d404" + + req = PresignedRequest.new( + :get_bucket, + client: client, + bucket: bucket, + key: key, + expires_in: 86400 + ) + expect(req.uri.to_s).to eq(expected_url) + end + + it 'uses the correct :endpoint scheme' do + client.config.endpoint = URI("http://example.com") + req = PresignedRequest.new( + :get_bucket, + client: client, + bucket: 'bucket', + key: 'key' + ) + expect(req.uri.to_s).to match(/^http:/) + end + + it 'can generate http (non-secure) urls' do + req = PresignedRequest.new( + :get_bucket, + client: client, + bucket: 'bucket', + key: 'key', + secure: false + ) + expect(req.uri.to_s).to match(/^http:/) + end + + it 'can generate virtual host style urls' do + req = PresignedRequest.new( + :get_bucket, + client: client, + bucket: 'virtual.hosted.com', + key: 'foo', + virtual_host: true + ) + expect(req.uri.to_s).to match(/^http:\/\/virtual.hosted.com\/foo/) + end + + it 'returns same url when called twice' do + req = PresignedRequest.new( + :get_bucket, + client: client, + bucket: 'virtual.hosted.com', + key: 'foo', + virtual_host: true + ) + first = req.uri.to_s + second = req.uri.to_s + expect(first).to eq(second) + expect(second).to match(/^http:\/\/virtual.hosted.com\/foo/) + end + end + + describe "#headers" do + + it 'allows overwrite blacklist headers in Sigv4' do + req = PresignedRequest.new( + :put_bucket, + client: client, + bucket: 'bucket', + key: 'key', + cache_control: 'max-age=20000' + ) + expect(req.headers['x-amz-cache-control']).to eq('max-age=20000') + expect(req.uri.to_s).to match(/.*X-Amz-SignedHeaders=host%3Bx-amz-cache-control.*/) + end + + it 'allows customize header signing' do + req = PresignedRequest.new( + :put_bucket, + client: client, + bucket: 'bucket', + key: 'key', + headers: { + 'foo' => 'bar' + } + ) + expect(req.headers['x-amz-foo']).to eq('bar') + expect(req.uri.to_s).to match(/.*X-Amz-SignedHeaders=host%3Bx-amz-foo.*/) + + end + + end + + end + end +end diff --git a/aws-sdk-resources/lib/aws-sdk-resources/services/s3/object.rb b/aws-sdk-resources/lib/aws-sdk-resources/services/s3/object.rb index f023c335ace..da414ac8371 100644 --- a/aws-sdk-resources/lib/aws-sdk-resources/services/s3/object.rb +++ b/aws-sdk-resources/lib/aws-sdk-resources/services/s3/object.rb @@ -192,6 +192,51 @@ def presigned_url(http_method, params = {}) )) end + # Returns Aws::S3::PresignedRequest object that provides both presigned + # url and signed headers hash. + # + # r = s3.bucket('bucket-name').object('obj-key').presigned_request( + # :put, + # acl: 'public-read' + # ) + # # => # # {"x-amz-acl"=>"public-read"} + # + # @param [Symbol] http_method + # The HTTP method to generate a presigned URL for. Valid values + # are `:get`, `:put`, `:head`, and `:delete`. + # + # @option params [Integer] :expires_in (900) + # How long the presigned URL should be valid for. Defaults + # to 15 minutes (900 seconds). + # + # @option params [Boolean] :secure (true) When `false`, a HTTP URL + # is returned instead of the default HTTPS URL. + # + # @option params [Boolean] :virtual_host (false) When + # `true`, the bucket name will be used as the hostname. + # This will cause the returned URL to be 'http' and not + # 'https'. + # + # @option options [Hash] :headers ({}) Customized headers to + # be signed and sent along with the request. + # + # @option options [Time] :time (Time.now) Time of the signature. + # You should only set this value for testing. + # + def presigned_request(http_method, params = {}) + PresignedRequest.new("#{http_method.downcase}_object", params.merge( + client: client, + bucket: bucket_name, + key: key + )) + end + # Returns the public (un-signed) URL for this object. # # s3.bucket('bucket-name').object('obj-key').public_url From 80393c2ed4c98c7b6ef467ef68387a7ea85eac2c Mon Sep 17 00:00:00 2001 From: chejingy Date: Wed, 5 Apr 2017 11:25:54 -0700 Subject: [PATCH 3/7] Add to object_summary as well --- .../lib/aws-sdk-resources/services/s3/object_summary.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aws-sdk-resources/lib/aws-sdk-resources/services/s3/object_summary.rb b/aws-sdk-resources/lib/aws-sdk-resources/services/s3/object_summary.rb index 5785758d892..034dc9e7d93 100644 --- a/aws-sdk-resources/lib/aws-sdk-resources/services/s3/object_summary.rb +++ b/aws-sdk-resources/lib/aws-sdk-resources/services/s3/object_summary.rb @@ -44,6 +44,14 @@ def presigned_url(http_method, params = {}) object.presigned_url(http_method, params) end + # @param (see Object#presigned_request) + # @options (see Object#presigned_request) + # @return (see Object#presigned_request) + # @see Object#presigned_request + def presigned_request(http_method, params = {}) + object.presigned_request(http_method, params) + end + # @param (see Object#public_url) # @options (see Object#public_url) # @return (see Object#public_url) From 464defaddd3dc12d7840cea70e882644570fe050 Mon Sep 17 00:00:00 2001 From: chejingy Date: Thu, 6 Apr 2017 13:32:45 -0700 Subject: [PATCH 4/7] clean up a bit --- .../lib/aws-sdk-core/s3/presigned_request.rb | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb b/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb index 4ff76b45cf4..88afead5d1d 100644 --- a/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb +++ b/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb @@ -123,15 +123,16 @@ def build_headers(params) end def build_url(uri) + # build url from endpoint + uri = scheme(uri) unless @virtual_host if @virtual_host uri = virtual_host(uri) uri.path = "/#{@key}" elsif @client.config.force_path_style - uri = scheme(uri) uri.path = "/#{@bucket}/#{@key}" else + # bucket DNS uri = dns(uri) - uri.path = "/#{@key}" end uri end @@ -142,18 +143,22 @@ def virtual_host(uri) uri end - def dns(endpoint) - return endpoint unless Aws::Plugins::S3BucketDns.dns_compatible?(@bucket, @secure) - endpoint.host = "#{@bucket}.#{endpoint.host}" - scheme(endpoint) + def dns(uri) + if Aws::Plugins::S3BucketDns.dns_compatible?(@bucket, @secure) + uri.host = "#{@bucket}.#{uri.host}" + uri.path = "/#{@key}" + else + uri.path = "/#{@bucket}/#{@key}" + end + uri end - def scheme(url) - return url if @secure.nil? + def scheme(uri) + return uri if @secure.nil? if @secure - https_scheme(url) + https_scheme(uri) else - http_scheme(url) + http_scheme(uri) end end From cc457c1c571f049c67fd111c3ca7efa62bdf96c7 Mon Sep 17 00:00:00 2001 From: chejingy Date: Thu, 6 Apr 2017 14:44:38 -0700 Subject: [PATCH 5/7] clean-up scheme --- .../lib/aws-sdk-core/s3/presigned_request.rb | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb b/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb index 88afead5d1d..77a0e587c58 100644 --- a/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb +++ b/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb @@ -122,25 +122,24 @@ def build_headers(params) params.inject(headers) {|h, (k, v)| h["x-amz-#{k.to_s.gsub(/_/, '-')}"] = v; h} end + # Build url from endpoint def build_url(uri) - # build url from endpoint uri = scheme(uri) unless @virtual_host if @virtual_host - uri = virtual_host(uri) - uri.path = "/#{@key}" + virtual_host(uri) elsif @client.config.force_path_style uri.path = "/#{@bucket}/#{@key}" + uri else # bucket DNS - uri = dns(uri) + dns(uri) end - uri end def virtual_host(uri) - uri = http_scheme(uri) uri.host = @bucket - uri + uri.path = "/#{@key}" + http_scheme(uri) end def dns(uri) From 494fe0612257bcbe043eb481acdb606c7ffe2261 Mon Sep 17 00:00:00 2001 From: chejingy Date: Mon, 10 Apr 2017 17:32:16 -0700 Subject: [PATCH 6/7] use client handler to build request --- .../features/s3/presigned_request.feature | 6 +- aws-sdk-core/features/s3/step_definitions.rb | 4 +- .../lib/aws-sdk-core/s3/presigned_request.rb | 134 ++++++++---------- .../spec/aws/s3/presigned_request_spec.rb | 112 +++++++-------- 4 files changed, 121 insertions(+), 135 deletions(-) diff --git a/aws-sdk-core/features/s3/presigned_request.feature b/aws-sdk-core/features/s3/presigned_request.feature index d1cb61cba3f..50fe795ee9e 100644 --- a/aws-sdk-core/features/s3/presigned_request.feature +++ b/aws-sdk-core/features/s3/presigned_request.feature @@ -29,7 +29,7 @@ Feature: Enhanced presigned request interface @headers Scenario: Presigning a put object request with custom headers - Given I have a custom header hash to sign: + Given I have a header hash to sign: | foo | bar | When I create a presigned request for "put_object" with the hash and: | key | test | @@ -42,7 +42,9 @@ Feature: Enhanced presigned request interface @headers Scenario: Presigning a put object request with whitelist headers - When I create a presigned request for "put_object" with: + Given I have a header hash to sign: + | cache-control | max-age=20000 | + When I create a presigned request for "put_object" with the hash and: | key | test | | cache-control | max-age=20000 | And I send an HTTP "put" request with uri and headers diff --git a/aws-sdk-core/features/s3/step_definitions.rb b/aws-sdk-core/features/s3/step_definitions.rb index 11a1b5f66b9..4023610a376 100644 --- a/aws-sdk-core/features/s3/step_definitions.rb +++ b/aws-sdk-core/features/s3/step_definitions.rb @@ -286,6 +286,8 @@ def create_bucket(options = {}) params[:bucket] = @bucket_name params[:secure] = false if non_secure params[:headers] = @custom_headers if hash + params[:region] = @client.config.region + params[:credentials] = @client.config.credentials presigned_request = Aws::S3::PresignedRequest.new(method.to_sym, params) @uri = presigned_request.uri @headers = presigned_request.headers @@ -308,6 +310,6 @@ def create_bucket(options = {}) table.rows_hash.each {|k, v| expect(@headers[k]).to eq(v)} end -Given(/^I have a custom header hash to sign:$/) do |hash| +Given(/^I have a header hash to sign:$/) do |hash| @custom_headers = hash.rows_hash.inject({}) {|h, (k, v)| h[k] = v; h} end diff --git a/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb b/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb index 77a0e587c58..5c88183dee0 100644 --- a/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb +++ b/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb @@ -30,12 +30,10 @@ class PresignedRequest # @api private ONE_WEEK = 60 * 60 * 24 * 7 + # TODO # @param [Symbol] method Symbolized method name of the operation you # want to presign. # - # @option params [Client] :client Optionally provide an existing - # S3 client - # # @option params [Integer] :expires_in (900) # How long the presigned URL should be valid for. Defaults # to 15 minutes (900 seconds). @@ -54,14 +52,22 @@ class PresignedRequest # @option options [Time] :time (Time.now) Time of the signature. # You should only set this value for testing. def initialize(method, params = {}) - @bucket, @key = bucket_and_key(params) - @client = params.delete(:client) || Aws::S3::Client.new - @http_method = method[/[^_]+/].upcase + bucket_and_key(params) + @credentials, @region = credentials_and_region(params) + client = Aws::S3::Client.new(region: @region, credentials: @credentials) + + @http_method = Aws::S3::Client.api.operation(method).http_method @time = params.delete(:time) || Time.now @expires = expires_in(params) - @virtual_host = !!params.delete(:virtual_host) - @secure = params.delete(:secure) - @headers = build_headers(params) + virtual_host = !!params.delete(:virtual_host) + secure = params.delete(:secure) + # build customize or whitelist headers when presents + headers = build_headers(params.delete(:headers) || {}) + + req = client.build_request(method, params) + endpoint_and_headers(req, secure, virtual_host) + @endpoint, req_headers = req.send_request.data + @headers = headers.merge(req_headers) end # Returns the presigned URL for the S3 operation @@ -70,7 +76,7 @@ def initialize(method, params = {}) def uri v4_signer.presign_url( http_method: @http_method, - url: build_url(@client.config.endpoint), + url: @endpoint, headers: @headers, body_digest: 'UNSIGNED-PAYLOAD', expires_in: @expires, @@ -82,30 +88,30 @@ def uri # # @return [Hash] headers attr_reader :headers +=begin + def headers + signature = v4_signer.sign_request( + http_method: @http_method, + url: @endpoint, + headers: @headers, + body: '' + ) + signature.headers.merge(@headers) + end +=end private def v4_signer Aws::Sigv4::Signer.new( service: 's3', - region: @client.config.region, - credentials_provider: @client.config.credentials, - apply_checksum_header: @client.config.compute_checksums, + region: @region, + credentials_provider: @credentials, uri_escape_path: false, unsigned_headers: Aws::Signers::V4::BLACKLIST_HEADERS - @headers.keys ) end - def bucket_and_key(params) - if params[:bucket].nil? or params[:bucket] == '' - raise ArgumentError, ":bucket must not be blank" - end - if params[:key].nil? or params[:key] == '' - raise ArgumentError, ":key must not be blank" - end - [ params.delete(:bucket), params.delete(:key) ] - end - def expires_in(params) if expires = params.delete(:expires_in) if expires > ONE_WEEK @@ -116,65 +122,47 @@ def expires_in(params) end end - def build_headers(params) - h = params.delete(:headers) || {} - headers = h.inject({}) {|h, (k, v)| h["x-amz-#{k.to_s.gsub(/_/, '-')}"] = v; h} - params.inject(headers) {|h, (k, v)| h["x-amz-#{k.to_s.gsub(/_/, '-')}"] = v; h} - end - - # Build url from endpoint - def build_url(uri) - uri = scheme(uri) unless @virtual_host - if @virtual_host - virtual_host(uri) - elsif @client.config.force_path_style - uri.path = "/#{@bucket}/#{@key}" - uri - else - # bucket DNS - dns(uri) - end + def bucket_and_key(params) + bucket = params.fetch(:bucket) + key = params.fetch(:key) + raise ArgumentError, 'bucket must not be blank' if bucket == '' + raise ArgumentError, 'key must not be blank' if key == '' end - def virtual_host(uri) - uri.host = @bucket - uri.path = "/#{@key}" - http_scheme(uri) + def credentials_and_region(params) + credentials = params.delete(:credentials) + region = params.delete(:region) + raise ArgumentError, ':credentials must be provided for a presigned request' if credentials.nil? + raise ArgumentError, ':region must be provided for a presigned request' if region.nil? + [credentials, region] end - def dns(uri) - if Aws::Plugins::S3BucketDns.dns_compatible?(@bucket, @secure) - uri.host = "#{@bucket}.#{uri.host}" - uri.path = "/#{@key}" - else - uri.path = "/#{@bucket}/#{@key}" - end - uri + def build_headers(hash) + hash.inject({}) {|h, (k, v)| h["x-amz-#{k.to_s.gsub(/_/, '-')}"] = v; h} end - def scheme(uri) - return uri if @secure.nil? - if @secure - https_scheme(uri) - else - http_scheme(uri) + def endpoint_and_headers(req, secure, virtual_host) + req.handlers.remove(Plugins::S3BucketDns::Handler) if @virtual_host + req.handlers.remove(Plugins::S3RequestSigner::SigningHandler) + req.handlers.remove(Seahorse::Client::Plugins::ContentLength::Handler) + req.handle(step: :send) do |context| + headers = context.http_request.headers.inject({}) do |h, (k, v)| + h[k] = v unless k.match(/x-amz-/).nil? + h + end + endpoint = context.http_request.endpoint + if secure == false || virtual_host + endpoint.scheme = 'http' + endpoint.port = 80 + end + if virtual_host + endpoint.host = context.params[:bucket] + endpoint.path.sub!("/#{context.params[:bucket]}", '') + end + Seahorse::Client::Response.new(context: context, data: [endpoint, headers]) end end - def http_scheme(uri) - return uri if uri.scheme == 'http' - uri.scheme = 'http' - uri.port = 80 - uri - end - - def https_scheme(uri) - return uri if uri.scheme == 'https' - uri.scheme = 'https' - uri.port = 443 - uri - end - end end diff --git a/aws-sdk-core/spec/aws/s3/presigned_request_spec.rb b/aws-sdk-core/spec/aws/s3/presigned_request_spec.rb index 4a9f868ec9c..5f5b7abe16c 100644 --- a/aws-sdk-core/spec/aws/s3/presigned_request_spec.rb +++ b/aws-sdk-core/spec/aws/s3/presigned_request_spec.rb @@ -5,24 +5,17 @@ module S3 describe PresignedRequest do before (:each) do - Aws.config[:s3] = { - region: 'us-east-1', - credentials: Credentials.new( - "AKIAIOSFODNN7EXAMPLE", - "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), - retry_limit: 0 - } - allow(Time).to receive(:now).and_return(now) allow(now).to receive(:utc).and_return(utc) allow(utc).to receive(:strftime).and_return(datetime) end - after(:each) do - Aws.config = {} - end - - let(:client) { Aws::S3::Client.new } + let(:region) {'us-east-1'} + let(:credentials) { + Credentials.new( + "AKIAIOSFODNN7EXAMPLE", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY") + } let(:now) { double('now') } let(:utc) { double('utc-time') } @@ -30,37 +23,39 @@ module S3 describe '#initialize' do - it 'accepts an injected S3 client' do - req = PresignedRequest.new( - :put_object, - bucket: 'bucket', - key: 'key', - client: client - ) - expect(req.class).to eq(Aws::S3::PresignedRequest) - end - - it 'can be constructed without a client' do - req = PresignedRequest.new( - :put_object, - bucket: 'bucket', - key: 'key' - ) - expect(req.class).to eq(Aws::S3::PresignedRequest) + it 'raises error when missing :region or :credentials' do + expect { + req = PresignedRequest.new( + :put_object, + credentials: credentials, + bucket: 'bucket', + key: 'key' + ) + }.to raise_error(ArgumentError) + expect { + req = PresignedRequest.new( + :put_object, + region: region, + bucket: 'bucket', + key: 'key' + ) + }.to raise_error(ArgumentError) end it 'raises error when missing valid :bucket or :key' do expect { req = PresignedRequest.new( :put_object, - client: client, + region: region, + credentials: credentials, key: 'key' ) - }.to raise_error(ArgumentError, /bucket must not be blank/) + }.to raise_error(KeyError) expect { req = PresignedRequest.new( :put_object, - client: client, + region: region, + credentials: credentials, bucket: 'bucket', key: '' ) @@ -70,8 +65,9 @@ module S3 it 'raises when expires_in length is over 1 week' do expect { req = PresignedRequest.new( - :get_bucket, - client: client, + :get_object, + region: region, + credentials: credentials, bucket: 'bucket', key: 'key', expires_in: (7 * 86400) + 1 @@ -96,8 +92,9 @@ module S3 "7d157751f604d404" req = PresignedRequest.new( - :get_bucket, - client: client, + :get_object, + region: region, + credentials: credentials, bucket: bucket, key: key, expires_in: 86400 @@ -105,21 +102,11 @@ module S3 expect(req.uri.to_s).to eq(expected_url) end - it 'uses the correct :endpoint scheme' do - client.config.endpoint = URI("http://example.com") - req = PresignedRequest.new( - :get_bucket, - client: client, - bucket: 'bucket', - key: 'key' - ) - expect(req.uri.to_s).to match(/^http:/) - end - it 'can generate http (non-secure) urls' do req = PresignedRequest.new( - :get_bucket, - client: client, + :get_object, + region: region, + credentials: credentials, bucket: 'bucket', key: 'key', secure: false @@ -129,8 +116,9 @@ module S3 it 'can generate virtual host style urls' do req = PresignedRequest.new( - :get_bucket, - client: client, + :get_object, + region: region, + credentials: credentials, bucket: 'virtual.hosted.com', key: 'foo', virtual_host: true @@ -140,8 +128,9 @@ module S3 it 'returns same url when called twice' do req = PresignedRequest.new( - :get_bucket, - client: client, + :get_object, + region: region, + credentials: credentials, bucket: 'virtual.hosted.com', key: 'foo', virtual_host: true @@ -157,11 +146,15 @@ module S3 it 'allows overwrite blacklist headers in Sigv4' do req = PresignedRequest.new( - :put_bucket, - client: client, + :put_object, + region: region, + credentials: credentials, bucket: 'bucket', key: 'key', - cache_control: 'max-age=20000' + cache_control: 'max-age=20000', + headers: { + cache_control: 'max-age=20000', + } ) expect(req.headers['x-amz-cache-control']).to eq('max-age=20000') expect(req.uri.to_s).to match(/.*X-Amz-SignedHeaders=host%3Bx-amz-cache-control.*/) @@ -169,8 +162,9 @@ module S3 it 'allows customize header signing' do req = PresignedRequest.new( - :put_bucket, - client: client, + :put_object, + region: region, + credentials: credentials, bucket: 'bucket', key: 'key', headers: { From c207f92abafe9763603bc5d1c12f2659f9c4b528 Mon Sep 17 00:00:00 2001 From: chejingy Date: Mon, 10 Apr 2017 17:33:26 -0700 Subject: [PATCH 7/7] fix typo --- aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb b/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb index 5c88183dee0..c8abfa67ccb 100644 --- a/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb +++ b/aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb @@ -142,7 +142,7 @@ def build_headers(hash) end def endpoint_and_headers(req, secure, virtual_host) - req.handlers.remove(Plugins::S3BucketDns::Handler) if @virtual_host + req.handlers.remove(Plugins::S3BucketDns::Handler) if virtual_host req.handlers.remove(Plugins::S3RequestSigner::SigningHandler) req.handlers.remove(Seahorse::Client::Plugins::ContentLength::Handler) req.handle(step: :send) do |context|