-
Notifications
You must be signed in to change notification settings - Fork 31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HTTPS implementation #88
Conversation
5fb4811
to
1b9371a
Compare
@michalpokusa I'm sorry we missed merging this. Is it ready to go? Any retesting needed since various other library changes? Thanks. |
No problem. I didn't push on merging this as although this works (or did when I marked it as ready) on ESP32-S3, it is not compatible with RPico, S2 etc. Recently there were also some changes to the SSL module on CircuitPython, so I was waiting on how things play out. I am not sure whether the MemoryError is fixed on other MCUs or if there are plans to resolve them in future. Meanwhile this PR can be merged as is or I could add a warning that prints info about compatibility or checks the board id, or I can convert it to draft for now. Please tell me what would be best in your opinion. Retest is always good, if you decide to merge I will do it with latest CircuitPython. |
I would be nice to get this merged so it can be used easily with boards that have enough memory (primarily ESP32-S3, but also perhaps some raspberrypi boards with enough memory. Some mention of the memory issue in documentation would help. |
I will retest everything with latest CircuitPython and make sure it works as before. Will also include info about th memory in docs near HTTPS example. |
@anecdata I think we are good to go with this PR. |
Sounds good! I should be able to do some testing this weekend. |
Brief testing so far on:
Client: Typical client output
HTTPS Server code.pyimport time
import os
import storage
import gc
import board
import digitalio
import traceback
import ssl
import adafruit_connection_manager
from adafruit_httpserver import Server, Request, Response, REQUEST_HANDLED_RESPONSE_SENT
time.sleep(3) # wait for serial
print(f"{gc.mem_free()=}")
try: # native wifi: espressif or raspberypi
import wifi
import socketpool
wifi.radio.connect(os.getenv("WIFI_SSID"), os.getenv("WIFI_PASSWORD"))
radio = wifi.radio
ipv4_address = str(wifi.radio.ipv4_address)
except ImportError:
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K
spi = board.SPI()
# WIZnet EVB-Pico or EVB-Pico2 (except W55RP20)
cs = digitalio.DigitalInOut(board.GP17)
rst = digitalio.DigitalInOut(board.GP20)
radio = WIZNET5K(spi, cs, reset=rst, mac='de:ad:be:ef:fe:ee', debug=False)
ipv4_address = radio.pretty_ip(radio.ip_address)
pool = adafruit_connection_manager.get_radio_socketpool(radio)
server = Server(
pool,
root_path="/static",
https=True,
certfile="cert.pem",
keyfile="key.pem",
debug=True,
)
@server.route("/")
def base(request: Request):
resp = f"{storage.getmount("/").label} t={time.monotonic_ns()} mem={gc.mem_free()}"
print(resp)
return Response(request, resp)
server.start(ipv4_address)
while True:
try:
server.poll()
except Exception as ex:
traceback.print_exception(ex, ex, ex.__traceback__) Typical server output:
|
I tested on ESP32-S2, ESP32-S3 and Pico W, I do not own Pico with Ethernet or any of the Pico 2.
I used the MEMENTO board for testing ESP32-S3, and Feather ESP32-S2 TFT. Maybe using the |
HTTP Just to check any regressions, ran all 7 configs above with update: The espressif and ethernet devices still running this morning, just a few non-fatal exceptions that were automatically recovered in code. All 3 raspberrypi devices got hung up seemingly coinciding with a loss of wifi connection and re-connection. Perhaps it's expected to restart the server in that case, but HTTPS I'll make a note to test with Pico W did work with CircuitPython 8.0.0, albeit slowly, but now doesn't have enough RAM. Could be a combination of factors that are contributing to insufficient RAM. I'm not aware of any Pico W variants with extra PSRAM. Pico W may be a lost cause to run more than a simple TLS socket server. Ethernet: ESP32-S2 (4MB PSRAM) lack of memory is perhaps something to do with how RAM is partitioned / allocated in circuitpython 9, and between circuitpython and esp-idf... technically 4MB PSRAM should be plenty if a larger chunk was usable by esp-idf for sockets and such, and perhaps some other uses. ESP32-S3 (4MB PSRAM, but more SRAM than ESP32-S2) should work. It worked early in the year, but there have been updates to esp-idf, and to the core. Hard fault safemode points to something deeper than this library. RP2350-based Pico 2 W & Pimoroni Pico Plus 2 W look promising, both seem to have plenty of memory. But the boards and SDK changes are new, and there could be some lingering socket or related issues causing the delays and timeouts. Sometimes it can recover from a timeout, other times it requires restart. |
To try to isolate where the issues may lie, I'm now bypassing the library and running a relatively simple TLS socket server mimicking HTTPS. The ESP32-S2 still gets The Ethernet boards sometimes work, but usually timeout (often from the sever perspective the response completes, but the client times out anyway). So for now, I'm focusing on the ESP32-S3 and the Client has been the Client code.py#!/usr/bin/env python3
import time
import sys
import requests
import traceback
def duration(start):
return time.time() - start
while True:
for last in (251, 190, 252, 198, 56, 79,): # No S2 (memory)
start = time.time()
try:
url = f"https://192.168.6.{str(last)}:5000"
print(f"Connecting to {url} ...")
with requests.get(url, timeout=(30, 60), verify=False) as resp:
print(f"{resp.status_code} {resp.reason} {resp.content}")
print(f"🟢 Completed in {duration(start):.3f}s")
except requests.exceptions.Timeout as ex:
print(f"🔴\aTimeout in {duration(start):.3f}s")
except Exception as ex:
print(f"🔴\a")
traceback.print_exception(ex, ex, ex.__traceback__)
time.sleep(5) Minimal HTTP TLS socket server code.pyimport time
import gc
import os
import rtc
import storage
import traceback
import ssl
import adafruit_connection_manager
import adafruit_ntp
HOST = "" # see below
PORT = 5000
BACKLOG = 2
MAXBUF = 256
def connect(radio):
if radio.__class__.__name__ == "Radio":
while not radio.connected:
try:
radio.connect(os.getenv("WIFI_SSID"), os.getenv("WIFI_PASSWORD"))
print(f"Connected: {radio.ipv4_address}")
except Exception as ex:
traceback.print_exception(ex, ex, ex.__traceback__)
return str(wifi.radio.ipv4_address)
elif radio.__class__.__name__ == "WIZNET5K":
ipv4_address = radio.pretty_ip(radio.ip_address)
print(f"Connected: {radio.ipv4_address}")
return ipv4_address
return None
def create_response(body):
r = "HTTP/1.1 200 OK\r\n"
r += f"Content-length: {len(body)}\r\n"
r += "\r\n"
r += body
return r.encode()
time.sleep(3) # wait for serial
print(f"{gc.mem_free()=}")
try: # native wifi: espressif or raspberypi
import wifi
import socketpool
radio = wifi.radio
except ImportError:
import board
import digitalio
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K
spi = board.SPI()
# WIZnet EVB-Pico or EVB-Pico2 (except W55RP20)
cs = digitalio.DigitalInOut(board.GP17)
rst = digitalio.DigitalInOut(board.GP20)
radio = WIZNET5K(spi, cs, reset=rst, mac='de:ad:be:ef:fe:ed', debug=False)
pool = adafruit_connection_manager.get_radio_socketpool(radio)
ipv4_address = connect(radio)
HOST = ipv4_address
ntp = adafruit_ntp.NTP(pool, tz_offset=0)
while True:
ipv4_address = connect(radio)
try:
rtc.RTC().datetime = ntp.datetime
print(f"{time.localtime(time.time() - 6*60*60)}")
break
except Exception as ex:
traceback.print_exception(ex, ex, ex.__traceback__)
time.sleep(5)
print("Create TCP Server socket", (HOST, PORT))
s = pool.socket(pool.AF_INET, pool.SOCK_STREAM)
s.setsockopt(pool.SOL_SOCKET, pool.SO_REUSEADDR, 1)
ssl_context = ssl.create_default_context()
ssl_context.load_verify_locations(cadata="")
ssl_context.load_cert_chain("cert.pem", "key.pem")
ssl_context.check_hostname = False
ss = ssl_context.wrap_socket(s, server_side=True)
ss.bind((HOST, PORT))
ss.listen(BACKLOG)
# ss.setblocking(False)
print("Listening\n")
buf = bytearray(MAXBUF)
while True:
try:
ipv4_address = connect(radio)
print(f"Accepting connections (mem={gc.mem_free()})")
conn, addr = ss.accept()
t = time.localtime(time.time() - 6*60*60)
print(f"Accepted from {addr} at {t}")
conn.settimeout(None)
size = conn.recv_into(buf, MAXBUF)
print(f"Received {buf[:size]} {size} bytes")
body = f"{storage.getmount("/").label} {ipv4_address} {gc.mem_free()=} {t}"
r = create_response(body)
conn.send(r)
print(f"Sent {r}\n")
conn.close()
except Exception as ex:
traceback.print_exception(ex, ex, ex.__traceback__) The ESP32-S3 and the I haven't seen a safemode yet, which baffles me since the code sequences are basically cribbed from here (which in turn is cribbed from some jepler code I can no longer find), and from this library. I'll let it run for some time longer, then go back to testing the library on these boards. Update: Adding a |
Back to the library and this PR... for whatever reason, HTTPS Server is behaving better today using the requests client from above, and updated server code: HTTPS Server code.pyimport time
import rtc
import os
import storage
import gc
import traceback
import ssl
import adafruit_connection_manager
import adafruit_ntp
from adafruit_httpserver import Server, Request, Response
def connect(radio):
if radio.__class__.__name__ == "Radio":
while not radio.connected:
try:
radio.connect(os.getenv("WIFI_SSID"), os.getenv("WIFI_PASSWORD"))
print(f"Connected: {radio.ipv4_address}")
except Exception as ex:
traceback.print_exception(ex, ex, ex.__traceback__)
return str(wifi.radio.ipv4_address)
elif radio.__class__.__name__ == "WIZNET5K":
ipv4_address = radio.pretty_ip(radio.ip_address)
if not radio.ip_address:
print(f"Connected: {radio.ipv4_address}")
return ipv4_address
return None
time.sleep(3) # wait for serial
print(f"{gc.mem_free()=}")
try: # native wifi: espressif or raspberypi
import wifi
import socketpool
radio = wifi.radio
except ImportError:
import board
import digitalio
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K
spi = board.SPI()
# WIZnet EVB-Pico or EVB-Pico2 (except W55RP20)
cs = digitalio.DigitalInOut(board.GP17)
rst = digitalio.DigitalInOut(board.GP20)
radio = WIZNET5K(spi, cs, reset=rst, mac='de:ad:be:ef:fe:ed', debug=False)
pool = adafruit_connection_manager.get_radio_socketpool(radio)
ntp = adafruit_ntp.NTP(pool, tz_offset=0)
while True:
ipv4_address = connect(radio)
try:
rtc.RTC().datetime = ntp.datetime
break
except Exception as ex:
traceback.print_exception(ex, ex, ex.__traceback__)
time.sleep(5)
server = Server(
pool,
root_path="/static",
https=True,
certfile="cert.pem",
keyfile="key.pem",
debug=True,
)
@server.route("/")
def base(request: Request):
resp = f"{storage.getmount("/").label} {ipv4_address} {gc.mem_free()=} {time.localtime(time.time() - 6*60*60)}"
print(resp)
return Response(request, resp)
ipv4_address = connect(radio)
server.start(ipv4_address)
while True:
try:
ipv4_address = connect(radio)
server.poll()
except Exception as ex:
traceback.print_exception(ex, ex, ex.__traceback__) No safemode observed yet. Leaving off ESP32-S2 and Pico W due to Initially trying requests to each board every 5+ seconds: ESP32-S3: Looking good so far. Pico 2 W boards: Looking good so far. Ethernet: The two Ethernet boards are generally OK, with a couple of low-level issues:
The first couple of client IP addresses are wrong (the first one I think is the address from the NTP server), I think this is coming from the WIZnet library. Also, the reported transaction durations are way off - client reports these in under 5 seconds.
Encountered these exceptions after a few successful responses, but the server recovered automatically and handled subsequent requests fine. Also, occasional Timeouts on the Ethernet boards (after ~30s, which is the requests connect timeout kwarg). Update: I removed the NTP call in case that was that was making a difference from the first test, and let the boards run all night. ESP32-S3, both Ethernet boards, and both Pico 2 W boards, ran fine all night. Some auto-recovered timeouts on the Ethernet boards. No safemodes on the S3, no timeouts on the Pico 2 Ws, or any other issues. Update: I left each server board running (polling / listening) for over an hour, but without any clients trying to connect. No issues. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went back to the original code:
#88 (comment)
No client running, and the ESP32-S3 goes into safemode relatively soon, and frequently again after reset. safemode.py
(which writes debug data to disk and resets) yields:
supervisor.SafeModeReason.WATCHDOG
microcontroller.ResetReason.WATCHDOG
So I think there is something here, though don't see a material difference between this code and the later test code (even without NTP) that had no issues.
Update: Ran the httpserver_start_and_poll.py
with the addition of import os
and explicit wifi connect, and the changes in the Server init for HTTPS. No client running. It also regularly goes into safemode every 1-5 minutes. safemode.py
yields:
supervisor.SafeModeReason.HARD_FAULT
microcontroller.ResetReason.SOFTWARE
Not seeing any issues on the two RP2350 wifi boards running the original test code with no clients.
@anecdata Do you think the issue is somewhere in the There is little to none information about the specific reason, and I am not as familiar with CP to determine it from your results. |
I would think any entry into safemode is ultimately due to circuitpython core or lower-level code (included SDK/ESP-IDF, MBEDTLS, or other modules). They are hard to debug. edit: ...or possibly disk corruption. I'm trying it on the ESP32-S3, freshly reformatted ( |
The cleaned ESP32-S3 is still going into safemode. I'll try an alternate device. @michalpokusa Can you try this exact file on your ESP32-S3? It will need appropriate |
Good news: I can't reproduce it on a brand new identically-configured ESP32-S3 reverse TFT, so if you can't reproduce it either, I'm going to chalk it up to some permanent flash corruption on the other S3 board. |
@anecdata Which board exactly is the one that is not working well? |
It's an Adafruit ESP32-S3 Feather TFT (non-reverse). It has been in nearly continuous operation for other uses for over two years, so it's not inconceivable it's bad. I have two pretty new ESP32-S3 QT Py also now running fine, along with the new reverse TFT. edit: the feather TFT was perhaps an unfortunate choice of test board, just occurred to me it had been getting regular flash writes from safemode.py and boot.py for about the past year. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now all (3) of the replacement ESP32-S3 boards have run for an hour just listening with no clients, and then for another 3 hours+ (and going) with the client code above looping to each. No issues. Sorry for the bad board rabbit hole. Server reports response times on ESP32-S3 in the 300-400ms range. Client reports slightly longer overall times.
Looks good to me operationally (thanks, @michalpokusa!). The following configs are working:
- ESP32-S3 wifi (N4R2 or better; not tested w/o PSRAM)
- RPi Pico with WIZnet *
- RPi Pico 2 with WIZnet *
- RPI Pico 2 W wifi (and variants)
(* Functional but not quite as robust: minor caveats about timeouts in comments above, and IP address oddities in the first couple of transactions. I don't think these are due to this library.)
The following configs don't have enough memory currently:
- ESP32-S2 wifi
- RPi Pico W wifi
I'll leave this for @dhalbert or @FoamyGuy to merge, pending any code review or other comments.
I ran the file provided by @anecdata without any clients on a nearly new MEMENTO for a whole day:
|
@michalpokusa Is it repeatable on any other device(s)? I'm trying to reproduce it with a You could try to erase flash and clean install to see if that makes any difference. |
I have only one board type with ESP32-S3 - MEMENTO, so unfortunately I am unable to test on other devices. Following your suggestion I will do some more testing on the second unopened MEMENTO and come back with results. |
A crash is a CircuitPython bug. If the server code otherwise works, we can merge it in to the library and open an issue on CircuitPython itself. |
The code works from my perspective, and I think we would benefit from merging and getting it out there for others to use. I found no regressions with HTTP from the HTTPS changes. @michalpokusa on my "bad" board, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @michalpokusa and thanks @anecdata for testing!
I tried a I have a hard time coming up with an explanation for the observations. Something reading or writing past a boundary, and it matters what's in flash (or ram) at that location? Reminds me of that ATB/FTB core issue from a few years ago. update: Got one, but it's update: A few more, always corrupted though. Not much help. |
Updating https://github.com/adafruit/Adafruit_CircuitPython_HTTPServer to 4.6.0 from 4.5.10: > Merge pull request adafruit/Adafruit_CircuitPython_HTTPServer#88 from michalpokusa/https-implementation Updating https://github.com/adafruit/Adafruit_CircuitPython_Bundle/circuitpython_library_list.md to NA from NA: > Updated download stats for the libraries
Depends on #84, #87 and adafruit/circuitpython#9003
⭐ Added:
adafruit_httpserver
with HTTPS🛠️ Updated/Changed:
Server
constructor now acceptshttps
,certfile
andkeyfile
args that together enable HTTPS.accept()
, which includes the time of handling TLS decryption, thus the time is more "correct"🏗️ Refactor:
After fixing the issues mentioned at the top, this code should also work on other platforms than ESP32-S3.