TinyHttpServer 服务器复现

4 篇文章 0 订阅

TinyHttpServer 服务器复现


一、Tinyhttpd服务器

原项目地址:https://github.com/EZLippi/Tinyhttpd
学习借读:https://www.cnblogs.com/nengm1988/p/7816618.html
本人复现项目地址(带中文,对代码的注释):https://github.com/yeshenyong/TinyHttpServer

工具:Ubuntu、vscode、grel-cgi

二、代码呈现

1.注意问题

由于index.html 不可赋予执行权限 需执行 chmod 600 index.html
由于color.cgi check cgi 需要赋予执行权限 需执行 chmod 714 color.cgi check.cgi

2.代码实现

代码如下(示例):

/* J. David's webserver */
/* This is a simple webserver.
 * Created November 1999 by J. David Blackstone.
 * CSE 4344 (Network concepts), Prof. Zeigler
 * University of Texas at Arlington
 */
/* This program compiles for Sparc Solaris 2.6.
 * To compile for Linux:
 *  1) Comment out the #include <pthread.h> line.
 *  2) Comment out the line that defines the variable newthread.
 *  3) Comment out the two lines that run pthread_create().
 *  4) Uncomment the line that runs accept_request().
 *  5) Remove -lsocket from the Makefile.
 */
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <strings.h>
#include <string.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdlib.h>

// Http请求,后续主要是处理这个头
//
// GET / HTTP/1.1
// Host: 192.168.0.23:47310
// Connection: keep-alive
// Upgrade-Insecure-Requests: 1
// User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36
// Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*; q = 0.8
// Accept - Encoding: gzip, deflate, sdch
// Accept - Language : zh - CN, zh; q = 0.8
// Cookie: __guid = 179317988.1576506943281708800.1510107225903.8862; monitor_count = 5
//

// POST / color1.cgi HTTP / 1.1
// Host: 192.168.0.23 : 47310
// Connection : keep - alive
// Content - Length : 10
// Cache - Control : max - age = 0
// Origin : http ://192.168.0.23:40786
// Upgrade - Insecure - Requests : 1
// User - Agent : Mozilla / 5.0 (Windows NT 6.1; WOW64) AppleWebKit / 537.36 (KHTML, like Gecko) Chrome / 55.0.2883.87 Safari / 537.36
// Content - Type : application / x - www - form - urlencoded
// Accept : text / html, application / xhtml + xml, application / xml; q = 0.9, image / webp, */*;q=0.8
// Referer: http://192.168.0.23:47310/
// Accept-Encoding: gzip, deflate
// Accept-Language: zh-CN,zh;q=0.8
// Cookie: __guid=179317988.1576506943281708800.1510107225903.8862; monitor_count=281
// Form Data
// color=gray

//宏定义,是否是空格
#define ISspace(x) isspace((int)(x))

#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"

//每次收到请求,创建一个线程来处理接受到的请求
//把client_sock转成地址作为参数传入pthread_create
void accept_request(void *arg);

//错误请求
void bad_request(int);

//读取文件
void cat(int, FILE *);

//无法执行
void cannot_execute(int);

//错误输出
void error_die(const char *);

//执行cgi脚本
void execute_cgi(int, const char *,const char *,const char *);

//得到一行数据,只要发现c为\n,就认为是一行结束,如果读到\r,再用MSG_PEEK的方式读入一个字符,如果是\n,从socket用读出
//如果是下个字符则不处理,将c置为\n,结束。如果读到的数据为0中断,或者小于0,也视为结束,c置为\n
int get_line(int, char *, int);

//返回http头
void headers(int, const char *);

//没有发现文件
void not_found(int);

//如果不是CGI文件,直接读取文件返回给请求的http客户端
void serve_file(int, const char *);

//开启tcp连接,绑定端口等操作
int startup(u_short *);

//如果不是Get或者Post,就报方法没有实现
void unimplemented(int);

/**********************************************************************/
/* Get a line from a socket, whether the line ends in a newline,
 * carriage return, or a CRLF combination.  Terminates the string read
 * with a null character.  If no newline indicator is found before the
 * end of the buffer, the string is terminated with a null.  If any of
 * the above three line terminators is read, the last character of the
 * string will be a linefeed and the string will be terminated with a
 * null character.
 * Parameters: the socket descriptor
 *             the buffer to save the data in
 *             the size of the buffer
 * Returns: the number of bytes stored (excluding null) */
/**********************************************************************/

