1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
|
# frozen_string_literal: true
require 'aws-sigv4'
module Aws
module Plugins
# @api private
class SignatureV4 < Seahorse::Client::Plugin
option(:sigv4_signer) do |cfg|
SignatureV4.build_signer(cfg)
end
option(:sigv4_name) do |cfg|
cfg.api.metadata['signingName'] || cfg.api.metadata['endpointPrefix']
end
option(:sigv4_region) do |cfg|
# The signature version 4 signing region is most
# commonly the configured region. There are a few
# notable exceptions:
#
# * Some services have a global endpoint to the entire
# partition. For example, when constructing a route53
# client for a region like "us-west-2", we will
# always use "route53.amazonaws.com". This endpoint
# is actually global to the entire partition,
# and must be signed as "us-east-1".
#
# * When the region is configured, but it is configured
# to a non region, such as "aws-global". This is similar
# to the previous case. We use the Aws::Partitions::EndpointProvider
# to resolve to the actual signing region.
#
prefix = cfg.api.metadata['endpointPrefix']
if prefix && cfg.endpoint.to_s.match(/#{prefix}\.amazonaws\.com/)
'us-east-1'
elsif cfg.region
Aws::Partitions::EndpointProvider.signing_region(cfg.region, cfg.sigv4_name)
end
end
option(:unsigned_operations) do |cfg|
cfg.api.operation_names.inject([]) do |unsigned, operation_name|
if cfg.api.operation(operation_name)['authtype'] == 'none' ||
cfg.api.operation(operation_name)['authtype'] == 'custom'
# Unsign requests that has custom apigateway authorizer as well
unsigned << operation_name
else
unsigned
end
end
end
def add_handlers(handlers, cfg)
if cfg.unsigned_operations.empty?
handlers.add(Handler, step: :sign)
else
operations = cfg.api.operation_names - cfg.unsigned_operations
handlers.add(Handler, step: :sign, operations: operations)
end
end
class Handler < Seahorse::Client::Handler
def call(context)
SignatureV4.apply_signature(context: context)
@handler.call(context)
end
end
class MissingCredentialsSigner
def sign_request(*args)
raise Errors::MissingCredentialsError
end
end
class << self
# @api private
def build_signer(cfg)
if cfg.credentials && cfg.sigv4_region
Aws::Sigv4::Signer.new(
service: cfg.sigv4_name,
region: cfg.sigv4_region,
credentials_provider: cfg.credentials,
unsigned_headers: ['content-length', 'user-agent', 'x-amzn-trace-id']
)
elsif cfg.credentials
raise Errors::MissingRegionError
elsif cfg.sigv4_region
# Instead of raising now, we return a signer that raises only
# if you attempt to sign a request. Some services have unsigned
# operations and it okay to initialize clients for these services
# without credentials. Unsigned operations have an "authtype"
# trait of "none".
MissingCredentialsSigner.new
end
end
# @api private
def apply_signature(options = {})
context = apply_authtype(options[:context])
signer = options[:signer] || context.config.sigv4_signer
req = context.http_request
# in case this request is being re-signed
req.headers.delete('Authorization')
req.headers.delete('X-Amz-Security-Token')
req.headers.delete('X-Amz-Date')
if context.config.respond_to?(:clock_skew) &&
context.config.clock_skew &&
context.config.correct_clock_skew
endpoint = context.http_request.endpoint
skew = context.config.clock_skew.clock_correction(endpoint)
if skew.abs > 0
req.headers['X-Amz-Date'] = (Time.now.utc + skew).strftime("%Y%m%dT%H%M%SZ")
end
end
# compute the signature
begin
signature = signer.sign_request(
http_method: req.http_method,
url: req.endpoint,
headers: req.headers,
body: req.body
)
rescue Aws::Sigv4::Errors::MissingCredentialsError
raise Aws::Errors::MissingCredentialsError
end
# apply signature headers
req.headers.update(signature.headers)
# add request metadata with signature components for debugging
context[:canonical_request] = signature.canonical_request
context[:string_to_sign] = signature.string_to_sign
end
# @api private
def apply_authtype(context)
if context.operation['authtype'].eql?('v4-unsigned-body') &&
context.http_request.endpoint.scheme.eql?('https')
context.http_request.headers['X-Amz-Content-Sha256'] = 'UNSIGNED-PAYLOAD'
end
context
end
end
end
end
end
|