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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
venv/
__pycache__
pyrobusta.env
pyrobusta.passwd
pyrobusta.roles
build/
runtime/
runtime-test/
Expand Down
10 changes: 7 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ deploy-config:
@echo "Uploading pyrobusta.env"
@mpremote $(DEVICE) soft-reset
@if [ -f pyrobusta.env ]; then mpremote $(DEVICE) cp pyrobusta.env :pyrobusta.env; fi
@if [ -f pyrobusta.roles ]; then mpremote $(DEVICE) cp pyrobusta.roles :pyrobusta.roles; fi
@if [ -f pyrobusta.passwd ]; then mpremote $(DEVICE) cp pyrobusta.passwd :pyrobusta.passwd; fi
@mpremote $(DEVICE) reset
@sleep 5


# -----------------------------
Expand Down Expand Up @@ -191,6 +194,8 @@ stage-app:
@cp $(TLS_DIR)/key.der $(RUNTIME_DIR)/

@if [ -f pyrobusta.env ]; then cp pyrobusta.env $(RUNTIME_DIR)/; fi
@if [ -f pyrobusta.roles ]; then cp pyrobusta.roles $(RUNTIME_DIR)/; fi
@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

Expand All @@ -212,9 +217,7 @@ deploy-app:
mpremote $(DEVICE) cp $(APP_DIR)/boot.py :boot.py
mpremote $(DEVICE) cp $(APP_DIR)/app.py :app.py

@echo "Uploading pyrobusta.env"
@if [ -f pyrobusta.env ]; then mpremote $(DEVICE) cp pyrobusta.env :pyrobusta.env; fi
@mpremote $(DEVICE) reset
$(MAKE) deploy-config
@echo "\e[32m$(APP_DIR) app is successfully deployed, \n"\
"run 'make DEVICE=$(DEVICE) run-device' to restart the device and check the output.\e[0m"

Expand Down Expand Up @@ -299,6 +302,7 @@ test-unix: stage-test tls-cert
.PHONY: test-device
test-device: stage-test #clean-device upload
@mpremote $(DEVICE) soft-reset
@mpremote $(DEVICE) cp $(TEST_RUNTIME)/env_utils.py :/env_utils.py
@cd $(TEST_RUNTIME); \
for test in test_*.py; do \
echo "\n==================================="; \
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ PyRobusta is a memory-conscious HTTP/1.1 server library built for embedded devic
- Built-in API for uploading, downloading, and deleting files stored on the server
- Persistent connection handling via the `Connection: keep-alive` header
- HTTP/1.0 and HTTP/1.1 support
- Basic authentication and RBAC authorization
- TLS support

