Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
djangochannelsrestframework
Advanced tools
Django Channels Rest Framework provides a DRF like interface for building channels-v4_ websocket consumers.
This project can be used alongside HyperMediaChannels_ and ChannelsMultiplexer_ to create a Hyper Media Style api over websockets. However Django Channels Rest Framework is also a free standing framework with the goal of providing an api that is familiar to DRF users.
theY4Kman_ has developed a useful Javascript client library dcrf-client_ to use with DCRF.
DCRF is based of a fork of Channels Api <https://github.com/linuxlewis/channels-api>
_ and of course inspired by Django Rest Framework <http://www.django-rest-framework.org/>
_.
ReadTheDocs_
.. code-block:: bash
pip install djangochannelsrestframework
.. warning ::
In your application definition when you declare your consumers it is very important to use the ``.as_asgi()`` class method (e.g. ``MyConsumer.as_asgi()``). You **should not** have any instances of ``MyConsumer()`` in your code base.
In DCRF you can create a GenericAsyncAPIConsumer
that works much like a GenericAPIView_ in DRF: For a more indepth look into Rest-Like Websocket consumers read this blog post_.
.. code-block:: python
from . import models
from . import serializers
from djangochannelsrestframework import permissions
from djangochannelsrestframework.generics import GenericAsyncAPIConsumer
from djangochannelsrestframework.mixins import (
ListModelMixin,
PatchModelMixin,
UpdateModelMixin,
CreateModelMixin,
DeleteModelMixin,
)
class LiveConsumer(ListModelMixin, GenericAsyncAPIConsumer):
queryset = models.Test.objects.all()
serializer_class = serializers.TestSerializer
permission_classes = (permissions.IsAuthenticated,)
Because this class uses the ListModelMixin
, one has access to the list
action.
One may use the same exact querysets and serializer_class
utilized in their DRF Views, but must omit the DRF permissions. Permissions are to be imported from djangochannelsrestframework
, which provides the standard AllowAny
and IsAuthenticated
permissions.
To call an action from the client send a websocket message: {action: "list", "request_id": 42}
There are a selection of mixins that expose the common CRUD actions:
ListModelMixin
- list
PatchModelMixin
- patch
CreateModelMixin
- create
RetrieveModelMixin
- retrieve
UpdateModelMixin
- update
DeleteModelMixin
- delete
Consumer that let you subscribe to changes on an instance:
.. code-block:: python
class TestConsumer(ObserverModelInstanceMixin, GenericAsyncAPIConsumer): queryset = get_user_model().objects.all() serializer_class = UserSerializer
this exposes the retrieve
, subscribe_instance
and unsubscribe_instance
actions.
To subscribe send:
.. code-block:: python
{ "action": "subscribe_instance", "pk": 42, # the id of the instance you are subscribing to "request_id": 4 # this id will be used for all result updates. }
Actions will be sent down out from the server:
.. code-block:: python
{ "action": "update", "errors": [], "response_status": 200, "request_id": 4, "data": {'email': '42@example.com', 'id': 42, 'username': 'thenewname'}, }
.. code-block:: python
class UserConsumer(GenericAsyncAPIConsumer): queryset = get_user_model().objects.all() serializer_class = UserSerializer
@action()
async def send_email(self, pk=None, to=None, **kwargs):
user = await database_sync_to_async(self.get_object)(pk=pk)
# ... do some stuff
# remember to wrap all db actions in `database_sync_to_async`
return {}, 200 # return the context and the response code.
@action() # if the method is not async it is already wrapped in `database_sync_to_async`
def publish(self, pk=None, **kwargs):
user = self.get_object(pk=pk)
# ...
return {'pk': pk}, 200
You can also create consumers that are not at all related to any models.
.. code-block:: python
from djangochannelsrestframework.decorators import action from djangochannelsrestframework.consumers import AsyncAPIConsumer
class MyConsumer(AsyncAPIConsumer):
@action()
async def an_async_action(self, some=None, **kwargs):
# do something async
return {'response with': 'some message'}, 200
@action()
def a_sync_action(self, pk=None, **kwargs):
# do something sync
return {'response with': 'some message'}, 200
.. code-block:: python
from djangochannelsrestframework.consumers import view_as_consumer
application = ProtocolTypeRouter({ "websocket": AuthMiddlewareStack( URLRouter([ url(r"^front(end)/$", view_as_consumer(YourDjangoView)), ]) ), })
In this situation if your view needs to read the GET
query string values you can provides these using the query
option.
And if the view method reads parameters from the URL you can provides these with the parameters
.
Sending the following over your WS connection will result in a GET request being evaluated on your View.
.. code-block:: javascript
{
action: "retrieve",
query: {"user_id": 42}
parameters: {"project_id": 92}
}
One can subscribe to a custom Signal
utilizing the observer
decorator.
Here we have a custom signal that will be triggered when a user join a chat.
.. code-block:: python
# signals.py
from django.dispatch.dispatcher import Signal
joined_chat_signal = Signal()
Now we will create the consumer with two actions, one for subscribing to our custom signal for specific chat, and another one for manually trigger the signal.
.. code-block:: python
# consumers.py
from djangochannelsrestframework.consumers import AsyncAPIConsumer
from djangochannelsrestframework.decorators import action
from djangochannelsrestframework.observer import observer
from rest_framework import status
from .signals import joined_chat_signal
from .serializers import UserSerializer
class TestConsumer(AsyncAPIConsumer):
@action()
def join_chat(self, chat_id, **kwargs):
serializer = UserSerializer(instance=self.scope['user'])
joined_chat_signal.send(sender='join_chat', data=serializer.data, **kwargs)
return {}, status.HTTP_204_NO_CONTENT
@observer(signal=joined_chat_signal)
async def joined_chat_handler(self, data, observer=None, action=None, subscribing_request_ids=[], **kwargs):
for request_id in subscribing_request_ids:
await self.reply(action='joined_chat', data=data, status=status.HTTP_200_OK, request_id=request_id)
@joined_chat_handler.serializer
def join_chat_handler(self, sender, data, **kwargs): # the data comes from the signal.send and will be available in the observer
return data
@joined_chat_handler.groups_for_signal
def joined_chat_handler(self, instance, **kwargs):
yield f'chat__{instance}'
@joined_chat_handler.groups_for_consumer
def joined_chat_handler(self, chat, **kwargs):
if chat:
yield f'chat__{chat}'
@action()
async def subscribe_joined(self, chat_id, request_id, **kwargs):
await self.joined_chat_handler.subscribe(chat_id, request_id=request_id)
One can subscribe to all instances of a model by utilizing the model_observer
.
.. code-block:: python
from djangochannelsrestframework.observer import model_observer
@model_observer(models.Test)
async def model_activity(self, message, observer=None, action=None, **kwargs):
# send activity to your frontend
await self.send_json(message)
This method will send messages to the client on all CRUD operations made through the Django ORM. The action
arg here it will take values such as create
, delete
and update
you should consider passing this to your frontend client.
Note: These notifications do not include bulk updates, such as models.Test.objects.filter(name="abc").update(name="newname")
**WARNING**
When using this to decorate a method to avoid the method firing multiple
times you should ensure that if there are multiple `@model_observer`
wrapped methods for the same model type within a single file that each
method has a different name.
model_observer
You can do this in a few placed, a common example is in the websocket_connect
method.
.. code-block:: python
async def websocket_connect(self, message):
# Super Save
await super().websocket_connect(message)
# Initialized operation
await self.activities_change.subscribe()
This method utilizes the previously mentioned model_activity
method to subscribe to all instances of the current Consumer's model.
One can also subscribe by creating a custom action
Another way is override AsyncAPIConsumer.accept(self, **kwargs)
.. code-block:: python
class ModelConsumerObserver(AsyncAPIConsumer):
async def accept(self, **kwargs):
await super().accept(** kwargs)
await self.model_change.subscribe()
@model_observer(models.Test)
async def model_change(self, message, action=None, **kwargs):
"""
This method is evaluated once for every user that subscribed,
here you have access to info about the user by reading `self.scope`
However it is best to avoid doing DB quires here since if you have lots of
subscribers to a given instance you will end up with a LOT of database traffic.
"""
await self.send_json(message)
# If you want the data serialized instead of pk
@model_change.serializer
def model_serialize(self, instance, action, **kwargs):
"""
This block is evaluated before the data is sent over the channel layer
this means you are unable to access information
such as the user that it will be sent to.
If you need the user info when serializing then you can do the serialization
in the above method.
"""
return TestSerializer(instance).data
.. note::
New Feature!
Now you can rewrite this as:
.. code-block:: python
class ModelConsumerObserver(AsyncAPIConsumer):
async def accept(self, **kwargs):
await super().accept(** kwargs)
await self.model_change.subscribe()
@model_observer(models.Test, serializer_class=TestSerializer)
async def model_change(self, message, action=None, **kwargs):
# in this case since we subscribe int he `accept` method
# we do not expect to have any `subscribing_request_ids` to loop over.
await self.reply(data=message, action=action)
In most situations you want to filter the set of models that you subscribe to.
To do this we need to split the model updates into groups
and then in the consumer subscribe to the groups that we want/have permission to see.
.. code-block:: python
class MyConsumer(AsyncAPIConsumer):
# This class MUST subclass AsyncAPIConsumer
to use @model_observer
@model_observer(models.Classroom)
async def classroom_change_handler(
self,
message,
observer=None,
action=None,
subscribing_request_ids=[],
**kwargs
):
# due to not being able to make DB QUERIES when selecting a group
# maybe do an extra check here to be sure the user has permission
# send activity to your frontend
for request_id in subscribing_request_ids:
# we can send a separate message for each subscribing request
# this lets ws clients rout these messages.
await self.send_json(dict(body=message, action=action, request_id=request_id))
# note if we do not pass `request_id` to the `subscribe` method
# then `subscribing_request_ids` will be and empty list.
@classroom_change_handler.groups_for_signal
def classroom_change_handler(self, instance: models.Classroom, **kwargs):
# this block of code is called very often *DO NOT make DB QUERIES HERE*
yield f'-school__{instance.school_id}'
yield f'-pk__{instance.pk}'
@classroom_change_handler.groups_for_consumer
def classroom_change_handler(self, school=None, classroom=None, **kwargs):
# This is called when you subscribe/unsubscribe
if school is not None:
yield f'-school__{school.pk}'
if classroom is not None:
yield f'-pk__{classroom.pk}'
@action()
async def subscribe_to_classrooms_in_school(self, school_pk, request_id, **kwargs):
# check user has permission to do this
await self.classroom_change_handler.subscribe(school=school, request_id=request_id)
@action()
async def subscribe_to_classroom(self, classroom_pk, request_id, **kwargs):
# check user has permission to do this
await self.classroom_change_handler.subscribe(classroom=classroom, request_id=request_id)
.. _ReadTheDocs: https://djangochannelsrestframework.readthedocs.io/en/latest/ .. _post: https://lostmoa.com/blog/DjangoChannelsRestFramework/ .. _GenericAPIView: https://www.django-rest-framework.org/api-guide/generic-views/ .. _channels-v4: https://channels.readthedocs.io/en/latest/ .. _dcrf-client: https://github.com/theY4Kman/dcrf-client .. _theY4Kman: https://github.com/theY4Kman .. _HyperMediaChannels: https://github.com/hishnash/hypermediachannels .. _ChannelsMultiplexer: https://github.com/hishnash/channelsmultiplexer
FAQs
RESTful API for WebSockets using django channels.
We found that djangochannelsrestframework demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.