/*
 * reihttpd.c
 *
 * Copyright (c) 2008 Luis Rei <luis.rei@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */


#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <stdio.h>
#include <string.h>
#include <getopt.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <netdb.h>
#include <strings.h>


#define MAXLEN 1024
#define TERMINATOR1 "\xD\xA\xD\xA" /*CRLFCRLF - separates header from msg*/
#define TERMINATOR2 "\n\n" /*apparently this is supposed to work too*/
#define SEP " \xD\xA\n\0"    /*this is used for string processing*/
#define BACKLOG 10
#define DEFAULT_PORT 80
#define DEFAULT_ROOT "/var/www/data"
#define SERVER "Rhttpd/0.1.0\n"


#define R_BADREQUEST "<html><head>\n\
<title>400 Bad Request</title>\n\
</head><body>\n\
<h1>Bad Request</h1>\n\
<p>Your browser sent a request that this server could not understand.<br />\n\
</p>\n\
</body></html>\n"

#define R_FORBIDDEN "<html><head>\n\
<title>403 Forbidden</title>\n\
</head><body>\n\
<h1>Forbidden</h1>\n\
<p>You don't have permission to access this resource.</p>\n\
</body></html>\n"

#define R_NOTFOUND "<html><head>\n\
<title>404 Not Found</title>\n\
</head><body>\n\
<h1>Not Found</h1>\n\
<p>The requested URL was not found on this server.</p>\n\
</body></html>\n"

#define R_NOTIMPLEMENTED "<html><head>\n\
<title>501 Not Implemented</title>\n\
</head><body>\n\
<h1>Not Implemented</h1>\n\
<p>The method you tried to use is not supported by this server.</p>\n\
</body></html>\n"

#define R_INTERNAL "<html><head>\n\
<title>500 Internal Server Error</title>\n\
</head><body>\n\
<h1>Internal Server Error</h1>\n\
<p>Something is broken. Try going back to where you came from.</p>\n\
</body></html>\n"

typedef enum _code {
    C_OK = 202,
    C_NOTMOD = 304,
    C_BADREQUEST = 400,
    C_FORBIDDEN = 403,
    C_NOTFOUND = 404,
    C_INTERNAL = 500,
    C_NOTIMPLEMENTED = 501
 } code;

int port;
char *root;


