Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ stage-example:

@echo "Copying built package"
@cp -r build/pyrobusta $(RUNTIME_DIR)/lib
@cp -r build/pyrobusta/assets/www $(RUNTIME_DIR)/

@echo "Copying example app"
@cp $(EXAMPLE_DIR)/app.py $(RUNTIME_DIR)/
Expand All @@ -162,6 +163,8 @@ stage-example:
@cp $(TLS_DIR)/key.der $(RUNTIME_DIR)/

@if [ -f pyrobusta.env ]; then cp pyrobusta.env $(RUNTIME_DIR)/; fi
@echo "http_port=8080" >> $(RUNTIME_DIR)/pyrobusta.env
@echo "https_port=4443" >> $(RUNTIME_DIR)/pyrobusta.env

# -----------------------------
# Run example locally with unix micropython
Expand Down
8 changes: 5 additions & 3 deletions assets/www/examples.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ <h2>Simple Server Application</h2>
asyncio.get_event_loop().close()
</textarea>
<textarea readonly="true" rows="13">
# /boot.py

# This file is executed on every boot (including wake-boot from deepsleep)
import machine
from os import listdir
Expand Down Expand Up @@ -136,16 +138,16 @@ <h2>Simple Server Application</h2>
[INFO] pyrobusta.server.http_server.init_pools: 2 connection(s) allowed
[INFO] pyrobusta.server.http_server: started

# You can now reach the device at 192.168.1.101:8080 (replace with your IP)
# You can now reach the device at 192.168.1.101 (replace with your IP)
# Press Ctrl-x to exit
</textarea>

<p>Use curl to test your application.</p>
<textarea readonly="true" rows="6">
$ curl "http://192.168.1.101:8080/app"
$ curl "http://192.168.1.101/app"
Free memory [bytes]: 115456

$ curl "http://192.168.1.101:8080/app?format=%"
$ curl "http://192.168.1.101/app?format=%"
Free memory [%]: 71.76
</textarea>

Expand Down
8 changes: 5 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ to upload it to the root directory of the target device.
|-------------------|-------------------------------------------------------------------------------------------------------|-------------------------------|
| wifi_ssid | Name of the Wi-Fi network. When empty, Wi-Fi is not initalized by the built-in wifi.py module. | None |
| 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_port | Port number for HTTP. | 80 |
| https_port | Port number for HTTPS. | 443 |
| 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. | "/www /lib/pyrobusta" |
| http_serve_files | Enable/disable file serving. | "True" |
| http_serve_files | Enable/disable file serving. | True |
| 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" |
| 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. | "info" |
23 changes: 8 additions & 15 deletions example/mip_repo/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,28 @@

import pyrobusta.server.http_server as http_server
from pyrobusta.protocol.http import HttpEngine
from pyrobusta.utils import logging, config
from pyrobusta.utils import logging, config, assets, helpers


def append_package_files(dir, package_files, host_name, protocol):
"""
Construct package file list recursively.
"""
for name in os.listdir(dir):
current_path = f"{dir}/{name}"
st = os.stat(current_path)
mode = st[0]
if mode & 0x4000: # directory bit set
append_package_files(current_path, package_files, host_name, protocol)
continue

target_path = current_path[4:] if current_path.startswith("lib/") else current_path
dir = helpers.normalize_path(dir)

for asset in assets.iterate_fs(dir):
package_files["urls"].append(
[
target_path,
f"{protocol}://{host_name}/files/{current_path}",
asset,
f"{protocol}://{host_name}/files" + asset,
]
)