//得到一行数据,只要发现c为\n,就认为是一行结束,如果读到\r,再用MSG_PEEK的方式读入一个字符,如果是\n,从socket用读出
//如果是下个字符则不处理,将c置为\n,结束。如果读到的数据为0中断,或者小于0,也视为结束,c置为\n
int get_line(int sock, char *buf, int size)
{
    int i = 0;
    char c = '\0';
    int n;
    
    while ((i < size - 1) && (c != '\n'))
    {
        // 10.recv函数:
        // 功能:在已建立连接的套接字上接收数据。
        // 格式:int recv(SOCKET s, char *buf, int len, int flags)。
        // 参数:s-已建立连接的套接字;buf-存放接收到的数据的缓冲区指针;len-buf的长度;flags-调用方式:
        // (1)0:接收的是正常数据,无特殊行为。
        // (2)MSG_PEEK:系统缓冲区数据复制到提供的接收缓冲区,但是系统缓冲区内容并没有删除。
        // (3)MSG_OOB:表示处理带外数据。
        // 返回值:接收成功时返回接收到的数据长度,连接结束时返回0,连接失败时返回SOCKET_ERROR。
        n = recv(sock, &c, 1, 0);
        /* DEBUG printf("%02X\n", c); */
        if (n > 0)
        {
            if(c == '\r')
            {
                //偷窥一个字节,如果是\n就读走
                n = recv(sock, &c, 1, MSG_PEEK);
                /* DEBUG printf("%02X\n", c); */
                if((n > 0) && (c == '\n'))
                    recv(sock, &c, 1, 0);
                else
                    //不是\n(读到下一行的字符)或者没读到,置c为\n 跳出循环,完成一行读取
                    c = '\n';
            }
            buf[i] = c;
            i++;
        }
        else
            c = '\n';
    }
    buf[i] = '\0';
    return (i);
}

/**********************************************************************/
/* Put the entire contents of a file out on a socket.  This function
 * is named after the UNIX "cat" command, because it might have been
 * easier just to do something like pipe, fork, and exec("cat").
 * Parameters: the client socket descriptor
 *             FILE pointer for the file to cat */
/**********************************************************************/

//得到文件内容,发送
void cat(int client, FILE *resource)
{
    char buf[1024];
    fgets(buf, sizeof(buf), resource);
    while(!feof(resource)){
        send(client, buf, strlen(buf), 0);
        fgets(buf, sizeof(buf), resource);
    }
}


/**********************************************************************/
/* Return the informational HTTP headers about a file. */
/* Parameters: the socket to print the headers on
 *             the name of the file */
/**********************************************************************/

//加入http的headers
void headers(int client, const char *filename)
{
    char buf[1024];
    (void)filename; /* could use filename to determine file type */
    
    strcpy(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);
    strcpy(buf, SERVER_STRING);
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    strcpy(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
}

void serve_file(int client,const char *filename)
{
    FILE *resource = NULL;
    int numchars = 1;
    char buf[1024];

    //默认字符
    buf[0] = 'A'; buf[1] = '\0';
    while( (numchars > 0) && strcmp("\n", buf) )
        numchars = get_line(client, buf, sizeof(buf));
    resource = fopen(filename, "r");
    if (resource == NULL)
        not_found(client);
    else{
        headers(client, filename);
        cat(client, resource);
    }
    fclose(resource);
}

/**********************************************************************/
/* Print out an error message with perror() (for system errors; based
 * on value of errno, which indicates system call errors) and exit the
 * program indicating an error. */
/**********************************************************************/
void error_die(const char *str)
{
    // perror(s) 用来将上一个函数发生错误的原因输出到标准设备(stderr)。参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。
    // 此错误原因依照全局变量errno的值来决定要输出的字符串
    perror(str);
    exit(1);
}

void not_found(int client)
{
    char buf[1024];
    sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, SERVER_STRING);
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "your request because the resource specified\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "is unavailable or nonexistent.\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</BODY></HTML>\r\n");
    send(client, buf, strlen(buf), 0);
}

/**********************************************************************/
/* Inform the client that a request it has made has a problem.
 * Parameters: client socket */
/**********************************************************************/
void bad_request(int client)
{
    char buf[1024];
    sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "Content-type: text/html\r\n");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "<P>Your browser sent a bad request, ");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "such as a POST without a Content-Length.\r\n");
    send(client, buf, sizeof(buf), 0);
}

/**********************************************************************/
/* Inform the client that a CGI script could not be executed.
 * Parameter: the client socket descriptor. */
/**********************************************************************/
void cannot_execute(int client)
{
    char buf[1024];

    sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<P>Error prohibited CGI execution.\r\n");
    send(client, buf, strlen(buf), 0);
}

/**********************************************************************/
/* Execute a CGI script.  Will need to set environment variables as
 * appropriate.
 * Parameters: client socket descriptor
 *             path to the CGI script */
