Invoer controleren: een bestandsnaam van een bezoeker
In HTML in bestanden kreeg elke pagina een
eigen endpoint met FileResponse. Met tien pagina's wil je liever één endpoint,
waarin de bezoeker de pagina kiest met een query-parameter, zoals ?term= in
Een waarde na een vraagteken: query-parameters. Maar die naam komt van de bezoeker,
net als een formulierveld. Hij kan er meer in zetten dan een paginanaam.
Eén endpoint voor alle pagina's
Maak naast formulier.html een map static/pages met twee pagina's. Eerst
home.html:
<!doctype html>
<html>
<body>
<h1>Welkom in het gastenboek</h1>
</body>
</html>
Maak over.html net zo, met de kop Over dit gastenboek.
FileResponse komt uit fastapi.responses. Zet deze regel bij de imports
bovenaan main.py:
from fastapi.responses import FileResponse
Zet dan dit endpoint onder bericht_plaatsen:
@app.get("/pagina")
async def pagina(naam: str):
return FileResponse(f"static/pages/{naam}")
- Regel 2:
naamheeft geenFormen staat niet tussen accolades in het pad, dus FastAPI haalt hem uit het adres, na?naam=. - Regel 3:
Het pad naar het bestand, met de naam die de bezoeker koos.
Open http://127.0.0.1:8000/pagina?naam=home.html, en daarna
?naam=over.html. Je ziet de twee koppen.
Probeer ook ?naam=oeps.html, een pagina die er niet is. Je browser toont
Internal Server Error, een 500, en in de terminal van je server staat:
RuntimeError: File at path static/pages/oeps.html does not exist.
Het endpoint geeft elke naam door aan FileResponse, ook een naam waar geen
bestand bij hoort.
Doe het na, op je eigen server
In een pad betekent .. de map erboven. Vanuit static/pages is ../.. twee
mappen omhoog: de map van je project, waar main.py staat. Een scriptje
pagina.py vraagt om dat bestand:
import httpx
antwoord = httpx.get("http://127.0.0.1:8000/pagina?naam=../../main.py")
print(antwoord.status_code)
print(antwoord.text)
Uitvoer, het begin:
200
import time
from fastapi import FastAPI, Form, HTTPException
from fastapi.responses import FileResponse
Je server stuurt zijn eigen code. Zo leest een bezoeker elk bestand waar je
server bij kan, zoals gastenboek.db met alle berichten, of in een echt project
een bestand met een geheime sleutel. Met .. uit de bedoelde map klimmen heet
path traversal.
Alleen wat in je lijst staat
Een bestandsnaam controleer je net als de stemming in les 7: met een lijst met wat mag.
PAGINAS = ["home.html", "over.html"]
@app.get("/pagina")
async def pagina(naam: str):
if naam not in PAGINAS:
raise HTTPException(status_code=404, detail="Deze pagina bestaat niet")
return FileResponse(f"static/pages/{naam}")
- Regel 1:
De pagina's die een bezoeker mag zien, bovenaan
main.py, onderSTEMMINGEN. - Regel 5-6:
Staat de naam niet in de lijst, dan bestaat de pagina voor de bezoeker niet: een
404, zoals in Een 404 sturen.FileResponsekomt pas daarna.
Vervang je /pagina door deze. Een script paden.py probeert er drie:
import httpx
for naam in ["home.html", "../../main.py", "oeps.html"]:
antwoord = httpx.get(f"http://127.0.0.1:8000/pagina?naam={naam}")
print(naam, "->", antwoord.status_code)
Je hoeft niet te bedenken welke trucs er met .., / of \ bestaan. Wat niet
in je lijst staat, komt er niet door. Met het endpoint van daarnet gaf
oeps.html een 500, zoals je zelf zag. Nu is dat een nette 404.
Er gaat iets mis
NameError: name 'FileResponse' is not defined
http://127.0.0.1:8000/pagina?naam=home.html geeft Internal Server Error,
en in de terminal van je server staat:
NameError: name 'FileResponse' is not defined
Oorzaak: de regel from fastapi.responses import FileResponse ontbreekt
bovenaan main.py. Je server start wel, maar loopt vast zodra het endpoint
draait.
Oplossing: zet de import bovenaan, onder die uit fastapi. Een script dat
antwoord.json() doet, stopt bij zo'n 500 met een JSONDecodeError: de
echte fout staat in de terminal van je server.
RuntimeError: File at path static/pages/over.html does not exist.
http://127.0.0.1:8000/pagina?naam=over.html geeft Internal Server Error,
en in de terminal van je server staat:
RuntimeError: File at path static/pages/over.html does not exist.
Oorzaak: over.html staat in je lijst, maar het bestand staat niet in
static/pages, of het heet daar anders, zoals over-ons.html. De controle laat de
naam door, en FileResponse vindt het bestand niet.
Oplossing: zet het bestand in static/pages, met precies de naam uit je
lijst.
{"detail":"Deze pagina bestaat niet"} bij een pagina die er wel is
?naam=home geeft een 404, terwijl home.html wel in static/pages
staat.
Oorzaak: in de lijst staat home.html, en home is een andere tekst.
Oplossing: vraag de naam op zoals hij in de lijst staat:
# FOUT
"http://127.0.0.1:8000/pagina?naam=home"
# GOED
"http://127.0.0.1:8000/pagina?naam=home.html"
Meer over FileResponse en de map static/pages: HTML in bestanden.
Opdrachten
Opdracht 1: Predict - Met een path-parameter
Zet dit endpoint tijdelijk onder je andere endpoints. De naam staat nu in het
pad, en .html plakt de server er zelf achter:
@app.get("/pagina/{naam}")
async def pagina_pad(naam: str):
return FileResponse(f"static/pages/{naam}.html")
Een / in een adres schrijf je als %2F als hij geen scheiding mag zijn. Een
script stuurt twee verzoeken:
import httpx
print(httpx.get("http://127.0.0.1:8000/pagina/home").status_code)
print(httpx.get("http://127.0.0.1:8000/pagina/..%2F..%2Fmain").status_code)
Vraag: welke twee statuscodes print het?
Antwoord
200
404
FastAPI maakt van %2F weer een /, en een path-parameter houdt op bij een
/. Er is dus geen endpoint dat bij /pagina/../../main past, en FastAPI
stuurt {"detail":"Not Found"}.
Dat is geluk en geen controle: het slot zit in hoe FastAPI een endpoint zoekt,
niet in jouw code. Met {naam:path}, dat wel een / doorlaat, komt dit
verzoek wél in je functie, en zoekt de server buiten static/pages. Je krijgt
dan een 500 en niet je code, maar alleen omdat er geen main.html is: ook
dat is geluk. Op Windows is ook \ een scheiding in een pad, en een \ past
wel in een path-parameter. De lijst werkt in alle gevallen. Haal het endpoint
weer weg.
Opdracht 2: Modify - Een derde pagina
Voeg een pagina contact.html toe, zodat
http://127.0.0.1:8000/pagina?naam=contact.html een 200 geeft.
Tip
Er zijn twee plekken: de map static/pages en je main.py.
Antwoord
Maak static/pages/contact.html, net als home.html met een eigen kop, en zet
de naam in je lijst:
PAGINAS = ["home.html", "over.html", "contact.html"]
Maak je alleen het bestand, dan krijg je een 404, want de naam staat niet in
de lijst. Zet je alleen de naam in de lijst, dan krijg je een 500, want het
bestand bestaat niet.
Opdracht 3: Make - In je eigen project
Open je gastenboek, of een eigen project dat je bouwde. Geef elk formulierveld een grens op de server, en zet in je HTML dezelfde grens als hulp voor de bezoeker. Test het met een script dat een te lang veld stuurt.
Tip
Zoek in je main.py naar Form(...). Elk van die velden krijgt een
max_length, en een getal wordt een int met ge en le. Zoek ook naar
FileResponse(f": daar kiest een bezoeker misschien een bestand.
Antwoord
Loop deze lijst af:
- Elk tekstveld heeft
max_lengthinForm. - Elk getal is een
intmetgeenle. - Een veld dat niet uit alleen spaties mag bestaan, controleer je met
strip()en een400. - Een keuze uit een paar woorden controleer je tegen een lijst.
- Een bestandsnaam van een bezoeker gaat pas naar
FileResponseals hij in je lijst staat. Heeft je project geen endpoint metFileResponse(f", dan sla je dit punt over. - Een script dat een te lang veld stuurt, krijgt een
422.
In het gastenboek uit de FastAPI-lessen zet je de grenzen in de parameters die
je endpoints al hebben, en de controle op spaties bovenaan de functie. Met de
sessies uit Onthouden op de server: sessies komt in
POST /gastenboek alleen nog het bericht uit het formulier:
@app.post("/gastenboek")
async def gastenboek_opslaan(
bericht: str = Form(..., max_length=80),
sessie_id: str = Cookie(default=""),
):
if not bericht.strip():
raise HTTPException(status_code=400, detail="Vul een bericht in")
Daaronder blijft je functie zoals hij was. Laat sessie_id niet weg: de rest
van de functie gebruikt hem. De naam typt een bezoeker nu bij /registreer;
geef naam daar max_length=30, en weiger daar een naam van alleen spaties.
Heb je de uitbreiding cookies en sessies nog niet gedaan, dan heeft je
POST /gastenboek ook nog naam en wachtwoord, en zet je de grens ook op
naam.
Het bericht krijgt hier 80 tekens en geen 280 zoals in deze reeks. Zo past het
bij Server of browser?, waar je gastenboek
maxlength="80" krijgt; zet die ook in je HTML. Deed je die les al, en staat
de if len(bericht) > 80 daaruit nog in je functie, dan mag hij weg:
max_length=80 doet hetzelfde, alleen krijgt een te lang bericht nu een 422
in plaats van jouw 400.
Zo ziet je main.py er nu uit
import time
from fastapi import FastAPI, Form, HTTPException
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from sqlitedict import SqliteDict
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
STEMMINGEN = ["blij", "neutraal", "boos"]
PAGINAS = ["home.html", "over.html"]
@app.post("/bericht")
async def bericht_plaatsen(
naam: str = Form(..., max_length=30),
bericht: str = Form(..., max_length=280),
cijfer: int = Form(..., ge=1, le=10),
stemming: str = Form(...),
):
if not naam.strip():
raise HTTPException(status_code=400, detail="Vul een naam in")
if stemming not in STEMMINGEN:
raise HTTPException(status_code=400, detail="Kies blij, neutraal of boos")
with SqliteDict("gastenboek.db") as db:
db[f"bericht_{time.time_ns()}"] = {
"naam": naam,
"bericht": bericht,
"cijfer": cijfer,
"stemming": stemming,
}
db.commit()
return {"lengte": len(bericht), "cijfer": cijfer, "stemming": stemming}
@app.get("/pagina")
async def pagina(naam: str):
if naam not in PAGINAS:
raise HTTPException(status_code=404, detail="Deze pagina bestaat niet")
return FileResponse(f"static/pages/{naam}")
Naast main.py staan static/formulier.html en de map static/pages met
home.html en over.html. Een bezoeker kiest nu alleen uit jouw lijst.
Door naar les 9: in de praktijk.