Sunteți pe pagina 1din 14

11.

Protocoale de nivel aplicatie: HTTP


HTTP este protocolul retelei de WorldWide Web WWW care a asigurat succesul acesteia, precum si a Internet-ului in general. Structura lui este extrem de simpla, bazata pe o sintaxa asemanatoare limbii engleze, ceea ce face scrierea de aplicatii ca servere, browsere, roboti extrem de simpla. O atentie deosebita trebuie insa acordata realizarii unor astfel de programe de retea. Un program destinat sa ruleze local va consuma resurse locale, deci daca este scris prost, va inghiti doar resursele masinii pe care ruleaza, sau o va chiar bloca. Un program de retea prost scris insa va bloca resursele multor alti utilizatori, si poate nu acesta e scopul pentru care l-am scris. Un exemplu ar fi robotii, programe care executa cautari si indexari automate de pagini web, programe usor de scris de catre programatori nu prea priceputi si apoi lasate de capul lor... 11.1. HTTP = HyperText Transfer Protocol Este un protocol destinat livrarii unor resurse unui client - browser, de catre un server. Bineinteles ca lucreaza prin intermediul stivei TCP/IP, portul standard pe care asculta serverul fiind 80. Aceste resurse pot fi fisiere de orice tip, sau iesirea generata de alte programe. 11.2. Structura tranzactiilor HTTP Protocolul HTTP are particularitatea de a nu memora o succesiune a starilor prin care trece legatura client-server. Astfel fiecare tranzactie este independenta: clientul trimite o cerere, serverul raspunde cu resursa ceruta, punct. Alta resursa, alta tranzactie. Deoarece in unele situatii acest model nu a fost satisfacator (de exemplu in cazul prezentarii unei succesiuni de ecrane cand doream sa stiu oricand unde am ajuns) s-a inventat mecanismul stocarii de cookies pentru identificare, dar aceasta solutie este rezolvata inafara protocolului. Mesajul HTTP va avea intotdeauna formatul acesta: <linie initiala, diferita pentru cerere sau raspuns> Header1: valoare1 Header2: valoare2

81

<corpul mesajului, continand orice in orice format> Linia initiala poate fi de exemplu: GET /cale/pana/la/fisier.html HTTP/1.0 Deci contine trei parti: metoda (cu majuscule), identificatorul resursei si versionea protocolului (tot cu majuscule). Toate aceste linii, cea initiala si headerele (zero sau mai multe) se termina cu perechea CR LF deci caracterele ASCII cu codul 0dh si 0ah dupa care urmeaza o linie goala si eventual corpul mesajului. O linie initiala de raspuns va avea formatul diferit: HTTP/1.0 404 Not Found De asemenea continand trei parti: versiunea protocolului, codul raspunsului la cererea primita si motivul IN ENGLEZA. Acest motiv este destinat intelegerii utilizatorului, de aceea textul poate diferi. Codul raspunsului este destinat browserului, si in general avem grupurile de raspunsuri: - 1xx mesaj general de informare - 2xx succes - 3xx redirecteaza clientul catre alta resursa - 4xx eroare din parte clientului - 5xx eroare a serverului iar cateva exemple de raspunsuri ar fi: 200 400 404 405 301 302 303 500 OK cererea a fost tratata cu succes, resursa vine in corpul mesajului Bad Request eroare de sintaxa in cererea exprimata Not Found resursa ceruta nu exista Not Implemented cererea nu a fost inteleasa de server Moved Permanently Moved Temporarily See Other clientul se muta automat la noua adresa pentru a cere resursa Server Error ceva s-a stricat pe server...

Liniile de header contin diferite informatii utile, astfel browserul trimite de exemplu: From: birou@biciclete.com User-Agent: Mozilla/3.0Gold
82

