1. 程式人生 > >C語言開發Linux下web伺服器(支援GET/POST,SSL,目錄顯示等)

C語言開發Linux下web伺服器(支援GET/POST,SSL,目錄顯示等)

這個主要是在CSAPP基礎上做的,添加了POST,SSL,目錄顯示等功能。

一、實現功能:
1.支援GET/POST方法
2.支援SSL安全連線即HTTPS
3.支援CGI
4.基於IP地址和掩碼的認證
5.目錄顯示
6.日誌功能

7.錯誤提示頁面

二、設計原理

首先介紹一些HTTP協議基本知識。
#1.GET/POST
本實現支援GET/POST方法,都是HTTP協議需要支援的標準方法。
GET方法主要是通過URL傳送請求和傳送資料,而POST方法在請求頭空一格之後傳送資料,所以POST方法比GET方法安全性高,因為GET方法可以直接看到傳送的資料。另外一個區別就是GET方法傳輸的資料較小,而POST方法很大。所以一般表單,登陸頁面等都是通過POST方法。

#2.MIME型別
   當伺服器獲取客戶端的請求的檔名,將分析檔案的MIME型別,然後告訴瀏覽器改檔案的MIME型別,瀏覽器通過MIME型別解析傳送過來的資料。具體來說,瀏覽器請求一個主頁面,該頁面是一個HTML檔案,那麼伺服器將”text/html”型別發給瀏覽器,瀏覽器通過HTML解析器識別傳送過來的內容並顯示。

下面將描述一個具體情景。
   客戶端使用瀏覽器通過URL傳送請求,伺服器獲取請求。
如瀏覽器URL為:127.0.0.1/postAuth.html,
那麼伺服器獲取到的請求為:GET  /postAuth.html  HTTP/1.1
意思是需要根目錄下postAuth.html檔案的內容,通過GET方法,使用HTTP/1.1協議(1.1是HTTP的版本號)。這是伺服器將分析檔名,得知postAuth.html是一個HTML檔案,所以將”text/html”傳送給瀏覽器,然後讀取postAuth.html內容發給瀏覽器。

實現簡單的MIME型別識別程式碼如下:
主要就是通過檔案字尾獲取檔案型別。

static void get_filetype(const char *filename, char *filetype) 
{
    if (strstr(filename, ".html"))
		strcpy(filetype, "text/html");
    else if (strstr(filename, ".gif"))
		strcpy(filetype, "image/gif");
    else if (strstr(filename, ".jpg"))
		strcpy(filetype, "image/jpeg");
    else if (strstr(filename, ".png"))
		strcpy(filetype, "image/png");
    else
	strcpy(filetype, "text/plain");
}  

如果支援HTTPS的話,那麼我們就#define HTTPS,這主要通過gcc 的D選項實現的,具體細節可參考man手冊。

靜態內容顯示實現如下:
static void serve_static(int fd, char *filename, int filesize) 
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];
 
    /* Send response headers to client */
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
    	SSL_write(ssl, buf, strlen(buf));
	SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
	Rio_writen(fd, buf, strlen(buf));
	Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

#3.CGI規範
   如果只能顯示頁面那麼無疑缺少動態互動能力,於是CGI產生了。CGI是公共閘道器介面(Common Gateway Interface),是在CGI程式和Web伺服器之間傳遞資訊的規則。CGI允許Web伺服器執行外部程式,並將它們的輸出傳送給瀏覽器。這樣就提供了動態互動能力。 

那麼伺服器是如何分開處理靜態頁面和動態CGI程式的呢?這主要是通過解析URL的方式。我們可以定義CGI程式的目錄,如cgi-bin,那麼如果URL包含”cgi-bin”字串則這是動態程式,且將URL的引數給cgiargs。如果是靜態頁面,parse_uri返回1,反正返回0。所以我們可以通過返回值區別不同的服務型別。
具體解析URL方式如下:
static int parse_uri(char *uri, char *filename, char *cgiargs) 
{
    char *ptr;
    char tmpcwd[MAXLINE];
    strcpy(tmpcwd,cwd);
    strcat(tmpcwd,"/");

    if (!strstr(uri, "cgi-bin")) 
    {  /* Static content */
	strcpy(cgiargs, "");
	strcpy(filename, strcat(tmpcwd,Getconfig("root")));
	strcat(filename, uri);
	if (uri[strlen(uri)-1] == '/')
	    strcat(filename, "home.html");
	return 1;
    }
    else 
    {  /* Dynamic content */
	ptr = index(uri, '?');
	if (ptr) 
	{
	    strcpy(cgiargs, ptr+1);
	    *ptr = '\0';
	}
	else 
	    strcpy(cgiargs, "");
	strcpy(filename, cwd);
	strcat(filename, uri);
	return 0;
    }
}

