CGI
Common Gateway Interface je protokol, který popisuje komunikaci mezi programem a webovým serverem. Je to standard, který podporuje mnoho serverů. V této kapitole popíši, jak rozchodit CGI se serverem Apache a jak napsat takový CGI program, se kterým si bude Apache rozumět.
O CGI
CGI byl původně jediný způsob, jak vytvářet stránky s dynamicky se měnícím obsahem. Při každém požadavku na stránku se programem obsah stránky generuje znovu, většinou trochu jiný (jinak by nemělo smysl jej generovat dynamicky).
Později bylo CGI vytlačeno moduly skriptovacích jazyků, jako je třeba PHP, Python, servlet kontainery v Javě, jazykem ASP.NET atd.
Dnes už píše webové stránky v C/C++ opravdu málo kdo. Když už je potřeba napsat něco, co funguje fakt rychle, tak se napíše jen kritická část v C/C++ jako modul do daného skriptovacího jazyka.
Jediný důvod, proč dneska psát CGI programy je ten, že je to zábava :-). A taky se při tom naučíte něco o fungování HTTP.
CGI můžete psát v jakémkoliv jazyce, třeba i v tom PHP, ale PHP jako modul funguje mnohem rychleji, než PHP jako CGI program.
Jak tedy CGI funguje? Pro vás, jako vývojáře CGI programů, až překvapivě jednoduše.
Program získává data od serveru ze standardního vstup a z proměnných prostředí.
Zpět serveru (tedy klientovi, který požádal server o spuštění CGI programu) posílá
CGI program odpověď standardním výstupem.
Na první pohled tak nerozeznáte CGI program od jakéhokoliv jiného programu
pracujícího se standardním vstupem a výstupem.
Pokud byste chtěli vyvíjet server podporující CGI, museli byste se toho o CGI naučit více.
Konfigurace Apache
Abyste si mohli CGI programy vyzkoušet, nejdříve si musíte nainstalovat server Apache a nakonfigurovat ho, aby CGI programy spouštěl. Tedy, můžete si nainstalovat a nakonfigurovat jakýkoliv server podporující CGI, ale já budu popisovat Apache (verzi 2).
Apache2 budete mít zajisté v balíčcích své distribuce, tak jej nainstalujte z nich.
Popíšu, jak zprovoznit CGI skripty v domácím adresáři každého uživatele. K tomu
musíte mít zapnutý modul mod_userdir
. Pro zprovoznění CGI zase musíte
mít nainstalovaný modul mod_cgi
.
Tyto moduly mohou být nainstalované současně z balíčkem Apache, nebo je můžete mít jako extra balíčky. Záleží na vaší distribuci.
Když máte balíčky nainstalované, musíte je taky zapnout. V OpenSuSE
najdete v souboru /etc/sysconfig/apache2 řádek s APACHE_MODULES="…",
kde se uvádějí moduly, které se zapnou při spuštění apache. Přidejte mezi ně cgi
a userdir
a restartujte Apache.
Nebo použijte program a2enmod
, který modul zapne (a pak restartujte Apache).
$ sudo /usr/sbin/a2enmod cgi
$ sudo systemctl restart apache2
Modul můžete vypnout pomocí a2dismod
V Debianu můžete použít také a2enmod
. Nebo se v Debianu zapínají moduly tak,
že se udělají symbolické odkazy v /etc/apache2/mods-enabled/ (podívejte se do tohoto adresáře
a uvidíte). V jiných distribucích to může být zase jinak :(.
Server v Debianu restartujete pomocí příkazu /etc/init.d/apache2 restart
.
Dalším krokem je konfigurace userdir
tak, abyste mohli spouštět cgi.
Modul userdir
se obvykle nainstaluje s předpřipravenou konfigurací v
souboru mod_userdir.conf, který najdete někde v /etc/apache2/. Obsah tohoto souboru
by měl vypadat nějak takto:
# not every user's directory should be visible:
UserDir disabled root
<Directory /home/*/public_html>
AllowOverride FileInfo AuthConfig Limit Indexes
Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec ExecCGI
AddHandler cgi-script .cgi
<Limit GET POST OPTIONS PROPFIND>
Order allow,deny
Allow from all
Require all granted
</Limit>
<LimitExcept GET POST OPTIONS PROPFIND>
Order deny,allow
Deny from all
</LimitExcept>
</Directory>
</IfModule>
Podobnou sekci (<Directory /home/*/public_html> ...) jsem našel i v souboru /etc/apache2/vhosts.d/default-vhost.conf. Stačilo ji smazat …
Konfigurace v podstatě popisuje, jaké konfigurační volby může uživatel měnit přes .htaccess
a co se bude spouštět v adresáři public_html adresáře každého uživatele (vyjma uživatele root).
Nejdůležitější je pro nás mít v Options ExecCGI
a řádka AddHandler cgi-script .cgi
,
která způsobí, že každý spustitelný soubor s koncovkou .cgi bude spuštěn jako cgi program.
Po změně konfigurace je potřeba apache restartovat.
Do uživatelova adresáře se přes webový prohlížeč podíváte na adrese http://localhost/~username/
.
Tedy za localhost (nebo můžete použít IP adresu či doménu vašeho stroje) je vlnovka a uživatelské jméno.
Pokud se chci podívat na soubor /home/petr/public_html/index.html, zadám do prohlížeče
http://localhost/~petr/index.html
. Soubor index.html může vypadat třeba jako
tady.
Pokud jde o cgi, důležité je mít v <Directory>
části AddHandler cgi-script .cgi
.
Tím je řečeno, že se za CGI program bude považovat každý spustitelný soubor (s právem spuštění), který má příponu .cgi
.
Pokud chcete používat více přípon, třeba .py
pro python, přidejte je oddělené mezerou.
Druhá bezpodmínečná záležitost je mít v řádku s Options
volbu ExecCGI
,
která povoluje spouštění CGI programů.
Zbytek nastavení se týká fungování modulu userdir
nebo nějakých jiných modulů a nastavení Apache.
To můžete nechat tak, jak to máte přednastaveno po instalaci.
Kdybyste měli konfigurací nějaké problémy, oficiální dokumentaci k CGI najdete na httpd.apache.org/docs/2.2/howto/cgi.html.
Příprava příkladů
V této kapitole uvidíte 4 příklady, které sdílejí následující zdrojový kód.
V html.h jsou deklarované funkce, které vypisují HTML.
Funkce htmlStart()
začíná vypsáním hlavičky
a nezbytného prázdného řádku za hlavičkou a pak vypíše značky pro začátek
HTML s titulkem a nadpisem. Funkce htmlEnd()
vypíše značky pro konec HTML dokumentu.
Funkce htmlForm()
a htmlFormWithFile()
vypíšou HTML značky pro formuláře s dvěma inputy (pro jméno
a příjmení). Druhá funkce obashuje navíc input pro odeslání souboru.
Všechny funkce odesílají výstup do standardního výstupního proudu, což v případě CGI bude znamenat do prohlížeče klienta.
- /*------------------------------------------------*/
- /* 45cgi/html.c */
- #include "html.h"
- {
- /* hlavicky */
- /* html */
- }
- {
- }
- {
- }
- {
- }
- /*------------------------------------------------*/
Druhá sada pomocných funkcí je deklarována v common.h.
Funkce printEnvironmentVals()
vypíše dvě nejzajímavější
proměnné prostředí: QUERY_STRING
a
REQUEST_METHOD
. Tyto proměnné prostředí definuje
server pro každý spuštěný CGI program. Pokud funkce nenajde QUERY_STRING,
pak zřejmě není program spouštěn jako CGI. Vypíše se varování a program
se ukončí.
Funkce printAllEnvironmentVals()
se bude používat pro výpis všech
proměnných prostředí.
Funkce printPostData()
vypíše do elementu <pre>
vše, co přečte ze standardního vstupu. Internetové prohlížeče obvykle ignorují nové řádky v HTML dokumentech,
ale v elementu <pre>
je neignorují a text podle nich zalamují.
Do standardního vstupu dostane CGI program vše, co je posláno metodou POST.
O POST ještě bude řeč.
- /*------------------------------------------------*/
- /* 45cgi/common.c */
- #include <stdlib.h>
- #include "common.h"
- {
- }
- usage();
- }
- method = "-";
- }
- }
- }
- }
- }
- /*------------------------------------------------*/
Proměnné prostředí
Podívejte se na první příklad CGI programu. Nejdříve vypíše hlavičku Content-Type, pak začátek HTML.
Druhá funkce vypíše proměnné prostředí,
které v Linuxu můžete „nestandardně“ získat jako třetí argument
funkce main()
. Poslední volaná funkce HTML uzavře.
Program přeložte a přesunte do adresáře /home/username/public_html/ pod
jménem example1.cgi
. Koncovka .cgi
je nezbytná,
protože takto koncovka byla nastavená v konfiguraci Apache, viz AddHandler.
Pak už můžete spustit CGI program na adrese http://localhost/~username/example1.cgi.
Výsledek by měl vypadat nějak takto:
Example 1
HTTP_HOST=localhostHTTP_USER_AGENT=Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Firefox/31.0
HTTP_ACCEPT=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
HTTP_ACCEPT_LANGUAGE=cs,en-us;q=0.7,en;q=0.3
HTTP_ACCEPT_ENCODING=gzip, deflate
HTTP_DNT=1
HTTP_CONNECTION=keep-alive
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SERVER_SIGNATURE=Apache/2.4.6 (Linux/SUSE) Server at localhost Port 80
SERVER_SOFTWARE=Apache/2.4.6 (Linux/SUSE)
SERVER_NAME=localhost
SERVER_ADDR=::1
SERVER_PORT=80
REMOTE_HOST=localhost
REMOTE_ADDR=::1
DOCUMENT_ROOT=/srv/www/htdocs
REQUEST_SCHEME=http
CONTEXT_PREFIX=/~username
CONTEXT_DOCUMENT_ROOT=/home/username/public_html
SERVER_ADMIN=[no address given]
SCRIPT_FILENAME=/home/username/public_html/example1.cgi
REMOTE_PORT=33143
GATEWAY_INTERFACE=CGI/1.1
SERVER_PROTOCOL=HTTP/1.1
REQUEST_METHOD=GET
QUERY_STRING=
REQUEST_URI=/~username/example1.cgi
SCRIPT_NAME=/~username/example1.cgi
Pokud se vám místo html stránky stáhne .cgi program, pak mátě špatně nakonfigurovaný Apache.
Program si můžete spustit i z příkazové řádky. Uvidíte, že dostanete seznam úplně jiných proměnných prostředí (například SHELL, USER, TMPDIR, HOME atd.).
CGI program dostane od serveru jinou sadu proměnných prostředí. Nejzajímavější
je například QUERY_STRING
, ve kterém jsou parametry předávané v URL
požadavku (vše za otazníkem, viz dále), nebo REQUEST_METHOD.
REQUEST_METHOD určuje typ požadavku. Mezi nejpoužívanější patří GET a POST. POST se používá pro odesílání formulářů (o tom budou další příklady).
REMOTE_ADDR obsahuje IP adresu uživatele, který požádal přes web o spuštění CGI programu. SERVER_ADDR je adresa serveru, na kterém CGI program běží. V příkladu výše se jedná o IPv6 adresy, ale pravděpodobněji uvidíte IPv4 adresy. Když třeba otevřete stránku http://127.0.0.1/~username/example1.cgi, bude IP adresa 127.0.0.1 (REMOTE_ADDR i SERVER_ADDR, protože se připojujete k serveru ze svého vlastního počítače).
Proměnná HTTP_ACCEPT_LANGUAGE obsahuje seznam jazyků, které by si prohlížeč přál. Jde o informace získané z hlaviček odeslaných prohlížečem. Význam dalších proměnných prostředí si už domyslíte.
Požadavek GET
V následujícím příkladu trochu přiblížím GET požadavek.
Program zase nejdříve vypíše hlavičky a začátek HTML, pak
dvě nejzajímavější proměnné prostředí, formulář s method="get"
a nakonec vypíše všechny proměnné prostředí jako předchozí příklad.
/* 45cgi/example2.c */
#include "common.h"
#include "html.h"
int main(int argc, char *argv[], char *env[])
{
htmlStart("Example 2");
printEnvironmentVals();
htmlForm("get");
printAllEnvironmentVals(env);
htmlEnd();
return 0;
}
/*------------------------------------------------*/
V prohlížeči, na adrese http://localhost/~username/example2.cgi, uvidíte něco takového:
Example 2
QUERY_STRING:
REQUEST_METHOD: GET
… výstup zkrácen …
Tady formulář odeslat nemůžete, na mém webhostingu nejdou spouštět CGI programy :(
Pro zajímavost se můžete podívat na HTML pro formulář. Jestli vás zajímají podrobnosti o tom, jak se píšou HTML formuláře, budete si to muset nastudovat někde jinde. HTML tu popisovat nebudu.
<fieldset>
Jméno: <input type='text' name='jmeno' value='' /><br />
Příjmení: <input type='text' name='prijmeni' value='' /><br />
<input type='submit' />
</fieldset>
</form>
Pokud do formuláře zadáte jméno Julius a příjmení Caesar
a formulář odešlete, budete přesměrováni na adresu
http://localhost/~username/example2.cgi?jmeno=Julius&prijmeni=Caesar
To, co je za otazníkem, se stane hodnotou proměnné prostředí QUERY_STRING. REQUEST_URI bude obsahovat "/~username/example2.cgi?jmeno=Julius&prijmeni=Caesar". Z těchto proměnných prostředí můžete vyčíst data, která uživetel formulářem odeslal.
Example 2
QUERY_STRING: jmeno=Julius&prijmeni=Caesar
REQUEST_METHOD: GET
… výstup zkrácen …
REQUEST_METHOD=GET
QUERY_STRING=jmeno=Julius&prijmeni=Caesar
REQUEST_URI=/~username/example2.cgi?jmeno=Julius&prijmeni=Caesar
SCRIPT_NAME=/~username/example2.cgi
Při čtení těchto dat buďte opatrní. Uživatel/cracker může do adresního řádku napsat cokoliv, nemusí ani odesílat formulář.
Zkuste si do jména zadat Žluťoučký kůň a příjmení nechat prázdné. Query string pak bude vypadat tatko:
jmeno=%C5%BDlu%C5%A5ou%C4%8Dk%C3%BD+k%C5%AF%C5%88&prijmeni=
Co to? Co to? Místo mezery je plus, místo Ž je %C5%BD. Nějak se nám ty data zakódovali. Nj, nějak se musí udržet pořádek a odlišit jména proměnných od jejich hodnot. Každý ne-ASCII znak je zapsán pomocí procenta a hexadecimální reprezentace kódu. A mezera se zapisuje jako plus.
Není vám to %C5%BD povědomé? Vzpomeňte si na příklad s tabulkou UTF-8 znaků. Znak Ž se skládá právě ze dvou bajtů, C5 a BD. To nám to ale všechno do sebe hezky zapadá :-).
Hodnotu posílá prohlížeč na server zakódovanou v UTF-8, protože server, resp. náš CGI program, posílá hlavičku
Content-Type: text/html;charset=UTF-8
, kterou říká, že bude posílat HTML dokument
v kódování UTF-8. Prohlížeč pak komunikuje ve stejném kódování.
Než se pustíte do psaní funkce na parsování query stringu, vyčkejte do kapitoly o FastCGI, kde vám předsavím knihovnu, která to udělá za vás.
Požadavek POST
Další příklad je stejný jako předchozí, pouze mění metodu formuláře z GET na
POST. A navíc přibylo volání funkce, která načte a zobrazí data ze standardního
vstupu stdin
.
Formulář vypadá na první pohled stejně, ale po odeslání dostanete něco trochu jiného.
- /*------------------------------------------------*/
- /* 45cgi/example3.c */
- #include "common.h"
- #include "html.h"
- {
- htmlStart("Example 3");
- printEnvironmentVals();
- printPostData();
- htmlForm("post");
- printAllEnvironmentVals(env);
- htmlEnd();
- }
- /*------------------------------------------------*/
Tohle uvidíte po odeslání:
Example 3
QUERY_STRING:
REQUEST_METHOD: POST
jmeno=Julius&prijmeni=Caesar
… výstup zkrácen …
REQUEST_METHOD=POST
QUERY_STRING=
REQUEST_URI=/~username/example3.cgi
SCRIPT_NAME=/~username/example3.cgi
Tentokráte je proměnná prostředí QUERY_STRING prázdná. Formulář vás odešle na stránku http://localhost/~username/example3.cgi, tj. bez query stringu v URL.
Data z formuláře jsou CGI programu předány do standardního vstupu. POST má oproti GETu tu výhodu, že může obsahovat téměř libovolně dlouhá data. GET požadavek je omezen jen na cca 8KiB, ačkoliv některé (staré) prohlížeče omezují délku URL na pouhých 255 znaků. POST takové omezení nemá.
Nevýhoda POSTu je, že nemůžete zkopírovat URL a někomu ho poslat, protože query string není součástí URL. Je to ale i jeho výhoda, protože požadavky POST by se správně měli používat na akce, které něco mění (ukládají do databáze, přihlašují uživatele atp), kdežto požadavky GET by měli jen ovlivnit, co se uživateli zobrazí. To už ale zacházím příliš daleko do popisu fungování webu.
Požadavek se souborem
Poslední ukázka vychází zase z té předešlé. Pouze ve formuláři
přibylo políčko na odeslání souboru. Taky se musí v elementu
form
nastavit tag enctype
na multipart/form-data
. Jinak (v jiném
kódování) nelze soubor odeslat.
Data budou programu CGI předána zase ve standardním vstupu, ale budou
vypadat (budou zakodována) trochu jinak.
- /*------------------------------------------------*/
- /* 45cgi/example4.c */
- #include "common.h"
- #include "html.h"
- {
- htmlStart("Example 4");
- printEnvironmentVals();
- printPostData();
- htmlFormWithFile();
- printAllEnvironmentVals(env);
- htmlEnd();
- }
- /*------------------------------------------------*/
Formulář vypadá takto:
Example 4
QUERY_STRING:
REQUEST_METHOD: GET
… výstup zkrácen …
REQUEST_METHOD=GET
QUERY_STRING=
REQUEST_URI=/~username/example4.cgi
SCRIPT_NAME=/~username/example4.cgi
A po odeslání se souborem hello-world.txt, který obsahuje jen řádku Hello World!:
Example 4
QUERY_STRING:
REQUEST_METHOD: POST
-----------------------------3613503935902733491650184458
Content-Disposition: form-data; name="jmeno"
Julius
-----------------------------3613503935902733491650184458
Content-Disposition: form-data; name="prijmeni"
Caesar
-----------------------------3613503935902733491650184458
Content-Disposition: form-data; name="soubor"; filename="hello-world.txt"
Content-Type: text/plain
Hello World!
-----------------------------3613503935902733491650184458--
… výstup zkrácen …
REQUEST_METHOD=POST
QUERY_STRING=
REQUEST_URI=/~username/example4.cgi
SCRIPT_NAME=/~username/example4.cgi
Při tomto způsobu kódování začíná každá proměnná řádkou s několika
mínusy a pak náhodně vygenerovaným číslem.
Pak následují hlavičky (Content-Disposition, Content-Type), prázdná řádka
a hodnota proměnné nebo obsah souboru.
Za poslední proměnnou (v našem případě
souborem) je tato řádka ještě doplněná o dva mínusy.
Pokud se pokusíte poslat binární soubor, vypíše vám example4.cgi všechna binární data (vyzkoušejte na vlastní nebezpečí :-).
V této kapitole jste se naučili psát CGI programy, jak posílat POST a GET požadavky, jak nastavit encoding ve formuláři abyste mohli posílat soubory a kde získat odeslaná data.
Jak asi tušíte, vyparsovat taková data nebude nic snadného. Sice už umíte regulární výrazy, ale to vám v případě binárních souborů nebude moc platné (protože binární soubor neobsahuje jen text a reg. výrazy by kvůli tomu selhaly). Naštěstí existují zdarma dostupné knihovny, které vám s tím pomůžou. O jedné z nich vám povím v kapitole o Fast CGI.