Aici s-a trimis identificatorul utilizatorului (configurabil de catre utilizator evident) si numele programului care a generat cererea aici Netscape 3.0 Gold. Un server va trimite ca si header: Server: Apache/1.2b3-dev Last-Modified: Tue, 9 Nov 1999 10:11:41 GMT In care precizeaza identitatea sa precum si data resursei. Daca serverul si raspunde cu o resursa in corpul mesajului, de obicei mai adauga linii de header in care descrie aceasta resursa, ca si tip si dimeniune: Content-Type: text/html Content-Length: 4028 Denumirile tipurilor resurselor (tipuri MIME) sunt standardizate. 11.3. Exemplu de sesiune HTTP Iata cum ar decurge conversatia client-server pentru aducerea fisierului /nume/de/cale/catre/index.html de pe serverul www.undevreau.com. Clientul va deschide o conexiune (un socket) cu adresa precizata, pe portul 80 daca nu am precizat altul, lucrez pe cel implicit. Dupa aceea va trimite serverului cererea: GET /nume/de/cale/catre/index.html HTTP/1.0 [crlf] From: badeaion@stana.ro [crlf] User-Agent: BrowserBun/1.0beta [crlf] [crlf] iar serverul va raspunde pe acelasi socket cu ceva in genul mesajului urmator, dupa care va inchide socket-ul. Am renuntat la scrierea explicita a perechii CR+LF, dar este evident ca fiecare linie se termina astfel, in plus sa nu uitam prezenta liniei goale dupa header. HTTP/1.0 200 OK Date: Tue, 9 Nov 1999 13:50:22 GMT Content-Type: text/html Content-Length: 1055
83

<html> <body> <h1>No ase!</h1> (alte linii) . . . </body> </html> 11.4. Alte cereri HTTP Mai intalnite sunt doar trei tipuri de cereri emise de client: GET (pe care tocmai am vazut-o), HEAD si POST. HEAD este ca sintaxa intru totul similara metodei GET, dar serverul nu reutrneaza resursa ceruta. Aceasta metoda este folosita de client pentru a testa existenta resursei respective si pentru a afla informatii despre ea (tip, dimensiune). Deci serverul returneaza doar liniile de header ce descriu resursa referita. POST este folosita de client pentru a trimite informatii serverului, de obicei pentru a fi procesate de catre un program ruland pe server. In acest caz resursa referita va fi numele programului caruia ii sunt destinate informatiile. Astfel: POST /cale/catre/program.cgi HTTP/1.0 From: acasa@lamine.com User-Agent: ClientHTTP/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 40 nume=Vasile&culoare+favorita=lilandungi (de fapt si metoda GET poate fi folosita pentru a trimite informatii... dar asta nu o vom discuta acum)

84

11.5. HTTP 1.1 pe scurt Nu vom intra in detalii despre aceasta versiune a protocolului HTTP, doar vom puncta cateva din noutatile aduse de el: - cererea clientului TREBUIE sa contina linia de header Host: www.careserver.com Deoarece acelasi server poate gazdui mai multe domenii (www.server1.com, www.server2.com etc) trebuie sa identific cumva daca fisierul cerut se afla in directoarele apartinand primului domeniu sau celui de-al doilea. Dar solutia la aceasta problema este de fapt folosirea cererilor cu referinta absoluta, de pilda: GET http://www.undeva.net/cale/la/nume.htm HTTP/1.2 - clientul sa suporte conexiuni persistente. Da, HTTP 1.1 renunta la ideea unei singure conexiuni/cerere, caci deschiderea/inchiderea conexiunilor TCP/IP consuma mult timp si resurse CPU. Deci pe aceeasi conexiune clientul trimite mai multe cereri, iar serverul raspunde cererilor in ordinea lor. Daca clientul nu doreste aceasta, trimite linia de header Connection: Close - serverul poate transmite datele in sectiuni (chunks) pe masura ce sunt obtinute, astfel clientul nemaitrebuind sa astepte dupa intreaga cantitate odata. Raspunsul serverului va indica aceasta prin linia: Transfer-Encoding: chunked - in cazul comunicatiei lente, sau daca serverul are de asteptat mult pana va fi capabil sa trimita resursa, el trimite un raspuns de tipul 100 Continue clientului spunandu-i ca e in regula, dar sa mai stea putin: HTTP/1.0 100 Continue Acest raspuns de fapt va fi generat imediat dupa primirea primei linii de cerere. - cererile clientului pot sa fie conditionale, folosind linii de header de tipul: If-Modified-Since: sau If-Unmodified-Since: 11.6. Un server HTTP 1.0 simplu Iata un exemplu simplu de server HTTP care raspunde numai la cererile de tip GET si HEAD. De altfel doar aceste cereri sunt obligatorii de implementat conform standardului.
85

