Kapitel 2. Dynamiske websider

Den mest interessante feature for en webserver er i denne sammenhæng at webserveren ikke kun kan afsende statiske sider, men kan fortolke kode indlejret i websider så som Server-Side Includes, Mod_Perl og PHP, men også at webserveren kan udføre selvstændige programmer, programmets output kan være tekst, billeder eller lyd, og dette bliver af webserveren returneret til browseren. Denne sidste mulighed kaldes for CGI-programmer (Common Gateway Interface). CGI-programmer bruges typisk til at fortolke svaret fra webformularer, tællere af besøgende på en webside og til søgemaskiner.

2.1. CGI-programmer

Til at programmere CGI-programmer kan man vælge et vilkårligt programmeringssprog. Til små programmer der udskriver server information kan et kommandofortolkerprogram være passende. Til sider hvor svartiden er kritisk eller hvor der skal udføres komplekse matematiske operationer, kan oversatte sprog som Ada, C eller C++ med fordel anvendes. Men normalt skal kun tekst indlæses og udskrives, og det er en opgave som programmeringssproget Perl er designet til at løse. Perl er et fortolket sprog, der er beregnet til at behandle tekst med. Fordi det er et fortolket sprog, skal Perl-koden fortolkes hver gang et program udføres, (hvilket tager en del tid). Til gengæld behøver man ikke at bekymre sig om buffer overløb (eng. buffer overflows) i Perl, hvilket er den hyppigste fejl i C-programmer der anvender strenge. Hvis man skal udføre mange CGI-programmer kan det være nødvendigt at anvende Fast_CGI- eller Mod_Perl-modulet. Mod_Perl er et modul til Apache, som gør at du kan have inline Perl i HTML uden at det koster mange ressourcer per CGI-kald.

De følgende afsnit indeholder Perl-eksempler og kodestumper. Funktioner der allerede er anvendt i tidligere eksempler er slettet for at holde længden af programmerne nede, men der står også hvilke kodestumper der mangler og hvor de kan findes. Mange af disse eksempler kan hentes i et fuldt fungerende eksempel. Står der f.eks. debug.cgi i Perl-koden betyder det, at kode eksempel kan hentes på www.linuxbog.dk/web/eksempler/debug.cgi, eller via den pakke af eksempler, som hører til webbogen og ligger under www.linuxbog.dk.

2.1.1. Opsætning af Apache til at udføre programmer.

Hvis du selv bestyrer en webserver skal den først sættes op til at udføre filer af typen .cgi som CGI-programmer, hvis du bruger et webhotel så vil du sikkert have adgang til noget information om hvor og hvordan programmer kan udføres. Følgende ændringer skal udføres i Apaches opsætningsfiler, der ligger normalt i /etc/httpd/conf/ kataloget:

Filen srm.conf skal indeholde følgende linjer

AddHandler cgi-script .cgi
ScriptAlias /cgi-bin/ /home/httpd/cgi-bin/

Filen access.conf definere web browseres rettigheder til at hente sider i kataloger, og 'Options ExecCGI' skal være slået til i alle kataloger hvor webserveren skal udføre programmer. Typisk er der i forvejen et /home/httpd/cgi-bin katalog hvor webserveren har rettigheder til at udføre programmer:

<Directory /home/httpd/cgi-bin>
  AllowOverride None
  Options ExecCGI
</Directory>

Det kan være nødvendigt at ændre adgangen til dette katalog, så du som normal bruger kan skrive til det. Dette gøres med kommandoen chmod o+rwx /home/httpd/cgi-bin.

Filen httpd.conf skal indeholde følgende linjer:

LoadModule dir_module         modules/mod_dir.so
AddModule mod_cgi.c

Mange af disse ændringer består i at fjerne udkommenteringen af '#' i starten af eksisterende linjer. Når disse ændringer er udført og webserveren genstartet med /etc/rc.d/init.d/httpd restart, så vil et program, runme.cgi, der ligger i /home/httpd/cgi-bin/ kataloget, kunne udføres ved at man i en browser beder om http://localhost/cgi-bin/runme.cgi.

2.1.2. Kommunikation mellem webserver og CGI-programmer

Et program har altid tre datastrømme: stdin (standard input) der er input fra keyboard, stdout (standard output) der er output til skærmen, og stderr (standard error) der er fejlmeddelelser og også udskrives til skærmen. Dog skal det bemærkes at alle tre standard-datastrømme være omdirigerede.

Når webserveren udfører et CGI-program, vil alle data der udskrives til stdout blive returneret til browseren. Data der skrives til stderr vil blive gemt i webserverens fejllog /var/log/httpd/error_log. Programmet kan også modtage data på stdin fra webserveren, dette vil typisk være data der er indtastet i en webformular eller filer, der skal lægges op på serveren. CGI-programmer modtager også en række data igennem systemvariable (eng. environment variable).

I det følgende afsnit vil vi introducere CGI-programmer der returnerer et simpelt svar og ikke modtager data, forklare hvordan programmer kan returnere forskellige typer data, og gå igennem hvordan programmer kan modtage data fra webformularer og anvende systemvariable til at bestemme f.eks. IP addressen på den webbrowser som ønsker data. Desuden vil vi forklare, hvilke sikkerhedsproblemer, der er omkring programmer, og hvordan man debugger programmer.'

2.1.3. Simpelt svar

Det simpleste eksempel på et CGI-program er et, der bare returnerer en enkel oplysning, f.eks. hvad klokken er. Gem følgende program i /home/httpd/cgi-bin/ som klokken.cgi:

#!/usr/bin/perl -w
#Filnavn: klokken.cgi

#Udskriv hoved
print "Content-Type: text/plain; charset=iso-8859-1\r\n";
print "\r\n";

#Udskriv indhold
print "Datoen er ".scalar(localtime);

Køres klokken.cgi fra kommandolinjen får man:

Content-Type: text/plain; charset=iso-8859-1

Datoen er Thu Jul 20 11:38:08 2000

