Munka kiszolgálói környezetben

A korábbi fejezetekben áttekintettük, hogyan társaloghatunk távoli számítógépekkel
és hogyan vehetünk át adatokat a felhasználótól. Ebben a fejezetben ismét kitekint
ünk, olyan eljárásokat tanulunk meg, amelyek külsõ programok saját gépünk
ön való futtatására használhatók. A fejezet példái Linux operációs rendszerre
készültek, de a legtöbb alapelv felhasználható Microsoft Windows rendszerben is.
A fejezetben a következõket tekintjük át:
• Hogyan közvetítsünk adatokat a programok között?
• A héjparancsok végrehajtására és az eredmények megjelenítésére milyen lehet
õségeink vannak?
• Hogyan írhatunk biztonságosabb PHP programokat?
Folyamatok összekötése a popen() függvénnyel
Ahogy a fájlokat nyitottuk meg írásra vagy olvasásra az fopen() segítségével,
ugyanúgy nyithatunk adatcsatornát két folyamat között a popen() paranccsal.
A popen() paramétereiben meg kell adni egy parancsot elérési úttal, és azt, hogy
írási vagy olvasási módban akarjuk használni a függvényt. A popen() visszatérési
értéke az fopen() visszatérési értékéhez hasonló fájlazonosító. A popen()-nek
a 'w' jelzõvel adhatjuk meg, hogy írási módban, az 'r' jelzõvel pedig azt, hogy
olvasási módban akarjuk használni a függvényt. Ugyanazzal az adatcsatornával
nem lehet egyszerre írni és olvasni is egy folyamatot. Amikor befejeztük a munkát
a popen() által megnyitott folyamattal, a pclose() paranccsal le kell zárnunk az
adatcsatornát. A pclose() függvény paraméterében egy érvényes fájlazonosítót
kell megadni.
A popen() használata akkor javasolt, amikor egy folyamat kimenetét sorról sorra
szeretnénk elemezni. A 21.1-es példában a GNU who parancs kimenetét elemezzük,
és a kapott felhasználóneveket mailto hivatkozásokban használjuk fel.
21.1. program A who UNIX parancs kimenetének olvasása
a popen() segítségével
1:
2:
3: 21.1. program A who UNIX parancs kimenetének<br />4: olvasása a popen() segítségével
5:
6:
7:

A rendszerbe bejelentkezett felhasználók