## Design Principles
Expand Down
5 changes: 4 additions & 1 deletion docs/application_development/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,12 @@ $ mpremote a0 cp pyrobusta.env :/pyrobusta.env
| `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 /lib/pyrobusta` |
| `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 |
| `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 |
| `log_level` | Logging level. Can be one of: `warning`, `info`, `debug`. | info |
| `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: `warning`, `info`, `debug`. | `info` |

## Configuration API

Expand Down
1 change: 1 addition & 0 deletions docs/application_development/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Then continue with the remaining guides in the order listed below.
* [Response Processing](response.md)
* [Static Content](static_content.md)
* [File Server API](file_server.md)
* [Authentication & Security](security.md)
* [Source Code](https://github.com/szeka9/PyRobusta)

---
Expand Down
3 changes: 2 additions & 1 deletion docs/application_development/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ Unlike regular request bodies, multipart parts are parsed by the server before b
to the application. Preprocessed parts consist of headers (dictionary) and the raw part body
(bytes), passed as a tuple.

**Multipart state tracking**
### Multipart State Tracking

The HTTP context exposes the boolean attributes
`mp_is_first` and
`mp_is_last`
Expand Down
181 changes: 177 additions & 4 deletions docs/application_development/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,199 @@

[← Back](index.md)

This page is work in progress. Refer to the [main page](index.md) for available documentation.
This page documents the security features provided by PyRobusta,
including HTTP Basic Authentication, role-based authorization, and TLS.

Note: Authentication verifies the client's identity. Authorization determines
whether the authenticated user is permitted to access a resource.

---

## Table of Contents

* [Authentication & Security](#authentication-security)
+ [API Key Authentication](#api-key-authentication)
+ [Basic Authentication](#basic-authentication)
+ [Authorization](#authorization)
+ [HTTPS / TLS](#https-tls)
+ [Certificate Installation](#certificate-installation)

---

## API Key Authentication

## Basic Authentication

PyRobusta supports HTTP Basic Authentication with per-user credentials
stored on the device. Passwords are stored as SHA-256 hashes.
Basic authentication is disabled by default. It can be enabled by setting
`http_auth=basic` in [pyrobusta.env](configuration.md).

During initialization, PyRobusta reads `pyrobusta.passwd` by default
from the server's working directory. Each entry specifies a username,
a password hash, and one or more role names. Set `passwd_file` in the configuration
to specify an alternative user credentials file to load. For more information,
see [Server Configuration](./configuration.md).

PyRobusta defines a single realm (`realm="Device"`) to authenticate against.
The server response includes the `WWW-Authenticate: Basic realm="Device"` header
when a request does not contain valid credentials.

### pyrobusta.passwd

`pyrobusta.passwd` utilizes a passwd-like format, specifying one user per line,
with each element of the user data separated by `:`. The expected format
of a user entry is `<username>:<SHA256-password-hash>:<comma-separated-role-names>`.

```
# Example
# File: /pyrobusta.passwd
szeka9:9b085649ac73a164314f00ec70010ec37d6c663cf71e9672a0600d6609b3ae12:api_admin
user-1:c0418ce1f43b9013c6871d4ac4941de6cd83a3fac29677785b250b86c88320a0:api_user,app_viewer
[...]
user-n:a8105c3c8b75556a9099b8dcab9cc13362d5a6f9fa5888ce39efc57961deb519:api_user,app_maintainer
```

When adding new users, follow the below steps to calculate the password hash:

```python3
import hashlib

password_raw = "<password-data>"
password_hash = hashlib.sha256(password_raw.encode()).digest().hex()
print(password_hash) # paste the output into pyrobusta.passwd

b9fa35baa8069f3dabe214aed3525da0824efefdbce5ad83dfe4e7f48f8ce15f
```

## Authorization

When HTTP Basic Authentication is enabled, PyRobusta provides role-based access control (RBAC).
Resource permissions are determined by the authenticated user's assigned roles.
Each user can be assigned one or more roles, configured in `pyrobusta.passwd`.

While `pyrobusta.passwd` assigns roles to users, `pyrobusta.roles` maps those roles to
HTTP methods and server resources. Authorization follows a least-privilege model.
Users have no permissions unless explicitly granted by one or more authorization rules.
Each matching rule grants additional permissions. Requests that do not match an authorization
rule receive HTTP 403 Forbidden.

Set `roles_file` in the configuration to specify an alternative role configuration file to load.
For more information, see [Server Configuration](./configuration.md).

### pyrobusta.roles

`pyrobusta.roles` consists of one or more authorization blocks. Each block defines one or
more path patterns, followed by one or more authorization rules. Every authorization rule
applies to every path pattern declared within the same block.

```
<path-pattern>
<path-pattern>
...
<HTTP-method>: <role>,<role>,...
<HTTP-method>: <role>,<role>,...
...