int process_request(int sockfd, char *buf, int bytes) {
    struct stat st;
    struct tm since_tm, file_tm;
    code rcode;
    time_t now, since_tt, file_tt;
    size_t fsize;
    int file_des;
    char *line;
    char *request, *location, *protocol, *host, *since, *connection, *rstr;
    char *args[MAXLEN];
    char rbuf[MAXLEN];
    char temp[MAXLEN];
    char headers[MAXLEN];
    int ret, ii, argc, rbytes, major, minor;
    int COND_FLAG;
    

    printf("++++++++++++++++++++++ REQUEST ++++++++++++++++++++++++++++\n");
    printf("%s", buf);
    printf("---------------------- REQUEST ----------------------------\n");
    
    
    argc = 0;
    ret = 0;
    line = strtok(buf, "\n");
    do {
        args[argc] = (char *) malloc(MAXLEN);
        if (args[argc] == NULL) {
            perror("malloc");
            exit(0);
        }
        strncpy(args[argc], line, MAXLEN-1);
        
        argc++;
    } while ((line = strtok(NULL, "\n")) != NULL);
    
    /* Parse the GET request */
    request = strtok(args[0], SEP);
    location = strtok(NULL, SEP);
    protocol = strtok(NULL, SEP);
    
    
    /*check the protocol used, if < HTTP/1.1 it's a non-persistent connection*/
    major = minor = 0;
    if (protocol == NULL)
        ret = -1;   /*protocol not specified*/
    else {
        strncpy(temp, protocol, MAXLEN-1);
        line = strtok(temp, "/");
        if (strcmp(temp, "HTTP") != 0)
            ret = -1;   /*protocol not HTTP*/
        else {
            line = strtok(NULL, ".");
            major = atoi(line);
            if(major < 1)
                ret = -1;   /*< HTTP/1.0*/
            else {
                line = strtok(NULL, "\0");
                minor = atoi(line);
                if(major == 1 && minor == 0)
                    ret = -1;   /*HTTP/1.0*/
            }
        }
    }

    /*method*/
    if (strcmp(request, "GET") == 0) {
        if (strcmp(location, "/") == 0)
            location = "index.html";
        else if(*location == '/') 
            location++; /*skip forward slash */
        else
            exit(0);
        

        file_des = open(location, O_RDONLY);
        if (file_des < 0) {
            if (errno == EACCES)
                rcode = C_FORBIDDEN;
            else
                rcode = C_NOTFOUND; /*as far as I care*/
        }
        else
            rcode = C_OK;
        
        /* Parse Headers */
        connection = NULL;
        host = NULL;
        since = NULL;
        COND_FLAG = 0;
        
        for (ii = 1; ii < argc; ii++) {
            line = strtok(args[ii], ": ");
            if (line == NULL) {
                printf("Line is null.\n");
                continue;
            }
            
            /*host*/
            else if (strcmp(line, "Host") == 0)
                host = strtok(NULL, " ");
                
            /*If-Modified-Since*/
            else if(strcmp(line, "If-Modified-Since") == 0) {
                since = strtok(NULL, "\n");
                COND_FLAG = 1;
                if(since != NULL)
                    since++; /*skip whitespace*/
                else {
                    COND_FLAG = 0;
                    continue;
                }
                
                if (since[3] == ',') { /*Sun, 06 Nov 1994 08:49:37 GMT*/
                    rstr = strptime(since, "%a, %d %b %Y %T GMT", &since_tm);
                    if(rstr == NULL) {
                        COND_FLAG = 0;
                    }
                }
                else if (since[6] == ',') { /*Sunday, 06-Nov-94 08:49:37 GMT*/
                    rstr = strptime(since, "%a, %d-%b-%y %T GMT", &since_tm);
                    if(rstr == NULL) {
                        COND_FLAG = 0;
                    }
                }
                else { /*Sun Nov 6 08:49:37 1994*/
                    rstr = strptime(since, "%a %b %d %T %Y", &since_tm);
                    if(rstr == NULL) {
                        COND_FLAG = 0;
                    }
                }
                if (COND_FLAG) {
                    since_tt = mktime(&since_tm);
                    if (time(NULL) < since_tt)
                        COND_FLAG = 0; /*this is not the future!*/
                }
            }
            
            /*Connection*/
            else if(strcmp(line, "Connection") == 0)
                connection = strtok(NULL, " ");
            
             /*unknown*/
            else
                continue;
        }
    
        /*if protocol >= HTTP/1.1 Host must be specified*/
        if ((major == 1 && minor >= 1) || major > 1)
            if (host == NULL) {
                rcode = C_BADREQUEST;
            }
    }
    else
        rcode = C_NOTIMPLEMENTED;

    
    /*build Header*/
    
    /*get current time*/
    now = time(NULL);
    
    /*message type*/
    if (rcode == C_OK) {
        /*get size and last modified date*/
        fstat(file_des, &st);

       
        if (COND_FLAG) {
            /* convert file time to a time_t value for comparison*/
            /*this way of doing is lame but it works*/
            strftime(temp, MAXLEN-1, "%a, %d %b %Y %T GMT%n",
                         gmtime(&st.st_mtimespec.tv_sec));
            strptime(temp, "%a, %d %b %Y %T GMT", &file_tm);
            file_tt = mktime(&file_tm);
            
            /*compare*/
            if(file_tt <= since_tt) { /*the file has not been modified*/
                rcode = C_NOTMOD;
                fsize = 0;
            }
        }
    }

    switch(rcode) {
        case C_OK:
            fsize = st.st_size;
            sprintf(headers, "HTTP/1.1 %d OK\n", C_OK);
            break;
        case C_NOTMOD:    
            sprintf(headers, "HTTP/1.1 %d Not Modified\n", C_NOTMOD);
            break;
        case C_FORBIDDEN:
            strcpy(rbuf, R_FORBIDDEN);
            sprintf(headers, "HTTP/1.1 %d Forbidden\n", C_FORBIDDEN);
            break;
        case C_NOTFOUND:
            strcpy(rbuf, R_NOTFOUND);
            sprintf(headers, "HTTP/1.1 %d Not Found\n", C_NOTFOUND);
            break;
        case C_NOTIMPLEMENTED:
            strcpy(rbuf, R_NOTIMPLEMENTED);
            sprintf(headers, "HTTP/1.1 %d Not Implemented\n",
                    C_NOTIMPLEMENTED);
            break;
        case C_BADREQUEST:
            strcpy(rbuf, R_BADREQUEST);
            sprintf(headers, "HTTP/1.1 %d Bad Request\n", C_BADREQUEST);
            break;
        default: /*internal error*/
            strcpy(rbuf, R_INTERNAL);
            sprintf(headers, "HTTP/1.1 %d Internal Server Error\n",
                    C_INTERNAL);
            break;
    }
    
    if(rcode != C_OK && rcode != C_NOTMOD) {
        fsize = strlen(rbuf);
        ret = -1;
    }
        

    
    /*example: Date: Mon, 05 May 2008 10:06:01 GMT*/
    strcat(headers, "Date: ");
    strftime(temp, MAXLEN-1, "%a, %d %b %Y %T GMT%n", gmtime(&now));
    strcat(headers, temp);
    
    /*example: Server: Apache/2.2.8 (Unix) [...]*/
    strcat(headers, "Server: ");
    strcat(headers, SERVER);
    
    if (rcode == C_OK) {
        /*example: Last-Modified: Mon, 05 May 2008 10:06:01 GMT*/
        strcat(headers, "Last-Modified: ");
        strftime(temp, MAXLEN-1, "%a, %d %b %Y %T GMT%n",
                 gmtime(&st.st_mtimespec.tv_sec));
        strcat(headers, temp);
    }
    
    /*example: Content-Length: 5754 */
    strcat(headers, "Content-Length: ");
    sprintf(temp, "%lu\n", (unsigned long) fsize);
    strcat(headers, temp);
    
    /*example: Content-Type: text/html*/
    /*TODO: ^^*/
    
    /*add a newline*/
    strcat(headers, "\n");
    
    
    /*send header fields and data*/
    printf("++++++++++++++++++++++ REPLY +++++++++++++++++++++++++++\n");
    printf("header:\n%s", headers);
    send(sockfd, headers, strlen(headers), 0);
    if (rcode == C_OK)
        while ((rbytes = read(file_des, rbuf, MAXLEN-1)) > 0)
            send(sockfd, rbuf, rbytes, 0);
    else
        send(sockfd, rbuf, fsize, 0);
    printf("message:\n%s\n", rbuf);
    printf("---------------------- REPLY ---------------------------\n");
    
    
    if (connection != NULL)
        if (strncmp(connection, "close", 5) == 0)
            ret = -1; /*client asked to close the connection*/
    
    /*free dynamically allocated memory*/
        for (ii=0; ii < argc; ii++)
            free(args[ii]);
    
    
    return ret;
}