8: 9: $ph = popen( "who", "r" )
10: or die( "A 'who' paranccsal nem vehetõ fel
kapcsolat" );
11: $kiszolgalo="www.kiskapu.hu";
12: while ( ! feof( $ph ) )
13: {
14: $sor = fgets( $ph, 1024 );
15: if ( strlen( $sor ) <= 1 )
16: continue;
17: $sor = ereg_replace( "^([a-zA-Z0-9_\-]+).*",
18: "href=\"mailto:\\1@$kiszolgalo\">\\1
\n",
19: $sor );
20: print "$sor";
21: }
396 21. óra
21.1. program (folytatás)
22: pclose( $ph );
23: ?>
24:
25:
A who parancs eredményének kiolvasásához szükségünk van egy fájlmutatóra
a popen() függvénytõl, így a while utasítás segítségével sorról sorra elemezhetjük
a kimenetet. Ha a sorban olvasható kimenet csupán egyetlen karakter, a while
ciklus következõ lépésére ugrunk, kihagyva az elemzést, különben
az ereg_replace() függvénnyel újabb HTML hivatkozással és soremeléssel
bõvítjük a végeredményt. Végül a pclose() függvénnyel lezárjuk az adatcsatornát.
A program egy lehetséges kimenete a 21.1. ábrán látható.
A popen() függvény használható parancsok bemenetének írására is. Ez akkor
hasznos, ha egy parancs a szabványos bemenetrõl vár parancssori kapcsolókat.
A 21.2. példában azt láthatjuk, hogyan használhatjuk a popen() függvényt
a column parancs bemenetének írására.
Munka kiszolgálói környezetben 397
21
21.1. ábra
A who UNIX parancs
kimenetének olvasása
21.2. program A column parancs bemenetének írása
a popen() függvény segítségével
1:
2:
3: 21.2. program A column parancs bemenetének<br />írása<br />4: a popen() függvény segítségével
5:
6:
7: 8: $termekek = array(
9: array( "HAL 2000", 2, "piros" ),
10: array( "Modem", 3, "kék" ),
11: array( "Karóra", 1, "rózsaszín" ),
12: array( "Ultrahangos csavarhúzó", 1,
"narancssárga" )
13: );
14: $ph = popen( "column -tc 3 -s / >
fizetve/3as_felhasznalo.txt", "w" )
15: or die( "A 'column' paranccsal nem vehetõ fel
kapcsolat" );
16: foreach ( $termekek as $termek )
17: fputs( $ph, join(‘/’, $termek)."\n");
18: pclose( $ph );
19: ?>
20:
21:
A 21.2. példában megfigyelhetõ, hogyan érhetõk el és írhatók ASCII táblázatként
fájlba a többdimenziós tömbök elemei. A column parancs bemenetéhez adatcsatorn
át kapcsolunk, amin keresztül parancssori kapcsolókat küldünk. A –t kapcsol
óval megadjuk, hogy táblázattá formázza a kimenetet, a –c 3 megadja a szüksé-
ges oszlopok számát, a –s / a /-t állítja be mezõelválasztóként. Megadjuk, hogy
a végeredményt a 3as_felhasznalo.txt fájlba írja. A fizetve könyvtárnak
léteznie kell és a megfelelõ jogosultság szükséges, hogy a program írhasson bele.
Most egyetlen utasítással egyszerre több dolgot tettünk. Meghívtuk a column
programot és kimenetét fájlba írtuk. Valójában parancsokat adtunk ki a héjnak:
ez azt jelenti, hogy az adatcsatorna használatával, egy folyamat futtatásával más
feladatokat is elindíthatunk. A column kimenetét például a mail parancs segíts
égével postázhatjuk valakinek:
398 21. óra
popen( "column -tc 3 -s / | mail kiskapu@kiskapu.hu", "w" )
Így elérhetjük, hogy a felhasználó által beírt adatokkal rendszerparancsokat hajtsunk
végre PHP függvényekkel. Ennek néhány hátrányos tulajdonságát még áttekintjük
az óra további részében.
Miután rendelkezünk egy fájlazonosítóval, a $termekek tömb minden elemén
végiglépkedünk. Minden elem maga is egy tömb, amit a join() függvény segíts
égével karakterlánccá alakítunk. Az üres karakter helyett mezõelválasztóként
a ’/’ karaktert választjuk. Ez azért szükséges, mert ha üres karakterek jelennének
meg a termékek tömbjében, az összezavarná a column parancsot. Az átalakítás
után a karakterláncot és egy újsor karaktert írunk ki az fputs() függvénnyel.
Végül lezárjuk az adatcsatornát. A 3as_felhasznalo.txt fájlban a következõk
szerepelnek:
HAL 2000 2 piros
Modem 3 kék
Karóra 1 rózsaszín
Ultrahangos csavarhúzó 1 narancssárga
A kódot hordozhatóbbá tehetjük, ha a szöveg formázására a sprintf() függv
ényt használjuk.
Parancsok végrehajtása az exec() függvénnyel
Az exec() egyike azoknak a függvényeknek, amelyekkel parancsokat adhatunk ki
a héjnak. A függvény paraméterében meg kell adni a futtatandó program elérési
útját. Ezen kívül megadható még egy tömb, mely a parancs kimenetének sorait fogja
tartalmazni és egy változó, amelybõl a parancs visszatérési értéke tudható meg.
Ha az exec() függvénynek az 'ls –al .' parancsot adjuk meg, akkor az aktu-
ális könyvtár tartalmát jeleníti meg. Ez látható a 21.3. példában.
21.3. program Könyvtárlista betöltése a böngészõbe
1:
2:
3: 21.3. program Könyvtárlista betöltése<br />a böngészõbe
4:
5:
Munka kiszolgálói környezetben 399
21
21.3. program (folytatás)
6: 7: exec( "ls -al .", $kimenet, $vissza );
8: print "

Visszatérési érték: $vissza

";
9: foreach ( $kimenet as $fajl )
10: print "$fajl
";
11: ?>
12:
13:
Ha az ls parancs sikeresen végrehajtódik, a visszatérési érték 0 lesz. Ha a program
a megadott könyvtárat nem találja vagy az nem olvasható, a visszatérési érték 1.
A végeredmény szempontjából nem tettünk semmi újat, hiszen az opendir() és
a readdir() függvényekkel ugyanezt elérhettük volna, de elképzelhetõ olyan
eset, amikor rendszerparancsokkal vagy korábban megírt Perl programokkal
sokkal gyorsabban megoldhatjuk a feladatot, mint PHP függvényekkel. Ha a PHP
program kifejlesztésének sebessége fontos szempont, esetleg érdemesebb
a korábban megírt Perl program meghívása mellett dönteni, mint átültetni azt
PHP-be, legalábbis rövid távon. Meg kell azonban jegyeznünk, hogy rendszerparancsok
használatával programjaink több memóriát fogyasztanak és többnyire
futásuk is lassabb.
400 21. óra
21.2. ábra
A könyvtárlista betölt
ése a böngészõbe
az exec() függvény
segítségével
Külsõ programok futtatása a system() függvénnyel
vagy a ` mûveletjel segítségével
A system() függvény az exec() függvényhez hasonlóan külsõ programok
indítására használható. A függvénynek meg kell adni a futtatandó program elérési
útját, valamint megadható egy változó, amely a program visszatérési értékét tartalmazza
majd. A system() függvény a kimenetet közvetlenül a böngészõbe írja.
A következõ kódrészlet a man parancs leírását adja meg:
print "
";
system( "man man | col -b", $vissza );
print "
";
?>
A PRE HTML elemet azért adtuk meg, hogy a böngészõ megtartsa a kimenet
eredeti formátumát. A system() függvényt használtuk, hogy meghívjuk a man
parancsot, ennek kimenetét hozzákapcsoltuk a col parancshoz, amely ASCII-ként
újraformázza a megjelenõ szöveget. A visszatérési értéket a $vissza változóba
mentjük. A system() közvetlenül a böngészõbe írja a kimenetet.
Ugyanezt az eredményt érhetjük el a ` mûveletjel használatával. A kiadandó
parancsot egyszerûen ilyen fordított irányú aposztrófok közé kell tennünk.
Az így megadott parancsot a rendszer végrehajtja és visszatérési értékként
a parancs kimenetét adja, amit kiírhatunk vagy változóban tárolhatunk.
Íme az elõzõ példa ilyetén megvalósítása:
print "
";
print `man man | col -b`;
print "
";
Vegyük észre, hogy ebben a megvalósításban rögtön kiírtuk a végeredményt.
Biztonsági rések megszüntetése
az escapeshellcmd() függvény használatával
Az escapeshellcmd() függvény ismertetése elõtt tekintsük át, mivel szemben
van szükségünk védelemre. A példa erejéig tegyük fel, hogy meg szeretnénk
engedni a látogatóknak, hogy különbözõ súgóoldalakat tekinthessenek meg.
Ha bekérjük egy oldal nevét, a felhasználó bármit beírhat oda. A 21.4. példában
található programot ne telepítsük, mert biztonsági rést tartalmaz!
Munka kiszolgálói környezetben 401
21
21.4. program A man program meghívása
1:
2:
3: 21.4. program A man program meghívása.<br />4: Ez a kód nem biztonságos
5:
6:
7:

8:
9:

10:

11: 12: if ( isset($manoldal) )
13: system( "man $manoldal | col -b" );
14: ?>
15:

16:
17:
Korábbi példánkat egy szöveges mezõvel és a system() függvénnyel egészí-
tettük ki. Megbízhatónak tûnik, UNIX rendszeren azonban a felhasználó
a manoldal mezõhöz hozzáadhatja saját parancsait, így hozzáférhet a kiszolgálón
számára tiltott részen lévõ adatokhoz is. Erre láthatunk példát a 21.3. ábrán.
402 21. óra
21.3. ábra
A man program
meghívása
A felhasználó az oldalon keresztül kiadta az xxx; ls –al parancsot.
Ezt a $manoldal változóban tároljuk. A program futása közben a system()
függvénynek a következõ parancsot kell végrehajtania:
"man xxx; ls -al | col -b"
Vagyis meg kell jelenítenie az xxx parancshoz tartozó súgóoldalt, ami természetesen
nem létezik. Ebben az esetben a col parancs bemenete a teljes könyvtárlista
lesz. Ezzel a támadó kiírathatja a rendszer bármelyik olvasható könyvtárának tartalm
át. A következõ utasítással például a /etc/passwd tartalmát kapja meg:
xxx; cat /etc/passwd
Bár a jelszavak egy titkosított fájlban, az /etc/shadow-ban vannak tárolva,
amely csak a rendszergazda (root) által olvasható, ez mégis egy igen veszélyes
biztonsági rés. A legegyszerûbben úgy védekezhetünk ellene, hogy soha nem
engedélyezzük, hogy a felhasználók közvetlenül adjanak parancsot a rendszernek.
Ennél kicsit több lehetõséget ad, ha az escapeshellcmd() függvénnyel
fordított perjel (\) karaktert adunk minden metakarakterhez, amit a felhasználó
kiadhat. Az escapeshellcmd() függvény paramétere egy karakterlánc,
végeredménye pedig egy átalakított másolat. A korábbi kód biztonságosabb
változata a 21.5. példában található.
21.5. program A felhasználói bemenet javítása
az escapeshellcmd() függvény használatával
1:
2:
3: 21.5. program A felhasználói bemenet javítása<br />4: az escapeshellcmd() függvény<br />használatával
5:
6:
7:

8: name="manoldal">
9:

10:

11: Munka kiszolgálói környezetben 403
21
21.5. program (folytatás)
12: if ( isset($manoldal) )
13: {
14: $manoldal = escapeshellcmd( $manoldal );
15: system( "man $manoldal | col -b" );
16: }
17: ?>
18:

19:
20:
Ha a felhasználó most kiadja az xxx; cat etc/passwd parancsot, akkor
a system() függvény meghívása elõtt az escapeshellcmd() az xxx\; cat
/etc/passwd paranccsá alakítja azt, azaz a cat utasítás súgóját kapjuk a jelszófájl
helyett.
A rendszer biztonságát az escapeshellcmd() függvény segítségével tovább
növelhetjük. Lehetõség szerint kerüljük azokat a helyzeteket, ahol a felhasználók
közvetlenül a rendszernek adnak ki parancsokat. A programot még biztonságosabb
á tehetjük, ha listát készítünk az elérhetõ súgóoldalakról és még mielõtt megh
ívnánk a system() függvényt, összevetjük a felhasználó által beírtakat ezzel
a listával.
Külsõ programok futtatása a passthru() függvénnyel
A passthru() függvény a system()-hez hasonló, azzal a különbséggel, hogy
a passthru()-ban kiadott parancs kimenete nem kerül átmeneti tárba. Így olyan
parancsok kiadására is lehetõség nyílik, amelyek kimenete nem szöveges, hanem
bináris formátumú. A passthru() függvény paramétere egy rendszerparancs és
egy elhagyható változó. A változóba kerül a kiadott parancs visszatérési értéke.
Lássunk egy példát! Készítsünk programot, amely a képeket kicsinyített mintaként
jeleníti meg és meghívható HTML vagy PHP oldalalakról is. A feladatot külsõ
alkalmazások használatával oldjuk meg, így a program nagyon egyszerû lesz.
A 21.6. példában látható, hogyan küldhetünk a képrõl mintát a böngészõnek.
404 21. óra
21.6. program A passthru() függvény használata képek megjelenítésére
1: 2: if ( isset($kep) && file_exists( $kep ) )
3: {
4: header( "Content-type: image/gif" );
5: passthru( "giftopnm $kep | pnmscale -xscale
.5 -yscale .5 | ppmtogif" );
6: }
7: else
8: print "A(z) $kep nevû kép nem található.";
9: ?>
Vegyük észre, hogy nem használtuk az escapeshellcmd() függvényt, ehelyett
a felhasználó által megadott fájl létezését ellenõriztük a file_exists()
függvénnyel. Ha a $kep változóban tárolt kép nem létezik, nem is próbáljuk
megjeleníteni. A program még biztonságosabbá tehetõ, ha csak bizonyos kiterjeszt
ésû fájlokat engedélyezünk és korlátozzuk az elérhetõ könyvtárakat is.
A passthru() hívásakor három alkalmazást indítunk el. Ha ezt a programot
használni akarjuk, rendszerünkre telepítenünk kell ezen alkalmazásokat és meg
kell adni azok elérési útját. Elõször a giftopnm-nek átadjuk a $kep változó
értékét. Az beolvassa a GIF képet és hordozható formátumúra alakítja. Ezt a kimenetet
rákapcsoljuk a pnmscale bemenetére, amely 50 százalékkal lekicsinyíti
a képet. Ezt a kimenetet a ppmtogif bemenetére kapcsoljuk, amely visszaalakítja
GIF formátumúvá, majd a kapott képet megjelenítjük a böngészõben.
A programot a következõ utasítással bármelyik weboldalról meghívhatjuk:
">
Külsõ CGI program meghívása
a virtual() függvénnyel
Ha egy oldalt HTML-rõl PHP-re alakítunk, azt vesszük észre, hogy a kiszolgálóoldali
beillesztések nem mûködnek. Ha Apache modulként futtatjuk a PHP-t,
a virtual() függvénnyel külsõ CGI programokat hívhatunk meg, például Perl
vagy C nyelven írt számlálókat. Minden felhasznált CGI programnak érvényes
HTTP fejléccel kell kezdenie a kimenetét.
Munka kiszolgálói környezetben 405
21
Írjunk egy Perl CGI programot! Ne aggódjunk, ha nem ismerjük a Perlt. Ez a program
egyszerûen egy HTTP fejlécet ír ki és minden rendelkezésre álló környezeti
változót felsorol:
#!/usr/bin/perl -w
print "Content-type: text/html\n\n";
foreach ( keys %ENV ){
print "$_: $ENV{$_}
\n";
}
Mentsük a programot a cgi-bin könyvtárba proba.pl néven.
Ezután a virtual() függvénnyel a következõképpen hívhatjuk meg:
virtual("/cgi-bin/proba.pl");
?>
Összefoglalás
Ebben a fejezetben áttekintettük, hogyan mûködhetünk együtt a héjjal és rajta
keresztül a külsõ alkalmazásokkal. A PHP sok mindenre használható, de lehet,
hogy külsõ programok használatával az adott probléma elegánsabb megoldá-
sához jutunk.
Megtanultuk, hogyan kapcsolhatunk össze alkalmazásokat a popen() függvény
segítségével. Ez akkor hasznos, amikor a programok a szabványos bemenetrõl
várnak adatokat, de mi egy alkalmazás kimenetébõl szeretnénk továbbítani azokat.
Megnéztük, hogyan használható az exec() és a system() függvény, valamint
a fordított irányú aposztróf mûveletjel a felhasználói utasítások közvetítésére
a rendszermag felé. Láttuk, hogyan védekezzünk a betörésre használható utasítá-
sok ellen az escapeshellcmd() függvény segítségével. Láttuk, hogyan fogadhat
ók bináris adatok rendszerparancsoktól a passthru() függvénnyel és hogy
hogyan valósíthatjuk meg a kiszolgálóoldali beillesztéseket (SSI) a virtual()
függvénnyel.
406 21. óra
Kérdések és válaszok
Sokat emlegettük a biztonságot ebben a fejezetben. Honnan tudhatunk
meg többet a szükséges biztonsági óvintézkedésekrõl?
A legbõvebb számítógépes biztonsággal foglalkozó forrás Lincoln Stein (a híres
Perl modul, a CGI.pm szerzõje) által fenntartott FAQ. Megtalálható
a http://www.w3.org/Security/Faq/ címen. Érdemes a PHP kézikönyv
biztonságról szóló részét is tanulmányozni.
Mikor használjunk külsõ alkalmazásokat saját PHP kódok helyett?
Három szempontot kell megvizsgálnunk: a hordozhatóságot, a fejlesztési sebességet
és a hatékonyságot. Ha saját kódot használunk külsõ programok helyett, programunk
könnyebben átvihetõ lesz a különbözõ rendszerek között. Egyszerû feladatokn
ál, például könyvtár tartalmának kiíratásakor, saját kóddal oldjuk meg a problé-
mát, így csökkenthetjük a futási idõt, mivel nem kell minden futás alkalmával egy
külsõ programot meghívnunk. Másrészrõl nagyobb feladatoknál sokat segíthetnek
a kész külsõ programok, mivel képességeiket nehéz lenne megvalósítani PHP-ben.
Ezekben az esetekben egy külön erre a célra készített külsõ alkalmazás használata
javasolt.
Mûhely
A mûhelyben kvízkérdések találhatók, melyek segítenek megszilárdítani az órában
szerzett tudást. A válaszokat az A függelékben helyeztük el.
Kvíz
1. Melyik függvényt használjuk alkalmazások összekapcsolására?
2. Hogyan olvasunk adatokat egy folyamat kimenetérõl, miután elindítottuk?
3. Hogyan írunk adatokat egy folyamat bemenetére, miután elindítottuk?
4. Az exec() függvény közvetlenül a böngészõbe írja a végeredményét?
5. Mit csinál a system() függvény a végrehajtott külsõ parancs kimenetével?
6. Mi a fordított irányú aposztróf visszatérési értéke?
7. Hogyan adhat ki biztonságosan a felhasználó parancsokat a rendszerhéjnak?
8. Hogyan hajthatunk végre külsõ CGI programot a PHP programokból?
Munka kiszolgálói környezetben 407
21
Feladatok
1. Írjunk programot, amely a UNIX ps parancsának segítségével megjeleníti
a böngészõben a futó folyamatokat! (Megjegyezzük, hogy nem biztonságos,
ha a felhasználók futtathatják a programot!).
2. Nézzük meg a ps parancs súgójában a lehetséges parancssori kapcsolókat!
Módosítsuk az elõzõ programot, hogy a felhasználó a kapcsolók egy részét
használhassa. Ne küldjünk ellenõrizetlen utasításokat a rendszermagnak!