Mestre Python CGI-programmering fra grunnen av. Denne dyptgående guiden dekker oppsett, skjemahåndtering, tilstandsstyring, sikkerhet og dets plass i den moderne weben.
Python CGI-programmering: En omfattende guide til å bygge webgrensesnitt
I en verden av moderne webutvikling, dominert av sofistikerte rammeverk som Django, Flask og FastAPI, kan begrepet CGI (Common Gateway Interface) høres ut som et ekko fra en svunnen tid. Men å avfeie CGI er å overse en grunnleggende teknologi som ikke bare drev den tidlige dynamiske weben, men som også fortsetter å tilby verdifulle lærdommer og praktiske anvendelser i dag. Å forstå CGI er som å forstå hvordan en motor fungerer før du lærer å kjøre bil; det gir en dyp, fundamental kunnskap om klient-server-interaksjonen som ligger til grunn for alle webapplikasjoner.
Denne omfattende guiden vil avmystifisere Python CGI-programmering. Vi vil utforske den fra grunnleggende prinsipper og vise deg hvordan du bygger dynamiske, interaktive webgrensesnitt kun ved hjelp av Pythons standardbiblioteker. Enten du er en student som lærer det grunnleggende om web, en utvikler som jobber med eldre systemer, eller noen som opererer i et begrenset miljø, vil denne guiden utstyre deg med ferdighetene til å utnytte denne kraftige og enkle teknologien.
Hva er CGI og hvorfor er det fortsatt relevant?
Common Gateway Interface (CGI) er en standardprotokoll som definerer hvordan en webserver kan samhandle med eksterne programmer, ofte kalt CGI-skript. Når en klient (som en nettleser) ber om en spesifikk URL knyttet til et CGI-skript, serverer ikke webserveren bare en statisk fil. I stedet utfører den skriptet og sender skriptets utdata tilbake til klienten. Dette muliggjør generering av dynamisk innhold basert på brukerinput, databaseforespørsler eller annen logikk skriptet inneholder.
Tenk på det som en samtale:
- Klient til Server: "Jeg vil gjerne se ressursen på `/cgi-bin/process-form.py` og her er litt data fra et skjema jeg fylte ut."
- Server til CGI-skript: "En forespørsel har kommet inn for deg. Her er klientens data og informasjon om forespørselen (som IP-adresse, nettleser, osv.). Vennligst kjør og gi meg responsen som skal sendes tilbake."
- CGI-skript til Server: "Jeg har behandlet dataene. Her er HTTP-headerne og HTML-innholdet som skal returneres."
- Server til Klient: "Her er den dynamiske siden du ba om."
Selv om moderne rammeverk har abstrahert bort denne rå interaksjonen, forblir de underliggende prinsippene de samme. Så hvorfor lære CGI i en tid med høynivårammeverk?
- Grunnleggende forståelse: Det tvinger deg til å lære kjernemekanismene i HTTP-forespørsler og -responser, inkludert headere, miljøvariabler og datastrømmer, uten noe magi. Denne kunnskapen er uvurderlig for feilsøking og ytelsesjustering av enhver webapplikasjon.
- Enkelhet: For en enkelt, isolert oppgave kan det å skrive et lite CGI-skript være betydelig raskere og enklere enn å sette opp et helt rammeverksprosjekt med ruting, modeller og kontrollere.
- Språkuavhengig: CGI er en protokoll, ikke et bibliotek. Du kan skrive CGI-skript i Python, Perl, C++, Rust eller ethvert språk som kan lese fra standard input og skrive til standard output.
- Eldre systemer og begrensede miljøer: Mange eldre webapplikasjoner og noen delte hostingmiljøer er avhengige av eller tilbyr kun støtte for CGI. Å vite hvordan man jobber med det kan være en kritisk ferdighet. Det er også vanlig i innebygde systemer med enkle webservere.
Sette opp ditt CGI-miljø
Før du kan kjøre et Python CGI-skript, trenger du en webserver som er konfigurert til å utføre det. Dette er den vanligste snublesteinen for nybegynnere. For utvikling og læring kan du bruke populære servere som Apache eller til og med Pythons innebygde server.
Forutsetninger: En webserver
Nøkkelen er å fortelle webserveren din at filer i en bestemt katalog (tradisjonelt kalt `cgi-bin`) ikke skal serveres som tekst, men skal utføres, og utdataene deres skal sendes til nettleseren. Selv om de spesifikke konfigurasjonstrinnene varierer, er de generelle prinsippene universelle.
- Apache: Du må vanligvis aktivere `mod_cgi` og bruke et `ScriptAlias`-direktiv i konfigurasjonsfilen din for å kartlegge en URL-sti til en filsystemkatalog. Du trenger også et `Options +ExecCGI`-direktiv for den katalogen for å tillate kjøring.
- Nginx: Nginx har ikke en direkte CGI-modul som Apache. Den bruker vanligvis en bro som FCGIWrap for å utføre CGI-skript.
- Pythons `http.server`: For enkel lokal testing kan du bruke Pythons innebygde webserver, som støtter CGI rett ut av boksen. Du kan starte den fra kommandolinjen med: `python3 -m http.server --cgi 8000`. Dette vil starte en server på port 8000 og behandle alle skript i en `cgi-bin/`-underkatalog som kjørbare.
Ditt første "Hello, World!" i Python CGI
Et CGI-skript har et veldig spesifikt utdataformat. Det må først skrive ut alle nødvendige HTTP-headere, etterfulgt av en enkelt tom linje, og deretter innholdet (f.eks. HTML).
La oss lage vårt første skript. Lagre følgende kode som `hello.py` i `cgi-bin`-katalogen din.
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
# 1. HTTP-headeren
# Den viktigste headeren er Content-Type, som forteller nettleseren hva slags data den skal forvente.
print("Content-Type: text/html;charset=utf-8")
# 2. Den tomme linjen
# En enkelt tom linje er avgjørende. Den skiller headerne fra innholdet.
print()
# 3. Innholdet
# Dette er det faktiske HTML-innholdet som vil bli vist i nettleseren.
print("<h1>Hello, World!</h1>")
print("<p>Dette er mitt første Python CGI-skript.</p>")
print("<p>Det kjører på en global webserver, tilgjengelig for alle!</p>")
La oss bryte dette ned:
#!/usr/bin/env python3
: Dette er "shebang"-linjen. På Unix-lignende systemer (Linux, macOS) forteller den operativsystemet at denne filen skal utføres med Python 3-tolken.print("Content-Type: text/html;charset=utf-8")
: Dette er HTTP-headeren. Den informerer nettleseren om at det følgende innholdet er HTML og er kodet i UTF-8, noe som er essensielt for å støtte internasjonale tegn.print()
: Dette skriver ut den obligatoriske tomme linjen som skiller headere fra innholdet. Å glemme denne er en veldig vanlig feil.- De siste `print`-setningene produserer HTML-en som brukeren vil se.
Til slutt må du gjøre skriptet kjørbart. På Linux eller macOS ville du kjørt denne kommandoen i terminalen din: `chmod +x cgi-bin/hello.py`. Nå, når du navigerer til `http://din-server-adresse/cgi-bin/hello.py` i nettleseren din, bør du se din "Hello, World!"-melding.
Kjernen i CGI: Miljøvariabler
Hvordan kommuniserer webserveren informasjon om forespørselen til skriptet vårt? Den bruker miljøvariabler. Dette er variabler satt av serveren i skriptets kjøremiljø, som gir en mengde informasjon om den innkommende forespørselen og selve serveren. Dette er "Gateway" i Common Gateway Interface.
Viktige CGI-miljøvariabler
Pythons `os`-modul lar oss få tilgang til disse variablene. Her er noen av de viktigste:
REQUEST_METHOD
: HTTP-metoden som brukes for forespørselen (f.eks. 'GET', 'POST').QUERY_STRING
: Inneholder dataene som sendes etter '?' i en URL. Slik sendes data i en GET-forespørsel.CONTENT_LENGTH
: Lengden på dataene som sendes i forespørselskroppen, brukt for POST-forespørsler.CONTENT_TYPE
: MIME-typen til dataene i forespørselskroppen (f.eks. 'application/x-www-form-urlencoded').REMOTE_ADDR
: IP-adressen til klienten som sender forespørselen.HTTP_USER_AGENT
: User-agent-strengen til klientens nettleser (f.eks. 'Mozilla/5.0...').SERVER_NAME
: Vertsnavnet eller IP-adressen til serveren.SERVER_PROTOCOL
: Protokollen som brukes, som 'HTTP/1.1'.SCRIPT_NAME
: Stien til det nåværende kjørende skriptet.
Praktisk eksempel: Et diagnostisk skript
La oss lage et skript som viser alle tilgjengelige miljøvariabler. Dette er et utrolig nyttig verktøy for feilsøking. Lagre dette som `diagnostics.py` i `cgi-bin`-katalogen din og gjør det kjørbart.
#!/usr/bin/env python3
import os
print("Content-Type: text/html\n")
print("<h1>CGI Miljøvariabler</h1>")
print("<p>Dette skriptet viser alle miljøvariabler sendt av webserveren.</p>")
print("<table border='1' style='border-collapse: collapse; width: 80%;'>")
print("<tr><th>Variabel</th><th>Verdi</th></tr>")
# Iterer gjennom alle miljøvariabler og skriv dem ut i en tabell
for key, value in sorted(os.environ.items()):
print(f"<tr><td>{key}</td><td>{value}</td></tr>")
print("</table>")
Når du kjører dette skriptet, vil du se en detaljert tabell som lister opp all informasjon serveren har sendt til skriptet ditt. Prøv å legge til en querystreng i URL-en (f.eks. `.../diagnostics.py?name=test&value=123`) og se hvordan `QUERY_STRING`-variabelen endres.
Håndtering av brukerinput: Skjemaer og data
Hovedformålet med CGI er å behandle brukerinput, vanligvis fra HTML-skjemaer. Pythons standardbibliotek gir robuste verktøy for dette. La oss utforske hvordan vi håndterer de to viktigste HTTP-metodene: GET og POST.
Først, la oss lage et enkelt HTML-skjema. Lagre denne filen som `feedback_form.html` i din hovedwebkatalog (ikke i cgi-bin-katalogen).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Globalt tilbakemeldingsskjema</title>
</head>
<body>
<h1>Send inn din tilbakemelding</h1>
<p>Dette skjemaet demonstrerer både GET- og POST-metoder.</p>
<h2>Eksempel med GET-metode</h2>
<form action="/cgi-bin/form_handler.py" method="GET">
<label for="get_name">Ditt 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="Send med GET">
</form>
<hr>
<h2>Eksempel med POST-metode (flere funksjoner)</h2>
<form action="/cgi-bin/form_handler.py" method="POST">
<label for="post_name">Ditt navn:</label>
<input type="text" id="post_name" name="username">
<br/><br/>
<label for="email">Din e-post:</label>
<input type="email" id="email" name="email">
<br/><br/>
<p>Er du fornøyd med tjenesten vår?</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">Nei</label><br>
<input type="radio" id="happy_neutral" name="satisfaction" value="neutral">
<label for="happy_neutral">Nøytral</label>
<br/><br/>
<p>Hvilke produkter er du interessert i?</p>
<input type="checkbox" id="prod_a" name="products" value="Product A">
<label for="prod_a">Produkt A</label><br>
<input type="checkbox" id="prod_b" name="products" value="Product B">
<label for="prod_b">Produkt B</label><br>
<input type="checkbox" id="prod_c" name="products" value="Product 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="Send med POST">
</form>
</body>
</html>
Dette skjemaet sender dataene sine til et skript kalt `form_handler.py`. Nå må vi skrive det skriptet. Selv om du kunne manuelt parse `QUERY_STRING` for GET-forespørsler og lese fra standard input for POST-forespørsler, er dette feilutsatt og komplekst. I stedet bør vi bruke Pythons innebygde `cgi`-modul, som er designet for akkurat dette formålet.
Klassen `cgi.FieldStorage` er helten her. Den parser den innkommende forespørselen og gir et ordliste-lignende grensesnitt til skjemadataene, uavhengig av om de ble sendt via GET eller POST.
Her er koden for `form_handler.py`. Lagre den i `cgi-bin`-katalogen din og gjør den kjørbar.
#!/usr/bin/env python3
import cgi
import html
# Lag en instans av FieldStorage
# Dette ene objektet håndterer både GET- og POST-forespørsler transparent
form = cgi.FieldStorage()
# Begynn å skrive ut responsen
print("Content-Type: text/html\n")
print("<h1>Skjemainnsending mottatt</h1>")
print("<p>Takk for din tilbakemelding. Her er dataene vi mottok:</p>")
# Sjekk om skjemadata ble sendt inn
if not form:
print("<p><em>Ingen skjemadata ble sendt inn.</em></p>")
else:
print("<table border='1' style='border-collapse: collapse;'>")
print("<tr><th>Feltnavn</th><th>Verdi(er)</th></tr>")
# Iterer gjennom alle nøklene i skjemadataene
for key in form.keys():
# VIKTIG: Saniter brukerinput før du viser det for å forhindre XSS-angrep.
# html.escape() konverterer tegn som <, >, & til deres HTML-entiteter.
sanitized_key = html.escape(key)
# .getlist()-metoden brukes til å håndtere felt som kan ha flere verdier,
# slik som avkrysningsbokser. Den returnerer alltid en liste.
values = form.getlist(key)
# Saniter hver verdi i listen
sanitized_values = [html.escape(v) for v in values]
# Slå sammen listen med verdier til en kommaseparert streng for visning
display_value = ", ".join(sanitized_values)
print(f"<tr><td><strong>{sanitized_key}</strong></td><td>{display_value}</td></tr>")
print("</table>")
# Eksempel på å hente en enkeltverdi direkte
# Bruk form.getvalue('key') for felt du forventer kun har én verdi.
# Den returnerer None hvis nøkkelen ikke finnes.
username = form.getvalue("username")
if username:
print(f"<h2>Velkommen, {html.escape(username)}!</h2>")
Viktige poenger fra dette skriptet:
- `import cgi` og `import html`: Vi importerer de nødvendige modulene. `cgi` for skjema-parsing og `html` for sikkerhet.
- `form = cgi.FieldStorage()`: Denne ene linjen gjør alt det tunge arbeidet. Den sjekker miljøvariablene (`REQUEST_METHOD`, `CONTENT_LENGTH`, osv.), leser den riktige input-strømmen, og parser dataene til et lett-å-bruke objekt.
- Sikkerhet først (`html.escape`): Vi skriver aldri ut brukerinnsendt data direkte i HTML-en vår. Å gjøre det skaper en Cross-Site Scripting (XSS)-sårbarhet. `html.escape()`-funksjonen brukes til å nøytralisere ondsinnet HTML eller JavaScript en angriper kan sende inn.
- `form.keys()`: Vi kan iterere over alle feltnavnene som ble sendt inn.
- `form.getlist(key)`: Dette er den sikreste måten å hente verdier på. Siden et skjema kan sende flere verdier for samme navn (f.eks. avkrysningsbokser), returnerer `getlist()` alltid en liste. Hvis feltet bare hadde én verdi, vil det være en liste med ett element.
- `form.getvalue(key)`: Dette er en praktisk snarvei for når du bare forventer én verdi. Den returnerer den ene verdien direkte, eller hvis det er flere verdier, returnerer den en liste av dem. Den returnerer `None` hvis nøkkelen ikke blir funnet.
Åpne nå `feedback_form.html` i nettleseren din, fyll ut begge skjemaene, og se hvordan skriptet håndterer dataene forskjellig, men effektivt hver gang.
Avanserte CGI-teknikker og beste praksis
Tilstandsstyring: Cookies
HTTP er en tilstandsløs protokoll. Hver forespørsel er uavhengig, og serveren har ingen minne om tidligere forespørsler fra samme klient. For å skape en vedvarende opplevelse (som en handlekurv eller en innlogget økt), må vi administrere tilstand. Den vanligste måten å gjøre dette på er med cookies.
En cookie er en liten databit som serveren sender til klientens nettleser. Nettleseren sender deretter den cookien tilbake med hver påfølgende forespørsel til samme server. Et CGI-skript kan sette en cookie ved å skrive ut en `Set-Cookie`-header og kan lese innkommende cookies fra `HTTP_COOKIE`-miljøvariabelen.
La oss lage et enkelt besøkstellerskript. Lagre dette som `cookie_counter.py`.
#!/usr/bin/env python3
import os
import http.cookies
# Last eksisterende cookies fra miljøvariabelen
cookie = http.cookies.SimpleCookie(os.environ.get("HTTP_COOKIE"))
visit_count = 0
# Prøv å hente verdien til vår 'visit_count'-cookie
if 'visit_count' in cookie:
try:
# Cookie-verdien er en streng, så vi må konvertere den til et heltall
visit_count = int(cookie['visit_count'].value)
except ValueError:
# Håndter tilfeller der cookie-verdien ikke er et gyldig tall
visit_count = 0
# Øk besøkstallet
visit_count += 1
# Sett cookien for responsen. Dette vil bli sendt som en 'Set-Cookie'-header.
# Vi setter den nye verdien for 'visit_count'.
cookie['visit_count'] = visit_count
# Du kan også sette cookie-attributter som utløpsdato, sti, etc.
# cookie['visit_count']['expires'] = '...'
# cookie['visit_count']['path'] = '/'
# Skriv ut Set-Cookie-headeren først
print(cookie.output())
# Deretter skriv ut den vanlige Content-Type-headeren
print("Content-Type: text/html\n")
# Og til slutt HTML-innholdet
print("<h1>Cookie-basert besøksteller</h1>")
print(f"<p>Velkommen! Dette er ditt besøk nummer: <strong>{visit_count}</strong>.</p>")
print("<p>Oppdater denne siden for å se tallet øke.</p>")
print("<p><em>(Nettleseren din må ha cookies aktivert for at dette skal fungere.)</em></p>")
Her forenkler Pythons `http.cookies`-modul parsingen av `HTTP_COOKIE`-strengen og genereringen av `Set-Cookie`-headeren. Hver gang du besøker denne siden, leser skriptet det gamle antallet, øker det, og sender den nye verdien tilbake for å bli lagret i nettleseren din.
Feilsøking av CGI-skript: `cgitb`-modulen
Når et CGI-skript feiler, returnerer serveren ofte en generisk "500 Internal Server Error"-melding, som er lite hjelpsom for feilsøking. Pythons `cgitb`- (CGI Traceback) modul er en livredder. Ved å aktivere den øverst i skriptet ditt, vil eventuelle uhåndterte unntak generere en detaljert, formatert rapport direkte i nettleseren.
For å bruke den, legg bare til disse to linjene i begynnelsen av skriptet ditt:
import cgitb
cgitb.enable()
Advarsel: Selv om `cgitb` er uvurderlig for utvikling, bør du deaktivere den eller konfigurere den til å logge til en fil i et produksjonsmiljø. Å eksponere detaljerte tracebacks for offentligheten kan avsløre sensitiv informasjon om serverens konfigurasjon og kode.
Filopplastinger med CGI
`cgi.FieldStorage`-objektet håndterer også filopplastinger sømløst. HTML-skjemaet må være konfigurert med `method="POST"` og, avgjørende, `enctype="multipart/form-data"`.
La oss lage et filopplastingsskjema, `upload.html`:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Filopplasting</title>
</head>
<body>
<h1>Last opp en fil</h1>
<form action="/cgi-bin/upload_handler.py" method="POST" enctype="multipart/form-data">
<label for="userfile">Velg en fil å laste opp:</label>
<input type="file" id="userfile" name="userfile">
<br/><br/>
<input type="submit" value="Last opp fil">
</form>
</body>
</html>
Og nå håndtereren, `upload_handler.py`. Merk: Dette skriptet krever en katalog ved navn `uploads` på samme sted som skriptet, og webserveren må ha tillatelse til å skrive til den.
#!/usr/bin/env python3
import cgi
import os
import html
# Aktiver detaljert feilrapportering for feilsøking
import cgitb
cgitb.enable()
print("Content-Type: text/html\n")
print("<h1>Filopplastingshåndterer</h1>")
# Katalog hvor filer vil bli lagret. SIKKERHET: Dette bør være en sikker, ikke-web-tilgjengelig katalog.
upload_dir = './uploads/'
# Opprett katalogen hvis den ikke eksisterer
if not os.path.exists(upload_dir):
os.makedirs(upload_dir, exist_ok=True)
# VIKTIG: Sett korrekte tillatelser. I et reelt scenario ville dette vært mer restriktivt.
# os.chmod(upload_dir, 0o755)
form = cgi.FieldStorage()
# Hent fil-elementet fra skjemaet. 'userfile' er 'name' på input-feltet.
file_item = form['userfile']
# Sjekk om en fil faktisk ble lastet opp
if file_item.filename:
# SIKKERHET: Stol aldri på filnavnet som er oppgitt av brukeren.
# Det kan inneholde stitegn som '../' (directory traversal-angrep).
# Vi bruker os.path.basename for å fjerne all kataloginformasjon.
fn = os.path.basename(file_item.filename)
# Lag den fullstendige stien for å lagre filen
file_path = os.path.join(upload_dir, fn)
try:
# Åpne filen i skrive-binær-modus og skriv de opplastede dataene
with open(file_path, 'wb') as f:
f.write(file_item.file.read())
message = f"Filen '{html.escape(fn)}' ble lastet opp vellykket!"
print(f"<p style='color: green;'>{message}</p>")
except IOError as e:
message = f"Feil ved lagring av fil: {e}. Sjekk servertillatelser for '{upload_dir}'-katalogen."
print(f"<p style='color: red;'>{message}</p>")
else:
message = 'Ingen fil ble lastet opp.'
print(f"<p style='color: orange;'>{message}</p>")
print("<a href='/upload.html'>Last opp en annen fil</a>")
Sikkerhet: Den viktigste bekymringen
Fordi CGI-skript er kjørbare programmer som er direkte eksponert for internett, er sikkerhet ikke et alternativ – det er et krav. En enkelt feil kan føre til at serveren blir kompromittert.
Inputvalidering og sanitering (forhindre XSS)
Som vi allerede har sett, må du aldri stole på brukerinput. Anta alltid at det er ondsinnet. Når du viser brukerleverte data tilbake på en HTML-side, må du alltid escape dem med `html.escape()` for å forhindre Cross-Site Scripting (XSS)-angrep. En angriper kan ellers injisere `