In diesem Beitrag möchte ich dir zeigen, wie du eine Webseite auf dem Raspberry Pi Pico W mit einem Login Dialog absichern kannst.
Wie du eine Webseite auf dem Raspberry Pi Pico W programmierst und veröffentlichst, habe ich dir bereits im Beitrag Raspberry Pi Pico W – Webseite ins Internet veröffentlichen erläutert.
Programmieren einer kleinen Seite mit einem Login Dialog
Erstellen wir zunächst eine kleine Webseite mit den Feldern für Benutzername & Passwort und einer Schaltfläche “Login”.
Das Styling in CSS und die JavaScript-Dateien lege ich auf einem Server ab:
- http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/css/style.css
- http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/js/jquery-3.6.3.min.js
- http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/js/functions.js
Die Webseite schreibe ich zunächst in einem normalen Texteditor wie dem Notepad++. Das macht das Testen der Seite deutlich einfacher, da man nicht immer das Programm neu auf den Mikrocontroller schreiben muss.
Da wir später den HTML-Code in eine Variable im MicroPython-Code ablegen wollen, dürfen wir innerhalb des Codes nur die doppelten Anführungszeichen verwenden!
Wenn der HTML-Code fertig ist, dann muss dieser noch mit einem Tool komprimiert werden, dazu werden die unnötigen Leerzeilen & Zeilenumbrüche entfernt, hier nutze ich TextFixer.
Nachfolgend nun die kleine Seite mit dem formatierten Login Dialog für den Raspberry Pi Pico W.
Die Felder “{serverIP}” & “{messages}” werden später im MicroPyton-Code durch die IP-Adresse des Mikrocontrollers bzw. der Meldungen ersetzt.
<!DOCTYPE html> <html> <head> <title>Raspberry Pi Pico W</title> <link rel="stylesheet" type="text/css" href="http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/css/style.css"/> <script src="http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/js/jquery-3.6.3.min.js"></script> <script src="http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/js/functions.js"></script> </head> <script> var serverIp= "{serverIP}"; </script> <body> <div class="outer"> <h2>Anmelden</h2> <center> <input type="text" id="inpUsername" name="inpUsername" placeholder="Benutzername"></input><br/><br/> <input type="text" id="inpPassword" name="inpPassword" placeholder="Passwort"></input><br/><br/> <input type="button" id="loginBtn" value="Login"/> </center> <br> <div class="messages">{messages}</div> </div> </body> </html>
Wenn wir diesen HTML-Code nun mit TextFixer komprimieren, erhalten wir folgende reduzierten Text:
<!DOCTYPE html><html><head><title>Raspberry Pi Pico W</title><link rel="stylesheet" type="text/css" href="http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/css/style.css"/><script src="http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/js/jquery-3.6.3.min.js"></script><script src="http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/js/functions.js"></script></head><script> var serverIp= "192.168.178.70";</script><body><div class="outer"><h2>Anmelden</h2><center><input type="text" id="inpUsername" name="inpUsername" placeholder="Benutzername"></input><br/><br/><input type="text" id="inpPassword" name="inpPassword" placeholder="Passwort"></input><br/><br/><input type="button" id="loginBtn" value="Login"/></center><br><div class="messages"></div></div></body></html>
JavaScript Funktion zum Absenden der Formulardaten
Wenn der Benutzer die Schaltfläche “Login” betätigt, wird eine JavaScript-Funktion ausgeführt. Hier nutzte ich wieder jQuery dieses JavaScript Framework ist sehr leichtgewichtig und vor allem sehr gut Dokumentiert.
Wenn die Seite fertig geladen ist, dann binden wir an den Login Button die Funktion “click” welche wiederum die Daten aus den Feldern “inpUsername” & “inpPassword” entnimmt und diese wiederum zu einer Adresszeile zusammenfügt.
$( document ).ready(function() { $('#loginBtn').on( "click", function() { var username = $("#inpUsername").val(); var password = $("#inpPassword").val(); window.open("http://"+serverIp+"?username="+username+"&password="+password,'_self'); }); });
Hier finden wir auch wieder unser Feld “serverIp” selcher im HTML-Code als JavaScript Block eingefügt wurde.
<script> var serverIp= "{serverIP}"; </script>
Styling per CSS
Das Styling des Dialoges habe ich in einer separaten CSS Datei abgelegt, dieses hält den eigentlichen HTML-Code sehr schlank und belegt nicht zusätzlich Speicher auf dem Mikrocontroller.
h2{text-align:center;} input[type=text]{border-radius: 4px;border: 1px solid gray;height: 26px;padding: 3px;} input[type=text]:focus{background-color:#FEF9E7;outline: none !important;border: 1px solid gray;} input[type=button]{transition-duration: 0.4s;border-radius: 8px;padding: 10px 24px; background-color: white; color: black; border: 2px solid #008CBA;} input[type=button]:hover {background-color: #008CBA; color: white;} input[type=text], input[type=button]{width:200px} label{display: inline-block;width: 95px;text-align: right;padding-right: 10px;} .outer{width:250px;margin:0px auto;border:1px solid #BFC9CA;padding:25px;box-shadow: #E5E8E8 6px 6px 2px; margin-top: 60px;border-radius:2px;} .messages{color:red;}
Programmieren in MicroPython auf dem Pi Pico W
Da wir das Frontend soeben fertiggestellt haben, müssen wir das passende Backend entwickeln. Gerne möchte ich dir hier meine Lösung präsentieren.
Die Werte für Benutzername & Passwort werden als GET Parameter an die URL gehängt, dies ist ein kleines Sicherheitsrisiko da jeder dieses Passwort lesen kann.
Exkurs – Aufbau einer WLAN-Verbindung am Pi Pico W
Schauen wir uns zunächst einmal kurz an wie man eine WLAN-Verbindung am Pi Pico W aufbaut.
Den nachfolgenden Code habe ich bereits im Beitrag Raspberry Pi Pico W – Webseite ins Internet veröffentlichen vorgestellt und erläutert. Jedoch dient dieser als Ausgangsbasis für unser Login Dialog.
import network import socket import time from machine import Pin ssid = '***' password = '****' wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(ssid, password) html = '<!DOCTYPE html><html><head><title>Raspberry Pi Pico W</title></head><body><h1>Hello World!</h1></body></html>' print('waiting for connection...') max_wait = 10 while max_wait > 0: if wlan.status() < 0 or wlan.status() >= 3: break max_wait -= 1 print('.', end='') time.sleep(1) print('') if wlan.status() != 3: raise RuntimeError('network connection failed') else: print('connected') status = wlan.ifconfig() print('ip = ' + status[0]) addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1] s = socket.socket() s.bind(addr) s.listen(1) print('listening on', addr) while True: try: cl, addr = s.accept() print('client connected from', addr) cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n') cl.send(html) cl.close() except OSError as e: cl.close() print('connection closed')
Login Dialog einbauen
Zunächst legen wir ein Feld für den HTML-Code unserer Seiten an. Zum einen für den Login Dialog und eine Seite, wenn der Benutzer erfolgreich eingeloggt wurde.
htmlPage = '<!DOCTYPE html><html><head><title>Raspberry Pi Pico W</title><link rel="stylesheet" type="text/css" href="http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/css/style.css"/><script src="http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/js/jquery-3.6.3.min.js"></script><script src="http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/js/functions.js"></script></head><script> var serverIp= "{serverIp}";</script><body><div class="outer"><h2>Anmelden</h2><center><input type="text" id="inpUsername" name="inpUsername" placeholder="Benutzername"></input><br/><br/><input type="text" id="inpPassword" name="inpPassword" placeholder="Passwort"></input><br/><br/><input type="button" id="loginBtn" value="Login"/></center><br><div class="messages">{messages}</div></div></body></html>' loggedInPage = "<!DOCTYPE html><html><head><title>Raspberry Pi Pico W</title><link rel='stylesheet' type='text/css' href='http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/css/style.css'/></head><body><h1>Hallo {username}</h1><br/></body></html>"
Dictionary mit Benutzern
Die Benutzer speichern wir in einem Dictionary, wobei der Key der Benutzername ist.
users = { "sdraeger": { "password":"draeger" }, "mmustermann": { "password":"mustermann" } }
Auslesen der GET Parameter
Wenn wir die Schaltfläche Login betätigen, dann wird der HTTP-Request zusammengebaut mit den Benutzername & Passwort aus den entsprechenden Feldern.
Diese Werte finden wir in unserem Code wieder, wenn wir uns das Request Objekt ausgeben:
cl, addr = s.accept() print('client connected from', addr) request = cl.recv(1024) request = str(request) print(request)
Diesen Text können wir nun parsen und die Werte entnehmen.
b'GET /?username=sdraeger&password=draeger HTTP/1.1\r\nHost:
Parsen des Requests
Wie man erkennt, beginnen die Parameter am Index 8 und enden mit ” HTTP”. Hier können wir recht einfach mit Python Logik an diesen Substring gelangen.
Dann prüfen wir, ob die Schlüsselwörter “username” und “password” darin enthalten sind. Wenn dieses nicht der Fall ist, soll der Login Dialog ausgeliefert werden.
params = request[8:request.find(' HTTP')] if "username" in params and "password" in params: values = params.split("&") username = values[0].split("=")[1] password = values[1].split("=")[1]
Wenn diese Schlüsselwörter enthalten sind, werden die Werte geparst. Im nächsten Schritt muss nun geprüft werden ob der Benutzername als Key im Dictionary “users” hinterlegt ist.
if username in users: messages.append("user found")
Wenn der Benutzername hinterlegt wurde, dann wird eine Message gespeichert und das Passwort geprüft.
Wenn wiederum das Passwort korrekt ist, dann wird die Variable “loginOK” auf True gesetzt andernfalls verbleibt diese auf False.
if users[username]["password"] ==password: messages.append("login OK") loginOk = True else: messages.append("login fail")
Auswerten des Login Prozesses und ausliefern der Seite
Wenn die Werte geprüft wurden, dann soll eine entsprechende Seite ausgeliefert werden.
if loginOk: cl.send(loggedInPage.format(username=username)) else: if messages: loginMessages = messages else: loginMessages = "" cl.send(htmlPage.format(serverIp=serverIpAdress, messages=loginMessages))
In meinem Fall zeige ich dem angemeldeten Benutzer lediglich eine kleine Seite mit einer Begrüßung an.
Wenn der Login nicht erfolgreich war, dann wir im Dialog eine entsprechende Meldung ausgegeben.
fertiger MicroPython-Code
Hier nun der komplette MicroPython-Code zum einfachen Download als ZIP-Datei oder zum kopieren.
import network import socket import time from machine import Pin ssid = '***' password = '***' users = { "sdraeger": { "password":"draeger" }, "mmustermann": { "password":"mustermann" } } htmlPage = '<!DOCTYPE html><html><head><title>Raspberry Pi Pico W</title><link rel="stylesheet" type="text/css" href="http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/css/style.css"/><script src="http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/js/jquery-3.6.3.min.js"></script><script src="http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/js/functions.js"></script></head><script> var serverIp= "{serverIp}";</script><body><div class="outer"><h2>Anmelden</h2><center><input type="text" id="inpUsername" name="inpUsername" placeholder="Benutzername"></input><br/><br/><input type="text" id="inpPassword" name="inpPassword" placeholder="Passwort"></input><br/><br/><input type="button" id="loginBtn" value="Login"/></center><br><div class="messages">{messages}</div></div></body></html>' loggedInPage = "<!DOCTYPE html><html><head><title>Raspberry Pi Pico W</title><link rel='stylesheet' type='text/css' href='http://progs.ressourcen-draeger-it.de/raspberrypipicow/login/css/style.css'/></head><body><h1>Hallo {username}</h1><br/></body></html>" wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(ssid, password) html = '<!DOCTYPE html><html><head><title>Raspberry Pi Pico W</title></head><body><h1>Hello World!</h1></body></html>' print('waiting for connection...') max_wait = 10 while max_wait > 0: if wlan.status() < 0 or wlan.status() >= 3: break max_wait -= 1 print('.', end='') time.sleep(1) print('') if wlan.status() != 3: raise RuntimeError('network connection failed') else: print('connected') status = wlan.ifconfig() print('ip = ' + status[0]) addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1] s = socket.socket() s.bind(addr) s.listen(1) print('listening on', addr) while True: try: cl, addr = s.accept() print('client connected from', addr) request = cl.recv(1024) request = str(request) print(request) params = request[8:request.find(' HTTP')] serverIpAdress = status[0] messages = [] loginOk = False cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n') if "username" in params and "password" in params: values = params.split("&") username = values[0].split("=")[1] password = values[1].split("=")[1] if username in users: messages.append("user found") if users[username]["password"] ==password: messages.append("login OK") loginOk = True else: messages.append("login fail") else: messages.append("user not found") if loginOk: cl.send(loggedInPage.format(username=username)) else: if messages: loginMessages = messages else: loginMessages = "" cl.send(htmlPage.format(serverIp=serverIpAdress, messages=loginMessages)) cl.close() except OSError as e: cl.close() print('connection closed')