Prøv nu at hente det i en webbrowser som http://localhost/cgi-bin/klokken.cgi. Du skulle gerne kunne se hvad datoen er. Prøv at genindlæse siden nogle gange, så du kan se sekunderne gå. Hvis dette ikke virker, så kig i afsnittet om »Typiske Fejl«.

Først udskriver programmet en linje med en "Content-Type", der fortæller webbrowseren, hvad den skal gøre med hvad de data der følger, feltet kaldes for medietypen. Her betyder »text/plain« det at indholdet er klar tekst, og »charset=iso-8859-1« at teksten er kodet i UTF-8. Der kan være flere linjer i hovedet og de afsluttes med en tom linje, alt under den tomme linje er data der vises i browseren.

Tekst er lidt kedeligt, så vi kunne istedet ønske at returnere HTML-formateret tekst. Dette kan let gøres ved at modificere programmet som følger:

#!/usr/bin/perl -w

#Filnavn: klokken2.cgi

#Udskriv hoved
print "Content-Type: text/html; charset=iso-8859-1\r\n";
print "\r\n";

#Vælg tilfældig farve 
my $color=sprintf("%x",int rand(0x1000000));

#Udskriv HTML indhold
print "<H1>Datoen er: ";
print "  <font color=\"#$color\">", scalar(localtime) ,"</font>";
print "</H1>";

Programmet vil nu udskrive datoen med store bogstaver og med en tilfældig farve, der ændrer sig hver gang programmet kaldes/siden genindlæses.

2.1.4. Medietyper

Et CGI-program kan ikke kun returnere ren tekst og HTML-formatteret tekst, men også en lang række andre medieformater. I Netscape kan man i Edit/Preferences/Navigator/Applications se hvilke programmer, der udføres når browseren modtager en fil med en bestemt Medietype, hvis Netscape ikke kender filtypen bliver brugerne bedt om at gemme filen på harddisken.

Følgende er en liste af medietyper (eng. Media) for nogle hyppigt anvendte formater.

Tabel 2-1.

Media type Beskrivelse
text/plain ASCII-formateret tekst (.txt)
text/html HTML formateret tekst (.html eller .htm)
image/jpeg JPEG billed (.jpeg eller .jpg)
image/png PNG billed (.png)
image/gif GIF billed (.gif)
application/postscript Postscript-fil (.ps)
application/x-dvi DVI-fil (oversat LaTeX-fil - .dvi)
application/pdf Portable Document Format fil (.pdf)
audio/x-mpeg MPEG komprimeret lyd (.mp3)
audio/x-wav Wave fil (.wav)
video/x-mpeg2 MPEG animation (.mpeg eller .mpg)
video/quicktime Quicktime animation (.mov)
video/x-msvideo Microsoft Video (.avi)

2.1.5. HTTP-hovedet

Hvis man returnere et svar med et CGI-program kan det ske at browseren gemmer en lokal kopi af det, og en genindlæsning vil derfor ikke køre programmet igen, men kun vise den lokale kopi. Følgende stump kode fortæller browseren og stråmænd (som for eksempel Squid), at siden ikke må gemmes:

#Udskriv hoved
print "Content-Type: text/html\r\n";

#HTTP 1.1
print "Cache-Control: no-cache\r\n";

#HTTP 1.0 tilbage-kompatibilitet
print "Pragma: no-cache\r\n";
print "\r\n";

#Resten af programmet

Hvis siden må gemmes, men man ønsker at bestemme hvor længe, kan det gøres med Expires-feltet. Følgende stump kode viser hvordan:

# Indsættes: Udskriv hoved

use POSIX qw(strftime);

#må leve en time i cachen
my $levetid = 1*60*60;

#korrekt format laves med følgende linje
my $date = strftime "%a, %e %b %Y %H:%M:%S GMT", gmtime(time+$levetid);

print "Content-Type: text/html\r\n";
print "Expires: ",$date,"\r\n";
print "\r\n";

#Her udføres resten af programmet

Programmet vil f.eks. udskrive en "Expires: Sat, 29 Jul 2000 16:45:27 GMT" linje, der betyder at efter den dato er cache versionen forældet. Programmet fungerer ved at time returnerer antallet af sekunder, der er gået siden 1. januar 1970, til dette antal sekunder lægges dokumentets levetid, og dette konverteres af gmtime til time, dag, måned mv. Strftime konverterer derefter disse data til det angivne tidsformat, som er det, HTTP standarden forskriver.

Mere information om HTTP-hovedet kan findes i dokumenterne RFC 1945 og RFC 2068, der definerer HTTP 1.0- og HTTP 1.1-protokollerne.

2.1.6. Omdirigering

I mange situationer bruges et program til at sende browsereren en side som er dynamisk genereret. F.eks. kunne et program returnere forskellige sider afhængig af hvilken browser, man bruger, hvilket tidspunkt af døgnet det er, bruger et tilfældigt baggrundsbillede og/eller varierer indhold alt efter hvilken IP-addresse, man har.

Istedet for at returnere indholdet af filen eller outputtet af programmet til browseren kan et program returnere en redirektion, der fortæller browseren, at den skal finde indholdet på en anden URL.

Et eksempel:

Hver gang en browser forsøger at hente et katalog uden at specificere en index-fil, så afsendes index.html filen. Denne funktionalitet specificeres i /etc/httpd/conf/httpd.conf med linjen:

DirectoryIndex index.htm index.html index.shtml index.cgi

Dvs. at index.cgi køres hvis ikke nogen af de andre indeks-filer kan findes i kataloget. Så gem følgende program som index.cgi i et katalog med en masse HTML-filer, og hvor der ikke er nogen anden indeks-fil.

#!/usr/bin/perl -w
#Filnavn: index.cgi

#Lav en liste med indholdet af ./ kataloget
#dvs. samme katalog som index.cgi selv
opendir DIR, "./" or die "Can't list directory /.!\n";

#filtrér listen så den kun indeholder .html-filer
my @files = grep /\.html$/, readdir(DIR);

