Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add enhanced presign request feature #1477

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions aws-sdk-core/features/s3/presigned_request.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 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 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
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
Then Signed headers should be:
| x-amz-cache-control | max-age=20000 |
Then the response should have a 200 status code
33 changes: 33 additions & 0 deletions aws-sdk-core/features/s3/step_definitions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,36 @@ 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
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
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 header hash to sign:$/) do |hash|
@custom_headers = hash.rows_hash.inject({}) {|h, (k, v)| h[k] = v; h}
end
1 change: 1 addition & 0 deletions aws-sdk-core/lib/aws-sdk-core/s3.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
169 changes: 169 additions & 0 deletions aws-sdk-core/lib/aws-sdk-core/s3/presigned_request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
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
# # => #<URI::HTTPS https://...&X-Amz-SignedHeaders=host%3Bx-amz-acl%3Bx-amz-foo...
#
# presigned_request.headers
# # => {"x-amz-acl"=>"private", "x-amz-foo"=>'bar', ...}
#
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 [Integer<Seconds>] :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_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)
# 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
#
# @return [HTTPS::URI, HTTP::URI]
def uri
v4_signer.presign_url(
http_method: @http_method,
url: @endpoint,
headers: @headers,
body_digest: 'UNSIGNED-PAYLOAD',
expires_in: @expires,
time: @time
)
end

# Returns signed headers
#
# @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: @region,
credentials_provider: @credentials,
uri_escape_path: false,
unsigned_headers: Aws::Signers::V4::BLACKLIST_HEADERS - @headers.keys
)
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 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 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 build_headers(hash)
hash.inject({}) {|h, (k, v)| h["x-amz-#{k.to_s.gsub(/_/, '-')}"] = v; h}
end

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

end

end
end
Loading