adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.13k stars 1.22k forks source link

`MemoryError` with HTTPS server and `SSLSocket.accept()` on ESP32-S2, Pico W (possibly other) #9003

Open michalpokusa opened 8 months ago

michalpokusa commented 8 months ago

CircuitPython version

Adafruit CircuitPython 9.0.0-beta.2-4-gf23205c822 on 2024-02-20; Adafruit Feather ESP32-S2 TFT with ESP32S2

Code/REPL

import socketpool
import wifi

from lib.adafruit_httpserver import Server, Request, Response

pool = socketpool.SocketPool(wifi.radio)
server = Server(
    pool,
    root_path="/static",
    https=True,
    certfile="cert.pem",
    keyfile="key.pem",
    debug=True,
)

@server.route("/")
def base(request: Request):
    """
    Serve a default static plain text message.
    """
    return Response(request, "Hello from the CircuitPython HTTPS Server!")

server.serve_forever(str(wifi.radio.ipv4_address), 443)

Behavior

This example code should be used with adafruit_httpserver from PR https://github.com/adafruit/Adafruit_CircuitPython_HTTPServer/pull/88.

When using the HTTP server with SSLSocket the accept() method throws MemoryError without additional details. The example code works correctly on ESP32-S3 (tested on MatrixPortal S3).

Traceback (most recent call last):
  File "lib/adafruit_httpserver/server.py", line 444, in poll
MemoryError:

Description

Related to:

Additional information

No response

tannewt commented 8 months ago

Does this work in 8.x? (I don't think it does.) I suspect this is simply due to not enough memory. I wouldn't mark this for 9.0.0 because it is a feature addition.

anecdata commented 8 months ago

This does work with 8.0.0 on Pico W, but Espressif pieces were not in place until very recently.

jerryneedell commented 8 months ago

possibly related to #8942 ?

anecdata commented 8 months ago

On PicoW with the adafruit_httpserver library, after the MemoryError: is caught in httpserver, gc.mem_free() shows ~40KB+ free.

Tested an alternate simple-as-possible socket-level HTTPS server (w/o the library)... free memory before the accept call is ~80KB+, and ~50KB+ before the socket close. But simple requests can be received, and simple responses can be returned.

So it may be a "purely" memory issue (as opposed to something else masquerading as a memory error)... perhaps some combination of CP 9 increased core usage / new memory model + richer adafruit_httpserver library.

"server" code... ```py import time import gc import os import wifi import socketpool import ssl HOST = "" # see below PORT = 443 BACKLOG = 2 MAXBUF = 256 def resp(body): r = "HTTP/1.1 200 OK\r\n" r += "\r\n" r += body return r time.sleep(3) # wait for serial ssid = os.getenv("WIFI_SSID") password = os.getenv("WIFI_PASSWORD") print("Connecting...", end=" ") wifi.radio.connect(ssid, password) print(wifi.radio.ipv4_address) HOST = str(wifi.radio.ipv4_address) pool = socketpool.SocketPool(wifi.radio) print("Create TCP Server socket", (HOST, PORT)) s = pool.socket(pool.AF_INET, pool.SOCK_STREAM) ssl_context = ssl.create_default_context() ssl_context.load_verify_locations(cadata="") ssl_context.load_cert_chain("cert.pem", "key.pem") ss = ssl_context.wrap_socket(s, server_side=True) ss.bind((HOST, PORT)) ss.listen(BACKLOG) print("Listening\n") buf = bytearray(MAXBUF) while True: print(f"Accepting connections (mem={gc.mem_free()})") conn, addr = ss.accept() print("Accepted from", addr) conn.settimeout(None) size = conn.recv_into(buf, MAXBUF) print("Received", buf[:size], size, "bytes") conn.send(resp("nano-https-server").encode()) print(f"Sent response (mem={gc.mem_free()})\n") conn.close() ```
dhalbert commented 6 months ago

@michalpokusa Could you retry with the latest ConnectionManager library? Thanks. https://github.com/adafruit/Adafruit_CircuitPython_ConnectionManager/pull/16 has fixed several other issues that seem similar.

michalpokusa commented 6 months ago

@dhalbert My example code does not use ConnectionManager, adafruit_httpserver also does not use it.

The issue raises MemoryError on .accept() on SSLSocket object, I believe if anything, the CP's ssl or socket module might be related, not any Python code.

dhalbert commented 6 months ago

@michalpokusa Thanks. Currently when you call create_default_context() it creates an SSL context with all the root certificates loaded, etc., which uses up a lot of RAM. It may be that loading the server cert info pushes things over the top. I did some work on refactoring that, but I'm not sure it's going to help. There are also some ESP-IDF LWIP settings that could be changed.

In CPython, create_default_context() takes an optional argument that describes whether the context is going to be used for a server or a client socket. If we said server, it could avoid loading some things into RAM it doesn't need to. I started working on adding that arg but there were some complications.

The amount of SRAM actually available on the ESP32-S2 vs S3 is confusing. The datasheets say the S2 has 320kB and the S3 has 512kB. But in the S3 case, it appears the usable RAM might also be 320kB, because part of the RAM is used for memory caches from the flash, etc. I'm still puzzling this out.

michalpokusa commented 6 months ago

@dhalbert Would it be possible to create a ssl context without loading the root certs? I understand it is necessary when we want to request website on public internet, but when creating a local server maybe it might be reasonable to use it with self-signed certs?

I may also be completely wrong about how this works, in that case please correct me.

dhalbert commented 6 months ago

Would it be possible to create a ssl context without loading the root certs?

Exactly, that's what might help here, if create_default_context() took the right arg. See purpose in the CPython version: https://docs.python.org/3/library/ssl.html#ssl.create_default_context. In the HTTPS server example, your certs are self-signed, and you don't need the regular root certs for the client of the server to connect. Is that right? Or, you only need one root cert.

michalpokusa commented 6 months ago

@dhalbert I created my certs using:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem

There is also a .load_verify_locations() method on SSLContext, but as CP's docs says, it is not implemented. https://docs.circuitpython.org/en/latest/shared-bindings/ssl/index.html#ssl.SSLContext.load_verify_locations