Django channel not getting message - django

I am using django channel in my current project. From one of my django app, I am sending notification to the channel layer, so that websocket can broadcast the message. But problem is
consumer is not getting my message.
Utils in django app for sending notification to channel:
from asgiref.sync import AsyncToSync
from channels.layers import get_channel_layer
import json
def async_send(group_name, text):
channel_layer = get_channel_layer()
AsyncToSync(channel_layer.group_send)(
group_name,
{
'type': 'notify',
'text': json.dumps(text)
}
)
My consumer file is:
from channels.generic.websocket import AsyncWebsocketConsumer
class InformationConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.channel_layer.group_add(str(self.scope['user']), self.channel_name)
await self.accept()
async def notify(self, event):
await self.send(
{
"message": event['text'],
},
)
print(event['text'])
I am supposed to get the output of event['text'], but getting nothing :(

change from
self.channel_layer.group_add(str(self.scope['user']), self.channel_name)
to
await self.channel_layer.group_add(str(self.scope['user']), self.channel_name)

Related

Best practices for authenticating Django Channels

Django 4.1.4
djoser 2.1.0
channels 4.0.0
I have followed the documented recommendation for creating custom middleware to authenticate a user when using channels and I am successfully getting the user
and checking that the user is authenticated though I am sending the user ID in the querystring when connecting to the websocket to do this. The user is not automatically available in the websocket scope.
I am unsure if there are any potential security risks as the documentation mentions that their recommendation is insecure, I do check that the user.is_authenticated. So I believe I have secured it.
I do believe that using the token created by djoser would be better though I am not sure how to send headers with the websocket request unless I include the token in the querystring instead of the user's ID.
I am keen to hear what the best practices are.
I am passing the user ID to the websocket via querystring as follows at the frontend:
websocket.value = new WebSocket(`ws://127.0.0.1:8000/ws/marketwatch/? ${authStore.userId}`)
middleware.py
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
#database_sync_to_async
def get_user(user_id):
User = get_user_model()
try:
user = User.objects.get(id=user_id)
except ObjectDoesNotExist:
return AnonymousUser()
else:
if user.is_authenticated:
return user
else:
return AnonymousUser()
class QueryAuthMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
scope['user'] = await get_user(int(scope["query_string"].decode()))
return await self.app(scope, receive, send)
consumers.py
import os
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from channels.security.websocket import AllowedHostsOriginValidator
from api.middleware import QueryAuthMiddleware
from .routing import ws_urlpatterns
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api.settings')
application = ProtocolTypeRouter({
'http':get_asgi_application(),
'websocket': AllowedHostsOriginValidator(
QueryAuthMiddleware(
URLRouter(ws_urlpatterns)
)
)
})
After doing some extensive research I decided not to pass the id or the token via the querystring as this poses a risk due to this data being stored in the server logs.
IMO the best option with the least amount of risk was passing the token as a message to the websocket after the connection was established and then verifying the token; closing the websocket if invalid.
This meant not requiring the middleware previously implemented. In this particular project no other messages would be received from the client so I don't need to do any checking on the key of the message received. This could be changed for chat apps and other apps that will receive further messages from the client.
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
import json
from rest_framework.authtoken.models import Token
class MarketWatchConsumer(AsyncWebsocketConsumer):
#database_sync_to_async
def verify_token(self, token_dict):
try:
token = Token.objects.get(key=token_dict['token'])
except Token.DoesNotExist:
return False
else:
if token.user.is_active:
return True
else:
return False
async def connect(self):
await self.channel_layer.group_add('group', self.channel_name)
await self.accept()
async def receive(self, text_data=None, bytes_data=None):
valid_token = await self.verify_token(json.loads(text_data))
if not valid_token:
await self.close()
async def disconnect(self, code):
await self.channel_layer.group_discard('group', self.channel_name)

Both logging and print statements inside django channels consumer are not working

I'm trying to integrate celery with django channels but channels consumer is not working as it supposed. Logging or prints inside consumer functions are not displaying message in terminal.
The following celery task is called from signals.py
tasks.py
from channels.layers import get_channel_layer
#app.task
def send_request_notification():
text = 'You have new cleaning request'
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
'companies_received_request', # group name
{
'type': 'send_notification',
'text': text
}
)
print("SENT ---") # can see result of this print
consumers.py
class NotificationConsumer(AsyncWebsocketConsumer):
async def connect(self):
logger.info("Connected to websocket")
await self.channel_layer.group_add(
'companies_received_request', self.channel_name
)
await self.accept()
async def disconnect(self):
await self.channel_layer.group_discard(
'companies_received_request', self.channel_name
)
logger.info("Disconnected from websocket")
async def send_notification(self, event):
logger.info("Here in sending notification")
text_message = event['text']
await self.send(text_message)
print("EVENT.TEXT")
print(text_message)
settings.py
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [('127.0.0.1', 6379)],
},
},
}
I am supposed to get the output of prints and logging of consumer functions in terminal but I'm getting nothing when I call channel_layer.group_send method from celery task. (It seems that celery couldn't make a connection to consumer)

