CGI-ohjelmointia C:llä
Mureakuha
Huom. Tämä artikkeli on johdanto CGI-ohjelmien tekemiseen C-kielellä. Jotta saisit jotain irti, sinun tulee osata sekä C:tä että HTML:ää.
Turvallisten CGI-ohjelmien tekeminen C-kielellä vaatii taitoa ja huolellisuutta! Muista tämä tai käytä muuta kieltä.
Sisällysluettelo |
Johdanto
Internet olisi varsin tylsä ilman CGI:tä. HTML on kivaa ja helppoa, mutta pelkällä HTML:llä tehty toteutus on aina staattinen. Sivut eivät muutu katselukerrasta toiseen, vaan pysyvät aina samanlaisena. Tietysti client-pään scriptikielillä (esim. Javascript) pystytään tekemään joitain kikkoja, mutta nämä tekniikat ovat epävarmoja, sillä kaikki selaimet eivät tue niitä, ja vaikka tukisivatkin, niin luultavasti tulos on hieman erilainen joka selaimella. CGI-ohjelma toimiikin serveripäässä, ja palauttaa käyttäjän selaimelle aivan tavallista HTML-koodia, jota kaikki Internet-selaimet poikkeuksetta tukevat.
Yleistä CGI:stä
CGI on lyhenne sanoista Common Gateway Interface. CGI-ohjelmat ovat ohjelmia, jotka www-serveri ajaa, kun sitä pyydetään niin tekemään. Yleensä CGI-ohjelmat tuottavat HTML-koodia, jonka www-serveri sitten lähettää käyttäjälle. (Ne voivat myös tuottaa esim. kuvia, mutta tämä artikkeli käsittelee HTML:n tuottamista) Useimmiten CGI-ohjelmia kutsutaan siten, että HTML-sivulla on form-elementti, jonka kenttien arvot ohjelmalle lähetetään parametreinä. Näin CGI-ohjelma voi tuottaa www-sivun käyttäjän antaman syötteen ja/tai vaikkapa tietokannasta hakemansa datan perusteella.
CGI-ohjelmien tekemiseen voi käyttää monia erilaisia kieliä, kuten esim. Perliä tai PHP:tä. Periaatteessa millä vain ohjelmointikielellä voi tehdä CGI-ohjelmia. Tämä artikkeli käsittelee CGI-ohjelmien tekemistä C-kielellä.
CGI-ohjelmointi C-kielellä
Yleistä
Kuten sanottu, CGI-ohjelmat ovat aivan tavallisia ohjelmia. CGI-ohjelmassa pitää olla main-funktio ja ohjelma pitää kääntää ennenkuin sitä voi käyttää.
CGI-ohjelman olisi siis tarkoitus tuottaa HTML-koodia. Mutta mihin se koodin pistää kun on sen tuottanut? Tämä on järjestetty erittäin kätevästi. Ohjelman pitää vain tulostaa tuottamansa koodi standardiin tulostusvirtaan eli stdout:iin. Tämä onnistuu yleisesti ottaen millä tahansa funktiolla, jolla voi kirjoittaa tekstiä ruudulle, esim. printf:llä. Kun ohjelma sitten päättyy, lukee www-palvelin stdout:n sisällön ja lähettää sen sivun lukijan selaimelle. Koska CGI-ohjelmat voivat tuottaa minkälaista dataa tahansa (kuvia, pelkkää tekstiä, html-sivuja), pitää selaimelle kertoa, minkälaista dataa se tulee saamaan. Siksi stdout:iin pitää tulostaa header ennen kuin tulostetaan mitään muuta koodia. Meidän tapauksessamme header näyttää tältä:
Content-type:text/html
Tämä kertoo selaimelle, että kyseessä on html-sivu. On myös erittäin suositeltavaa lisätä headeriin charset jota käytetään. Jos vaikkapa joku kiinalainen tulee katselemaan sivujasi, ei hänen merkistöllään ole hirveän hienoa katsella länsimaista tekstiä. Siispä headeristamme tulee seuraavanlainen:
Content-type:text/html;charset=iso-8859-1
Tämän rivin pitää siis aina olla ensimmäinen rivi, jonka www-serveri lähettää selaimelle. Huomaa, että HTTP-header käyttää CR+LF -rivinvaihtoja (C:ssä "\r\n"). Headerin lopussa pitää olla vielä yksi tyhjä rivi.
Nyt on headerhommat hoidettu. Hyvä. Eikun sitten vain tuottamaan koodia. Tässä on esimerkkiohjelma, ensimmäinen CGI-ohjelmasi:
#include <stdio.h> int main(void) { int i; /* Tulostetaan header */ printf("%s\r\n\r\n", "Content-type:text/html;charset=iso-8859-1"); printf("<html>\n"); printf("<head>\n"); printf("<title>Ensimmäinen CGI-ohjelmani</title>\n"); printf("</head>\n\n"); printf("<body>\n"); for(i = 0; i < 10; i++) printf("%d. tervehdys CGI-maailmalle!<br>\n", i); printf("</body>\n"); printf("</html>\n"); return 0; }
Kuten varmaan huomaatkin, tämä pieni ohjelma tuottaa aivan tavallista HTML-koodia. Se saa aikaan sivun, joka sisältää tervehdyksen CGI-maailmalle 10 kertaa.
CGI-ohjelmien kääntäminen ja käyttäminen
Jotta voisit käyttää CGI-ohjelmaasi sinun pitää luonnollisesti kääntää se lähdekoodista ajettavaan muotoon. Jotta voit sen tehdä, tarvitset shell-oikeudet www-palvelimelle. Sinun pitää ensin siirtää lähdekooditiedostosi serverille ja sen jälkeen kääntää se serverin C-kääntäjällä. Jos käyttämälläsi palvelimella on gcc-kääntäjä, voisi operaatio tapahtua vaikkapa näin:
gcc -o omacgi.cgi omacgi.c
Useimmiten ajettavien CGI-ohjelmien tiedostopäätteen tulee olla .cgi, mutta käytäntö voi vaihdella riippuen palvelimesta. Usein käännetyt cgi-ohjelmat tulee kopioida cgi-bin -hakemistoon, jotta niitä voi käyttää. Joillakin palvelimilla ei ole GCC-kääntäjää, vaan jokin aivan muu. Ikävä kyllä näissä asioissa en voi auttaa hirveästi, sinun täytyy ottaa selvää palvelimen ylläpitäjältä.
Oletetaan nyt kuitenkin, että olet saanut käännettyä ohjelmasi ja siirrettyä sen cgi-bin -kansioon. Voit sen jälkeen koittaa selaimellasi mitä tapahtuu, kirjoitat osoitteeksi CGI-ohjelmasi osoitteen. Mikäli palvelin antaa virheilmoituksen, tai takaisin ei tule mitään, on koodissasi luultavasti virhe, ohjelmasi on väärässä hakemistossa tai palvelimen asetukset on säädetty väärin.
Äsken tekemämme CGI-ohjelma on vielä hyvinkin staattinen; se tulostaa joka kerta samanlaisen HTML-sivun. Saadaksemme jotain vaihteluvuutta, pitää tutustua metodeihin, jolla palvelin välittää parametreja CGI-ohjelmalle lomakkeiden kautta. Metodeita on kaksi: GET ja POST.
method="GET"
Olet varmaan joskus nähnyt sellaisten sivujen osoitteita, jotka ovat tyyliin http://www.blaa.com/blah.cgi?nimi=Pertti+Paasio&ika=14. ?-merkin jälkeistä osaa kutsutaan query stringiksi. Se sisältää CGI-ohjelman parametrit muodossa muuttuja=arvo. Muuttujien nimet tulevat suoraan html-sivun lomakkeen objektien name-kentistä. Query string on koodattu, ja sen takia se on hieman kryptisen näköinen. Tuo koodaus on kuitenkin suhteellisen helppo purkaa, kun tietää miten se toimii. Muuttujien arvot ovat aina merkkijonoja. + -merkki tarkoittaa välilyöntiä. % -merkin jälkeen tulee aina 2-numeroinen luku, joka on siinä kohdassa olevan erikoismerkin ASCII-koodi Hexalukuna. Esimerkiksi skandit esitetään näin. Jos muuttujien arvot ovat numeerisia, pitää käyttää funktiota, joka muuttaa merkkijonoja numeroiksi, esimerkiksi sscanf, sillä query string on aina merkkijono. GET-metodi toimii niin, että query string näkyy aina mukana CGI-ohjelman osoitteessa. Mutta siitä ei paljoa CGI-ohjelmoija kostu, sillä sivun täydelliseen osoitteeseen ei päästä käsiksi. GET-metodia käytettäessä www-palvelin tallentaa osoitteesta kysymysmerkin jälkeisen osan ympäristömuuttujaan QUERY_STRING. Noniin, nyt alkaa jo kuulostaa joltain. Voimme siis käyttää getenv -funktiota hyväksemme (löytyy stdlib.h:sta). Se palauttaa ympäristömuuttujan arvon merkkijonona. Sen jälkeen voimme dekoodata query stringin vastaamaan omia tarkoitusperiämme.
Otetaan tähän väliin esimerkki.
<form method="get" action="http://www.omaservu.fi/cgi-bin/omacgi.cgi"> Anna yhteenlaskettavat:<br> <input name="luku1" size="5"> + <input name="luku2" size="5"><br> <input type="submit" value="Laske!"> </form>
Siinä on pieni sievä pätkä HTML:ää. Se näyttää sivulla lomakkeen, jossa on 2 tekstilaatikkoa sekä nappula, jossa lukee 'Laske'. Lomake voisi näyttää selaimessasi esimerkiksi tältä:
Sitten C-koodi:
#include <stdio.h> #include <stdlib.h> int main(void) { /* Olen tarkoituksella jättänyt pois html-sivun alku- ja lopputagit tilaa säästääkseni. Muista silti aina sisällyttää ne! */ int luku1, luku2; const char *data = getenv("QUERY_STRING"); /* Haetaan QUERY_STRING */ /* Tulostetaan header */ printf("%s\r\n\r\n", "Content-type:text/html;charset=iso-8859-1"); /* Tähän väliin HTML-tageja! */ printf("<h3>Yhteenlaskun tulos:</h3>\n"); if(data == NULL) // QUERY_STRING:iä ei ole olemassa eli { // CGI-ohjelmaamme ei kutsuttu lomakkeen kautta printf("Virhe luettaessa lomakkeen dataa!\n"); exit(0); } /* Haetaan query stringistä muuttujien luku1 ja luku2 arvot, eli 2 numeroa */ if(sscanf(data, "luku1=%d&luku2=%d", &luku1, &luku2) != 2) // Tutkitaan data { printf("Virheellinen data! Pistitkö varmasti numerot tekstikenttiin?\n"); exit(0); } printf("<p>%d + %d = %d</p>", luku1, luku2, luku1 + luku2); /* Tänne HTML:n lopputagit */ return 0; }
Tämä pieni ohjelma ottaa lomakkeelta syötteeksi kaksi lukua ja generoi sivun, jossa luvut on laskettu yhteen.
Ensin siis asetetaan data-osoitin osoittamaan QUERY_STRINGin sisältöön. getenv palauttaa arvon NULL, jos haettua ympäristömuuttujaa ei ole olemassa.
data = getenv("QUERY_STRING");
Näin pystymme tarkistamaan, onko sivullemme tultu lomakkeen kautta vai jotakin muuta reittiä. Seuraavaksi haetaan datasta yhteenlaskettavien lukujen arvot. sscanf on juuri sopiva funktio tähän tarkoitukseen, sillä datamme sisältää ainoastaan kaksi numeroa.
if(sscanf(data, "luku1=%d&luku2=%d", &luku1, &luku2) != 2)
sscanf palauttaa lukuna sen määrän, kuinka monta arvoa se luki sille annetusta merkkijonosta. Tässä tapauksessa luvun pitäisi olla 2, sillä sen määrän lukuja haluamme saada. Mikäli sscanf ei pysty lukemaan kahta lukua, on käyttäjä luultavasti syöttänyt lomakkeeseen jotain muuta kuin lukuja.
method="POST"
POST-metodin käyttäminen on useissa tilanteissa paljon mielekkäämpää kuin GET-metodin käyttäminen. Jos käyttäjän pitää esimerkiksi syöttää oma salasanansa tai todella pitkä teksti, ei ole järkevää antaa sen näkyä osoiterivillä. POST-metodia käytettäessä ei dataa luetakaan enää ympäristömuuttujasta vaan standardista input-streamista, eli stdin:stä. Datan pituus kylläkin luetaan ympäristömuuttujasta CONTENT_LENGTH. GET-metodin tapauksessa datan pituutta ei oikeastaan tarvita mihinkään, sillä sen saa tarvittaessa selville strlen-funktiolla. Vaikka POST-metodia käytettäessä data ei näykään osoiterivillä, on se silti samanlaisessa ikävän kryptisessä muodossa kuin GET-metodia käytettäessäkin. Siispä sekin pitää dekoodata. Eli POST:n ja GET:n käyttö loppujen lopuksi eroaa hyvin vähän. Otetaan lopuksi sitten vielä vähän rankempaa dekoodausta vaativa esimerkki:
viestit.html:
<html> <head> <title>Viestit</title> </head> <a href="http://www.omaservu.fi/cgi-bin/nayta_viestit.cgi">Näytä viestit</a>< tai <form method="post" action="http://www.omaservu.fi/cgi-bin/kirjoita_viesti.cgi"> Kirjoita viestisi (max 100 merkkiä) <input name="teksti" size="50" maxlength="100"> <input type="submit" value="Lähetä!"> </form> </body> </html>
kirjoita_viesti.c:
#include #include /* data on muotoa 'teksti=Olen+Kalle+ja...' EXTRA on siis tuon 'teksti=' -merkkijonon pituus. Datan dekoodaaminen aloitetaan vasta sen jälkeen. */ #define EXTRA 7 /* Käyttäjän kirjoittaman merkkijonon maksimipituus */ #define MAX_PITUUS 100 /* MAX_DATA on datan sekä dekoodatun merkkijonon maksimipituus. Vaikka käyttäjä kirjoittaisi kaiken yhteen pötköön (ilman välilyöntejä) ei lopullisen merkkijonon pituus voi olla pidempi kuin datan pituus + 2 (rivinvaihto ja 0-tavu loppuun) Ja vähän pitää olla ylimääräistä, ettei satu vahinkoja */ #define MAX_DATA MAX_PITUUS + EXTRA + 2 #define TIEDOSTO "viestit.txt" void dekoodaa(char *data, int pituus, char *tulos) { char *datan_loppu = (data + pituus); for( ; data != datan_loppu; data++, tulos++) { if(*data == '+') *tulos = ' '; else if(*data == '%') { int koodi; if(sscanf(data + 1, "%2x", &koodi) != 1) koodi = '?'; *tulos = koodi; data += 2; } else *tulos = *data; } *tulos = '\n'; *++tulos = '\0'; } int main(void) { /* Tässäkin olen jättänyt HTML:n alku- sekä lopputagit merkitsemättä */ FILE *tiedosto; char *pituus_data; char data[MAX_DATA], tulos[MAX_DATA]; int pituus; printf("%s\r\n\r\n", "Content-type:text/html;charset=iso-8859-1"); /* Tähän HTML:n alkutagit */ pituus_data = getenv("CONTENT_LENGTH"); if(pituus_data == NULL || sscanf(pituus_data, "%d", &pituus) != 1 || pituus > MAX_PITUUS) { printf("<p>Virhe syötteen lukemisessa - käytitkö väärää lomaketta?</p>\n"); exit(0); } fgets(data, pituus + 1, stdin); dekoodaa(data + EXTRA, pituus, tulos); tiedosto = fopen(TIEDOSTO, "a"); if(tiedosto == NULL) { printf("<p>Viestiäsi ei voi tallentaa. Pahoittelut.</p>\n"); exit(0); } fputs(tulos, tiedosto); fclose(tiedosto); printf("<p>Kiitos! Seuraava visti tallennettiin: %s</p>\n", tulos); /* Ja tänne HTML:n lopputagit */ return 0; }
Funktio dekoodaa() dekoodaa lomakkeelta saadun datan siis normaaliksi merkkijonoksi. Se käy datan läpi merkki kerrallaan. Jos vastaan tulee '+'-merkki se korvaa sen tuloksessa välilyönnillä. Jos vastaan tulee '%'-merkki, on sitä seuraava luku siihen kohtaan kuuluvan merkin ASCII-arvo heksadesimaalilukuna. Se luetaan sscanf:llä ja asetetaan tulokseen oikeaan kohtaan. Lopuksi tuloksen loppuun vielä lisätään rivinvaihto ('\n') ja nollatavu ('\0'), koska jokainen merkkijonohan loppuu nollatavuun. main:ssa haetaan ensin CONTENT_LENGTH -ympäristömuuttujan sisältö pituus_data -merkkijonoon. Sen jälkeen sieltä yritetään lukea pituus sscanf:llä. Jos ympäristömuuttujaa ei ole, pituutta ei voi lukea tai pituus on liian pitkä, tulostetaan virheilmoitus ja poistutaan. Muutoin jatketaan suoritusta. Seuraavaksi luetaan data stdin:stä käyttäen fgets-funktiota. C:ssähän kaikki streamit ovat tavallaan tiedostoja Unix-tyyliin, joten fgets-funktio on juuri passeli tähän. Sitten dekoodataan data, avataan tiedosto append-tilaan ja kirjoitetaan käyttäjän syöttämä viesti tiedoston loppuun. Tässä vielä lähdekoodi CGI:lle, joka näyttää käyttäjien asettamat viestit:
nayta_viestit.c:
#include #include #define TIEDOSTO "viestit.txt" int main(void) { /* Ja taas puuttuvat HTML-tagit... */ int ch; FILE *f = fopen(TIEDOSTO,"r"); if(f == NULL) { printf("%s\r\n\r\n", "Content-Type:text/html;charset=iso-8859-1"); printf("<p>Viestitiedoston avaaminen epäonnistui!</p>"); } else { printf("%s\r\n\r\n", "Content-Type:text/plain;charset=iso-8859-1"); while((ch=fgetc(f)) != EOF) putchar(ch); fclose(f); } return 0; }
Tässä ei pitäisikään olla mitään epäselvää. Huomaa, että tiedoston sisältö tulostetaan plain text:inä, jotta rivinvaihdot näkyisivät oikein. Siksi header on
Content-type:text/plain;charset=iso-8859-1
Aiheesta muualla
The CGI Specification, CGI:n spesifikaatio (englanniksi)