@HttpEngine.route("/pyrobusta/package.json", "GET")
def self_serve_mip_package(http_ctx, _):
package_files = {"version": config.PYROBUSTA_VERSION, "deps": [], "urls": []}
tls_enabled = config.get_config("tls").lower() == "true"
tls_enabled = config.get_config(config.CONF_TLS)
server_addr = http_ctx.headers["host"]
if ":" not in server_addr:
port = (
Expand All @@ -44,8 +38,7 @@ def self_serve_mip_package(http_ctx, _):
protocol = "https" if tls_enabled else "http"

logging.debug(f"[mip_repo] server_addr: {server_addr}")
root = "pyrobusta" if "pyrobusta" in os.listdir() else "lib/pyrobusta"
append_package_files(root, package_files, server_addr, protocol)
append_package_files("/lib/pyrobusta", package_files, server_addr, protocol)
return "application/json", package_files


Expand Down
6 changes: 3 additions & 3 deletions src/pyrobusta/con/wifi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@

from network import WLAN, STA_IF

from ..utils.config import get_config
from ..utils.config import get_config, CONF_WIFI_SSID, CONF_WIFI_PASSWORD
from ..utils import logging


def initialize():
"""
Initialize WLAN interface in station mode
"""
ssid = get_config("wifi_ssid")
password = get_config("wifi_password")
ssid = get_config(CONF_WIFI_SSID)
password = get_config(CONF_WIFI_PASSWORD)

if not ssid or not password:
logging.warning(__name__ + ": missing SSID/password")
Expand Down
13 changes: 9 additions & 4 deletions src/pyrobusta/protocol/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
from json import dumps
from io import BytesIO

from ..utils.config import get_config
from ..utils.config import (
get_config,
CONF_HTTP_SERVED_PATHS,
CONF_HTTP_MULTIPART,
CONF_HTTP_SERVE_FILES,
)


class HeaderParsingError(ValueError):
Expand Down Expand Up @@ -217,7 +222,7 @@ 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())
served_paths = get_config(CONF_HTTP_SERVED_PATHS)
parts = path.split("/")
for i, _ in enumerate(parts):
current_path = "/".join(parts[: i + 1])
Expand Down Expand Up @@ -644,12 +649,12 @@ def enable_optional_features():
"""
Enable related optional features, set in the config.
"""
if get_config("http_multipart").lower() == "true":
if get_config(CONF_HTTP_MULTIPART):
from pyrobusta.protocol import http_multipart

http_multipart.apply_patches()

if get_config("http_serve_files").lower() == "true":
if get_config(CONF_HTTP_SERVE_FILES):
from pyrobusta.protocol import http_file_server

http_file_server.apply_patches()
21 changes: 14 additions & 7 deletions src/pyrobusta/server/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@
from ..protocol import http
from ..bindings.socket_http import SocketHttp
from ..stream.buffer import MemoryPool, SlidingBuffer
from ..utils.config import get_config
from ..utils.config import (
get_config,
CONF_HTTP_PORT,
CONF_HTTPS_PORT,
CONF_HTTP_MEM_CAP,
CONF_TLS,
CONF_SOCKET_MAX_CON,
)
from ..utils.helpers import normalize_path
from ..utils import logging

Expand All @@ -31,8 +38,8 @@ class HttpServer:
CON_ACCEPT_SLEEP_MS = (
100 # Duration of sleep between attempts to accept new connection
)
LISTEN_PORT_HTTP = 80
LISTEN_PORT_HTTPS = 443
LISTEN_PORT_HTTP = get_config(CONF_HTTP_PORT)
LISTEN_PORT_HTTPS = get_config(CONF_HTTPS_PORT)
TLS_CERT_PATH = "/cert.der"
TLS_KEY_PATH = "/key.der"
CON_TIMEOUT_S = 30
Expand All @@ -41,7 +48,7 @@ class HttpServer:
# Constants for controlled memory footprint
# -----------------------------------------

MEM_CAP = float(get_config("http_mem_cap")) # Default memory cap (percentage / 100)
MEM_CAP = get_config(CONF_HTTP_MEM_CAP) # Default memory cap (percentage / 100)
SEND_BUF_MIN_BYTES = 512 # Minimum buffer size for responses
SEND_BUF_MAX_BYTES = 4096 # Max buffer size for responses
RECV_BUF_MIN_BYTES = 512 # Minimum buffer size for requests
Expand Down Expand Up @@ -105,7 +112,7 @@ def __init__(self):
self._host = "0.0.0.0"
self._port = (
HttpServer.LISTEN_PORT_HTTPS
if get_config("tls").lower() == "true"
if get_config(CONF_TLS)
else HttpServer.LISTEN_PORT_HTTP
)
self._server = None
Expand Down Expand Up @@ -190,11 +197,11 @@ async def start_socket_server(self):
logging.debug(
__name__ + f"registered endpoints: {http.HttpEngine.ENDPOINTS}"
)
self._max_clients = int(get_config("socket_max_con"))
self._max_clients = get_config(CONF_SOCKET_MAX_CON)
self._init_pools(self._max_clients)
ssl_ctx = None

