C语言开发Linux下web服务器(支持GET/POST,SSL,目录显示等)

http://blog.csdn.net/yueguanghaidao/article/details/8450938

这个主要是在CSAPP基础上做的,添加了POST,SSL,目录显示等功能。 一、 实现功能: 1. 支持GET/POST方法 2. 支持SSL安全连接即HTTPS 3. 支持CGI 4. 基于IP地址和掩码的认证 5. 目录显示 6. 日志功能

7. 错误提示页面

github地址:https://github.com/Skycrab/Linux-C-Web-Server

源代码下载地址:点击打开链接

二、设计原理 首先介绍一些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类型识别代码如下: 主要就是通过文件后缀获取文件类型。

[cpp] view plaincopy

  1. static void get_filetype(const char *filename, char *filetype)   
  2. {  
  3. if (strstr(filename, ".html"))  
  4.         strcpy(filetype, "text/html");  
  5. else if (strstr(filename, ".gif"))  
  6.         strcpy(filetype, "image/gif");  
  7. else if (strstr(filename, ".jpg"))  
  8.         strcpy(filetype, "image/jpeg");  
  9. else if (strstr(filename, ".png"))  
  10.         strcpy(filetype, "image/png");  
  11. else
  12.     strcpy(filetype, "text/plain");  
  13. }    

如果支持HTTPS的话,那么我们就#define HTTPS,这主要通过gcc 的D选项实现的,具体细节可参考man手册。 静态内容显示实现如下:

[cpp] view plaincopy

  1. static void serve_static(int fd, char *filename, int filesize)   
  2. {  
  3. int srcfd;  
  4. char *srcp, filetype[MAXLINE], buf[MAXBUF];  
  5. /* Send response headers to client */
  6.     get_filetype(filename, filetype);  
  7.     sprintf(buf, "HTTP/1.0 200 OK\r\n");  
  8.     sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);  
  9.     sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);  
  10.     sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);  
  11. /* Send response body to client */
  12.     srcfd = Open(filename, O_RDONLY, 0);  
  13.     srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);  
  14.     Close(srcfd);  
  15.     #ifdef HTTPS 
  16. if(ishttps)  
  17.     {  
  18.         SSL_write(ssl, buf, strlen(buf));  
  19.     SSL_write(ssl, srcp, filesize);  
  20.     }  
  21. else
  22.     #endif
  23.     {  
  24.     Rio_writen(fd, buf, strlen(buf));  
  25.     Rio_writen(fd, srcp, filesize);  
  26.     }  
  27.     Munmap(srcp, filesize);  
  28. }  

#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方式如下:

[cpp] view plaincopy

  1. static int parse_uri(char *uri, char *filename, char *cgiargs)   
  2. {  
  3. char *ptr;  
  4. char tmpcwd[MAXLINE];  
  5.     strcpy(tmpcwd,cwd);  
  6.     strcat(tmpcwd,"/");  
  7. if (!strstr(uri, "cgi-bin"))   
  8.     {  /* Static content */
  9.     strcpy(cgiargs, "");  
  10.     strcpy(filename, strcat(tmpcwd,Getconfig("root")));  
  11.     strcat(filename, uri);  
  12. if (uri[strlen(uri)-1] == '/')  
  13.         strcat(filename, "home.html");  
  14. return 1;  
  15.     }  
  16. else
  17.     {  /* Dynamic content */
  18.     ptr = index(uri, '?');  
  19. if (ptr)   
  20.     {  
  21.         strcpy(cgiargs, ptr+1);  
  22.         *ptr = '\0';  
  23.     }  
  24. else
  25.         strcpy(cgiargs, "");  
  26.     strcpy(filename, cwd);  
  27.     strcat(filename, uri);  
  28. return 0;  
  29.     }  
  30. }  

GET方式的CGI规范实现原理:    服务器通过URL获取传给CGI程序的参数,设置环境变量QUERY_STRING,并将标准输出重定向到文件描述符,然后通过EXEC函数簇执行外部CGI程序。外部CGI程序获取QUERY_STRING并处理,处理完后输出结果。由于此时标准输出已重定向到文件描述符,即发送给了浏览器。 实现细节如下:由于涉及到HTTPS,所以稍微有点复杂。