GET方式的CGI規範實現原理:
   伺服器通過URL獲取傳給CGI程式的引數,設定環境變數QUERY_STRING,並將標準輸出重定向到檔案描述符,然後通過EXEC函式簇執行外部CGI程式。外部CGI程式獲取QUERY_STRING並處理,處理完後輸出結果。由於此時標準輸出已重定向到檔案描述符,即傳送給了瀏覽器。
實現細節如下:由於涉及到HTTPS,所以稍微有點複雜。
void get_dynamic(int fd, char *filename, char *cgiargs) 
{
    char buf[MAXLINE], *emptylist[] = { NULL },httpsbuf[MAXLINE];
    int p[2];

    /* Return first part of HTTP response */
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Web Server\r\n",buf);
    #ifdef HTTPS 
    if(ishttps)
    	SSL_write(ssl,buf,strlen(buf));
    else
    #endif
       	Rio_writen(fd, buf, strlen(buf));
	
    #ifdef HTTPS 
    if(ishttps)
    {
    	Pipe(p);
       	if (Fork() == 0)
	{  /* child  */ 
		Close(p[0]);
		setenv("QUERY_STRING", cgiargs, 1); 
		Dup2(p[1], STDOUT_FILENO);         /* Redirect stdout to p[1] */
		Execve(filename, emptylist, environ); /* Run CGI program */	
	}
	Close(p[1]);
	Read(p[0],httpsbuf,MAXLINE);   /* parent read from p[0] */
	SSL_write(ssl,httpsbuf,strlen(httpsbuf));
    }
    else
    #endif
    {
	if (Fork() == 0) 
	{ /* child */
		/* Real server would set all CGI vars here */
		setenv("QUERY_STRING", cgiargs, 1); 
		Dup2(fd, STDOUT_FILENO);         /* Redirect stdout to client */
		Execve(filename, emptylist, environ); /* Run CGI program */
	}
}
}

POST方式的CGI規範實現原理:
   由於POST方式不是通過URL傳遞引數,所以實現方式與GET方式不一樣。
POST方式獲取瀏覽器傳送過來的引數長度設定為環境變數CONTENT-LENGTH。並將引數重定向到CGI的標準輸入,這主要通過pipe管道實現的。CGI程式從標準輸入讀取CONTENT-LENGTH個字元就獲取了瀏覽器傳送的引數,並將處理結果輸出到標準輸出,同理標準輸出已重定向到檔案描述符,所以瀏覽器就能收到處理的響應。
具體實現細節如下:

static void post_dynamic(int fd, char *filename, int contentLength,rio_t *rp)
{
    char buf[MAXLINE],length[32], *emptylist[] = { NULL },data[MAXLINE];
    int p[2];


    #ifdef HTTPS 
    int httpsp[2];
    #endif


    sprintf(length,"%d",contentLength);
    memset(data,0,MAXLINE);


    Pipe(p);


    /*       The post data is sended by client,we need to redirct the data to cgi stdin.
    *  	 so, child read contentLength bytes data from fp,and write to p[1];
    *    parent should redirct p[0] to stdin. As a result, the cgi script can
    *    read the post data from the stdin. 
    */


    /* https already read all data ,include post data  by SSL_read() */
   
    	if (Fork() == 0)
	{                     /* child  */ 
		Close(p[0]);
		#ifdef HTTPS 
		if(ishttps)
		{
			Write(p[1],httpspostdata,contentLength);	
		}
		else
		#endif
		{
			Rio_readnb(rp,data,contentLength);
			Rio_writen(p[1],data,contentLength);
		}
		exit(0)	;
	}
    
    /* Send response headers to client */
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n",buf);


    #ifdef HTTPS 
    if(ishttps)
    	SSL_write(ssl,buf,strlen(buf));
    else
    #endif
        Rio_writen(fd, buf, strlen(buf));


    Dup2(p[0],STDIN_FILENO);  /* Redirct p[0] to stdin */
    Close(p[0]);
    Close(p[1]);
    setenv("CONTENT-LENGTH",length , 1); 


    #ifdef HTTPS 
    if(ishttps)  /* if ishttps,we couldnot redirct stdout to client,we must use SSL_write */
    {
    	Pipe(httpsp);
	   if(Fork()==0)
	  {
    	Dup2(httpsp[1],STDOUT_FILENO);        /* Redirct stdout to https[1] */ 
		Execve(filename, emptylist, environ); 
	}
	Read(httpsp[0],data,MAXLINE);
	SSL_write(ssl,data,strlen(data));
    }
    else
    #endif
    {
    	Dup2(fd,STDOUT_FILENO);        /* Redirct stdout to client */ 
	    Execve(filename, emptylist, environ); 
    }
}

