C語言開發Linux下web伺服器(支援GET/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傳送的。