Im ersten Teil haben wir einen kleinen HTTP-Server aufgebaut, der GET-Anfragen beantworten konnte. Damit konnten wir unser Wifi konfigurieren. In diesem Teil wollen wir den HTTP-Server erweitern, sodass er auch POST-Anfragen verarbeiten kann.
Seit dem letzten mal habe ich ein paar Dinge umsortiert. Ich habe den HTTP-Server in die Datei ep_http.py ausgelagert und die Netzwerk-Funktionalität in die Datei ep_network.py. Das dient lediglich der Übersichtlichkeit. Zusätzlich habe ich noch ein paar Blöcke in separate Funktion ausgelagert, damit wir sie recyclen können. Weiterhin läuft der HTTP-Server jetzt in einem separaten Thread. Wir können ihn also nebenher laufen lassen und müssen die eigentliche Arbeit des ESP32 nicht unterbrechen, um HTTP-Anfragen zu beantworten.
Meine Dateien sehen jetzt so aus:
import ep_http import ep_network import _thread manager = ep_network.manager() wlan = manager.connect_or_start_ap() http = ep_http.http_config_server() t = _thread.start_new_thread(http.start, ())
import ujson import network import utime class manager: def __init__(self, credentials_file="credentials.json"): self.credentials_file = credentials_file def connect_or_start_ap(self): try: with open(self.credentials_file, "r") as f: credentials = ujson.load(f) except: credentials = {"wifi":{}} wlan = None if len(credentials["wifi"]) > 0: wlan = network.WLAN(network.STA_IF) wlan.active(True) nets = sorted(wlan.scan(), key=lambda x: x[3], reverse=True) nets = [x for x in nets if x[0].decode("ascii") in credentials["wifi"]] for ssid in credentials["wifi"]: if ssid in [net[0].decode("ascii") for net in nets]: wlan.connect(ssid, credentials["wifi"][ssid]) i = 5 while not wlan.isconnected() and i > 0: utime.sleep(1) i -= 1 if wlan.isconnected(): break if not wlan.isconnected(): wlan.active(False) wlan = None if wlan is None: wlan = network.WLAN(network.AP_IF) wlan.active(True) return wlan
import usocket import ure import uos import ujson class http_config_server: def __init__(self): pass def start(self): s = usocket.socket() ai = usocket.getaddrinfo("0.0.0.0", 80) addr = ai[0][-1] s.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1) s.bind(addr) s.listen(5) while True: client_sock, client_addr = s.accept() req = client_sock.readline() req = self.split_request(req) if req["type"] == "GET": self.process_GET_req(req, client_sock) client_sock.close() def process_GET_req(self, req, sock): header = self.split_header(sock) if req["file"] in ["", "/"]: req["file"] = "config.html" print(req["file"]) fields = self.split_fields(req["query"]) self.process_query(req["file"], fields) self.return_file(req["file"], sock) def return_file(self, file, sock): if file in uos.listdir("html"): with open("html/"+file, "r") as f: sock.write(f.read()) def split_header(self, sock): header = {} while True: h = sock.readline().decode("ascii") if h == "" or h == "\r\n": break if ":" in h: key, val = h.split(": ") header[key] = val[:-2] return header def split_fields(self, query, encoding="application/x-www-form-urlencoded"): fields = {} if encoding == "application/x-www-form-urlencoded": if "=" in query: for field in query.split("&"): key, val = field.split("=") fields[key] = self.urlencode(val) return fields def split_request(self, req): g = ure.search(r'(GET|POST) (.*?) HTTP\/(.*)', req.decode("ascii")) request = { "type": g.group(1), "ressource": g.group(2)[1:], "version": g.group(3)[:-2] } if "?" in request["ressource"]: file, query = request["ressource"].split("?") else: file = request["ressource"] query = "" request["file"] = file request["query"] = query return request def process_query(self, file, fields): if file == "config.html": if ("ssid" in fields) & ("pass" in fields): credentials = {"wifi":{}} try: with open("credentials.json", "r") as f: credentials = ujson.load(f) except: pass credentials["wifi"][fields["ssid"]] = fields["pass"] with open("credentials.json", "w") as f: credentials["wifi"][fields["ssid"]] = fields["pass"] ujson.dump(credentials, f) def urlencode(self, line): d = { r"+": " ", r"%20": "!", r"%21": "!", r"%22": '"', r"%23": "#", r"%24": "$", r"%25": "%", r"%26": "&", r"%27": "'", r"%28": "(", r"%29": ")", r"%2A": "*", r"%2B": "+", r"%2C": ",", r"%2D": "-", r"%2E": ".", r"%2F": "/", r"%3A": ":", r"%3B": ";", r"%3C": "<", r"%3D": "=", r"%3E": ">", r"%3F": "?", r"%4A": "@", r"%5B": "[", r"%5C": "\\", r"%5D": "]", r"%7B": "{", r"%7C": "|", r"%7D": "}", } for key in d: line = line.replace(key, d[key]) return line
process_POST_req
.
Der unterschied zwischen POST- und GET-Requests ist, dass beim POST-Request die Daten nicht in der URL stehen, sondern nach dem Header nicht sichtbar übertragen werden. Aber Vorsicht: nur weil sie für uns nicht sicher sind, heißt das nicht, dass sie vor Angreifern geschützt sind. Dafür müssten wir die Daten verschlüsseln und https verwenden, was wir an dieser Stelle noch nicht tun.
Die neue Funktion macht also fast das gleiche, wie die alte process_GET_req
, nur dass sie nach dem Header noch die POST-Daten einliest. Wir können hier aber nicht warten, bis ein Zeilenende gesendet wird. Das wird hier nämlich nicht gesendet. Stattdessen wird uns im Header mitgeteilt, wie viel wir einlesen sollen:def process_POST_req(self, req, sock): header = self.split_header(sock) if req["file"] in ["", "/"]: req["file"] = "config.html" query = sock.read(int(header["Content-Length"])).decode("ascii") fields = self.split_fields(query) self.process_query(req["file"], fields) self.return_file(req["file"], sock)
Und das wars auch schon. Um das Ganze auszuprobieren, können wir in unserer config.html im <form>-Tag die Methode “get” einfach in “post” ändern.
return_file
-Funktion:def return_file(self, file, sock): if file == "favicon.ico": file = "favicon.png" if file in uos.listdir("html"): with open("html/"+file, "r") as f: sock.write(f.read())
Um jetzt auch noch die IP-Adresse, die in der Adresszeile angezeigt wird, wegzubekommen, können wir den dhcp-hostname ändern. Dadurch wird auch im Router ein sinnvoller Name angezeigt (bisher ist es einfach nur espressiv). Dazu einfach in ep_network.py nach dem wir das WiFi-Interface aktiviert haben folgende Zeile hinzufügen:
wlan.active(True) wlan.config(dhcp_hostname="foo-bar-baz")
Jetzt können wir einfach “http://foo-bar-baz/” oder nur “foo-bar-baz” in die Adresszeile eingeben und gelangen zu unserem ESP32.
Aus diesem kleinen Tutorial habe ich eine kleine Bibliothek zusammengestellt, die ich ständig erweitere. Ihr könnt sie gern kostenlos nutzen: ep_http auf github
Eydam-Prototyping
Saccasner Straße 19
03096 Schmogrow-Fehrow
Germany