/**********************************************************************/
void execute_cgi(int client, const char *path,
                const char *method, const char *query_string)
{
    
    char buf[1024];

    // two cgi
    int cgi_output[2];
    int cgi_input[2];

    pid_t pid;
    int status;

    int i;
    char c;

    int numchars = 1;

    int content_length = -1;

    buf[0] = 'A'; buf[1] = '\0';

    if (strcasecmp(method, "GET") == 0)
    //读取数据,把整个header都读掉,以为Get写死了直接读取index.html,没有必要分析余下的http信息了
        while( (numchars > 0) && strcmp("\n", buf) )
            numchars = get_line(client, buf, sizeof(buf));
    else{   /* POST */
        numchars = get_line(client, buf, sizeof(buf));
        while( (numchars > 0) && strcmp("\n", buf) )
        {
            //如果是POST请求,就需要得到Content-Length,Content-Length:这个字符串一共长为15位,所以
            //取出头部一句后,将第16位设置结束符,进行比较
            //第16位置为结束
            buf[15] = '\0';
            if (strcasecmp(buf, "Content-Length:") == 0)
                //内存从第17位开始就是长度,将17位开始的所有字符串转成整数就是content_length
                content_length = atoi(&buf[16]);
            numchars = get_line(client, buf, sizeof(buf));
        }
        if(content_length == -1){
            bad_request(client);
            return;
        }
    }   
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);
    //建立output管道
    if (pipe(cgi_output) < 0){
        cannot_execute(client);
        return;
    }

    //建立input管道
    if (pipe(cgi_input) < 0){
        cannot_execute(client);
        return;
    }
    //       fork后管道都复制了一份,都是一样的
    //       子进程关闭2个无用的端口,避免浪费             
    //       ×<------------------------->1    output
    //       0<-------------------------->×   input 

    //       父进程关闭2个无用的端口,避免浪费             
    //       0<-------------------------->×   output
    //       ×<------------------------->1    input
    //       此时父子进程已经可以通信


    //fork进程,子进程用于执行CGI
    //父进程用于收数据以及发送子进程处理的回复数据
    if( (pid = fork()) < 0 ){
        cannot_execute(client);
        return;
    }

    if (pid == 0)   /* child: CGI script */
    {
        char meth_env[255];
        char query_env[255];
        char length_env[255];

        //子进程输出重定向到output管道的1端
        dup2(cgi_output[1], 1);
        //子进程输入重定向到input管道的0端
        dup2(cgi_input[0], 0);

        //关闭无用管道口
        close(cgi_output[0]);
        close(cgi_input[1]);

        //CGI环境变量
        sprintf(meth_env, "REQUEST_METHOD=%s", method);
        putenv(meth_env);
        if (strcasecmp(method, "GET") == 0){
            sprintf(query_env, "QUERY_STRING=%s", query_string);
            putenv(query_env);
        }
        else{   /* POST */
            sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
            putenv(length_env);            
        }
        //替换执行path
        //如果path有问题,例如将html网页改成可执行的,但是执行后m为-1
        //退出子进程,管道被破坏,但是父进程还在往里面写东西,触发Program received signal SIGPIPE, Broken pipe.
        execl(path, path, NULL);
        exit(0);
    }else{  /* parent */
        close(cgi_output[1]);
        close(cgi_input[0]);

        if (strcasecmp(method, "POST") == 0)
            //得到post请求数据,写到input管道中,供子进程使用
            for(i = 0; i < content_length; ++i){
                recv(client, &c, 1, 0);
                write(cgi_input[1], &c, 1);
            }
        //管道!!!!!!
        //从output管道读到子进程处理后的信息,然后send出去
        while(read(cgi_output[0], &c, 1) > 0)
        {
            send(client, &c, 1, 0);
            // //子进程输入重定向到input管道的0端
            // html content
            // printf("%c ", c);
        }
        close(cgi_output[0]);
        close(cgi_input[1]);
        waitpid(pid, &status, 0);
    }

}

