From f4fc2d658098e2e4772eaf442b6a1b71337c45a6 Mon Sep 17 00:00:00 2001 From: salah ahmed Date: Sun, 25 May 2025 21:49:59 -0400 Subject: [PATCH 01/12] utility methods for validating data --- pusher/crypto.py | 10 ++-------- pusher/util.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/pusher/crypto.py b/pusher/crypto.py index 0f24428..d271abd 100644 --- a/pusher/crypto.py +++ b/pusher/crypto.py @@ -16,7 +16,8 @@ ensure_text, ensure_binary, data_to_string, - is_base64) + is_base64, + is_encrypted_channel) import nacl.secret import nacl.utils @@ -24,13 +25,6 @@ # The prefix any e2e channel must have ENCRYPTED_PREFIX = 'private-encrypted-' -def is_encrypted_channel(channel): - """ - is_encrypted_channel() checks if the channel is encrypted by verifying the prefix - """ - if channel.startswith(ENCRYPTED_PREFIX): - return True - return False def parse_master_key(encryption_master_key, encryption_master_key_base64): """ diff --git a/pusher/util.py b/pusher/util.py index da4b6cb..d95c875 100644 --- a/pusher/util.py +++ b/pusher/util.py @@ -30,6 +30,14 @@ byte_type = 'a python3 bytes' +def is_encrypted_channel(channel): + """ + is_encrypted_channel() checks if the channel is encrypted by verifying the prefix + """ + if channel.startswith(ENCRYPTED_PREFIX): + return True + return False + def ensure_text(obj, name): if isinstance(obj, six.text_type): return obj @@ -100,6 +108,39 @@ def validate_channel(channel): return channel +def validate_channels(channels): + if len(channels) > 100: + raise ValueError("Too many channels") + + channels = [validate_channel(ch) for ch in channels] + + if len(channels) > 1: + for chan in channels: + if is_encrypted_channel(chan): + raise ValueError("You cannot trigger to multiple channels when using encrypted channels") + return channels + + +def validate_event_name(self, event_name): + """Ensure data is within limits + + https://pusher.com/docs/channels/server_api/http-api/#publishing-events + """ + if len(event_name) > 200: + raise ValueError("event_name too long") + return event_name + + +def validate_data(self, data): + """Ensure data is within limits + + https://pusher.com/docs/channels/server_api/http-api/#publishing-events + """ + + if len(data) > 10240: + raise ValueError("Too much data") + return data + def validate_socket_id(socket_id): socket_id = ensure_text(socket_id, "socket_id") From ef956d93239e78fa3e4af385c034cbbfecc2a2a4 Mon Sep 17 00:00:00 2001 From: salah ahmed Date: Sun, 25 May 2025 21:50:21 -0400 Subject: [PATCH 02/12] validate len of event names/data from central function --- pusher/pusher_client.py | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/pusher/pusher_client.py b/pusher/pusher_client.py index 8c35fd2..8657a8d 100644 --- a/pusher/pusher_client.py +++ b/pusher/pusher_client.py @@ -23,7 +23,10 @@ from pusher.util import ( ensure_text, + is_encrypted_channel, validate_channel, + validate_data, + validate_event_name, validate_socket_id, validate_user_id, join_attributes, @@ -83,23 +86,9 @@ def trigger(self, channels, event_name, data, socket_id=None): channels, (collections.Sized, collections.Iterable)): raise TypeError("Expected a single or a list of channels") - if len(channels) > 100: - raise ValueError("Too many channels") - - event_name = ensure_text(event_name, "event_name") - if len(event_name) > 200: - raise ValueError("event_name too long") - - data = data_to_string(data, self._json_encoder) - if sys.getsizeof(data) > 30720: - raise ValueError("Too much data") - - channels = list(map(validate_channel, channels)) - - if len(channels) > 1: - for chan in channels: - if is_encrypted_channel(chan): - raise ValueError("You cannot trigger to multiple channels when using encrypted channels") + channels = validate_channels(channels) + event_name = validate_event_name(ensure_text(event_name, 'event_name')) + data = validate_data(data_to_string(data, self._json_encoder)) if is_encrypted_channel(channels[0]): data = json.dumps(encrypt(channels[0], data, self._encryption_master_key), ensure_ascii=False) @@ -124,14 +113,8 @@ def trigger_batch(self, batch=[], already_encoded=False): for event in batch: validate_channel(event['channel']) - event_name = ensure_text(event['name'], "event_name") - if len(event['name']) > 200: - raise ValueError("event_name too long") - - event['data'] = data_to_string(event['data'], self._json_encoder) - - if sys.getsizeof(event['data']) > 10240: - raise ValueError("Too much data") + event['name'] = validate_event_name(ensure_text(event['name'], 'event_name')) + event['data'] = validate_data(data_to_string(event['data'], self._json_encoder)) if is_encrypted_channel(event['channel']): event['data'] = json.dumps(encrypt(event['channel'], event['data'], self._encryption_master_key), ensure_ascii=False) From ce85ce6f4eea2b4aac523d61edbf48a6765185c8 Mon Sep 17 00:00:00 2001 From: salah ahmed Date: Sun, 25 May 2025 22:10:52 -0400 Subject: [PATCH 03/12] formatting --- pusher/crypto.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pusher/crypto.py b/pusher/crypto.py index d271abd..f581066 100644 --- a/pusher/crypto.py +++ b/pusher/crypto.py @@ -9,21 +9,20 @@ import hashlib import nacl import base64 -import binascii import warnings from pusher.util import ( - ensure_text, ensure_binary, - data_to_string, is_base64, - is_encrypted_channel) + is_encrypted_channel as iec, + ENCRYPTED_PREFIX as EP) import nacl.secret import nacl.utils -# The prefix any e2e channel must have -ENCRYPTED_PREFIX = 'private-encrypted-' +# for backwards compatibility +ENCRYPTED_PREFIX = EP +is_encrypted_channel = iec def parse_master_key(encryption_master_key, encryption_master_key_base64): From 963229ca33abed60ff86ed65e13ea6054ec87fa7 Mon Sep 17 00:00:00 2001 From: salah ahmed Date: Sun, 25 May 2025 22:11:00 -0400 Subject: [PATCH 04/12] add missing import --- pusher/pusher_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pusher/pusher_client.py b/pusher/pusher_client.py index 8657a8d..0d9472a 100644 --- a/pusher/pusher_client.py +++ b/pusher/pusher_client.py @@ -25,6 +25,7 @@ ensure_text, is_encrypted_channel, validate_channel, + validate_channels, validate_data, validate_event_name, validate_socket_id, From ff22aeca6230d8bcaf87cd505f3fabe228d7e536 Mon Sep 17 00:00:00 2001 From: salah ahmed Date: Sun, 25 May 2025 22:12:01 -0400 Subject: [PATCH 05/12] add missing constant --- pusher/util.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pusher/util.py b/pusher/util.py index d95c875..7180c2f 100644 --- a/pusher/util.py +++ b/pusher/util.py @@ -11,6 +11,9 @@ import six import sys import base64 + +# The prefix any e2e channel must have +ENCRYPTED_PREFIX = "private-encrypted-" SERVER_TO_USER_PREFIX = "#server-to-user-" channel_name_re = re.compile(r'\A[-a-zA-Z0-9_=@,.;]+\Z') @@ -38,6 +41,7 @@ def is_encrypted_channel(channel): return True return False + def ensure_text(obj, name): if isinstance(obj, six.text_type): return obj From f39d00560361a37128f558564b9190762167ffee Mon Sep 17 00:00:00 2001 From: salah ahmed Date: Sun, 25 May 2025 22:49:17 -0400 Subject: [PATCH 06/12] add tests --- pusher/pusher_client.py | 6 +++--- pusher/util.py | 7 +++++-- pusher_tests/test_util.py | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/pusher/pusher_client.py b/pusher/pusher_client.py index 0d9472a..0219aac 100644 --- a/pusher/pusher_client.py +++ b/pusher/pusher_client.py @@ -88,8 +88,8 @@ def trigger(self, channels, event_name, data, socket_id=None): raise TypeError("Expected a single or a list of channels") channels = validate_channels(channels) - event_name = validate_event_name(ensure_text(event_name, 'event_name')) - data = validate_data(data_to_string(data, self._json_encoder)) + event_name = validate_event_name(event_name) + data = validate_data(data, self._json_encoder) if is_encrypted_channel(channels[0]): data = json.dumps(encrypt(channels[0], data, self._encryption_master_key), ensure_ascii=False) @@ -115,7 +115,7 @@ def trigger_batch(self, batch=[], already_encoded=False): validate_channel(event['channel']) event['name'] = validate_event_name(ensure_text(event['name'], 'event_name')) - event['data'] = validate_data(data_to_string(event['data'], self._json_encoder)) + event['data'] = validate_data(event['data'], self._json_encoder) if is_encrypted_channel(event['channel']): event['data'] = json.dumps(encrypt(event['channel'], event['data'], self._encryption_master_key), ensure_ascii=False) diff --git a/pusher/util.py b/pusher/util.py index 7180c2f..98d2f8c 100644 --- a/pusher/util.py +++ b/pusher/util.py @@ -112,6 +112,7 @@ def validate_channel(channel): return channel + def validate_channels(channels): if len(channels) > 100: raise ValueError("Too many channels") @@ -125,22 +126,24 @@ def validate_channels(channels): return channels -def validate_event_name(self, event_name): +def validate_event_name(event_name): """Ensure data is within limits https://pusher.com/docs/channels/server_api/http-api/#publishing-events """ + event_name = ensure_text(event_name, "event_name") if len(event_name) > 200: raise ValueError("event_name too long") return event_name -def validate_data(self, data): +def validate_data(data, json_encoder=None): """Ensure data is within limits https://pusher.com/docs/channels/server_api/http-api/#publishing-events """ + data = data_to_string(data, json_encoder) if len(data) > 10240: raise ValueError("Too much data") return data diff --git a/pusher_tests/test_util.py b/pusher_tests/test_util.py index 5125217..ec6f8cc 100644 --- a/pusher_tests/test_util.py +++ b/pusher_tests/test_util.py @@ -37,6 +37,23 @@ def test_validate_server_to_user_channel(self): pusher.util.validate_channel("#server-to-user1234") pusher.util.validate_channel("#server-to-users") + def test_validate_event_name(self): + valid_events = ["e" * 200, "123", "xyz", "xyz123", "xyz_123", "xyz-123", "Channel@123", "channel_xyz", "channel-xyz", "channel,456", "channel;asd", "-abc_ABC@012.xpto,987;654"] + invalid_events = ["e" * 201] + invalid_types = [123, None, {}] + + for event in valid_events: + self.assertEqual(event, pusher.util.validate_event_name(event)) + + for invalid_event in invalid_events: + with self.assertRaises(ValueError): + pusher.util.validate_event_name(invalid_event) + + for invalid_event in invalid_types: + with self.assertRaises(TypeError): + pusher.util.validate_event_name(invalid_event) + + if __name__ == '__main__': unittest.main() From 5783743bde11904f72ad8f4458e19f1204ae0cc8 Mon Sep 17 00:00:00 2001 From: salah ahmed Date: Sun, 25 May 2025 22:51:05 -0400 Subject: [PATCH 07/12] formatting --- pusher/pusher_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pusher/pusher_client.py b/pusher/pusher_client.py index 0219aac..0160a03 100644 --- a/pusher/pusher_client.py +++ b/pusher/pusher_client.py @@ -30,8 +30,7 @@ validate_event_name, validate_socket_id, validate_user_id, - join_attributes, - data_to_string) + join_attributes) from pusher.client import Client from pusher.http import GET, POST, Request, request_method From 83947edb7145edd2a68d8e8f90e6a19197330df3 Mon Sep 17 00:00:00 2001 From: salah ahmed Date: Sun, 25 May 2025 22:52:57 -0400 Subject: [PATCH 08/12] formatting --- pusher/util.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pusher/util.py b/pusher/util.py index 98d2f8c..9e5e903 100644 --- a/pusher/util.py +++ b/pusher/util.py @@ -127,10 +127,6 @@ def validate_channels(channels): def validate_event_name(event_name): - """Ensure data is within limits - - https://pusher.com/docs/channels/server_api/http-api/#publishing-events - """ event_name = ensure_text(event_name, "event_name") if len(event_name) > 200: raise ValueError("event_name too long") @@ -138,7 +134,7 @@ def validate_event_name(event_name): def validate_data(data, json_encoder=None): - """Ensure data is within limits + """Ensure data is within 10kB limit https://pusher.com/docs/channels/server_api/http-api/#publishing-events """ From fdaf604e1cd2d7ac913fc99779cd210aa170d2fd Mon Sep 17 00:00:00 2001 From: salah ahmed Date: Sun, 25 May 2025 23:06:51 -0400 Subject: [PATCH 09/12] add tests --- pusher/pusher_client.py | 12 ------------ pusher/util.py | 20 ++++++++++++++++---- pusher_tests/test_util.py | 27 +++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/pusher/pusher_client.py b/pusher/pusher_client.py index 0160a03..3540f5a 100644 --- a/pusher/pusher_client.py +++ b/pusher/pusher_client.py @@ -8,11 +8,6 @@ import sys -# Abstract Base Classes were moved into collections.abc in Python 3.3 -if sys.version_info >= (3, 3): - import collections.abc as collections -else: - import collections import hashlib import os import re @@ -79,13 +74,6 @@ def trigger(self, channels, event_name, data, socket_id=None): http://pusher.com/docs/rest_api#method-post-event """ - if isinstance(channels, six.string_types): - channels = [channels] - - if isinstance(channels, dict) or not isinstance( - channels, (collections.Sized, collections.Iterable)): - raise TypeError("Expected a single or a list of channels") - channels = validate_channels(channels) event_name = validate_event_name(event_name) data = validate_data(data, self._json_encoder) diff --git a/pusher/util.py b/pusher/util.py index 9e5e903..dd9970c 100644 --- a/pusher/util.py +++ b/pusher/util.py @@ -12,6 +12,13 @@ import sys import base64 +# Abstract Base Classes were moved into collections.abc in Python 3.3 +if sys.version_info >= (3, 3): + import collections.abc as collections +else: + import collections + + # The prefix any e2e channel must have ENCRYPTED_PREFIX = "private-encrypted-" SERVER_TO_USER_PREFIX = "#server-to-user-" @@ -114,15 +121,20 @@ def validate_channel(channel): def validate_channels(channels): + if isinstance(channels, six.string_types): + channels = [channels] + + if isinstance(channels, dict) or not isinstance( + channels, (collections.Sized, collections.Iterable)): + raise TypeError("Expected a single or a list of channels") + if len(channels) > 100: raise ValueError("Too many channels") channels = [validate_channel(ch) for ch in channels] - if len(channels) > 1: - for chan in channels: - if is_encrypted_channel(chan): - raise ValueError("You cannot trigger to multiple channels when using encrypted channels") + if len(channels) > 1 and any(is_encrypted_channel(chan) for chan in channels): + raise ValueError("You cannot trigger to multiple channels when using encrypted channels") return channels diff --git a/pusher_tests/test_util.py b/pusher_tests/test_util.py index ec6f8cc..9e4c6de 100644 --- a/pusher_tests/test_util.py +++ b/pusher_tests/test_util.py @@ -53,6 +53,33 @@ def test_validate_event_name(self): with self.assertRaises(TypeError): pusher.util.validate_event_name(invalid_event) + def test_validate_channels(self): + valid_channels = ["123", "xyz", "xyz123", "xyz_123", "xyz-123", "Channel@123", "channel_xyz", "channel-xyz", "channel,456", "channel;asd", "-abc_ABC@012.xpto,987;654"] + invalid_channels = ["#123", "x" * 201, "abc%&*", "#server-to-user1234", "#server-to-users"] + self.assertEqual(valid_channels, pusher.util.validate_channels(valid_channels)) + for invalid_channel in invalid_channels: + with self.assertRaises(ValueError): + pusher.util.validate_channels(valid_channels + [invalid_channel]) + + with self.assertRaises(ValueError): + pusher.util.validate_channels(["123"] * 101) + + invalid_types = [101, {"x": 1}] + for invalid_channel in invalid_types: + with self.assertRaises(TypeError): + pusher.util.validate_channels(valid_channels + [invalid_channel]) + + with self.assertRaises(ValueError): + pusher.util.validate_channels(["123", "private-encrypted-pippo"]) + + + def test_validate_data(self): + data_too_long = "1" * 10241 + with self.assertRaises(ValueError): + pusher.util.validate_data(data_too_long) + + valid_data = "1" * 10240 + self.assertEqual(valid_data, pusher.util.validate_data(valid_data)) if __name__ == '__main__': From b51b11698740020c3d05d7e785be840f7272e8b7 Mon Sep 17 00:00:00 2001 From: salah ahmed Date: Sun, 25 May 2025 23:09:22 -0400 Subject: [PATCH 10/12] validate_event_name ensures text --- pusher/pusher_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pusher/pusher_client.py b/pusher/pusher_client.py index 3540f5a..d195767 100644 --- a/pusher/pusher_client.py +++ b/pusher/pusher_client.py @@ -101,7 +101,7 @@ def trigger_batch(self, batch=[], already_encoded=False): for event in batch: validate_channel(event['channel']) - event['name'] = validate_event_name(ensure_text(event['name'], 'event_name')) + event['name'] = validate_event_name(event['name']) event['data'] = validate_data(event['data'], self._json_encoder) if is_encrypted_channel(event['channel']): From e4563127686552705e65de86d2e83a080616ce75 Mon Sep 17 00:00:00 2001 From: salah ahmed Date: Mon, 26 May 2025 05:42:18 -0400 Subject: [PATCH 11/12] rename --- pusher/util.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pusher/util.py b/pusher/util.py index dd9970c..30902c9 100644 --- a/pusher/util.py +++ b/pusher/util.py @@ -22,6 +22,9 @@ # The prefix any e2e channel must have ENCRYPTED_PREFIX = "private-encrypted-" SERVER_TO_USER_PREFIX = "#server-to-user-" +MAX_PAYLOAD_SIZE_BYTES = 10240 +MAX_CHANNELS = 100 +MAX_CHANNEL_NAME_SIZE = 200 channel_name_re = re.compile(r'\A[-a-zA-Z0-9_=@,.;]+\Z') server_to_user_channel_re = re.compile(rf'\A{SERVER_TO_USER_PREFIX}[-a-zA-Z0-9_=@,.;]+\Z') @@ -96,7 +99,7 @@ def validate_user_id(user_id): if length == 0: raise ValueError("User id is empty") - if length > 200: + if length > MAX_CHANNEL_NAME_SIZE: raise ValueError("User id too long: '{}'".format(user_id)) if not channel_name_re.match(user_id): @@ -108,7 +111,7 @@ def validate_user_id(user_id): def validate_channel(channel): channel = ensure_text(channel, "channel") - if len(channel) > 200: + if len(channel) > MAX_CHANNEL_NAME_SIZE: raise ValueError("Channel too long: %s" % channel) if channel.startswith(SERVER_TO_USER_PREFIX): @@ -128,7 +131,7 @@ def validate_channels(channels): channels, (collections.Sized, collections.Iterable)): raise TypeError("Expected a single or a list of channels") - if len(channels) > 100: + if len(channels) > MAX_CHANNELS: raise ValueError("Too many channels") channels = [validate_channel(ch) for ch in channels] @@ -140,7 +143,7 @@ def validate_channels(channels): def validate_event_name(event_name): event_name = ensure_text(event_name, "event_name") - if len(event_name) > 200: + if len(event_name) > MAX_CHANNEL_NAME_SIZE: raise ValueError("event_name too long") return event_name @@ -152,7 +155,7 @@ def validate_data(data, json_encoder=None): """ data = data_to_string(data, json_encoder) - if len(data) > 10240: + if len(data) > MAX_PAYLOAD_SIZE_BYTES: raise ValueError("Too much data") return data From 39f48cfd760a2b1a2e949b96d913d3a017ab089c Mon Sep 17 00:00:00 2001 From: salah ahmed Date: Mon, 26 May 2025 12:20:05 -0400 Subject: [PATCH 12/12] formatting --- pusher/util.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pusher/util.py b/pusher/util.py index 30902c9..23e4225 100644 --- a/pusher/util.py +++ b/pusher/util.py @@ -47,9 +47,7 @@ def is_encrypted_channel(channel): """ is_encrypted_channel() checks if the channel is encrypted by verifying the prefix """ - if channel.startswith(ENCRYPTED_PREFIX): - return True - return False + return channel.startswith(ENCRYPTED_PREFIX) def ensure_text(obj, name):