[cpp] view plaincopy

  1. void get_dynamic(int fd, char *filename, char *cgiargs)   
  2. {  
  3. char buf[MAXLINE], *emptylist[] = { NULL },httpsbuf[MAXLINE];  
  4. int p[2];  
  5. /* Return first part of HTTP response */
  6.     sprintf(buf, "HTTP/1.0 200 OK\r\n");  
  7.     sprintf(buf, "%sServer: Web Server\r\n",buf);  
  8.     #ifdef HTTPS 
  9. if(ishttps)  
  10.         SSL_write(ssl,buf,strlen(buf));  
  11. else
  12.     #endif
  13.         Rio_writen(fd, buf, strlen(buf));  
  14.     #ifdef HTTPS 
  15. if(ishttps)  
  16.     {  
  17.         Pipe(p);  
  18. if (Fork() == 0)  
  19.     {  /* child  */
  20.         Close(p[0]);  
  21.         setenv("QUERY_STRING", cgiargs, 1);   
  22.         Dup2(p[1], STDOUT_FILENO);         /* Redirect stdout to p[1] */
  23.         Execve(filename, emptylist, environ); /* Run CGI program */
  24.     }  
  25.     Close(p[1]);  
  26.     Read(p[0],httpsbuf,MAXLINE);   /* parent read from p[0] */
  27.     SSL_write(ssl,httpsbuf,strlen(httpsbuf));  
  28.     }  
  29. else
  30.     #endif
  31.     {  
  32. if (Fork() == 0)   
  33.     { /* child */
  34. /* Real server would set all CGI vars here */
  35.         setenv("QUERY_STRING", cgiargs, 1);   
  36.         Dup2(fd, STDOUT_FILENO);         /* Redirect stdout to client */
  37.         Execve(filename, emptylist, environ); /* Run CGI program */
  38.     }  
  39. }  
  40. }  

POST方式的CGI规范实现原理:    由于POST方式不是通过URL传递参数,所以实现方式与GET方式不一样。 POST方式获取浏览器发送过来的参数长度设置为环境变量CONTENT-LENGTH。并将参数重定向到CGI的标准输入,这主要通过pipe管道实现的。CGI程序从标准输入读取CONTENT-LENGTH个字符就获取了浏览器传送的参数,并将处理结果输出到标准输出,同理标准输出已重定向到文件描述符,所以浏览器就能收到处理的响应。 具体实现细节如下:

[cpp] view plaincopy

  1. static void post_dynamic(int fd, char *filename, int contentLength,rio_t *rp)  
  2. {  
  3. char buf[MAXLINE],length[32], *emptylist[] = { NULL },data[MAXLINE];  
  4. int p[2];  
  5.     #ifdef HTTPS 
  6. int httpsp[2];  
  7.     #endif
  8.     sprintf(length,"%d",contentLength);  
  9.     memset(data,0,MAXLINE);  
  10.     Pipe(p);  
  11. /*       The post data is sended by client,we need to redirct the data to cgi stdin.
  12.     *    so, child read contentLength bytes data from fp,and write to p[1];
  13.     *    parent should redirct p[0] to stdin. As a result, the cgi script can
  14.     *    read the post data from the stdin. 
  15.     */
  16. /* https already read all data ,include post data  by SSL_read() */
  17. if (Fork() == 0)  
  18.     {                     /* child  */
  19.         Close(p[0]);  
  20.         #ifdef HTTPS 
  21. if(ishttps)  
  22.         {  
  23.             Write(p[1],httpspostdata,contentLength);      
  24.         }  
  25. else
  26.         #endif
  27.         {  
  28.             Rio_readnb(rp,data,contentLength);  
  29.             Rio_writen(p[1],data,contentLength);  
  30.         }  
  31.         exit(0) ;  
  32.     }  
  33. /* Send response headers to client */
  34.     sprintf(buf, "HTTP/1.0 200 OK\r\n");  
  35.     sprintf(buf, "%sServer: Tiny Web Server\r\n",buf);  
  36.     #ifdef HTTPS 
  37. if(ishttps)  
  38.         SSL_write(ssl,buf,strlen(buf));  
  39. else
  40.     #endif
  41.         Rio_writen(fd, buf, strlen(buf));  
  42.     Dup2(p[0],STDIN_FILENO);  /* Redirct p[0] to stdin */
  43.     Close(p[0]);  
  44.     Close(p[1]);  
  45.     setenv("CONTENT-LENGTH",length , 1);   
  46.     #ifdef HTTPS 
  47. if(ishttps)  /* if ishttps,we couldnot redirct stdout to client,we must use SSL_write */
  48.     {  
  49.         Pipe(httpsp);  
  50. if(Fork()==0)  
  51.       {  
  52.         Dup2(httpsp[1],STDOUT_FILENO);        /* Redirct stdout to https[1] */
  53.         Execve(filename, emptylist, environ);   
  54.     }  
  55.     Read(httpsp[0],data,MAXLINE);  
  56.     SSL_write(ssl,data,strlen(data));  
  57.     }  
  58. else
  59.     #endif
  60.     {  
  61.         Dup2(fd,STDOUT_FILENO);        /* Redirct stdout to client */
  62.         Execve(filename, emptylist, environ);   
  63.     }  
  64. }  

