tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

django.rst (10983B)


      1 Integrate with Django
      2 =====================
      3 
      4 If you're looking at adding real-time capabilities to a Django project with
      5 WebSocket, you have two main options.
      6 
      7 1. Using Django Channels_, a project adding WebSocket to Django, among other
      8   features. This approach is fully supported by Django. However, it requires
      9   switching to a new deployment architecture.
     10 
     11 2. Deploying a separate WebSocket server next to your Django project. This
     12   technique is well suited when you need to add a small set of real-time
     13   features — maybe a notification service — to an HTTP application.
     14 
     15 .. _Channels: https://channels.readthedocs.io/
     16 
     17 This guide shows how to implement the second technique with websockets. It
     18 assumes familiarity with Django.
     19 
     20 Authenticate connections
     21 ------------------------
     22 
     23 Since the websockets server runs outside of Django, we need to integrate it
     24 with ``django.contrib.auth``.
     25 
     26 We will generate authentication tokens in the Django project. Then we will
     27 send them to the websockets server, where they will authenticate the user.
     28 
     29 Generating a token for the current user and making it available in the browser
     30 is up to you. You could render the token in a template or fetch it with an API
     31 call.
     32 
     33 Refer to the topic guide on :doc:`authentication <../topics/authentication>`
     34 for details on this design.
     35 
     36 Generate tokens
     37 ...............
     38 
     39 We want secure, short-lived tokens containing the user ID. We'll rely on
     40 `django-sesame`_, a small library designed exactly for this purpose.
     41 
     42 .. _django-sesame: https://github.com/aaugustin/django-sesame
     43 
     44 Add django-sesame to the dependencies of your Django project, install it, and
     45 configure it in the settings of the project:
     46 
     47 .. code-block:: python
     48 
     49    AUTHENTICATION_BACKENDS = [
     50        "django.contrib.auth.backends.ModelBackend",
     51        "sesame.backends.ModelBackend",
     52    ]
     53 
     54 (If your project already uses another authentication backend than the default
     55 ``"django.contrib.auth.backends.ModelBackend"``, adjust accordingly.)
     56 
     57 You don't need ``"sesame.middleware.AuthenticationMiddleware"``. It is for
     58 authenticating users in the Django server, while we're authenticating them in
     59 the websockets server.
     60 
     61 We'd like our tokens to be valid for 30 seconds. We expect web pages to load
     62 and to establish the WebSocket connection within this delay. Configure
     63 django-sesame accordingly in the settings of your Django project:
     64 
     65 .. code-block:: python
     66 
     67    SESAME_MAX_AGE = 30
     68 
     69 If you expect your web site to load faster for all clients, a shorter lifespan
     70 is possible. However, in the context of this document, it would make manual
     71 testing more difficult.
     72 
     73 You could also enable single-use tokens. However, this would update the last
     74 login date of the user every time a WebSocket connection is established. This
     75 doesn't seem like a good idea, both in terms of behavior and in terms of
     76 performance.
     77 
     78 Now you can generate tokens in a ``django-admin shell`` as follows:
     79 
     80 .. code-block:: pycon
     81 
     82    >>> from django.contrib.auth import get_user_model
     83    >>> User = get_user_model()
     84    >>> user = User.objects.get(username="<your username>")
     85    >>> from sesame.utils import get_token
     86    >>> get_token(user)
     87    '<your token>'
     88 
     89 Keep this console open: since tokens expire after 30 seconds, you'll have to
     90 generate a new token every time you want to test connecting to the server.
     91 
     92 Validate tokens
     93 ...............
     94 
     95 Let's move on to the websockets server.
     96 
     97 Add websockets to the dependencies of your Django project and install it.
     98 Indeed, we're going to reuse the environment of the Django project, so we can
     99 call its APIs in the websockets server.
    100 
    101 Now here's how to implement authentication.
    102 
    103 .. literalinclude:: ../../example/django/authentication.py
    104 
    105 Let's unpack this code.
    106 
    107 We're calling ``django.setup()`` before doing anything with Django because
    108 we're using Django in a `standalone script`_. This assumes that the
    109 ``DJANGO_SETTINGS_MODULE`` environment variable is set to the Python path to
    110 your settings module.
    111 
    112 .. _standalone script: https://docs.djangoproject.com/en/stable/topics/settings/#calling-django-setup-is-required-for-standalone-django-usage
    113 
    114 The connection handler reads the first message received from the client, which
    115 is expected to contain a django-sesame token. Then it authenticates the user
    116 with ``get_user()``, the API for `authentication outside a view`_. If
    117 authentication fails, it closes the connection and exits.
    118 
    119 .. _authentication outside a view: https://django-sesame.readthedocs.io/en/stable/howto.html#outside-a-view
    120 
    121 When we call an API that makes a database query such as ``get_user()``, we
    122 wrap the call in :func:`~asyncio.to_thread`. Indeed, the Django ORM doesn't
    123 support asynchronous I/O. It would block the event loop if it didn't run in a
    124 separate thread. :func:`~asyncio.to_thread` is available since Python 3.9. In
    125 earlier versions, use :meth:`~asyncio.loop.run_in_executor` instead.
    126 
    127 Finally, we start a server with :func:`~websockets.server.serve`.
    128 
    129 We're ready to test!
    130 
    131 Save this code to a file called ``authentication.py``, make sure the
    132 ``DJANGO_SETTINGS_MODULE`` environment variable is set properly, and start the
    133 websockets server:
    134 
    135 .. code-block:: console
    136 
    137    $ python authentication.py
    138 
    139 Generate a new token — remember, they're only valid for 30 seconds — and use
    140 it to connect to your server. Paste your token and press Enter when you get a
    141 prompt:
    142 
    143 .. code-block:: console
    144 
    145    $ python -m websockets ws://localhost:8888/
    146    Connected to ws://localhost:8888/
    147    > <your token>
    148    < Hello <your username>!
    149    Connection closed: 1000 (OK).
    150 
    151 It works!
    152 
    153 If you enter an expired or invalid token, authentication fails and the server
    154 closes the connection:
    155 
    156 .. code-block:: console
    157 
    158    $ python -m websockets ws://localhost:8888/
    159    Connected to ws://localhost:8888.
    160    > not a token
    161    Connection closed: 1011 (internal error) authentication failed.
    162 
    163 You can also test from a browser by generating a new token and running the
    164 following code in the JavaScript console of the browser:
    165 
    166 .. code-block:: javascript
    167 
    168    websocket = new WebSocket("ws://localhost:8888/");
    169    websocket.onopen = (event) => websocket.send("<your token>");
    170    websocket.onmessage = (event) => console.log(event.data);
    171 
    172 If you don't want to import your entire Django project into the websockets
    173 server, you can build a separate Django project with ``django.contrib.auth``,
    174 ``django-sesame``, a suitable ``User`` model, and a subset of the settings of
    175 the main project.
    176 
    177 Stream events
    178 -------------
    179 
    180 We can connect and authenticate but our server doesn't do anything useful yet!
    181 
    182 Let's send a message every time a user makes an action in the admin. This
    183 message will be broadcast to all users who can access the model on which the
    184 action was made. This may be used for showing notifications to other users.
    185 
    186 Many use cases for WebSocket with Django follow a similar pattern.
    187 
    188 Set up event bus
    189 ................
    190 
    191 We need a event bus to enable communications between Django and websockets.
    192 Both sides connect permanently to the bus. Then Django writes events and
    193 websockets reads them. For the sake of simplicity, we'll rely on `Redis
    194 Pub/Sub`_.
    195 
    196 .. _Redis Pub/Sub: https://redis.io/topics/pubsub
    197 
    198 The easiest way to add Redis to a Django project is by configuring a cache
    199 backend with `django-redis`_. This library manages connections to Redis
    200 efficiently, persisting them between requests, and provides an API to access
    201 the Redis connection directly.
    202 
    203 .. _django-redis: https://github.com/jazzband/django-redis
    204 
    205 Install Redis, add django-redis to the dependencies of your Django project,
    206 install it, and configure it in the settings of the project:
    207 
    208 .. code-block:: python
    209 
    210    CACHES = {
    211        "default": {
    212            "BACKEND": "django_redis.cache.RedisCache",
    213            "LOCATION": "redis://127.0.0.1:6379/1",
    214        },
    215    }
    216 
    217 If you already have a default cache, add a new one with a different name and
    218 change ``get_redis_connection("default")`` in the code below to the same name.
    219 
    220 Publish events
    221 ..............
    222 
    223 Now let's write events to the bus.
    224 
    225 Add the following code to a module that is imported when your Django project
    226 starts. Typically, you would put it in a ``signals.py`` module, which you
    227 would import in the ``AppConfig.ready()`` method of one of your apps:
    228 
    229 .. literalinclude:: ../../example/django/signals.py
    230 
    231 This code runs every time the admin saves a ``LogEntry`` object to keep track
    232 of a change. It extracts interesting data, serializes it to JSON, and writes
    233 an event to Redis.
    234 
    235 Let's check that it works:
    236 
    237 .. code-block:: console
    238 
    239    $ redis-cli
    240    127.0.0.1:6379> SELECT 1
    241    OK
    242    127.0.0.1:6379[1]> SUBSCRIBE events
    243    Reading messages... (press Ctrl-C to quit)
    244    1) "subscribe"
    245    2) "events"
    246    3) (integer) 1
    247 
    248 Leave this command running, start the Django development server and make
    249 changes in the admin: add, modify, or delete objects. You should see
    250 corresponding events published to the ``"events"`` stream.
    251 
    252 Broadcast events
    253 ................
    254 
    255 Now let's turn to reading events and broadcasting them to connected clients.
    256 We need to add several features:
    257 
    258 * Keep track of connected clients so we can broadcast messages.
    259 * Tell which content types the user has permission to view or to change.
    260 * Connect to the message bus and read events.
    261 * Broadcast these events to users who have corresponding permissions.
    262 
    263 Here's a complete implementation.
    264 
    265 .. literalinclude:: ../../example/django/notifications.py
    266 
    267 Since the ``get_content_types()`` function makes a database query, it is
    268 wrapped inside :func:`asyncio.to_thread()`. It runs once when each WebSocket
    269 connection is open; then its result is cached for the lifetime of the
    270 connection. Indeed, running it for each message would trigger database queries
    271 for all connected users at the same time, which would hurt the database.
    272 
    273 The connection handler merely registers the connection in a global variable,
    274 associated to the list of content types for which events should be sent to
    275 that connection, and waits until the client disconnects.
    276 
    277 The ``process_events()`` function reads events from Redis and broadcasts them
    278 to all connections that should receive them. We don't care much if a sending a
    279 notification fails — this happens when a connection drops between the moment
    280 we iterate on connections and the moment the corresponding message is sent —
    281 so we start a task with for each message and forget about it. Also, this means
    282 we're immediately ready to process the next event, even if it takes time to
    283 send a message to a slow client.
    284 
    285 Since Redis can publish a message to multiple subscribers, multiple instances
    286 of this server can safely run in parallel.
    287 
    288 Does it scale?
    289 --------------
    290 
    291 In theory, given enough servers, this design can scale to a hundred million
    292 clients, since Redis can handle ten thousand servers and each server can
    293 handle ten thousand clients. In practice, you would need a more scalable
    294 message bus before reaching that scale, due to the volume of messages.