Repository: django/channels (python)

Issue:
get_handler_name allow call to private methods
Hi there !

## Environment:
- python3.8
- channels 2.4.0

## Bug:
Not sure if it's really a bug or an intended behaviour. But it seem that the function [get_handler_name](https://github.com/django/channels/blob/42070bfe400048d7c8efe551d37f7ac0d26d8e7e/channels/consumer.py#L12) behave quite strangely and allow the call of "private" methods in some corner cases.

Examples:

```python

channel_layer = get_channel_layer()

message = { "type": "_my_private_function", "data": "here some data" }
await channel_layer.group_send(response_data['id'], message=message)

# Will fail, raising ValueError("Malformed type in message (leading underscore)")
# from get_handler_name


message = { "type": ".my_private_function", "data": "here some data" }
await channel_layer.group_send(response_data['id'], message=message)

# Will work and call "_my_private_function" from consumer instance
```

## Expected Behavior:

Should either, allow to call `_private_methods` all the time (and not raise error in the first case).
Or never allow this kind of call and consider them invalid.

## Fix proposal:

The origin of the inconsistency come from the fact that we check the function name, before mutation and not after.

Into [this](https://github.com/avallete/channels/commit/544a1d5c17a051d59aad22be009d7f87b8d0ab50) commit, I perform the mutation before checking the result. Disallowing call to any function starting with "_".

If this issue is validated as a "bug", I will happily open a PR.

Regards,

Pull request:
fix(consumer): get_handler_name
Perform check after replace into get_handler_name to avoid class private members call.

Close #1430

Patch:
diff --git a/channels/consumer.py b/channels/consumer.py
index 704c038..33af10c 100644
--- a/channels/consumer.py
+++ b/channels/consumer.py
@@ -17,10 +17,11 @@ def get_handler_name(message):
     # Check message looks OK
     if "type" not in message:
         raise ValueError("Incoming message has no 'type' attribute")
-    if message["type"].startswith("_"):
-        raise ValueError("Malformed type in message (leading underscore)")
     # Extract type and replace . with _
-    return message["type"].replace(".", "_")
+    handler_name = message["type"].replace(".", "_")
+    if handler_name.startswith("_"):
+        raise ValueError("Malformed type in message (leading underscore)")
+    return handler_name
 
 
 class AsyncConsumer:


Test patch:
diff --git a/tests/test_generic_websocket.py b/tests/test_generic_websocket.py
index 552c8c7..5d8d568 100644
--- a/tests/test_generic_websocket.py
+++ b/tests/test_generic_websocket.py
@@ -289,3 +289,69 @@ async def test_async_json_websocket_consumer():
     await communicator.send_to(bytes_data=b"w\0\0\0")
     with pytest.raises(ValueError):
         await communicator.wait()
+
+
+@pytest.mark.asyncio
+async def test_block_underscored_type_function_call():
+    """
+    Test that consumer prevent calling private functions as handler
+    """
+
+    class TestConsumer(AsyncWebsocketConsumer):
+        channel_layer_alias = "testlayer"
+
+        async def _my_private_handler(self, _):
+            await self.send(text_data="should never be called")
+
+    channel_layers_setting = {
+        "testlayer": {"BACKEND": "channels.layers.InMemoryChannelLayer"}
+    }
+    with override_settings(CHANNEL_LAYERS=channel_layers_setting):
+        communicator = WebsocketCommunicator(TestConsumer, "/testws/")
+        await communicator.connect()
+
+        channel_layer = get_channel_layer("testlayer")
+        # Test that the specific channel layer is retrieved
+        assert channel_layer != None
+
+        channel_name = list(channel_layer.channels.keys())[0]
+        # Should block call to private functions handler and raise ValueError
+        message = {"type": "_my_private_handler", "text": "hello"}
+        await channel_layer.send(channel_name, message)
+        with pytest.raises(
+            ValueError, match=r"Malformed type in message \(leading underscore\)"
+        ):
+            await communicator.receive_from()
+
+
+@pytest.mark.asyncio
+async def test_block_leading_dot_type_function_call():
+    """
+    Test that consumer prevent calling private functions as handler
+    """
+
+    class TestConsumer(AsyncWebsocketConsumer):
+        channel_layer_alias = "testlayer"
+
+        async def _my_private_handler(self, _):
+            await self.send(text_data="should never be called")
+
+    channel_layers_setting = {
+        "testlayer": {"BACKEND": "channels.layers.InMemoryChannelLayer"}
+    }
+    with override_settings(CHANNEL_LAYERS=channel_layers_setting):
+        communicator = WebsocketCommunicator(TestConsumer, "/testws/")
+        await communicator.connect()
+
+        channel_layer = get_channel_layer("testlayer")
+        # Test that the specific channel layer is retrieved
+        assert channel_layer != None
+
+        channel_name = list(channel_layer.channels.keys())[0]
+        # Should not replace dot by underscore and call private function (see issue: #1430)
+        message = {"type": ".my_private_handler", "text": "hello"}
+        await channel_layer.send(channel_name, message)
+        with pytest.raises(
+            ValueError, match=r"Malformed type in message \(leading underscore\)"
+        ):
+            await communicator.receive_from()