File: message.rb

package info (click to toggle)
ruby-aws-sdk 1.67.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 6,840 kB
  • sloc: ruby: 28,436; makefile: 7
file content (204 lines) | stat: -rw-r--r-- 5,297 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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# 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.

require 'base64'
require 'json'
require 'net/http'
require 'net/https'
require 'openssl'
require 'uri'

Dir.glob("#{File.dirname __FILE__}/originators/*.rb").each { |rb| require rb }

module AWS
  class SNS
    class MessageWasNotAuthenticError < StandardError
    end

    # Represents a single SNS message.
    #
    # See also http://docs.aws.amazon.com/sns/latest/gsg/json-formats.html
    #
    # = Originators
    # Originators are sources of SNS messages.  {FromAutoScaling} is one.  {Message}
    # can be extended by originators if their #applicable? method returns true when
    # passed the raw message.
    # Originator modules must implement `applicable? sns` module function.
    # If an originator is applicable, it should set the `@origin` accessor to denote
    # itself.
    class Message
      SIGNABLE_KEYS = [
        'Message',
        'MessageId',
        'Subject',
        'SubscribeURL',
        'Timestamp',
        'Token',
        'TopicArn',
        'Type',
      ].freeze

      attr_reader :raw
      attr_accessor :origin

      # @return {Message} Constructs a new {Message} from the raw SNS, sets origin
      def initialize sns
        if sns.is_a? String
          @raw = parse_from sns
        else
          @raw = sns
        end
        @origin = :sns
        self.extend FromAutoScaling if FromAutoScaling.applicable? @raw
      end

      # @param [String] key Indexer into raw SNS JSON message.
      # @return [String] the value of the SNS' field
      def [] key
        @raw[key]
      end

      # @return [Boolean] true when the {Message} is authentic:
      #   SigningCert is hosted at amazonaws.com, on https
      #   correctly cryptographically signed by sender
      #   nothing went wrong during authenticating the {Message}
      #
      # See http://docs.aws.amazon.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html
      def authentic?
        begin
          decoded_from_base64 = decode signature
          public_key = get_public_key_from signing_cert_url
          public_key.verify OpenSSL::Digest::SHA1.new, decoded_from_base64, canonical_string
        rescue MessageWasNotAuthenticError
          false
        end
      end

      # @return[Symbol] the message type
      def type
        case when @raw['Type'] =~ /SubscriptionConfirmation/i
          then :SubscriptionConfirmation
        when @raw['Type'] =~ /Notification/i
          then :Notification
        when @raw['Type'] =~ /UnsubscribeConfirmation/i
          then :UnsubscribeConfirmation
        else
          :unknown
        end
      end

      def message_id
        @raw['MessageId']
      end

      def topic_arn
        @raw['TopicArn']
      end

      def subject
        @raw['Subject']
      end

      def message
        @raw['Message']
      end

      def timestamp
        @raw['Timestamp']
      end

      def signature
        @raw['Signature']
      end

      def signature_version
        @raw['SignatureVersion']
      end

      def signing_cert_url
        @raw['SigningCertURL']
      end

      def subscribe_url
        @raw['SubscribeURL']
      end

      def token
        @raw['Token']
      end

      def unsubscribe_url
        @raw['UnsubscribeURL']
      end

      def parse_from json
        JSON.parse json
      end

      protected
      def decode raw
        Base64.decode64 raw
      end

      def get_public_key_from(x509_pem_url)
        cert_pem = download x509_pem_url
        x509 = OpenSSL::X509::Certificate.new(cert_pem)
        OpenSSL::PKey::RSA.new(x509.public_key)
      end

      def canonical_string
        text = ''
        SIGNABLE_KEYS.each do |key|
          value = @raw[key]
          next if value.nil? or value.empty?
          text << key << "\n"
          text << value << "\n"
        end
        text
      end

      def download url
        uri = URI.parse(url)
        unless
          uri.scheme == 'https' &&
          uri.host.match(/^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$/) &&
          File.extname(uri.path) == '.pem'
        then
          msg = "cert is not hosted at AWS URL (https): #{url}"
          raise MessageWasNotAuthenticError, msg
        end
        tries = 0
        begin
          resp = https_get(url)
          resp.body
        rescue => error
          tries += 1
          retry if tries < 3
          raise error
        end
      end

      def https_get(url)
        uri = URI.parse(url)
        http = Net::HTTP.new(uri.host, uri.port)
        http.use_ssl = true
        http.verify_mode = OpenSSL::SSL::VERIFY_PEER
        http.start
        resp = http.request(Net::HTTP::Get.new(uri.request_uri))
        http.finish
        resp
      end

    end
  end
end