int main(int argc, char** argv){

    int	sockfd, newsockfd;
    unsigned int cli_size;
	struct sockaddr_in serv_addr, cli_addr;
	int	childpid;
    int opt, long_index;
    char buf[MAXLEN], request[MAXLEN];
	int	bytes, ret;
	
    static const char *opt_string = "hp:r:";
    static const struct option long_opts[] = {
        {"Port", required_argument, NULL, 'p'},
        {"Root", required_argument, NULL, 'r'},
        {"help", no_argument, NULL, 'h'},
        {NULL, no_argument, NULL, 0}
    };
    
    /*set defaults*/
    port = DEFAULT_PORT;
    root = DEFAULT_ROOT;
    
    /*command-line arguments*/
    while ((opt = getopt_long(argc, argv, opt_string, long_opts, &long_index))
            != -1) {
        switch (opt) {
            case 'p':
                port = atoi(optarg);
                break;

            case 'r':
                root = optarg;
                break;

            case 'h':
                //display_usage();
                break;
                
            case '?':
                break;

            default:
                exit(0);
                break;
        }
    }
    

    
    /*TODO: read a config file, close the config file*/
    /*TODO: if root, map UID/GID of low-priviledge acount*/
	
	/*open a TCP socket*/
	if ((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0) {
    		perror("socket()");
        	exit(0);
    }
	/*bind the local address so that a client can connect*/
	bzero((char*)&serv_addr,sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	/*server TCP port must be network byte ordered */	
	serv_addr.sin_port = htons(port);
    
	if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind()");
		exit(0);
	}
	
	/*TODO: connect to syslogd*/
	
	/*preload (possibly) dynamically lodaded objects*/
	gethostbyname("localhost");
	
    /*change directory*/
    if(chdir(root) < 0) {
        perror("chdir()");
        exit(0);
    }
    if(geteuid() == 0) { /* root can use chroot */
        if(chroot("./") < 0) {
            perror("chroot()");
            exit(0);
        }
    }
    
    
    /*TODO: if root, change to predefined low-priv user via setXXuid
    cf "Setuid Demystified", Chen & Wagner, Usenix 2002*/

	listen(sockfd, BACKLOG);

    /*main loop*/
	while(1) {
		/*wait for a connection request (concurrent server)*/
		cli_size = sizeof(cli_addr);
		newsockfd = accept(sockfd, (struct sockaddr *)&cli_addr, &cli_size);
		                    
		if (newsockfd < 0) {
			perror("accept()");
			exit(0);
		}
		
		/*fork the server to handle the connections*/
		if ((childpid = fork()) < 0) {
			perror("fork()");
			exit(0);
		}
		else if (childpid == 0) {	/*child process - process client requests*/
			close(sockfd);
            
            ret = 0;
            printf("READING REQUESTS FROM...\n");
            while (ret >= 0) {
                request[0] = '\0';
                while ((strstr(request, TERMINATOR1) == NULL) && 
                    (strstr(request, TERMINATOR2) == NULL)) {
                    bytes = recv(newsockfd, buf, MAXLEN-1, 0);
                    buf[bytes] = '\0';
                    strncat(request, buf, bytes);
                 }
            
                ret = process_request(newsockfd, request, strlen(request));
            }
            
			exit(0);
		}
		else {	/*parent process - waits the child process to terminate*/
			if (waitpid(childpid, NULL, 0) != childpid)
				perror("waitpid");
			close(newsockfd);
		}
	}
}


