alexa_ruby 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a28d3e44888f08923d448b5a675e682385fcada0
4
- data.tar.gz: be3114c0d065b28dd04f48bb8237a0198c38cea7
3
+ metadata.gz: 28630927ffe709806449fc5c67540dbef0753e4f
4
+ data.tar.gz: 84c0caa9f8f813736565eaee144634fa4a6bc457
5
5
  SHA512:
6
- metadata.gz: e92206b7c69748dbd3ff41762a0bffc1c2d0190a308174b3939a0c8653cafaae6bc2fd50b1613c112317f76d27f5f035a0099c6150d2cbecf8cb376d1e1576b2
7
- data.tar.gz: e6a018b12bac14cbfe84b92ffe8ef061e1ab03db338df73a5a5c7ed0b3c906dca5360180ef26329d8f3020d1efff87c8c0d81574cfeae666198e768bf7a28e65
6
+ metadata.gz: dc51a947340ba26899fc75e033372c5842edc5c653770608f56c67c934e878eceac2d9dcf1137ba286b87555895d889154e756712c91e904a9388385e9e02b57
7
+ data.tar.gz: c9524f193e8ff842a9f007050da4cebd727cf0ccfb9da5c131b5ca7857cfa7ae14009b91f0164371bdb36570b7accee08baee8befaeb2c9af720fb4d97adc970
data/CHANGELOG CHANGED
@@ -1,3 +1,6 @@
1
+ [1.4.0]
2
+ - Validate Amazon request signature and SSL certificates
3
+
1
4
  [1.3.1]
2
5
  - Slot can have no value at all
3
6
 
data/Gemfile.lock CHANGED
@@ -2,13 +2,17 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  alexa_ruby (1.3.1)
5
+ addressable (>= 2.5.1)
5
6
  bundler (>= 1.6.9)
7
+ httparty (>= 0.15.5)
6
8
  oj (~> 3.0)
7
9
  rake
8
10
 
9
11
  GEM
10
12
  remote: https://rubygems.org/
11
13
  specs:
14
+ addressable (2.5.1)
15
+ public_suffix (~> 2.0, >= 2.0.2)
12
16
  ansi (1.5.0)
13
17
  builder (3.2.3)
14
18
  coveralls (0.8.21)
@@ -18,6 +22,8 @@ GEM
18
22
  thor (~> 0.19.4)
19
23
  tins (~> 1.6)
20
24
  docile (1.1.5)
25
+ httparty (0.15.5)
26
+ multi_xml (>= 0.5.2)
21
27
  json (2.1.0)
22
28
  minitest (5.10.2)
23
29
  minitest-reporters (1.1.14)
@@ -25,7 +31,9 @@ GEM
25
31
  builder
26
32
  minitest (>= 5.0)
27
33
  ruby-progressbar
28
- oj (3.0.10)
34
+ multi_xml (0.6.0)
35
+ oj (3.1.3)
36
+ public_suffix (2.0.5)
29
37
  rake (12.0.0)
30
38
  ruby-progressbar (1.8.1)
31
39
  simplecov (0.14.1)
@@ -48,4 +56,4 @@ DEPENDENCIES
48
56
  minitest-reporters (~> 1.1, >= 1.1.14)
49
57
 
50
58
  BUNDLED WITH
51
- 1.15.0
59
+ 1.15.3
data/README.md CHANGED
@@ -63,12 +63,22 @@ class App < Roda
63
63
  end
64
64
  ```
65
65
 
66
- Request validations can be disabled:
66
+ Request structure validations can be disabled:
67
67
 
68
68
  ```ruby
69
69
  AlexaRuby.new(request, disable_validations: true)