目錄顯示功能原理:
   主要是通過URL獲取所需目錄,然後獲取該目錄下所有檔案,併發送相應資訊,包括檔案格式對應圖片,檔名,檔案大小,最後修改時間等。由於我們傳送的檔名是通過超連結的形式,所以我們可以點選檔名繼續瀏覽資訊。
具體實現細節如下:
static void serve_dir(int fd,char *filename)
{
	DIR *dp;
	struct dirent *dirp;
    	struct stat sbuf;
	struct passwd *filepasswd;
	int num=1;
	char files[MAXLINE],buf[MAXLINE],name[MAXLINE],img[MAXLINE],modifyTime[MAXLINE],dir[MAXLINE];
	char *p;

	/*
	* Start get the dir   
	* for example: /home/yihaibo/kerner/web/doc/dir -> dir[]="dir/";
	*/
	p=strrchr(filename,'/');
	++p;
	strcpy(dir,p);
	strcat(dir,"/");
	/* End get the dir */

	if((dp=opendir(filename))==NULL)
		syslog(LOG_ERR,"cannot open dir:%s",filename);

    	sprintf(files, "<html><title>Dir Browser</title>");
	sprintf(files,"%s<style type=""text/css""> a:link{text-decoration:none;} </style>",files);
	sprintf(files, "%s<body bgcolor=""ffffff"" font-family=Arial color=#fff font-size=14px>\r\n", files);

	while((dirp=readdir(dp))!=NULL)
	{
		if(strcmp(dirp->d_name,".")==0||strcmp(dirp->d_name,"..")==0)
			continue;
		sprintf(name,"%s/%s",filename,dirp->d_name);
		Stat(name,&sbuf);
		filepasswd=getpwuid(sbuf.st_uid);

		if(S_ISDIR(sbuf.st_mode))
		{
			sprintf(img,"<img src=""dir.png"" width=""24px"" height=""24px"">");
		}
		else if(S_ISFIFO(sbuf.st_mode))
		{
			sprintf(img,"<img src=""fifo.png"" width=""24px"" height=""24px"">");
		}
		else if(S_ISLNK(sbuf.st_mode))
		{
			sprintf(img,"<img src=""link.png"" width=""24px"" height=""24px"">");
		}
		else if(S_ISSOCK(sbuf.st_mode))
		{
			sprintf(img,"<img src=""sock.png"" width=""24px"" height=""24px"">");
		}
		else
			sprintf(img,"<img src=""file.png"" width=""24px"" height=""24px"">");


	sprintf(files,"%s<p><pre>%-2d%s""<a href=%s%s"">%-15s</a>%-10s%10d %24s</pre></p>\r\n",files,num++,img,dir,dirp->d_name,dirp->d_name,filepasswd->pw_name,(int)sbuf.st_size,timeModify(sbuf.st_mtime,modifyTime));
	}
	closedir(dp);
	sprintf(files,"%s</body></html>",files);

	/* Send response headers to client */
	sprintf(buf, "HTTP/1.0 200 OK\r\n");
	sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
	sprintf(buf, "%sContent-length: %d\r\n", buf, strlen(files));
	sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, "text/html");

	#ifdef HTTPS
	if(ishttps)
	{
		SSL_write(ssl,buf,strlen(buf));
		SSL_write(ssl,files,strlen(files));
	}
	else
	#endif
	{
		Rio_writen(fd, buf, strlen(buf));
		Rio_writen(fd, files, strlen(files));
	}
	exit(0);

}

HTTPS的實現:
   HTTPS主要基於openssl的開源庫實現。如果沒有安裝,那麼我們就不#define HTTPS。
HTTPS的功能主要就是提供安全的連線,伺服器和瀏覽器之間傳送的資料是通過加密的,加密方式可以自己選定。
   開始連線時,伺服器需要傳送CA,由於我們的CA是自己簽發的,所以需要我們自己新增為可信。


訪問控制功能:
主要是通過獲取客戶端IP地址,並轉換為整數,與上配置檔案中定義的掩碼,如果符合配置檔案中允許的網段,那麼可以訪問,否則不可以。
具體實現如下。
static long long ipadd_to_longlong(const char *ip)
{
	const char *p=ip;
	int ge,shi,bai,qian;
	qian=atoi(p);

	p=strchr(p,'.')+1;
	bai=atoi(p);

	p=strchr(p,'.')+1;
	shi=atoi(p);

	p=strchr(p,'.')+1;
	ge=atoi(p);

	return (qian<<24)+(bai<<16)+(shi<<8)+ge;
}


