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
139 changes: 117 additions & 22 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ publish:
# Bump version in Python source
@sed -E -i.bak \
's/(PYROBUSTA_VERSION[[:space:]]*=[[:space:]]*)"[^"]*"/\1"$(PYROBUSTA_VERSION)"/' \
$(SRC_DIR)/pyrobusta/utils/config.py \
&& rm -f $(SRC_DIR)/pyrobusta/utils/config.py.bak
$(SRC_DIR)/pyrobusta/__init__.py \
&& rm -f $(SRC_DIR)/pyrobusta/__init__.py.bak

$(MAKE) clean
$(MAKE) build docs BUILD_DIR=$(DIST_DIR)
Expand Down Expand Up @@ -198,6 +198,8 @@ stage-app:
@if [ -f pyrobusta.passwd ]; then cp pyrobusta.passwd $(RUNTIME_DIR)/; fi
@echo "http_port=8080" >> $(RUNTIME_DIR)/pyrobusta.env
@echo "https_port=4443" >> $(RUNTIME_DIR)/pyrobusta.env
@sed -i '/^wifi_ssid/d' $(RUNTIME_DIR)/pyrobusta.env
@sed -i '/^wifi_password/d' $(RUNTIME_DIR)/pyrobusta.env

# -----------------------------
# Run app locally with UNIX MicroPython
Expand Down Expand Up @@ -349,8 +351,10 @@ perf-test-device: perf-test-http-dimensioning perf-test-http-soak
perf-test-regenerate-plots:
@for type in dimensioning soak; do \
[ -d docs/$$type/esp32_c3 ] && \
echo "\n======================\nESP32-C3: $$type\n======================\n"; \
python3 tests/system/summary.py ESP32-C3 docs/$$type; \
[ -d docs/$$type/esp32_s3 ] && \
echo "\n======================\nESP32-S3: $$type\n======================\n"; \
python3 tests/system/summary.py ESP32-S3 docs/$$type; \
done

Expand All @@ -359,37 +363,128 @@ perf-test-regenerate-plots:
# ================================================

# -----------------------------
# Generate certificate
# TLS test certificate defaults
# TLS_FORMAT: DER, PEM
# TLS_KEY_TYPE: EC, RSA
# TLS_KEY_BITS (RSA only): 2048, 3072, 4096
# TLS_KEY_CURVE (EC only): prime256v1, secp384r1
# -----------------------------
TLS_FORMAT ?= DER
TLS_KEY_TYPE ?= EC
TLS_KEY_CURVE ?= prime256v1
TLS_KEY_BITS ?= 2048
TLS_CN ?= localhost
TLS_SAN ?= DNS:localhost

TLS_CA_CN ?= Test CA

TLS_EXT := $(shell printf '%s' "$(TLS_FORMAT)" | tr '[:upper:]' '[:lower:]')

TLS_CA_CERT = $(TLS_DIR)/ca-cert.$(TLS_EXT)
TLS_CA_KEY = $(TLS_DIR)/ca-key.$(TLS_EXT)
TLS_CERT = $(TLS_DIR)/cert.$(TLS_EXT)
TLS_KEY = $(TLS_DIR)/key.$(TLS_EXT)

TLS_CSR = $(TLS_DIR)/csr.pem
TLS_EXTFILE = $(TLS_DIR)/openssl-ext.conf
TLS_SERIAL = $(TLS_DIR)/ca-cert.srl

# -----------------------------
# Generate CA
# -----------------------------
.PHONY: tls-ca
tls-ca:
@mkdir -p "$(TLS_DIR)"

@if [ ! -f "$(TLS_CA_KEY)" ]; then \
echo "Generating CA private key ($(TLS_FORMAT))..."; \
openssl genpkey \
-algorithm EC \
-out "$(TLS_CA_KEY)" \
-outform "$(TLS_FORMAT)" \
-pkeyopt ec_paramgen_curve:prime256v1; \
fi

@if [ ! -f "$(TLS_CA_CERT)" ]; then \
echo "Generating CA certificate ($(TLS_FORMAT))..."; \
openssl req -new -x509 \
-key "$(TLS_CA_KEY)" \
-keyform "$(TLS_FORMAT)" \
-out "$(TLS_CA_CERT)" \
-outform "$(TLS_FORMAT)" \
-days 3650 \
-subj "/CN=$(TLS_CA_CN)" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign"; \
fi

# -----------------------------
# Generate server certificate
# -----------------------------
.PHONY: tls-cert
tls-cert:
@rm -f $(TLS_DIR)/cert.der $(TLS_DIR)/key.der; \
mkdir -p $(TLS_DIR);

@openssl genpkey \
-algorithm RSA \
-out $(TLS_DIR)/key.der \
-outform DER \
-pkeyopt rsa_keygen_bits:2048 2>/dev/null

@openssl req -new -x509 \
-key $(TLS_DIR)/key.der \
-keyform DER \
-out $(TLS_DIR)/cert.der \
-outform DER \
-days 365 \
-subj "/CN=localhost"
tls-cert: tls-ca
@mkdir -p "$(TLS_DIR)"

