def test_webservice_skill_handler_dispatch_serialization_failure_throw_exc( self): self.mock_serializer.deserialize.side_effect = AskSdkException( "test deserialization exception") test_webservice_skill_handler = WebserviceSkillHandler( skill=self.mock_skill, verify_signature=False, verify_timestamp=False, verifiers=[self.mock_verifier]) with self.assertRaises(AskSdkException) as exc: test_webservice_skill_handler.verify_request_and_dispatch( http_request_headers=None, http_request_body=None) self.assertIn( "test deserialization exception", str(exc.exception), "Webservice skill handler didn't raise deserialization exception " "during skill dispatch") self.assertFalse( self.mock_verifier.verify.called, "Webservice skill handler called verifier verify when request " "deserialization failed") self.assertFalse( self.mock_skill.invoke.called, "Webservice skill handler called skill invoke when request " "verification failed")
def _create_webservice_handler(self, skill, verifiers): # type: (CustomSkill, List[AbstractVerifier]) -> None """Create the handler for request verification and dispatch. :param skill: A :py:class:`ask_sdk_core.skill.CustomSkill` instance. If you are using the skill builder from ask-sdk, then you can use the ``create`` method under it, to create a skill instance :type skill: ask_sdk_core.skill.CustomSkill :param verifiers: A list of verifiers, that needs to be applied on the input request, before invoking the request handlers. :type verifiers: list[ask_sdk_webservice_support.verifier.AbstractVerifier] :rtype: None """ if verifiers is None: verifiers = [] self._webservice_handler = WebserviceSkillHandler( skill=skill, verify_signature=current_app.config.get( VERIFY_SIGNATURE_APP_CONFIG, True), verify_timestamp=current_app.config.get( VERIFY_TIMESTAMP_APP_CONFIG, True), verifiers=verifiers) self._webservice_handler._add_custom_user_agent("flask-ask-sdk")
def _create_webservice_handler(self): self._webservice_handler = WebserviceSkillHandler( skill=self._skill, verify_signature=self._verify_signature, verify_timestamp=self._verify_timestamp, verifiers=self._verifiers) self._webservice_handler._add_custom_user_agent("cherrypy-ask-sdk")
def __init__(self, skill, verify_signature=True, verify_timestamp=True, verifiers=None): # type: (CustomSkill, bool, bool, List[AbstractVerifier]) -> None """Instantiate the view and set the verifiers on the handler. :param skill: A :py:class:`ask_sdk_core.skill.CustomSkill` instance. If you are using the skill builder from ask-sdk, then you can use the ``create`` method under it, to create a skill instance :type skill: ask_sdk_core.skill.CustomSkill :param verify_signature: Enable request signature verification :type verify_signature: bool :param verify_timestamp: Enable request timestamp verification :type verify_timestamp: bool :param verifiers: An optional list of verifiers, that needs to be applied on the input request, before invoking the request handlers. For more information, look at `hosting the skill as webservice <https://developer.amazon.com/docs/custom-skills/host-a-custom-skill-as-a-web-service.html>`_ :type verifiers: list[ask_sdk_webservice_support.verifier.AbstractVerifier] :raises: :py:class:`TypeError` if the provided skill instance is not a :py:class:`ask_sdk_core.skill.CustomSkill` instance """ self._skill = skill if not isinstance(skill, CustomSkill): raise TypeError( "Invalid skill instance provided. Expected a custom " "skill instance.") if verifiers is None: verifiers = [] self._verifiers = verifiers if verify_signature: request_verifier = RequestVerifier( signature_cert_chain_url_key=SIGNATURE_CERT_CHAIN_URL_KEY, signature_key=SIGNATURE_KEY) self._verifiers.append(request_verifier) self._webservice_handler = WebserviceSkillHandler( skill=self._skill, verify_signature=False, verify_timestamp=verify_timestamp, verifiers=self._verifiers) self._webservice_handler._add_custom_user_agent("django-ask-sdk") super(SkillAdapter, self).__init__()
def test_webservice_skill_handler_dispatch_runs_verification_skill_invoke( self): try: test_webservice_skill_handler = WebserviceSkillHandler( skill=self.mock_skill, verify_signature=False, verify_timestamp=False, verifiers=[self.mock_verifier]) test_webservice_skill_handler.verify_request_and_dispatch( http_request_headers=None, http_request_body=None) except: self.fail( "Webservice skill handler failed request verification " "and request dispatch for a valid input request")
def test_webservice_skill_handler_init_create_custom_user_agent(self): WebserviceSkillHandler(skill=self.mock_skill, verify_signature=False, verify_timestamp=False) self.assertEqual( self.mock_skill.custom_user_agent, "ask-webservice", "Webservice skill handler didn't create new custom user agent " "value for a valid custom skill without a user agent")
def test_webservice_skill_handler_init_invalid_skill_raise_exception(self): with self.assertRaises(TypeError) as exc: WebserviceSkillHandler(skill=None) self.assertIn( "Expected a custom skill instance", str(exc.exception), "Webservice skill handler didn't raise TypError on " "initialization when an invalid skill instance is provided")
def test_webservice_skill_handler_init_append_custom_user_agent(self): self.mock_skill.custom_user_agent = "test-value" WebserviceSkillHandler(skill=self.mock_skill, verify_signature=False, verify_timestamp=False) self.assertEqual( self.mock_skill.custom_user_agent, "test-value ask-webservice", "Webservice skill handler didn't update custom user agent " "for a valid custom skill with an existing user agent")
def test_webservice_skill_handler_init_add_custom_user_agent(self): WebserviceSkillHandler(skill=self.mock_skill, verify_signature=False, verify_timestamp=False) self.assertEqual( self.mock_skill.custom_user_agent, " ask-webservice", "Webservice skill handler didn't update custom user agent " "for a valid custom skill")
def test_webservice_skill_handler_init_with_no_verifiers(self): test_webservice_skill_handler = WebserviceSkillHandler( skill=self.mock_skill, verify_signature=False, verify_timestamp=False) default_verifiers = test_webservice_skill_handler._verifiers self.assertEqual( len(default_verifiers), 0, "Webservice skill handler initialized invalid number of " "default verifiers")
def test_webservice_skill_handler_init_default_with_verifiers_set_correctly( self): test_verifier = mock.MagicMock(spec=AbstractVerifier) test_verifier.return_value = "Test" test_webservice_skill_handler = WebserviceSkillHandler( skill=self.mock_skill, verifiers=[test_verifier()]) default_verifiers = test_webservice_skill_handler._verifiers self.assertEqual( len(default_verifiers), 3, "Webservice skill handler initialized invalid number of " "verifiers, when an input list is passed")
def test_webservice_skill_handler_init_with_default_verifiers(self): test_webservice_skill_handler = WebserviceSkillHandler( skill=self.mock_skill) default_verifiers = test_webservice_skill_handler._verifiers self.assertEqual( len(default_verifiers), 2, "Webservice skill handler initialized invalid number of " "default verifiers") for verifier in default_verifiers: if not (isinstance(verifier, RequestVerifier) or isinstance(verifier, TimestampVerifier)): self.fail( "Webservice skill handler initialized invalid verifier " "when left as default")
def test_webservice_skill_handler_init_signature_check_disabled(self): test_webservice_skill_handler = WebserviceSkillHandler( skill=self.mock_skill, verify_signature=False) default_verifiers = test_webservice_skill_handler._verifiers self.assertEqual( len(default_verifiers), 1, "Webservice skill handler initialized invalid number of " "default verifiers, when signature verification env property is " "disabled") verifier = default_verifiers[0] self.assertIsInstance( verifier, TimestampVerifier, "Webservice skill handler initialized invalid default verifier, " "when request signature verification env property is set to false")
class SkillAdapter(View): """Provides a base interface to register skill and dispatch the request. The class constructor takes a :py:class:`ask_sdk_core.skill.CustomSkill` instance, an optional verify_request boolean, an optional verify_timestamp boolean and an optional list of :py:class:`ask_sdk_webservice_support.verifier.AbstractVerifier` instances. The :meth:`post` function is the only available method on the view, that intakes the input POST request from Alexa, verifies the request and dispatch it to the skill. By default, the :py:class:`ask_sdk_webservice_support.verifier.RequestVerifier` and :py:class:`ask_sdk_webservice_support.verifier.TimestampVerifier` instances are added to the skill verifier list. To disable this, set the ``verify_request`` and ``verify_timestamp`` input arguments to ``False`` respectively. For example, if you developed a skill using an instance of :py:class:`ask_sdk_core.skill_builder.SkillBuilder` or it's subclasses, then to register it as an endpoint in your django app ``example``, you can add the following in ``example.urls.py``: .. code-block:: python import skill from django_ask_sdk.skill_response import SkillAdapter view = SkillAdapter.as_view(skill=skill.sb.create()) urlpatterns = [ path("/myskill", view, name='index') ] """ # Creating class attributes, since Django View `as_view` method # sets these from __init__ only if they exist on the class. skill = None verify_signature = None verify_timestamp = None verifiers = None def __init__(self, skill, verify_signature=True, verify_timestamp=True, verifiers=None): # type: (CustomSkill, bool, bool, List[AbstractVerifier]) -> None """Instantiate the view and set the verifiers on the handler. :param skill: A :py:class:`ask_sdk_core.skill.CustomSkill` instance. If you are using the skill builder from ask-sdk, then you can use the ``create`` method under it, to create a skill instance :type skill: ask_sdk_core.skill.CustomSkill :param verify_signature: Enable request signature verification :type verify_signature: bool :param verify_timestamp: Enable request timestamp verification :type verify_timestamp: bool :param verifiers: An optional list of verifiers, that needs to be applied on the input request, before invoking the request handlers. For more information, look at `hosting the skill as webservice <https://developer.amazon.com/docs/custom-skills/host-a-custom-skill-as-a-web-service.html>`_ :type verifiers: list[ask_sdk_webservice_support.verifier.AbstractVerifier] :raises: :py:class:`TypeError` if the provided skill instance is not a :py:class:`ask_sdk_core.skill.CustomSkill` instance """ self._skill = skill if not isinstance(skill, CustomSkill): raise TypeError( "Invalid skill instance provided. Expected a custom " "skill instance.") if verifiers is None: verifiers = [] self._verifiers = verifiers if verify_signature: request_verifier = RequestVerifier( signature_cert_chain_url_key=SIGNATURE_CERT_CHAIN_URL_KEY, signature_key=SIGNATURE_KEY) self._verifiers.append(request_verifier) self._webservice_handler = WebserviceSkillHandler( skill=self._skill, verify_signature=False, verify_timestamp=verify_timestamp, verifiers=self._verifiers) self._webservice_handler._add_custom_user_agent("django-ask-sdk") super(SkillAdapter, self).__init__() @method_decorator(csrf_exempt) def dispatch(self, request, *args, **kwargs): # type: (HttpRequest, object, object) -> HttpResponse """Inspect the HTTP method and delegate to the view method. This is the default implementation of the :py:class:`django.views.View` method, which will inspect the HTTP method in the input request and delegate it to the corresponding method in the view. The only allowed method on this view is ``post``. :param request: The input request sent to the view :type request: django.http.HttpRequest :return: The response from the view :rtype: django.http.HttpResponse :raises: :py:class:`django.http.HttpResponseNotAllowed` if the method is invoked for other than HTTP POST request. :py:class:`django.http.HttpResponseBadRequest` if the request verification fails. :py:class:`django.http.HttpResponseServerError` for any internal exception. """ return super(SkillAdapter, self).dispatch(request) def post(self, request, *args, **kwargs): # type: (HttpRequest, object, object) -> HttpResponse """The method that handles HTTP POST request on the view. This method is called when the view receives a HTTP POST request, which is generally the request sent from Alexa during skill invocation. The request is verified through the registered list of verifiers, before invoking the request handlers. The method returns a :py:class:`django.http.JsonResponse` in case of successful skill invocation. :param request: The input request sent by Alexa to the skill :type request: django.http.HttpRequest :return: The response from the skill to Alexa :rtype: django.http.JsonResponse :raises: :py:class:`django.http.HttpResponseBadRequest` if the request verification fails. :py:class:`django.http.HttpResponseServerError` for any internal exception. """ try: content = request.body.decode( verifier_constants.CHARACTER_ENCODING) response = self._webservice_handler.verify_request_and_dispatch( http_request_headers=request.META, http_request_body=content) return JsonResponse(data=response, safe=False) except VerificationException: logger.exception(msg="Request verification failed") return HttpResponseBadRequest( content="Incoming request failed verification") except AskSdkException: logger.exception(msg="Skill dispatch exception") return HttpResponseServerError( content="Exception occurred during skill dispatch")
class SkillAdapter(object): """Provides a base interface to register skill and dispatch the request. The class constructor takes a :py:class:`ask_sdk_core.skill.CustomSkill` instance, the skill id, an optional list of :py:class:`ask_sdk_webservice_support.verifier.AbstractVerifier` instances and an optional flask application. One can also use the :meth:`init_app` method, to pass in a :py:class:`flask.Flask` application instance, to instantiate the config values and the webservice handler. The :meth:`dispatch_request` function can be used to map the input request to the skill invocation. The :meth:`register` function can also be used alternatively, to register the :meth:`dispatch_request` function to the provided route. By default, the :py:class:`ask_sdk_webservice_support.verifier.RequestVerifier` and :py:class:`ask_sdk_webservice_support.verifier.TimestampVerifier` instances are added to the skill verifier list. To disable this, set the :py:const:`VERIFY_SIGNATURE_APP_CONFIG` and :py:const:`VERIFY_TIMESTAMP_APP_CONFIG` app configuration values to ``False``. An instance of the extension is added to the application extensions mapping, under the key :py:const:`EXTENSION_NAME`. Since multiple skills can be configured on different routes in the same application, through multiple extension instances, each extension is added as a skill id mapping under the app extensions :py:const:`EXTENSION_NAME` dictionary. For example, to use this class with a skill created using ask-sdk-core: .. code-block:: python from flask import Flask from ask_sdk_core.skill_builder import SkillBuilder from flask_ask_sdk.skill_adapter import SkillAdapter app = Flask(__name__) skill_builder = SkillBuilder() # Register your intent handlers to skill_builder object skill_adapter = SkillAdapter( skill=skill_builder.create(), skill_id=<SKILL_ID>, app=app) @app.route("/"): def invoke_skill: return skill_adapter.dispatch_request() Alternatively, you can also use the ``register`` method: .. code-block:: python from flask import Flask from ask_sdk_core.skill_builder import SkillBuilder from flask_ask_sdk.skill_adapter import SkillAdapter app = Flask(__name__) skill_builder = SkillBuilder() # Register your intent handlers to skill_builder object skill_adapter = SkillAdapter( skill=skill_builder.create(), skill_id=<SKILL_ID>, app=app) skill_adapter.register(app=app, route="/") """ def __init__(self, skill, skill_id, verifiers=None, app=None): # type: (CustomSkill, int, List[AbstractVerifier], Flask) -> None """Instantiate the extension and set the app config values. :param skill: A :py:class:`ask_sdk_core.skill.CustomSkill` instance. If you are using the skill builder from ask-sdk, then you can use the ``create`` method under it, to create a skill instance :type skill: ask_sdk_core.skill.CustomSkill :param skill_id: Skill ID for the skill instance. This is used to store the skill under the app extensions :type skill_id: int :param verifiers: An optional list of verifiers, that needs to be applied on the input request, before invoking the request handlers. For more information, look at `hosting the skill as webservice <https://developer.amazon.com/docs/custom-skills/host-a-custom-skill-as-a-web-service.html>`_ :type verifiers: list[ask_sdk_webservice_support.verifier.AbstractVerifier] :param app: A :py:class:`flask.Flask` application instance :type app: flask.Flask :raises: :py:class:`TypeError` if the provided skill instance is not a :py:class:`ask_sdk_core.skill.CustomSkill` instance """ self._skill_id = skill_id self._skill = skill self._webservice_handler = None if verifiers is None: verifiers = [] self._verifiers = verifiers if not isinstance(skill, CustomSkill): raise TypeError( "Invalid skill instance provided. Expected a custom " "skill instance.") if app is not None: self.init_app(app) def init_app(self, app): # type: (Flask) -> None """Register the extension on the given Flask application. Use this function only when no Flask application was provided in the ``app`` keyword argument to the constructor of this class. The function sets ``True`` defaults for :py:const:`VERIFY_SIGNATURE_APP_CONFIG` and :py:const:`VERIFY_TIMESTAMP_APP_CONFIG` configurations. It adds the skill id: self instance mapping to the application extensions, and creates a :py:class:`ask_sdk_webservice_support.webservice_handler.WebserviceHandler` instance, for request verification and dispatch. :param app: A :py:class:`flask.Flask` application instance :type app: flask.Flask :rtype: None """ app.config.setdefault(VERIFY_SIGNATURE_APP_CONFIG, True) app.config.setdefault(VERIFY_TIMESTAMP_APP_CONFIG, True) if EXTENSION_NAME not in app.extensions: app.extensions[EXTENSION_NAME] = {} app.extensions[EXTENSION_NAME][self._skill_id] = self with app.app_context(): self._create_webservice_handler(self._skill, self._verifiers) def _create_webservice_handler(self, skill, verifiers): # type: (CustomSkill, List[AbstractVerifier]) -> None """Create the handler for request verification and dispatch. :param skill: A :py:class:`ask_sdk_core.skill.CustomSkill` instance. If you are using the skill builder from ask-sdk, then you can use the ``create`` method under it, to create a skill instance :type skill: ask_sdk_core.skill.CustomSkill :param verifiers: A list of verifiers, that needs to be applied on the input request, before invoking the request handlers. :type verifiers: list[ask_sdk_webservice_support.verifier.AbstractVerifier] :rtype: None """ if verifiers is None: verifiers = [] self._webservice_handler = WebserviceSkillHandler( skill=skill, verify_signature=current_app.config.get( VERIFY_SIGNATURE_APP_CONFIG, True), verify_timestamp=current_app.config.get( VERIFY_TIMESTAMP_APP_CONFIG, True), verifiers=verifiers) self._webservice_handler._add_custom_user_agent("flask-ask-sdk") def dispatch_request(self): # type: () -> Response """Method that handles request verification and routing. This method can be used as a function to register on the URL rule. The request is verified through the registered list of verifiers, before invoking the request handlers. The method returns a JSON response for the Alexa service to respond to the request. :return: The skill response for the input request :rtype: flask.Response :raises: :py:class:`werkzeug.exceptions.MethodNotAllowed` if the method is invoked for other than HTTP POST request. :py:class:`werkzeug.exceptions.BadRequest` if the verification fails. :py:class:`werkzeug.exceptions.InternalServerError` for any internal exception. """ if flask_request.method != "POST": raise exceptions.MethodNotAllowed() try: content = flask_request.data.decode( verifier_constants.CHARACTER_ENCODING) response = self._webservice_handler.verify_request_and_dispatch( http_request_headers=flask_request.headers, http_request_body=content) return jsonify(response) except VerificationException: current_app.logger.error("Request verification failed", exc_info=True) raise exceptions.BadRequest( description="Incoming request failed verification") except AskSdkException: current_app.logger.error("Skill dispatch exception", exc_info=True) raise exceptions.InternalServerError( description="Exception occurred during skill dispatch") def register(self, app, route, endpoint=None): # type: (Flask, str, str) -> None """Method to register the routing on the app at provided route. This is a utility method, that can be used for registering the ``dispatch_request`` on the provided :py:class:`flask.Flask` application at the provided URL ``route``. :param app: A :py:class:`flask.Flask` application instance :type app: flask.Flask :param route: The URL rule where the skill dispatch has to be registered :type route: str :param endpoint: The endpoint for the registered URL rule. This can be used to set multiple skill endpoints on same app. :type endpoint: str :rtype: None :raises: :py:class:`TypeError` if ``app`` or `route`` is not provided or is of an invalid type """ if app is None or not isinstance(app, Flask): raise TypeError("Expected a valid Flask instance") if route is None or not isinstance(route, str): raise TypeError("Expected a valid URL rule string") app.add_url_rule(route, view_func=self.dispatch_request, methods=["POST"], endpoint=endpoint)
class AskRequestHandler: def __init__(self, skill, verifiers=None, verify_signature=True, verify_timestamp=True): self._skill = skill self._verify_signature = verify_signature self._verify_timestamp = verify_timestamp if verifiers is None: verifiers = [] self._verifiers = verifiers if not isinstance(skill, CustomSkill): raise TypeError( "Invalid skill instance provided. Expected a custom " "skill instance.") self._create_webservice_handler() def _create_webservice_handler(self): self._webservice_handler = WebserviceSkillHandler( skill=self._skill, verify_signature=self._verify_signature, verify_timestamp=self._verify_timestamp, verifiers=self._verifiers) self._webservice_handler._add_custom_user_agent("cherrypy-ask-sdk") def dispatch(self): """Dispatch the POST request coming from an alexa (Ask SDK). We are directly decoding/encoding the request and responses and just passing along the request body and responses with the expected encoding for the ask_sdk. If everything goes well, we remove any possible next handler for the request, given that is considered full server when is handler by the ask_sdk. Something to keep in mind, is that we are not modifying the default request processors, these are there to handle regular form inputs, but because the request that the ask_sdk uses to communicate are POST with JSON bodies, those are basically ignored (unless we use the JSON tool). """ request = cherrypy.serving.request if request.method != "POST": raise cherrypy.HTTPError(405) try: response = self._webservice_handler.verify_request_and_dispatch( request.headers, request.body.read().decode( verifier_constants.CHARACTER_ENCODING)) except VerificationException: cherrypy.log.error("Request verification failed", traceback=True) raise cherrypy.HTTPError(400, "Incoming request failed verification") except AskSdkException: cherrypy.log.error("Skill dispatch exception", traceback=True) raise cherrypy.HTTPError( message="Exception occurred during skill dispatch") else: cherrypy.serving.response.body = json.dumps(response).encode( verifier_constants.CHARACTER_ENCODING) # remove the request handler if the request was handled as expected # using the ask sdk cherrypy.serving.request.handler = None