Package: django-aws-api-gateway-websockets
Django AWS API Gateway WebsocketsIt is the aim of this project to create a uniform way to record websocket connections, associate the Django user who established the connection and then retrieve that user within each request.This project is designed to work exclusively with AWS API Gateway.It is not intended to be a replacement ofDjango Channelsinstead this project allows you to addWebSocketssupport into your project by writing normal HTTP request-response views whilst allowingAWS API Gatewayto worry about the WebSocket connection.This project introduced a newClass-Based-Viewto handle connections, disconnections, routing, basic security checks and ensuring that the User object is available within every request.The project will keep track of which users created which WebSockets, which ones are active and will allow you to send messages back down the socket to the client via Boto3.Please refer to the installation notes and Getting Start Guides.Security ConcernsIMPORTANT:: In order to work the dispatch method requires thecsrf_exemptdecorator to be added. This has
already been added as a class decorator on the base view but if you overload the dispatch method you will need to add
it back to avoid receiving CSRF Token failures.Python and Django SupportThis project officially supports Python 3.8+ and Django 3.2+.Python/Django3.24.04.14.25.03.8YYYYN3.9YYYYN3.10YYYYY3.11NNY*YY3.12NNNYYPython 3.11 only works with Django 4.1.3+InstallationYou can install this package from pip usingpip install django-aws-api-gateway-websocketssettings.pyAdddjango_aws_api_gateway_websocketsintoINSTALLED_APPSIMPORTANTIf your site isnotalready running cross-origin you will need to update some settings and flush the sessions to ensure the primary domain and subdomain will work.Because the API Gateway will run from a subdomain you need ensure the cookies are set-up to allow subdomains to read them.
Assuming your site runs fromwww.example.comand you wanted to usews.www.example.comfor websockets you would need to
set the below CSRF and COOKIE settings (Django 4 examples shown)# CSRF
CSRF_COOKIE_SAMESITE=Lax
# CSRF_TRUSTED_ORIGINS=www.example.com,ws.www.example.com ## For Django 3*
## For Django 4+
CSRF_TRUSTED_ORIGINS=https://www.example.com,https://ws.www.example.com
CSRF_COOKIE_DOMAIN='.www.example.com'

# Sessions
SESSION_COOKIE_SAMESITE='Lax'
SESSION_COOKIE_NAME='mysessionid'
SESSION_COOKIE_DOMAIN='.www.example.com'If you wanted to usewww.example.comfor the main site andws.example.comfor websockets you'd need to use
these settings# CSRF
CSRF_COOKIE_SAMESITE=Lax
CSRF_TRUSTED_ORIGINS=https://www.example.com,https://ws.www.example.com
CSRF_COOKIE_DOMAIN='.example.com'

