Mestr Python CGI-programmering fra bunden. Denne dybdegående guide dækker opsætning, formularhåndtering, state management, sikkerhed og dets plads på det moderne web.
Python CGI-programmering: En Omfattende Guide til at Bygge Web-interfaces
I en verden af moderne webudvikling, domineret af sofistikerede frameworks som Django, Flask og FastAPI, kan udtrykket CGI (Common Gateway Interface) lyde som et ekko fra en svunden tid. Men at afvise CGI er at overse en fundamental teknologi, der ikke kun drev det tidlige dynamiske web, men også fortsat tilbyder værdifulde lektioner og praktiske anvendelser i dag. At forstå CGI er som at forstå, hvordan en motor virker, før man lærer at køre bil; det giver en dyb, fundamental viden om den klient-server-interaktion, der ligger til grund for alle webapplikationer.
Denne omfattende guide vil afmystificere Python CGI-programmering. Vi vil udforske det fra første principper og vise dig, hvordan du bygger dynamiske, interaktive web-interfaces kun ved hjælp af Pythons standardbiblioteker. Uanset om du er studerende, der lærer det grundlæggende om web, en udvikler, der arbejder med legacy-systemer, eller en person, der opererer i et begrænset miljø, vil denne guide udstyre dig med færdighederne til at udnytte denne kraftfulde og ligetil teknologi.
Hvad er CGI, og Hvorfor er det Stadig Vigtigt?
Common Gateway Interface (CGI) er en standardprotokol, der definerer, hvordan en webserver kan interagere med eksterne programmer, ofte kaldet CGI-scripts. Når en klient (som en webbrowser) anmoder om en specifik URL tilknyttet et CGI-script, serverer webserveren ikke blot en statisk fil. I stedet udfører den scriptet og sender scriptets output tilbage til klienten. Dette muliggør generering af dynamisk indhold baseret på brugerinput, databaseforespørgsler eller enhver anden logik, scriptet indeholder.
Tænk på det som en samtale:
- Klient til Server: "Jeg vil gerne se ressourcen på `/cgi-bin/process-form.py`, og her er noget data fra en formular, jeg har udfyldt."
- Server til CGI-script: "En anmodning er kommet ind til dig. Her er klientens data og oplysninger om anmodningen (som deres IP-adresse, browser osv.). Kør venligst og giv mig det svar, jeg skal sende tilbage."
- CGI-script til Server: "Jeg har behandlet dataene. Her er HTTP-headers og HTML-indholdet, der skal returneres."
- Server til Klient: "Her er den dynamiske side, du anmodede om."
Mens moderne frameworks har abstraheret denne rå interaktion væk, forbliver de underliggende principper de samme. Så hvorfor lære CGI i en tidsalder med højniveau-frameworks?
- Grundlæggende Forståelse: Det tvinger dig til at lære de centrale mekanismer i HTTP-requests og -responses, herunder headers, miljøvariabler og datastrømme, uden nogen magi. Denne viden er uvurderlig til fejlfinding og performance-tuning af enhver webapplikation.
- Enkelhed: For en enkelt, isoleret opgave kan det være betydeligt hurtigere og enklere at skrive et lille CGI-script end at oprette et helt framework-projekt med dets routing, modeller og controllere.
- Sproguafhængigt: CGI er en protokol, ikke et bibliotek. Du kan skrive CGI-scripts i Python, Perl, C++, Rust eller ethvert sprog, der kan læse fra standard input og skrive til standard output.
- Legacy-systemer og Begrænsede Miljøer: Mange ældre webapplikationer og nogle delte hosting-miljøer er afhængige af eller understøtter kun CGI. At vide, hvordan man arbejder med det, kan være en kritisk færdighed. Det er også almindeligt i indlejrede systemer med simple webservere.
Opsætning af Dit CGI-miljø
Før du kan køre et Python CGI-script, har du brug for en webserver, der er konfigureret til at udføre det. Dette er den mest almindelige faldgrube for begyndere. Til udvikling og læring kan du bruge populære servere som Apache eller endda Pythons indbyggede server.
Forudsætninger: En Webserver
Nøglen er at fortælle din webserver, at filer i en bestemt mappe (traditionelt navngivet `cgi-bin`) ikke skal serveres som tekst, men skal udføres, og deres output sendes til browseren. Selvom de specifikke konfigurationstrin varierer, er de generelle principper universelle.
- Apache: Du skal typisk aktivere `mod_cgi` og bruge en `ScriptAlias`-direktiv i din konfigurationsfil for at mappe en URL-sti til en mappe i filsystemet. Du har også brug for en `Options +ExecCGI`-direktiv for den pågældende mappe for at tillade eksekvering.
- Nginx: Nginx har ikke et direkte CGI-modul som Apache. Det bruger typisk en bro som FCGIWrap til at udføre CGI-scripts.
- Pythons `http.server`: Til simpel lokal testning kan du bruge Pythons indbyggede webserver, som understøtter CGI som standard. Du kan starte den fra din kommandolinje med: `python3 -m http.server --cgi 8000`. Dette starter en server på port 8000 og behandler alle scripts i en `cgi-bin/` undermappe som eksekverbare.
Dit Første "Hello, World!" i Python CGI
Et CGI-script har et meget specifikt outputformat. Det skal først udskrive alle nødvendige HTTP-headers, efterfulgt af en enkelt blank linje, og derefter indholdet (f.eks. HTML).
Lad os oprette vores første script. Gem følgende kode som `hello.py` i din `cgi-bin`-mappe.
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
# 1. HTTP-headeren
# Den vigtigste header er Content-Type, som fortæller browseren, hvilken type data den skal forvente.
print("Content-Type: text/html;charset=utf-8")
# 2. Den Tomme Linje
# En enkelt tom linje er afgørende. Den adskiller headers fra indholdsdelen.
print()
# 3. Indholdsdelen
# Dette er det faktiske HTML-indhold, der vil blive vist i browseren.
print("<h1>Hello, World!</h1>")
print("<p>Dette er mit første Python CGI-script.</p>")
print("<p>Det kører på en global webserver, tilgængeligt for alle!</p>")
Lad os bryde det ned:
#!/usr/bin/env python3
: Dette er "shebang"-linjen. På Unix-lignende systemer (Linux, macOS) fortæller den operativsystemet, at denne fil skal udføres med Python 3-fortolkeren.print("Content-Type: text/html;charset=utf-8")
: Dette er HTTP-headeren. Den informerer browseren om, at det følgende indhold er HTML og er kodet i UTF-8, hvilket er essentielt for at understøtte internationale tegn.print()
: Dette udskriver den obligatoriske blanke linje, der adskiller headers fra brødteksten. At glemme dette er en meget almindelig fejl.- De sidste `print`-sætninger producerer den HTML, som brugeren vil se.
Til sidst skal du gøre scriptet eksekverbart. På Linux eller macOS ville du køre denne kommando i din terminal: `chmod +x cgi-bin/hello.py`. Nu, når du navigerer til `http://din-server-adresse/cgi-bin/hello.py` i din browser, skulle du se din "Hello, World!"-besked.
Kernen i CGI: Miljøvariabler
Hvordan kommunikerer webserveren information om anmodningen til vores script? Den bruger miljøvariabler. Disse er variabler, som serveren sætter i scriptets eksekveringsmiljø, og de giver et væld af informationer om den indkommende anmodning og serveren selv. Dette er "Gateway" i Common Gateway Interface.
Vigtige CGI-miljøvariabler
Pythons `os`-modul giver os adgang til disse variabler. Her er nogle af de vigtigste:
REQUEST_METHOD
: HTTP-metoden, der bruges til anmodningen (f.eks. 'GET', 'POST').QUERY_STRING
: Indeholder de data, der sendes efter '?' i en URL. Det er sådan, data overføres i en GET-anmodning.CONTENT_LENGTH
: Længden af de data, der sendes i anmodningens brødtekst, brugt til POST-anmodninger.CONTENT_TYPE
: MIME-typen af dataene i anmodningens brødtekst (f.eks. 'application/x-www-form-urlencoded').REMOTE_ADDR
: IP-adressen på klienten, der foretager anmodningen.HTTP_USER_AGENT
: User-agent-strengen fra klientens browser (f.eks. 'Mozilla/5.0...').SERVER_NAME
: Værtsnavnet eller IP-adressen på serveren.SERVER_PROTOCOL
: Protokollen, der bruges, såsom 'HTTP/1.1'.SCRIPT_NAME
: Stien til det script, der aktuelt udføres.
Praktisk Eksempel: Et Diagnostisk Script
Lad os oprette et script, der viser alle tilgængelige miljøvariabler. Dette er et utroligt nyttigt værktøj til fejlfinding. Gem dette som `diagnostics.py` i din `cgi-bin`-mappe og gør det eksekverbart.
#!/usr/bin/env python3
import os
print("Content-Type: text/html\n")
print("<h1>CGI Miljøvariabler</h1>")
print("<p>Dette script viser alle miljøvariabler, som webserveren har overført.</p>")
print("<table border='1' style='border-collapse: collapse; width: 80%;'>")
print("<tr><th>Variabel</th><th>Værdi</th></tr>")
# Gennemgå alle miljøvariabler og udskriv dem i en tabel
for key, value in sorted(os.environ.items()):
print(f"<tr><td>{key}</td><td>{value}</td></tr>")
print("</table>")
Når du kører dette script, vil du se en detaljeret tabel, der lister hver eneste information, serveren har givet videre til dit script. Prøv at tilføje en query-streng til URL'en (f.eks. `.../diagnostics.py?name=test&value=123`) og se, hvordan `QUERY_STRING`-variablen ændrer sig.
Håndtering af Brugerinput: Formularer og Data
Det primære formål med CGI er at behandle brugerinput, typisk fra HTML-formularer. Pythons standardbibliotek giver robuste værktøjer til dette. Lad os undersøge, hvordan man håndterer de to primære HTTP-metoder: GET og POST.
Først skal vi oprette en simpel HTML-formular. Gem denne fil som `feedback_form.html` i din primære web-mappe (ikke i cgi-bin-mappen).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Global Feedbackformular</title>
</head>
<body>
<h1>Indsend din Feedback</h1>
<p>Denne formular demonstrerer både GET- og POST-metoder.</p>
<h2>GET-metode Eksempel</h2>
<form action="/cgi-bin/form_handler.py" method="GET">
<label for="get_name">Dit Navn:</label>
<input type="text" id="get_name" name="username">
<br/><br/>
<label for="get_topic">Emne:</label>
<input type="text" id="get_topic" name="topic">
<br/><br/>
<input type="submit" value="Indsend med GET">
</form>
<hr>
<h2>POST-metode Eksempel (Flere funktioner)</h2>
<form action="/cgi-bin/form_handler.py" method="POST">
<label for="post_name">Dit Navn:</label>
<input type="text" id="post_name" name="username">
<br/><br/>
<label for="email">Din Email:</label>
<input type="email" id="email" name="email">
<br/><br/>
<p>Er du tilfreds med vores 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">Nej</label><br>
<input type="radio" id="happy_neutral" name="satisfaction" value="neutral">
<label for="happy_neutral">Neutral</label>
<br/><br/>
<p>Hvilke produkter er du interesseret i?</p>
<input type="checkbox" id="prod_a" name="products" value="Produkt A">
<label for="prod_a">Produkt A</label><br>
<input type="checkbox" id="prod_b" name="products" value="Produkt B">
<label for="prod_b">Produkt B</label><br>
<input type="checkbox" id="prod_c" name="products" value="Produkt C">
<label for="prod_c">Produkt C</label>
<br/><br/>
<label for="comments">Kommentarer:</label><br>
<textarea id="comments" name="comments" rows="4" cols="50"></textarea>
<br/><br/>
<input type="submit" value="Indsend med POST">
</form>
</body>
</html>
Denne formular sender sine data til et script ved navn `form_handler.py`. Nu skal vi skrive det script. Selvom du kunne parse `QUERY_STRING` manuelt for GET-anmodninger og læse fra standard input for POST-anmodninger, er dette fejlbehæftet og komplekst. I stedet bør vi bruge Pythons indbyggede `cgi`-modul, som er designet til netop dette formål.
`cgi.FieldStorage`-klassen er helten her. Den parser den indkommende anmodning og giver et ordbogslignende interface til formulardataene, uanset om de blev sendt via GET eller POST.
Her er koden til `form_handler.py`. Gem den i din `cgi-bin`-mappe og gør den eksekverbar.
#!/usr/bin/env python3
import cgi
import html
# Opret en instans af FieldStorage
# Dette ene objekt håndterer både GET- og POST-anmodninger transparent
form = cgi.FieldStorage()
# Begynd at udskrive svaret
print("Content-Type: text/html\n")
print("<h1>Formularindsendelse Modtaget</h1>")
print("<p>Tak for din feedback. Her er de data, vi modtog:</p>")
# Tjek om der blev indsendt formulardata
if not form:
print("<p><em>Ingen formulardata blev indsendt.</em></p>")
else:
print("<table border='1' style='border-collapse: collapse;'>")
print("<tr><th>Feltnavn</th><th>Værdi(er)</th></tr>")
# Gennemgå alle nøglerne i formulardataene
for key in form.keys():
# VIGTIGT: Saner brugerinput, før det vises, for at forhindre XSS-angreb.
# html.escape() konverterer tegn som <, >, & til deres HTML-entiteter.
sanitized_key = html.escape(key)
# .getlist()-metoden bruges til at håndtere felter, der kan have flere værdier,
# såsom afkrydsningsfelter. Den returnerer altid en liste.
values = form.getlist(key)
# Saner hver værdi i listen
sanitized_values = [html.escape(v) for v in values]
# Sammenføj listen af værdier til en kommasepareret streng til visning
display_value = ", ".join(sanitized_values)
print(f"<tr><td><strong>{sanitized_key}</strong></td><td>{display_value}</td></tr>")
print("</table>")
# Eksempel på at tilgå en enkelt værdi direkte
# Brug form.getvalue('key') for felter, du forventer kun har én værdi.
# Det returnerer None, hvis nøglen ikke findes.
username = form.getvalue("username")
if username:
print(f"<h2>Velkommen, {html.escape(username)}!</h2>")
Vigtige pointer fra dette script:
- `import cgi` og `import html`: Vi importerer de nødvendige moduler. `cgi` til formularparsing og `html` til sikkerhed.
- `form = cgi.FieldStorage()`: Denne ene linje klarer alt det tunge arbejde. Den tjekker miljøvariablerne (`REQUEST_METHOD`, `CONTENT_LENGTH` osv.), læser den passende input-strøm og parser dataene til et letanvendeligt objekt.
- Sikkerhed Først (`html.escape`): Vi udskriver aldrig brugerindsendte data direkte i vores HTML. At gøre det skaber en Cross-Site Scripting (XSS) sårbarhed. `html.escape()`-funktionen bruges til at neutralisere enhver ondsindet HTML eller JavaScript, en angriber måtte indsende.
- `form.keys()`: Vi kan iterere over alle de indsendte feltnavne.
- `form.getlist(key)`: Dette er den sikreste måde at hente værdier på. Da en formular kan indsende flere værdier for det samme navn (f.eks. afkrydsningsfelter), returnerer `getlist()` altid en liste. Hvis feltet kun havde én værdi, vil det være en liste med ét element.
- `form.getvalue(key)`: Dette er en praktisk genvej, når du kun forventer én værdi. Den returnerer den enkelte værdi direkte, eller hvis der er flere værdier, returnerer den en liste af dem. Den returnerer `None`, hvis nøglen ikke findes.
Åbn nu `feedback_form.html` i din browser, udfyld begge formularer, og se hvordan scriptet håndterer dataene forskelligt, men effektivt, hver gang.
Avancerede CGI-teknikker og Bedste Praksis
State Management: Cookies
HTTP er en statsløs protokol. Hver anmodning er uafhængig, og serveren har ingen hukommelse om tidligere anmodninger fra den samme klient. For at skabe en vedvarende oplevelse (som en indkøbskurv eller en logget-ind session), skal vi administrere tilstand (state). Den mest almindelige måde at gøre dette på er med cookies.
En cookie er en lille stump data, som serveren sender til klientens browser. Browseren sender derefter den cookie tilbage med hver efterfølgende anmodning til den samme server. Et CGI-script kan sætte en cookie ved at udskrive en `Set-Cookie`-header og kan læse indkommende cookies fra `HTTP_COOKIE`-miljøvariablen.
Lad os oprette et simpelt script, der tæller besøgende. Gem dette som `cookie_counter.py`.
#!/usr/bin/env python3
import os
import http.cookies
# Indlæs eksisterende cookies fra miljøvariablen
cookie = http.cookies.SimpleCookie(os.environ.get("HTTP_COOKIE"))
visit_count = 0
# Prøv at hente værdien af vores 'visit_count' cookie
if 'visit_count' in cookie:
try:
# Cookie-værdien er en streng, så vi skal konvertere den til et heltal
visit_count = int(cookie['visit_count'].value)
except ValueError:
# Håndter tilfælde, hvor cookie-værdien ikke er et gyldigt tal
visit_count = 0
# Forøg besøgstallet
visit_count += 1
# Sæt cookien for svaret. Dette vil blive sendt som en 'Set-Cookie'-header.
# Vi sætter den nye værdi for 'visit_count'.
cookie['visit_count'] = visit_count
# Du kan også sætte cookie-attributter som udløbsdato, sti osv.
# cookie['visit_count']['expires'] = '...'
# cookie['visit_count']['path'] = '/'
# Udskriv først Set-Cookie-headeren
print(cookie.output())
# Udskriv derefter den almindelige Content-Type-header
print("Content-Type: text/html\n")
# Og til sidst HTML-brødteksten
print("<h1>Cookie-baseret Besøgstæller</h1>")
print(f"<p>Velkommen! Dette er dit besøg nummer: <strong>{visit_count}</strong>.</p>")
print("<p>Genopfrisk denne side for at se tælleren stige.</p>")
print("<p><em>(Din browser skal have cookies aktiveret, for at dette virker.)</em></p>")
Her simplificerer Pythons `http.cookies`-modul parsing af `HTTP_COOKIE`-strengen og generering af `Set-Cookie`-headeren. Hver gang du besøger denne side, læser scriptet det gamle antal, forøger det og sender den nye værdi tilbage, som gemmes i din browser.
Fejlfinding i CGI-scripts: `cgitb`-modulet
Når et CGI-script fejler, returnerer serveren ofte en generisk "500 Internal Server Error"-besked, hvilket er unyttigt til fejlfinding. Pythons `cgitb` (CGI Traceback)-modul er en livredder. Ved at aktivere det øverst i dit script, vil enhver uhåndteret undtagelse generere en detaljeret, formateret rapport direkte i browseren.
For at bruge det, skal du blot tilføje disse to linjer i begyndelsen af dit script:
import cgitb
cgitb.enable()
Advarsel: Selvom `cgitb` er uvurderligt under udvikling, bør du deaktivere det eller konfigurere det til at logge til en fil i et produktionsmiljø. At eksponere detaljerede tracebacks for offentligheden kan afsløre følsomme oplysninger om din servers konfiguration og kode.
Filuploads med CGI
`cgi.FieldStorage`-objektet håndterer også filuploads problemfrit. HTML-formularen skal konfigureres med `method="POST"` og, afgørende, `enctype="multipart/form-data"`.
Lad os oprette en filupload-formular, `upload.html`:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Filupload</title>
</head>
<body>
<h1>Upload en Fil</h1>
<form action="/cgi-bin/upload_handler.py" method="POST" enctype="multipart/form-data">
<label for="userfile">Vælg en fil at uploade:</label>
<input type="file" id="userfile" name="userfile">
<br/><br/>
<input type="submit" value="Upload Fil">
</form>
</body>
</html>
Og nu handleren, `upload_handler.py`. Bemærk: Dette script kræver en mappe ved navn `uploads` på samme placering som scriptet, og webserveren skal have tilladelse til at skrive til den.
#!/usr/bin/env python3
import cgi
import os
import html
# Aktiver detaljeret fejlrapportering til fejlfinding
import cgitb
cgitb.enable()
print("Content-Type: text/html\n")
print("<h1>Filupload Handler</h1>")
# Mappe, hvor filer vil blive gemt. SIKKERHED: Dette bør være en sikker, ikke-web-tilgængelig mappe.
upload_dir = './uploads/'
# Opret mappen, hvis den ikke eksisterer
if not os.path.exists(upload_dir):
os.makedirs(upload_dir, exist_ok=True)
# VIGTIGT: Sæt korrekte tilladelser. I et rigtigt scenarie ville dette være mere restriktivt.
# os.chmod(upload_dir, 0o755)
form = cgi.FieldStorage()
# Hent fil-elementet fra formularen. 'userfile' er 'name' på input-feltet.
file_item = form['userfile']
# Tjek om en fil rent faktisk blev uploadet
if file_item.filename:
# SIKKERHED: Stol aldrig på filnavnet, som brugeren angiver.
# Det kan indeholde sti-tegn som '../' (directory traversal-angreb).
# Vi bruger os.path.basename til at fjerne enhver mappeinformation.
fn = os.path.basename(file_item.filename)
# Opret den fulde sti til at gemme filen
file_path = os.path.join(upload_dir, fn)
try:
# Åbn filen i skriv-binær tilstand og skriv de uploadede data
with open(file_path, 'wb') as f:
f.write(file_item.file.read())
message = f"Filen '{html.escape(fn)}' blev uploadet succesfuldt!"
print(f"<p style='color: green;'>{message}</p>")
except IOError as e:
message = f"Fejl ved lagring af fil: {e}. Tjek servertilladelser for mappen '{upload_dir}'."
print(f"<p style='color: red;'>{message}</p>")
else:
message = 'Ingen fil blev uploadet.'
print(f"<p style='color: orange;'>{message}</p>")
print("<a href='/upload.html'>Upload en anden fil</a>")
Sikkerhed: Den Vigtigste Bekymring
Fordi CGI-scripts er eksekverbare programmer, der er direkte eksponeret for internettet, er sikkerhed ikke en mulighed – det er et krav. En enkelt fejl kan føre til et kompromitteret serversystem.
Inputvalidering og Sanering (Forebyggelse af XSS)
Som vi allerede har set, må du aldrig stole på brugerinput. Antag altid, at det er ondsindet. Når du viser brugerleverede data tilbage på en HTML-side, skal du altid escape dem med `html.escape()` for at forhindre Cross-Site Scripting (XSS) angreb. En angriber kunne ellers injicere `