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 +4 -4
- data/CHANGELOG +3 -0
- data/Gemfile.lock +10 -2
- data/README.md +11 -1
- data/lib/alexa_ruby/alexa.rb +23 -12
- data/lib/alexa_ruby/request/base_request/validator/certificates.rb +84 -0
- data/lib/alexa_ruby/request/base_request/validator/uri.rb +54 -0
- data/lib/alexa_ruby/request/base_request/validator.rb +54 -0
- data/lib/alexa_ruby/request/base_request.rb +9 -0
- data/lib/alexa_ruby/response/audio_player.rb +1 -1
- data/lib/alexa_ruby/response/card.rb +1 -1
- data/lib/alexa_ruby/version.rb +1 -1
- data/lib/alexa_ruby.rb +6 -0
- metadata +34 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28630927ffe709806449fc5c67540dbef0753e4f
|
4
|
+
data.tar.gz: 84c0caa9f8f813736565eaee144634fa4a6bc457
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dc51a947340ba26899fc75e033372c5842edc5c653770608f56c67c934e878eceac2d9dcf1137ba286b87555895d889154e756712c91e904a9388385e9e02b57
|
7
|
+
data.tar.gz: c9524f193e8ff842a9f007050da4cebd727cf0ccfb9da5c131b5ca7857cfa7ae14009b91f0164371bdb36570b7accee08baee8befaeb2c9af720fb4d97adc970
|
data/CHANGELOG
CHANGED
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
|
-
|
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.
|
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
|
|
data/lib/alexa_ruby/alexa.rb
CHANGED
@@ -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
|
-
#
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
data/lib/alexa_ruby/version.rb
CHANGED
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.
|
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-
|
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.
|
176
|
+
rubygems_version: 2.6.12
|
146
177
|
signing_key:
|
147
178
|
specification_version: 4
|
148
179
|
summary: Ruby toolkit for Amazon Alexa API
|