From 6a8a62af2671b9cd43a7ce8b5df3ef387db7ccfc Mon Sep 17 00:00:00 2001 From: Francois Gervais Date: Tue, 8 Dec 2020 22:28:11 -0500 Subject: [PATCH 1/7] Set _msg_id default value back to 0 This change got introduced in the python/mp split commit: 81dfc160a94c02ef03737cf00e426f69298dd148 Having it default to 1 is fine on the clear text port but doesn't authenticate when connecting to the server through SSL. The server will ack the token packet but won't reply anything. Setting it back to 0 by default which mirrors what blynklib.py has. --- blynklib_mp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blynklib_mp.py b/blynklib_mp.py index 6d30af3..d0d02d5 100644 --- a/blynklib_mp.py +++ b/blynklib_mp.py @@ -56,7 +56,7 @@ class Protocol(object): STATUS_OK = const(200) VPIN_MAX_NUM = const(32) - _msg_id = 1 + _msg_id = 0 def _get_msg_id(self, **kwargs): if 'msg_id' in kwargs: From c124e2210b0f8bd4134f731c8b6478337e1a3328 Mon Sep 17 00:00:00 2001 From: Francois Gervais Date: Wed, 9 Dec 2020 12:12:54 -0500 Subject: [PATCH 2/7] Add basic SSL support This is a partial support as the CA certificate is not validated. This is so as this functionality is not implemented in all micropython ports. For example, it isn't currently supported in the esp32 port although a draft of the feature has been published: https://github.com/micropython/micropython/pull/5998 As the CA functionality is not there, we cannot use this parameter do decide when to switch to SSL mode as done in the CPython version. Instead we enable SSL when connecting to port 443 or 8443 which are well known ports used for TLS communications. One more thing is that this library relies on short reads to get data from the socket as it always ask for the max buffer length when reading. In micropython, the interface provided for SSL socket is only the one of a stream which doesn't allow short reads. To go around this we change the socket to non blocking which which has the side-effect of allowing short reads. However we then need to do manual polling and timeout on the socket which we do here. --- blynklib_mp.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/blynklib_mp.py b/blynklib_mp.py index d0d02d5..df133dc 100644 --- a/blynklib_mp.py +++ b/blynklib_mp.py @@ -5,6 +5,7 @@ __version__ = '0.2.6' import usocket as socket +import ussl as ssl import utime as time import ustruct as struct import uselect as select @@ -125,6 +126,7 @@ def internal_msg(self, *args): class Connection(Protocol): SOCK_MAX_TIMEOUT = const(5) SOCK_TIMEOUT = 0.05 + SOCK_SSL_TIMEOUT = const(1) EAGAIN = const(11) ETIMEDOUT = const(60) RETRIES_TX_DELAY = const(2) @@ -164,7 +166,11 @@ def send(self, data): try: retries -= 1 self._last_send_time = ticks_ms() - return self._socket.send(data) + try: + bytes_written = self._socket.send(data) + except AttributeError: + bytes_written = self._socket.write(data) + return bytes_written except (IOError, OSError): sleep_ms(self.RETRIES_TX_DELAY) @@ -172,7 +178,16 @@ def receive(self, length, timeout): d_buff = b'' try: self._set_socket_timeout(timeout) - d_buff += self._socket.recv(length) + try: + d_buff += self._socket.recv(length) + except AttributeError: + timeout = self.SOCK_SSL_TIMEOUT + while not d_buff and timeout > 0: + ret = self._socket.read(length) + if ret: + d_buff += ret + timeout -= self.SOCK_TIMEOUT + time.sleep(self.SOCK_TIMEOUT) if len(d_buff) >= length: d_buff = d_buff[:length] return d_buff @@ -203,6 +218,13 @@ def _get_socket(self): self._socket = socket.socket() self._socket.connect(socket.getaddrinfo(self.server, self.port)[0][-1]) self._set_socket_timeout(self.SOCK_TIMEOUT) + if self.port == 443 or self.port == 8443: + self.log('Using SSL socket...') + self._socket = ssl.wrap_socket(self._socket) + # Short reads are not supported in ssl mode. We work around + # this by setting the socket non blocking and doing manual + # polling/timeout. + self._socket.setblocking(False) self.log('Connected to server') except Exception as g_exc: raise BlynkError('Server connection failed: {}'.format(g_exc)) From 1883d374010a6713a840275963155806b596c4bc Mon Sep 17 00:00:00 2001 From: Francois Gervais Date: Tue, 15 Dec 2020 10:35:12 -0500 Subject: [PATCH 3/7] Revert "Add basic SSL support" This reverts commit c124e2210b0f8bd4134f731c8b6478337e1a3328. --- blynklib_mp.py | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/blynklib_mp.py b/blynklib_mp.py index df133dc..d0d02d5 100644 --- a/blynklib_mp.py +++ b/blynklib_mp.py @@ -5,7 +5,6 @@ __version__ = '0.2.6' import usocket as socket -import ussl as ssl import utime as time import ustruct as struct import uselect as select @@ -126,7 +125,6 @@ def internal_msg(self, *args): class Connection(Protocol): SOCK_MAX_TIMEOUT = const(5) SOCK_TIMEOUT = 0.05 - SOCK_SSL_TIMEOUT = const(1) EAGAIN = const(11) ETIMEDOUT = const(60) RETRIES_TX_DELAY = const(2) @@ -166,11 +164,7 @@ def send(self, data): try: retries -= 1 self._last_send_time = ticks_ms() - try: - bytes_written = self._socket.send(data) - except AttributeError: - bytes_written = self._socket.write(data) - return bytes_written + return self._socket.send(data) except (IOError, OSError): sleep_ms(self.RETRIES_TX_DELAY) @@ -178,16 +172,7 @@ def receive(self, length, timeout): d_buff = b'' try: self._set_socket_timeout(timeout) - try: - d_buff += self._socket.recv(length) - except AttributeError: - timeout = self.SOCK_SSL_TIMEOUT - while not d_buff and timeout > 0: - ret = self._socket.read(length) - if ret: - d_buff += ret - timeout -= self.SOCK_TIMEOUT - time.sleep(self.SOCK_TIMEOUT) + d_buff += self._socket.recv(length) if len(d_buff) >= length: d_buff = d_buff[:length] return d_buff @@ -218,13 +203,6 @@ def _get_socket(self): self._socket = socket.socket() self._socket.connect(socket.getaddrinfo(self.server, self.port)[0][-1]) self._set_socket_timeout(self.SOCK_TIMEOUT) - if self.port == 443 or self.port == 8443: - self.log('Using SSL socket...') - self._socket = ssl.wrap_socket(self._socket) - # Short reads are not supported in ssl mode. We work around - # this by setting the socket non blocking and doing manual - # polling/timeout. - self._socket.setblocking(False) self.log('Connected to server') except Exception as g_exc: raise BlynkError('Server connection failed: {}'.format(g_exc)) From e51df7a07b269d530738e2a9205947501a6e8ffa Mon Sep 17 00:00:00 2001 From: Francois Gervais Date: Tue, 15 Dec 2020 12:42:43 -0500 Subject: [PATCH 4/7] Change Blynk-Connection link from inheritance to aggregation Now instead of Blynk _is_ a Connection, Blynk _uses_ a Connection. This allows for Blynk to use different types of Connection. --- blynklib_mp.py | 94 ++++++++++++++++++++++++++------------------------ 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/blynklib_mp.py b/blynklib_mp.py index d0d02d5..9380aa2 100644 --- a/blynklib_mp.py +++ b/blynklib_mp.py @@ -238,7 +238,7 @@ def connected(self): return True if self._state == self.AUTHENTICATED else False -class Blynk(Connection): +class Blynk: _CONNECT_TIMEOUT = const(30) # 30sec _VPIN_WILDCARD = '*' _VPIN_READ = 'read v' @@ -250,67 +250,71 @@ class Blynk(Connection): _VPIN_WRITE_ALL = '{}{}'.format(_VPIN_WRITE, _VPIN_WILDCARD) _events = {} - def __init__(self, token, **kwargs): - Connection.__init__(self, token, **kwargs) - self._start_time = ticks_ms() - self._last_rcv_time = ticks_ms() - self._last_send_time = ticks_ms() - self._last_ping_time = ticks_ms() - self._state = self.DISCONNECTED + def __init__(self, token, connection=None, **kwargs): + if connection: + self.connection = connection + else: + self.connection = Connection(token, **kwargs) + + self.connection._start_time = ticks_ms() + self.connection._last_rcv_time = ticks_ms() + self.connection._last_send_time = ticks_ms() + self.connection._last_ping_time = ticks_ms() + self.connection._state = self.connection.DISCONNECTED print(LOGO) def connect(self, timeout=_CONNECT_TIMEOUT): end_time = time.time() + timeout - while not self.connected(): - if self._state == self.DISCONNECTED: + while not self.connection.connected(): + if self.connection._state == self.connection.DISCONNECTED: try: - self._get_socket() - self._authenticate() - self._set_heartbeat() - self._last_rcv_time = ticks_ms() - self.log('Registered events: {}\n'.format(list(self._events.keys()))) + self.connection._get_socket() + self.connection._authenticate() + self.connection._set_heartbeat() + self.connection._last_rcv_time = ticks_ms() + self.connection.log('Registered events: {}\n'.format(list(self._events.keys()))) self.call_handler(self._CONNECT) return True except BlynkError as b_err: self.disconnect(b_err) - sleep_ms(self.TASK_PERIOD_RES) + sleep_ms(self.connection.TASK_PERIOD_RES) except RedirectError as r_err: self.disconnect() - self.server = r_err.server - self.port = r_err.port - sleep_ms(self.TASK_PERIOD_RES) + self.connection.server = r_err.server + self.connection.port = r_err.port + sleep_ms(self.connection.TASK_PERIOD_RES) if time.time() >= end_time: return False def disconnect(self, err_msg=None): self.call_handler(self._DISCONNECT) - if self._socket: - self._socket.close() - self._state = self.DISCONNECTED + if self.connection._socket: + self.connection._socket.close() + self.connection._state = self.connection.DISCONNECTED if err_msg: - self.log('[ERROR]: {}\nConnection closed'.format(err_msg)) + self.connection.log('[ERROR]: {}\nConnection closed'.format(err_msg)) time.sleep(self.RECONNECT_SLEEP) def virtual_write(self, v_pin, *val): - return self.send(self.virtual_write_msg(v_pin, *val)) + return self.connection.send(self.connection.virtual_write_msg(v_pin, *val)) def virtual_sync(self, *v_pin): - return self.send(self.virtual_sync_msg(*v_pin)) + return self.connection.send(self.connection.virtual_sync_msg(*v_pin)) def email(self, to, subject, body): - return self.send(self.email_msg(to, subject, body)) + return self.connection.send(self.connection.email_msg(to, subject, body)) def tweet(self, msg): - return self.send(self.tweet_msg(msg)) + return self.connection.send(self.connection.tweet_msg(msg)) def notify(self, msg): - return self.send(self.notify_msg(msg)) + return self.connection.send(self.connection.notify_msg(msg)) def set_property(self, v_pin, property_name, *val): - return self.send(self.set_property_msg(v_pin, property_name, *val)) + return self.connection.send(self.connection.set_property_msg(v_pin, property_name, *val)) def internal(self, *args): - return self.send(self.internal_msg(*args)) + return self.connection.send(self.connection.internal_msg(*args)) def handle_event(blynk, event_name): class Deco(object): @@ -331,16 +335,16 @@ def __call__(self): def call_handler(self, event, *args, **kwargs): if event in self._events.keys(): - self.log("Event: ['{}'] -> {}".format(event, args)) + self.connection.log("Event: ['{}'] -> {}".format(event, args)) self._events[event](*args, **kwargs) def process(self, msg_type, msg_id, msg_len, msg_args): - if msg_type == self.MSG_RSP: - self.log('Response status: {}'.format(msg_len)) - elif msg_type == self.MSG_PING: - self.send(self.response_msg(self.STATUS_OK, msg_id=msg_id)) - elif msg_type in (self.MSG_HW, self.MSG_BRIDGE, self.MSG_INTERNAL): - if msg_type == self.MSG_INTERNAL: + if msg_type == self.connection.MSG_RSP: + self.connection.log('Response status: {}'.format(msg_len)) + elif msg_type == self.connection.MSG_PING: + self.connection.send(self.connection.response_msg(self.connection.STATUS_OK, msg_id=msg_id)) + elif msg_type in (self.connection.MSG_HW, self.connection.MSG_BRIDGE, self.connection.MSG_INTERNAL): + if msg_type == self.connection.MSG_INTERNAL: self.call_handler("{}{}".format(self._INTERNAL, msg_args[0]), msg_args[1:]) elif len(msg_args) >= const(3) and msg_args[0] == 'vw': self.call_handler("{}{}".format(self._VPIN_WRITE, msg_args[1]), int(msg_args[1]), msg_args[2:]) @@ -350,24 +354,24 @@ def process(self, msg_type, msg_id, msg_len, msg_args): def read_response(self, timeout=0.5): end_time = time.ticks_ms() + int(timeout * const(1000)) while time.ticks_diff(end_time, time.ticks_ms()) > 0: - rsp_data = self.receive(self.rcv_buffer, self.SOCK_TIMEOUT) + rsp_data = self.connection.receive(self.connection.rcv_buffer, self.connection.SOCK_TIMEOUT) if rsp_data: - self._last_rcv_time = ticks_ms() - msg_type, msg_id, h_data, msg_args = self.parse_response(rsp_data, self.rcv_buffer) + self.connection._last_rcv_time = ticks_ms() + msg_type, msg_id, h_data, msg_args = self.connection.parse_response(rsp_data, self.connection.rcv_buffer) self.process(msg_type, msg_id, h_data, msg_args) def run(self): - if not self.connected(): + if not self.connection.connected(): self.connect() else: try: - self.read_response(timeout=self.SOCK_TIMEOUT) - if not self.is_server_alive(): + self.read_response(timeout=self.connection.SOCK_TIMEOUT) + if not self.connection.is_server_alive(): self.disconnect('Server is offline') except KeyboardInterrupt: raise except BlynkError as b_err: - self.log(b_err) + self.connection.log(b_err) self.disconnect() except Exception as g_exc: - self.log(g_exc) + self.connection.log(g_exc) From ce9e1fefa607e88501bfbd2221442ea7e55b6f4f Mon Sep 17 00:00:00 2001 From: Francois Gervais Date: Tue, 15 Dec 2020 12:51:00 -0500 Subject: [PATCH 5/7] Add a new SslConnection This can be used like so: ssl_connection = SslConnection(secret.BLYNK_AUTH, port=443) blynk = blynklib.Blynk(token, connection=ssl_connection) --- blynklib_mp_ssl.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 blynklib_mp_ssl.py diff --git a/blynklib_mp_ssl.py b/blynklib_mp_ssl.py new file mode 100644 index 0000000..d1f09a3 --- /dev/null +++ b/blynklib_mp_ssl.py @@ -0,0 +1,51 @@ +# Copyright (c) 2020 François Gervais +# See the file LICENSE for copying permission. + +import time +import ussl as ssl + +from blynklib_mp import Connection, BlynkError, IOError + + +class SslConnection(Connection): + SOCK_SSL_TIMEOUT = const(1) + + def send(self, data): + retries = self.RETRIES_TX_MAX_NUM + while retries > 0: + try: + retries -= 1 + self._last_send_time = time.ticks_ms() + return self._socket.write(data) + except (IOError, OSError): + sleep_ms(self.RETRIES_TX_DELAY) + + def receive(self, length, timeout): + d_buff = b"" + try: + self._set_socket_timeout(timeout) + timeout = self.SOCK_SSL_TIMEOUT + while not d_buff and timeout > 0: + ret = self._socket.read(length) + if ret: + d_buff += ret + timeout -= self.SOCK_TIMEOUT + time.sleep(self.SOCK_TIMEOUT) + if len(d_buff) >= length: + d_buff = d_buff[:length] + return d_buff + except (IOError, OSError) as err: + if str(err) == "timed out": + return b"" + if str(self.EAGAIN) in str(err) or str(self.ETIMEDOUT) in str(err): + return b"" + raise + + def _get_socket(self): + try: + super()._get_socket() + self.log("Using SSL socket...") + self._socket = ssl.wrap_socket(self._socket) + self._socket.setblocking(False) + except Exception as g_exc: + raise BlynkError("Server connection failed: {}".format(g_exc)) From ecbaa625821746d403090c8fd7e18e294f27d7c4 Mon Sep 17 00:00:00 2001 From: Francois Gervais Date: Tue, 12 Jan 2021 10:39:26 -0500 Subject: [PATCH 6/7] Add forgotten connection access for RECONNECT_SLEEP --- blynklib_mp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blynklib_mp.py b/blynklib_mp.py index 9380aa2..0c204d1 100644 --- a/blynklib_mp.py +++ b/blynklib_mp.py @@ -293,7 +293,7 @@ def disconnect(self, err_msg=None): self.connection._state = self.connection.DISCONNECTED if err_msg: self.connection.log('[ERROR]: {}\nConnection closed'.format(err_msg)) - time.sleep(self.RECONNECT_SLEEP) + time.sleep(self.connection.RECONNECT_SLEEP) def virtual_write(self, v_pin, *val): return self.connection.send(self.connection.virtual_write_msg(v_pin, *val)) From d28781f125a4981ca4511f2e25a870a75631d49f Mon Sep 17 00:00:00 2001 From: Francois Gervais Date: Tue, 12 Jan 2021 10:40:13 -0500 Subject: [PATCH 7/7] Reset _msg_id on disconnect This was added only to the cPython version in commit 81dfc160a94c02ef03737cf00e426f69298dd148. --- blynklib_mp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blynklib_mp.py b/blynklib_mp.py index 0c204d1..a3dbd18 100644 --- a/blynklib_mp.py +++ b/blynklib_mp.py @@ -293,6 +293,7 @@ def disconnect(self, err_msg=None): self.connection._state = self.connection.DISCONNECTED if err_msg: self.connection.log('[ERROR]: {}\nConnection closed'.format(err_msg)) + self.connection._msg_id = 0 time.sleep(self.connection.RECONNECT_SLEEP) def virtual_write(self, v_pin, *val):