Beheers Python CGI-programmeren van de basis. Deze diepgaande gids behandelt installatie, formulierverwerking, statusbeheer, beveiliging en de plek ervan in het moderne web.
Python CGI-programmeren: een uitgebreide gids voor het bouwen van webinterfaces
In de wereld van moderne webontwikkeling, gedomineerd door geavanceerde frameworks zoals Django, Flask en FastAPI, klinkt de term CGI (Common Gateway Interface) misschien als een echo uit een vervlogen tijdperk. CGI afdoen als irrelevant is echter een fundamentele technologie over het hoofd zien die niet alleen het vroege dynamische web aandreef, maar ook vandaag de dag nog waardevolle lessen en praktische toepassingen biedt. CGI begrijpen is als begrijpen hoe een motor werkt voordat je leert autorijden; het biedt een diepe, fundamentele kennis van de client-server interactie die de basis vormt van alle webapplicaties.
Deze uitgebreide gids zal Python CGI-programmeren demystificeren. We zullen het vanuit de eerste beginselen verkennen en u laten zien hoe u dynamische, interactieve webinterfaces bouwt met alleen de standaardbibliotheken van Python. Of u nu een student bent die de fundamenten van het web leert, een ontwikkelaar die met legacy-systemen werkt, of iemand die opereert in een beperkte omgeving, deze gids zal u de vaardigheden bijbrengen om deze krachtige en eenvoudige technologie te benutten.
Wat is CGI en waarom is het nog steeds belangrijk?
De Common Gateway Interface (CGI) is een standaardprotocol dat definieert hoe een webserver kan interageren met externe programma's, vaak CGI-scripts genoemd. Wanneer een client (zoals een webbrowser) een specifieke URL aanvraagt die geassocieerd is met een CGI-script, serveert de webserver niet zomaar een statisch bestand. In plaats daarvan voert het het script uit en stuurt de output van het script terug naar de client. Dit maakt de generatie van dynamische inhoud mogelijk op basis van gebruikersinvoer, databasevragen of elke andere logica die het script bevat.
Zie het als een gesprek:
- Client naar Server: "Ik wil de bron op `/cgi-bin/process-form.py` zien en hier is wat gegevens van een formulier dat ik heb ingevuld."
- Server naar CGI-script: "Er is een verzoek voor jou binnengekomen. Hier zijn de gegevens van de client en informatie over het verzoek (zoals hun IP-adres, browser, enz.). Voer het uit en geef mij de respons om terug te sturen."
- CGI-script naar Server: "Ik heb de gegevens verwerkt. Hier zijn de HTTP-headers en de HTML-inhoud om terug te sturen."
- Server naar Client: "Hier is de dynamische pagina die u hebt aangevraagd."
Hoewel moderne frameworks deze ruwe interactie hebben geabstraheerd, blijven de onderliggende principes hetzelfde. Dus, waarom CGI leren in het tijdperk van high-level frameworks?
- Fundamenteel begrip: Het dwingt je om de kernmechanismen van HTTP-verzoeken en -antwoorden te leren, inclusief headers, omgevingsvariabelen en datastromen, zonder enige magie. Deze kennis is van onschatbare waarde voor het debuggen en afstemmen van de prestaties van elke webapplicatie.
- Eenvoud: Voor een enkele, geïsoleerde taak kan het schrijven van een klein CGI-script aanzienlijk sneller en eenvoudiger zijn dan het opzetten van een heel frameworkproject met zijn routering, modellen en controllers.
- Taalonafhankelijk: CGI is een protocol, geen bibliotheek. U kunt CGI-scripts schrijven in Python, Perl, C++, Rust of elke taal die kan lezen van standaardinvoer en schrijven naar standaarduitvoer.
- Legacy-systemen en beperkte omgevingen: Veel oudere webapplicaties en sommige shared hosting-omgevingen vertrouwen op of bieden alleen ondersteuning voor CGI. Weten hoe ermee te werken kan een cruciale vaardigheid zijn. Het komt ook vaak voor in embedded systemen met eenvoudige webservers.
Uw CGI-omgeving instellen
Voordat u een Python CGI-script kunt uitvoeren, hebt u een webserver nodig die is geconfigureerd om het uit te voeren. Dit is de meest voorkomende struikelblok voor beginners. Voor ontwikkeling en leren kunt u populaire servers zoals Apache of zelfs de ingebouwde server van Python gebruiken.
Vereisten: een webserver
De sleutel is om uw webserver te vertellen dat bestanden in een specifieke directory (traditioneel `cgi-bin` genoemd) niet als tekst moeten worden geserveerd, maar moeten worden uitgevoerd, waarbij de uitvoer naar de browser wordt gestuurd. Hoewel de specifieke configuratiestappen variëren, zijn de algemene principes universeel.
- Apache: U moet meestal `mod_cgi` inschakelen en een `ScriptAlias`-richtlijn in uw configuratiebestand gebruiken om een URL-pad toe te wijzen aan een bestandssysteemdirectory. U hebt ook een `Options +ExecCGI`-richtlijn nodig voor die directory om uitvoering toe te staan.
- Nginx: Nginx heeft geen directe CGI-module zoals Apache. Het gebruikt meestal een brug zoals FCGIWrap om CGI-scripts uit te voeren.
- Python's `http.server`: Voor eenvoudige lokale tests kunt u de ingebouwde webserver van Python gebruiken, die out-of-the-box CGI ondersteunt. U kunt deze starten vanaf uw opdrachtregel met: `python3 -m http.server --cgi 8000`. Dit start een server op poort 8000 en behandelt alle scripts in een `cgi-bin/` subdirectory als uitvoerbaar.
Uw eerste "Hello, World!" in Python CGI
Een CGI-script heeft een zeer specifiek uitvoerformaat. Het moet eerst alle noodzakelijke HTTP-headers afdrukken, gevolgd door een enkele lege regel, en daarna de inhoud (bijv. HTML).
Laten we ons eerste script maken. Sla de volgende code op als `hello.py` in uw `cgi-bin` directory.
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
# 1. De HTTP-header
# De belangrijkste header is Content-Type, die de browser vertelt welk soort gegevens te verwachten.
print("Content-Type: text/html;charset=utf-8")
# 2. De lege regel
# Een enkele lege regel is cruciaal. Het scheidt de headers van de inhoud.
print()
# 3. De inhoud
# Dit is de eigenlijke HTML-inhoud die in de browser wordt weergegeven.
print("<h1>Hallo, Wereld!</h1>")
print("<p>Dit is mijn eerste Python CGI-script.</p>")
print("<p>Het draait op een globale webserver, toegankelijk voor iedereen!</p>")
Laten we dit ontleden:
#!/usr/bin/env python3
: Dit is de "shebang"-regel. Op Unix-achtige systemen (Linux, macOS) vertelt het het besturingssysteem om dit bestand uit te voeren met behulp van de Python 3-interpreter.print("Content-Type: text/html;charset=utf-8")
: Dit is de HTTP-header. Het informeert de browser dat de volgende inhoud HTML is en is gecodeerd in UTF-8, wat essentieel is voor het ondersteunen van internationale tekens.print()
: Dit drukt de verplichte lege regel af die headers van de body scheidt. Dit vergeten is een zeer veelvoorkomende fout.- De laatste `print`-statements produceren de HTML die de gebruiker zal zien.
Ten slotte moet u het script uitvoerbaar maken. Op Linux of macOS zou u dit commando in uw terminal uitvoeren: `chmod +x cgi-bin/hello.py`. Wanneer u nu in uw browser naar `http://your-server-address/cgi-bin/hello.py` navigeert, zou u uw "Hallo, Wereld!"-bericht moeten zien.
De kern van CGI: omgevingsvariabelen
Hoe communiceert de webserver informatie over het verzoek naar ons script? Het gebruikt omgevingsvariabelen. Dit zijn variabelen die door de server worden ingesteld in de uitvoeringsomgeving van het script en een schat aan informatie bieden over het binnenkomende verzoek en de server zelf. Dit is de "Gateway" in Common Gateway Interface.
Belangrijkste CGI-omgevingsvariabelen
Python's `os`-module stelt ons in staat om toegang te krijgen tot deze variabelen. Hier zijn enkele van de belangrijkste:
REQUEST_METHOD
: De HTTP-methode die voor het verzoek is gebruikt (bijv. 'GET', 'POST').QUERY_STRING
: Bevat de gegevens die na de '?' in een URL zijn verzonden. Dit is hoe gegevens worden doorgegeven in een GET-verzoek.CONTENT_LENGTH
: De lengte van de gegevens die in de verzoekbody zijn verzonden, gebruikt voor POST-verzoeken.CONTENT_TYPE
: Het MIME-type van de gegevens in de verzoekbody (bijv. 'application/x-www-form-urlencoded').REMOTE_ADDR
: Het IP-adres van de client die het verzoek doet.HTTP_USER_AGENT
: De user-agent string van de browser van de client (bijv. 'Mozilla/5.0...').SERVER_NAME
: De hostnaam of het IP-adres van de server.SERVER_PROTOCOL
: Het gebruikte protocol, zoals 'HTTP/1.1'.SCRIPT_NAME
: Het pad naar het momenteel uitvoerende script.
Praktisch voorbeeld: een diagnostisch script
Laten we een script maken dat alle beschikbare omgevingsvariabelen weergeeft. Dit is een ongelooflijk nuttig hulpmiddel voor het debuggen. Sla dit op als `diagnostics.py` in uw `cgi-bin` directory en maak het uitvoerbaar.
#!/usr/bin/env python3
import os
print("Content-Type: text/html\n")
print("<h1>CGI Omgevingsvariabelen</h1>")
print("<p>Dit script toont alle omgevingsvariabelen die door de webserver zijn doorgegeven.</p>")
print("<table border='1' style='border-collapse: collapse; width: 80%;'>")
print("<tr><th>Variabele</th><th>Waarde</th></tr>")
# Itereer door alle omgevingsvariabelen en druk ze af in een tabel
for key, value in sorted(os.environ.items()):
print(f"<tr><td>{key}</td><td>{value}</td></tr>")
print("</table>")
Wanneer u dit script uitvoert, ziet u een gedetailleerde tabel met elk stukje informatie dat de server aan uw script heeft doorgegeven. Probeer een query string toe te voegen aan de URL (bijv. `.../diagnostics.py?name=test&value=123`) en observeer hoe de `QUERY_STRING` variabele verandert.
Gebruikersinvoer verwerken: formulieren en gegevens
Het primaire doel van CGI is het verwerken van gebruikersinvoer, meestal van HTML-formulieren. De standaardbibliotheek van Python biedt robuuste tools hiervoor. Laten we eens kijken hoe de twee belangrijkste HTTP-methoden: GET en POST, worden verwerkt.
Laten we eerst een eenvoudig HTML-formulier maken. Sla dit bestand op als `feedback_form.html` in uw hoofdwebdirectory (niet de cgi-bin directory).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wereldwijd Feedbackformulier</title>
</head>
<body>
<h1>Verstuur uw Feedback</h1>
<p>Dit formulier demonstreert zowel de GET- als de POST-methode.</p>
<h2>GET-methode voorbeeld</h2>
<form action="/cgi-bin/form_handler.py" method="GET">
<label for="get_name">Uw Naam:</label>
<input type="text" id="get_name" name="username">
<br/><br/>
<label for="get_topic">Onderwerp:</label>
<input type="text" id="get_topic" name="topic">
<br/><br/>
<input type="submit" value="Verzenden met GET">
</form>
<hr>
<h2>POST-methode voorbeeld (meer functionaliteit)</h2>
<form action="/cgi-bin/form_handler.py" method="POST">
<label for="post_name">Uw Naam:</label>
<input type="text" id="post_name" name="username">
<br/><br/>
<label for="email">Uw E-mail:</label>
<input type="email" id="email" name="email">
<br/><br/>
<p>Bent u tevreden met onze service?</p>
<input type="radio" id="happy_yes" name="satisfaction" value="yes">
<label for="happy_yes">Ja</label><br>
<input type="radio" id="happy_no" name="satisfaction" value="no">
<label for="happy_no">Nee</label><br>
<input type="radio" id="happy_neutral" name="satisfaction" value="neutral">
<label for="happy_neutral">Neutraal</label>
<br/><br/>
<p>In welke producten bent u geïnteresseerd?</p>
<input type="checkbox" id="prod_a" name="products" value="Product A">
<label for="prod_a">Product A</label><br>
<input type="checkbox" id="prod_b" name="products" value="Product B">
<label for="prod_b">Product B</label><br>
<input type="checkbox" id="prod_c" name="products" value="Product C">
<label for="prod_c">Product C</label>
<br/><br/>
<label for="comments">Opmerkingen:</label><br>
<textarea id="comments" name="comments" rows="4" cols="50"></textarea>
<br/><br/>
<input type="submit" value="Verzenden met POST">
</form>
</body>
</html>
Dit formulier stuurt zijn gegevens naar een script genaamd `form_handler.py`. Nu moeten we dat script schrijven. Hoewel u handmatig de `QUERY_STRING` voor GET-verzoeken zou kunnen parsen en van standaardinvoer zou kunnen lezen voor POST-verzoeken, is dit foutgevoelig en complex. In plaats daarvan moeten we Python's ingebouwde `cgi`-module gebruiken, die precies voor dit doel is ontworpen.
De `cgi.FieldStorage`-klasse is hier de held. Deze parseert het binnenkomende verzoek en biedt een dictionary-achtige interface naar de formuliergegevens, ongeacht of deze via GET of POST zijn verzonden.
Hier is de code voor `form_handler.py`. Sla deze op in uw `cgi-bin` directory en maak het uitvoerbaar.
#!/usr/bin/env python3
import cgi
import html
# Maak een instantie van FieldStorage
# Dit object verwerkt zowel GET- als POST-verzoeken transparant
form = cgi.FieldStorage()
# Begin met het afdrukken van de respons
print("Content-Type: text/html\n")
print("<h1>Formulierinzending Ontvangen</h1>")
print("<p>Bedankt voor uw feedback. Hier zijn de gegevens die we hebben ontvangen:</p>")
# Controleer of er formuliergegevens zijn ingediend
if not form:
print("<p><em>Er zijn geen formuliergegevens ingediend.</em></p>")
else:
print("<table border='1' style='border-collapse: collapse;'>")
print("<tr><th>Veldnaam</th><th>Waarde(n)</th></tr>")
# Itereer door alle sleutels in de formuliergegevens
for key in form.keys():
# BELANGRIJK: Sanitize gebruikersinvoer voordat u deze weergeeft om XSS-aanvallen te voorkomen.
# html.escape() converteert tekens zoals <, >, & naar hun HTML-entiteiten.
sanitized_key = html.escape(key)
# De .getlist()-methode wordt gebruikt om velden te verwerken die meerdere waarden kunnen hebben,
# zoals selectievakjes. Het retourneert altijd een lijst.
values = form.getlist(key)
# Sanitize elke waarde in de lijst
sanitized_values = [html.escape(v) for v in values]
# Voeg de lijst met waarden samen tot een door komma's gescheiden string voor weergave
display_value = ", ".join(sanitized_values)
print(f"<tr><td><strong>{sanitized_key}</strong></td><td>{display_value}</td></tr>")
print("</table>")
# Voorbeeld van directe toegang tot een enkele waarde
# Gebruik form.getvalue('key') voor velden waarvan u verwacht dat ze slechts één waarde hebben.
# Het retourneert None als de sleutel niet bestaat.
username = form.getvalue("username")
if username:
print(f"<h2>Welkom, {html.escape(username)}!</h2>")
Belangrijkste lessen uit dit script:
- `import cgi` en `import html`: We importeren de benodigde modules. `cgi` voor formulierparsing en `html` voor beveiliging.
- `form = cgi.FieldStorage()`: Deze ene regel doet al het zware werk. Het controleert de omgevingsvariabelen (`REQUEST_METHOD`, `CONTENT_LENGTH`, enz.), leest de juiste invoerstroom en parseert de gegevens in een gemakkelijk te gebruiken object.
- Beveiliging eerst (`html.escape`): We printen nooit door de gebruiker ingediende gegevens rechtstreeks in onze HTML. Dit creëert een Cross-Site Scripting (XSS)-kwetsbaarheid. De `html.escape()`-functie wordt gebruikt om kwaadaardige HTML of JavaScript die een aanvaller zou kunnen indienen, te neutraliseren.
- `form.keys()`: We kunnen itereren over alle ingediende veldnamen.
- `form.getlist(key)`: Dit is de veiligste manier om waarden op te halen. Aangezien een formulier meerdere waarden voor dezelfde naam kan indienen (bijv. selectievakjes), retourneert `getlist()` altijd een lijst. Als het veld slechts één waarde had, zal het een lijst met één item zijn.
- `form.getvalue(key)`: Dit is een handige snelkoppeling voor wanneer u slechts één waarde verwacht. Het retourneert de enkele waarde rechtstreeks, of als er meerdere waarden zijn, retourneert het een lijst ervan. Het retourneert `None` als de sleutel niet wordt gevonden.
Open nu `feedback_form.html` in uw browser, vul beide formulieren in en zie hoe het script de gegevens elke keer anders maar effectief verwerkt.
Geavanceerde CGI-technieken en best practices
Statusbeheer: cookies
HTTP is een stateless protocol. Elk verzoek is onafhankelijk en de server heeft geen geheugen van eerdere verzoeken van dezelfde client. Om een persistente ervaring te creëren (zoals een winkelwagentje of een ingelogde sessie), moeten we de status beheren. De meest gebruikelijke manier om dit te doen is met cookies.
Een cookie is een klein stukje gegevens dat de server naar de browser van de client stuurt. De browser stuurt die cookie vervolgens terug bij elk volgend verzoek aan dezelfde server. Een CGI-script kan een cookie instellen door een `Set-Cookie`-header af te drukken en kan inkomende cookies lezen uit de `HTTP_COOKIE`-omgevingsvariabele.
Laten we een eenvoudig bezoekersscript maken. Sla dit op als `cookie_counter.py`.
#!/usr/bin/env python3
import os
import http.cookies
# Laad bestaande cookies uit de omgevingsvariabele
cookie = http.cookies.SimpleCookie(os.environ.get("HTTP_COOKIE"))
visit_count = 0
# Probeer de waarde van onze 'visit_count'-cookie op te halen
if 'visit_count' in cookie:
try:
# De cookie-waarde is een string, dus we moeten deze converteren naar een integer
visit_count = int(cookie['visit_count'].value)
except ValueError:
# Behandel gevallen waarin de cookie-waarde geen geldig nummer is
visit_count = 0
# Verhoog de bezoekersaantal
visit_count += 1
# Stel de cookie in voor de respons. Dit wordt verzonden als een 'Set-Cookie'-header.
# We stellen de nieuwe waarde in voor 'visit_count'.
cookie['visit_count'] = visit_count
# U kunt ook cookie-attributen instellen zoals vervaldatum, pad, enz.
# cookie['visit_count']['expires'] = '...'
# cookie['visit_count']['path'] = '/'
# Druk eerst de Set-Cookie header af
print(cookie.output())
# Druk vervolgens de reguliere Content-Type header af
print("Content-Type: text/html\n")
# En tot slot de HTML body
print("<h1>Cookie-gebaseerde Bezoekerssteller</h1>")
print(f"<p>Welkom! Dit is uw bezoeknummer: <strong>{visit_count}</strong>.</p>")
print("<p>Ververs deze pagina om de teller te zien oplopen.</p>")
print("<p><em>(Uw browser moet cookies ingeschakeld hebben om dit te laten werken.)</em></p>")
Hier vereenvoudigt Python's `http.cookies`-module het parsen van de `HTTP_COOKIE`-string en het genereren van de `Set-Cookie`-header. Elke keer dat u deze pagina bezoekt, leest het script de oude telling, verhoogt deze en stuurt de nieuwe waarde terug om in uw browser te worden opgeslagen.
CGI-scripts debuggen: de `cgitb`-module
Wanneer een CGI-script faalt, retourneert de server vaak een generieke "500 Internal Server Error"-melding, wat onbehulpzaam is voor debugging. Python's `cgitb` (CGI Traceback)-module is een redder in nood. Door deze bovenaan uw script in te schakelen, zullen alle onverwerkte uitzonderingen een gedetailleerd, geformatteerd rapport rechtstreeks in de browser genereren.
Om het te gebruiken, voegt u eenvoudig deze twee regels toe aan het begin van uw script:
import cgitb
cgitb.enable()
Waarschuwing: Hoewel `cgitb` van onschatbare waarde is voor ontwikkeling, moet u het uitschakelen of configureren om naar een bestand te loggen in een productieomgeving. Het blootstellen van gedetailleerde tracebacks aan het publiek kan gevoelige informatie over de configuratie en code van uw server onthullen.
Bestanden uploaden met CGI
Het `cgi.FieldStorage`-object verwerkt ook naadloos bestandsuploads. Het HTML-formulier moet geconfigureerd zijn met `method="POST"` en, cruciaal, `enctype="multipart/form-data"`.
Laten we een formulier voor bestandsuploads maken, `upload.html`:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Bestand uploaden</title>
</head>
<body>
<h1>Upload een bestand</h1>
<form action="/cgi-bin/upload_handler.py" method="POST" enctype="multipart/form-data">
<label for="userfile">Selecteer een bestand om te uploaden:</label>
<input type="file" id="userfile" name="userfile">
<br/><br/>
<input type="submit" value="Bestand uploaden">
</form>
</body>
</html>
And now the handler, `upload_handler.py`. Note: This script requires a directory named `uploads` in the same location as the script, and the web server must have permission to write to it.
#!/usr/bin/env python3
import cgi
import os
import html
# Schakel gedetailleerde foutrapportage in voor debugging
import cgitb
cgitb.enable()
print("Content-Type: text/html\n")
print("<h1>Bestandsupload Handler</h1>")
# Directory waar bestanden worden opgeslagen. BEVEILIGING: Dit moet een veilige, niet-web-toegankelijke directory zijn.
upload_dir = './uploads/'
# Maak de directory als deze niet bestaat
if not os.path.exists(upload_dir):
os.makedirs(upload_dir, exist_ok=True)
# BELANGRIJK: Stel de juiste rechten in. In een reëel scenario zou dit restrictiever zijn.
# os.chmod(upload_dir, 0o755)
form = cgi.FieldStorage()
# Haal het bestandsitem uit het formulier. 'userfile' is de 'name' van het invoerveld.
file_item = form['userfile']
# Controleer of er daadwerkelijk een bestand is geüpload
if file_item.filename:
# BEVEILIGING: Vertrouw nooit de bestandsnaam die door de gebruiker is opgegeven.
# Deze kan padtekens bevatten zoals '../' (directory traversal aanval).
# We gebruiken os.path.basename om directory-informatie te verwijderen.
fn = os.path.basename(file_item.filename)
# Maak het volledige pad om het bestand op te slaan
file_path = os.path.join(upload_dir, fn)
try:
# Open het bestand in write-binary modus en schrijf de geüploade gegevens
with open(file_path, 'wb') as f:
f.write(file_item.file.read())
message = f"Het bestand '{html.escape(fn)}' is succesvol geüpload!"
print(f"<p style='color: green;'>{message}</p>")
except IOError as e:
message = f"Fout bij het opslaan van het bestand: {e}. Controleer serverrechten voor de '{upload_dir}' directory."
print(f"<p style='color: red;'>{message}</p>")
else:
message = 'Geen bestand geüpload.'
print(f"<p style='color: orange;'>{message}</p>")
print("<a href='/upload.html'>Nog een bestand uploaden</a>")
Beveiliging: de allerbelangrijkste zorg
Omdat CGI-scripts uitvoerbare programma's zijn die direct aan het internet worden blootgesteld, is beveiliging geen optie – het is een vereiste. Eén enkele fout kan leiden tot een servercompromittering.
Invoervalidatie en sanitatie (XSS voorkomen)
Zoals we al hebben gezien, moet u gebruikersinvoer nooit vertrouwen. Ga er altijd van uit dat het kwaadaardig is. Wanneer u door de gebruiker aangeleverde gegevens teruggeeft in een HTML-pagina, ontsnap deze dan altijd met `html.escape()` om Cross-Site Scripting (XSS)-aanvallen te voorkomen. Een aanvaller zou anders `