# Sessions
SESSION_COOKIE_SAMESITE='Lax'
SESSION_COOKIE_NAME='mysessionid'
SESSION_COOKIE_DOMAIN='.example.com'NOTE:You need to rename the SESSION cookie. In the example I have renamed if fromsessionidtomysessionid. This will ensure that any old cookies are ignored.Flushing SessionsBecause you are changing the session cookie you will also need to flush any cached sessions usingpython manage.py clearsessions.Clearing Stale Websocket connectionsThe websocket connections will become stale over time and some housekeeping is required.  To help there is a management
command clearWebSocketSessions that can be run to delete the closed connections from the database.  Simply runpython manage.py clearWebSocketSessions. I recommend setting this as a scheduled task.AWS SetupIn order for this package to create the API Gateway, it's routes, integration, custom domain and to publish messages
you will need to assign the correct permission to the IAM User/Role following best practices of restrictive permission.If you are using a EC2/ECS then you should be using an IAM Role otherwise use a user.This packagedoes notinclude creating an AWS Certificate as you may already have one. You should create that
yourself.  If you do not know how then see theAppendixsection at the end of this file.IAM PolicyYou'll need to grant the IAM permission to allow this project to create the API Gateway, create the domain mappings and
to execute the API to send messages from the server to the client(s).I'm still reviewing the "minimum required permissions" but this project has been tested with the
following IAM policy which you can copy and paste into the JSON editor within the AWS console:{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DjangoApiGatewayPolicy01",
            "Effect": "Allow",
            "Action": [
                "apigateway:GET",
                "apigateway:PATCH",
                "apigateway:POST",
                "apigateway:PUT",
                "execute-api:*",
                "iam:CreateServiceLinkedRole"
            ],
            "Resource": [
                "arn:aws:apigateway:*::/apis",
                "arn:aws:apigateway:*::/apis/*",
                "arn:aws:apigateway:*::/apis/*/authorizers",
                "arn:aws:apigateway:*::/apis/*/authorizers/*",
                "arn:aws:apigateway:*::/apis/*/cors",
                "arn:aws:apigateway:*::/apis/*/deployments",
                "arn:aws:apigateway:*::/apis/*/deployments/*",
                "arn:aws:apigateway:*::/apis/*/exports/*",
                "arn:aws:apigateway:*::/apis/*/integrations",
                "arn:aws:apigateway:*::/apis/*/integrations/*",
                "arn:aws:apigateway:*::/apis/*/integrations/*/integrationresponses",
                "arn:aws:apigateway:*::/apis/*/integrations/*/integrationresponses/*",
                "arn:aws:apigateway:*::/apis/*/models",
                "arn:aws:apigateway:*::/apis/*/models/*",
                "arn:aws:apigateway:*::/apis/*/models/*/template",
                "arn:aws:apigateway:*::/apis/*/routes",
                "arn:aws:apigateway:*::/apis/*/routes/*",
                "arn:aws:apigateway:*::/apis/*/routes/*/requestparameters/*",
                "arn:aws:apigateway:*::/apis/*/routes/*/routeresponses",
                "arn:aws:apigateway:*::/apis/*/routes/*/routeresponses/*",
                "arn:aws:apigateway:*::/apis/*/stages",
                "arn:aws:apigateway:*::/apis/*/stages/*",
                "arn:aws:apigateway:*::/apis/*/stages/*/accesslogsettings",
                "arn:aws:apigateway:*::/apis/*/stages/*/cache/authorizers",
                "arn:aws:apigateway:*::/apis/*/stages/*/routesettings/*",
                "arn:aws:apigateway:{AWS-REGION-NAME}::/domainnames",
                "arn:aws:apigateway:{AWS-REGION-NAME}::/domainnames/*/apimappings",
                "arn:aws:apigateway:{AWS-REGION-NAME}::/domainnames/*/apimappings/*",
                "arn:aws:execute-api:{AWS-REGION-NAME}:{AWS-ACCOUNT-NUMBER}:*/*/*/*",
                "arn:aws:iam::{AWS-ACCOUNT-NUMBER}:role/aws-service-role/ops.apigateway.amazonaws.com/AWSServiceRoleForAPIGateway"
            ]
        }
    ]
}You will need to edit the permissio and replace the following:{AWS-REGION-NANE}with the correct AWS region you are using, E.Geu-west-1. If you wish to grant access to all regions then replace this placeholder with an*{AWS-ACCOUNT-NUMBER}with your account number E.G: 123456789101This policy grants permissions to ensure the API Gateway(s) will be created, the custom domain name mapped to the
gateway and that you can send messages from the server to clients.  The AWS Service role is required as it's used when
you create a custom domain name for API Gateway. If you do this via the console it will create the role for you so we
need to ensure the IAM user has the permission in order to replicate this.Once you have created your API Gateway(s) you may wish to follow AWS best practice and restrict of revoke the
permissions to the API Gateway(s) you have created.  Because I do not know what you will name your gateway, the
permissions above will allow you to add/edit and API gateway on your account.Getting StartedThe core files within this project are:django_aws_api_gateway_websockets.views.WebSocketView- The base class-based view from which you should extenddjango_aws_api_gateway_websockets.models.ApiGateway- A model for managing the API Gateway. A Django Admin
page is included along with custom actions to create the API Gateway and configure a Custom Domain.  For those with
projects not using Django Admin there are two management commands that perform the same actions.django_aws_api_gateway_websockets.models.WebSocketSession- The websocket session store. Every connection
writes to this model which contains a method to send a message to the connection.  The QuerySet of the objects model
manager has been extended to include a method to send messages to all records included within a queryset.DjangoURLS.pyEdit your urls.py file and add an entry for the URL you wish API Gateway to call.IMPORTANTThe slug parameter
must be called "route". This will be populated by API Gateway with the route it uses E.G. $connect, $default or
$disconnectE.G.path("ws/<slug:route>", ExampleWebSocketView.as_view(), name="example_websocket")Creating the ViewsSubclass theWebSocketViewand implement methods where the name of the method is the name of the route the
API Gateway has been setup to use. There are already methods for $connect and $disconnect you just need to implement
a method fordefaultalong with any other custom routes you have created.  The methods are selected dynamically
via thedispatchmethod with any leading dollar sign being remove.The methods take therequestparameter and only needs to return a response if you wish to return a negative HTTP
response such as a HttpResponseBadRequest otherwise there is no need to return anything.from django_aws_api_gateway_websockets.views import WebSocketView