目录显示功能原理:    主要是通过URL获取所需目录,然后获取该目录下所有文件,并发送相应信息,包括文件格式对应图片,文件名,文件大小,最后修改时间等。由于我们发送的文件名是通过超链接的形式,所以我们可以点击文件名继续浏览信息。 具体实现细节如下:

[cpp] view plaincopy

  1. static void serve_dir(int fd,char *filename)  
  2. {  
  3.     DIR *dp;  
  4. struct dirent *dirp;  
  5. struct stat sbuf;  
  6. struct passwd *filepasswd;  
  7. int num=1;  
  8. char files[MAXLINE],buf[MAXLINE],name[MAXLINE],img[MAXLINE],modifyTime[MAXLINE],dir[MAXLINE];  
  9. char *p;  
  10. /*
  11.     * Start get the dir   
  12.     * for example: /home/yihaibo/kerner/web/doc/dir -> dir[]="dir/";
  13.     */
  14.     p=strrchr(filename,'/');  
  15.     ++p;  
  16.     strcpy(dir,p);  
  17.     strcat(dir,"/");  
  18. /* End get the dir */
  19. if((dp=opendir(filename))==NULL)  
  20.         syslog(LOG_ERR,"cannot open dir:%s",filename);  
  21.         sprintf(files, "<html><title>Dir Browser</title>");  
  22.     sprintf(files,"%s<style type=""text/css""> a:link{text-decoration:none;} </style>",files);  
  23.     sprintf(files, "%s<body bgcolor=""ffffff"" font-family=Arial color=#fff font-size=14px>\r\n", files);  
  24. while((dirp=readdir(dp))!=NULL)  
  25.     {  
  26. if(strcmp(dirp->d_name,".")==0||strcmp(dirp->d_name,"..")==0)  
  27. continue;  
  28.         sprintf(name,"%s/%s",filename,dirp->d_name);  
  29.         Stat(name,&sbuf);  
  30.         filepasswd=getpwuid(sbuf.st_uid);  
  31. if(S_ISDIR(sbuf.st_mode))  
  32.         {  
  33.             sprintf(img,"<img src=""dir.png"" width=""24px"" height=""24px"">");  
  34.         }  
  35. else if(S_ISFIFO(sbuf.st_mode))  
  36.         {  
  37.             sprintf(img,"<img src=""fifo.png"" width=""24px"" height=""24px"">");  
  38.         }  
  39. else if(S_ISLNK(sbuf.st_mode))  
  40.         {  
  41.             sprintf(img,"<img src=""link.png"" width=""24px"" height=""24px"">");  
  42.         }  
  43. else if(S_ISSOCK(sbuf.st_mode))  
  44.         {  
  45.             sprintf(img,"<img src=""sock.png"" width=""24px"" height=""24px"">");  
  46.         }  
  47. else
  48.             sprintf(img,"<img src=""file.png"" width=""24px"" height=""24px"">");  
  49.     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));  
  50.     }  
  51.     closedir(dp);  
  52.     sprintf(files,"%s</body></html>",files);  
  53. /* Send response headers to client */
  54.     sprintf(buf, "HTTP/1.0 200 OK\r\n");  
  55.     sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);  
  56.     sprintf(buf, "%sContent-length: %d\r\n", buf, strlen(files));  
  57.     sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, "text/html");  
  58.     #ifdef HTTPS
  59. if(ishttps)  
  60.     {  
  61.         SSL_write(ssl,buf,strlen(buf));  
  62.         SSL_write(ssl,files,strlen(files));  
  63.     }  
  64. else
  65.     #endif
  66.     {  
  67.         Rio_writen(fd, buf, strlen(buf));  
  68.         Rio_writen(fd, files, strlen(files));  
  69.     }  
  70.     exit(0);  
  71. }  

HTTPS的实现:    HTTPS主要基于openssl的开源库实现。如果没有安装,那么我们就不#define HTTPS。 HTTPS的功能主要就是提供安全的连接,服务器和浏览器之间传送的数据是通过加密的,加密方式可以自己选定。    开始连接时,服务器需要发送CA,由于我们的CA是自己签发的,所以需要我们自己添加为可信。 访问控制功能: 主要是通过获取客户端IP地址,并转换为整数,与上配置文件中定义的掩码,如果符合配置文件中允许的网段,那么可以访问,否则不可以。 具体实现如下。

