Savladajte Python CGI programiranje od temelja. Ovaj vodič pokriva postavljanje, obradu obrazaca, upravljanje stanjem, sigurnost i njegovo mjesto u modernom webu.
Python CGI programiranje: Sveobuhvatan vodič za izgradnju web sučelja
U svijetu modernog web razvoja, kojim dominiraju sofisticirani okviri poput Djanga, Flaska i FastAPI-ja, pojam CGI (Common Gateway Interface) mogao bi zvučati kao odjek iz davno prošlog vremena. Međutim, odbaciti CGI značilo bi zanemariti temeljnu tehnologiju koja je ne samo pokretala rani dinamični web, već i danas nudi vrijedne lekcije i praktične primjene. Razumijevanje CGI-ja je poput razumijevanja kako motor radi prije nego što naučite voziti automobil; pruža duboko, fundamentalno znanje o interakciji klijent-poslužitelj koja je temelj svih web aplikacija.
Ovaj sveobuhvatan vodič će demistificirati Python CGI programiranje. Istražit ćemo ga od prvih principa, pokazujući vam kako izgraditi dinamična, interaktivna web sučelja koristeći samo Pythonove standardne biblioteke. Bilo da ste student koji uči osnove weba, programer koji radi sa zastarjelim sustavima, ili netko tko djeluje u ograničenom okruženju, ovaj vodič će vas opremiti vještinama za korištenje ove moćne i jednostavne tehnologije.
Što je CGI i zašto je još uvijek važan?
Common Gateway Interface (CGI) je standardni protokol koji definira kako web poslužitelj može komunicirati s vanjskim programima, često zvanim CGI skripte. Kada klijent (poput web preglednika) zatraži određeni URL povezan s CGI skriptom, web poslužitelj ne poslužuje samo statičku datoteku. Umjesto toga, izvršava skriptu i prosljeđuje izlaz skripte natrag klijentu. To omogućuje generiranje dinamičkog sadržaja na temelju korisničkog unosa, upita baze podataka ili bilo koje druge logike koju skripta sadrži.
Zamislite to kao razgovor:
- Klijent poslužitelju: "Želio bih vidjeti resurs na `/cgi-bin/process-form.py` i ovdje su neki podaci iz obrasca koji sam ispunio."
- Poslužitelj CGI skripti: "Stigao je zahtjev za vas. Evo podataka klijenta i informacija o zahtjevu (poput njihove IP adrese, preglednika itd.). Molim vas, pokrenite se i dajte mi odgovor koji trebam poslati natrag."
- CGI skripta poslužitelju: "Obradila sam podatke. Evo HTTP zaglavlja i HTML sadržaja za povratak."
- Poslužitelj klijentu: "Evo dinamičke stranice koju ste zatražili."
Dok su moderni okviri apstrahirali ovu sirovu interakciju, temeljna načela ostaju ista. Dakle, zašto učiti CGI u doba visoko-razinskih okvira?
- Temeljno razumijevanje: Tjerali ste se da naučite osnovnu mehaniku HTTP zahtjeva i odgovora, uključujući zaglavlja, varijable okoline i podatkovne tokove, bez ikakve magije. Ovo znanje je neprocjenjivo za otklanjanje grešaka i optimizaciju performansi bilo koje web aplikacije.
- Jednostavnost: Za jedan, izolirani zadatak, pisanje male CGI skripte može biti značajno brže i jednostavnije od postavljanja cijelog projekta okvira s njegovim usmjeravanjem, modelima i kontrolerima.
- Neovisnost o jeziku: CGI je protokol, a ne biblioteka. CGI skripte možete pisati u Pythonu, Perlu, C++, Rustu ili bilo kojem jeziku koji može čitati sa standardnog ulaza i pisati na standardni izlaz.
- Zastarjeli sustavi i ograničena okruženja: Mnoge starije web aplikacije i neka okruženja dijeljenog hostinga oslanjaju se na CGI ili pružaju podršku samo za njega. Znanje kako raditi s njim može biti ključna vještina. Također je uobičajeno u ugrađenim sustavima s jednostavnim web poslužiteljima.
Postavljanje vašeg CGI okruženja
Prije nego što možete pokrenuti Python CGI skriptu, potreban vam je web poslužitelj koji je konfiguriran za njezino izvršavanje. Ovo je najčešći kamen spoticanja za početnike. Za razvoj i učenje možete koristiti popularne poslužitelje poput Apachea ili čak Pythonov ugrađeni poslužitelj.
Preduvjeti: Web poslužitelj
Ključno je reći vašem web poslužitelju da datoteke u određenom direktoriju (tradicionalno nazvanom `cgi-bin`) ne smiju biti poslužene kao tekst, već ih treba izvršiti, a njihov izlaz poslati pregledniku. Iako se specifični koraci konfiguracije razlikuju, opći principi su univerzalni.
- Apache: Obično trebate omogućiti `mod_cgi` i koristiti `ScriptAlias` direktivu u svojoj konfiguracijskoj datoteci za mapiranje URL putanje na direktorij datotečnog sustava. Također vam je potrebna `Options +ExecCGI` direktiva za taj direktorij kako biste dopustili izvršavanje.
- Nginx: Nginx nema izravan CGI modul poput Apachea. Obično koristi most poput FCGIWrap-a za izvršavanje CGI skripti.
- Pythonov `http.server`: Za jednostavno lokalno testiranje, možete koristiti Pythonov ugrađeni web poslužitelj, koji podržava CGI odmah. Možete ga pokrenuti iz naredbenog retka s: `python3 -m http.server --cgi 8000`. Ovo će pokrenuti poslužitelj na portu 8000 i tretirati sve skripte u poddirektoriju `cgi-bin/` kao izvršne.
Vaš prvi "Hello, World!" u Python CGI-ju
CGI skripta ima vrlo specifičan izlazni format. Prvo mora ispisati sva potrebna HTTP zaglavlja, nakon čega slijedi jedan prazan redak, a zatim tijelo sadržaja (npr. HTML).
Napravimo našu prvu skriptu. Spremi sljedeći kod kao `hello.py` unutar vašeg `cgi-bin` direktorija.
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
# 1. The HTTP Header
# The most important header is Content-Type, which tells the browser what kind of data to expect.
print("Content-Type: text/html;charset=utf-8")
# 2. The Blank Line
# A single blank line is crucial. It separates the headers from the content body.
print()
# 3. The Content Body
# This is the actual HTML content that will be displayed in the browser.
print("<h1>Hello, World!</h1>")
print("<p>This is my first Python CGI script.</p>")
print("<p>It's running on a global web server, accessible to anyone!</p>")
Razložimo ovo:
#!/usr/bin/env python3
: Ovo je "shebang" linija. Na sustavima sličnim Unixu (Linux, macOS), govori operativnom sustavu da izvrši ovu datoteku pomoću Python 3 interpretera.print("Content-Type: text/html;charset=utf-8")
: Ovo je HTTP zaglavlje. Obavještava preglednik da je sljedeći sadržaj HTML i da je kodiran u UTF-8, što je bitno za podršku međunarodnim znakovima.print()
: Ovo ispisuje obaveznu praznu liniju koja odvaja zaglavlja od tijela. Zaboravljanje ovoga je vrlo česta pogreška.- Završne `print` naredbe proizvode HTML koji će korisnik vidjeti.
Naposljetku, morate skriptu učiniti izvršnom. Na Linuxu ili macOS-u, ovu naredbu biste pokrenuli u svom terminalu: `chmod +x cgi-bin/hello.py`. Sada, kada u pregledniku navigirate na `http://vaša-adresa-poslužitelja/cgi-bin/hello.py`, trebali biste vidjeti svoju "Hello, World!" poruku.
Srž CGI-ja: Varijable okoline
Kako web poslužitelj komunicira informacije o zahtjevu našoj skripti? Koristi varijable okoline. To su varijable koje poslužitelj postavlja u izvršnom okruženju skripte, pružajući obilje informacija o dolaznom zahtjevu i samom poslužitelju. Ovo je "Gateway" u Common Gateway Interfaceu.
Ključne CGI varijable okoline
Pythonov `os` modul omogućuje nam pristup tim varijablama. Evo nekih od najvažnijih:
REQUEST_METHOD
: HTTP metoda korištena za zahtjev (npr., 'GET', 'POST').QUERY_STRING
: Sadrži podatke poslane nakon '?' u URL-u. Tako se podaci prosljeđuju u GET zahtjevu.CONTENT_LENGTH
: Duljina podataka poslanih u tijelu zahtjeva, koristi se za POST zahtjeve.CONTENT_TYPE
: MIME tip podataka u tijelu zahtjeva (npr., 'application/x-www-form-urlencoded').REMOTE_ADDR
: IP adresa klijenta koji upućuje zahtjev.HTTP_USER_AGENT
: Korisnički agent klijentovog preglednika (npr., 'Mozilla/5.0...').SERVER_NAME
: Ime hosta ili IP adresa poslužitelja.SERVER_PROTOCOL
: Korišteni protokol, poput 'HTTP/1.1'.SCRIPT_NAME
: Putanja do skripte koja se trenutno izvršava.
Praktični primjer: Dijagnostička skripta
Napravimo skriptu koja prikazuje sve dostupne varijable okoline. Ovo je nevjerojatno koristan alat za otklanjanje grešaka. Spremi ovo kao `diagnostics.py` u vaš `cgi-bin` direktorij i učinite ga izvršnim.
#!/usr/bin/env python3
import os
print("Content-Type: text/html\n")
print("<h1>CGI Environment Variables</h1>")
print("<p>This script displays all environment variables passed by the web server.</p>")
print("<table border='1' style='border-collapse: collapse; width: 80%;'>")
print("<tr><th>Variable</th><th>Value</th></tr>")
# Iterate through all environment variables and print them in a table
for key, value in sorted(os.environ.items()):
print(f"<tr><td>{key}</td><td>{value}</td></tr>")
print("</table>")
Kada pokrenete ovu skriptu, vidjet ćete detaljnu tablicu koja prikazuje svaki dio informacije koji je poslužitelj proslijedio vašoj skripti. Pokušajte dodati upitni niz URL-u (npr., `.../diagnostics.py?name=test&value=123`) i promatrajte kako se mijenja varijabla `QUERY_STRING`.
Obrada korisničkog unosa: Obrasci i podaci
Primarna svrha CGI-ja je obrada korisničkog unosa, obično iz HTML obrazaca. Pythonova standardna biblioteka pruža robusne alate za to. Istražimo kako obraditi dvije glavne HTTP metode: GET i POST.
Prvo, napravimo jednostavan HTML obrazac. Spremi ovu datoteku kao `feedback_form.html` u vašem glavnom web direktoriju (ne u cgi-bin direktoriju).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Global Feedback Form</title>
</head>
<body>
<h1>Submit Your Feedback</h1>
<p>This form demonstrates both GET and POST methods.</p>
<h2>GET Method Example</h2>
<form action="/cgi-bin/form_handler.py" method="GET">
<label for="get_name">Your Name:</label>
<input type="text" id="get_name" name="username">
<br/><br/>
<label for="get_topic">Topic:</label>
<input type="text" id="get_topic" name="topic">
<br/><br/>
<input type="submit" value="Submit with GET">
</form>
<hr>
<h2>POST Method Example (More Features)</h2>
<form action="/cgi-bin/form_handler.py" method="POST">
<label for="post_name">Your Name:</label>
<input type="text" id="post_name" name="username">
<br/><br/>
<label for="email">Your Email:</label>
<input type="email" id="email" name="email">
<br/><br/>
<p>Are you happy with our service?</p>
<input type="radio" id="happy_yes" name="satisfaction" value="yes">
<label for="happy_yes">Yes</label><br>
<input type="radio" id="happy_no" name="satisfaction" value="no">
<label for="happy_no">No</label><br>
<input type="radio" id="happy_neutral" name="satisfaction" value="neutral">
<label for="happy_neutral">Neutral</label>
<br/><br/>
<p>Which products are you interested in?</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">Comments:</label><br>
<textarea id="comments" name="comments" rows="4" cols="50"></textarea>
<br/><br/>
<input type="submit" value="Submit with POST">
</form>
</body>
</html>
Ovaj obrazac šalje svoje podatke skripti naziva `form_handler.py`. Sada moramo napisati tu skriptu. Iako biste mogli ručno parsirati `QUERY_STRING` za GET zahtjeve i čitati sa standardnog ulaza za POST zahtjeve, to je sklono pogreškama i složeno. Umjesto toga, trebali bismo koristiti Pythonov ugrađeni `cgi` modul, koji je dizajniran upravo za tu svrhu.
Klasa `cgi.FieldStorage` je ovdje junak. Ona parsira dolazni zahtjev i pruža sučelje nalik rječniku za podatke obrasca, bez obzira je li poslan putem GET-a ili POST-a.
Evo koda za `form_handler.py`. Spremite ga u svoj `cgi-bin` direktorij i učinite ga izvršnim.
#!/usr/bin/env python3
import cgi
import html
# Create an instance of FieldStorage
# This one object handles both GET and POST requests transparently
form = cgi.FieldStorage()
# Start printing the response
print("Content-Type: text/html\n")
print("<h1>Form Submission Received</h1>")
print("<p>Thank you for your feedback. Here is the data we received:</p>")
# Check if any form data was submitted
if not form:
print("<p><em>No form data was submitted.</em></p>")
else:
print("<table border='1' style='border-collapse: collapse;'>")
print("<tr><th>Field Name</th><th>Value(s)</th></tr>")
# Iterate through all the keys in the form data
for key in form.keys():
# IMPORTANT: Sanitize user input before displaying it to prevent XSS attacks.
# html.escape() converts characters like <, >, & to their HTML entities.
sanitized_key = html.escape(key)
# The .getlist() method is used to handle fields that can have multiple values,
# such as checkboxes. It always returns a list.
values = form.getlist(key)
# Sanitize each value in the list
sanitized_values = [html.escape(v) for v in values]
# Join the list of values into a comma-separated string for display
display_value = ", ".join(sanitized_values)
print(f"<tr><td><strong>{sanitized_key}</strong></td><td>{display_value}</td></tr>")
print("</table>")
# Example of accessing a single value directly
# Use form.getvalue('key') for fields you expect to have only one value.
# It returns None if the key doesn't exist.
username = form.getvalue("username")
if username:
print(f"<h2>Welcome, {html.escape(username)}!</h2>")
Ključne spoznaje iz ove skripte:
- `import cgi` i `import html`: Uvozimo potrebne module. `cgi` za parsiranje obrasca i `html` za sigurnost.
- `form = cgi.FieldStorage()`: Ova jedna linija obavlja sav težak posao. Provjerava varijable okoline (`REQUEST_METHOD`, `CONTENT_LENGTH`, itd.), čita odgovarajući ulazni tok i parsira podatke u objekt jednostavan za korištenje.
- Sigurnost prije svega (`html.escape`): Nikada ne ispisujemo korisnički poslane podatke izravno u naš HTML. To stvara ranjivost Cross-Site Scriptinga (XSS). Funkcija `html.escape()` koristi se za neutraliziranje bilo kakvog zlonamjernog HTML-a ili JavaScripta koji bi napadač mogao poslati.
- `form.keys()`: Možemo iterirati kroz sva poslana imena polja.
- `form.getlist(key)`: Ovo je najsigurniji način dohvaćanja vrijednosti. Budući da obrazac može poslati više vrijednosti za isto ime (npr. potvrdni okviri), `getlist()` uvijek vraća listu. Ako je polje imalo samo jednu vrijednost, to će biti lista s jednim stavkom.
- `form.getvalue(key)`: Ovo je praktičan prečac kada očekujete samo jednu vrijednost. Vraća pojedinačnu vrijednost izravno, ili ako postoji više vrijednosti, vraća listu njih. Vraća `None` ako ključ nije pronađen.
Sada, otvorite `feedback_form.html` u svom pregledniku, ispunite oba obrasca i pogledajte kako skripta svaki put obrađuje podatke drugačije, ali učinkovito.
Napredne CGI tehnike i najbolje prakse
Upravljanje stanjem: Kolačići
HTTP je protokol bez stanja. Svaki zahtjev je neovisan, a poslužitelj nema memorije o prethodnim zahtjevima istog klijenta. Kako bismo stvorili trajno iskustvo (poput košarice za kupnju ili prijavljene sesije), moramo upravljati stanjem. Najčešći način za to je pomoću kolačića.
Kolačić je mali dio podataka koje poslužitelj šalje pregledniku klijenta. Preglednik zatim šalje taj kolačić natrag sa svakim sljedećim zahtjevom istom poslužitelju. CGI skripta može postaviti kolačić ispisivanjem `Set-Cookie` zaglavlja i može čitati dolazne kolačiće iz `HTTP_COOKIE` varijable okoline.
Napravimo jednostavnu skriptu brojača posjetitelja. Spremite ovo kao `cookie_counter.py`.
#!/usr/bin/env python3
import os
import http.cookies
# Load existing cookies from the environment variable
cookie = http.cookies.SimpleCookie(os.environ.get("HTTP_COOKIE"))
visit_count = 0
# Try to get the value of our 'visit_count' cookie
if 'visit_count' in cookie:
try:
# The cookie value is a string, so we must convert it to an integer
visit_count = int(cookie['visit_count'].value)
except ValueError:
# Handle cases where the cookie value is not a valid number
visit_count = 0
# Increment the visit count
visit_count += 1
# Set the cookie for the response. This will be sent as a 'Set-Cookie' header.
# We are setting the new value for 'visit_count'.
cookie['visit_count'] = visit_count
# You can also set cookie attributes like expiration date, path, etc.
# cookie['visit_count']['expires'] = '...'
# cookie['visit_count']['path'] = '/'
# Print the Set-Cookie header first
print(cookie.output())
# Then print the regular Content-Type header
print("Content-Type: text/html\n")
# And finally the HTML body
print("<h1>Cookie-based Visitor Counter</h1>")
print(f"<p>Welcome! This is your visit number: <strong>{visit_count}</strong>.</p>")
print("<p>Refresh this page to see the count increase.</p>")
print("<p><em>(Your browser must have cookies enabled for this to work.)</em></p>")
Ovdje, Pythonov `http.cookies` modul pojednostavljuje parsiranje niza `HTTP_COOKIE` i generiranje `Set-Cookie` zaglavlja. Svaki put kada posjetite ovu stranicu, skripta čita stari broj, inkrementira ga i šalje novu vrijednost natrag kako bi se pohranila u vašem pregledniku.
Otklanjanje grešaka u CGI skriptama: Modul `cgitb`
Kada CGI skripta ne uspije, poslužitelj često vraća generičku "500 Internal Server Error" poruku, što je beskorisno za otklanjanje grešaka. Pythonov `cgitb` (CGI Traceback) modul je spas. Omogućavanjem na vrhu vaše skripte, sve neobrađene iznimke generirat će detaljno, formatirano izvješće izravno u pregledniku.
Da biste ga koristili, jednostavno dodajte ove dvije linije na početak vaše skripte:
import cgitb
cgitb.enable()
Upozorenje: Iako je `cgitb` neprocjenjiv za razvoj, trebali biste ga onemogućiti ili konfigurirati da zapisuje u datoteku u produkcijskom okruženju. Izlaganje detaljnih tragova javnosti može otkriti osjetljive informacije o konfiguraciji i kodu vašeg poslužitelja.
Učitavanje datoteka putem CGI-ja
Objekt `cgi.FieldStorage` također besprijekorno obrađuje učitavanje datoteka. HTML obrazac mora biti konfiguriran s `method="POST"` i, što je ključno, `enctype="multipart/form-data"`.
Napravimo obrazac za učitavanje datoteka, `upload.html`:
<!DOCTYPE html>
<html lang="en">
<head>
<title>File Upload</title>
</head>
<body>
<h1>Upload a File</h1>
<form action="/cgi-bin/upload_handler.py" method="POST" enctype="multipart/form-data">
<label for="userfile">Select a file to upload:</label>
<input type="file" id="userfile" name="userfile">
<br/><br/>
<input type="submit" value="Upload File">
</form>
</body>
</html>
A sada handler, `upload_handler.py`. Napomena: Ova skripta zahtijeva direktorij pod nazivom `uploads` na istoj lokaciji kao i skripta, a web poslužitelj mora imati dozvolu za pisanje u njega.
#!/usr/bin/env python3
import cgi
import os
import html
# Enable detailed error reporting for debugging
import cgitb
cgitb.enable()
print("Content-Type: text/html\n")
print("<h1>File Upload Handler</h1>")
# Directory where files will be saved. SECURITY: This should be a secure, non-web-accessible directory.
upload_dir = './uploads/'
# Create the directory if it doesn't exist
if not os.path.exists(upload_dir):
os.makedirs(upload_dir, exist_ok=True)
# IMPORTANT: Set correct permissions. In a real scenario, this would be more restrictive.
# os.chmod(upload_dir, 0o755)
form = cgi.FieldStorage()
# Get the file item from the form. 'userfile' is the 'name' of the input field.
file_item = form['userfile']
# Check if a file was actually uploaded
if file_item.filename:
# SECURITY: Never trust the filename provided by the user.
# It could contain path characters like '../' (directory traversal attack).
# We use os.path.basename to strip any directory information.
fn = os.path.basename(file_item.filename)
# Create the full path to save the file
file_path = os.path.join(upload_dir, fn)
try:
# Open the file in write-binary mode and write the uploaded data
with open(file_path, 'wb') as f:
f.write(file_item.file.read())
message = f"The file '{html.escape(fn)}' was uploaded successfully!"
print(f"<p style='color: green;'>{message}</p>")
except IOError as e:
message = f"Error saving file: {e}. Check server permissions for the '{upload_dir}' directory."
print(f"<p style='color: red;'>{message}</p>")
else:
message = 'No file was uploaded.'
print(f"<p style='color: orange;'>{message}</p>")
print("<a href='/upload.html'>Upload another file</a>")
Sigurnost: Glavna briga
Budući da su CGI skripte izvršni programi izravno izloženi internetu, sigurnost nije opcija – to je zahtjev. Jedna pogreška može dovesti do kompromitacije poslužitelja.
Validacija i sanitizacija unosa (sprječavanje XSS-a)
Kao što smo već vidjeli, nikada ne smijete vjerovati korisničkom unosu. Uvijek pretpostavite da je zlonamjeran. Prilikom prikazivanja korisnički unesenih podataka natrag na HTML stranici, uvijek ih escape-ajte s `html.escape()` kako biste spriječili Cross-Site Scripting (XSS) napade. Napadač bi inače mogao ubrizgati `