70
70
  ```
71
71
 
72
+ If needed, you can validate request signature and Amazon SSL certificates chain. To do so specify several parameters:
73
+
74
+ ```ruby
75
+ AlexaRuby.new(
76
+ request,
77
+ certificates_chain_url: url,
78
+ request_signature: signature
79
+ )
80
+ ```
81
+
72
82
  After initializing new AlexaRuby instance you will have a possibility to access
73
83
  all parameters of the received request.
74
84
 
@@ -15,16 +15,19 @@ module AlexaRuby
15
15
  invalid_request_exception if invalid_request?
16
16
  @request = define_request
17
17
  raise ArgumentError, 'Unknown type of Alexa request' if @request.nil?
18
+ @request.valid? if ssl_check?
18
19
  @response = Response.new(@request.type, @request.version)
19
20
  end
20
21
 
21
22
  private
22
23
 
23
- # Check if validations are enabled
24
- #
25
- # @return [Boolean]
26
- def validations_enabled?
27
- !@opts[:disable_validations] || @opts[:disable_validations].nil?
24
+ # Request structure isn't valid, raise exception
25
+ def invalid_request_exception
26
+ raise ArgumentError,
27
+ 'Invalid request structure, ' \
28
+ 'please, refer to the Amazon Alexa manual: ' \
29
+ 'https://developer.amazon.com/public/solutions' \
30
+ '/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference'
28
31
  end
29
32
 
30
33
  # Check if it is an invalid request
@@ -34,13 +37,11 @@ module AlexaRuby
34
37
  @req[:version].nil? || @req[:request].nil? if validations_enabled?
35
38
  end
36
39
 
37
- # Request structure isn't valid, raise exception
38
- def invalid_request_exception
39
- raise ArgumentError,
40
- 'Invalid request structure, ' \
41
- 'please, refer to the Amazon Alexa manual: ' \
42
- 'https://developer.amazon.com/public/solutions' \
43
- '/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference'
40
+ # Check if validations are enabled
41
+ #
42
+ # @return [Boolean]
43
+ def validations_enabled?
44
+ !@opts[:disable_validations] || @opts[:disable_validations].nil?
44
45
  end
45
46
 
46
47
  # Initialize proper request object
@@ -58,5 +59,15 @@ module AlexaRuby
58
59
  AudioPlayerRequest.new(@req)
59
60
  end
60
61
  end
62
+
63
+ # Check if we have SSL certificates URL and request signature
64
+ # and need to validate Amazon request
65
+ #
66
+ # @return [Boolean]
67
+ def ssl_check?
68
+ @request.certificates_chain_url = @opts[:certificates_chain_url]
69
+ @request.signature = @opts[:request_signature]
70
+ @request.certificates_chain_url && @request.signature
71
+ end
61
72
  end
62
73
  end
@@ -0,0 +1,84 @@
1
+ require 'httparty'
2
+ require 'openssl'
3
+
4
+ module AlexaRuby
5
+ # SSL certificates validator
6
+ class Certificates
7
+ # Setup new certificates chain
8
+ #
9
+ # @param certificates_chain_url [String] SSL certificates chain URL
10
+ # @param signature [String] HTTP request signature
11
+ # @param request [String] plain HTTP request body
12
+ def initialize(certificates_chain_url, signature, request)
13
+ download_certificates(certificates_chain_url)
14
+ @signature = signature
15
+ @request = request
16
+ end
17
+
18
+ # Check if it is a valid certificates chain and request signature
19
+ #
20
+ # @return [Boolean]
21
+ def valid?
22
+ raise ArgumentError, 'Inactive Amazon SSL certificate' unless active?
23
+ raise ArgumentError, 'Inactive host in SSL certificate' unless amazon?
24
+ raise ArgumentError, 'Signature and request mismatch' unless verified?
25
+ true
26
+ end
27
+
28
+ private
29
+
30
+ # Download SSL certificates chain from Amazon URL
31
+ #
32
+ # @param certificates_chain_url [String] SSL certificates chain URL
33
+ def download_certificates(certificates_chain_url)
34
+ resp = HTTParty.get(certificates_chain_url)
35
+ raise ArgumentError, 'Invalid certificates chain' unless resp.code == 200
36
+ @cert = OpenSSL::X509::Certificate.new(resp.body)
37
+ end
38
+
39
+ # Check if it is an active certificate
40
+ #
41
+ # @return [Boolean]
42
+ def active?
43
+ now = Time.now
44
+ @cert.not_before < now && @cert.not_after > now
45
+ end
46
+
47
+ # Check if Subject Alternative Names includes Amazon domain name
48
+ #
49
+ # @return [Boolean]
50
+ def amazon?
51
+ @cert.subject.to_a.flatten.include? 'echo-api.amazon.com'
52
+ end
53
+
54
+ # Check if given signature matches given request
55
+ #
56
+ # @return [Boolean]
57
+ def verified?
58
+ sign = decode_signature
59
+ pkey = public_key
60
+ pkey.verify(hash, sign, @request)
61
+ end
62
+
63
+ # Decode base64-encoded signature
64
+ #
65
+ # @return [String] decoded signature
66
+ def decode_signature
67
+ Base64.decode64(@signature)
68
+ end
69
+
70
+ # Get public key from certificate
71
+ #
72
+ # @return [OpenSSL::PKey::RSA] OpenSSL PKey object
73
+ def public_key
74
+ @cert.public_key
75
+ end
76
+
77
+ # Get hash type for comparison
78
+ #
79
+ # @return [OpenSSL::Digest::SHA1] OpenSSL SHA1 hash
80
+ def hash
81
+ OpenSSL::Digest::SHA1.new
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,54 @@
1
+ module AlexaRuby
2
+ # URI request validator
3
+ class URI
4
+ attr_reader :uri
5
+
6
+ # Setup new URI
7
+ #
8
+ # @param uri [String] URI
9
+ def initialize(uri)
10
+ @uri = Addressable::URI.parse(uri).normalize!
11
+ end
12
+
13
+ # Check if it is a valid Amazon URI
14
+ #
15
+ # @return [Boolean]
16
+ def valid?
17
+ raise ArgumentError, 'Certificates chain URL must be HTTPS' unless https?
18
+ raise ArgumentError, 'Not Amazon host in certificates URL' unless amazon?
19
+ raise ArgumentError, 'Invalid certificates chain URL' unless echo_api?
20
+ raise ArgumentError, 'Certificates chain URL must be HTTPS' unless port?
21
+ true
22
+ end
23
+
24
+ private
25
+
26
+ # Check if URI scheme is HTTPS
27
+ #
28
+ # @return [Boolean]
29
+ def https?
30
+ @uri.scheme == 'https'
31
+ end
32
+
33
+ # Check if URI host is a valid Amazon host
34
+ #
35
+ # @return [Boolean]
36
+ def amazon?
37
+ @uri.host.casecmp('s3.amazonaws.com').zero?
38
+ end
39
+
40
+ # Check if URI path starts with /echo.api/
41
+ #
42
+ # @return [Boolean]
43
+ def echo_api?
44
+ @uri.path[0..9] == '/echo.api/'
45
+ end
46
+
47
+ # Check if URI port is 443 if port is present
48
+ #
49
+ # @return [Boolean]
50
+ def port?
51
+ @uri.port.nil? || @uri.port == 443
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,54 @@
1
+ module AlexaRuby
2
+ # Validator is responsible for Amazon request validation:
3
+ # - SignatureCertChainUrl validation
4
+ # - Amazon Alexa request signature validation
5
+ class Validator
6
+ TIMESTAMP_TOLERANCE = 150
7
+
8
+ # Setup new validator
9
+ #
10
+ # @param cert_chain_url [String] SSL certificates chain URI
11
+ # @param signature [String] HTTP request signature
12
+ # @param request [Object] json request
13
+ # @param timestamp_diff [Integer] valid distance in seconds between
14
+ # current time and the request timestamp
15
+ def initialize(cert_chain_url, signature, request, timestamp_diff = nil)
16
+ @chain_url = cert_chain_url
17
+ @signature = signature
18
+ @request = request
19
+ @timestamp_diff = timestamp_diff || TIMESTAMP_TOLERANCE
20
+ end
21
+
22
+ # Check if it is a valid Amazon request
23
+ #
24
+ # @return [Boolean]
25
+ def valid_request?
26
+ raise ArgumentError, 'Outdated request' unless timestamp_tolerant?
27
+ valid_uri? && valid_certificates?
28
+ end
29
+
30
+ private
31
+
32
+ # Check if request is timestamp tolerant
33
+ #
34
+ # @return [Boolean]
35
+ def timestamp_tolerant?
36
+ request_ts = @request[:request][:timestamp]
37
+ Time.parse(request_ts) >= (Time.now - @timestamp_diff)
38
+ end
39
+
40
+ # Check if it is a valid Amazon URI
41
+ #
42
+ # @return [Boolean]
43
+ def valid_uri?
44
+ URI.new(@chain_url).valid?
45
+ end
46
+
47
+ # Check if it is a valid certificates chain and request signature
48
+ #
49
+ # @return [Boolean]
50
+ def valid_certificates?
51
+ Certificates.new(@chain_url, @signature, Oj.to_json(@request)).valid?
52
+ end
53
+ end
54
+ end
@@ -2,6 +2,7 @@ module AlexaRuby
2
2
  # Amazon Alexa web service request
3
3
  class BaseRequest
4
4
  attr_reader :version, :type, :session, :context, :id, :timestamp, :locale
5
+ attr_accessor :certificates_chain_url, :signature
5
6
 
6
7
  # Initialize new request object
7
8
  #
@@ -17,6 +18,14 @@ module AlexaRuby
17
18
  parse_base_params(@req[:request])
18
19
  end
19
20
 
21
+ # Check if it is a valid Amazon request
22
+ #
23
+ # @return [Boolean]
24
+ def valid?
25
+ validator = Validator.new(certificates_chain_url, signature, @req)
26
+ validator.valid_request?
27
+ end
28
+
20
29
  # Return JSON representation of given request
21
30
  #
22
31
  # @return [String] request json
@@ -84,7 +84,7 @@ module AlexaRuby
84
84
  # @param url [String] some URL
85
85
  # @return [Boolean]
86
86
  def invalid_url?(url)
87
- URI.parse(url).scheme != 'https'
87
+ Addressable::URI.parse(url).scheme != 'https'
88
88
  end
89
89
 
90
90
  # Get station token
@@ -95,7 +95,7 @@ module AlexaRuby
95
95
  # @param url [String] some URL
96
96
  # @return [Boolean]
97
97
  def invalid_url?(url)
98
- URI.parse(url).scheme != 'https'
98
+ Addressable::URI.parse(url).scheme != 'https'
99
99
  end
100
100
  end
101
101
  end
@@ -1,3 +1,3 @@
1
1
  module AlexaRuby
2
- VERSION = '1.3.1'.freeze
2
+ VERSION = '1.4.0'.freeze
3
3
  end
data/lib/alexa_ruby.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # Utilities
2
+ require 'addressable/uri'
2
3
  require 'oj'
3
4
  require 'securerandom'
4
5
 
@@ -8,6 +9,9 @@ require 'alexa_ruby/request/base_request'
8
9
  require 'alexa_ruby/request/base_request/context'
9
10
  require 'alexa_ruby/request/base_request/context/device'
10
11
  require 'alexa_ruby/request/base_request/session'
12
+ require 'alexa_ruby/request/base_request/validator'
13
+ require 'alexa_ruby/request/base_request/validator/uri'
14
+ require 'alexa_ruby/request/base_request/validator/certificates'
11
15
  require 'alexa_ruby/request/base_request/user'
12
16
  require 'alexa_ruby/request/audio_player_request'
13
17
  require 'alexa_ruby/request/launch_request'
@@ -28,6 +32,8 @@ module AlexaRuby
28
32
  # can be hash or JSON encoded string
29
33
  # @param opts [Hash] additional options:
30
34
  # :disable_validations [Boolean] disables request validation if true
35
+ # :certificates_chain_url [String] URL of Amazon SSL certificates chain
36
+ # :request_signature [String] Base64-encoded request signature
31
37
  # @return [Object] new Request object instance
32
38
  # @raise [ArgumentError] if given object isn't a valid JSON object
33
39
  def new(request, opts = {})
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alexa_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Mulev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-07-24 00:00:00.000000000 Z
11
+ date: 2017-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: addressable
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.5.1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.5.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: httparty
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 0.15.5
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 0.15.5
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: minitest
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -113,6 +141,9 @@ files:
113
141
  - lib/alexa_ruby/request/base_request/context/device.rb
114
142
  - lib/alexa_ruby/request/base_request/session.rb
115
143
  - lib/alexa_ruby/request/base_request/user.rb
144
+ - lib/alexa_ruby/request/base_request/validator.rb
145
+ - lib/alexa_ruby/request/base_request/validator/certificates.rb
146
+ - lib/alexa_ruby/request/base_request/validator/uri.rb
116
147
  - lib/alexa_ruby/request/intent_request.rb
117
148
  - lib/alexa_ruby/request/intent_request/slot.rb
118
149
  - lib/alexa_ruby/request/launch_request.rb
@@ -142,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
173
  version: '0'
143
174
  requirements: []
144
175
  rubyforge_project:
145
- rubygems_version: 2.2.2
176
+ rubygems_version: 2.6.12
146
177
  signing_key:
147
178
  specification_version: 4
148
179
  summary: Ruby toolkit for Amazon Alexa API