File: presign_v4.rb

package info (click to toggle)
ruby-aws-sdk 1.67.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, buster
  • size: 6,840 kB
  • sloc: ruby: 28,436; makefile: 7
file content (136 lines) | stat: -rw-r--r-- 4,139 bytes parent folder | download | duplicates (3)
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
# Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
#     http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

module AWS
  class S3

    # Utility class for building pre-signed URLs for Amazon S3 objects using
    # signature version 4.
    class PresignV4

      # @param [S3Object] object
      def initialize(object)
        @object = object
        @client = object.client
        @signer = object.client.send(:v4_signer)
      end

      # @return [S3Object]
      attr_reader :object

      # @return [Client]
      attr_reader :client

      # @return [Core::Signers::Version4]
      attr_reader :signer

      # @param (see S3Object#url_for)
      # @option (see S3Object#url_for)
      # @return (see S3Object#url_for)
      def presign(method, options = {})

        now = Time.now.utc
        one_week = 60 * 60 * 24 * 7
        if options[:expires] - now.to_i > one_week
          msg = "presigned URLs using sigv4 may not expire more than one week out"
          raise ArgumentError, msg
        end

        now = now.strftime("%Y%m%dT%H%M%SZ")

        request = build_request(method, options)

        request.headers.clear
        request.headers['host'] = request.host
        signed_headers = 'Host'

        if options[:acl]
          request.add_param("x-amz-acl", options[:acl].to_s.gsub(/_/, '-'))
        end

        # must be sent along with the PUT request headers
        if options[:content_md5]
          request.headers['Content-MD5'] = options[:content_md5]
          signed_headers << ';Content-MD5'
        end

        request_params = Core::Signers::S3::QUERY_PARAMS.map do |p|
          param = p.tr("-","_").to_sym
          if options.key?(param)
            request.add_param(p, options[param])
          end
        end

        token = client.credential_provider.session_token

        request.add_param("X-Amz-Algorithm", "AWS4-HMAC-SHA256")
        request.add_param("X-Amz-Date", now)
        request.add_param("X-Amz-SignedHeaders", signed_headers)
        request.add_param("X-Amz-Expires", seconds_away(options[:expires]))
        request.add_param('X-Amz-Security-Token', token) if token
        request.add_param("X-Amz-Credential", signer.credential(now))
        request.add_param("X-Amz-Signature", signature(request, now))

        build_uri(request, options)

      end

      private

      def build_request(method, options)
        path_style = object.config.s3_force_path_style
        params = options.merge(
          :bucket_name => object.bucket.name,
          :key => object.key,
          :data => ''
        )
        req = client.send(:build_request, operation_name(method), params)
        req.force_path_style = options.fetch(:force_path_style, path_style)
        req
      end

      def operation_name(method)
        case method
        when :get, :read then :get_object
        when :put, :write then :put_object
        when :delete then :delete_object
        when :head then :head_object
        else
          msg = "invalid method, expected :get, :put or :delete, got "
          msg << method.inspect
          raise ArgumentError msg
        end
      end

      def signature(request, datetime)
        key = signer.derive_key(datetime)
        signer.signature(request, key, datetime, 'UNSIGNED-PAYLOAD')
      end

      def build_uri(request, options)
        uri_class = options[:secure] ? URI::HTTPS : URI::HTTP
        uri_class.build(
          :host => request.host,
          :port => request.port,
          :path => request.path,
          :query => request.querystring
        )
      end

      def seconds_away(expires)
        expires - Time.now.to_i
      end

    end
  end
end