Functia main() a programului este mentinuta simpla pentru a ilustra cat mai clar functionarea programului. Astfel programul incepe prin obtinerea identificatorului utilizatorului care la lansat. Aceasta informatie va fi folosita ulterior pentru restrictionarea accesului la resurse prin cererile adresate serverului. Regula instituita de program este ca sa fie tratate numai cererile referitoare la fisiere apartinand utilizatorului care a lansat serverul. Aceasta identitate poate fi modificata intentionat de catre cel care lanseaza programul, specificand un alt utilizator prin linia de comanda. Aceasta linie de comanda este analizata pentru a trata optiunile urmatoare: - u specificarea unui alt user ID decat cel implicit (cel care a lansat programul) - l alegerea unui fisier de urmarire a activitatii serverului (log). Daca nu este ales nici un fisier, aceasta trasare nu se va face. - d directorul din care se vor servi fisierele cerute de clienti (home directory). Implicit acesta va fi directorul propriu al utilizatorului care lanseaza serverul. In caz ca serverul nu reuseste sa acceseze acest director, va iesi semnaland aceasta (serioasa) eroare. - p portul pe care va asculta serverul cererile de la clienti. Bineinteles, implicit este portul 80 standard pentru protocolul HTTP. Pentru programul nostru de testare se recomanda folosirea unui alt port, de exemplu de le 8000 in sus pentru a nu crea coliziuni cu serverul deja existent si cu serverele lansate de ceilalti. Optiunile sunt prefixate cu caracterul - astfel ca un exemplu de linie de comanda va arata astfel: host# serverhttp p 8080 u 120 l http.log d ./http & (prezenta caracterului & ma asigura ca programul va rula in background lasand astfel posibilitatea rularii altor comenzi de la consola) Iesirea din acest server se poate face doar trimitand programului semnalele de terminare a procesului, prin comanda kill de exemplu (evident, daca nu am lansat serverul in background, ii putem trimite Ctrl-C de la tastatura). Programul le va recunoaste si trata pe toate cu aceeasi functie closSock inregistrata ca atare. Evident, la semnalul de oprire imediata SIGKILL programul nu mai poate face nimic. La semnalele pe care le tratam, oricum aveam actiunea implicita de

86

terminare a programului, dar prin inregistrarea functiei noastre asiguram elegant terminarea comunicatiei. In sfarsit, se initializeaza conexiunea serverului si se intra in bucla de asteptare a cererilor de conexiune. In momentul sosirii unei cereri, ea este tratata si se asteapta urmatoarea. Iesirea din program, repetam, se face prin trimiterea explicita a unui semnal de terminare. initConexServer() Prima functie implementata este cea de initializare a conexiunii serverului. In primul rand se creeaza un socket de tip stream, stiut fiind ca HTTP lucreaza folosind protocolul TCP pentru transport. Acestui socket i se ataseaza portul pe care va astepta conexiunile si i se specifica numarul de conexiuni pe care le va stoca in vederea tratarii secventiale. asteptConex() este o functie simpla ce ascunde asteptarea unei conexiuni pe socket-ul deschis. Aici trebuie subliniat ca accept() returneaza un nou socket prin care se vor transmite datele, cel original fiind astfel liber sa primeasca noi conexiuni chiar daca programul nostru asteapta terminarea conexiunii curente. Alte functii folosite in program sunt outError()-cu rolul de a trimite catre client mesajele corespunzatoare de eroare - in cazul in care cererile sunt recunoscute ca HTTP, mesajul este formatat corespunzator, logHttp() inregistreaza activitatea serverului, scrieData() formateaza data si ora curenta intr-un sir si o trimite clientului, closSock() inchide socket-urile si termina programul. tratezCerere() este functia de cel mai mare interes pentru implementarea serverului. In primul rand se citeste cererea, care trebuie sa fie conforma specificatiilor protocolului HTTP. Toate situatiile de eroare sunt inregistrate, un raspuns semnaland eroarea este trimis clientului si cererea este abandonata. Daca am primit o comanda necunoscuta (serverul trateaza doar GET si HEAD) de asemenea clientul primeste aceasta informatie si cererea este abandonata. Dupa alte verificari, se trimite clientului raspunsul standard HTTP si, daca cererea a fost GET, si continutul fisierului cerut.

