设为首页 收藏本站
查看: 429|回复: 0

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

[复制链接]

尚未签到

发表于 2016-4-6 09:58:44 | 显示全部楼层 |阅读模式
这个主要是在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:

DSC0000.jpg

  
RedHateth0:
DSC0001.jpg

  

  
  RedHat主机编译项目:
DSC0002.jpg

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

  HTTP的首页:

DSC0003.jpg

  

  
  目录显示功能:
DSC0004.jpg

  

  
  HTTP GET页面:
DSC0005.jpg

  

  HTTPGET响应:
DSC0006.jpg

  
  从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不可信,所以需要我们认可
DSC0007.jpg

  

  
  认可后HTTPS首页:
DSC0008.jpg

  

  
  HTTPS POST页面:
DSC0009.jpg

  

  
  HTTPS POST响应:
DSC00010.jpg

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

  

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-200347-1-1.html 上篇帖子: linux文件系统上的特殊权限 下篇帖子: linux下解压.tar.gz的命令
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表