@if [ "$(TLS_KEY_TYPE)" = "RSA" ]; then \
echo "Generating RSA server key ($(TLS_KEY_BITS) bits)..."; \
openssl genpkey \
-algorithm RSA \
-out "$(TLS_KEY)" \
-outform "$(TLS_FORMAT)" \
-pkeyopt "rsa_keygen_bits:$(TLS_KEY_BITS)"; \
elif [ "$(TLS_KEY_TYPE)" = "EC" ]; then \
echo "Generating EC server key ($(TLS_KEY_CURVE))..."; \
openssl genpkey \
-algorithm EC \
-out "$(TLS_KEY)" \
-outform "$(TLS_FORMAT)" \
-pkeyopt "ec_paramgen_curve:$(TLS_KEY_CURVE)"; \
else \
echo "Unsupported TLS_KEY_TYPE: $(TLS_KEY_TYPE)" >&2; \
exit 1; \
fi

@openssl req -new \
-key "$(TLS_KEY)" \
-keyform "$(TLS_FORMAT)" \
-out "$(TLS_CSR)" \
-subj "/CN=$(TLS_CN)"

@printf '%s\n' \
"basicConstraints=critical,CA:FALSE" \
"keyUsage=critical,digitalSignature,keyEncipherment" \
"subjectAltName=$(TLS_SAN)" \
> "$(TLS_EXTFILE)"

@openssl x509 -req \
-in "$(TLS_CSR)" \
-CA "$(TLS_CA_CERT)" \
-CAkey "$(TLS_CA_KEY)" \
-CAform "$(TLS_FORMAT)" \
-CAkeyform "$(TLS_FORMAT)" \
-CAcreateserial \
-out "$(TLS_CERT)" \
-outform "$(TLS_FORMAT)" \
-days 365 \
-extfile "$(TLS_EXTFILE)"

@rm -f \
"$(TLS_CSR)" \
"$(TLS_EXTFILE)" \
"$(TLS_SERIAL)"


# -----------------------------
# Deploy certificate
# -----------------------------
.PHONY: deploy-cert
deploy-cert:
@mpremote $(DEVICE) soft-reset
@mpremote $(DEVICE) cp $(TLS_DIR)/key.der :key.der
@mpremote $(DEVICE) cp $(TLS_DIR)/cert.der :cert.der
@mpremote $(DEVICE) cp "$(TLS_DIR)/key.$(TLS_EXT)" ":key.$(TLS_EXT)"
@mpremote $(DEVICE) cp "$(TLS_DIR)/cert.$(TLS_EXT)" ":cert.$(TLS_EXT)"
@mpremote $(DEVICE) reset


# ================================================
# Cleanup
# ================================================
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,10 @@ install_www()

# Start the HTTP server
import asyncio
from pyrobusta.server.http_server import HttpServer
from pyrobusta import application

async def main():
server = HttpServer()
await server.start_socket_server()
await application.run()
while True:
await asyncio.sleep(1)

Expand All @@ -79,9 +78,10 @@ payloads, and advanced HTTP features.

```python
import asyncio
import machine
from gc import mem_free, mem_alloc, collect

import pyrobusta.server.http_server as http_server
from pyrobusta import application
from pyrobusta.protocol.http import HttpEngine

@HttpEngine.route("/mem-usage", "GET")
Expand All @@ -98,12 +98,12 @@ def mem_usage(http_ctx, _):
)

async def main():
server = http_server.HttpServer()
await server.start_socket_server()
await application.run()
while True:
await asyncio.sleep(1)

asyncio.run(main())
if machine.reset_cause() != machine.SOFT_RESET:
asyncio.run(main())
```