Django Channels after sending message, current tab showing 2 messages(sender + receiver) but other tab does not show anything?

I am following this tutorial Channels Tutorial Link
My Goal was to make a simple asgi chat server. But It is showing weird behaviour. The message sent from one tab..should print "HI" in current tab..and also "HI" in the tab connected in the same room. but its printing the both "HI" in the current tab, no message is shown in the other tab connected in the same room.
my consumers.py is simeple, just from the tutorials file...
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name
# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
# Send message to room group
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
# Receive message from room group
def chat_message(self, event):
message = event['message']
# Send message to WebSocket
self.send(text_data=json.dumps({
'message': message
}))
my settings file is configured to receive redis connection in 127.0.0.1. I am using docker redis image just s the tutorial said.
ASGI_APPLICATION = 'mysite.asgi.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
my asgi.py file...configured like the tutorial said-->
# mysite/asgi.py
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import chat.routing
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
This is the issue of the latest release.
I solved it by reinstalling the package: channels == 2.4.0
Then I changed the file asgi.py, commented out the line there: "http": get_asgi_application(),
also I removed as_asgi() from file routing.py
Here is the tutorial of the previous version.

Custom Django Channels middleware not finishing processing before Websocket connects

I have an existing WSGI application which I'm adding Django Channels to to give websocket functionality. I created a consumer using WebsocketConsumer, added the custom middleware into the routing file, and implemented a basic version of pulling the token from the incoming connection request. I can successfully print the token that's in the database, so I know the correct information is passing.
I can connect to the socket, but it always comes back as being an anonymous user within the scope. It seems that the get_user_from_token function is not getting a chance to execute before the connect function executes, because all of the prints within the __call__ function of the TokenAuthMiddleware class are printed and none of the prints from the get_user_from_Token are printing. I tried switching the consumer to an async consumer, but that opened up a whole other set of problems that I couldn't figure out. I tried putting async in front of the __call__ and await in front of the function call, but that didn't work either. The current error I'm getting is:
Exception inside application: 'coroutine' object has no attribute '_wrapped'
File "C:\Users\PC\Envs\p3\lib\site-packages\channels\sessions.py", line 183, in __call__
return await self.inner(receive, self.send)
File "C:\Users\PC\Envs\p3\lib\site-packages\channels\middleware.py", line 40, in coroutine_
call
await self.resolve_scope(scope)
File "C:\Users\PC\Envs\p3\lib\site-packages\channels\auth.py", line 166, in resolve_scope
scope["user"]._wrapped = await get_user(scope)
'coroutine' object has no attribute '_wrapped'
How do I get my middleware to finish what it's doing before connect tries to test the user?
my_app/routing.py
from channels.routing import ProtocolTypeRouter, URLRouter
import api.channels.routing
from my_app.ws_token_auth import TokenAuthMiddlewareStack
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': TokenAuthMiddlewareStack(
URLRouter(
api.channels.routing.websocket_urlpatterns
)
),
})
api/channels/consumers.py
import json
from asgiref.sync import async_to_sync
from channels.db import database_sync_to_async
from channels.generic.websocket import WebsocketConsumer
class HeaderConsumer(WebsocketConsumer):
def connect(self):
if self.scope["user"].is_anonymous:
# Reject the connection
print('rejected')
self.close()
else:
self.accept()
self.user = self.scope['user']
self.message_threads = set()
def disconnect(self, code):
"""
Called when the WebSocket closes for any reason.
"""
# Leave all the rooms we are still in
for thread_id in list(self.message_threads):
try:
self.leave_thread(thread_id)
except ClientError:
pass
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
self.send(text_data=json.dumps({
'message': message + message
}))
my_app/ws_token_auth.py
from channels.auth import AuthMiddlewareStack
from channels.db import database_sync_to_async
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
#database_sync_to_async
def close_connections():
close_old_connections()
#database_sync_to_async
def get_user_from_token(t):
try:
print("trying token" + t)
token = Token.objects.get(token=t).prefetch_related('user')
return token.user
except Token.DoesNotExist:
print("failed")
return AnonymousUser()
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
close_connections()
print("hi")
headers = dict(scope['headers'])
if b'cookie' in headers:
pieces = headers[b'cookie'].decode().split("; ")
key_values = {i.split('=', 1)[0]: i.split('=', 1)[1] for i in pieces}
print("x")
if 'token' in key_values:
try:
scope['token'] = key_values['token']
print("y")
user = get_user_from_token(key_values['token'])
print("z")
except Token.DoesNotExist:
print("no token")
user = AnonymousUser()
else:
print("no token?")
else:
print("no cookie")
return self.inner(dict(scope, user=user))
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
Change you class HeaderConsumer(WebsocketConsumer): with
class HeaderConsumer(AsyncWebsocketConsumer):
And also check if your websocket_urlpatterns:
websocket_urlpatterns = [
re_path(r'your path', consumers.HeaderConsumer.as_asgi()),
]

