Perl en CGI scripts 1

Ik heb deze pagina gesplitst:
dit deel is voor 'beginners',
deel twee gaat verder met zaken als cookies

Gewone webpagina's (.html files) zijn nogal statisch. Wat wordt weergegeven is wat er in staat, en dat is wat er van tevoren in is gezet met bijvoorbeeld een html-editor. Maar, soms wil je de inhoud van een webpagina meer dynamisch laten zijn; bijvoorbeeld de inhoud aanpassen aan de omstandigheden. Een voorbeeld hiervan in mijn zoek-pagina: deze pagina wordt op het moment van aanvraag gemaakt, en geeft dan de resultaten van een zoek-opdracht weer. Andere voorbeelden zijn pagina's die databases raadplegen, die bezoekersstatistieken bijhouden, feedback in een file op de server opslaat (zie onderaan deze pagina), en zo voort.

Dit gaat niet zo maar met alleen html, of zelfs java(script). Je moet voor iets dergelijks echt een 'programma' op de web-server laten draaien, die bijvoorbeeld voor de search de daar aanwezige set van html pagina's afzoekt op de gezochte termen. Een manier op dit te doen is met zogeheten CGI scripts, stukjes programma die op verzoek van een gebruiker draaien op de web server. CGI scripts zijn dus anders dan bijvoorbeeld Java scripts: Java scripts draaien op de computer van de gebruiker, en niet op de web server.

CGI staat voor 'Common Gateway Interface', en is een standaard manier om gegevens van een gebruiker (dus iemand die een web-page bekijkt met zijn browser) door te geven naar een programma (script) op de server, dus net de andere kant op waarin html pagina's gestuurd worden. Bij de genoemde zoekpagina is dit bijvoorbeeld het woord waarop gezocht moet worden.

Perl is vanouds vaak gebruikt voor het maken van deze 'CGI scripts'. Een voorbeeld van een Perl CGI script is de actie onder de zoek-knop op mijn web-pagina's: via het invulformuliertje voor de zoekterm wordt dit woord doorgegeven naar dit CGI script, deze gaat vervolgens mijn webpagina's afzoeken op dat woord, en maakt een HTML-pagina waar de zoekresultaten op te vinden zijn. Ps: een andere veel gebruikte taal voor dit doel is PHP.

Deze pagina legt uit hoe je simpele CGI scripts kunt maken met behulp van Perl. Uitgebreidere documentatie van de CGI module is te vinden op onder ander de site van ActiveState, of op CPAN. Ook de Perl-CGI FAQ geeft natuurlijk inzicht. Ps: ik gebruik gewoon de CGI functies, en niet de object-georiënteerde stijl, die ook mogelijk is.

Een simpel CGI script

Een van de simpelste CGI scripts: het befaamde afdrukken van 'hello world' in het scherm van de internet browser (Internet Explorer, FireFox, of welk programma dan ook) van de kijkende persoon. De kijker ziet verder niet dat dit door een CGI script wordt verzorgt, die ziet alleen het resultaat: een scherm met daarop de tekst 'Hello, World' (ja, dat zou ook gewoon met een statische html pagina kunnen, nog even geduld). Let op: ook verder op de pagina gebruik ik de gele achtergrond om resultaten van de scripts weer te geven. Hieronder mijn Perl script genaamd hello_world.cgi:

#!/usr/bin/perl -T

use CGI qw/:standard -no_xhtml/;        # load standard CGI routines

print header(),                         # create the HTTP header
      start_html('Hello World demo'),   # start the HTML
      p('Hello, World.'),
      end_html();                       # end the HTML

#end of script

