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

这个主要是在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类型识别代码如下:
主要就是通过文件后缀获取文件类型。

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

 

tatic 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, "Dir Browser");
    sprintf(files,"%s",files);
    sprintf(files, "%s\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,"");
        }
        else if(S_ISFIFO(sbuf.st_mode))
        {
            sprintf(img,"");
        }
        else if(S_ISLNK(sbuf.st_mode))
        {
            sprintf(img,"");
        }
        else if(S_ISSOCK(sbuf.st_mode))
        {
            sprintf(img,"");
        }
        else
            sprintf(img,"");
 
 
    sprintf(files,"%s

%-2d%s""%-15s%-10s%10d %24s

\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",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如下:

 

 
  1. #include "wrap.h"

  2. #include "parse.h"

  3.  
  4. int main(void) {

  5. char *buf, *p;

  6. char name[MAXLINE], passwd[MAXLINE],content[MAXLINE];

  7.  
  8. /* Extract the two arguments */

  9. if ((buf = getenv("QUERY_STRING")) != NULL) {

  10. p = strchr(buf, '&');

  11. *p = '\0';

  12. strcpy(name, buf);

  13. strcpy(passwd, p+1);

  14. }

  15.  
  16.  
  17. /* Make the response body */

  18. sprintf(content, "Welcome to auth.com:%s and %s\r\n

    ",name,passwd);

  19. sprintf(content, "%s\r\n", content);

  20.  
  21. sprintf(content, "%sThanks for visiting!\r\n", content);

  22.  
  23. /* Generate the HTTP response */

  24. printf("Content-length: %d\r\n", strlen(content));

  25. printf("Content-type: text/html\r\n\r\n");

  26. printf("%s", content);

  27. fflush(stdout);

  28. exit(0);

  29. }

 

 

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

 

 

认可后HTTPS首页:

 

 

HTTPS POST页面:

 

 

HTTPS POST响应:

 

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

你可能感兴趣的:(HTTP,Server)