Interactions between HTTP and websocket connections in Django Channels

I have a frontend consumer that handles HTTP requests (webhooks)
from django.views.decorators.csrf import csrf_exempt
from channels.generic.http import AsyncHttpConsumer
from django.http import JsonResponse
import json
import myapp.models
import redis
#csrf_exempt
class frontEndConsumer(AsyncHttpConsumer):
async def http_request(self, request):
await self.channel_layer.group_add("link", self.channel_name)
await self.channel_layer.group_send("link",
{
"type": "websocket.reg",
"where": "webhook",
"message": "test message",
})
#channel layer functions
async def webhook_reg(self, event):
print("WH: message from " + event["where"] + ": " + event["message"]
# make response
fulfillmentText = "device connected"
fulfillmentText = {'fulfillmentText': fulfillmentText}
fulfillmentText = json.dumps(fulfillmentText).encode('utf-8')
await self.send_response(200, fulfillmentText,
headers=[(b"Content-Type", b"text/plain"),])
and I have a backend consumer that handles websocket connections.
from channels.generic.websocket import AsyncJsonWebsocketConsumer
import redis
import myapp.models
account = redis.Redis(db=0)
class backEndConsumer(AsyncJsonWebsocketConsumer):
async def websocket_connect(self, type):
await self.channel_layer.group_add("link", self.channel_name)
print("channel name: " + self.channel_name)
await self.accept()
#channel layer functions
async def websocket_reg(self, event):
print("WS: message from " + event["where"] + ": " + event["message"])
await self.channel_layer.group_send("link",
{
"type": "webhook.reg",
"where": "websocket",
"message": "msg from websocket",
})
await self.send(event["message"])
I start them in procfile with:
web: daphne APbackend.asgi:application --port $PORT --bind 0.0.0.0
I am using django channels to have the front and back talk to each other. In my code, I'm trying to get both sides to send messages to each other.
I would establish websocket connection
have webhook send a http request to my front end.
front end would send a message to the backend (via channel messaging)
backend would send a message to the frontend.
front end message would finish the http request.
But this doesn't work, step 4, with the message going back to the front end saying the handler is missing. Why is that?
You are using the same group to talk in both directions this means when you send webhook.reg from the backend it will end up trying to call the function webhook_reg on each backEndConsumer instance but there is no such method there.
You should create 2 groups.
backend_to_frontend
frontend_to_backend
the frontend should group_add the backend_to_frontend
the backend should group_add the frontend_to_backend
&
in the backend you should group_send to the backend_to_frontend
in the frontend you should group_send to the frontend_to_backend
I was able to get this to work using either two group names or two channel names. Also knowing http_reponse would end the http_request context helped me debug why I wasn't able to get message back to dialogflow.
Thanks!