From f53934020ce5cae9b21fa89cb08c496d8b8eac26 Mon Sep 17 00:00:00 2001 From: szeka9 Date: Sat, 28 Mar 2026 13:52:58 +0100 Subject: [PATCH 1/8] Chunked transfer decoding in request bodies - support "Transfer-Encoding: chunked" in requests - rename _recv_payload to _recv_payload_st to remain consistent in naming internal states - remove unnecessary branch in response generation for image/jpeg content type - add new unit tests and functional test --- README.md | 1 + src/pyrobusta/protocol/http.py | 49 +++++++++++++++++++----- tests/functional/test_http.py | 43 ++++++++++++++++++++- tests/unit/test_http.py | 70 ++++++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1156afc..0a14064 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ A lightweight HTTP server library for MicroPython designed for constrained embed - Routing decorators - Fixed-size, configurable request/response buffers - Multipart request and response handling +- Chunked transfer decoding for streamed request bodies - Bounded-copy memory footprint - Finite-state-machine parser with linear sliding buffer - Robust byte-stream handling diff --git a/src/pyrobusta/protocol/http.py b/src/pyrobusta/protocol/http.py index f72cb1a..d11d78b 100644 --- a/src/pyrobusta/protocol/http.py +++ b/src/pyrobusta/protocol/http.py @@ -40,6 +40,7 @@ class HttpEngine: "url", "query", "content_length_cnt", + "recv_chunk_size", "mp_boundary", "mp_first_part", "mp_last_part", @@ -109,7 +110,7 @@ class HttpEngine: MULTIPART_BOUNDARY = b"pyrobusta-boundary" - CONTENT_LENGTH_ERROR = b"Content-Length mismatch" + CONTENT_LENGTH_ERROR = b"content length mismatch" HEADER_ERROR = b"Invalid headers" MULTIPART_BOUNDARY_ERROR = b"Invalid multipart boundary" BAD_REQUEST_ERROR = b"Bad request" @@ -127,6 +128,7 @@ def __init__(self): self.url = None self.query = None self.content_length_cnt = 0 + self.recv_chunk_size = 0 # [Multipart state] self.mp_boundary = None @@ -443,17 +445,22 @@ def _parse_headers_st(self, rx, tx): return try: self.headers = self._parse_headers(rx.peek(blank_idx)) + if self.version == b"HTTP/1.1" and "host" not in self.headers: + raise HeaderParsingError() except HeaderParsingError: self.on_client_error(tx, self.HEADER_ERROR) return rx.consume(blank_idx + 4) self.state = self._route_request_st + def _is_chunked(self): + return self.headers.get("transfer-encoding") == "chunked" + def _has_payload(self): return ( self.CONTENT_LENGTH in self.headers and self.headers[self.CONTENT_LENGTH] > 0 - ) + ) or self._is_chunked() def _route_request_st(self, _, tx): """ @@ -481,8 +488,10 @@ def _route_request_st(self, _, tx): if mp_boundary := self._is_multipart(self.headers): self.mp_boundary = mp_boundary.encode(self.ASCII) self.state = self._start_multipart_parser_st + elif self._is_chunked(): + self.state = self._recv_chunked_size_st else: - self.state = self._recv_payload + self.state = self._recv_payload_st else: self.state = self._app_endpoint_st return @@ -501,7 +510,23 @@ def _route_request_st(self, _, tx): return self.on_missing_resource(tx) - def _recv_payload(self, rx, tx): + def _recv_chunked_size_st(self, rx, _): + if (blank_idx := rx.find(b"\r\n")) == -1: + return + self.recv_chunk_size = int(bytes(rx.peek(blank_idx)), 16) + rx.consume(blank_idx + 2) + self.state = self._recv_chunk_st + + def _recv_chunk_st(self, rx, tx): + if self.recv_chunk_size + 2 > rx.size(): + return + if self.recv_chunk_size + 2 <= rx.size(): + if rx.peek()[self.recv_chunk_size : self.recv_chunk_size + 2] != b"\r\n": + self.on_client_error(tx, self.CONTENT_LENGTH_ERROR) + return + self.state = self._app_endpoint_st + + def _recv_payload_st(self, rx, tx): if self.headers[self.CONTENT_LENGTH] > rx.size(): return if self.headers[self.CONTENT_LENGTH] < rx.size(): @@ -514,8 +539,16 @@ def _app_endpoint_st(self, rx, tx): method = self.GET if self.method == self.HEAD else self.method callback = self._get_callback(self.url, method) if self._has_payload(): - self.state = None - dtype, data = callback(self, bytes(rx.peek())) + if self._is_chunked(): + if self.recv_chunk_size: + callback(self, bytes(rx.peek(self.recv_chunk_size))) + rx.consume(self.recv_chunk_size + 2) + self.state = self._recv_chunked_size_st + return + dtype, data = callback(self, bytes(rx.peek(self.recv_chunk_size))) + rx.consume(self.recv_chunk_size + 2) + else: + dtype, data = callback(self, bytes(rx.peek())) dtype = dtype.encode(self.ASCII) else: if not callable(callback): @@ -527,9 +560,7 @@ def _app_endpoint_st(self, rx, tx): dtype, data = callback(self, b"") dtype = dtype.encode(self.ASCII) self._set_response_header(b"content-type", dtype) - if dtype == b"image/jpeg": - self.terminate(200, dtype) - return self._generate_response(tx, data) + if dtype in (b"multipart/x-mixed-replace", b"multipart/form-data"): part_content_type = data[0] callback = data[1] diff --git a/tests/functional/test_http.py b/tests/functional/test_http.py index 611a0d2..80b2468 100644 --- a/tests/functional/test_http.py +++ b/tests/functional/test_http.py @@ -1,5 +1,6 @@ import asyncio import ssl +import json from pyrobusta.server import http_server from pyrobusta.protocol import http_multipart @@ -89,6 +90,16 @@ def busy_callback(*_): raise ServerBusyError() +def create_chunked_app_endpoint(endpoint): + recv_chunks = [] + + @HttpEngine.route(endpoint, "POST") + def chunked_callback(http_ctx, chunk): + if not chunk: # Received terminating chunk + return "application/json", recv_chunks + recv_chunks.append(chunk.decode("utf8")) + + async def test_simple_response(tls_enabled): setup_config(multipart=False, tls_enabled=tls_enabled) @@ -180,7 +191,6 @@ async def test_server_busy(): server_task = asyncio.create_task(server.run_server()) await asyncio.sleep_ms(100) - # Test: 1 part plain_response = await send_request( b"POST /test/busy HTTP/1.1\r\n" b"Host: localhost\r\n\r\n" ) @@ -194,6 +204,36 @@ async def test_server_busy(): await server.terminate() +async def test_chunked_transfer_encoding(): + setup_config() + create_chunked_app_endpoint("/test/chunked") + + server = http_server.HttpServer() + server_task = asyncio.create_task(server.run_server()) + await asyncio.sleep_ms(100) + + json_response = await send_request( + ( + b"POST /test/chunked HTTP/1.1\r\n" + b"Host: localhost\r\n" + b"Transfer-Encoding: chunked\r\n\r\n" + b"14\r\nchunking\r\ntest\r\ncase\r\n" + b"E\r\nchunking\r\ntest\r\n" + b"8\r\nchunking\r\n" + b"0\r\n\r\n" + ) + ) + response_body = json.loads(json_response.split(b"\r\n\r\n")[1]) + test_assert( + f"chunked transfer encoding - all chunks are received", + response_body, + ["chunking\r\ntest\r\ncase", "chunking\r\ntest", "chunking"], + ) + + server_task.cancel() + await server.terminate() + + ################################################# # Test methods ################################################# @@ -246,6 +286,7 @@ def test_main(): asyncio.run(test_multipart_response(tls_enabled=True)) asyncio.run(test_server_busy()) + asyncio.run(test_chunked_transfer_encoding()) test_main() diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index b0c8655..c50c399 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -311,6 +311,76 @@ def test_empty_or_missing_url_encoded_query_parameter(self): with self.assertRaises(KeyError): self.engine.get_url_encoded_query_param(self.engine.query, "param3") + def test_chunked_transfer_encoding_valid(self): + self.engine.url = b"/api/test" + self.engine.method = b"GET" + self.engine.version = b"HTTP/1.1" + self.engine.headers["transfer-encoding"] = "chunked" + self.engine.state = self.engine._recv_chunked_size_st + + test_callback = mock.Mock(return_value=("text/plain", "OK")) + self.engine.register("/api/test", test_callback, "GET") + + for chunk in ( + b"14\r\nchunking\r\ntest\r\ncase\r\n", + b"E\r\nchunking\r\ntest\r\n", + b"8\r\nchunking\r\n", + b"0\r\n\r\n", + ): + for i in range(len(chunk)): + self.rx.write(chunk[i : i + 1]) + self.engine.state(self.rx, self.tx) + + self.assertEqual(self.engine.state, self.engine._app_endpoint_st) + self.engine.state(self.rx, self.tx) + size_delimiter = chunk.find(b"\r\n") + test_callback.assert_called_with( + self.engine, chunk[size_delimiter + 2 : -2] + ) + + self.assertEqual(self.engine.status_code, 200) + self.assertEqual(self.engine.state, None) + + def test_chunked_transfer_encoding_invalid_chunk_size_smaller(self): + self.engine.url = b"/api/test" + self.engine.method = b"GET" + self.engine.version = b"HTTP/1.1" + self.engine.headers["transfer-encoding"] = "chunked" + self.engine.state = self.engine._recv_chunked_size_st + + test_callback = mock.Mock(return_value=("text/plain", "OK")) + self.engine.register("/api/test", test_callback, "GET") + + chunk = b"2\r\nchunking\r\n" + for i in range(len(chunk)): + self.rx.write(chunk[i : i + 1]) + self.engine.state(self.rx, self.tx) + if self.engine.state is None: + break + + self.assertEqual(self.engine.status_code, 400) + self.assertEqual(self.engine.state, None) + + def test_chunked_transfer_encoding_chunk_incomplete(self): + self.engine.url = b"/api/test" + self.engine.method = b"GET" + self.engine.version = b"HTTP/1.1" + self.engine.headers["transfer-encoding"] = "chunked" + self.engine.state = self.engine._recv_chunked_size_st + + test_callback = mock.Mock(return_value=("text/plain", "OK")) + self.engine.register("/api/test", test_callback, "GET") + + chunk = b"FF\r\nchunking\r\n" + for i in range(len(chunk)): + self.rx.write(chunk[i : i + 1]) + self.engine.state(self.rx, self.tx) + if self.engine.state is None: + break + + self.assertEqual(self.engine.status_code, None) + self.assertEqual(self.engine.state, self.engine._recv_chunk_st) + class TestMultipartStateMachine(TestWebStateMachineBase): """ From 7039cb4caeece687929f615adcadcccec4495384 Mon Sep 17 00:00:00 2001 From: szeka9 Date: Sun, 29 Mar 2026 01:01:24 +0100 Subject: [PATCH 2/8] Deploy package under /lib - always use /lib for deployment to match the default mip installation path --- Makefile | 21 +++++++++++---------- docs/configuration.md | 2 +- src/pyrobusta/utils/config.py | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 4d1b045..9d2a87b 100644 --- a/Makefile +++ b/Makefile @@ -68,12 +68,13 @@ deploy: @echo "Uploading build/$(PKG) to device $(DEVICE)" @find $(BUILD_DIR)/$(PKG) | while read source; do \ rel=$${source#$(BUILD_DIR)/}; \ + remote="/lib/$${rel}"; \ if [ -d "$$source" ]; then \ - mpremote $(DEVICE) mkdir "$$rel" || true; \ + mpremote $(DEVICE) mkdir "$$remote" || true; \ elif [ -f "$$source" ]; then \ - echo "Uploading $$rel"; \ - mpremote $(DEVICE) rm "$$rel" || true; \ - mpremote $(DEVICE) cp "$$source" ":$$rel"; \ + echo "Uploading $$remote"; \ + mpremote $(DEVICE) rm ":$$remote" || true; \ + mpremote $(DEVICE) cp "$$source" ":$$remote"; \ fi; \ sleep 1; \ done @@ -121,10 +122,10 @@ publish: stage-example: @echo "Preparing unix runtime in $(RUNTIME_DIR)" @rm -rf $(RUNTIME_DIR) - @mkdir -p $(RUNTIME_DIR) + @mkdir -p $(RUNTIME_DIR)/lib @echo "Copying built package" - @cp -r build/pyrobusta $(RUNTIME_DIR)/ + @cp -r build/pyrobusta $(RUNTIME_DIR)/lib @echo "Copying example files" @cp $(EXAMPLE_DIR)/app.py $(RUNTIME_DIR)/ @@ -142,7 +143,7 @@ stage-example: .PHONY: run-unix run-unix: stage-example @echo "Running example with unix micropython" - cd $(RUNTIME_DIR) && ../$(MICROPYTHON) app.py + cd $(RUNTIME_DIR) && MICROPYPATH=":.frozen:lib" ../$(MICROPYTHON) app.py # ----------------------------- # Deploy example app @@ -211,9 +212,9 @@ static-checkers: pylint black .PHONY: stage-test stage-test: @rm -rf $(TEST_RUNTIME) - @mkdir -p $(TEST_RUNTIME) + @mkdir -p $(TEST_RUNTIME)/lib - @cp -r build/pyrobusta $(TEST_RUNTIME)/ + @cp -r build/pyrobusta $(TEST_RUNTIME)/lib @cp tests/functional/*.py $(TEST_RUNTIME)/ # ----------------------------- @@ -225,7 +226,7 @@ test-unix: stage-test tls-cert @cd $(TEST_RUNTIME); \ for test in test_*.py; do \ echo "Running $$test"; \ - ../$(MICROPYTHON) $$(basename $$test) || exit 1; \ + MICROPYPATH=":.frozen:lib" ../$(MICROPYTHON) $$(basename $$test) || exit 1; \ done # ----------------------------- diff --git a/docs/configuration.md b/docs/configuration.md index 5b595a7..8ab2b12 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -9,7 +9,7 @@ to upload it to the root directory of the target device. | wifi_password | Password of the Wi-Fi network. When empty, Wi-Fi is not initalized by the built-in wifi.py module. | None | | http_multipart | Enable multipart HTTP requests/responses. | "False" | | http_mem_cap | Max memory cap (% × 0.01) of usable heap for HTTP request/response stream buffers. | 0.1 | -| http_served_paths | Space delimited list of filesystem paths allowed to be served through HTTP. | "pyrobusta lib" | +| http_served_paths | Space delimited list of filesystem paths allowed to be served through HTTP. | "/lib/pyrobusta" | | socket_max_con | Max number of socket connections of any enabled application server. | 2 | | tls | Enable/disable TLS. When turned on, cert.der/key.der must be installed at the root. | "False" | | log_level | Can be one of: warning, info, debug. | "warning" | diff --git a/src/pyrobusta/utils/config.py b/src/pyrobusta/utils/config.py index ea8e017..53429b5 100644 --- a/src/pyrobusta/utils/config.py +++ b/src/pyrobusta/utils/config.py @@ -17,7 +17,7 @@ "http_mem_cap", 0.1, "http_served_paths", - "pyrobusta lib package.json", + "/lib/pyrobusta", "socket_max_con", 2, "tls", From e82fadb3bbc2a1bf72e994c488e8ed91bf04549b Mon Sep 17 00:00:00 2001 From: szeka9 Date: Sun, 29 Mar 2026 01:07:55 +0100 Subject: [PATCH 3/8] Refine filesystem access control and path normalization - refine lexical path normalization to account for differences in default working directories between Unix and device runtimes; always treat the default working directory as a virtual root - return HTTP 403 (Forbidden) when accessing files outside configured directories served over HTTP - add unit and functional tests for access control and path normalization --- src/pyrobusta/protocol/http.py | 48 ++++++++++++++------ src/pyrobusta/utils/config.py | 12 +++++ src/pyrobusta/utils/helpers.py | 27 +++++++++++ tests/.pylintrc | 3 +- tests/functional/test_http.py | 83 ++++++++++++++++++++++++++-------- tests/unit/test_helpers.py | 56 +++++++++++++++++++++++ tests/unit/test_http.py | 27 ++++++++++- 7 files changed, 221 insertions(+), 35 deletions(-) create mode 100644 src/pyrobusta/utils/helpers.py create mode 100644 tests/unit/test_helpers.py diff --git a/src/pyrobusta/protocol/http.py b/src/pyrobusta/protocol/http.py index d11d78b..7ed3b75 100644 --- a/src/pyrobusta/protocol/http.py +++ b/src/pyrobusta/protocol/http.py @@ -8,6 +8,7 @@ from os import stat from ..utils.config import get_config +from ..utils.helpers import normalize_path class HeaderParsingError(ValueError): @@ -56,6 +57,8 @@ class HttpEngine: b"204 No Content", 400, b"400 Bad Request", + 403, + b"403 Forbidden", 404, b"404 Not Found", 405, @@ -208,6 +211,21 @@ def get_url_encoded_query_param(query: str, key: str, default: str = None): raise KeyError() return default + @classmethod + def is_norm_path_served(cls, path: str): + """ + Returns true if a normalized path is configured to be served + """ + served_paths = set(get_config("http_served_paths").split()) + parts = path.split("/") + for i, _ in enumerate(parts): + current_path = "/".join(parts[: i + 1]) + if not current_path: + current_path = "/" + if current_path in served_paths: + return True + return False + @staticmethod def _lookup(tuple_, key): idx = tuple_.index(key) @@ -368,6 +386,11 @@ def on_client_error(self, tx, info: bytes): self._write_response_head(tx, len(response)) tx.write(response) + def on_forbidden(self, tx): + """Terminate state machine and write 403 response""" + self.terminate(403) + self._write_response_head(tx) + def on_missing_resource(self, tx): """Terminate state machine and write 404 response""" self.terminate(404) @@ -583,22 +606,17 @@ def _app_endpoint_st(self, rx, tx): def _send_file_st(self, _, tx, web_resource: bytes): """State for returning a static resource""" - # Normalize path - parts = [] - for p in web_resource.split(b"/"): - if p in (b".", b""): - continue - if p == b"..": - if parts: - parts.pop() - else: - parts.append(p) - if parts[0].decode(self.ASCII) not in get_config("http_served_paths").split(): - self.on_missing_resource(tx) - return extension = web_resource.rsplit(b".", 1)[-1] - norm_path = b"/".join(parts) - + norm_path = normalize_path(web_resource.decode(self.ASCII)) + is_path_served = self.is_norm_path_served(norm_path) + if not is_path_served: + try: + stat(norm_path) + self.on_forbidden(tx) + return + except OSError: + self.on_missing_resource(tx) + return try: content_type = self._lookup(self.CONTENT_TYPES, extension) except ValueError: diff --git a/src/pyrobusta/utils/config.py b/src/pyrobusta/utils/config.py index 53429b5..ad37cdf 100644 --- a/src/pyrobusta/utils/config.py +++ b/src/pyrobusta/utils/config.py @@ -4,6 +4,8 @@ Values can be encapsulated by single or double quotes. """ +from .helpers import normalize_path + PYROBUSTA_VERSION = "0.2.0" CONFIG_LOADED = False CONFIG_LOCATION = "pyrobusta.env" @@ -27,6 +29,15 @@ ] +def normalize(key, value): + """ + Normalize a configuration value depending on the key. + """ + if key == "http_served_paths": + return " ".join([normalize_path(p) for p in value.split()]) + return value + + def read_config(config=CONFIG_LOCATION): """ Read configuration from a file and update CONFIG_CACHE. @@ -40,6 +51,7 @@ def read_config(config=CONFIG_LOCATION): continue value = line.split("=")[1].strip().strip("'").strip('"') if key and value: + value = normalize(key, value) if ( key in CONFIG_CACHE and (conf_idx := CONFIG_CACHE.index(key)) % 2 == 0 diff --git a/src/pyrobusta/utils/helpers.py b/src/pyrobusta/utils/helpers.py new file mode 100644 index 0000000..cc0d9d3 --- /dev/null +++ b/src/pyrobusta/utils/helpers.py @@ -0,0 +1,27 @@ +""" +Helper methods +""" + +from os import getcwd + + +def normalize_path(path: str): + """Normalize a path string to resolve file and directory paths""" + if not path: + return "" + parts = [] + for p in path.split("/"): + if p in (".", ""): + continue + if p == "..": + if parts: + parts.pop() + else: + parts.append(p) + normalized = "/".join(parts) + cwd = getcwd() + if normalized: + if cwd.endswith("/"): + return cwd + normalized + return cwd + "/" + normalized + return cwd diff --git a/tests/.pylintrc b/tests/.pylintrc index 7bff1de..1824775 100644 --- a/tests/.pylintrc +++ b/tests/.pylintrc @@ -2,4 +2,5 @@ disable=W0212, C0114, C0115, - C0116 + C0116, + R0904 diff --git a/tests/functional/test_http.py b/tests/functional/test_http.py index 80b2468..231f0c4 100644 --- a/tests/functional/test_http.py +++ b/tests/functional/test_http.py @@ -2,6 +2,8 @@ import ssl import json +from os import getcwd, mkdir + from pyrobusta.server import http_server from pyrobusta.protocol import http_multipart from pyrobusta.protocol.http import ( @@ -10,6 +12,7 @@ ServerBusyError, ) from pyrobusta.utils import config +from pyrobusta.utils.helpers import normalize_path ################################################# # Test helpers @@ -100,13 +103,19 @@ def chunked_callback(http_ctx, chunk): recv_chunks.append(chunk.decode("utf8")) -async def test_simple_response(tls_enabled): - setup_config(multipart=False, tls_enabled=tls_enabled) - - # start server as background task +async def start_server(): + """ + Start an HTTP server as a background task + """ server = http_server.HttpServer() server_task = asyncio.create_task(server.run_server()) await asyncio.sleep_ms(100) + return server, server_task + + +async def test_simple_response(tls_enabled): + setup_config(multipart=False, tls_enabled=tls_enabled) + server, server_task = await start_server() # Test: text/plain plain_response = await send_request( @@ -150,11 +159,7 @@ async def test_simple_response(tls_enabled): async def test_multipart_response(tls_enabled): setup_config(multipart=True, tls_enabled=tls_enabled) - - # start server as background task - server = http_server.HttpServer() - server_task = asyncio.create_task(server.run_server()) - await asyncio.sleep_ms(100) + server, server_task = await start_server() # Test: 1 part plain_response = await send_request( @@ -186,10 +191,7 @@ async def test_multipart_response(tls_enabled): async def test_server_busy(): setup_config() - - server = http_server.HttpServer() - server_task = asyncio.create_task(server.run_server()) - await asyncio.sleep_ms(100) + server, server_task = await start_server() plain_response = await send_request( b"POST /test/busy HTTP/1.1\r\n" b"Host: localhost\r\n\r\n" @@ -207,10 +209,7 @@ async def test_server_busy(): async def test_chunked_transfer_encoding(): setup_config() create_chunked_app_endpoint("/test/chunked") - - server = http_server.HttpServer() - server_task = asyncio.create_task(server.run_server()) - await asyncio.sleep_ms(100) + server, server_task = await start_server() json_response = await send_request( ( @@ -234,16 +233,63 @@ async def test_chunked_transfer_encoding(): await server.terminate() +async def test_fs_access_control(): + setup_config(served_paths="/www") + server, server_task = await start_server() + + # Index page under /www -> accepted + workdir = normalize_path("/www") + index_html = normalize_path("/www/index.html") + mkdir(workdir) + with open(index_html, "w") as f: + f.write("PyRobusta Home") + + # Index page under / -> rejected + index_html = normalize_path("/index.html") + with open(index_html, "w") as f: + f.write("PyRobusta Home") + + # Case #1: /www/index.html + response = await send_request( + (b"GET /www/index.html HTTP/1.1\r\n" b"Host: localhost\r\n\r\n") + ) + + response_body = response.split(b"\r\n\r\n")[1] + test_assert( + f"test FS access control - index page loaded", + response_body, + b"PyRobusta Home", + ) + + # Case #2: /index.html + response = await send_request( + (b"GET /index.html HTTP/1.1\r\n" b"Host: localhost\r\n\r\n") + ) + + test_assert( + f"test FS access control - index page rejected", + response.startswith(b"HTTP/1.1 403 Forbidden"), + True, + ) + + server_task.cancel() + await server.terminate() + + ################################################# # Test methods ################################################# -def setup_config(multipart=False, tls_enabled=False): +def setup_config(multipart=False, tls_enabled=False, served_paths=""): config_idx = config.CONFIG_CACHE.index("http_multipart") config.CONFIG_CACHE[config_idx + 1] = str(multipart) config_idx = config.CONFIG_CACHE.index("tls") config.CONFIG_CACHE[config_idx + 1] = str(tls_enabled) + config_idx = config.CONFIG_CACHE.index("http_served_paths") + config.CONFIG_CACHE[config_idx + 1] = config.normalize( + "http_served_paths", served_paths + ) enable_optional_features() @@ -287,6 +333,7 @@ def test_main(): asyncio.run(test_server_busy()) asyncio.run(test_chunked_transfer_encoding()) + asyncio.run(test_fs_access_control()) test_main() diff --git a/tests/unit/test_helpers.py b/tests/unit/test_helpers.py new file mode 100644 index 0000000..6886fcc --- /dev/null +++ b/tests/unit/test_helpers.py @@ -0,0 +1,56 @@ +import unittest +from unittest.mock import patch +from os import getcwd + +from .utils import load_module + + +class TestHelpers(unittest.TestCase): + """ + Base class for stat machine tests. + """ + + @classmethod + def setUpClass(cls): + cls.config = {} + + def setUp(self): + self.helpers_module = load_module("pyrobusta/utils/helpers.py") + + def test_path_normalization_virtual_root(self): + """ + Test lexical path normalization in a Unix-port environment + with a virtual root. Simulates the situation where the process + working directory acts as a virtual filesystem root. + """ + cwd = getcwd() + for case in ( + ("", ""), + ("/path/to/resource", f"{cwd}/path/to/resource"), + ("/path/to/resource/", f"{cwd}/path/to/resource"), + ("///path///to///resource///", f"{cwd}/path/to/resource"), + ("/path/../to/resource", f"{cwd}/to/resource"), + ("/path/./to/resource", f"{cwd}/path/to/resource"), + ("/path/../../resource", f"{cwd}/resource"), + ("/path/../../resource/..", f"{cwd}"), + ): + self.assertEqual(self.helpers_module.normalize_path(case[0]), case[1]) + + @patch("pyrobusta.utils.helpers.getcwd", return_value="/") + def test_path_normalization_host_root(self, _): + """ + Test lexical path normalization assuming the working directory + is the device root ("/"). This simulates the target device environment + where all paths are rooted at "/". + """ + for case in ( + ("", ""), + ("/path/to/resource", "/path/to/resource"), + ("/path/to/resource/", "/path/to/resource"), + ("///path///to///resource///", "/path/to/resource"), + ("/path/../to/resource", "/to/resource"), + ("/path/./to/resource", "/path/to/resource"), + ("/path/../../resource", "/resource"), + ("/path/../../resource/..", "/"), + ): + self.assertEqual(self.helpers_module.normalize_path(case[0]), case[1]) diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index c50c399..13f5bd9 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -35,6 +35,7 @@ def setUp(self): self.set_mock_config(key, value) # Load your web and buffer modules + self.helpers_module = load_module("pyrobusta/utils/helpers.py") buffer_module = load_module("pyrobusta/stream/buffer.py") web_module = load_module("pyrobusta/protocol/http.py") web_module.enable_optional_features() @@ -62,7 +63,7 @@ class TestWebStateMachine(TestWebStateMachineBase): @classmethod def setUpClass(cls): - cls.config = {"http_multipart": "False"} + cls.config = {} def test_status_parsing_valid(self): request = b"GET /index.html HTTP/1.1\r\nContent-Length:10" @@ -381,6 +382,30 @@ def test_chunked_transfer_encoding_chunk_incomplete(self): self.assertEqual(self.engine.status_code, None) self.assertEqual(self.engine.state, self.engine._recv_chunk_st) + def test_path_serving_list(self): + self.set_mock_config("http_served_paths", "/path/to/dir1 /path/to/dir2") + self.assertEqual(self.engine.is_norm_path_served(""), False) + self.assertEqual(self.engine.is_norm_path_served("/"), False) + self.assertEqual(self.engine.is_norm_path_served("/path/to/dir1"), True) + self.assertEqual(self.engine.is_norm_path_served("/path/to/dir2"), True) + self.assertEqual(self.engine.is_norm_path_served("/path/to/dir12"), False) + self.assertEqual(self.engine.is_norm_path_served("/path/to/dir1/file"), True) + self.assertEqual(self.engine.is_norm_path_served("/path/to/dir2/file"), True) + self.assertEqual(self.engine.is_norm_path_served("/path/to/other"), False) + self.assertEqual(self.engine.is_norm_path_served("/path/to"), False) + + def test_path_serving_root(self): + self.set_mock_config("http_served_paths", "/") + self.assertEqual(self.engine.is_norm_path_served(""), True) + self.assertEqual(self.engine.is_norm_path_served("/"), True) + self.assertEqual(self.engine.is_norm_path_served("/path/to/served"), True) + + def test_path_serving_none(self): + self.set_mock_config("http_served_paths", "") + self.assertEqual(self.engine.is_norm_path_served(""), False) + self.assertEqual(self.engine.is_norm_path_served("/"), False) + self.assertEqual(self.engine.is_norm_path_served("/path/to/served"), False) + class TestMultipartStateMachine(TestWebStateMachineBase): """ From c77dd4da2039420dc9fc8225cb54ac44ba72f2e3 Mon Sep 17 00:00:00 2001 From: szeka9 Date: Tue, 31 Mar 2026 00:44:03 +0200 Subject: [PATCH 4/8] Optimize logging strings and reduce import-time RAM usage - use __name__ for module-prefixed log messages - reduce log message lengths - lazy-load ssl library based on TLS setting --- src/pyrobusta/bindings/socket_http.py | 14 +++++++------- src/pyrobusta/con/wifi.py | 6 +++--- src/pyrobusta/server/http_server.py | 21 +++++++++++---------- src/pyrobusta/stream/buffer.py | 6 +++--- src/pyrobusta/transport/socket.py | 6 +++--- 5 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/pyrobusta/bindings/socket_http.py b/src/pyrobusta/bindings/socket_http.py index 955ba3b..a09664a 100644 --- a/src/pyrobusta/bindings/socket_http.py +++ b/src/pyrobusta/bindings/socket_http.py @@ -55,7 +55,7 @@ def init_pools(max_sockets): if is_low_memory: logging.warning( ( - "[SocketHttp.init_pools] low-memory mode with reduced buffer size, " + __name__ + ".init_pools: low-memory mode with reduced buffer size, " "decrease max_clients to use larger buffers" ) ) @@ -73,13 +73,13 @@ def init_pools(max_sockets): if usable < per_conn: raise MemoryError( ( - f"Insufficient memory for webserver: {mem_available // 1024} KB " + f"Insufficient memory: {mem_available // 1024} KB " f"at {SocketHttp.MEM_CAP*100}% cap, " f"at least {per_conn // 1024} KB required" ) ) con_limit = min(usable // per_conn, con_limit) - logging.info((f"[SocketHttp.init_pools] {con_limit} connection(s) allowed")) + logging.info((__name__ + f".init_pools: {con_limit} connection(s) allowed")) SocketHttp.RECV_POOL = MemoryPool(recv_size, con_limit, wrapper=SlidingBuffer) SocketHttp.SEND_POOL = MemoryPool(send_size, con_limit, wrapper=SlidingBuffer) @@ -113,7 +113,7 @@ async def run(self): await self._run_state_machine() await sleep_ms(SocketHttp.STATE_MACHINE_SLEEP_MS) except Exception as e: # pylint: disable=W0718 - logging.warning(f"[SocketHttp] error in run_web: {e}") + logging.warning(__name__ + f": error in run_web: {e}") finally: if self._send_buf: self._send_buf.consume() @@ -126,7 +126,7 @@ async def run(self): async def _reserve_buffers(self): if SocketHttp.SEND_POOL is None or SocketHttp.RECV_POOL is None: - raise RuntimeError("Buffer pools are uninitialized") + raise RuntimeError("Pools are ninitialized") while not self._recv_buf or not self._send_buf: if not self._recv_buf: @@ -158,7 +158,7 @@ async def _run_state_machine(self): await self._flush_response() return except Exception as e: # pylint: disable=W0718 - logging.warning(f"[SocketHttp] error in _run_state_machine: {e}") + logging.warning(__name__ + f"._run_state_machine: {e}") self._engine.on_failure(self._send_buf, str(e).encode("ascii")) await self._flush_response() return @@ -188,7 +188,7 @@ async def _read_to_buf(self): await self._flush_response() return 0 self._recv_buf.write(request) - logging.debug(f"[SocketHttp._read_to_buf] read new message chunk: {request}") + logging.debug(__name__ + f"._read_to_buf: [{request}]") return len(request) async def _response_handler(self, resp_handler): diff --git a/src/pyrobusta/con/wifi.py b/src/pyrobusta/con/wifi.py index 5a2f170..3565082 100644 --- a/src/pyrobusta/con/wifi.py +++ b/src/pyrobusta/con/wifi.py @@ -14,7 +14,7 @@ def initialize(): ssid = get_config("wifi_ssid") password = get_config("wifi_password") if not ssid or not password: - logging.warning("[Wi-Fi] Missing SSID/password, skip Wi-Fi initialization") + logging.warning(__name__ + ": missing SSID/password, skip initialization") return sta_if = WLAN(STA_IF) @@ -22,9 +22,9 @@ def initialize(): nets = sta_if.scan() for net in nets: if net[0].decode() == get_config("wifi_ssid"): - logging.info(f"[Wi-Fi] Network {net[0]} found!") + logging.info(__name__ + f": network {net[0]} found!") sta_if.connect(net[0], get_config("wifi_password")) - logging.info("[Wi-Fi] WLAN connection succeeded!") + logging.info(__name__ + ": connection succeeded!") break diff --git a/src/pyrobusta/server/http_server.py b/src/pyrobusta/server/http_server.py index 811b3cd..e0cecff 100644 --- a/src/pyrobusta/server/http_server.py +++ b/src/pyrobusta/server/http_server.py @@ -2,9 +2,8 @@ Socket server application """ -from asyncio import sleep_ms, start_server, run # pylint: disable=E1101 import gc -import ssl +from asyncio import sleep_ms, start_server, run # pylint: disable=E1101 from time import ticks_ms, ticks_diff from ..protocol import http @@ -38,7 +37,7 @@ async def drop_client(cls, socket): """Remove socket from active list""" if socket not in cls.ACTIVE_SOCKETS: return - logging.debug(f"[HttpServer] {socket.id} dropped") + logging.debug(__name__ + f": {socket.id} dropped") await socket.close() cls.ACTIVE_SOCKETS.remove(socket) del socket @@ -72,8 +71,8 @@ async def can_handle_new_socket(self): if not socket.connected or socket_inactive > self._timeout: logging.debug( ( - f"[HttpSever] evicted {socket.id} " - f"timeout:{self._timeout - socket_inactive}s" + __name__ + f": evicted {socket.id} " + f"timeout: {self._timeout - socket_inactive}s" ) ) await self.drop_client(socket) @@ -87,13 +86,13 @@ async def accept_http(self, reader, writer): - creates SocketHttp object """ if not await self.can_handle_new_socket(): - logging.debug("[HttpSever] cannot accept new client") + logging.debug(__name__ + ": cannot accept new client") writer.close() await writer.wait_closed() return new_client = SocketHttp(reader, writer) - logging.debug(f"[HttpSever] new client: {new_client.id}") + logging.debug(__name__ + f": accept {new_client.id}") self.ACTIVE_SOCKETS.append(new_client) await new_client.run() @@ -108,6 +107,8 @@ async def run_server(self): SocketHttp.init_pools(self._max_sockets) ssl_ctx = None if get_config("tls").lower() == "true": + import ssl + ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ssl_ctx.load_cert_chain(self.TLS_CERT_PATH, self.TLS_KEY_PATH) self._server = await start_server( @@ -117,15 +118,15 @@ async def run_server(self): backlog=self._max_sockets, ssl=ssl_ctx, ) - logging.info("[HttpSever] Started") + logging.info(__name__ + ": started") except MemoryError as e: - logging.warning(f"[HttpSever] Memory allocation failed: {e}") + logging.warning(__name__ + f": allocation failed - {e}") async def terminate(self): """ Terminate HTTP server and close sockets """ - logging.info("[HttpSever] Terminated") + logging.info(__name__ + ": terminated") while self.ACTIVE_SOCKETS: await self.drop_client(self.ACTIVE_SOCKETS[0]) if self._server: diff --git a/src/pyrobusta/stream/buffer.py b/src/pyrobusta/stream/buffer.py index 8ef43e9..812a1f0 100644 --- a/src/pyrobusta/stream/buffer.py +++ b/src/pyrobusta/stream/buffer.py @@ -151,17 +151,17 @@ def prepare(self, n: int): otherwise attempt to compact the buffer """ if n > self.capacity: - raise ValueError("Requested size exceeds capacity") + raise ValueError("Capacity exceeded") if n > self.writable(): self._compact() if n > self.writable(): - raise ValueError("Buffer full") + raise ValueError("Capacity exceeded") def commit(self, n): """Increase the window size by n bytes by incrementing the 'end' index""" if self._end + n > self.capacity: - raise ValueError("Buffer full") + raise ValueError("Capacity exceeded") self._end += n def find(self, term: bytes) -> int: diff --git a/src/pyrobusta/transport/socket.py b/src/pyrobusta/transport/socket.py index e4d7a3a..2d8dc63 100644 --- a/src/pyrobusta/transport/socket.py +++ b/src/pyrobusta/transport/socket.py @@ -36,7 +36,7 @@ async def read(self, read_bytes, decoding="utf8", timeout_seconds=0): - read_error is set to true upon timeout or other exception - data holds bytes or decoded string read from the socket """ - logging.debug(f"[SocketBase] read from {self.id}") + logging.debug(__name__ + f": read from {self.id}") self.last_event = ticks_ms() if timeout_seconds: request = await asyncio.wait_for( @@ -52,10 +52,10 @@ async def close(self): """ Async socket close method """ - logging.debug(f"[SocketBase] close connection: {self.id}") + logging.debug(__name__ + f": close connection: {self.id}") try: self.writer.close() await self.writer.wait_closed() except OSError as e: - logging.warning(f"[SocketBase] Error while closing {self.id}: {e}") + logging.warning(__name__ + f": error while closing {self.id}: {e}") self.connected = False From 9abadd6d80d2b08a8c2e4e9353d5e8f97d147d86 Mon Sep 17 00:00:00 2001 From: szeka9 Date: Tue, 31 Mar 2026 21:59:19 +0200 Subject: [PATCH 5/8] Create /lib before deployment Create missing root directory for mip packages. --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 9d2a87b..fc8c4d1 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,7 @@ $(BUILD_DIR)/%.py: $(SRC_DIR)/%.py .PHONY: deploy deploy: @echo "Uploading build/$(PKG) to device $(DEVICE)" + @mpremote $(DEVICE) mkdir :/lib || true @find $(BUILD_DIR)/$(PKG) | while read source; do \ rel=$${source#$(BUILD_DIR)/}; \ remote="/lib/$${rel}"; \ From e4bf5216424d21bacd124b31030e900270f2741f Mon Sep 17 00:00:00 2001 From: szeka9 Date: Tue, 31 Mar 2026 22:03:55 +0200 Subject: [PATCH 6/8] Shorten identifiers and reduce string literal size Optimize memory usage and replace ambiguous variable names (e.g. "mp_is_first" instead of "mp_first_part"). --- src/pyrobusta/bindings/socket_http.py | 5 +--- src/pyrobusta/con/wifi.py | 2 +- src/pyrobusta/protocol/http.py | 30 +++++++++---------- src/pyrobusta/protocol/http_multipart.py | 38 ++++++++++++------------ tests/unit/test_http.py | 22 +++++++------- 5 files changed, 47 insertions(+), 50 deletions(-) diff --git a/src/pyrobusta/bindings/socket_http.py b/src/pyrobusta/bindings/socket_http.py index a09664a..799fea7 100644 --- a/src/pyrobusta/bindings/socket_http.py +++ b/src/pyrobusta/bindings/socket_http.py @@ -54,10 +54,7 @@ def init_pools(max_sockets): ) if is_low_memory: logging.warning( - ( - __name__ + ".init_pools: low-memory mode with reduced buffer size, " - "decrease max_clients to use larger buffers" - ) + __name__ + ".init_pools: low-memory mode with reduced buffer size" ) recv_size = ( SocketHttp.RECV_BUF_MIN_BYTES diff --git a/src/pyrobusta/con/wifi.py b/src/pyrobusta/con/wifi.py index 3565082..8755f44 100644 --- a/src/pyrobusta/con/wifi.py +++ b/src/pyrobusta/con/wifi.py @@ -24,7 +24,7 @@ def initialize(): if net[0].decode() == get_config("wifi_ssid"): logging.info(__name__ + f": network {net[0]} found!") sta_if.connect(net[0], get_config("wifi_password")) - logging.info(__name__ + ": connection succeeded!") + logging.info(__name__ + f": connected, available at {sta_if.ifconfig()[0]}") break diff --git a/src/pyrobusta/protocol/http.py b/src/pyrobusta/protocol/http.py index 7ed3b75..0de2a54 100644 --- a/src/pyrobusta/protocol/http.py +++ b/src/pyrobusta/protocol/http.py @@ -34,19 +34,19 @@ class HttpEngine: __slots__ = ( "state", "status_code", - "response_headers", + "resp_headers", "version", "headers", "method", "url", "query", - "content_length_cnt", + "content_len_cnt", "recv_chunk_size", "mp_boundary", - "mp_first_part", - "mp_last_part", + "mp_is_first", + "mp_is_last", "mp_delimiter", - "mp_closing_delimiter", + "mp_last_delimiter", ) ENDPOINTS = [] # (endpoint, callback, method) @@ -122,7 +122,7 @@ def __init__(self): # [State machine] self.state = self._parse_request_line_st self.status_code = None - self.response_headers = [] + self.resp_headers = [] # [Recived request] self.version = None @@ -130,7 +130,7 @@ def __init__(self): self.method = None self.url = None self.query = None - self.content_length_cnt = 0 + self.content_len_cnt = 0 self.recv_chunk_size = 0 # [Multipart state] @@ -310,13 +310,13 @@ def _parse_body_part(cls, part: memoryview) -> tuple[dict, bytes]: def _set_response_header(self, key, value): if ( - key in self.response_headers - and (index := self.response_headers.index(key) % 2) == 0 + key in self.resp_headers + and (index := self.resp_headers.index(key) % 2) == 0 ): - self.response_headers[index + 1] = value + self.resp_headers[index + 1] = value else: - self.response_headers.append(key) - self.response_headers.append(value) + self.resp_headers.append(key) + self.resp_headers.append(value) def terminate(self, status_code: int, content_type: bytes = b"text/plain"): """ @@ -343,9 +343,9 @@ def _write_response_head(self, tx, content_length: int = 0): if content_length is not None: tx.write(b"\r\n") tx.write(b"content-length: %s" % str(content_length).encode(self.ASCII)) - for i in range(0, len(self.response_headers), 2): - key = self.response_headers[i] - value = self.response_headers[i + 1] + for i in range(0, len(self.resp_headers), 2): + key = self.resp_headers[i] + value = self.resp_headers[i + 1] tx.write(b"\r\n") tx.write(key) tx.write(b": ") diff --git a/src/pyrobusta/protocol/http_multipart.py b/src/pyrobusta/protocol/http_multipart.py index a8e6e96..dc95393 100644 --- a/src/pyrobusta/protocol/http_multipart.py +++ b/src/pyrobusta/protocol/http_multipart.py @@ -14,9 +14,9 @@ def add_method(cls, func, method_type="instance"): """ if method_type == "instance": setattr(cls, func.__name__, func) - elif method_type == "staticmethod": + elif method_type == "static": setattr(cls, func.__name__, staticmethod(func)) - elif method_type == "classmethod": + elif method_type == "class": setattr(cls, func.__name__, classmethod(func)) else: raise ValueError("Invalid type") @@ -69,12 +69,12 @@ def _start_multipart_parser_st(self, rx, tx): if (start_delimiter := rx.find(b"\r\n")) == -1: return self.mp_delimiter = b"--" + self.mp_boundary + b"\r\n" - self.mp_closing_delimiter = b"--" + self.mp_boundary + b"--" + self.mp_last_delimiter = b"--" + self.mp_boundary + b"--" if rx.peek(start_delimiter + 2) != self.mp_delimiter: self.on_client_error(tx, http.HttpEngine.MULTIPART_BOUNDARY_ERROR) return rx.consume(start_delimiter + 2) - self.content_length_cnt += start_delimiter + 2 + self.content_len_cnt += start_delimiter + 2 self.state = self._parse_boundary_st @@ -82,7 +82,7 @@ def _parse_boundary_st(self, rx, _): """State for parsing multipart boundary delimiter""" if ( rx.find(b"\r\n" + self.mp_delimiter) == -1 - and rx.find(b"\r\n" + self.mp_closing_delimiter) == -1 + and rx.find(b"\r\n" + self.mp_last_delimiter) == -1 ): return self.state = self._parse_complete_part_st @@ -96,10 +96,10 @@ def _parse_complete_part_st(self, rx, tx): next_delimiter = rx.find(b"\r\n--" + self.mp_boundary) part = rx.peek(next_delimiter) rx.consume(next_delimiter + 2) # Consume leading CRLF - self.content_length_cnt += next_delimiter + 2 - is_final = rx.peek(len(self.mp_closing_delimiter)) == self.mp_closing_delimiter + self.content_len_cnt += next_delimiter + 2 + is_final = rx.peek(len(self.mp_last_delimiter)) == self.mp_last_delimiter # Validate part and content-length - if self.headers[http.HttpEngine.CONTENT_LENGTH] < self.content_length_cnt: + if self.headers[http.HttpEngine.CONTENT_LENGTH] < self.content_len_cnt: self.on_client_error(tx, http.HttpEngine.CONTENT_LENGTH_ERROR) return try: @@ -115,21 +115,21 @@ def _parse_complete_part_st(self, rx, tx): self.on_client_error(tx, http.HttpEngine.MULTIPART_BOUNDARY_ERROR) return rx.consume(len(self.mp_delimiter)) - self.content_length_cnt += len(self.mp_delimiter) - self.mp_first_part = False + self.content_len_cnt += len(self.mp_delimiter) + self.mp_is_first = False self.state = self._parse_boundary_st return # Process last part - rx.consume(len(self.mp_closing_delimiter)) - self.content_length_cnt += len(self.mp_closing_delimiter) + rx.consume(len(self.mp_last_delimiter)) + self.content_len_cnt += len(self.mp_last_delimiter) if ( - self.headers[http.HttpEngine.CONTENT_LENGTH] != self.content_length_cnt - and self.content_length_cnt + rx.size() + self.headers[http.HttpEngine.CONTENT_LENGTH] != self.content_len_cnt + and self.content_len_cnt + rx.size() != self.headers[http.HttpEngine.CONTENT_LENGTH] ): self.on_client_error(tx, http.HttpEngine.CONTENT_LENGTH_ERROR) return - self.mp_last_part = True + self.mp_is_last = True dtype, data = callback(self, (part_headers, part_body)) self.terminate(200, dtype.encode(http.HttpEngine.ASCII)) return self._generate_response(tx, data) @@ -145,14 +145,14 @@ def apply_patches(): def new_init(self, *args, **kwargs): orig_init(self, *args, **kwargs) - self.mp_first_part = True - self.mp_last_part = False + self.mp_is_first = True + self.mp_is_last = False self.mp_delimiter = None - self.mp_closing_delimiter = None + self.mp_last_delimiter = None cls.__init__ = new_init - add_method(http.HttpEngine, _multipart_wrapper_factory, "staticmethod") + add_method(http.HttpEngine, _multipart_wrapper_factory, "static") add_method(http.HttpEngine, _start_multipart_parser_st) add_method(http.HttpEngine, _parse_boundary_st) add_method(http.HttpEngine, _parse_complete_part_st) diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index 13f5bd9..ee04f02 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -162,8 +162,8 @@ def test_routing_unsupported_method(self): self.assertEqual(self.engine.status_code, 405) self.assertEqual(self.engine.state, None) - self.assertIn(b"allow", self.engine.response_headers) - self.assertIn(b"POST", self.engine.response_headers) + self.assertIn(b"allow", self.engine.resp_headers) + self.assertIn(b"POST", self.engine.resp_headers) def test_routing_options_method(self): self.engine.state = self.engine._route_request_st @@ -180,8 +180,8 @@ def test_routing_options_method(self): self.assertEqual(self.engine.status_code, 204) self.assertEqual(self.engine.state, None) - self.assertIn(b"allow", self.engine.response_headers) - self.assertIn(b"GET, POST, PUT", self.engine.response_headers) + self.assertIn(b"allow", self.engine.resp_headers) + self.assertIn(b"GET, POST, PUT", self.engine.resp_headers) def test_routing_get_method(self): self.engine.state = self.engine._route_request_st @@ -484,7 +484,7 @@ def test_multipart_receiver_complete_part(self): self.engine.headers["content-length"] = 1000 self.engine.mp_boundary = b"test-boundary" self.engine.mp_delimiter = b"--test-boundary\r\n" - self.engine.mp_closing_delimiter = b"--test-boundary--" + self.engine.mp_last_delimiter = b"--test-boundary--" body_part = ( b'Content-Disposition:form-data;name="file-chunk";filename="upload.txt"\r\n' @@ -500,7 +500,7 @@ def test_multipart_receiver_complete_part(self): self.assertEqual(self.engine.state, self.engine._parse_complete_part_st) self.assertEqual(self.rx.peek(), body_part) - self.assertEqual(self.engine.mp_first_part, True) + self.assertEqual(self.engine.mp_is_first, True) self.engine.state(self.rx, self.tx) @@ -515,8 +515,8 @@ def test_multipart_receiver_complete_part(self): b"Upload content", ), ) - self.assertEqual(self.engine.mp_first_part, False) - self.assertEqual(self.engine.mp_last_part, False) + self.assertEqual(self.engine.mp_is_first, False) + self.assertEqual(self.engine.mp_is_last, False) def test_multipart_receiver_last_part(self): self.engine.state = self.engine._parse_boundary_st @@ -526,7 +526,7 @@ def test_multipart_receiver_last_part(self): self.engine.headers["content-length"] = 131 self.engine.mp_boundary = b"test-boundary" self.engine.mp_delimiter = b"--test-boundary\r\n" - self.engine.mp_closing_delimiter = b"--test-boundary--" + self.engine.mp_last_delimiter = b"--test-boundary--" test_callback = mock.Mock(return_value=("text/plain", "OK")) self.engine.register("/api/test", test_callback) @@ -560,8 +560,8 @@ def test_multipart_receiver_last_part(self): b"Upload content", ), ) - self.assertEqual(self.engine.mp_first_part, True) - self.assertEqual(self.engine.mp_last_part, True) + self.assertEqual(self.engine.mp_is_first, True) + self.assertEqual(self.engine.mp_is_last, True) if __name__ == "__main__": From dcbc484594c8648e68be932195a9d882b5824773 Mon Sep 17 00:00:00 2001 From: szeka9 Date: Tue, 31 Mar 2026 22:16:37 +0200 Subject: [PATCH 7/8] Fix config parsing on ESP8266 str.splitlines() may be unavailable in ESP8266 builds, (depending on MICROPY_PY_BUILTINS_STR_SPLITLINES). Replace its usage with manual line iteration and rstrip() to handle line endings. --- src/pyrobusta/utils/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pyrobusta/utils/config.py b/src/pyrobusta/utils/config.py index ad37cdf..b307d2b 100644 --- a/src/pyrobusta/utils/config.py +++ b/src/pyrobusta/utils/config.py @@ -45,7 +45,8 @@ def read_config(config=CONFIG_LOCATION): """ try: with open(config, encoding="utf-8") as conf: - for line in conf.read().splitlines("\n"): + for line in conf: + line = line.rstrip("\r\n") key = line.split("=")[0].strip() if key.startswith("#") or not line.strip(): continue From eff6f85527e334acb64f09977e5eaba1aff9f518 Mon Sep 17 00:00:00 2001 From: szeka9 Date: Tue, 31 Mar 2026 22:32:01 +0200 Subject: [PATCH 8/8] Bump version to v0.3.0 --- Makefile | 2 +- dist/pyrobusta/bindings/socket_http.mpy | Bin 3138 -> 3001 bytes dist/pyrobusta/con/wifi.mpy | Bin 566 -> 564 bytes dist/pyrobusta/protocol/http.mpy | Bin 6685 -> 7369 bytes dist/pyrobusta/protocol/http_multipart.mpy | Bin 1827 -> 1832 bytes dist/pyrobusta/server/http_server.mpy | Bin 2042 -> 1979 bytes dist/pyrobusta/stream/buffer.mpy | Bin 1577 -> 1549 bytes dist/pyrobusta/transport/socket.mpy | Bin 794 -> 770 bytes dist/pyrobusta/utils/config.mpy | Bin 734 -> 856 bytes dist/pyrobusta/utils/helpers.mpy | Bin 0 -> 241 bytes package.json | 6 +++++- src/pyrobusta/utils/config.py | 2 +- 12 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 dist/pyrobusta/utils/helpers.mpy diff --git a/Makefile b/Makefile index fc8c4d1..4486799 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PYROBUSTA_VERSION := 0.2.0 +PYROBUSTA_VERSION := 0.3.0 DEVICE ?= u0 SRC_DIR := src diff --git a/dist/pyrobusta/bindings/socket_http.mpy b/dist/pyrobusta/bindings/socket_http.mpy index 95d8afa31be09689dde2887c4f35eae0b8555624..b56ceb730aadb38c6d71e2fe4c72dd2db89ead53 100644 GIT binary patch delta 425 zcmX>ku~U45Fbl7#US?iqNqj+meonE~WNj8rF$PvU&%EN&w6x6R%+$ORh1}HK{G!Uq zOIUPyRZ$ebWRYePvzjc(D$Xqy0Mf6JSd^+T*_u^`NnCGo9;>>DuwHyoYGO)!Nq&4% zX_}Qnbaid)Y6moa9tA%~}i#8oFPy@-b7 zgal0$6(vo1r)X6T9U#)&Afj)^5Eqyc8Mr}0#nW^0A`TS^<#3_c`z|pDJ%ym=vx>-W z1eto9BZ9GJvI%E|+$4#IPF4mH4edsn%1S_>p{EHn)lpZ|Q(M#9aPmq{RYv#8=Q&-O z{xeJ#<#H6NIqYc!HH|+j6rxga)es1L#Ic%d9Jf>@R8WaxwWX#AQ9z0#2#)JqS}Tbi&Niij zicAcM1>MBJ0!znGCMHCg*@2~ELE<0iz|@PU4siO0`yRj7r?I`v;3S{v+`sEuZR{`m zeowI-+c$cy+wtlYcywEaCbkGRJq%s*iD7kY?D!t|E(C1UG08e6uokphn0S%sP0jJx z-?oNV(8XPsY(dK^6#^irv=>b1}apuTQF<7Lg;kyR2UWZ`=4&G#rJq#wNB{Z^xa3WYp6r|MEsW`zdAWF_)&b)&XA$P~0CdLmI6 zg=)IKzfk+4jfBimA@_k%-bm=&bNW7^=U(@Isk2jh|K&6i^XwHBlt)GxX(hCx&{Ii? zTcTH!i`+B%B6)3O>#H^y@+5@ZN5=Cbxt!JisB%T`?*jNBw52xLQyF=L8^s$doo*d!uk#sE_l2?S2gPOItzlN${L VxP%+z6V!#6gqVRY*Z@(N3;^`SR7(H= delta 331 zcmdnOvWlK8}wl%mw)Vg`ZCwB-D}w9Iq{R^IZ=w9NQ|#Ny)e z{Gt>FR*UHHOkKCkSOwqA;^NG_bcNtxPZxcdypBR~c4mPBScyVrUS>&VVoqjNVo7Fx z9s{c+OsijN3DB@?h3Z;`wEWV%6h#JBWhIzoxR0Zs0?_un)MSu0h2qlWgsBmPC!D{z(8M9h*f~ggzU}((&a^~H=R3Y15_O_!$95Wqy4bRwrX^XH9fwqolqFlDWIg;y z(j1Rw9JfsxB}~z#MO%NQA1P4ac%+>6qd+3bPK)%TMLuk~aRL-bTcAaODn*fx0`2UP zvSqiZsJoq=nVp@PotYi-hk;)|Q8`e7waK2&H!Gv9mgL;R?8MCU3yJ9b#L^Vjw7fJu zv)DeBn3+#3EWVleKyO}1EY4@!@s2&M;#_hmkz9(-B$81vxrDVYB*cqRacVhvKDr1Z zvh(;GX8ZJF6baSjMHdqb7ZY(T&DCZ9jX#wQ>At8l&n2Uia|_Q;$K#1))>`({GQ-qd z_Tw^ZBOQgcnmAZeUjnj!D+?TPM;~sLh-;fg7nd-mh@PLgGBY;;XoQ)VpN}Sz@%g!F z&=fG19ZR!Mm7mJ~p!_E$gJx`cH`;wgN)y#?yt&3w zO;?YP@EB2RTn?1cwIe)E)EO@VSU1A!i28tzuII~WV?->MG0aeVy*^|-PhTQx>B|uj zVd}0^MNA#Se81oM8(sbNrc`ApLs&x@I+WRE6|CByUqFD6pgmfIMqlw@w41}}eojZv6AiSTE2E#{ z%IOz4Jxy>G^bl7`$2kN2B3DJfWPHI$7>A5;0FnVDugVAS{B8XNFaHsKQUJv75pjE)QALf-X zI$$)wcmT#K7!Sf&4dWr;tAQ~D^I8}W^L6wQPTQ)U?nxoB4fIi7BIp6$NFV1(dYCuS zBRoZq@{RNfo~Fn6UGyMtrcd&_>2bb^HuKH&8Qwxq@GZ2+x6(1bjh^HgdYa!upXFJ4 zis$GgZ>1Bkvki8fjfnP)+1R>E#!@fXtqH3QokUzQnHBTyh+=N?V2W942FT(Cs0Dyl zc-r7$Qi^#GJS@OmM7&1)N^iHyxU7@QT2pVuVlG#ovz>F~D4Sxk2k{T{C172l9Jzc@ z7y0ayjl5!V-v2}xB$qjoa=|1BE>0ALBY-dgq?p{4Cnu?ScUcf0hj==~zKwfjU2E#y z7&FOWxRPTq-I^-m^09-|g6x2gGxeUAtf#CtOts;`vT;?sHWjh>JQcHOABV;(_sHS(Dt9VX@K0G3e2S3+rm*$a-YN!^M0 zoxTo^vsp>GmNalw0H=aMEP$`&s35TqHYMe{d;pL3v3}jgEn*+#&qoR}m;|QWzS-SP z?7Mway)T51i($(#QQq$+_RICMA@xUxLOlU?tsx(bis_!=ddh*$wl(z^M?0KKmT72` z(pWEb0AW*qcQ~wojrP^9<LCQuRD?Ar~ zsDYTNKU>Yyj6e&yX2tGue)`7m3lIWQ6xNH(*p9=s$zO-Qy74gzKKWy z{C0RigGvmyZ{}A|guf@{QFQ$xRJ|xWR;q@T;o-jO4|8lvlJ1@z4+kRRtNSllLMqR~c7MCqrBF|EW(fJ5u$jtfTZXyaL0tG4WqJ-nzHlM0DHkNV zG-Wq#6}FWngiT?%sQQz{Gw9TW@J%ryhRY)z;x+6Pdl=kBuGksY=VLuKo7W|+6{G3u zm#OgR*m^FX&!7Y6nzEk}w1m8(P=|66RSSZRoSNA}s@y89x^-g4dC+o1#vqJMEBhG7 zv0hhBF*V;8PQhCXi1b%u>#0qlR|P~~-Z~LJjwl`om2nMFwKTnI$m5nC8EcZMm8WAN ztCe#>k=UJsQqrUfM-}P_*Q^G0;|Q>DEdWKtZ!y2e`J=FRqfD<1#6otLv(xSj`s_}p z!|Bgay+Ld|NA)Qt3v2-Z*IFDEYQJLYS4>So9Uvf8J`tWnWQY@ut&Nm-sSr8EDkOac zXq3LD<^Y}w&yTGmLq<2y8zTMIj+KRy0H!#u!jS&rV-}0mIFcr2cTq(h*z=hrsJAnZ7VM`F4m_|J?vEi zXzHWNJXj%5vE(pXvGNK7DFECi;6{pT0ugyc(&*nT(W%GjqZ;{*5*Z!gBg;={sJG~yeSAoM|en~ ze5=4wEOS8|uIfS>xGajPRVrS%kbM8Po-cuNube zXG-S$j`6WicnlQY1OERrTqIq6-X)L6?{TpPm&ePt``pf~+(=0WY?!GH z>hhmt8%8K?h+9Qr7`I!r3_o zT)IJ&wJ`5t<^D(!JgP<{&2~VGK}@)pdbrCV;Et;>FCIF~LNI}V9@`Mc3X}~d&{5ei zGY3!t#@*plzy!7ny?~k2Fg*aBeE>}%C?W#aw$YbtQqgE+Wg9X@08#%>`%=gNJ6b}( zGvwCY7fz$~-2&$f3kY)Pl~riU9_%+#j4E92`JkL$(pof^?7m^LVG@;8^QcD?Gv@B5 zNiD+m+%Wstz;<$M#}?etJMv>Cpo6w^N0?4)A@hpAt97~FADw2s?p}x02EsiyYoE(wA7DM00UU~a4Uu;k z214_2D~c>FTA__}0E-qsDHNF~0`8zdVukZIpcI{k=BTa&bZ~K@00E~!dc*k_Lj}U( zfAFG?aVL9tqkMn}6&;m#WaC3ok z-536vf;x5^ik8Db${Z46I?uVGX!HZ;pvrl+GX)Z;2dx4lW%C@fo%QNsCX)M%JTzgSFpg z{!t!I!Wlwkk*znPjK}BTSi7Sa-ox!IH|}=2T)lZ}j`KlrmPR+o>U^mV2k=`8wHLYw z=)1+92C`x`AffwfxR@Seq-^_K=mkV6U(WQ@YOT#YnEO$0;~2U8xiAQ1=yvA0c7(&v z^n_6d)d3w25{F+KMSV_5S=3!2E8hnr;S$vx0~dgH`yVA``zmqaTtEljcx-173*WQ+ zU1XvZsxLwRpmKf?6A@$0#H?H`X)777_mgE{t2z delta 4134 zcmaJDTW}NCbyt2O%VxD&FY*Hrd-axVi?!JBXc9%%y#bhVNp?M#})HY9C-GD$PwDNcTpe&x%V&ZIN_=(#KL2%V|r z-Fsg5-1ENsmwoRBE4s^xoyv~ZH!EUqUjJx|Z6+~0JvBL-$aIh`7P~YxnM_P3W9Jf+ zr<3C`X)>94hx}D0WO$K8nX86>W@bv>%j_!Ml^HB;-CiG?Nl0_C>CwyQrbfpITWoZC zI+mCmo1U70eP5Ig8I?va;FaXfi>R zRpIsGY)qOO1B`lXRi=E8O#iP{u;Ki4?BvwN$+6Ly%S2f+{F2bWMYOWV}L82o0^@NJRN&NHZm0@d>~L>4li48CxNkGc@3ZGt?=Y-R=5mX z+6vKW>dJh$<*m%P@%;>6Rc9(UPMjY-ooG2Toj9F|RL!Fn!e}Tbn~8FQC^s}W6J_S{ zWO6z-o0yqPjDdm4@!8CutDZOCuv_cNc*yVXI(IgHHvZoJOtmQ>ALR+kXg(V(VU0(5 zlBzPF1F-5SZ=g&;18d?-*sW2iR3V5;<6?El9B0o{Ms_kPVM^7ar9i2|l($9qpA4qO zx>Q9dz11E{Gokc0yJ*+{0wM;)bnQ)Mky@;Iqjo$r9$&0jtdtk6i#3aM+W4Ajkxe(G zE5<^_UwkZ-t_F%}ES_#oSEfB_ZcIPk#7-iDo$?W^3z6(0WMEHG)ht3K?0!_rPNOom z29>iBRKXrcmFxwyg`F`^n`_Jw^KtV9^NhEgbs!^q3{|nC$Rx8dRLu^e8g>?KW&3%8 ztu-g98ZX(`N(M=Gm-!-ZfYk$Q39Mf8#VF>L0^AI=GFW+Yk}rqV4r>LhL0Btc-2>|u zSOt(X!g>JkDp+6QO>7uZSWPu6x1}&p!;1V?wwPgJ z;~r0^yBWE%45wO9pG7p|1Vn%cCi3DTAXorW zEiT5L)pFhvfgFMY3Ua?DJ@Tc-)NkXqQ#OKBvo^x8qDiE3_y9Am)WfeK^*cXpVs<$Q z%|Qw!Lz;9W6s`CEJoN_~;!x|pgPvv|@kweW{838P`YGSKct%^;q_3z!^1c|$GH&`8 z^sB0-Y--mwn+^R-LC7T>u3h_XCEN{cRg?QMu6(zm6?V1VktsjZlod$nhHaI!H}AMA zr#?(ocpaR_;b>`bI8~-4O9eIsay2JE6ldi%AW*v@25UL_fhgtS)vM;{E7>5qsch!q zSm_BG#i$njT0BTAM36{mZb=DvYj}mcn)B#DPK z#+?I~!){G_jhXQZ^NJz$ecaZB5=}0b!@t|r)#Rd;60n|m4l!VjpIFsmoryJCDV2F5 zKN5tjLQajS7IKQ%Jf!?(+6cbnVFRlOhJfMjy=7*vKrj?M`}BgjyR2IFuZvyutG6*H zNZt!~>rjz@?{;qaSoj?|hqM33ka0q(PyMS;?d$6_ev;)<@`KYu;b2sje!2HTU2M2xti`lG8=Pa|In+X@#N{v#w*1Hs~%sE83|t9a*$XkFRQJR4tK*;)t#&Yw64~ zT>|mL18DA2&@6%PxYeSPyfEEn?Z!UQOJ{5z&aX0U+%*b#+D{C5yg`X`R1e56U9drc zgDeg`Hd$APxxJV>SLD_{%RP*_SBhM#AV0G?irJ|m8z=p->V?4rYzJ_`(Cao&!Omn+ z;88@u%0fkAJC0yguNPI#KdlOL`!ILOCKsHz{51QGP~QveoMHiicWrJ57w~vFr_X7# zH63w!T^#&-vP=uEIheCB;Gz~7$u|{9X2gz#>#V(cUhSurVbCOT>!*!Nd z`e7Jx(BOBMfgoGA(^r;*q_QrGlt0|7GTvp7D2yh7gnKW)0Nd*IgyK64`l+|zBQ%F< zIaVO`6-dxHxrI&ai)8@csM7V&fNh}Q$_KV4jvMeI-FB$wfmLj~9F^KBm<0v%@qrBy zW`H(+BX&dRvGIet@h7nAbV2oyu38y1n}+9>)*!%^{i>y5FTgAa;zv z1M>p9yzMLo;W@tE7B7O4b@BbDC2Mp^@hzYgMa2uCXb+&EbrrxcLdecEL7u=Tz}q-Q z?ur&*QNut~0`jmxh@YjNoYDidZVm849sxT5NCSj$O&rKG93b%F;UKIwfL?)hTeuyd z6OW-04C$&Lxi}9>u!oH@ytZS*#RQQ4kFCX|<2~@yz!UKrHpJt2f18LLVG%>FGIbd~ zRUV($Q>Y74Y5%yCJ)?iPG%DMdet@$iEe9Via*33-w$`60)X0x5T#FZGr=CWAWIVv- z{WRhJqoR)K;ZiLldNurpnf;s`(Y4$nY(kUU5$x)i;QU^%)8}#79iZ0duy=ZV&Th^} zRD!M8jmWwz9)z_S3q=rVjo2jM2iw_`E%YvD7rQtN;( z+qO>igD3vO9%Oq~7SzIpln3kc$O-2k1p*X8JEj2Gt}P1&DUVzm;`b@&(0kC1u1Z>A zv3d&(eQ3BYkn7Np=TX5t*=`yAw5T6>H}7l~R2E9>k;@OoJ`g$SsJxBMmvQOb5lY}L z2kH|=MLWm^>hn_T=klLOv(wMD3ovF3#7^ z^)+*7$m{laB0gd*2eZc~kg-m5cNzINp#Q^rmQmBX*qIj(=9w|LcEgAAj2mBo;Rcvk z#Yse0qUOSEE6kf>UFDbrYJpKE_uoT4rU`D*PuorU9ewJyVE8OjJ3ycPM(lw}0@wB_ z6u>m{M0*&23haifnl6{`1j!ry_{J`}gTE7S7(Rg5!R*(&^3iS9K`Yif)ajrBqBTG( zb>D~)kkM@cwea2UZlzu+)F8eLHN@)~8f>UJd0DA=@m6nCS~yIZw%+QCw{(I_<{;UK azso4|x)@DSw3lqKG}0e~P6h%ImHrFdz?MS* diff --git a/dist/pyrobusta/protocol/http_multipart.mpy b/dist/pyrobusta/protocol/http_multipart.mpy index f6fac8699a4515334b0e82b023d67ae2adb5e70e..03fdbedc412b020480978add600f8e9b3e3b43ae 100644 GIT binary patch delta 302 zcmZ3?w}Ow=myJQ*ej;luKTmN@J`o*Ei$ zG+{Jhj&zFMB*1agB|ykw{ClSG$1{4tB}nW6UOV#0zg@1W}vv!wEDp0 zP6lf=E(=Sm$-XT8%odi`lkc;b0%@zs;;cc8Hj}ehOM&DARt0{W4Z=buj6iK0!iB|! YY$o%wNrJ?Lk;Tl}jx*U>Ok!6C03rffssI20 delta 295 zcmZ3%x0sLBmyJQ*b|PymOOs|J_r$`5+#0zB@yR*)#hH2Olcg9<_*Ihg^GZ_lO5$@; z^U_N)CPy)LDoO#>q-7Qrm&6w&7L_n?6lYeYGDrXga}pt9lbBj2cQ8FQWZ!7QXu=%n z6uU`)|J zGg*d3O2%r73FCEVfz2Y?%FJerkxtX<1Cu)$tkt;8%q=DdvGg;WnOjbN!eR=fEhfvb z1~FPq&SxzJl22I`%&j&E3z;wiwQUF&78io>o<>QC2W*f~+#oEmK|(Sxd83FJNJbb* Q-DDfK<4o3OlUdo70r;6zz5oCK diff --git a/dist/pyrobusta/server/http_server.mpy b/dist/pyrobusta/server/http_server.mpy index 4dc378b110224f800118c2648bb7cb033125d71d..467a75e11e9a01b845443be387cc014c2eb736a5 100644 GIT binary patch delta 1147 zcmY*XO>7%Q6y6`N<2WXn%uI%jy(Eh_{>?f~Hrpj}X)*RTNz*iR11K^j)X~P!NNyb^ z1|dP^Ei$wbT%z2O;KFt79FX9E(s1K~#G!CNh!c0<0w>-!$pMWt^WK~9y*J-C@86+s zhr&0)-1u7bAe;_`-f_7DJ>9(}3!(7dUgL0AxE;ksb+5I#+p2FJ{wOrfSW*4pzPh#B z+}WvbaS>^HYI=G`8ZD~z_qJUC1-Gs3yY=R~tpK^lN%7+0ftctYD5{&TTi29{RoA=bWH@6#*T7U#jf)1_v?%nNsn^2B>*pOwurl#lCbX{-kf9N?0fe9WG zQ3y$SF%ecoCMqEoQY0o-J=BLHc_GoS^szoA!Xnj=P&Cgc29zj^f*k|<870QrF>nUK z8Bzw>AlL}(VFfW%ea+wyHU)+ao{c)L@UW#C_|Z_Uv^tTh>sl%`VLuw^40Pfh)QJz< z>?*=+0TGQu6pbJ@DY3;#jw#8nB5Wq<6)p0v>qfA@zrrIf8RWB}P(1b4& zeB%MeBxMS|oW_GFWAZd>aVYnTy#Voi*@k`6mnj^&FON9B%p7e5*++-GvfU3hyv^pQNAI}f<$Qd_b8A{AGn-FdxSG?GnwDy=FL~$U<0!odJ%Wl_Uz?8M zD6z;377vwhjLv~TU^&PC|A04O+~*(nXt75wS^ROAzUY(7KDlBERUGn(;S=H$ie7?j zG~YElB1i+@15HZUMlT1{mbG>4{ARy-at%Yc4AT;!V{IT1TsqE4R@_+THyrol>=z}u zvf|yZ>B-qtc5>phh}IwA%B!F}F!x?RRUgbs!6(dKhwZ6$!FjG;@PhM+dyqwuQSZ|l QmM}Is8nap)KnJ``eMf>9UY+y}9#GrA0P!%8K)mn|Ab8@56SqT^rR#IgkMFtXbMFtqe}mpN zFEyhDo_OO=a&f==u14#zSnNF%(|c*J5H z@5ziEPOw3glxZ%dP$>PYdJO`Zk_v0q4o|>u)(#WR4o_MU4wJTw2c!_)zXFzn|I3UF}uQ!jWULKUS=fMKQevN8{(Y0 za%f}ni&}Gra&t37EP&(iZ-s$5Y>po)^k}pVz4U#LWo*14JNr|y0Zf4NPnW;y-PAur zCqfbm`D`T;(R76N>Z%7b^ZP`3IR?5eMij4&9DLd5?*w! zi5=%&1}Gq?9XZf@|5M*)ecTf3!aGCaafK1@)?0q<7ZFnQ4;y5tuLyqo5eE4MHG0? H@Z$ahCUh<~ diff --git a/dist/pyrobusta/stream/buffer.mpy b/dist/pyrobusta/stream/buffer.mpy index c17d5acdf5aab11a026c2ec41cf81a7722f77fa4..1117c231d3e04608b82aef137cae3937e01562a7 100644 GIT binary patch delta 51 zcmZ3<)62u=%f=vY&bpE9E2EI0b7Dbaa%M@TLTW{FYHCVq%4SWber86N$vau)f#glr F004Mx4+8)I delta 79 zcmeC>S;@oZ%f=vY&bE>5E2Ex#P-+wib8Q_RjNX2MRICtO0hz6VnJeZW=SOj hE4Nc=T3Tw6LRx7~&SqYwer872$#Yrdf#hD+007or7?1z} diff --git a/dist/pyrobusta/transport/socket.mpy b/dist/pyrobusta/transport/socket.mpy index 18a1dfdf1431f7a0b3a5235b18b039af4303a6e7..3389bfcb4debd7f29af9bceabf5b01adfe6552ec 100644 GIT binary patch delta 203 zcmbQm*2K2Kk5P}$N}(tK)rD9^goUJ(8AU`k2=kgTewy6KWR=<` zanngeT&}@5!B|Nn%oeJK38u!3G15bHgRp>zxEVtvh?X#8hzrb!^im5KQoipJf6!AD is6`TJEHhZkMl+^QKs|wh2Q?UDy%=KyHc0RVCIbN9q%>{- delta 189 zcmZo-o5i-lk5N-HIygT$JGI0qu{bqWp(r&mMIo&yKUbl;mVs3fS#EM7qd31hvWSA~ zL{xVv5E*QGm4085au;woSe*LCDWR5(Md#H zuE99LKuIIa3ao$$reF_~q?U5Hkn(+(n1h}|KsiZeX0Y5wGbS^}$iTpZ8jP`CjIjY5 IB=`c80f;6uDF6Tf diff --git a/dist/pyrobusta/utils/config.mpy b/dist/pyrobusta/utils/config.mpy index 9f08f564bd2591a93e5eb48accf8a98d7e7af330..5e9b55a70d436304648b2fa770b0be735657ead4 100644 GIT binary patch delta 645 zcmX|4&2AD=6ux)pP?!Q<#_Lpt!ocNEY5*yjsIf+wF{Q1Ij7_Z=8=zBciTnj<(z+04 zqDB)F!_u9P00kcatxw>_F08L$y3)jjLu);Y^W}Vh=aBwpq?e4q9qQ1YDQBfxFXv0e zXN7Dn-`oPWtwO0*s5bx;i_CPwkqrCa0L-ea5w2dyZ)BBf2q-U5ukGT^Li03$ZbqHCS;yQ93KZ9V|Rz)@(ko`@fO87 zWW}K2IAT2BHlGW`x`aNE(r5x(;-H7E0!TmsDm?|NS8tn~!M1N2bhC@hkfnMF7{$qi z^49d*aM?!MmvTIb1VWLR_j(}62ky;>R#WdG>+7O32qiQ)_Zkj+*+mt8+QW9l3Ocs1 z6`d7edj(?weD33Oh!dbVw>LV)WD@V4=<2bKF`5*hzRwGAzc1|!q-~bz4-(XVpXI+x z4_O{x=nHW5i}tI1Ak7b0{-b*IAeq+ri#nef;XudapXc)9wFPFC^>6%;#LNO*la-gL zS>EAkOSxP8w4Zoo6849>dhJ(HVqId2wIx92{-i6#jJ|XQI2`2zqqwZBv3yTGT9FT> MrDNZz<%CxL0++z5H~;_u delta 475 zcmcb?c8`_SmyJPQaU!cC3zvbO(L`sxiH(wijnyK>1v!}|IhlE>#SH9C$`cpJPdx3& z$CI3&mzJ5%(3qRp7#uiRmNBYcs-UtcKdH31BvBzJGfAN!F*!RiJykEOI6sepm9IEI zIXkr^J~y!<9;lU}rKCcMF(E-kStZ%oMF|M{IXfijTyu^j2Okl1CyD$>pgxlYjDUk$R#K! z>*?zmC|Ri~sQG(pgnLY86q4N_z$w6G(!}WA#CUL{2)7x72#)}-8DqS6(VOyq4JGjc@Y5tVKYV`DPqQWWV1`*7MHxuLeh6# zB82LtMMO87F@_&)`q6OH#dC|0^w!2tVNL-;(jsC&@kkK81xP#f)Cb1IdU1sd$=!Ex zj`U)T4NPwJuvHM3Z!k}AP|#LYRti+sQq|Z1H1@Kyi;$$VhY*JeLu8P+qz4z!5YY{0 hj0Zv56+qeplVgRXH#U9>4HUoU;tN&?Qv(rC1^_*wd`bWS diff --git a/dist/pyrobusta/utils/helpers.mpy b/dist/pyrobusta/utils/helpers.mpy new file mode 100644 index 0000000000000000000000000000000000000000..19f006a38bd046d5e783068a3be696d5a4ff25a2 GIT binary patch literal 241 zcmeZeW02=#v?(r1)-R|m%1I&*3U@IDM&3U)+?xF;O9wCElDm5!Z*X=NU`^I$ zIJiNCO@Q5uG2UDG&?aSuyDm;nrkeyfZn}7HbaC4#!fD13>Eymqglm%-W2BR`0J{kH jMl;64LQEzMM>e~JY!TrBX?AhhviVc;76JB8ASxLEcS}LY literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 30a9089..ddfce0e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.2.0", + "version": "0.3.0", "urls": [ [ "pyrobusta/transport/socket.mpy", @@ -9,6 +9,10 @@ "pyrobusta/transport/__init__.py", "github:szeka9/PyRobusta/dist/pyrobusta/transport/__init__.py" ], + [ + "pyrobusta/utils/helpers.mpy", + "github:szeka9/PyRobusta/dist/pyrobusta/utils/helpers.mpy" + ], [ "pyrobusta/utils/__init__.py", "github:szeka9/PyRobusta/dist/pyrobusta/utils/__init__.py" diff --git a/src/pyrobusta/utils/config.py b/src/pyrobusta/utils/config.py index b307d2b..35dc2ea 100644 --- a/src/pyrobusta/utils/config.py +++ b/src/pyrobusta/utils/config.py @@ -6,7 +6,7 @@ from .helpers import normalize_path -PYROBUSTA_VERSION = "0.2.0" +PYROBUSTA_VERSION = "0.3.0" CONFIG_LOADED = False CONFIG_LOCATION = "pyrobusta.env" CONFIG_CACHE = [