#vælg tilfældigt index i listen
my $no=int rand($#files+1);

#udskriv en location til dokument no $no
print 'Location: ', $files[$no] ,"\r\n\r\n"; 

Programmet vil finde alle .html-filer i samme katalog, og udskrive en redirektion til en filfældig .html-fil.

Den samme metode kan bruges til at returnere et tilfældigt baggrundbillede, hver gang en person besøger en side. Hvis der ligger en masse JPEG billeder i /baggrund-kataloget, kan ovenstående program gemmes i /baggrund-kataloget, og modificeres så '.html' ændres til '.jpeg', og HTML-koden for sider, der skal have en tilfældig baggrund er nu:

<body background="/baggrund/index.cgi">

2.1.7. Webformularer (Get-metoden)

CGI-programmer bruges også til at fortolke data, der bliver indtastet i webformularer på nettet. Følgende eksempel er en simpel HTML-formular, hvor man kan indtaste et navn og en e-postadresse:

<form action="/cgi-bin/get_metoden.cgi" method="GET">
   Indtast navn: <input type="text" name="navn"><br>
   Indtast Email: <input type="text" name="adresse"><br>
   <input type="Submit">
</form>

I browseren vil det se ud som:

Figur 2-1. Illustration

Når et navn og en e-postaddresse er indtastet, og der trykkes på Submit knappen, så vil browseren sende de indtastede data til webserveren, der starter get_metoden.cgi. Dataene overfører webserveren igennem systemvariablen QUERY_STRING. (C/C++ programmer vil også modtage dem igennem argv-tabellen).

Gem følgende program som get_metoden.cgi i /home/httpd/cgi-bin:

#!/usr/bin/perl -w

print "Content-Type: text/plain\r\n\r\n";
print $ENV{'QUERY_STRING'};

I eksemplet vist vil get_data.cgi udskrive "navn=Tux+Penguin&adresse=Tux%40linux.org". Programmet modtager altså data fra webformularen, men de ser lidt mystiske ud! Formattet af data er "variabel1=indtastning1&variabel2=indtastning2&..". Hvor variabel1 og variabel2 svarer til indeholdet af name feltet i HTML-koden (altså "navn" og "adresse").

Både navne og indtastninger er URL-kodet. Det betyder at mellemrum skiftes ud med '+', ligeledes bliver en række tegn. specielt '=','%' og '&' ombyttet med den tilsvarende hexadecimale kode skrevet som %xx. I eksemplet blev '@' lavet om til %40.

For at URL-afkode en streng skal den altså først klippes i stykker ved alle &-tegnene. Dette giver en tabel af strenge af formen "variabel=indtastning". Hver streng skal så klippes ved '=' og man får variabel og indtastning hver for sig, og så skal alle '+' konverteres til mellemrum og bagefter skal alle %xx konverteres til det tilsvarende UTF-8-tegn i både variabel- og indtastningsstrengene.

En lille detalje er at %00 — tegnet "NULL" — skal fjernes. Det er vigtigt, at dette gøres netop i denne rigtige rækkefølge ellers vil alle "+"-tegnene, der er indtastet ændres til mellemrum.

Følgende Perl-program laver en URL-kodet streng om til en hashtabel %data af (variabelnavn->værdi)-par.

#!/usr/bin/perl

#Filnavn: get_metoden.cgi

sub URLdekrypt
#Udfører url dekryption på en streng.
{
    my ($input)=(@_);

    my ($variabel,$indtastning);
    my %data;

#Klip input strengen ved all '&'-tegn

    my @query=split /&/, $input;

#loop gennem tabellen af "variabel=indtastning" strenge

    foreach (@query)
      {

#Klip ved '='

        ($variabel,$indtastning)=split /=/, $_;

#fix '+' før %xy!

        $variabel    =~ tr/+/ /;
        $indtastning =~ tr/+/ /;

#null tegn er uønskede!
        $variabel    =~ s/%00//g;
        $indtastning =~ s/%00//g;

#substituter %xy med det tilsvarende tegn

        $variabel    =~ s/%([0-9A-Fa-f]{2})/pack("c",hex($1))/ge;
        $indtastning =~ s/%([0-9A-Fa-f]{2})/pack("c",hex($1))/ge;

#Hvis flere felter har samme navn så konkateneres deres indhold
#separeret af et "|"-tegn.

        if ($data{$variabel})
            { $data{$variabel}=$data{$variabel}."|".$indtastning; }
          else
            { $data{$variabel}=$indtastning; } 
      }

   return %data;
}

my %data=URLdekrypt($ENV{'QUERY_STRING'});

#udskriv data
print "Content-Type: text/plain\r\n\r\n";
print "Jeg modtog følgende data:\n";

foreach (sort keys %data)
    {
       print "   $_ = \"$data{$_}\"\n";
    }

Programmet vil udskrive

Jeg modtog følgende data:
    adresse = "Tux@linux.org"
    navn = "Tux Penguin"

I stedet for at udskrive disse i browseren kunne programmet tilføjes navn og e-postaddresse til en database eller en besøgsbog, sende et brev til addressen, eller hvad nu man kan finde på.

2.1.8. Webformulare (Post-metoden)

Rent praktisk returnere get metoden data ved at konkatenere dem til URL'en til CGI-programmet afskildt af et spørgsmålstegn (se i browserens titel vindue). Browseren sender så hele denne URL til webserveren. Get metoden har den fordel at man kan lave bogmærker med den side, som programmet returnere på basis af de indtastet data, dette bruges typisk til at bogmærker af søge resulteter når man bruger en søgemaskine. Hvis denne streng er meget lang risikere man at få et bufferoverflow i browseren eller visse webservere, der så vil crashe. En overgang kunne enhver Microsoft Internet Server version 4.0 hackes på den måde. Get metoden kan altså kun bruges til programmer hvor mindre end ca. 200 bytes skal returneres.

Alternativt findes post metoden for at sende data tilbage til webserveren. Når et data Post'es så modtager CGI-programmet data på standard input, ligesom data var indtastet på tastaturet, men de er stadig URLenkrypterede, og systemvariablen CONTENT_LENGTH indeholder antallet af bytes der kan indlæses. Det har fordelen at meget store datamængder kan returneres uden problemer, men det er umuligt at lave en bookmark til den side, der returneres efter en indtastning. Det tidligere eksempel kan let omskrives til at bruge post metoden:

<form action="/cgi-bin/post_metoden.cgi" method="POST">
   Indtast navn: <input type="text" name="navn"><br>
   Indtast e-postadresse: <input type="text" name="adresse"><br>
   <input type="Submit">
</form>
#!/usr/bin/perl

#filnavn: post_metoden.cgi
#URLdekrypt funktionen kommer fra get_metoden.cgi

sub HentPostData
#Returnere en variabel med strengen på stdin
{
#antallet af bytes der venter på stdin
   my $ContentLength = $ENV{"CONTENT_LENGTH"};

   my $input="";

#max 10 kb input.
   my $maxsize=10240;

   if($ContentLength)
    {
       if ($ContentLength<$maxSize)
          {
            read(STDIN,$input,$ContentLength);
          }
        else
          {
            print "Content-Type: text/plain\r\n\r\n";
            print "Script input exceeds acceptible size limit!";
            die "Error! $ENV{'REMOTE_ADDR'} attempted to submit $contentLength bytes!\n"; 
          }
     }

   return $input;
}

my %data=URLdekrypt(HentPostData());

#udskriv data
print "Content-Type: text/plain\r\n\r\n";
print "Jeg modtog følgende data:\n";

foreach (sort keys %data)
    {
       print "   $_ = \"$data{$_}\"\n";
    }

Post metoden kan modtage lige så store datamænger som man ønsker. Men antallet af programmer der kan kører parallelt og mængden af hukommelse på den computer de køre på sætter en grænse. I ovenstående eksempel er grænsen sat ved 10kb. Hvis en browser forsøger at sende flere data vil CGI-programmet returnere en fejl til browseren, programmet vil så dø efter at havde gemt en diagnostisk tekst i webserverens fejllog, den diagnostiske tekst indeholder $ENV{'REMOTE_ADDR'}, hvilket er IP addressen på den computer der har submittet et for stort svar. Det følgende afsnit forklarer hvilke andre data et CGI-program modtager.

2.1.9. Information tilgængelig for et CGI-program

Når et program udføres, så modtager det information om browserens IP address (typisk ikke dens DNS navn), og browserens navn og version. Kun hvis programet befinder sig i et adgangskodebeskyttet katalog kan man få information om brugeren, der har indtastet data.

I HTML-koden der kalder programmet kan der også indlejres information f.eks.:

<input type="hidden" navn="hemmelig" value="Jeg er en hemmelig streng">

Hvis HTML-koden til en webformular udskrives af et program, og det program ønsker at overføre information til det program der modtager de indtastede data, så kan skjulte felter anvendes. Webbrowseren viser ikke disse felter for brugeren, og deres indhold returneres uændret til programmet, der modtager de indtastede data. I det ovenstående eksempel vil programmet modtage en variabel med navnet "hemmelig", der har indholdet "Jeg er en hemmelig streng".

CGI-programmer kan også modtage en sti i systemvariablen PATH_INFO, og argumenter i QUERY_STRING. Disse kommer fra action=".." linjen i HTML-koden for webformularen. Følgende er en rækker eksempler på hvordan et program kan kaldes fra HTML-koden i en webformular:

Tabel 2-2. URL til CGI-program

URL til CGI-program Forklaring
/cgi-bin/debug.cgi Programmet modtager ikke nogen kommandolinjeargumenter.
/cgi-bin/debug.cgi?arg1&arg2&arg3 QUERY_STRING-systemvariablen vil indeholde strengen "arg1&arg2&arg3".
/cgi-bin/debug.cgi/foo/bar PATH_INFO-systemvariablen vil indeholde strengen "/foo/bar".
/cgi-bin/debug.cgi/foo/bar?arg1&arg2&arg3 Både QUERY_STRING og PATH_INFO vil indeholde data.

Et eksempel på HTML-koden til en webformular der anvender kommandolinjeargumenter og skjulte variable:

<form action="/cgi-bin/debug.cgi/foo/bar?arg1&arg2" method="POST">
   Indtast navn: <input type="text" name="navn"><br>
   Indtast Email: <input type="text" name="adresse"><br>
   <input type="hidden" navn="hemmelig" value="Jeg er en hemmelig streng">
   <input type="Submit">
</form>

Følgende er en liste af de mest interessante systemvariable som CGI-programmer har til rådighed:

Tabel 2-3. Variable

Variabel Indhold
REQUEST_METHOD Indeholder GET eller POST. I eksemplet ovenover "POST".
PATH_INFO Indeholder stien program URLen i webformularens HTML-kode. I eksemplet ovenover indeholder den "/foo/bar".
PATH_TRANSLATED Indeholder PATH_INFO katalog data men relativt til WebRoot. Hvis webfiler ligger i /www så vil den i eksemplet indeholde "/www/foo/bar".
QUERY_STRING

Get metode: Indeholder URLenkrypted indtastningsdata. Post metode: Indeholder argumenter efter '?' i URL'en til CGI-programmet. I eksemplet ovenover indeholder den "Arg1&Arg2"

HTTP_ACCEPT Indeholder de media typer, som browseren kan forstå. En tekst browser vil for eksempel ikke forvente grafik.
HTTP_ACCEPT_CHARSET Tegnkodning som browseren kan forstå. Værdien kan for eksempel være "iso-8859-1,utf-8,*", hvilket betyder at browseren foretrækker UTF-8-tegnkodningen, sekundært UTF-8-tegnkodningen, men i øvrigt accepterer vilkårlige tegnkodninger.
HTTP_ACCEPT_LANGUAGE Det sprog som browseren fortrækker et svar på. For en bruger der fortrækker Dansk men den indeholde "da, en". (I Netscape sættes sprog preferencer i Edit/Preferences/Navigator/Languages)
HTTP_ACCEPT_ENCODING Hvis browseren er istand til at klare komprimerede data. f.eks. "gzip, compress".
HTTP_USER_AGENT

Identificere brugerens browser. f.eks. Netscape 4.73: "Mozilla/4.73 [en] (X11; U; Linux 2.2.16-3 i686)" Galeon (en gtk browser baseret på Gecko): "Mozilla/5.0 (X11; U; Linux 2.2.16-3 i686; en-US; Galeon) Gecko/20000713".

REMOTE_ADDR IP addresse på computer hvor data er blevet indtastet.
REMOTE_HOST DNS navnet der svarer til IP-adressen i REMOTE_ADDR. Denne returneres kun hvis HostNameLookups er slået til i Apache's kontrol filer. Det er den ikke som standard da det forsinker Apache at skulle lave et DNS opslag for hver forbindelse. (Den var slået til i den første netcraft sammenligning mellem Apache og MS Interne Server)
REMOTE_USER Hvis programmet ligger i et kodeordsbeskyttet katalog, så indeholder REMOTE_USER navnet som brugeren har indtastet sammen med sit kodeord. Det er ikke nok at kodeordsbeskytte den html side, der indeholder webformularen!

2.1.10. Et udgangspunkt for CGI-programmer

Det følgende program, echo.cgi, kan bruges som udgangspunkt for egne CGI-programmer og for at debugge webformularer. Det kan modtage både Get og Post data, udskriver alle de data det modtager.

#!/usr/bin/perl

#Filnavn: echo.cgi

#URLdekrypt er den samme funktion som i get_metoden.cgi
#HentPostData kommer fra kodestumpen i afsnittet om Post metoden 

my %data;

if ($ENV{'REQUEST_METHOD'} eq "POST") 
   {
      %data= URLdekrypt(HentPostData());                      #Post data
   }
  else
   {
      %data= URLdekrypt($ENV{'QUERY_STRING'});                #Get Data
   }

#Indtastningsdata er nu indlæst i %data

#Udskrivning af data
print "Content-Type: text/plain\r\n\r\n";
print "Variable Modtaget:\n\n";

#udskriv systemvariable
print "Systemvariable:\n";
foreach (sort keys %ENV)
    {
       print "   $_ = \"$ENV{$_}\"\n";
    }

#udskriv webformular data

if (%data)
{
  print "\nWebformular data:\n";

  foreach (sort keys %data)
    {
       print "   $_ = \"$data{$_}\"\n";
    }
}

#Hvis POST metoden bruges og der er argumenter så udskriv disse.

if ($ENV{'QUERY_STRING'} and $ENV{'REQUEST_METHOD'} eq "POST")
 {
   print "\nArgumenter:\n";

   foreach (split /&/, $ENV{'QUERY_STRING'})
       {
          print "   \"$_\"\n";
       }
}

2.1.11. Typiske fejl

Apache returnerer "Not Found":

Årsagen er at Apache ikke kan finde CGI-programmet.

Hvis URL'en indeholder /cgi-bin/ kan fejlen være at du ikke har tilladt cgi-programmer i /etc/httpd/conf/httpd.conf.

Apache returnerer "Forbidden":

Årsagen er, at Apache ikke kan udføre programmet.

Problemet skyldes "Option ExecCGI" ikke er slået til for det katalog som CGI-programmet ligger i, dette gøres i access.conf. Alternativt kan fejlen skyldes at Apache ikke har lov til at udføre programmet, det kan typisk kan det løses ved "chmod o+rx program.cgi".

Apache returnere "Internal Server Error"

Denne fejl betyder at det første der udskrives ikke er et korrekt formateret HTTP-hoved. Fejlen kan jo også have andre årsager.

Stderr og stdout output fra Perl-programmer kan findes i webserverens fejllog ofte lokaliseret i /var/log/httpd/error_log, og det gør det let at løse denne type fejl. Et hurtigt syntakstjek er at køre CGI-programmet på kommandolinjen og se om der kommer syntaks fejl.

En anden mulighed er at Apache ikke har rettigheder til at læse programmet, det kan typisk kan løses med "chmod o+r program.cgi", denne type fejl bliver også logged i error_log. Tager Perl-programmet for lang tid at udføre vil en timeout af webserveren også returnere denne fejl.

Fejlen kan også opstå hvis Perl-fortolkeren ikke ligger i #!/usr/bin/perl, hvilket er en meget lumsk fejl fordi når man kører programmet vil den svare program.cgi: command not found (tcsh) eller bash: program.cgi: No such file or directory, programmet findes og udføres, fejlen skyldes at den ikke kan finde fortolkeren.

Browseren viser en The document contained no data.. Denne fejl kommer hvis programmet kun udskriver HTTP-hovedet. Typisk sker der en fejl i programmet så det dør, og udskriver en fejl i Apaches fejllog, men undlader at printer en fejl først der kan vises i browseren. Istedet for die kan følgende stump kode bruges hvis man i forvejen har udskrevet HTTP-hovedet ($! er fejlteksten svarende til sidste fejl):

sub Fejl
#udskriver fejl til browser og error_log
{
  print "Fejl! $!";
  die "Fejl! $!";
}

CGI-program situationer, der typisk giver fejl:

Programmer køres af webserveren og ikke af dig. Typisk er webserver processen eget af bruger 'nobody' og group 'nobody', hvilket betyder at programmet skal have være udførbart og læsbart hvilket gøres med chmod o+rx program.cgi.

Hvis programmet skal gemme en fil i et katalog som du ejer, så skal det kataloget være skrivbart af webserveren. Det betyder at den bruger eller gruppe webserveren kører som (typisk »nobody« og »nobody«) skal have brugs og skrivetilladelse til kataloget. Det kan klares med kommandoen: chmod o+rwx katalog. Hvis man ikke vil give alle og enhver adgang til at gemme filer i det katalog kan man bruge suExec som forklaret i afsnittet om sikkerhedsaspekter.

2.1.12. Debugging af CGI-programmer

Der opstår fejl i alle programmer. CGI-programmer er dog mere besværlige at debugge fordi de typisk skal køres fra en browser med et korrekt form input for at generere en bestemt fejl, der typisk viser sig som en "Internal Server Error" eller "The document contained no data." i browseren. Og problemet er nu at finde fejlen.

Alt tekst der udskrives til stderr vil udskrives i Apaches fejllog. Dvs. opstår der en fejl i programmet og det udføre en die "Fatal error occured!", så vil der ikke udskrives nogen information til browseren men "tail /var/log/httpd/error_log" vil udskrive fejlmeddelelsen. Det er derfor praktisk at udskrive en fejl med print før at die kaldes, så fejlen kan ses i browseren.

Den hurtigste måde at debugge et lille program på er at indsætte print-linjer på passende steder i koden, gerne med en informativ besked om, hvad koden skal til at udføre ('Åbner forbindelse til databasen...'), eller hvad den netop har konstateret ('Der blev angivet en e-postadresse, sender nu mail...'). (Det hænder dog, at et program er blevet debugget med kortere og knap så informative beskeder, så som 'A' og 'ghjk'.)

Men er Perl-programmet meget stort eller komplekst kan det tage en del tid at finde fejlen ved at genstarte programmet gang på gang. Det er istedet mulig at fange input data og gemme dem i på harddisken, og så senere hente %data og %ENV hashes fra harddisken. CGI-programmet kan så køres i en debugger men med de samme data og systemvariable, som hvis det blev kørt af en webserver. Bemærk at det ikke køres med samme bruger og gruppe id!

Den følgende stump Perl-kode kan bruges til at debugge CGI-programmer med. Den anvender de funktioner der er erklæret i "Et udgangspunkt for et CGI-program" afsnittet. Køres programmet med $debug=0 vil det indlæse webformular data fra get eller post metoden og fortsætte, altså uden at der sker noget.

Køres programmet med $debug=1 og $gemdata=1 vil det indlæse webformular data og gemme dem i to filer. %data gemmes i en fil ved navn "grab.var", og systemvariable %ENV i en fil med navn "grab.env", programmet vil så stoppe.

Køres programmet bagefter med $debug=1 og $gemdata=0 vil programmet genoprette %data og %ENV, som da programmet kørte første gang og det vil så på en reproducibel måde behandle data, men med den fordel at dette kan ske på kommandolinjen eller i en debugger. De to filer er formateret i en syntaks, der ligner XML så det er også let at lave ændringer i datafilerne.

#!/usr/bin/perl

#filnavn: debug.cgi
#koden til URLdekrypt og HentPostData funktionerne er den samme som i echo.cgi

my %data;

#debug kode ------------------------------------------------

#basefilenavn
my $filename="grab";

#0 for at modtage data fra webserveren og bearbejde dem.
#1 for at slå debugging til

my $isDebug = 1;    

#0 for at afspille data fra fil.
#1 for at gemme data

my $grabdata = 0;   

sub Fejl
#udskriver fejl til browser og error_log
{
  print "Content-Type: text/plain\r\n\r\n";
  print "Fejl! $!";
  die "Fejl! $!";
}

sub GemHash
#Gemmer en hash til en fil i et XML lignende format.
{
   my ($hashref,$filnavn)=@_;
   my $tag,$streng;

   open OUT, $filnavn or die Fejl;
   while (($tag,$streng)=each %$hashref)
      {
#For at afkodning skal være entydig må '<' og '>' escapes.
         $streng =~ s/&/&amp;/g;
         $streng =~ s/</&lt;/g;
         $streng =~ s/>/&gt;/g;

         print OUT "<$tag>$streng</$tag>\n";
      }
   close OUT;  
}

sub HentHash
#Henter en hash fra en fil.
{
   my ($hashref,$filnavn)=@_;
   my $content;

#backup af record separatoren.
   my $tmp=$/; 

#indlæs hele filen på en gang
   open IN, $filnavn or die Fejl;
   $/=undef;
   $content=<IN>;

#loop gennem alle <tag>data</tag>'s.
   while ($content =~ /<(.*?)>([^<]*)<\/\1>/gc) { tohash($hashref,$1,$2); }

   $/=$tmp;
}

sub tohash
#addere hash data efter at escaped tegn er fixet
{
  my ($hashref,$key,$str)=(@_);

#Fix escaped tegn.
  $str =~ s/&lt;/</g;
  $str =~ s/&gt;/>/g;
  $str =~ s/&amp;/&/g;

  $$hashref{$key}=$str;
}

if (not $isDebug or $isDebug and $grabdata)
   {
#hvis ikke i fejlsøgningsmodus skal inddata fortolkes.
#hvis i fejlsøgningsmodus og grabdata så skal inddata også fortolkes.

        if ($ENV{'REQUEST_METHOD'} eq "POST") 
          {
             %data= URLdekrypt(HentPostData());          #Post data
          }
         else
          {
             %data= URLdekrypt($ENV{'QUERY_STRING'});    #Get Data
          }
   }

if ($isDebug)
{
  if ($grabdata)
     {
#Gem systemvariable og data hash.
        GemHash(\%ENV, ">".$filename.".env");
        GemHash(\%data,">".$filename.".var");

#udskriv og stop programmet når data er gemt.
        print "Content-Type: text/plain\r\n\r\n";
        print "Data er opsamlet og gemt i \"$filename.*\".";
        print "Sæt \$grabdata=0 og kør programmet i en debugger\n";
        die;
     }
  else
     {     
#Slet alle systemvariable
        %env=();

#hent systemvariable og data hash fra filer.
        HentHash(\%ENV,  $filename.".env");
        HentHash(\%data, $filename.".var");

#fortsæt CGI-programmet.
     }
}


#Her behandles webformular data.

Hvis programmet er skrevet i C eller C++ har man den mulighed at man kan debugge program processen mens det kører. Dette gøres ved at oversætte programmet med en option "-ggdb" for inkludere debug information, og indsætte en sleep(30) i starten af programmet (kræver unistd.h). Før programmet kører su'er man til webserver brugeren. Når man trykker Submit vil programmet pause i 30 sekunder, i den tid kan man udføre "ps aux | grep program.cgi" her kan man se programmets procesnummer og så udføre "ddd program.cgi <pid>" hvor <pid> er procesnummeret man fik fra ps-kommandoen. Man kan så single steppe programmet som det modtager, bearbejder og udskriver data. Dette trick virker desværre ikke med Java-, Pyton- og Perl-programmer da disse jdb, pydb og Perl-debuggere ikke kan debugge en kørende process. Og det kræver at man har nok rettigheder til at kunne attache til webserverens process.

2.1.13. Sikkerhedsaspekter

CGI-programmer modtager data fra en tvivelsom kilde, og kan bruge disse data som basis for hvilken som helst operation. I eksemplet der modtog data vha. post metoden blev problematikken omkring meget store datamængder omtalt, og koden der blev vist kan automatisk begrænse mængden af data programmet vil acceptere.

Og i afsnittet de data der var tilgængelig til et program blev skjulte variable præsenteret som en sikker måde at overføre skjult information i en webformular, men enhver kan i HTML-koden se at der er skjulte variable, og det er en let sag at ændre de skjulte variable (gem HTML-koden, ret i de skjulte variable, åben HTML-koden, indtast data, og submit dem). Er det vigtigt, at de skjulte variable ikke forfalskes må man bruge en eller anden tjeksums algoritme. Ligeledes kan en ondskabsfuld person returnere helt andre variable end dem der står i webformularen, man kan altså ikke stole på at fordi man har et name="navn" felt i formularen, at det URLenkrypteret svar vil indeholde det tilsvarende navn=.. felt. I perl vil dette svare til %data{'navn'} hvor "navn" ikke findes i hashen og den returnere "", så det er ikke noget stort problem.

En typisk problem kan være at data fra en webformular kan bruges som filnavn til at gemme information, og derfor overskrive filer der ikke burde overskrives, det bør derfor altid tjekkes om filen findes i forvejen, og hvad der så skal ske. I stedet for et filnavn kan programmet måske snydes til at anvende en kanal (engelsk "pipe"). For eksempel betyder |program at mens programmet tror det skriver til en fil, skriver det i virkeligheden til en kanal, der leder dataene videre til programmet program. På denne måde kan man altså udefra komme til at køre en vilkårlig kommando på systemet, hvis kanal-tegnet (|) ikke bliver fjernet fra filnavne.

Det kan også være farligt at udskrive for meget information om den server som programmet kører på, siden at den type information kan misbruges af hackere. Det følgende stump kode indeholder to sikkerhedsfejl:

#$filnavn er navnet på en fil og er indtastet i en webformular.

my $fil = "/www/filer/$filnavn";

#Indlæs og udskriv filen

Indtastes nu ../../etc/passwd som filnavn i webformularen, så vil programmet vise alle brugere på serveren og deres enkrypterede passwords (med mindre shadow password bruges), og den information er tilgængelig for enhver på nettet. En hacker kunne så på sin egen computer lave et brute force angreb ved at prøve en masse passwords igennem, indtil at han rammer et password der giver det enkrypteret password, som det i password filen. Han kan så logge ind på serveren hvor CGI-programmet udføres. Moralen er at alle filnavne skal tjekkes for at sikre sig at de ikke indeholder katalog information. En anden fejl er at programmet selv udskriver siden, det betyder at webserverens adgangskontrol til sider bliver tilside sat. Den følgende kode stump tjekker om filnavnet indeholder '/' og istedet for at udskrive filen udskrives en Location til filen. På den måde virker Apaches adgangskontrol og ikke mindst adgangsbegrænsning stadig

#$filnavn er navnet på filen og kommer fra et en web formular.

die "Suspekt filnavn!\n" if  $filename =~ /\//;
my $fil = "/filer/$filnavn";

#udskriv en Location til filen.

Specielt farlige er de tre perl kommandoer eval, exec og system der gør det muligt at udføre programmer, der kører med samme rettigheder som Apache webserveren. System ved at der startes en shell (/bin/sh) og at denne modtager en streng af kommandoer, der skal udføres. Følgende program udskriver hvilke processer en bestemt bruger køre og indeholder en bagdør til webserver kontoen.

#$user er et bruger navn fra en webformular.

print "Content-Type: text/plain\r\n\r\n";
system("ps aux | grep $user");

En bruger kan nu taste "root" og se alle de tjenester der kører på maskinen. Det giver en idé om hvilke sikkerhedshuller der kan være i systemet. Men som om det ikke var nok så kan enhver taste "tyge ; rm -rf /" i webformularen.

Sker dette vil programmet udføre "ps aux | grep tyge ; rm -rf /", hviket betyder at programmet efter at havde udskrivet alle tyge's processer vil fortsætter med at rekursivt slette alle filer på filsystemet. Typisk vil webdeamonen kun have adgang til at slette alle webfiler, og de data som andre CGI-programmer har gemt. Men det kan være slemt nok hvis serveren er en dedikeret webserver for et firma. Alternativt kunne en indtastning "tyge ; ls -la /home/" udskrive alle hjemmekataloger og deres adgangsrettigheder. Hvis der er et hjemmekatalog, der er læsbart for webserveren kan hackeren læse alle filer, som den bruger har, og skrive til alle de filer, der er skrivebare for alle brugere, hvis der findes en .rhosts fil og hackeren kan skrive til den, vil han kunne logge ind som den bruger uden password fra en vilkårlig server i verden. Hackeren ville også kunne udsende breve, der tilsyneladende kommer fra webserveren.

Men det er stadig kun småting en hacker kan med dette simple program oploade en terminal server (f.eks. netcat), oversætte den og køre den, således at han kunne logge ind på webserveren med samme rettigheder som webserveren og uden noget password. Og når en hacker har adgang til at køre vilkårlige kommandoer kunne han tjekker versionsnumre på alle deamons og sammenligne disse med kendte sikkerhedsfejl indtil han finder en bagdør til systemadministratorkontoen (root).

Derfor bør man i et CGI-program altid undgå exec og system kommandoer. Med eval kan ethvert perl udtryk udføres, dvs. ethvert perl udtryk der åbner filer, sletter filer, udfører system eller exec. Det er derfor umuligt at tjekke at et eval udtryk ikke indeholder farlige kommandoer.

2.1.13.1. Tainted variable

Perl har indbygget en smart funktion der gør det muligt at mærke variable, der kommer fra farlige kilder (en farlig variabel kaldes "tainted" der betyder noget i retning af mærket/beskidt/usikker), systemvariable, input, og output som andre programmer laver er automatisk tainted. Bruges en tainted variabel som filnavn eller som argument i exec eller system kald, vil Perl automatisk stoppe med en fejl. Tainted variable slås til ved at kalde Perl som "#/usr/bin/perl -T" og de slås automatisk til hvis programmet er setuid dvs. "chmod ug+s program.cgi", således at programmet kan køres af webserveren med dine rettigheder.

Hvis man tager en delstreng ud af en tainted streng vil delstrengen også være tainted, og konkateneres en tainted og en normal streng bliver resultatet også tainted. Man kan derfor være rimelig sikker på at der opstår en fejl, hvis man forsøger at bruge usikker information i en farlig operation. Den eneste måde at untainte en streng på er ved at ekstrahere den fra et regulært udtryk, f.eks. et der fjerner forbudte tegn som ';','|' og '/'. Se "man perlsec" for mere information om tainted variable.

Det følgende eksempel bruger tainted variable, og viser hvordan det untaintes.

#!/usr/bin/perl -T

#hent variable fra webformularen.
#$user er et brugernavn indtastet i webformularen.
#det er derfor automatisk tainted.

if ($user =~ /^(\w+)$/)
 {
   $user = $1;

#$user er nu untainted og kan kun indeholde
#følgende tegn A-Z,a-z,0-9, samt '_'
 }
else 
 {
   die "Suspekt brugernavn!\n";
 }

die "Fy! fingerene væk!" if ($user =~ /root/);

print "Content-Type: text/plain\r\n\r\n";
system("ps aux | grep $user");
#da $user er untainted generere dette ikke en fejl.

Hvis flere bruger skal have adgang til at køre programmer på den samme webserver, så kan det være praktisk at anvende suEXEC (Swich User for Exec). suEXEC er et add-on til Apache, der tillader CGI-programmer at køre med samme rettigheder som den individuelle bruger, der har lavet programmet istedet for som webserveren. Det er langt sikrere end at setuid'e programmet med "chmod ug+s program.cgi", men kræver også at det bliver korrekt sat op for at undgå sikkerhedshuller og Apache genoversættes, det er derfor ikke inkluderet i den Apache version der normalt distribueres. Dokumentation hvordan Apache genoversættes med suEXEC indbygget er inkluderet i Apache dokumentationen og kan findes i /home/httpd/html/manual/suexec.html. suEXEC giver også mulighed for at hver bruger har sin egen error_log fil hvilket letter debugging af programmer.

2.1.14. FastCGI

En ulempe ved at skriver CGI-programmer i Perl er at hver gang de køres skal Perl-fortolkeren starte og den skal fortolke den samme Perl-kode igen og igen, og det tager en del tid hvilket kan være upraktisk hvis programmet skal køres mange gange i sekunded.

Bedre ville det være hvis CGI-programmet kørte hele tiden, og kunne modtage mange data uden at skulle genstartes. Dette er hvad fastCGI gør muligt ved at definere en protokol for hvordan en process kan kommunikere med webserveren's CGI-modul.

2.1.14.1. Installation af FastCGI

Installér apache-devel programpakken (indeholder apxs programmet).

Hent kildeteksten til FastCGI apache-modulet fra http://www.FastCGI.com

Oversæt koden med:

apxs -c -o mod_fastcgi.so *.c

Installér Apache-modulet med:

apxs -i -a -n fastcgi mod_fastcgi.so

Tjek at mod_fastcgi er inkluderet i httpd.conf og at path til det dynamiske bibliotek er korrekt.

Adder følgende linje til srm.conf

AddHandler fastcgi-script fcg fcgi fpl

Genstart Apache:

[root@hven /root]# /etc/rc.d/init.d/httpd restart

Apache webserveren understøtter nu fastCGI.

http://www.cpan.org kan man hente et FCGI-modul der tillader Perl-programmer at anvende fastCGI-modulet. Perl-modulet som andre Perl-moduler med:

[tyge@hven ~]$ tar xzvf FCGI-0.53.tar.gz
[tyge@hven ~]$ cd FCGI-0.53/
[tyge@hven ~/FCGI-0.53]$ perl Makefile.PL
[tyge@hven ~/FCGI-0.53]$ make && echo O.k.
...
O.k.
[tyge@hven ~/FCGI-0.53]$ su -c 'make install&& echo O.k.'
...
O.k.
[tyge@hven ~/FCGI-0.53]$ cd ../
[tyge@hven ~]$ rm -rf FCGI-0.53/

2.1.14.2. Brug af FastCGI

Gem følgende program som fastcgi_test.fcgi og kør det flere gange i browseren.

#!/usr/bin/perl
use FCGI;

#filnavn: fastcgi_test.fcgi
my $count=0;

my $request = FCGI::Request();

while($request::accept() >= 0) 
{
   print "Content-Type: text/html\r\n\r\n";
   print "<h1>FastCGI test</h1>\n";
   print "<p>Antal af forbindelser for dette program: ++$count</p>\n";

#her kan resten af CGI-programmet udføres.
}

Det specielle ved FastCGI-programmer er at de kan huske tidligere forbindelser. $count i eksemplet tælles op for hver gang programmet modtager en forbindelse. Efter at fcgi-programmet er udført kan man med "ps aux | grep perl" se at det venter på næste gang det skal modtage data. En forskel på FastCGI og normale Perl-programmer er at Perl-programmer løbende udskriver print bufferen til webserveren, mens FastCGI-programmer kun returnere data når de afsluttes, eller hvis bufferen flushes med $request->Flush().

Hvis man ændrer kildeteksten, og prøver programmet igen skal man være opmærksom på at man kan fælde et antal af gamle fcgi-programmer, der må dræbes manuelt med "kill <pid>". FastCGI-programmer kan også laves i C og Tcl se http://www.fastcgi.com for mere dokumentation om protokollen og hvordan den anvendes.