class ExampleWebSocketView(WebSocketView):
    """Custom Websocket view."""

    def default(self, request, *args, **kwargs) -> JsonResponse:
        """Add the logic you wish to make here when you receive a message.
         create your JSON response that you will handle within the Javascript
         """
        logger.debug(f"body {self.body}")If you want to send a response to the websocket that made the request then you need to call thesend_message()on
the WebSocketSession that is being used. See the example belowfrom django_aws_api_gateway_websockets.views import WebSocketView

class ExampleWebSocketView(WebSocketView):
    """Custom Websocket view."""

    def default(self, request, *args, **kwargs) -> JsonResponse:
        # Do stuff 
        ...        
        
        # Send a message back to the client - i.e unicast
        self.websocket_session.send_message({"key1": "value1", "key2": "value2"})If you are using the "channels" to group WebSocket connections together for multicasting, that is one-to-many
communication then you can use the following examplefrom django_aws_api_gateway_websockets.models import WebSocketSession
from django_aws_api_gateway_websockets.views import WebSocketView


class ExampleWebSocketView(WebSocketView):
    """Custom Websocket view."""

    def default(self, request, *args, **kwargs) -> JsonResponse:
        # Do stuff 
        ...        
        
        # Multicast a message to ALL CONNECTED clients on the same "channel"
        WebSocketSession.objects.filter(
            channel_name=self.websocket_session.channel_name
        ).send_message({"key": "value})

        # Multicast a message to ALL CONNECTED clients who are with the 
        # channel_nme "my-example-channel"
        WebSocketSession.objects.filter(
            channel_name=my-example-channel
        ).send_message(
            {
                "key": "value
            }
        )Using the Route Selection Key value to call specific methodsAPI Gateway works by routing messages based on the "Route Selection Key". This project sets you up with a default route
so that you have a catch-all route but the Route Selection Key is preserved and is used by the dispatch method when
selecting the method to use to handle the request.  This means that you can write individual methods to handle each
route individually for cleaner, more testable code.In the example that follows the default Route Selection Key ofactionis being used.
Assume that two series of sends are made to the WebSocket. The first with the payload:{"action": "test", "value": "hello world"}and the second{"action": "help", "value": "Help Me"}. You can
either handle these within the catch-alldefaultmethod or you can write individual methods for each actionfrom django_aws_api_gateway_websockets.models import WebSocketSession
from django_aws_api_gateway_websockets.views import WebSocketView


class ExampleWebSocketView(WebSocketView):
    """Custom Websocket view."""

    def test(self, request, *args, **kwargs) -> JsonResponse:
        print(self.body.get("value")
        # Prints "hello world" 

    def help(self, request, *args, **kwargs) -> JsonResponse:
        print(self.body.get("value")
        # Prints "Help Me"Remember: The "action" key is the defaultroute_selection_key, if you chose to use a different one when
setting uip the websocket make sure to update theroute_selection_keyclass property to use the same valueDebugging the ViewSometimes you the view may return a HTTP400 that you wish to debug further. In order to help with this you can passdebug=Trueinto theas_view()method. The class will then call the private method_debug(msg)passing
in a string. By default this method will update a list property calleddebug_logwith the message string but
you may wish to simply overload the method and call your logger.E.G.def _debug(self, msg: str):
    if self.debug:
        logger.debug(msg)This can help track the issue which may be as simply as sending a message from the client that is missing theroute_select_key.Example of sending a message from the server to the clientTo send a message to a specific connection simple load itsWebSocketSessionrecord and then call thesend_messagemethod passing in a JSON compatible dictionary of the payload you wish to send to the client.Sending a message to one connectionfromdjango_aws_api_gateway_websockets.modelsimportWebSocketSessionobj=WebSocketSession.objects.get(pk=1)obj.send_message({"type":"example","msg":"This is a message"})Sending a message to ALL active connections associated with the same channelfromdjango_aws_api_gateway_websockets.modelsimportWebSocketSessionWebSocketSession.objects.filter(channel_name="Chatroom 1").send_message({"msg":"This is a a sample message"})TheWebSocketSessionQuerySet.send_messagemethod automatically adds a filter ofconnected=TrueDjango AdminThree Django Admin pages will be added to your project under the appDjango AWS APIGateway WebSockets. Those pages
allow you to view and manage the three base models.Creating an API Gateway EndpointImportantThis section assumes that you are using an IAM account with the permissions listed earlier.Using the Django Admin page create a new API Gateway record using the following for reference:API Name- The human friendly API NameAPI Description- OptionalDefault channel name- Fill this in if you want all connections to this Websocket to also be associated with
the same "channel" otherwise leave it blank. "Channels" are groups of web socket connections and nothing more.Target base Endpoint- This is the full URL path to the view you wish to use to handle the requestsexcludingtherouteslug portion that will be automatically appended.Certificate ARN- You'll need to manually create certificate within AWS. Once you have, copy the ARN into this fieldHosted Zone ID- If you use Route53 then you'll need to enter the Hosted Zone ID here if you wish to use a custom
domain name with the API Gateway EndpointAPI Key Selection Expression- In most cases leave this as the default value. See the AWS docs for moreRoute selection expression- As per the above. This is the field that maps the "action" key within the payload
as being the key to determine the route to take.  If you change this then you must overload theroute_selection_keyof the viewRoute key- This is the default root key. In most cases you will not need to change this.Stage Name- The name you wish to give to the staging. Currently this package does not support multiple stages.
If you leave it blank it will default to "production"**Stage description" - OptionalTags- Currently not implement but these will be used to create the tags with AWSAPI ID- This will be populated when the API is created.API Endpoint- This will be populated when the API is created.API Gateway Domain Name- This will be populated when you run the Custom Domain setup. The value that appears
here is the value to which you should your DNS CNAME entry should point.API Mapping ID- This will be populated when the API is created.Once you have created the record within the database simply select it from the Django Admin list view, chooseCreate API Gatewayaction from the actions list and click Go.  The API Gateway record will be created within your
account. When it's ready the "API Created" column will show as True.Once the API has been created you can now add a custom domain name mapping by choosing the row again and this time
selecting theCreate Custom Domain record for the API. This will create the Custom Domain record and will associate
it with the stage name you entered earlier. Once it's completed theCustom Domain Createdflag will be set as True.At this point you can open the record where you'll find that theAPI Gateway Domain Namehas been populated.Django Management CommandsIf you are not using Django Admin then you can populate the apigateway database table manually using the same list
as shown above.Once you've populated those fields you an then run the two actions as management commands rather than via Django Admin.python manage.py createApiGateway --pk=1python manage.py createCustomDomain --pk=1The same actions will run as above.Adding Additional RoutesThis project will route all requests to one URL by default however that is not always what you will need,
sometimes you will want to route requests to different URL to send requests to different views potentially within
different apps.  API Gateway support this by using creating integrations and routes that use unique "route keys" to
identify requests and then route those requests to a URL.To support this approach this project usesApiGatewayAdditionalRouteentries to map a route key to a chosen URL.
They are available to manage as inline forms on the mainApiGatewayadmin or as their own admin page.The models are set-up to auto-deploy the new route if theApiGatewayhas already been deployed. If it has not been
deployed already then these routes will be setup during the main deployment.From the client side you choose the route you wish to take by setting the value of theactionto the route key you
wish to match.Gotchas and debuggingFailure to connect to websocketsDiffering required headersThe most common reasons for the websocket failing to connect is due to different required headers. The base view is
set-up with two lists of expected headers.required_headersandadditional_required_headers. If you are
deploying to an EC2 server then you shouldn't have to change these but if you are deploying else where or are testing
locally you may find that you need to change some of these. During development of this library I was using anNGROKnetwork edge tunnel and found that the "X-Real-Ip" and "Connection" headers were being lost
during which is why they were moved to the additional_required_headers. If you find this is the case for you then simply
overload the class property and set it to an empty list.Client Side Integration (Javascript)This section will guide you through two common ways of connecting to and using this project from a webpage.Basic IntegrationBelow is a very basic integration using the WebSockets API built into browsers. It does not handle reconnecting dropped
websockets, see the next section for that.WARNING: This method will create a WebSocket that will timeout after around 10 minutes.The below example assumes you created the API Gateway to work on the custom domain name ws.example.comletwss_url='wss://ws.example.com';letregDeskWSocket=newWebSocket(wss_url);regDeskWSocket.onmessage=function(event){// Take your action here to handle messages being receivedconsole.log(event);letmsg=JSON.parse(event.data);console.log(msg);};You can set the channel by using thechannelquerystring parameter during the connectionletwss_url='wss://ws.example.com?channel=my+example+channel';letexampleWS=newWebSocket(wss_url);exampleWS.onmessage=function(event){// Take your action here to handle messages being receivedconsole.log(event);letmsg=JSON.parse(event.data);console.log(msg);};Reconnecting WebSocketsWebsockets can disconnect due top a variety of reasons to work around this here are some links to libraries of proposed
solutionsStack Overflow- WebSocket: How to automatically reconnect after it diesJS library - reconnecting-websocketThe below example is using the JS library. Note you just include the lib and then use theReconnectingWebSocketclass rather thanWebSocket:<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/reconnecting-websocket/1.0.0/reconnecting-websocket.min.js"integrity="sha512-B4skI5FiLurS86aioJx9VfozI1wjqrn6aTdJH+YQUmCZum/ZibPBTX55k5d9XM6EsKePDInkLVrN7vPmJxc1qA=="crossorigin="anonymous"referrerpolicy="no-referrer"></script><script>letwss_url='wss://ws.example.com';letexampleWS=newReconnectingWebSocket(wss_url);exampleWS.onmessage=function(event){// Take your action here to handle messages being receivedconsole.log(event);letmsg=JSON.parse(event.data);console.log(msg);};</script>Sending a message from the client to the serverBoth the example above use the same method.letwss_url='wss://ws.example.com?channel=my+example+channel';letexampleWS=newWebSocket(wss_url);// Or use ReconnectingWebSocket it does not matter// Send a messageexampleWS.send(JSON.stringify({"action":"custom","message":"What is this"}))IMPORTANTThe value ofactiondetermines the route that is used byAPI Gateway. By default, the only
routes that are set-up are$connect,$disconnectanddefault. Any messages sent to unknown routes on
the API Gateway are delivered to thedefaultroute.  So if you created a custom route calledboband then
sent the following message from the client:exampleWS.send(JSON.stringify({"action": "bob", "message": "What is this"}))API Gateway will route this to the endpoint set for the "bob" route. This will be calling your view with the route slugs
value being assigned tobob. Thedispatchmethod of the view will then look for a method on the class calledbob. If one is found then it will be invoked otherwise thedefaultmethod will be called.AppendixCreating an SSL Certificate within AWS Certificate ManagerPlease refer to theofficial AWS documentationFound a Bug?Issues are tracked via GitHub issues at theproject issue pageHave A Feature Request?Feature requests can be raised by creating an issue within theproject issue page, but please create the issue with "Feature Request -" at the start of the issueTestingTo run the tests usecoverage erase && \
python -W error::DeprecationWarning -W error::PendingDeprecationWarning -m coverage run --parallel -m pytest --ds tests.settings && \
coverage combine && \
coverage reportCompiling RequirementsRunpip install pip-toolsthen runpython requirements/compile.pyto generate the various requirements files.
I use two local VIRTUALENVS to build the requirements, one running Python3.8 and the other running Python 3.11.Also requirepytest-djangofor testingtoxContributingCheck for open issuesat the project issue page or open a new issue to start a discussion about a feature or bug.Fork therepository on GitHubto start making changes.Clone the repositoryInitialise pre-commit by runningpre-commit installInstall requirements from one of the requirement files depending on the versions of Python and Django you wish to use.Add a test case to show that the bug is fixed or the feature is implemented correctly.Test usingpython -W error::DeprecationWarning -W error::PendingDeprecationWarning -m coverage run --parallel -m pytest --ds tests.settingsCreate a pull request, tagging the issue, bug me until I can merge your pull request. Also, don't forget to add yourself to AUTHORS.