void accept_request(void *arg)
{
    //socket
    int client = (intptr_t)arg;
    char buf[1024];
    int numchars;
    char method[255];
    char url[255];
    char path[512];
    size_t i, j;
    struct stat st;
    int cgi = 0;    /* becomes true if server decides this is a CGI
                    * program */
    char *query_string = NULL;
    //根据上面的Get请求,可以看到这边就是取第一行
    //这边都是在处理第一条http信息
    //"GET / HTTP/1.1\n"
    numchars = get_line(client, buf, sizeof(buf));
    i = 0; j = 0;
    while( !ISspace(buf[j] ) && ( i < sizeof(method) - 1 ))
    {
        method[i] = buf[j];
        i++; j++;
    }
    //ending
    method[i] = '\0';

    if(strcasecmp(method, "GET") && strcasecmp(method, "POST")){
        unimplemented(client);
        return;
    }

    //if method == POST
    if(strcasecmp(method, "POST") == 0)
        cgi = 1;
    i = 0;
    //jump space 
    while(ISspace(buf[j]) && (j < sizeof(buf)))
        j++;
    //得到 "/"   注意:如果你的http的网址为http://192.168.0.23:47310/index.html
    //               那么你得到的第一条http信息为GET /index.html HTTP/1.1,那么
    //               解析得到的就是/index.html
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
    {
        url[i] = buf[j];
        i++;j++;
    }
    url[i] = '\0';
    
    //judge if or not get
    if (strcasecmp(method, "GET") == 0)
    {
        query_string = url;
        while((*query_string != '?') && (*query_string != '\0'))
            query_string++;
        if(*query_string == '?')
        {
            cgi = 1;
            *query_string = '\0';
            query_string++;
        }
    }

    sprintf(path, "htdocs%s", url);

    if( path[strlen(path) - 1] == '/' )
        strcat(path, "index.html");
    
    //get file message
    if( stat(path, &st) == -1 ){
        //把所有http信息读出然后丢弃
        while ((numchars > 0) && strcmp("\n", buf))
            numchars = get_line(client, buf, sizeof(buf));
        not_found(client);
    }else{
        // 文件权限类型
        // S_IFDIR    0040000   directory
        // S_IFMT     0170000   bitmask for the file type bitfields
        if( (st.st_mode & S_IFMT) == S_IFDIR )
            strcat(path, "/index.html");
        //如果你的文件默认是有执行权限的,自动解析成cgi程序,
        // 如果有执行权限但是不能执行,会接受到报错信号
        // S_IXUSR    00100     owner has execute permission
        // S_IXGRP    00010     group has execute permission
        // S_IXOTH    00001     others have execute permission
        if( (st.st_mode & S_IXUSR) ||
            (st.st_mode & S_IXGRP) ||
            (st.st_mode & S_IXOTH))
            cgi = 1;
        if(!cgi)
            //接读取文件返回给请求的http客户端
            serve_file(client, path);
        else 
            //执行cgi文件
            execute_cgi(client, path, method, query_string);
    }
    close(client);

}

/**********************************************************************/
/* Inform the client that the requested web method has not been
 * implemented.
 * Parameter: the client socket */
/**********************************************************************/

//如果方法没有实现,就返回此信息
void unimplemented(int client)
{
    char buf[1024];
    sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, SERVER_STRING);
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</TITLE></HEAD>\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</BODY></HTML>\r\n");
    send(client, buf, strlen(buf), 0);
}

/**********************************************************************/
/* This function starts the process of listening for web connections
 * on a specified port.  If the port is 0, then dynamically allocate a
 * port and modify the original port variable to reflect the actual
 * port.
 * Parameters: pointer to variable containing the port to connect on
 * Returns: the socket */
/**********************************************************************/
int startup(u_short *port)
{
    int httpd = 0;
    struct sockaddr_in name;
    httpd = socket(PF_INET, SOCK_STREAM, 0);
    if( httpd == -1 )
        error_die("socket");
    bzero(&name, sizeof(name));
    // memset(&name, 0, sizeof(name));
    name.sin_family = AF_INET;
    name.sin_port = htons(*port);
    // Address to accept any incoming messages.
    name.sin_addr.s_addr = htonl(INADDR_ANY);
    //绑定socket
    if(bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
        error_die("bind");
    // prove a random port
    if( *port == 0 ){
        socklen_t namelen = sizeof(name);
        if(getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
            error_die("getsockname");
        *port = ntohs(name.sin_port);
    }
    //listen
    if( listen(httpd, 5) < 0 )
        error_die("listen");
    return (httpd);
}


int main(void)
{
    int server_sock = -1;
    
    //define typedef unsigned short u_short
    u_short port = 0;

    int client_sock = -1;
    struct sockaddr_in client_name;

    socklen_t client_name_len = sizeof(client_name);
    // typedef unsigned long pthread_t
    pthread_t newthread;

    server_sock = startup(&port);
    printf("httpd running on port %d\n", port);

    while(1)
    {
        //接受请求,函数原型
        //#include <sys/types.h>
        //#include <sys/socket.h>  
        //int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
        client_sock = accept(server_sock,
                        (struct sockaddr *)&client_name,
                        &client_name_len);
        if(client_sock == -1)
            error_die("accept");
        /* accept_request(client_sock); */
        //每次收到请求,创建一个线程来处理接受到的请求
        //把client_sock转成地址作为参数传入pthread_create
        if(pthread_create(&newthread, NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
            perror("pthread_create");
    }

    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值