diff --git a/.pubnub.yml b/.pubnub.yml index db226c6d..1dd9ab96 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,5 +1,5 @@ name: python -version: 7.3.1 +version: 7.3.2 schema: 1 scm: github.com/pubnub/python sdks: @@ -18,7 +18,7 @@ sdks: distributions: - distribution-type: library distribution-repository: package - package-name: pubnub-7.3.1 + package-name: pubnub-7.3.2 location: https://pypi.org/project/pubnub/ supported-platforms: supported-operating-systems: @@ -97,8 +97,8 @@ sdks: - distribution-type: library distribution-repository: git release - package-name: pubnub-7.3.1 - location: https://github.com/pubnub/python/releases/download/v7.3.1/pubnub-7.3.1.tar.gz + package-name: pubnub-7.3.2 + location: https://github.com/pubnub/python/releases/download/v7.3.2/pubnub-7.3.2.tar.gz supported-platforms: supported-operating-systems: Linux: @@ -169,6 +169,11 @@ sdks: license-url: https://github.com/aio-libs/aiohttp/blob/master/LICENSE.txt is-required: Required changelog: + - date: 2023-11-27 + version: v7.3.2 + changes: + - type: bug + text: "Gracefully handle decrypting an unencrypted method. If a decryption error occurs when trying to decrypt plain text, the plain text message will be returned and an error field will be set in the response. This works for both history and subscription messages." - date: 2023-10-30 version: v7.3.1 changes: diff --git a/CHANGELOG.md b/CHANGELOG.md index 539366c3..a702235d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v7.3.2 +November 27 2023 + +#### Fixed +- Gracefully handle decrypting an unencrypted method. If a decryption error occurs when trying to decrypt plain text, the plain text message will be returned and an error field will be set in the response. This works for both history and subscription messages. + ## v7.3.1 October 30 2023 diff --git a/pubnub/models/consumer/history.py b/pubnub/models/consumer/history.py index 0d64b5a6..cbd5a637 100644 --- a/pubnub/models/consumer/history.py +++ b/pubnub/models/consumer/history.py @@ -1,3 +1,6 @@ +import binascii + + class PNHistoryResult(object): def __init__(self, messages, start_timetoken, end_timetoken): self.messages = messages @@ -44,12 +47,16 @@ def __init__(self, entry, crypto, timetoken=None, meta=None): self.meta = meta self.entry = entry self.crypto = crypto + self.error = None def __str__(self): return "History item with tt: %s and content: %s" % (self.timetoken, self.entry) def decrypt(self, cipher_key): - self.entry = self.crypto.decrypt(cipher_key, self.entry) + try: + self.entry = self.crypto.decrypt(cipher_key, self.entry) + except binascii.Error as e: + self.error = e class PNFetchMessagesResult(object): diff --git a/pubnub/models/consumer/pubsub.py b/pubnub/models/consumer/pubsub.py index a44070af..bf8f2505 100644 --- a/pubnub/models/consumer/pubsub.py +++ b/pubnub/models/consumer/pubsub.py @@ -2,7 +2,7 @@ class PNMessageResult(object): - def __init__(self, message, subscription, channel, timetoken, user_metadata=None, publisher=None): + def __init__(self, message, subscription, channel, timetoken, user_metadata=None, publisher=None, error=None): if subscription is not None: assert isinstance(subscription, str) @@ -29,6 +29,7 @@ def __init__(self, message, subscription, channel, timetoken, user_metadata=None self.timetoken = timetoken self.user_metadata = user_metadata self.publisher = publisher + self.error = error class PNSignalMessageResult(PNMessageResult): diff --git a/pubnub/pubnub_core.py b/pubnub/pubnub_core.py index 26fed2e1..fc55059b 100644 --- a/pubnub/pubnub_core.py +++ b/pubnub/pubnub_core.py @@ -85,7 +85,7 @@ class PubNubCore: """A base class for PubNub Python API implementations""" - SDK_VERSION = "7.3.1" + SDK_VERSION = "7.3.2" SDK_NAME = "PubNub-Python" TIMESTAMP_DIVIDER = 1000 diff --git a/pubnub/workers.py b/pubnub/workers.py index 2eb2de6d..81eb5b78 100644 --- a/pubnub/workers.py +++ b/pubnub/workers.py @@ -52,22 +52,23 @@ def _get_url_for_file_event_message(self, channel, extracted_message): def _process_message(self, message_input): if self._pubnub.config.cipher_key is None: - return message_input + return message_input, None else: try: return self._pubnub.config.crypto.decrypt( self._pubnub.config.cipher_key, message_input - ) + ), None except Exception as exception: logger.warning("could not decrypt message: \"%s\", due to error %s" % (message_input, str(exception))) + pn_status = PNStatus() pn_status.category = PNStatusCategory.PNDecryptionErrorCategory pn_status.error_data = PNErrorData(str(exception), exception) pn_status.error = True pn_status.operation = PNOperationType.PNSubscribeOperation self._listener_manager.announce_status(pn_status) - return message_input + return message_input, exception def _process_incoming_payload(self, message): assert isinstance(message, SubscribeMessage) @@ -125,7 +126,7 @@ def _process_incoming_payload(self, message): ) self._listener_manager.announce_membership(membership_result) elif message.type == SubscribeMessageWorker.TYPE_FILE_MESSAGE: - extracted_message = self._process_message(message.payload) + extracted_message, _ = self._process_message(message.payload) download_url = self._get_url_for_file_event_message(channel, extracted_message) pn_file_result = PNFileMessageResult( @@ -142,7 +143,7 @@ def _process_incoming_payload(self, message): self._listener_manager.announce_file_message(pn_file_result) else: - extracted_message = self._process_message(message.payload) + extracted_message, error = self._process_message(message.payload) publisher = message.issuing_client_id if extracted_message is None: @@ -172,6 +173,7 @@ def _process_incoming_payload(self, message): channel=channel, subscription=subscription_match, timetoken=publish_meta_data.publish_timetoken, - publisher=publisher + publisher=publisher, + error=error ) self._listener_manager.announce_message(pn_message_result) diff --git a/setup.py b/setup.py index 562cf3ff..cf20f2d2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='pubnub', - version='7.3.1', + version='7.3.2', description='PubNub Real-time push service in the cloud', author='PubNub', author_email='support@pubnub.com', diff --git a/tests/integrational/fixtures/native_sync/history/unencrypted.json b/tests/integrational/fixtures/native_sync/history/unencrypted.json new file mode 100644 index 00000000..8a99d295 --- /dev/null +++ b/tests/integrational/fixtures/native_sync/history/unencrypted.json @@ -0,0 +1,185 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "DELETE", + "uri": "https://ps.pndsn.com/v3/history/sub-key/{PN_KEY_SUBSCRIBE}/channel/test_unencrypted", + "body": null, + "headers": { + "User-Agent": [ + "PubNub-Python/7.3.0" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "0" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Age": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "Content-Type": [ + "text/javascript; charset=\"UTF-8\"" + ], + "Content-Length": [ + "52" + ], + "Cache-Control": [ + "no-cache" + ], + "Server": [ + "Pubnub Storage" + ], + "Date": [ + "Wed, 22 Nov 2023 15:33:23 GMT" + ], + "Access-Control-Allow-Origin": [ + "*" + ], + "Access-Control-Allow-Methods": [ + "GET, POST, DELETE, OPTIONS" + ], + "Accept-Ranges": [ + "bytes" + ] + }, + "body": { + "string": "{\"status\": 200, \"error\": false, \"error_message\": \"\"}" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://ps.pndsn.com/publish/{PN_KEY_PUBLISH}/{PN_KEY_SUBSCRIBE}/0/test_unencrypted/0/%22Lorem%20Ipsum%22?seqn=1", + "body": null, + "headers": { + "User-Agent": [ + "PubNub-Python/7.3.0" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Connection": [ + "keep-alive" + ], + "Content-Type": [ + "text/javascript; charset=\"UTF-8\"" + ], + "Content-Length": [ + "30" + ], + "Cache-Control": [ + "no-cache" + ], + "Date": [ + "Wed, 22 Nov 2023 15:33:23 GMT" + ], + "Access-Control-Allow-Origin": [ + "*" + ], + "Access-Control-Allow-Methods": [ + "GET" + ] + }, + "body": { + "string": "[1,\"Sent\",\"17006672033304156\"]" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://ps.pndsn.com/v2/history/sub-key/{PN_KEY_SUBSCRIBE}/channel/test_unencrypted?count=100", + "body": null, + "headers": { + "User-Agent": [ + "PubNub-Python/7.3.0" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "Accept": [ + "*/*" + ], + "Connection": [ + "keep-alive" + ] + } + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "Age": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "Content-Type": [ + "text/javascript; charset=\"UTF-8\"" + ], + "Content-Length": [ + "53" + ], + "Cache-Control": [ + "no-cache" + ], + "Server": [ + "Pubnub Storage" + ], + "Date": [ + "Wed, 22 Nov 2023 15:33:25 GMT" + ], + "Access-Control-Allow-Origin": [ + "*" + ], + "Access-Control-Allow-Methods": [ + "GET, POST, DELETE, OPTIONS" + ], + "Accept-Ranges": [ + "bytes" + ] + }, + "body": { + "string": "[[\"Lorem Ipsum\"],17006672033304156,17006672033304156]" + } + } + } + ] +} diff --git a/tests/integrational/native_sync/test_history.py b/tests/integrational/native_sync/test_history.py index a19b26f6..335aaf85 100644 --- a/tests/integrational/native_sync/test_history.py +++ b/tests/integrational/native_sync/test_history.py @@ -1,3 +1,4 @@ +import binascii import logging import time import unittest @@ -9,7 +10,7 @@ from pubnub.models.consumer.history import PNHistoryResult from pubnub.models.consumer.pubsub import PNPublishResult from pubnub.pubnub import PubNub -from tests.helper import pnconf_copy, pnconf_enc_copy, pnconf_pam_copy +from tests.helper import pnconf_copy, pnconf_enc_copy, pnconf_enc_env_copy, pnconf_env_copy, pnconf_pam_copy from tests.integrational.vcr_helper import use_cassette_and_stub_time_sleep_native pubnub.set_stream_logger('pubnub', logging.DEBUG) @@ -104,3 +105,30 @@ def test_super_call_with_all_params(self): assert isinstance(envelope.result, PNHistoryResult) assert not envelope.status.is_error() + + +class TestHistoryCrypto(unittest.TestCase): + @use_cassette_and_stub_time_sleep_native('tests/integrational/fixtures/native_sync/history/unencrypted.json', + serializer='pn_json', filter_query_parameters=['uuid', 'pnsdk']) + def test_unencrypted(self): + ch = "test_unencrypted" + pubnub = PubNub(pnconf_env_copy()) + pubnub.config.uuid = "history-native-sync-uuid" + pubnub.delete_messages().channel(ch).sync() + envelope = pubnub.publish().channel(ch).message("Lorem Ipsum").sync() + assert isinstance(envelope.result, PNPublishResult) + assert envelope.result.timetoken > 0 + + time.sleep(2) + + pubnub_enc = PubNub(pnconf_enc_env_copy()) + pubnub_enc.config.uuid = "history-native-sync-uuid" + envelope = pubnub_enc.history().channel(ch).sync() + + assert isinstance(envelope.result, PNHistoryResult) + assert envelope.result.start_timetoken > 0 + assert envelope.result.end_timetoken > 0 + assert len(envelope.result.messages) == 1 + + assert envelope.result.messages[0].entry == 'Lorem Ipsum' + assert isinstance(envelope.result.messages[0].error, binascii.Error) diff --git a/tests/integrational/native_threads/test_subscribe.py b/tests/integrational/native_threads/test_subscribe.py index 59da1f86..1da89273 100644 --- a/tests/integrational/native_threads/test_subscribe.py +++ b/tests/integrational/native_threads/test_subscribe.py @@ -1,3 +1,4 @@ +import binascii import logging import unittest import time @@ -8,7 +9,7 @@ from pubnub.models.consumer.pubsub import PNPublishResult, PNMessageResult from pubnub.pubnub import PubNub, SubscribeListener, NonSubscribeListener from tests import helper -from tests.helper import pnconf_sub_copy +from tests.helper import pnconf_enc_env_copy, pnconf_env_copy, pnconf_sub_copy from tests.integrational.vcr_helper import pn_vcr @@ -253,3 +254,49 @@ def test_subscribe_cg_join_leave(self): pubnub.stop() pubnub_listener.stop() + + def test_subscribe_pub_unencrypted_unsubscribe(self): + ch = helper.gen_channel("test-subscribe-sub-pub-unsub") + + config_plain = pnconf_env_copy() + config_plain.enable_subscribe = True + pubnub_plain = PubNub(config_plain) + + config = pnconf_enc_env_copy() + config.enable_subscribe = True + pubnub = PubNub(config) + + subscribe_listener = SubscribeListener() + publish_operation = NonSubscribeListener() + message = "hey" + + try: + pubnub.add_listener(subscribe_listener) + + pubnub.subscribe().channels(ch).execute() + subscribe_listener.wait_for_connect() + + pubnub_plain.publish().channel(ch).message(message).pn_async(publish_operation.callback) + + if publish_operation.pn_await() is False: + self.fail("Publish operation timeout") + + publish_result = publish_operation.result + assert isinstance(publish_result, PNPublishResult) + assert publish_result.timetoken > 0 + + result = subscribe_listener.wait_for_message_on(ch) + assert isinstance(result, PNMessageResult) + assert result.channel == ch + assert result.subscription is None + assert result.timetoken > 0 + assert result.message == message + assert result.error is not None + assert isinstance(result.error, binascii.Error) + + pubnub.unsubscribe().channels(ch).execute() + subscribe_listener.wait_for_disconnect() + except PubNubException as e: + self.fail(e) + finally: + pubnub.stop()