if get_config("tls").lower() == "true":
if get_config(CONF_TLS):
import ssl

ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
Expand Down
120 changes: 80 additions & 40 deletions src/pyrobusta/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,82 @@
Values can be encapsulated by single or double quotes.
"""

try:
from micropython import const
except ImportError:

def const(n): # pylint: disable=C0116
return n


from .helpers import normalize_path

PYROBUSTA_VERSION = "v0.4.0"
CONFIG_LOADED = False
CONFIG_LOCATION = "pyrobusta.env"
CONFIG_CACHE = [
"wifi_ssid",

# -------------------------------------------
# Global runtime configuration keys.
# Provide these keys when using get_config().
# -------------------------------------------
CONF_WIFI_SSID = const(0)
CONF_WIFI_PASSWORD = const(1)
CONF_HTTP_PORT = const(2)
CONF_HTTPS_PORT = const(3)
CONF_HTTP_MULTIPART = const(4)
CONF_HTTP_MEM_CAP = const(5)
CONF_HTTP_SERVED_PATHS = const(6)
CONF_HTTP_SERVE_FILES = const(7)
CONF_SOCKET_MAX_CON = const(8)
CONF_TLS = const(9)
CONF_LOG_LEVEL = const(10)

# -------------------
# Configuration state
# -------------------
_CONFIG_LOADED = False
_CONFIG_CACHE = [
CONF_WIFI_SSID,
None,
"wifi_password",
CONF_WIFI_PASSWORD,
None,
"http_multipart",
"False",
"http_mem_cap",
CONF_HTTP_PORT,
80,
CONF_HTTPS_PORT,
443,
CONF_HTTP_MULTIPART,
False,
CONF_HTTP_MEM_CAP,
0.1,
"http_served_paths",
"/www /lib/pyrobusta",
"http_serve_files",
"True",
"socket_max_con",
CONF_HTTP_SERVED_PATHS,
["/www", "/lib/pyrobusta"],
CONF_HTTP_SERVE_FILES,
True,
CONF_SOCKET_MAX_CON,
2,
"tls",
"False",
"log_level",
CONF_TLS,
False,
CONF_LOG_LEVEL,
"info",
]


def normalize(key, value):
# --------------
# Public helpers
# --------------
def parse_config(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()])
if key in (CONF_HTTP_MULTIPART, CONF_HTTP_SERVE_FILES, CONF_TLS):
return value.lower() == "true"
if key in (CONF_HTTP_PORT, CONF_HTTPS_PORT, CONF_SOCKET_MAX_CON):
return int(value)
if key == CONF_HTTP_MEM_CAP:
return float(value)
if key == CONF_HTTP_SERVED_PATHS:
return [normalize_path(p) for p in value.split()]
if key not in (CONF_WIFI_SSID, CONF_WIFI_PASSWORD):
return value.lower()
return value


Expand All @@ -52,18 +95,22 @@ def read_config(config=CONFIG_LOCATION):
if not line.strip():
continue
parts = line.split("=")
key = parts[0].strip()
key_name = "CONF_" + parts[0].strip().upper()
if key_name in globals():
key = globals()[key_name]
else:
key = len(_CONFIG_CACHE) // 2 + 1
globals()[key_name] = key
value = parts[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
):
CONFIG_CACHE[conf_idx + 1] = value
else:
CONFIG_CACHE.append(key)
CONFIG_CACHE.append(value)
value = parse_config(key, value)
if (
key in _CONFIG_CACHE
and (conf_idx := _CONFIG_CACHE.index(key)) % 2 == 0
):
_CONFIG_CACHE[conf_idx + 1] = value
else:
_CONFIG_CACHE.append(key)
_CONFIG_CACHE.append(value)
except OSError:
pass

Expand All @@ -74,15 +121,8 @@ def get_config(key):
The cache is reloaded if the key is missing
or the value is set to None.
"""
global CONFIG_LOADED # pylint: disable=W0603
if key not in CONFIG_CACHE or not CONFIG_LOADED:
read_config()
CONFIG_LOADED = True
try:
conf_idx = CONFIG_CACHE.index(key)
except IndexError:
return None
if CONFIG_CACHE[conf_idx + 1] is None:
global _CONFIG_LOADED # pylint: disable=W0603
if _CONFIG_CACHE[2 * key + 1] is None or not _CONFIG_LOADED:
read_config()
conf_idx = CONFIG_CACHE.index(key)
return CONFIG_CACHE[conf_idx + 1]
_CONFIG_LOADED = True
return _CONFIG_CACHE[2 * key + 1]
Loading
Loading