<path-pattern>
...
```

Authorization rules are selected based on pattern specificity rather than their
order in `pyrobusta.roles`. Path patterns support the following forms, ordered
from least to most specific. Each request is matched against the most specific
pattern, and only the roles associated with the selected pattern are considered
during authorization.

```
/** # Match any path
/* # Match direct children of /
/app # Exact match
/app/** # Match any descendant of /app
/app/* # Match direct children of /app
/app/*/resources # Match a single wildcard segment
/app/endpoint/resources # Exact match
```

**Note**: recursive globs (`**`) are only supported as the final path segment.
Allowing `**` in intermediate segments would introduce ambiguous matches
and is therefore not supported.

```
# File: /pyrobusta.roles

# ---------------------------------------------------------
# Example: public resources (/, /index.html, /styles.css, ...)
/
/*
GET,HEAD: *

# ---------------------------------------------------------
# Example: apply rule to parent and child resources
/app/resource
/app/resource/*
GET: resource_viewer
POST,PUT,DELETE: resource_maintainer

# ---------------------------------------------------------
# Example: apply rule to paths of any length with **
/app/api/**
GET: api_viewer
POST,PUT,DELETE: api_maintainer

# ---------------------------------------------------------
# Example: require api_admin access for all HTTP methods
/app/api/management
*: api_admin

# ---------------------------------------------------------
# Example: restrict all access to a resource
/app/secret
*:

# ---------------------------------------------------------
# Example: assign multiple roles
/app/logs
GET: api_admin,api_maintainer
```

**Note**: The special role `*` grants public access regardless of the authenticated user's roles.
Rules that specify `*` do not require authentication.

## Authentication & Authorization Flow

```mermaid
flowchart TD
A[Request] --> B[Resolve requested resource]
B --> C[Determine security policy for resource]

C --> D[Public]
C --> E[Protected]

D --> F[Ignore auth requirement]
F --> G[Process request]

E --> H{Is Authorization header present?}

H -->|No| I[401 +<br/>WWW-Authenticate]
H -->|Yes| J[Authenticate]

J --> K{Valid credentials?}

K -->|No| L[401 +<br/>WWW-Authenticate]
K -->|Yes| M{Authorized?}

M -->|No| N[403]
M -->|Yes| G
```

| Condition | Response |
| --- | --- |
| Missing credentials | 401 Unauthorized |
| Invalid credentials | 401 Unauthorized |
| Authenticated but insufficient permissions | 403 Forbidden |

## HTTPS / TLS



## Certificate Installation

---
Expand Down
14 changes: 14 additions & 0 deletions src/pyrobusta/protocol/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
CONF_HTTP_MULTIPART,
CONF_HTTP_FILES_API,
CONF_HTTP_SERVED_PATHS,
CONF_HTTP_AUTH,
)
from ..utils import logging, helpers
from ..stream.buffer import BufferFullError
Expand Down Expand Up @@ -49,6 +50,7 @@ class HttpEngine:
- http_files_api: serve files at the /files API, with support for uploads,
removal and directory listing
- http_multipart: support for multipart requests/responses
- http_auth: authenticate and authorize users
"""

__slots__ = (
Expand Down Expand Up @@ -79,6 +81,8 @@ class HttpEngine:
b"204 No Content",
400,
b"400 Bad Request",
401,
b"401 Unauthorized",
403,
b"403 Forbidden",
404,
Expand Down Expand Up @@ -692,6 +696,11 @@ def _parse_headers_st(self, rx):
if self.version == b"HTTP/1.1" and "host" not in self.headers:
raise InvalidHeaders()
rx.consume(blank_idx + 4)
self.state = self._handle_auth_st

def _handle_auth_st(self, _):
# This a placeholder for authentication & authorization
# purposes to be overridden by extensions.
self.state = self._route_request_st

def _route_request_st(self, _):
Expand Down Expand Up @@ -904,3 +913,8 @@ def enable_optional_features():
from pyrobusta.protocol import http_file_server

http_file_server.apply_patches()

if get_config(CONF_HTTP_AUTH) == "basic":
from pyrobusta.protocol import http_basic_auth

http_basic_auth.apply_patches()
Loading
Loading