From ff2db9ac7aa2c14053d4b40e72434f6de0d9551c Mon Sep 17 00:00:00 2001 From: David Buxton Date: Sun, 27 Mar 2016 16:38:43 +0100 Subject: [PATCH 01/10] Pass through user to intent --- django_alexa/alexa.py | 10 +++++----- django_alexa/internal/intents_schema.py | 3 ++- django_alexa/views.py | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/django_alexa/alexa.py b/django_alexa/alexa.py index 5130687..8472cb0 100644 --- a/django_alexa/alexa.py +++ b/django_alexa/alexa.py @@ -3,7 +3,7 @@ @intent -def LaunchRequest(session): +def LaunchRequest(**kwargs): """ Default Start Session Intent --- @@ -21,7 +21,7 @@ def LaunchRequest(session): @intent -def CancelIntent(session): +def CancelIntent(**kwargs): """ Default Cancel Intent --- @@ -33,7 +33,7 @@ def CancelIntent(session): @intent -def StopIntent(session): +def StopIntent(**kwargs): """ Default Stop Intent --- @@ -45,7 +45,7 @@ def StopIntent(session): @intent -def HelpIntent(session): +def HelpIntent(**kwargs): """ Default Help Intent --- @@ -57,7 +57,7 @@ def HelpIntent(session): @intent -def SessionEndedRequest(session): +def SessionEndedRequest(**kwargs): """ Default End Session Intent --- diff --git a/django_alexa/internal/intents_schema.py b/django_alexa/internal/intents_schema.py index b16c921..e6ef23d 100644 --- a/django_alexa/internal/intents_schema.py +++ b/django_alexa/internal/intents_schema.py @@ -31,13 +31,14 @@ def get_intent(cls, app, intent): return cls.intents[key_name] @classmethod - def route(cls, session, app, intent, intent_kwargs): + def route(cls, session, app, intent, intent_kwargs, user=None): """Routes an intent to the proper method""" func, slot = cls.get_intent(app, intent) if slot and bool(intent_kwargs) is False: msg = "Intent '{0}.{1}' requires slots data and none was provided".format(app, intent) raise InternalError(msg) intent_kwargs['session'] = session.get('attributes', {}) + intent_kwargs['user'] = user msg = "Routing: '{0}.{1}' with args {2} to '{3}.{4}'".format(app, intent, intent_kwargs, diff --git a/django_alexa/views.py b/django_alexa/views.py index 069b0c4..d5c9372 100644 --- a/django_alexa/views.py +++ b/django_alexa/views.py @@ -33,6 +33,7 @@ def handle_request(self, validated_data): log.info("Alexa Request Body: {0}".format(validated_data)) intent_kwargs = {} session = validated_data['session'] + user = validated_data.get('user', None) app = ALEXA_APP_IDS[session['application']['applicationId']] if validated_data["request"]["type"] == "IntentRequest": intent_name = validated_data["request"]["intent"]["name"] @@ -50,7 +51,7 @@ def handle_request(self, validated_data): slots = slot(data=intent_kwargs) slots.is_valid() intent_kwargs = slots.data - data = IntentsSchema.route(session, app, intent_name, intent_kwargs) + data = IntentsSchema.route(session, app, intent_name, intent_kwargs, user) return Response(data=data, status=HTTP_200_OK) def post(self, request, *args, **kwargs): From b58aa27382f9c359d018558b57ce471322dd0959 Mon Sep 17 00:00:00 2001 From: David Buxton Date: Mon, 28 Mar 2016 12:04:57 +0100 Subject: [PATCH 02/10] Send through validated data --- django_alexa/internal/intents_schema.py | 6 ++++-- django_alexa/views.py | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/django_alexa/internal/intents_schema.py b/django_alexa/internal/intents_schema.py index e6ef23d..07cba6c 100644 --- a/django_alexa/internal/intents_schema.py +++ b/django_alexa/internal/intents_schema.py @@ -31,14 +31,16 @@ def get_intent(cls, app, intent): return cls.intents[key_name] @classmethod - def route(cls, session, app, intent, intent_kwargs, user=None): + def route(cls, session, app, intent, intent_kwargs, validated_data=None): + if validated_data is None: + validated_data = {} """Routes an intent to the proper method""" func, slot = cls.get_intent(app, intent) if slot and bool(intent_kwargs) is False: msg = "Intent '{0}.{1}' requires slots data and none was provided".format(app, intent) raise InternalError(msg) intent_kwargs['session'] = session.get('attributes', {}) - intent_kwargs['user'] = user + intent_kwargs['validated_data'] = validated_data msg = "Routing: '{0}.{1}' with args {2} to '{3}.{4}'".format(app, intent, intent_kwargs, diff --git a/django_alexa/views.py b/django_alexa/views.py index d5c9372..d6b90bc 100644 --- a/django_alexa/views.py +++ b/django_alexa/views.py @@ -33,7 +33,6 @@ def handle_request(self, validated_data): log.info("Alexa Request Body: {0}".format(validated_data)) intent_kwargs = {} session = validated_data['session'] - user = validated_data.get('user', None) app = ALEXA_APP_IDS[session['application']['applicationId']] if validated_data["request"]["type"] == "IntentRequest": intent_name = validated_data["request"]["intent"]["name"] @@ -51,7 +50,7 @@ def handle_request(self, validated_data): slots = slot(data=intent_kwargs) slots.is_valid() intent_kwargs = slots.data - data = IntentsSchema.route(session, app, intent_name, intent_kwargs, user) + data = IntentsSchema.route(session, app, intent_name, intent_kwargs, validated_data) return Response(data=data, status=HTTP_200_OK) def post(self, request, *args, **kwargs): From d33888792f673b6b45c3cd8ecf8caf3de95abc3d Mon Sep 17 00:00:00 2001 From: David Buxton Date: Mon, 28 Mar 2016 12:15:52 +0100 Subject: [PATCH 03/10] Correct session --- django_alexa/internal/intents_schema.py | 5 ++--- django_alexa/views.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/django_alexa/internal/intents_schema.py b/django_alexa/internal/intents_schema.py index 07cba6c..92ad24e 100644 --- a/django_alexa/internal/intents_schema.py +++ b/django_alexa/internal/intents_schema.py @@ -31,7 +31,7 @@ def get_intent(cls, app, intent): return cls.intents[key_name] @classmethod - def route(cls, session, app, intent, intent_kwargs, validated_data=None): + def route(cls, session, app, intent, intent_kwargs): if validated_data is None: validated_data = {} """Routes an intent to the proper method""" @@ -39,8 +39,7 @@ def route(cls, session, app, intent, intent_kwargs, validated_data=None): if slot and bool(intent_kwargs) is False: msg = "Intent '{0}.{1}' requires slots data and none was provided".format(app, intent) raise InternalError(msg) - intent_kwargs['session'] = session.get('attributes', {}) - intent_kwargs['validated_data'] = validated_data + intent_kwargs['session'] = session msg = "Routing: '{0}.{1}' with args {2} to '{3}.{4}'".format(app, intent, intent_kwargs, diff --git a/django_alexa/views.py b/django_alexa/views.py index d6b90bc..069b0c4 100644 --- a/django_alexa/views.py +++ b/django_alexa/views.py @@ -50,7 +50,7 @@ def handle_request(self, validated_data): slots = slot(data=intent_kwargs) slots.is_valid() intent_kwargs = slots.data - data = IntentsSchema.route(session, app, intent_name, intent_kwargs, validated_data) + data = IntentsSchema.route(session, app, intent_name, intent_kwargs) return Response(data=data, status=HTTP_200_OK) def post(self, request, *args, **kwargs): From 376a71de87e829fb1e609c2fcf7c01b14e178b25 Mon Sep 17 00:00:00 2001 From: David Buxton Date: Mon, 28 Mar 2016 12:33:01 +0100 Subject: [PATCH 04/10] Remove reference to validated_data --- django_alexa/internal/intents_schema.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/django_alexa/internal/intents_schema.py b/django_alexa/internal/intents_schema.py index 92ad24e..1afab47 100644 --- a/django_alexa/internal/intents_schema.py +++ b/django_alexa/internal/intents_schema.py @@ -32,8 +32,6 @@ def get_intent(cls, app, intent): @classmethod def route(cls, session, app, intent, intent_kwargs): - if validated_data is None: - validated_data = {} """Routes an intent to the proper method""" func, slot = cls.get_intent(app, intent) if slot and bool(intent_kwargs) is False: From c7a65aa265ee5f2fff976221c746941b38320f9e Mon Sep 17 00:00:00 2001 From: Thorrak Date: Mon, 18 Apr 2016 22:07:54 -0400 Subject: [PATCH 05/10] Changing to make sure the request isn't old (in addition to isn't from the future) --- django_alexa/internal/validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_alexa/internal/validation.py b/django_alexa/internal/validation.py index 756b9a8..a7a58d7 100644 --- a/django_alexa/internal/validation.py +++ b/django_alexa/internal/validation.py @@ -48,7 +48,7 @@ def validate_current_timestamp(value): log.debug("Alexa: {0}".format(utc_timestamp)) log.debug("Server: {0}".format(utc_timestamp_now)) log.debug("Delta: {0}".format(delta)) - return False if delta > timedelta(minutes=2, seconds=30) else True + return False if abs(delta) > timedelta(minutes=2, seconds=30) else True def validate_char_limit(value): From 1ce80fc8bf27d0d60e3a9a0172ff4228837e126c Mon Sep 17 00:00:00 2001 From: Thorrak Date: Thu, 21 Apr 2016 10:11:43 -0400 Subject: [PATCH 06/10] Adding port check (scheme isn't sufficient) --- django_alexa/internal/validation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django_alexa/internal/validation.py b/django_alexa/internal/validation.py index a7a58d7..03d6fa4 100644 --- a/django_alexa/internal/validation.py +++ b/django_alexa/internal/validation.py @@ -71,7 +71,8 @@ def verify_cert_url(cert_url): if parsed_url.scheme == 'https': if parsed_url.hostname == "s3.amazonaws.com": if os.path.normpath(parsed_url.path).startswith("/echo.api/"): - return True + if parsed_url.port == 443: + return True return False From 2d20cff6bba4cd956e9df50bbce525e2686f53cc Mon Sep 17 00:00:00 2001 From: Thorrak Date: Thu, 21 Apr 2016 10:41:48 -0400 Subject: [PATCH 07/10] Adding carveout for port 'none' when no port is explicitly specified --- django_alexa/internal/validation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/django_alexa/internal/validation.py b/django_alexa/internal/validation.py index 03d6fa4..a89d28b 100644 --- a/django_alexa/internal/validation.py +++ b/django_alexa/internal/validation.py @@ -71,7 +71,9 @@ def verify_cert_url(cert_url): if parsed_url.scheme == 'https': if parsed_url.hostname == "s3.amazonaws.com": if os.path.normpath(parsed_url.path).startswith("/echo.api/"): - if parsed_url.port == 443: + if parsed_url.port is None: + return True + elif parsed_url.port == 443: return True return False From 011f1f3eab5186bc19b34f8a63e55db0c5a3d5fb Mon Sep 17 00:00:00 2001 From: Thorrak Date: Thu, 28 Apr 2016 18:27:22 -0400 Subject: [PATCH 08/10] Updating certificate validation issues to return an error code rather than 200 OK --- django_alexa/internal/validation.py | 18 +++++++++++++----- django_alexa/views.py | 21 +++++++++++++++++++-- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/django_alexa/internal/validation.py b/django_alexa/internal/validation.py index a89d28b..0a7bf4e 100644 --- a/django_alexa/internal/validation.py +++ b/django_alexa/internal/validation.py @@ -48,7 +48,10 @@ def validate_current_timestamp(value): log.debug("Alexa: {0}".format(utc_timestamp)) log.debug("Server: {0}".format(utc_timestamp_now)) log.debug("Delta: {0}".format(delta)) - return False if abs(delta) > timedelta(minutes=2, seconds=30) else True + if abs(delta) > timedelta(minutes=2, seconds=30): + return False + else: + return True def validate_char_limit(value): @@ -84,6 +87,8 @@ def verify_signature(request_body, signature, cert_url): """ if signature is None or cert_url is None: return False + if len(signature) == 0: + return False cert_str = requests.get(cert_url) certificate = crypto.load_certificate(crypto.FILETYPE_PEM, str(cert_str.text)) if certificate.has_expired() is True: @@ -95,7 +100,7 @@ def verify_signature(request_body, signature, cert_url): if crypto.verify(certificate, decoded_signature, request_body, 'sha1') is None: return True except: - raise InternalError("Error occured during signature validation") + raise InternalError("Error occured during signature validation", {"error": 400}) return False @@ -106,9 +111,12 @@ def validate_alexa_request(request_headers, request_body): """ if ALEXA_REQUEST_VERIFICATON is True: timestamp = json.loads(request_body)['request']['timestamp'] + # For each of the following errors, the alexa service expects an HTTP error code. This isn't well documented. + # I'm going to return 403 forbidden just to be safe (but need to pass a message to the custom error handler, + # hence why I'm adding an argument when raising the error) if validate_current_timestamp(timestamp) is False: - raise InternalError("Invalid Request Timestamp") + raise InternalError("Invalid Request Timestamp", {"error": 400}) if verify_cert_url(request_headers.get('HTTP_SIGNATURECERTCHAINURL')) is False: - raise InternalError("Invalid Certificate Chain URL") + raise InternalError("Invalid Certificate Chain URL", {"error": 400}) if verify_signature(request_body, request_headers.get('HTTP_SIGNATURE'), request_headers.get('HTTP_SIGNATURECERTCHAINURL')) is False: - raise InternalError("Invalid Request Signature") + raise InternalError("Invalid Request Signature", {"error": 400}) diff --git a/django_alexa/views.py b/django_alexa/views.py index 069b0c4..589fa35 100644 --- a/django_alexa/views.py +++ b/django_alexa/views.py @@ -4,6 +4,7 @@ from django.conf import settings from rest_framework.response import Response from rest_framework.status import HTTP_200_OK +from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect from rest_framework.views import APIView from .serializers import ASKInputSerializer from .internal import ALEXA_APP_IDS, ResponseBuilder, IntentsSchema, validate_alexa_request, validate_reponse_limit @@ -27,7 +28,22 @@ def handle_exception(self, exc): msg = "An internal error occured in the skill." log.exception(msg) data = ResponseBuilder.create_response(message=msg) - return Response(data=data, status=HTTP_200_OK) + # If we need to return an error code, do so. + # There is probably a better way of doing this, but this works. If anyone knows of a better way, please - + # submit a correction + try: + error = exc.args[1] + if error['error'] == 403: + log.debug(data) + return HttpResponseForbidden() + elif error['error'] == 400: + log.debug(data) + return HttpResponseBadRequest() + else: + # If we are passed an error code we should probably do something more here, but for now - this works. + return Response(data=data, status=HTTP_200_OK) + except: + return Response(data=data, status=HTTP_200_OK) def handle_request(self, validated_data): log.info("Alexa Request Body: {0}".format(validated_data)) @@ -68,6 +84,7 @@ def post(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs): log.debug("#" * 10 + "Start Alexa Request" + "#" * 10) response = super(ASKView, self).dispatch(request, *args, **kwargs) - validate_reponse_limit(response.render().content) + if response.status_code == 200: + validate_reponse_limit(response.render().content) log.debug("#" * 10 + "End Alexa Request" + "#" * 10) return response From 811e140dc2f7d9559f10e39e884db04882a33d5b Mon Sep 17 00:00:00 2001 From: Thorrak Date: Thu, 28 Apr 2016 18:54:48 -0400 Subject: [PATCH 09/10] Not sure why Travis CI is failing on this --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5cd6e24..e5dd61b 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ from setuptools import setup from os import path + def open_file(fname): return open(path.join(path.dirname(__file__), fname)) @@ -8,5 +9,5 @@ def open_file(fname): setup_requires=['pbr', 'pyversion'], pbr=True, auto_version="PBR", - install_requires=open_file('requirements.txt').readlines(), + install_requires=open(path.join(path.dirname(__file__), 'requirements.txt')).readlines(), ) From 73585db5188e1315414bbb55c7cfe9754d374819 Mon Sep 17 00:00:00 2001 From: Thorrak Date: Thu, 28 Apr 2016 18:58:37 -0400 Subject: [PATCH 10/10] Fix issues flake8 found --- django_alexa/internal/validation.py | 2 +- django_alexa/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/django_alexa/internal/validation.py b/django_alexa/internal/validation.py index 0a7bf4e..683462f 100644 --- a/django_alexa/internal/validation.py +++ b/django_alexa/internal/validation.py @@ -100,7 +100,7 @@ def verify_signature(request_body, signature, cert_url): if crypto.verify(certificate, decoded_signature, request_body, 'sha1') is None: return True except: - raise InternalError("Error occured during signature validation", {"error": 400}) + raise InternalError("Error occured during signature validation", {"error": 400}) return False diff --git a/django_alexa/views.py b/django_alexa/views.py index 589fa35..e873e81 100644 --- a/django_alexa/views.py +++ b/django_alexa/views.py @@ -4,7 +4,7 @@ from django.conf import settings from rest_framework.response import Response from rest_framework.status import HTTP_200_OK -from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect +from django.http import HttpResponseBadRequest, HttpResponseForbidden from rest_framework.views import APIView from .serializers import ASKInputSerializer from .internal import ALEXA_APP_IDS, ResponseBuilder, IntentsSchema, validate_alexa_request, validate_reponse_limit