[cpp] view plaincopy

  1. static long long ipadd_to_longlong(const char *ip)  
  2. {  
  3. const char *p=ip;  
  4. int ge,shi,bai,qian;  
  5.     qian=atoi(p);  
  6.     p=strchr(p,'.')+1;  
  7.     bai=atoi(p);  
  8.     p=strchr(p,'.')+1;  
  9.     shi=atoi(p);  
  10.     p=strchr(p,'.')+1;  
  11.     ge=atoi(p);  
  12. return (qian<<24)+(bai<<16)+(shi<<8)+ge;  
  13. }  
  14. int access_ornot(const char *destip) // 0 -> not 1 -> ok
  15. {  
  16. //192.168.1/255.255.255.0
  17. char ipinfo[16],maskinfo[16];  
  18. char *p,*ip=ipinfo,*mask=maskinfo;  
  19. char count=0;  
  20. char *maskget=Getconfig("mask");  
  21. const char *destipconst,*ipinfoconst,*maskinfoconst;  
  22. if(maskget=="")  
  23.     {  
  24.         printf("ok:%s\n",maskget);  
  25. return 1;  
  26.     }     
  27.     p=maskget;  
  28. /* get ipinfo[] start */
  29. while(*p!='/')  
  30.     {  
  31. if(*p=='.')  
  32.             ++count;  
  33.         *ip++=*p++;  
  34.     }  
  35. while(count<3)  
  36.     {  
  37.         *ip++='.';  
  38.         *ip++='0';  
  39.         ++count;  
  40.     }  
  41.     *ip='\0';  
  42. /* get ipinfo[] end */
  43. /* get maskinfo[] start */
  44.     ++p;  
  45. while(*p!='\0')  
  46.     {  
  47. if(*p=='.')  
  48.             ++count;  
  49.         *mask++=*p++;  
  50.     }  
  51. while(count<3)  
  52.     {  
  53.         *mask++='.';  
  54.         *mask++='0';  
  55.         ++count;  
  56.     }  
  57.     *mask='\0';  
  58. /* get maskinfo[] end */
  59.     destipconst=destip;  
  60.     ipinfoconst=ipinfo;  
  61.     maskinfoconst=maskinfo;  
  62. return ipadd_to_longlong(ipinfoconst)==(ipadd_to_longlong(maskinfoconst)&ipadd_to_longlong(destipconst));  
  63. }  

配置文件的读取: 主要选项信息都定义与配置文件中。 格式举例如下; #HTTP PORT PORT = 8888 所以读取配置文件函数具体如下:

[cpp] view plaincopy

  1. static char* getconfig(char* name)  
  2. {  
  3. /*
  4. pointer meaning:
  5. ...port...=...8000...
  6.    |  |   |   |  |
  7.   *fs |   |   |  *be    f->forward  b-> back
  8.       *fe |   *bs       s->start    e-> end
  9.           *equal
  10. */
  11. static char info[64];  
  12. int find=0;  
  13. char tmp[256],fore[64],back[64],tmpcwd[MAXLINE];  
  14. char *fs,*fe,*equal,*bs,*be,*start;  
  15.     strcpy(tmpcwd,cwd);  
  16.     strcat(tmpcwd,"/");  
  17. FILE *fp=getfp(strcat(tmpcwd,"config.ini"));  
  18. while(fgets(tmp,255,fp)!=NULL)  
  19.     {  
  20.         start=tmp;  
  21.         equal=strchr(tmp,'=');  
  22. while(isblank(*start))  
  23.             ++start;  
  24.         fs=start;  
  25. if(*fs=='#')  
  26. continue;  
  27. while(isalpha(*start))  
  28.             ++start;  
  29.         fe=start-1;  
  30.         strncpy(fore,fs,fe-fs+1);  
  31.         fore[fe-fs+1]='\0';  
  32. if(strcmp(fore,name)!=0)  
  33. continue;  
  34.         find=1;  
  35.         start=equal+1;  
  36. while(isblank(*start))  
  37.             ++start;  
  38.         bs=start;  
  39. while(!isblank(*start)&&*start!='\n')  
  40.             ++start;  
  41.         be=start-1;  
  42.         strncpy(back,bs,be-bs+1);  
  43.         back[be-bs+1]='\0';  
  44.         strcpy(info,back);  
  45. break;  
  46.     }  
  47. if(find)  
  48. return info;  
  49. else
  50. return NULL;  
  51. }  

