Add an example server (very slow)

This implements a simple HTTP and HTTPS server pair that serve a static HTML file. The HTTP server uses adafruit_httpserver in a straightforward, basic way. The HTTPS server works by wrapping a socket pool object in an object that wraps all sockets in SSLSockets.

There are throwaway, self-signed certificates for testing under src/certificates. However, HTTPS is very slow. Probably need an optimized AES implementation.
This commit is contained in:
James Ide 2023-02-26 18:31:37 -08:00
parent 54b1dbe7f1
commit 86009ef6ae
No known key found for this signature in database
GPG key ID: 680F76929C43E3B9
20 changed files with 202 additions and 8 deletions

3
.gitignore vendored
View file

@ -1,2 +1,5 @@
# macOS # macOS
.DS_Store .DS_Store
# Python
__pycache__/

8
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"python.analysis.diagnosticSeverityOverrides": {
"reportMissingImports": "none",
"reportMissingModuleSource": "none",
"reportShadowedImports": "none"
},
"python.formatting.provider": "black"
}

View file

@ -1,23 +1,39 @@
# CircuitPython HTTPS Web Server (for Raspberry Pi Pico W) # CircuitPython HTTPS Web Server (for Raspberry Pi Pico W)
> Note: this example isn't complete! There is an issue with the TLS library, perhaps an incompatible certificate. Help investigating is appreciated!
This is an example of an HTTPS web server written in [CircuitPython](https://circuitpython.org/), intended to run on a [Raspberry Pi Pico W](https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html#raspberry-pi-pico-w-and-pico-wh). [Adafruit](https://www.adafruit.com/) (the makers of CircuitPython) and the CircuitPython documentation have [guides](https://learn.adafruit.com/pico-w-http-server-with-circuitpython/code-the-pico-w-http-server) on running an unsecured HTTP server but none on serving content over HTTPS. This example will show you how to run an HTTPS server from a Pico W. This is an example of an HTTPS web server written in [CircuitPython](https://circuitpython.org/), intended to run on a [Raspberry Pi Pico W](https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html#raspberry-pi-pico-w-and-pico-wh). [Adafruit](https://www.adafruit.com/) (the makers of CircuitPython) and the CircuitPython documentation have [guides](https://learn.adafruit.com/pico-w-http-server-with-circuitpython/code-the-pico-w-http-server) on running an unsecured HTTP server but none on serving content over HTTPS. This example will show you how to run an HTTPS server from a Pico W.
Note: the server is very slow and takes about 6 seconds to respond with a tiny HTML file over HTTPS. In comparison, responding with the same file over HTTP takes 75 to 350 milliseconds.
## Getting started
1. Follow [Adafruit's guide to connecting your Pico W to Wi-Fi](https://learn.adafruit.com/pico-w-wifi-with-circuitpython/overview). Make sure you can run the the basic Wi-Fi test successfully.
2. Clone this repository to your computer: `git clone https://github.com/ide/circuitpython-https-server.git`
3. Copy the contents of the **src** directory to your **CIRCUITPY** volume. On macOS, you can run `scripts/deploy.sh` if you have [`rsync`](https://formulae.brew.sh/formula/rsync) and [`circup`](https://github.com/adafruit/circup) installed. Make sure your **settings.toml** file on your Pico still has your Wi-Fi credentials you configured when following Adafruit's Wi-Fi guide.
4. Connect to your Pico W's serial console. See [Adafruit's guide](https://learn.adafruit.com/welcome-to-circuitpython/kattni-connecting-to-the-serial-console) on how to do this. On macOS you can run `scripts/repl.sh`. Reload the code running in CircuitPython by entering Ctrl-C in the console.
5. The program running on your Pico W will start a web server and print two URLs, one with the Pico W's local IP address (e.g. https://192.168.1.2) and one with its local mDNS hostname (configured to be https://picow.local).
6. Run `curl --insecure https://picow.local` (or specify your Pico W's IP address). After about 10 seconds, you should see a small HTML response.
## Why HTTPS for Pico W? (A better user experience for IoT web apps) ## Why HTTPS for Pico W? (A better user experience for IoT web apps)
In the context of a Pico W serving content to your local network, the main motivation for HTTPS is to enable [web browser features limited to secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts). These include [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), which are needed to implement websites that work offline or use push notifications, two common features you might want in an IoT application. In the context of a Pico W serving content to your local network, the main motivation for HTTPS is to enable [web browser features limited to secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts). These include [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), which are needed to implement websites that work offline or use push notifications, two common features you might want in an IoT application.
Imagine you're at home and you visit your Pico W's homepage from your web browser. You add the web app to your home screen and your phone presents the web app somewhat like a native app with a home screen icon and its own entry in the task switcher. The web app lets you subscribe to push notifications from your Pico that you'll receive even when you're away from home. And, the web app also loads in "offline" mode when you're away from home and can't connect to your Pico. This is what the user experience should be like for web-based IoT applications. Imagine you're at home and you visit your Pico W's homepage from your web browser. You add the web app to your home screen and your phone presents the web app somewhat like a native app with a home screen icon and its own entry in the task switcher. The web app lets you subscribe to push notifications from your Pico that you'll receive even when you're away from home. And, the web app also loads in "offline" mode when you're away from home and can't connect to your Pico. This is what the user experience should be like for private, web-based IoT applications.
The secondary motivation for HTTPS is security. The threat model of your Pico W accessed from your local network is different from a web server accessed from the internet. Your Pico W is already protected by your router and only trusted devices with your Wi-Fi password or physical Ethernet connections can access it. However, defense in depth is a good security principle and HTTPS prevents even your trusted devices from sniffing or tampering with traffic to your Pico W. The secondary motivation for HTTPS is security. The threat model of your Pico W accessed from your local network is different from that of a web server accessed from the internet. Your Pico W is already protected by your router and only trusted devices with your Wi-Fi password or physical Ethernet connections can access it. However, defense in depth is a good security principle and HTTPS prevents even your trusted devices from sniffing or tampering with traffic to your Pico W.
## Goals and non-goals ## Goals and non-goals
The main goal of this repository is to show how to set up a web server that serves content over HTTPS and runs with CircuitPython on a Raspberry Pi Pico W. It's intended for a small, private home network. It uses self-signed certificates and requires installing the CA certificate on client devices. The main goal of this repository is to show how to set up a web server that serves content over HTTPS and runs with CircuitPython on a Raspberry Pi Pico W. It's intended for a small, private home network. It uses self-signed certificates and requires installing the CA certificate on client devices.
There are also several non-goals of this repository, which help keep its scope small. The example server targets only the Pico W and not other boards that CircuitPython supports, though it might happen to work for them, too. C There are also several non-goals of this repository, which help keep its scope small. The repository provides an example, not a Python package. If support for serving content over HTTPS with CircuitPython is actually important, it probably makes sense for Adafruit to steer developers towards a package they provide. The example server targets only the Pico W and not other boards that CircuitPython supports, though it might happen to work for them, too.
4096-bit
## Things to be aware of
This example uses a 1024-bit RSA certificate for performance. With a 1024-bit certificate, the server responds in about 6 seconds, while a 2048-bit certificate causes it to take about 9 seconds. However, 1024-bit certificates are considered cryptographically insecure. This said, the primary motivation of this project is to enable web browser APIs that require HTTPS on Pico W devices running in a private, local network that is already protected.

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
adafruit_httpserver==2.3.0

31
scripts/deploy.sh Executable file
View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
project_directory="$script_directory/.."
if ! command -v rsync &> /dev/null; then
echo 'rsync is not installed on this computer. rsync is used to copy the application code to your device. Install it using the package manager you use with your OS.'
exit 1
fi
if ! command -v circup &> /dev/null; then
echo 'circup is not installed on this computer. circup is used to install CircuitPython dependencies on your device. Install it by following the instructions in the circup repository at: https://github.com/adafruit/circup'
exit 1
fi
echo 'Detecting your CircuitPython device...'
device_path=$(python3 -c 'import circup; print(circup.find_device())')
if [ "$device_path" == 'None' ]; then
echo 'circup could not detect a connected CircuitPython device. Make sure your device is flashed with CircuitPython and connected to your computer with a USB data cable.'
exit 1
fi
echo "Found device at $device_path"
echo 'Copying application code...'
rsync --verbose --recursive --delete --checksum \
--include '/._*' --exclude '/.*' \
--exclude '/boot_out.txt' --exclude '/settings.toml' \
"$project_directory/src/" "$device_path"
echo 'Finished copying application code'

View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Derived from https://www.linode.com/docs/guides/create-a-self-signed-tls-certificate/
openssl req -new -newkey rsa:1024 -x509 -sha256 -days 365 -nodes \
-subj '/CN=picow.local' \
-out "$script_directory/../src/certificates/certificate-chain.pem" \
-keyout "$script_directory/../src/certificates/key.pem"

8
scripts/repl.sh Executable file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
device=$(ls -1 -t /dev/tty.usbmodem*)
# Reattach to an existing session if one exists; otherwise create a new one
screen -D -R -S circuitpython "$device" 115200

View file

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBozCCAQwCCQCwlOkfTi8J9zANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDDAtw
aWNvdy5sb2NhbDAeFw0yMzAyMjgwNTQyNTZaFw0yNDAyMjgwNTQyNTZaMBYxFDAS
BgNVBAMMC3BpY293LmxvY2FsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDq
UQiuhGBzHCLaesEjk6e6Kx7iNAKneaPht0Q6TsvF+IgOpuQFNkvQTU5S7POKDl2c
UGkMT9Q1x4xRdM3u3qT3OmutyAtfL94RkK1qsblJwIH8xop5p7P+gwHTT4UOEicj
MOSYBmkI6YvO/QImGdEoezWBqxKjOzyYLnv4UrwtYQIDAQABMA0GCSqGSIb3DQEB
CwUAA4GBABuzaLw1tcFNYZ0ViWnii1j97otemOFkM31TW8Ohm3zRS6f/aQi+hujN
BYMWIhxlK2cr0zsN+mXCVZN5u2BY0Q/w/7cNMBPNcGG7dJCGoBJRKRbEoBb/AMaO
vjp0giQ8kFOtRcf0Gun5gPdDcmZayZwU1n/V1Q7UjwKXcqe0+eXu
-----END CERTIFICATE-----

16
src/certificates/key.pem Normal file
View file

@ -0,0 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOpRCK6EYHMcItp6
wSOTp7orHuI0Aqd5o+G3RDpOy8X4iA6m5AU2S9BNTlLs84oOXZxQaQxP1DXHjFF0
ze7epPc6a63IC18v3hGQrWqxuUnAgfzGinmns/6DAdNPhQ4SJyMw5JgGaQjpi879
AiYZ0Sh7NYGrEqM7PJgue/hSvC1hAgMBAAECgYEAvZ05s0/4ZO493iM8LDgOoP7I
DTEdfL1Yuw19LtoY2GmYYJL5LqaTj0sfuMd7BRs+8YG4oHfxOFv01u34v/Z38vRa
E0tzSMVz29CRF0LUcko7c8Q2TzeTVsk3tEb3Kuf3tfVh5bhhjSakhUIY7D+Gb6LT
GRIUg72tP9wjayTDsCkCQQD6oyJqxUhxuuiL5D+e0ssp582YruEJK4f5CzRsjmVV
COBbqzRkIMCxPMCJCB+DbiMPlu/6CuxGh6i8rczDlkdfAkEA71SAt9PFx3hLW0hu
OBR9w7BF6B2YTR3cG5+SQXOiF1feMUIaAn/dDr1OxqAQ/hQ/QKUk5JDwquy6phYT
9qWDPwJBAPMqGLccBkgI/ZrTbIILov5aHdcXO88ow7f0jf0QPfG9NebZ+G94c1rB
RU7taZ2a2jtCxjqCJG/dJ/E+cZ4Ei+MCQFu3O3i2/FU7wU0jDbIKEEQc2j1gkgwD
hGVFmovgn15ouuqPlV4d1/4dCAJQNxLXeYHxh5jb/o7SF5ksXswnk4sCQA7Jlj5M
8+7qQV9DrOieFGpXMqlc9J6u9L0Tev2P9uE7a7z61NvLIdskGDpiaYOmADob5nHo
Duv3lSMQg6ql7EE=
-----END PRIVATE KEY-----

86
src/code.py Normal file
View file

@ -0,0 +1,86 @@
import os
import ssl
import time
import traceback
import socketpool
import supervisor
import wifi
from adafruit_httpserver.server import HTTPServer
def main() -> None:
print("Connecting to the local Wi-Fi network...")
wifi.radio.hostname = "picow"
wifi.radio.connect(
os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")
)
print("Connected to the local network")
print(f" IP address = {wifi.radio.ipv4_address}")
print(f" Router = {wifi.radio.ipv4_gateway}")
print(f" DNS server = {wifi.radio.ipv4_dns}")
host = str(wifi.radio.ipv4_address)
pool = socketpool.SocketPool(wifi.radio)
http_server = HTTPServer(pool)
http_server.start(host, port=80, root_path="public_html")
ssl_context = ssl.create_default_context()
# The Pico is the server and does not require a certificate from the client, so disable
# certificate validation by explicitly specifying no verification CAs
ssl_context.load_verify_locations(cadata="")
ssl_context.load_cert_chain(
"certificates/certificate-chain.pem", "certificates/key.pem"
)
tls_pool = TLSServerSocketPool(pool, ssl_context)
https_server = HTTPServer(tls_pool)
https_server.start(host, port=443, root_path="public_html")
print()
print("The web server is listening on:")
print(f" http://{host}")
print(f" https://{host}")
print(f" http://{wifi.radio.hostname}.local")
print(f" https://{wifi.radio.hostname}.local")
print()
while True:
http_server.poll()
try:
https_server.poll()
except OSError as error:
if error.strerror.startswith("MBEDTLS_ERR_"):
print(f"TLS library error {error.strerror} with code {error.errno}")
else:
raise
class TLSServerSocketPool:
def __init__(self, pool, ssl_context):
self._pool = pool
self._ssl_context = ssl_context
@property
def AF_INET(self):
return self._pool.AF_INET
@property
def SOCK_STREAM(self):
return self._pool.SOCK_STREAM
def socket(self, *args, **kwargs):
socket = self._pool.socket(*args, **kwargs)
return self._ssl_context.wrap_socket(socket, server_side=True)
def getaddrinfo(self, *args, **kwargs):
return self._pool.getaddrinfo(*args, **kwargs)
try:
main()
except Exception as exception:
print("".join(traceback.format_exception(exception, limit=8)))
print("Reloading in 3 seconds...")
time.sleep(3)
supervisor.reload()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,3 @@
<!DOCTYPE html>
<title>Hello world &middot; Raspberry Pi Pico W</title>
<p>Hello world from Raspberry Pi Pico W!</p>