Check the [Application Development](./docs/application_development/index.md) guide for
Expand Down
73 changes: 37 additions & 36 deletions docs/application_development/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ configuration API.
* [Configuration](#configuration)
+ [Configuration Format & Deployment](#configuration-format-deployment)
+ [Parameter Description](#parameter-description)
+ [Configuration API](#configuration-api)
+ [Configuration Loading](#configuration-loading)

---

Expand All @@ -35,63 +35,64 @@ tls=False # turn off TLS
Perform a soft reset and upload `pyrobusta.env` using mpremote.

```
$ mpremote a0 soft-reset
$ mpremote a0 cp pyrobusta.env :/pyrobusta.env
$ mpremote connect /dev/ttyACM1 soft-reset
$ mpremote connect /dev/ttyACM1 cp pyrobusta.env :/pyrobusta.env
```

## Parameter Description

| Name | Description | Default |
| --- | --- | --- |
| `wifi_ssid` | Name of the Wi-Fi network. When empty, Wi-Fi is not initialized by the built-in `wifi.py` module. | None |
| `wifi_password` | Password of the Wi-Fi network. When empty, Wi-Fi is not initialized by the built-in `wifi.py` module. | None |
| `wifi_password` | Password of the Wi-Fi network. | None |
| `tls` | Enables or disables TLS. When enabled, `cert.der` and `key.der` must be installed at the server root. | False |
| `tls_cert_file` | Alternative path to the TLS certificate. | `/cert.der` |
| `tls_key_file` | Alternative path to the TLS private key. | `/key.der` |
| `passwd_file` | Path to the file containing user credentials used for authentication. | `/pyrobusta.passwd` |
| `roles_file` | Path to the file containing RBAC role definitions used for authorization. | `/pyrobusta.roles` |
| `log_level` | Logging level. Can be one of: `error`, `warning`, `info`, `debug`. | `info` |
| `socket_max_con` | Maximum number of simultaneous socket connections. | 2 |
| `http_served_paths` | Space-separated list of filesystem paths that may be served over HTTP. | `/www` |
| `http_mem_cap` | Fraction of available heap memory reserved for stream buffers. Valid range: (0, 1]. | 0.1 |
| `http_port` | Port number for HTTP. | 80 |
| `https_port` | Port number for HTTPS. | 443 |
| `http_multipart` | Enables or disables multipart request and response processing. Enabling multipart support increases memory usage. | False |
| `http_mem_cap` | Fraction of available heap memory reserved for stream buffers. Valid range: (0, 1]. | 0.1 |
| `http_served_paths` | Space-separated list of filesystem paths that may be served over HTTP. | `/www` |
| `http_multipart` | Enables or disables multipart request and response processing (`Content-Type: multipart/*`). | False |
| `http_files_api` | Enables or disables the file management API endpoint (`/files`), allowing upload, download, and listing of files. | False |
| `http_auth` | Selects the type of authentication method enforced by the server. Currently, basic authentication (`basic`) is supported. | None |
| `http_browser_security` | Enables or disables browser security features, including CSRF protection, and browser security headers (Content Security Policy, referrer policy). Disabling browser security is only recommended when using non-browser clients or during local development and testing. | True |
| `http_browser_security` | Enables or disables browser security features including browser security headers (Content Security Policy, referrer policy) and CSRF protection (if authentication is enabled). Disabling browser security is only recommended when using non-browser clients or during local development and testing. | True |
| `http_insecure_auth` | Allows clients to authenticate over unsecured HTTP (without TLS). This may expose credentials or authentication tokens in transit. | False |
| `http_sessions` | Allow the creation of session cookies after successful authentication requests. | False |
| `http_sessions` | Enables browser session cookies for authenticated clients. When enabled, successful authentication establishes a session that can be reused without resending authentication credentials. | True |
| `http_session_ttl_sec` | Duration of validity of session cookies in seconds. | 900 |
| `socket_max_con` | Maximum number of simultaneous socket connections. | 2 |
| `tls` | Enables or disables TLS. When enabled, `cert.der` and `key.der` must be installed at the server root. | False |
| `tls_cert_file` | Path to the TLS certificate. | `/cert.der` |
| `tls_key_file` | Path to the TLS private key. | `/key.der` |
| `passwd_file` | Path to the file containing user credentials used for authentication. | `/pyrobusta.passwd` |
| `roles_file` | Path to the file containing RBAC role definitions used for authorization. | `/pyrobusta.roles` |
| `log_level` | Logging level. Can be one of: `error`, `warning`, `info`, `debug`. | `info` |

## Configuration API
## Configuration Loading

Configuration values can be accessed through the
`pyrobusta.utils.config` module.
Values are loaded from `pyrobusta.env` during server initialization.
Configuration values can be retrieved using
`get_config()` together with one of the
predefined `CONF_*` constants.
Configuration is represented by the `Config` class in
`pyrobusta.utils.config`. During application initialization,
`pyrobusta.application` creates and loads a `Config` instance from
`pyrobusta.env`, converting values to their expected runtime types.

After initialization, configuration values are retrieved from an internal cache.
The cached values are normalized to their expected runtime types to avoid repeated
parsing of environment strings.
Configuration values are exposed as attributes on the `Config` instance:

Configuration values are treated as immutable during runtime.
Changes are applied only when the configuration cache is reloaded.
The configuration cache can be reloaded by calling
`read_config()`, which re-reads `pyrobusta.env`
and rebuilds the internal normalized cache.

```
from pyrobusta.utils.config import get_config, CONF_TLS
config = Config("/pyrobusta.env")

@HttpEngine.route("/tls", "GET")
def tls_status(http_ctx, _):
enabled = get_config(CONF_TLS)
return "text/plain", f"TLS enabled: {enabled}"
if config.tls:
...
```

The application uses the configuration to initialize and specialize the
relevant subsystems during startup. The `Config` instance is then discarded;
runtime components do not access configuration directly.

Configuration is **immutable for the lifetime of an application**. Changes
to `pyrobusta.env` therefore require a new application instance and, under
normal operation, an application restart.

The `pyrobusta.utils.config` module does not maintain a global configuration
object or runtime configuration cache. It provides the `Config` definition
and configuration-loading functionality.

---

PyRobusta v0.8.0 Web Server
Loading
Loading