Skip to content
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

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

Open
michalpokusa opened this issue Mar 4, 2024 · 10 comments

Comments

@michalpokusa
Copy link

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 adafruit/Adafruit_CircuitPython_HTTPServer#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

@dhalbert dhalbert added this to the 9.0.0 milestone Mar 5, 2024
@tannewt
Copy link
Member

tannewt commented Mar 5, 2024

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.

@dhalbert dhalbert modified the milestones: 9.0.0, Long term Mar 5, 2024
@anecdata
Copy link
Member

anecdata commented Mar 5, 2024

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

@jerryneedell
Copy link
Collaborator

possibly related to #8942 ?

@anecdata
Copy link
Member

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...
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
Copy link
Collaborator

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

@michalpokusa
Copy link
Author

@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
Copy link
Collaborator

@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
Copy link
Author

@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
Copy link
Collaborator

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
Copy link
Author

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants