From 7afbf5c090f2a500a1b38047804236fc38d89d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 8 Jun 2020 22:05:20 +0200 Subject: [PATCH 1/5] Ensure manual log ins --- README.md | 21 ++++++++----- example.py | 36 ---------------------- pyblnet/blnet.py | 31 ++++++++++--------- pyblnet/blnet_web.py | 23 +++++--------- pyblnet/tests/test_web.py | 65 +++++++++++++++++++-------------------- 5 files changed, 69 insertions(+), 107 deletions(-) delete mode 100644 example.py diff --git a/README.md b/README.md index 3974970..8530a17 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,15 @@ Setting switches and reading their manual/auto state is only possible via the BL ### Usage ```python +from pyblnet import test_blnet, BLNET, BLNETWeb, BLNETDirect + ip = '192.168.178.10' # Check if there is a blnet at given address test_blnet(ip) # -> True/False # Convenient high level interface -blnet = BLNET(ip, timeout=5) +blnet = BLNET(ip, password='pass', timeout=5) # Control a switch by its ID blnet.turn_on(10) @@ -44,12 +46,17 @@ print(blnet.fetch()) # note that the direct use of these modules is discouraged though # Fetch the latest data via web interface -blnet = BLNETWeb(ip, timeout=5) -print(blnet.read_analog_values()) -print(blnet.read_digital_values()) - -# For publishing values -blnet.set_digital_value('10', 'AUS') +# Note that manual log in and log out are required +# when not using the with statement +with BLNETWeb(ip, password='pass', timeout=5) as blnet_session: + print(blnet_session.read_analog_values()) + print(blnet_session.read_digital_values()) + + # For publishing values + blnet_session.set_digital_value('10', 'AUS') + # Note that without explicit log out, + # the BLNET will block any further web access for the next 150s + # this is handled automatically when using the with statement # Fetch data via the Protocol developed by TA blnet = BLNETDirect(ip) diff --git a/example.py b/example.py deleted file mode 100644 index 8210e6a..0000000 --- a/example.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Created on 12.08.2018 - -@author: Niels -""" - -from pyblnet import BLNETWeb, test_blnet, BLNETDirect, BLNET - -if __name__ == '__main__': - - ip = '192.168.178.10' - - # Check if there is a blnet at given address - print(test_blnet(ip)) - - # Easy to use high level interface - blnet = BLNET(ip, timeout=5) - print(blnet.turn_on(10)) - print(blnet.fetch()) - - # Fetch the latest data via web interface - blnet = BLNETWeb(ip, timeout=5) - print(blnet.read_analog_values()) - print(blnet.read_digital_values()) - - # For publishing values - #print(blnet.set_digital_value("10", 'AUS')) - #print(blnet.read_digital_values()) - - blnet = BLNETDirect(ip) - # Fetching the latest data from the backend - print(blnet.get_latest()) - # Still inofficial because unexplicably failing often - print(blnet._get_data(1)) diff --git a/pyblnet/blnet.py b/pyblnet/blnet.py index 54e79bd..6e30da9 100644 --- a/pyblnet/blnet.py +++ b/pyblnet/blnet.py @@ -80,12 +80,13 @@ def fetch(self, node=None): 'power': {}, } if self.blnet_web: - if node is not None: - self.blnet_web.set_node(node) - data['analog'] = self._convert_web( - self.blnet_web.read_analog_values()) - data['digital'] = self._convert_web( - self.blnet_web.read_digital_values()) + with self.blnet_web as blnet_session: + if node is not None: + blnet_session.set_node(node) + data['analog'] = self._convert_web( + blnet_session.read_analog_values()) + data['digital'] = self._convert_web( + blnet_session.read_digital_values()) if self.blnet_direct: direct = self.blnet_direct.get_latest(self.max_retries)[0] # Override values for analog and digital as values are @@ -124,15 +125,15 @@ def turn_auto(self, digital_id, can_node=None): def _turn(self, digital_id, value, can_node=None): if self.blnet_web: - if not self.blnet_web.log_in(): - raise ConnectionError('Could not log in') - if can_node is not None: - if not self.blnet_web.set_node(can_node): - raise ConnectionError('Could not set node') - if not self.blnet_web.set_digital_value(digital_id, value): - raise ConnectionError('Failed to set value') - else: - return True + with self.blnet_web as blnet_session: + if not blnet_session.logged_in(): + raise ConnectionError('Could not log in') + if can_node is not None: + if not blnet_session.set_node(can_node): + raise ConnectionError('Could not set node') + if not blnet_session.set_digital_value(digital_id, value): + raise ConnectionError('Failed to set value') + return True else: raise EnvironmentError('Can\'t set values with blnet web disabled') diff --git a/pyblnet/blnet_web.py b/pyblnet/blnet_web.py index 1c03c20..639337f 100644 --- a/pyblnet/blnet_web.py +++ b/pyblnet/blnet_web.py @@ -70,6 +70,13 @@ def __init__(self, ip, password=_def_password, timeout=5): self.password = password self._timeout = timeout + def __enter__(self): + self.log_in() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.log_out() + def logged_in(self): """ Determines whether the object is still connected to the BLNET @@ -153,10 +160,6 @@ def set_node(self, node): Return: Still logged in (indicating successful node change) """ - # ensure to be logged in - if not self.log_in(): - return False - # send the request to change the node try: r = requests.get( @@ -173,10 +176,6 @@ def read_analog_values(self): Reads all analog values (temperatures, speeds) from the web interface and returns list of quadruples of id, name, value, unit of measurement """ - # ensure to be logged in - if not self.log_in(): - return None - try: r = requests.get( self.ip + "/580500.htm", @@ -219,10 +218,6 @@ def read_digital_values(self): and returns list of quadruples of id, name, mode (AUTO/HAND), value (EIN/AUS) """ - # ensure to be logged in - if not self.log_in(): - return None - try: r = requests.get( self.ip + "/580600.htm", @@ -280,10 +275,6 @@ def set_digital_value(self, digital_id, value): raise ValueError( 'Device id can\'t be larger than 15, was {}'.format( digital_id)) - # ensure to be logged in - if not self.log_in(): - return False - # transform input value to 'EIN' or 'AUS' if isinstance(value, str): if value.lower() == 'AUTO'.lower() or value == '3': diff --git a/pyblnet/tests/test_web.py b/pyblnet/tests/test_web.py index 5c20ef0..5604aa5 100644 --- a/pyblnet/tests/test_web.py +++ b/pyblnet/tests/test_web.py @@ -147,45 +147,44 @@ def test_blnet_fetch_error(self): def test_blnet_web_analog(self): """ Test reading analog values """ - self.assertEqual( - BLNETWeb(self.url, password=PASSWORD, - timeout=10).read_analog_values(), STATE_ANALOG) + with BLNETWeb(self.url, password=PASSWORD, timeout=10) as blnet: + self.assertEqual( + blnet.read_analog_values(), STATE_ANALOG) def test_blnet_web_digital(self): """ Test reading digital values""" - self.assertEqual( - BLNETWeb(self.url, password=PASSWORD, - timeout=10).read_digital_values(), STATE_DIGITAL) + with BLNETWeb(self.url, password=PASSWORD, timeout=10) as blnet: + self.assertEqual(blnet.read_digital_values(), STATE_DIGITAL) def test_blnet_web_set_digital(self): """ Test setting digital values """ - blnet = BLNETWeb(self.url, password=PASSWORD, timeout=10) - blnet.set_digital_value(10, '2') - self.assertEqual(self.server.get_node('A'), '2') - blnet.set_digital_value(9, 'EIN') - self.assertEqual(self.server.get_node('9'), '2') - blnet.set_digital_value(8, 'auto') - self.assertEqual(self.server.get_node('8'), '3') - blnet.set_digital_value(1, 'on') - self.assertEqual(self.server.get_node('1'), '2') - blnet.set_digital_value(1, 'AUS') - self.assertEqual(self.server.get_node('1'), '1') - blnet.set_digital_value(5, 3) - self.assertEqual(self.server.get_node('5'), '3') - blnet.set_digital_value(4, True) - self.assertEqual(self.server.get_node('4'), '2') - blnet.set_digital_value(6, False) - self.assertEqual(self.server.get_node('6'), '1') - try: - blnet.set_digital_value(0, 'EIN') - self.fail("Didn't catch wrong id 0") - except ValueError: - pass - try: - blnet.set_digital_value(16, 'EIN') - self.fail("Didn't catch wrong id 16") - except ValueError: - pass + with BLNETWeb(self.url, password=PASSWORD, timeout=10) as blnet: + blnet.set_digital_value(10, '2') + self.assertEqual(self.server.get_node('A'), '2') + blnet.set_digital_value(9, 'EIN') + self.assertEqual(self.server.get_node('9'), '2') + blnet.set_digital_value(8, 'auto') + self.assertEqual(self.server.get_node('8'), '3') + blnet.set_digital_value(1, 'on') + self.assertEqual(self.server.get_node('1'), '2') + blnet.set_digital_value(1, 'AUS') + self.assertEqual(self.server.get_node('1'), '1') + blnet.set_digital_value(5, 3) + self.assertEqual(self.server.get_node('5'), '3') + blnet.set_digital_value(4, True) + self.assertEqual(self.server.get_node('4'), '2') + blnet.set_digital_value(6, False) + self.assertEqual(self.server.get_node('6'), '1') + try: + blnet.set_digital_value(0, 'EIN') + self.fail("Didn't catch wrong id 0") + except ValueError: + pass + try: + blnet.set_digital_value(16, 'EIN') + self.fail("Didn't catch wrong id 16") + except ValueError: + pass def tearDown(self): self.server_control.stop_server() From ad32fd2578ab98c9df08d2df2c5129ad87e34694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 8 Jun 2020 23:19:52 +0200 Subject: [PATCH 2/5] Improve mock server, test log out routine --- pyblnet/blnet_web.py | 4 +- .../tests/test_structure/blnet_mock_server.py | 42 +++++++++++++++---- pyblnet/tests/test_web.py | 31 +++++++++----- 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/pyblnet/blnet_web.py b/pyblnet/blnet_web.py index 639337f..139a319 100644 --- a/pyblnet/blnet_web.py +++ b/pyblnet/blnet_web.py @@ -288,9 +288,9 @@ def set_digital_value(self, digital_id, value): else: raise ValueError("Illegal input string {}".format(value)) elif isinstance(value, int) and not isinstance(value, bool): - if value is 3 or value is 2 or value is 1: + if value in (1, 2, 3): value = str(value) - elif value is 0: + elif value == 0: value = '1' else: raise ValueError("Illegal input integer {}".format(value)) diff --git a/pyblnet/tests/test_structure/blnet_mock_server.py b/pyblnet/tests/test_structure/blnet_mock_server.py index 0b7dee8..c6952e2 100644 --- a/pyblnet/tests/test_structure/blnet_mock_server.py +++ b/pyblnet/tests/test_structure/blnet_mock_server.py @@ -11,13 +11,20 @@ from pathlib import Path SERVER_DIR = Path(__file__).parent or Path('.') +PASSWORD = '0123' + +COOKIE_RANGE = (0xAAAA, 0xFFFF) +COOKIE_NUM = COOKIE_RANGE[1] - COOKIE_RANGE[0] +LOGIN_COOKIES = [hex(i) for i in range(*COOKIE_RANGE)] class BLNETServer(HTTPServer): + current_log_in_cookie = 0 + # Currently allowed login cookie # or none if no one logged in - logged_in = True + logged_in = None # no login necessary password = None # access to digital nodes @@ -30,8 +37,18 @@ def set_password(self, password): self.password = password self.logged_in = None - def set_logged_in(self, cookie): - self.logged_in = cookie + def log_in(self): + if self.logged_in is not None: + return False + self.logged_in = LOGIN_COOKIES[self.current_log_in_cookie] + self.current_log_in_cookie = (self.current_log_in_cookie + 1) % COOKIE_NUM + return self.logged_in + + def log_out(self): + self.logged_in = None + + def is_logged_in(self, cookie): + return cookie is not None and self.logged_in == cookie def set_node(self, id, value): self.nodes[id] = value @@ -48,6 +65,8 @@ def unset_blocked(self): class BLNETRequestHandler(SimpleHTTPRequestHandler): + server: BLNETServer + def do_GET(self): """ Handle get request, but check for errors in protocol @@ -57,11 +76,14 @@ def do_GET(self): self.send_error(403, "Access denied because server is blocked") return path = self.translate_path(self.path) + if (self.server.is_logged_in(self.headers.get("cookie")) + and "?blL=1" in self.requestline): + self.server.log_out() # Only access that is allowed without login is main.html if (not Path(path) == SERVER_DIR and not Path(path) == SERVER_DIR.joinpath('main.htm') and not Path(path) == SERVER_DIR.joinpath('main.html')): - if not self.server.logged_in: + if not self.server.is_logged_in(self.headers.get("cookie")): self.send_error(403, "Not logged in, access denied") return # Parse node sets @@ -121,8 +143,11 @@ def do_POST(self): return # All checks passed? set Set-Cookie header and do_GET # random cookie - do not hardcode - self.server.set_logged_in('C1A3') - self.headers.add_header('Cookie', 'C1A3') + cookie = self.server.log_in() + if not cookie: + self.send_error(403, "Previous user did not log out") + return + self.headers.add_header('Cookie', cookie) self.do_GET() def translate_path(self, path): @@ -203,9 +228,8 @@ def send_head(self): self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) # Addition: send cookie - if (self.server.logged_in is not None - and self.server.logged_in == self.headers.get('Cookie')): - self.send_header('Set-Cookie', 'C1A3') + if self.server.is_logged_in(self.headers.get('Cookie')): + self.send_header('Set-Cookie', self.server.logged_in) self.end_headers() return f except: diff --git a/pyblnet/tests/test_web.py b/pyblnet/tests/test_web.py index 5604aa5..30e6ea0 100644 --- a/pyblnet/tests/test_web.py +++ b/pyblnet/tests/test_web.py @@ -4,7 +4,7 @@ # general requirements import unittest from .test_structure.server_control import Server -from .test_structure.blnet_mock_server import BLNETServer, BLNETRequestHandler +from .test_structure.blnet_mock_server import BLNETServer, BLNETRequestHandler, PASSWORD # For the server in this case import time @@ -14,7 +14,6 @@ from .web_raw.web_state import STATE, STATE_ANALOG, STATE_DIGITAL ADDRESS = 'localhost' -PASSWORD = '0123' class OfflineTest(unittest.TestCase): @@ -27,7 +26,7 @@ def test_blnet(self): def test_blnet_web(self): try: - blnet = BLNETWeb(self.url) + BLNETWeb(self.url) self.fail("Didn't throw an exception for offline blnetweb") except ValueError: pass @@ -71,16 +70,18 @@ def test_blnet(self): self.server.set_blocked() self.assertTrue(test_blnet(self.url, timeout=10)) - def test_blnet_login(self): - """ Test logging in """ - self.assertTrue( - BLNETWeb(self.url, password=PASSWORD, timeout=10).log_in()) + def test_blnet_web_log_in(self): + """ + Test logging in via different urls + and that log out after a with clause happens correctly + """ + with BLNETWeb(self.url, password=PASSWORD, timeout=10) as blnet: + self.assertTrue(blnet.logged_in()) # test without http - self.assertTrue( - BLNETWeb( - "{}:{}".format(ADDRESS, self.port), + with BLNETWeb("{}:{}".format(ADDRESS, self.port), password=PASSWORD, - timeout=10).log_in()) + timeout=10) as blnet: + self.assertTrue(blnet.logged_in()) def test_blnet_fetch(self): """ Test fetching data in higher level class """ @@ -186,6 +187,14 @@ def test_blnet_web_set_digital(self): except ValueError: pass + def test_blnet_web_log_out(self): + """ Test setting digital values """ + blnet = BLNETWeb(self.url, password=PASSWORD, timeout=10) + self.assertFalse(blnet.logged_in()) + with blnet as blnet: + self.assertTrue(blnet.logged_in()) + self.assertFalse(blnet.logged_in()) + def tearDown(self): self.server_control.stop_server() pass From 41cd22c81209855eb68a7ea79fecfc14fd4ba8c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 8 Jun 2020 23:23:19 +0200 Subject: [PATCH 3/5] Fix new typing syntax usage --- pyblnet/tests/test_structure/blnet_mock_server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyblnet/tests/test_structure/blnet_mock_server.py b/pyblnet/tests/test_structure/blnet_mock_server.py index c6952e2..1f35fc3 100644 --- a/pyblnet/tests/test_structure/blnet_mock_server.py +++ b/pyblnet/tests/test_structure/blnet_mock_server.py @@ -65,7 +65,8 @@ def unset_blocked(self): class BLNETRequestHandler(SimpleHTTPRequestHandler): - server: BLNETServer + # uncomment on higher python versions for better debugging + #server: BLNETServer def do_GET(self): """ From 535a235321f08c7094666dfc03910f3d124d11de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 8 Jun 2020 23:31:15 +0200 Subject: [PATCH 4/5] Fix pep issues --- pyblnet/tests/test_structure/blnet_mock_server.py | 5 +++-- pyblnet/tests/test_web.py | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pyblnet/tests/test_structure/blnet_mock_server.py b/pyblnet/tests/test_structure/blnet_mock_server.py index 1f35fc3..b3f0407 100644 --- a/pyblnet/tests/test_structure/blnet_mock_server.py +++ b/pyblnet/tests/test_structure/blnet_mock_server.py @@ -41,7 +41,8 @@ def log_in(self): if self.logged_in is not None: return False self.logged_in = LOGIN_COOKIES[self.current_log_in_cookie] - self.current_log_in_cookie = (self.current_log_in_cookie + 1) % COOKIE_NUM + self.current_log_in_cookie += 1 + self.current_log_in_cookie %= COOKIE_NUM return self.logged_in def log_out(self): @@ -66,7 +67,7 @@ def unset_blocked(self): class BLNETRequestHandler(SimpleHTTPRequestHandler): # uncomment on higher python versions for better debugging - #server: BLNETServer + # server: BLNETServer def do_GET(self): """ diff --git a/pyblnet/tests/test_web.py b/pyblnet/tests/test_web.py index 30e6ea0..d619e34 100644 --- a/pyblnet/tests/test_web.py +++ b/pyblnet/tests/test_web.py @@ -4,7 +4,9 @@ # general requirements import unittest from .test_structure.server_control import Server -from .test_structure.blnet_mock_server import BLNETServer, BLNETRequestHandler, PASSWORD +from .test_structure.blnet_mock_server import ( + BLNETServer, BLNETRequestHandler, PASSWORD +) # For the server in this case import time @@ -78,9 +80,8 @@ def test_blnet_web_log_in(self): with BLNETWeb(self.url, password=PASSWORD, timeout=10) as blnet: self.assertTrue(blnet.logged_in()) # test without http - with BLNETWeb("{}:{}".format(ADDRESS, self.port), - password=PASSWORD, - timeout=10) as blnet: + blnet = BLNETWeb("{}:{}".format(ADDRESS, self.port), password=PASSWORD, timeout=10) + with blnet as blnet: self.assertTrue(blnet.logged_in()) def test_blnet_fetch(self): From 2563656e7efad94dc445509d9136d3d62db7d607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 8 Jun 2020 23:38:57 +0200 Subject: [PATCH 5/5] New version Successful log outs should make the system a lot more stable --- pyblnet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyblnet/__init__.py b/pyblnet/__init__.py index cc29845..c5aa5ad 100644 --- a/pyblnet/__init__.py +++ b/pyblnet/__init__.py @@ -10,7 +10,7 @@ except ImportError as e: warnings.warn(ImportWarning(e)) -VERSION = (0, 8, 1) +VERSION = (0, 9, 0) __version__ = '.'.join([str(i) for i in VERSION]) __author__ = 'nielstron'