二、 测试 本次测试使用了两台机器。一台Ubuntu的浏览器作为客户端,一台Redhat作为服务器端,其中Redhat是Ubuntu上基于VirtualBox的一台虚拟机。

IP地址信息如下:

Ubuntu的vboxnet0:

RedHateth0:

RedHat主机编译项目:

由于我们同事监听了8000和4444,所以有两个进程启动。

HTTP的首页:

目录显示功能:

HTTP GET页面:

HTTPGET响应:

从HTTP GET响应中我们观察URL,参数的确是通过URL传送过去的。

其中getAuth.c如下:

[cpp] view plaincopy

  1. #include "wrap.h"
  2. #include "parse.h"
  3. int main(void) {  
  4. char *buf, *p;  
  5. char name[MAXLINE], passwd[MAXLINE],content[MAXLINE];  
  6. /* Extract the two arguments */
  7. if ((buf = getenv("QUERY_STRING")) != NULL) {  
  8.     p = strchr(buf, '&');  
  9.     *p = '\0';  
  10.     strcpy(name, buf);  
  11.     strcpy(passwd, p+1);  
  12.     }  
  13. /* Make the response body */
  14.     sprintf(content, "Welcome to auth.com:%s and %s\r\n<p>",name,passwd);  
  15.     sprintf(content, "%s\r\n", content);  
  16.     sprintf(content, "%sThanks for visiting!\r\n", content);  
  17. /* Generate the HTTP response */
  18.     printf("Content-length: %d\r\n", strlen(content));  
  19.     printf("Content-type: text/html\r\n\r\n");  
  20.     printf("%s", content);  
  21.     fflush(stdout);  
  22.     exit(0);  
  23. }  

HTTPS的首页:由于我们的CA不可信,所以需要我们认可

认可后HTTPS首页:

HTTPS POST页面:

HTTPS POST响应:

从上我们可以看出,POST提交的参数的确不是通过URL传送的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏上善若水

003互联网网络技术之WireShark过滤语法

less than 小于 < lt 小于等于 le 等于 eq 大于 gt 大于等于 ge 不等 ne

15060
来自专栏木子墨的前端日常

nginx反向代理跨域基本配置与常见误区

最近公司前后端分离,前端独立提供页面和静态服务很自然的就想到了用nginx去做静态服务器。同时由于跨域了,就想利用nginx的反向代理去处理一下跨域,但是在解决...

32030
来自专栏程序员互动联盟

【专业技术】Android安全嘛?

安卓有一套自己的安全权限机制,大部分来自linux的权限机制,某些地方也做了延伸,比如linux中的用户概念,在安卓上来说就相当于app。对于一些刚学习安卓的同...

42390
来自专栏后台及大数据开发

API接口设计:防参数篡改+防二次请求

API接口由于需要供第三方服务调用,所以必须暴露到外网,并提供了具体请求地址和请求参数

1.2K20
来自专栏用户2442861的专栏

java数据库操作 (附带数据库连接池的代码)

本文来自:曹胜欢博客专栏。转载请注明出处:http://blog.csdn.net/csh624366188

41020
来自专栏程序员互动联盟

【专业技术】Android如何保证安全?

存在问题: 那么多小伙伴想root,root后好处多多你懂的,那么开发的小伙伴最想关心的是安全机制问题。 解决方案: 我们就以此来了解一下Android 安全...

40660
来自专栏二次元

中国电信登录RSA算法+分析图文

一、用到的工具 1.ie浏览器(9以上的版本) 2.httpwatch (中英文都可以) 3.js调试工具 目标网站:http://x...

12100
来自专栏散尽浮华

Python-Socket

socket通常也称作套接字,用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过“套接字”向网络发出请求或者应答网络请求 socket既是一种特殊文件...

28970
来自专栏小白安全

分享几个绕过URL跳转限制的思路

大家对URL任意跳转都肯定了解,也知道他的危害,这里我就不细说了,过~ 大家遇到的肯定都是很多基于这样的跳转格式 http://www.xxx.x...

92960
来自专栏决胜机器学习

设计模式专题(十)——观察者模式

设计模式专题(十)——观察者模式 (原创内容,转载请注明来源,谢谢) 一、概述 观察者模式(Observer),又称做发布-订阅模式(Publish/Subs...

37490

扫码关注云+社区

领取腾讯云代金券