De uitleg, regel voor regel, met de gegenereerde output:

  • De eerste regel (#!/usr/bin/perl) geeft aan dat het om een Perl script gaat, sterker nog: geeft ook aan waar het serversysteem Perl kan vinden. Opgelet, dit kan per provider anders zijn; kan je vaak wel op de help-pagina's van je provider vinden. De huidige regel zal meestal wel werken, maar is niet gegarandeerd. De -T optie is ook belangrijk, maar hierover later wel meer.
  • De regel 'use CGI qw/:standard -no_xhtml/;' zegt Perl dat we de standaard CGI module willen gebruiken. Niet per se nodig, maar hier zitten handige hulproutines (zoals header() ) die bijvoorbeeld een deel van de details voor het maken van een HTML pagina voor hun rekening nemen. Maakt het leven een stuk makkelijker. We vragen aan om de standaard set CGI routines te nemen, en om geen xhtml te genereren, maar standaard html.
  • Dan gaan we de eigenlijke web pagina maken en naar je web browser sturen. Het print-commando is al automatisch gekoppeld aan het internet-kanaal naar je web browser, dus alles wat we met print afdrukken, gaat naar je browser. We beginnen met de HTTP header via print header(), die naar je web browser aangeeft dat er een html pagina aan gaat komen (had ook bv een plaatje of zo kunnen komen):
    Content-Type: text/html; charset=ISO-8859-1
  • Nu de start van de eigenlijke html pagina, met de titel en zo: print start_html('Hello World demo');
    drukt de hele html header af (wordt al weer een boel voor ons gedaan):

    <!DOCTYPE html
        PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
    <html lang="en-US"><head><title>Hello World demo</title>
    </head><body>

  • Nu kunnen we de eigenlijke inhoud van de webpagina afdrukken: de 'Hello World' string als een paragraph in HTML: print p('Hello, World.'); zorgt hier voor de goede formattering
    <p>Hello, World.</p>
  • Nu nog de HTML pagina netjes afsluiten, hiervoor heeft de CGI module de functie end_html():
    </body></html>

De gebruikte CGI functies kunnen nog veel meer, bijvoorbeeld door het meegeven van extra parameters in de headers, maar voor dit simpele voorbeeld ga ik daar niet op in (later, met complexere voorbeelden). Voor meer uitleg, zie de documentatie van de CGI module in Perl.

Om dit werkend te krijgen moet dit script nu worden ge-upload naar de server waarop je je web-pagina's hebt staan (toch wel van tevoren gecheckt of je provider wel Perl ondersteunt? Is niet altijd het geval). Zoek eerst op in welke directory de cgi scripts horen te staan, dit is per provider verschillend (bij mij in de directory 'cgi'). Upload het script, en zorg dat de execute-rights goed staan. Dit is bij mij dan het resultaat: klik deze link.

En nu wat moeilijker

Waarschuwing: In dit voorbeeld wordt de waarde van het argument niet gecheckt voordat het in de HTML-pagina wordt afgedrukt... Dit geeft de gebruiker de kans het script te misbruiken, door bijvoorbeeld een slim gecodeerd Javascript mee te geven (code injection)!  Altijd dus checken of wat de gebruiker meegeeft wel met goede bedoelingen is; zie verderop bij 'Input checken!'.

Dit was nogal statisch. Hoe krijg je nu bijvoorbeeld input van de gebruiker in het script, daar was CGI toch voor? Dit kan bijvoorbeeld via de link naar de pagina, of via een invulformulier. Het script ziet er in beide gevallen hetzelfde uit: zie dit voorbeeld genaamd 'argument.cgi'. Dit wordt bijvoorbeeld via een link als hier: cgi/argument.cgi?argument=Ook%20hallo aangeroepen (later hoe dit met een formuliertje/invulveld als op een zoekpagina gaat). Het print-gedeelte uit het script (de rest blijft hetzelfde als in hello_world.cgi):

print header(),                         # create the HTTP header
      start_html('Hello World demo'),   # start the HTML
      p('Hello, World.'),
      p('De meegegeven parameter is: "' . param('argument') . '".'),
      end_html();                       # end the HTML

Er is nu een regel bijgekomen, die de meegegeven parameter in de link ophalen en afdrukken. Met behulp van de functie param() kunnen we parameters ophalen, die we vervolgens kunnen afdrukken. In dit geval verwacht ik een parameter genaamd 'argument' en haal deze op met param('argument'), en druk deze af. De bovenstaande aanroep levert nu de volgende pagina op:

Hello, World.
De meegegeven parameter is: "Ook hallo".

Wat misschien opviel is hoe het argument werd meegegeven in de link: sommige tekens mogen niet in een link staan (zoals spaties), deze worden vervangen door hun hexadecimale code (%20). De CGI module zorgt er voor dat je vanuit je Perl script hier geen last van hebt: wordt gewoon als een spatie afgedrukt.

Wel is dit natuurlijk nogal lastig voor de gebruiker om zelf zo een link te moeten maken: hoe gaat dit nu netjes en makkelijk, en zonder deze problemen van verboden tekens, via een net invulveldje? Hiervoor moet in je webpagina een html-'form' zijn opgenomen als hieronder (druk gerust op de knop om het te testen, en let op de link van de verschijnende pagina):

De parameter:

Voor de duidelijkheid de bijhorende html-code waarmee dit veld werd gemaakt (normaal zie je deze code niet, wordt door je html-editor onder water aangemaakt als je een form maakt):

  <form action="http://www.keesmoerman.nl/cgi/argument.cgi" method="get">
    <p>De parameter:
    <input size="40" maxlength="100" name="argument" type="text">
    <input value="Verzend!" type="submit"></p>
  </form>

Dit staat dus (zoals bij mij het zoek-scherm invulveld) op je normale html-pagina. Bij het drukken op de verzend-knop wordt de waarde van het invulveldje netjes aan een link (opgegeven in de action parameter van de form) geplakt, en doorgestuurd naar de server, waar het CGI-script wordt aangeroepen op de eerder uitgelegde manier.

Er is overigens nog een andere manier om parameters mee te geven, dit gaat via method="post" in de form definitie (ps: 'post' heeft niets met email te maken). Via 'get' kan je maar beperkt data meegeven (max 128 tekens?), en is de data zichtbaar in de link (wat je niet altijd wilt), via method="post" gebeurt dit 'onzichtbaar' voor de gebruiker, en is er geen limiet aan de grootte van de data. Maar, voor het CGI script maakt dit eigenlijk niet echt uit, werkt voor beide methodes: de CGI module kan beide methodes aan. Let op het verschil in de link van de antwoord-pagina!

De parameter:

'get' gebruik je bijvoorbeeld als je wilt dat mensen de link kunnen bookmarken (als favoriet kunnen opslaan) inclusief parameters, 'post' meer als je dat juist niet wilt (dat het script altijd 'schoon' start), of als er gegevens inzitten als wachtwoorden waarvan je niet wilt dat mensen dat in de link terugzien (hoewel dit niet waterdicht is).

Input checken!

Zoals al aangegeven met de waarschuwing aan het begin van dit hoofdstuk: check altijd de waardes die de gebruiker invult. Welke check je doet is afhankelijk van wat je met de data gaat doen. In dit geval wil ik voorkomen dat er (bedoeld of onbedoeld) HTML-code in gestopt wordt, die vervolgens in de antwoordpagina wordt ingevoegd. Hier kan dan bijvoorbeeld een Java-script verstopt zitten, met mogelijk kwalijke bedoelingen!

In HTML heeft een aantal tekens speciale bedoelingen (bijvoorbeeld de '<' en '>'). Wat ik dus doe in het eigenlijke script is net iets anders dan hierboven weergegeven: ik filter de speciale tekens uit, en vervang ze door een notatie die ze correct afdrukt, in plaats van ze als HTML te interpreteren... Oftewel param('argument') wordt vervangen door escapeHTML(param('argument')), wat alle speciale tekens vervangt door hun HTML-notatie (bijvoorbeeld '<' wordt '&lt;').

Een andere check zou kunnen zijn of de data langer is dan 100 tekens: dan komt het niet meer uit het formulier (dat de lengte tot 100 tekens beperkt heeft met maxlength="100") maar is het door iemand handmatig geconstrueerd: waarom zou iemand zo iets willen doen? ...

Mocht je meer met de input data doen, dan kunnen strengere checks nodig zijn. Voorkom dat mensen commando's kunnen verstoppen (niet bijvoorbeeld je disk kunnen wissen). Beter te veel checken (bv welke tekens mogen er in voor komen?) dan te weinig! Lees ook de opmerkingen over security in deel 2!

Met dank aan Mathias, die me hier met een prachtig voorbeeld op gewezen heeft!

Nog meer gegevens

Naast de parameters (op te vragen met de param() functie) zijn er nog meer gegevens die worden doorgegeven over de aanroeper. Zo kan je te weten komen waarvandaan het script werd aangeroepen, met welke browser, en zo voort. Een deel van deze functies staat in de onderstaande tabel (de rest in de documentatie):

param() Al eerder gezien: met de naam van een parameter/argument geeft dit je de waarde. Maar, extra functie: als je param() aanroept zonder argumenten, geeft het een lijst terug met de namen van de argumenten.
path_info() Een ander vorm van parameter: als je je script aanroept met een pad achter de naam van het script, dan komt dat pad hier in: bij cgi/test.cgi/nog/een/stuk?arg=hallo geeft path_info() de string: /nog/een/stuk
remote_host() De naam of het IP adres van de aanroeper
script_name() De naam van je CGI script
referer() De pagina vanwaar je script werd aangeroepen (niet voor alle browsers)
user_agent() De naam van het operating system en gebruikersprogramma (bijvoorbeeld de browser) waarmee de pagina is opgevraagd, als "Mozilla/5.0 (Windows; U; Win 9x 4.90; en-US; rv:1.7.12) Gecko/20050915 Firefox/1.0.7": ik gebruik Firefox op Windows ME (Win 9x 4.90)
request_method() De aanroepmethode van je script, meestal een van de volgende waarden: 'POST', 'GET' of 'HEAD'.
Dump() Geeft een mooi geformatteerde lijst van alle in de link meegegeven argumenten, handig bij foutzoeken.

Zo zijn er nog meer variabelen om meer te weten van je omgeving. Een script waar een aantal van deze aanroepen (en nog wat andere zaken, zoals cookies; zie deel 2) worden gedemonstreerd is meer_data.cgi: probeer, of download.

Foutzoeken en debuggen

Vaak is het lastig om cgi-scripts te debuggen. Ze draaien normaal op de server, waar je niet direct bij kan, en ook niet altijd de foutmeldingen kan bekijken. Hier verschillende hints om foutzoeken in cgi-scripts te vereenvoudigen.

Command-line debuggen

Om te beginnen: probeer het toch eens gewoon als Perl script op je eigen computer te draaien. De 'get'-methode voor CGI parameter overdracht kan bij de cgi-module namelijk ook nagespeeld worden met command-line parameters, bijvoorbeeld als (bij meerdere argumenten scheiden met spaties). Ook kan je op deze manier de Perl debugger gebruiken. Aanroepvoorbeeld:

C:\> perl c:\myscripts\argument.cgi argument=Ook%20hallo

Wel moet je even checken wat je nog meer 'na moet bouwen' van je serveromgeving, bijvoorbeeld bepaalde files, of het zetten van extra cgi-info in environment-variables (b.v. PATH_INFO als je path_info() wilt kunnen gebruiken).

Foutmeldingen op de server te zien krijgen

Een ander veel voorkomend probleem is dat je niet bij de error log op de server kan komen, en dus niet de door Perl gegenereerde foutmeldingen kan zien. Een oplossing die daarvoor door de cgi module ondersteund wordt is het redirecten van de foutmeldingen naar de html output, dus naar je gegenereerde web pagina. Neem daarvoor de volgende code op in het begin van je code (in plaats van use CGI....):

use CGI::Carp qw(fatalsToBrowser warningsToBrowser);
use CGI qw/:standard -no_xhtml/;  # load standard CGI routines
warningsToBrowser(1);
BEGIN { warningsToBrowser(1); }   # want warning as as HTML comments

Werkt overigens niet voor alle situaties: bijvoorbeeld fouten met de -T optie (zie eerder op deze pagina) vinden mogelijk te vroeg of op een te hoog niveau plaats om nog hierdoor opgevangen te kunnen worden.

Fouten in de aanroep van een CGI script

Ook kan het zijn dat de 'aanroep' van de cgi fout gaat: hier kan je het beste zo vroeg mogelijk in je script op testen, bijvoorbeeld met de volgende code:

my $error = cgi_error();
if ($error) {
    print header(-status=>$error),
    start_html('Problems'),
    h2('Request not processed'),
    strong($error);
    exit 0;
}

Nette foutmeldingen

Om bij later nog zelf gevonden foutsituaties (b.v. kan file niet openen) nette foutmeldingen te krijgen definieer ik een alternatief voor de 'die()' functie, die de foutmelding netjes in HTML verpakt; te gebruiken als bijvoorbeeld:

open FILE, "myfilename.txt" || die_html("Kan file 'myfilename.txt' niet openen");

De routine die_html() ziet er bijvoorbeeld in simpele vorm als volgt uit:

sub die_html                # fatal error: generate HTML error message
{
  my $message = shift;      # the error message
  my $prog_name = $0;
  $prog_name =~ s@.*/@@;    # get prog name, remove path
  my CGI scripts in Perl = "Error in '$prog_name'";

  if(!$header_sent)         # set as soon as I print header() in main
  {
    $header_sent = 1;       # header has been send; remember
    print header();         # CGI header: simple document type
    print start_html(-title => CGI scripts in Perl, -BGCOLOR=>'#F0FFFF');
  }
  print
    h1(CGI scripts in Perl),
    p("Error message: $message"),
    p("Please contact the web master of these web pages: ",
    a({href=>"mailto:mijn@email"}, 'Kees Moerman')
    ),
    end_html();             # end of HTML page

  exit(0);                  # terminate script
}

Diverse handige functies

Nog een handige functie: sommige tekens mogen niet zo maar in HTML strings voorkomen (zo worden de tekens '<' en '>' gebruikt om HTML tags weer te geven; in tekst moeten deze worden gecodeerd als '&lt;' en '&gt;'): de functie escapeHTML() zet alle speciale tekens om in deze vorm van codering:

$escaped_string = escapeHTML("unescaped string: <test>");

Verder naar deel 2.