int access_ornot(const char *destip) // 0 -> not 1 -> ok
{
	//192.168.1/255.255.255.0
	char ipinfo[16],maskinfo[16];
	char *p,*ip=ipinfo,*mask=maskinfo;
	char count=0;
	char *maskget=Getconfig("mask");
	const char *destipconst,*ipinfoconst,*maskinfoconst;
	if(maskget=="")
	{
		printf("ok:%s\n",maskget);
		return 1;
	}	
	p=maskget;
/* get ipinfo[] start */
	while(*p!='/')
	{
		if(*p=='.')
			++count;
		*ip++=*p++;
	}
	while(count<3)
	{
		*ip++='.';
		*ip++='0';
		++count;
	}
	*ip='\0';
/* get ipinfo[] end */
/* get maskinfo[] start */
	++p;
	while(*p!='\0')
	{
		if(*p=='.')
			++count;
		*mask++=*p++;
	}
	while(count<3)
	{
		*mask++='.';
		*mask++='0';
		++count;
	}
	*mask='\0';

/* get maskinfo[] end */
	destipconst=destip;
	ipinfoconst=ipinfo;
	maskinfoconst=maskinfo;
	return ipadd_to_longlong(ipinfoconst)==(ipadd_to_longlong(maskinfoconst)&ipadd_to_longlong(destipconst));
}

配置檔案的讀取:
主要選項資訊都定義與配置檔案中。
格式舉例如下;
#HTTP PORT
PORT = 8888
所以讀取配置檔案函式具體如下:
static char* getconfig(char* name)
{
/*
pointer meaning:

...port...=...8000...
   |  |   |   |  |
  *fs |   |   |  *be    f->forward  b-> back
      *fe |   *bs       s->start    e-> end
          *equal
*/
	static char info[64];
	int find=0;
	char tmp[256],fore[64],back[64],tmpcwd[MAXLINE];
	char *fs,*fe,*equal,*bs,*be,*start;

	strcpy(tmpcwd,cwd);
	strcat(tmpcwd,"/");
	FILE *fp=getfp(strcat(tmpcwd,"config.ini"));
	while(fgets(tmp,255,fp)!=NULL)
	{
		start=tmp;
		equal=strchr(tmp,'=');

		while(isblank(*start))
			++start;
		fs=start;

		if(*fs=='#')
			continue;
		while(isalpha(*start))
			++start;
		fe=start-1;

		strncpy(fore,fs,fe-fs+1);
		fore[fe-fs+1]='\0';
		if(strcmp(fore,name)!=0)
			continue;
		find=1;

		start=equal+1;
		while(isblank(*start))
			++start;
		bs=start;

		while(!isblank(*start)&&*start!='\n')
			++start;
		be=start-1;

		strncpy(back,bs,be-bs+1);
		back[be-bs+1]='\0';
		strcpy(info,back);
		break;
	}
	if(find)
		return info;
	else
		return NULL;
}
二、測試
本次測試使用了兩臺機器。一臺Ubuntu的瀏覽器作為客戶端,一臺Redhat作為伺服器端,其中Redhat是Ubuntu上基於VirtualBox的一臺虛擬機器。

IP地址資訊如下:

Ubuntu的vboxnet0:



RedHateth0:


RedHat主機編譯專案:


由於我們同事監聽了8000和4444,所以有兩個程序啟動。

HTTP的首頁:


目錄顯示功能:


HTTP GET頁面:


HTTPGET響應:


從HTTP GET響應中我們觀察URL,引數的確是通過URL傳送過去的。

其中getAuth.c如下:

#include "wrap.h"
#include "parse.h"

int main(void) {
    char *buf, *p;
    char name[MAXLINE], passwd[MAXLINE],content[MAXLINE];

    /* Extract the two arguments */
    if ((buf = getenv("QUERY_STRING")) != NULL) {
	p = strchr(buf, '&');
	*p = '\0';
	strcpy(name, buf);
	strcpy(passwd, p+1);
    }


    /* Make the response body */
    sprintf(content, "Welcome to auth.com:%s and %s\r\n<p>",name,passwd);
    sprintf(content, "%s\r\n", content);

    sprintf(content, "%sThanks for visiting!\r\n", content);
  
    /* Generate the HTTP response */
    printf("Content-length: %d\r\n", strlen(content));
    printf("Content-type: text/html\r\n\r\n");
    printf("%s", content);
    fflush(stdout);
    exit(0);
}

HTTPS的首頁:由於我們的CA不可信,所以需要我們認可


認可後HTTPS首頁:


HTTPS POST頁面:


HTTPS POST響應:


從上我們可以看出,POST提交的引數的確不是通過URL傳送的。