87

#include #include #include #include #include #include #include #include #include #include #include #include

<stdio.h> <unistd.h> <stdlib.h> <sys/stat.h> <sys/time.h> <sys/socket.h> <netinet/in.h> <arpa/inet.h> <netdb.h> <fcntl.h> <string.h> <signal.h>

/* definitii variabile globale */ int http_sock, con_sock; int http_port = 80; struct sockaddr_in source; char *log_httpfile = NULL; char *homedir; uid_t uid; char rep_head[] = "HTTP/1.0 200 OK\r\nServer: serverhttp\r\n"; char *rep_err_nget[2] = {"<HTML><HEAD><TITLE>Error </TITLE></HEAD><BODY><H1>Error 405</H1> This server answers only to GET and HEAD requests\n</BODY></HTML>\r\n", "HTTP/1.0 405 Method Not allowed\r\nAllow: GET,HEAD\r\nServer: serverhttp\r\n"}; char *rep_err_acc[2] = {"<HTML><HEAD><TITLE>Error</TITLE> </HEAD><BODY><H1>Error 404</H1> Not found - file doesn't exist or is read protected\n</BODY></HTML>\r\n", "HTTP/1.0 404 Not found\r\nServer: serverhttp\r\n"}; folosinta(void) { fprintf(stderr, "folosinta: serverhttp [-l log_httpfile][-p port][-d homedir][-u uid]\n"); exit(1); } logHttp(char *action, char *item) { int fd; char logline[1024]; time_t tl;

88

if (log_httpfile == NULL) return; tl = time(NULL); strcpy(logline, ctime(&tl)); logline[strlen(logline) - 1] = ' '; strcat(logline, inet_ntoa(source.sin_addr)); strcat(logline, " "); strcat(logline, action); strcat(logline, ":"); strcat(logline, item); strcat(logline, "\n"); fd = open(log_httpfile, O_WRONLY|O_APPEND|O_CREAT,0600); if (fd < 0) { perror("open log file"); exit(1); } write(fd, logline, strlen(logline)); close(fd); } scrieData(void) { time_t char

tl; buff[50];

tl = time(NULL); strftime(buff,50, "Date: %a, %d %h %Y %H:%M:%S %Z\r\n", gmtime(&tl)); write(con_sock, buff, strlen(buff)); } void closSock(int s) { close(con_sock); close(http_sock); logHttp("END on SIGNAL", ""); exit(0);

initConexServer(void) { struct sockaddr_in server; /* se creeaza socket */ http_sock = socket(AF_INET, SOCK_STREAM, 0);

89

if (http_sock < 0) { perror("socket server!"); exit(1); } server.sin_family = AF_INET; server.sin_port = htons(http_port); server.sin_addr.s_addr = INADDR_ANY; if (bind(http_sock, (struct sockaddr *) &server, sizeof(server)) <0){ perror("bind socket server"); exit(1); } if (listen(http_sock, 5) < 0) { perror("listen"); exit(1); } } asteptConex(void) { int lg; lg = sizeof(struct sockaddr_in); con_sock = accept(http_sock, (struct sockaddr *) & source, &lg); if (con_sock <= 0) { perror("accept"); exit(1); }

outError(char **rep, int http1) { if (http1) { write(con_sock, rep[1], strlen(rep[1])); scrieData(); write(con_sock, "\r\n", 2); } write(con_sock, rep[0], strlen(rep[0])); } tratezCerere(void) { char int char struct stat

buff[8192]; fd, lg, cmd, http1; *filename, *c; statres;

90

char

req[1024];

lg = read(con_sock, req, 1024); if (req[lg - 1] != '\n' || req[lg - 2] != '\r') { outError(rep_err_nget, 0); logHttp("error 405", req); goto error; } req[lg - 2] = '\0'; c = strtok(req, " "); if (c == NULL) { outError(rep_err_nget, 0); logHttp("error 405", req); goto error; } cmd = 0; if (strncmp(c, "GET", 3) == 0) cmd = 1; if (strncmp(c, "HEAD", 4) == 0) cmd = 2; filename = strtok(NULL, " "); /* verific daca e o cerere http valida */ http1 = 0; c = strtok(NULL, " "); if (c != NULL && strncmp(c, "HTTP", 4) == 0) http1 = 1; if (cmd == 0) { outError(rep_err_nget, http1); logHttp("error 405", req); goto error; } if (filename == NULL) { outError(rep_err_acc, http1); logHttp("error 404", filename); goto error; } /* suprima / din frunte */ while (filename[0] == '/') filename++;

91

if (filename == NULL) { outError(rep_err_acc, http1); logHttp("error 404", filename); goto error; } /* interzice .. in cale */ c = filename; while (*c != '\0') if (c[0] == '.' && c[1] == '.') { outError(rep_err_acc, http1); logHttp("error 404", filename); goto error; } else c++; fd = open(filename, O_RDONLY); if (fd < 0) { outError(rep_err_acc, http1); logHttp("error 404", filename); goto error; } if (fstat(fd, &statres) < 0) { outError(rep_err_acc, http1); logHttp("error 404", filename); goto error; } if (statres.st_uid != uid || !S_ISREG(statres.st_mode) || !(statres.st_mode & S_IROTH)) { outError(rep_err_acc, http1); logHttp("error 404", filename); goto error; } if (http1) { char buff[50]; time_t tl; write(con_sock, rep_head, strlen(rep_head)); sprintf(buff, "Contents-lenght: %d\r\n", statres.st_size); write(con_sock, buff, strlen(buff)); scrieData(); strftime(buff, 50, "Last-Modified: %a, %d %h %Y %H:%M:%S%Z\r\n\r\n",

92

gmtime(&statres.st_mtime)); write(con_sock, buff, strlen(buff)); } if (cmd == 1) { while (lg = read(fd, buff, 8192)) write(con_sock, buff, lg); logHttp("GET", filename); } else logHttp("HEAD", filename); error: close(fd); close(con_sock);

} main(int argc, char **argv) { int lg; lg = sizeof(struct sockaddr_in); uid = getuid(); argc--; argv++; while (argc > 0) { if (argv[0][0] != '-') folosinta(); switch (argv[0][1]) { case 'l': log_httpfile = argv[1]; argv += 2; argc -= 2; break; case 'p': http_port = atoi(argv[1]); argv += 2; argc -= 2; break; case 'u': uid = atoi(argv[1]); argv += 2; argc -= 2; break; case 'd': homedir = argv[1]; argv += 2;

93

argc -= 2; break; default: } } folosinta();

if (homedir == NULL) homedir = getenv("HOME"); if (chdir(homedir) < 0) { perror("chdir in homedir ratat!"); exit(1); } signal(SIGTERM, closSock); signal(SIGQUIT, closSock); signal(SIGINT, closSock); /*daca e apelat de inetd trateaza doar cererea curenta */ con_sock = 0; if (getpeername(con_sock, (struct sockaddr *) & source, &lg) == 0) { tratezCerere(); return (0); } initConexServer(); do { asteptConex(); tratezCerere(); } while (1); }

11.7. Intrebari si teme Studiati comunicatia cu serverul HTTP local cu ajutorul aplicatiei telnet. Modificati serverul astfel incat sa includa caracteristicile HTTP 1.1. Scrieti un client (browser) simplu dupa modelul serverului, implementand HTTP 1.0, apoi HTTP 1.1.

94