/* $Id: minihttptestserver.c,v 1.25 2020/05/29 21:14:22 nanard Exp $ */
/* Project : miniUPnP
 * Author : Thomas Bernard
 * Copyright (c) 2011-2018 Thomas Bernard
 * This software is subject to the conditions detailed in the
 * LICENCE file provided in this distribution.
 * */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <time.h>
#include <errno.h>

#ifndef INADDR_LOOPBACK
#define INADDR_LOOPBACK         0x7f000001
#endif

#define CRAP_LENGTH (2048)

static int server(unsigned short port, const char * expected_file_name, int ipv6);

volatile sig_atomic_t quit = 0;
volatile sig_atomic_t child_to_wait_for = 0;

/**
 * signal handler for SIGCHLD (child status has changed)
 */
void handle_signal_chld(int sig)
{
    (void)sig;
    /* printf("handle_signal_chld(%d)\n", sig); */
    ++child_to_wait_for;
}

/**
 * signal handler for SIGINT (CRTL C)
 */
void handle_signal_int(int sig)
{
    (void)sig;
    /* printf("handle_signal_int(%d)\n", sig); */
    quit = 1;
}

/**
 * build a text/plain content of the specified length
 */
void build_content(char * p, size_t n)
{
    char line_buffer[80];
    int k;
    int i = 0;

    while(n > 0) {
        k = snprintf(line_buffer, sizeof(line_buffer),
                     "%04d_ABCDEFGHIJKL_This_line_is_64_bytes_long_ABCDEFGHIJKL_%04d\r\n",
                     i, i);
        if(k != 64) {
            fprintf(stderr, "snprintf() returned %d in build_content()\n", k);
        }
        ++i;
        if(n >= 64) {
            memcpy(p, line_buffer, 64);
            p += 64;
            n -= 64;
        } else {
            memcpy(p, line_buffer, n);
            p += n;
            n = 0;
        }
    }
}

/**
 * build crappy content
 */
void build_crap(char * p, size_t n)
{
    static const char crap[] = "_CRAP_\r\n";
    size_t i;

    while(n > 0) {
        i = sizeof(crap) - 1;
        if(i > n)
            i = n;
        memcpy(p, crap, i);
        p += i;
        n -= i;
    }
}

/**
 * build chunked response.
 * return a malloc'ed buffer
 */
char * build_chunked_response(size_t content_length, size_t * response_len)
{
    char * response_buffer;
    char * content_buffer;
    size_t buffer_length;
    size_t i;
    unsigned int n;

    /* allocate to have some margin */
    buffer_length = 256 + content_length + (content_length >> 4);
    response_buffer = malloc(buffer_length);
    if(response_buffer == NULL)
        return NULL;
    *response_len = snprintf(response_buffer, buffer_length,
                             "HTTP/1.1 200 OK\r\n"
                             "Content-Type: text/plain\r\n"
                             "Transfer-Encoding: chunked\r\n"
                             "\r\n");

    /* build the content */
    content_buffer = malloc(content_length);
    if(content_buffer == NULL) {
        free(response_buffer);
        return NULL;
    }
    build_content(content_buffer, content_length);

    /* chunk it */
    i = 0;
    while(i < content_length) {
        n = (rand() % 199) + 1;
        if(i + n > content_length) {
            n = content_length - i;
        }
        /* TODO : check buffer size ! */
        *response_len += snprintf(response_buffer + *response_len,
                                  buffer_length - *response_len,
                                  "%x\r\n", n);
        memcpy(response_buffer + *response_len, content_buffer + i, n);
        *response_len += n;
        i += n;
        response_buffer[(*response_len)++] = '\r';
        response_buffer[(*response_len)++] = '\n';
    }
    /* the last chunk : "0\r\n" a empty body and then
     * the final "\r\n" */
    memcpy(response_buffer + *response_len, "0\r\n\r\n", 5);
    *response_len += 5;
    free(content_buffer);

    printf("resp_length=%lu buffer_length=%lu content_length=%lu\n",
           *response_len, buffer_length, content_length);
    return response_buffer;
}

/* favicon.ico generator */
#ifdef OLD_HEADER
#define FAVICON_LENGTH (6 + 16 + 12 + 8 + 32 * 4)
#else
#define FAVICON_LENGTH (6 + 16 + 40 + 8 + 32 * 4)
#endif
void build_favicon_content(unsigned char * p, size_t n)
{
    int i;
    if(n < FAVICON_LENGTH)
        return;
    /* header : 6 bytes */
    *p++ = 0;
    *p++ = 0;
    *p++ = 1;   /* type : ICO */
    *p++ = 0;
    *p++ = 1;   /* number of images in file */
    *p++ = 0;
    /* image directory (1 entry) : 16 bytes */
    *p++ = 16;  /* width */
    *p++ = 16;  /* height */
    *p++ = 2;   /* number of colors in the palette. 0 = no palette */
    *p++ = 0;   /* reserved */
    *p++ = 1;   /* color planes */
    *p++ = 0;   /* " */
    *p++ = 1;   /* bpp */
    *p++ = 0;   /* " */
#ifdef OLD_HEADER
    *p++ = 12 + 8 + 32 * 4; /* bmp size */
#else
    *p++ = 40 + 8 + 32 * 4; /* bmp size */
#endif
    *p++ = 0;   /* " */
    *p++ = 0;   /* " */
    *p++ = 0;   /* " */
    *p++ = 6 + 16;  /* bmp offset */
    *p++ = 0;   /* " */
    *p++ = 0;   /* " */
    *p++ = 0;   /* " */
    /* BMP */
#ifdef OLD_HEADER
    /* BITMAPCOREHEADER */
    *p++ = 12;  /* size of this header */
    *p++ = 0;   /* " */
    *p++ = 0;   /* " */
    *p++ = 0;   /* " */
    *p++ = 16;  /* width */
    *p++ = 0;   /* " */
    *p++ = 16 * 2;  /* height x 2 ! */
    *p++ = 0;   /* " */
    *p++ = 1;   /* color planes */
    *p++ = 0;   /* " */
    *p++ = 1;   /* bpp */
    *p++ = 0;   /* " */
#else
    /* BITMAPINFOHEADER */
    *p++ = 40;  /* size of this header */
    *p++ = 0;   /* " */
    *p++ = 0;   /* " */
    *p++ = 0;   /* " */
    *p++ = 16;  /* width */
    *p++ = 0;   /* " */
    *p++ = 0;   /* " */
    *p++ = 0;   /* " */
    *p++ = 16 * 2;  /* height x 2 ! */
    *p++ = 0;   /* " */
    *p++ = 0;   /* " */
    *p++ = 0;   /* " */
    *p++ = 1;   /* color planes */
    *p++ = 0;   /* " */
    *p++ = 1;   /* bpp */
    *p++ = 0;   /* " */
    /* compression method, image size, ppm x, ppm y */
    /* colors in the palette ? */
    /* important colors */
    for(i = 4 * 6; i > 0; --i)
        *p++ = 0;
#endif
    /* palette */
    *p++ = 0;   /* b */
    *p++ = 0;   /* g */
    *p++ = 0;   /* r */
    *p++ = 0;   /* reserved */
    *p++ = 255; /* b */
    *p++ = 255; /* g */
    *p++ = 255; /* r */
    *p++ = 0;   /* reserved */
    /* pixel data */
    for(i = 16; i > 0; --i) {
        if(i & 1) {
            *p++ = 0125;
            *p++ = 0125;
        } else {
            *p++ = 0252;
            *p++ = 0252;
        }
        *p++ = 0;
        *p++ = 0;
    }
    /* Opacity MASK */
    for(i = 16 * 4; i > 0; --i) {
        *p++ = 0;
    }
}

enum modes {
    MODE_INVALID, MODE_CHUNKED, MODE_ADDCRAP, MODE_NORMAL, MODE_FAVICON, MODE_MALFORMED
};

const struct {
    const enum modes mode;
    const char * text;
} modes_array[] = {
    {MODE_CHUNKED, "chunked"},
    {MODE_ADDCRAP, "addcrap"},
    {MODE_NORMAL, "normal"},
    {MODE_FAVICON, "favicon.ico"},
    {MODE_MALFORMED, "malformed"},
    {MODE_INVALID, NULL}
};

/**
 * write the response with random behaviour !
 */
void send_response(int c, const char * buffer, size_t len)
{
    ssize_t n;
    while(len > 0) {
        n = (rand() % 99) + 1;
        if((size_t)n > len)
            n = len;
        n = write(c, buffer, n);
        if(n < 0) {
            if(errno != EINTR) {
                perror("write");
                return;
            }
            /* if errno == EINTR, try again */
        } else {
            len -= n;
            buffer += n;
            usleep(10000); /* 10ms */
        }
    }
}

/**
 * handle the HTTP connection
 */
void handle_http_connection(int c)
{
    char request_buffer[2048];
    size_t request_len = 0;
    int headers_found = 0;
    ssize_t n, m;
    size_t i;
    char request_method[16];
    char request_uri[256];
    char http_version[16];
    char * p;
    char * response_buffer;
    size_t response_len;
    enum modes mode;
    size_t content_length = 16*1024;

    /* read the request */
    while(request_len < sizeof(request_buffer) && !headers_found) {
        n = read(c,
                 request_buffer + request_len,
                 sizeof(request_buffer) - request_len);
        if(n < 0) {
            if(errno == EINTR)
                continue;
            perror("read");
            return;
        } else if(n==0) {
            /* remote host closed the connection */
            break;
        } else {
            request_len += n;
            for(i = 0; i < request_len - 3; i++) {
                if(0 == memcmp(request_buffer + i, "\r\n\r\n", 4)) {
                    /* found the end of headers */
                    headers_found = 1;
                    break;
                }
            }
        }
    }
    if(!headers_found) {
        /* error */
        printf("no HTTP header found in the request\n");
        return;
    }
    printf("headers :\n%.*s", (int)request_len, request_buffer);
    /* the request have been received, now parse the request line */
    p = request_buffer;
    for(i = 0; i < sizeof(request_method) - 1; i++) {
        if(*p == ' ' || *p == '\r')
            break;
        request_method[i] = *p;
        ++p;
    }
    request_method[i] = '\0';
    while(*p == ' ')
        p++;
    for(i = 0; i < (int)sizeof(request_uri) - 1; i++) {
        if(*p == ' ' || *p == '\r')
            break;
        request_uri[i] = *p;
        ++p;
    }
    request_uri[i] = '\0';
    while(*p == ' ')
        p++;
    for(i = 0; i < (int)sizeof(http_version) - 1; i++) {
        if(*p == ' ' || *p == '\r')
            break;
        http_version[i] = *p;
        ++p;
    }
    http_version[i] = '\0';
    printf("Method = %s, URI = %s, %s\n",
           request_method, request_uri, http_version);
    /* check if the request method is allowed */
    if(0 != strcmp(request_method, "GET")) {
        const char response405[] = "HTTP/1.1 405 Method Not Allowed\r\n"
                                   "Allow: GET\r\n\r\n";
        const char * pc;
        /* 405 Method Not Allowed */
        /* The response MUST include an Allow header containing a list
         * of valid methods for the requested resource. */
        n = sizeof(response405) - 1;
        pc = response405;
        while(n > 0) {
            m = write(c, pc, n);
            if(m<0) {
                if(errno != EINTR) {
                    perror("write");
                    return;
                }
            } else {
                n -= m;
                pc += m;
            }
        }
        return;
    }

    mode = MODE_INVALID;
    /* use the request URI to know what to do */
    for(i = 0; modes_array[i].mode != MODE_INVALID; i++) {
        if(strstr(request_uri, modes_array[i].text)) {
            mode = modes_array[i].mode; /* found */
            break;
        }
    }

    switch(mode) {
    case MODE_MALFORMED:
        response_len = 2048;
        response_buffer = malloc(response_len);
        if(!response_buffer)
            break;
        n = snprintf(response_buffer, response_len,
                     "HTTP/1.1 \r\n"
                     "\r\n"
                     /*"0000\r\n"*/);
        for (i = n; i < response_len; i++) {
            response_buffer[i] = ' ';
        }
        response_len = n;
        break;
    case MODE_CHUNKED:
        response_buffer = build_chunked_response(content_length, &response_len);
        break;
    case MODE_ADDCRAP:
        response_len = content_length+256;
        response_buffer = malloc(response_len);
        if(!response_buffer)
            break;
        n = snprintf(response_buffer, response_len,
                     "HTTP/1.1 200 OK\r\n"
                     "Server: minihttptestserver\r\n"
                     "Content-Type: text/plain\r\n"
                     "Content-Length: %lu\r\n"
                     "\r\n", content_length);
        response_len = content_length+n+CRAP_LENGTH;
        p = realloc(response_buffer, response_len);
        if(p == NULL) {
            /* error 500 */
            free(response_buffer);
            response_buffer = NULL;
            break;
        }
        response_buffer = p;
        build_content(response_buffer + n, content_length);
        build_crap(response_buffer + n + content_length, CRAP_LENGTH);
        break;
    case MODE_FAVICON:
        content_length = FAVICON_LENGTH;
        response_len = content_length + 256;
        response_buffer = malloc(response_len);
        if(!response_buffer)
            break;
        n = snprintf(response_buffer, response_len,
                     "HTTP/1.1 200 OK\r\n"
                     "Server: minihttptestserver\r\n"
                     "Content-Type: image/vnd.microsoft.icon\r\n"
                     "Content-Length: %lu\r\n"
                     "\r\n", content_length);
        /* image/x-icon */
        build_favicon_content((unsigned char *)(response_buffer + n), content_length);
        response_len = content_length + n;
        break;
    default:
        response_len = content_length+256;
        response_buffer = malloc(response_len);
        if(!response_buffer)
            break;
        n = snprintf(response_buffer, response_len,
                     "HTTP/1.1 200 OK\r\n"
                     "Server: minihttptestserver\r\n"
                     "Content-Type: text/plain\r\n"
                     "\r\n");
        response_len = content_length+n;
        p = realloc(response_buffer, response_len);
        if(p == NULL) {
            /* Error 500 */
            free(response_buffer);
            response_buffer = NULL;
            break;
        }
        response_buffer = p;
        build_content(response_buffer + n, response_len - n);
    }

    if(response_buffer) {
        send_response(c, response_buffer, response_len);
        free(response_buffer);
    } else {
        /* Error 500 */
    }
}

/**
 */
int main(int argc, char * * argv) {
    int ipv6 = 0;
    int r, i;
    unsigned short port = 0;
    const char * expected_file_name = NULL;

    for(i = 1; i < argc; i++) {
        if(argv[i][0] == '-') {
            switch(argv[i][1]) {
            case '6':
                ipv6 = 1;
                break;
            case 'e':
                /* write expected file ! */
                expected_file_name = argv[++i];
                break;
            case 'p':
                /* port */
                if(++i < argc) {
                    port = (unsigned short)atoi(argv[i]);
                }
                break;
            default:
                fprintf(stderr, "unknown command line switch '%s'\n", argv[i]);
            }
        } else {
            fprintf(stderr, "unknown command line argument '%s'\n", argv[i]);
        }
    }

    srand(time(NULL));

    r = server(port, expected_file_name, ipv6);
    if(r != 0) {
        printf("*** ERROR ***\n");
    }
    return r;
}

static int server(unsigned short port, const char * expected_file_name, int ipv6)
{
    int s, c;
    int i;
    struct sockaddr_storage server_addr;
    socklen_t server_addrlen;
    struct sockaddr_storage client_addr;
    socklen_t client_addrlen;
    pid_t pid;
    int child = 0;
    int status;
    struct sigaction sa;

    memset(&sa, 0, sizeof(struct sigaction));

    /*signal(SIGCHLD, handle_signal_chld);*/
    sa.sa_handler = handle_signal_chld;
    if(sigaction(SIGCHLD, &sa, NULL) < 0) {
        perror("sigaction");
        return 1;
    }
    /*signal(SIGINT, handle_signal_int);*/
    sa.sa_handler = handle_signal_int;
    if(sigaction(SIGINT, &sa, NULL) < 0) {
        perror("sigaction");
        return 1;
    }

    s = socket(ipv6 ? AF_INET6 : AF_INET, SOCK_STREAM, 0);
    if(s < 0) {
        perror("socket");
        return 1;
    }
    memset(&server_addr, 0, sizeof(struct sockaddr_storage));
    memset(&client_addr, 0, sizeof(struct sockaddr_storage));
    if(ipv6) {
        struct sockaddr_in6 * addr = (struct sockaddr_in6 *)&server_addr;
        addr->sin6_family = AF_INET6;
        addr->sin6_port = htons(port);
        addr->sin6_addr = in6addr_loopback;
    } else {
        struct sockaddr_in * addr = (struct sockaddr_in *)&server_addr;
        addr->sin_family = AF_INET;
        addr->sin_port = htons(port);
        addr->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    }
    if(bind(s, (struct sockaddr *)&server_addr,
            ipv6 ? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in)) < 0) {
        perror("bind");
        return 1;
    }
    if(listen(s, 5) < 0) {
        perror("listen");
    }
    if(port == 0) {
        server_addrlen = sizeof(struct sockaddr_storage);
        if(getsockname(s, (struct sockaddr *)&server_addr, &server_addrlen) < 0) {
            perror("getsockname");
            return 1;
        }
        if(ipv6) {
            struct sockaddr_in6 * addr = (struct sockaddr_in6 *)&server_addr;
            port = ntohs(addr->sin6_port);
        } else {
            struct sockaddr_in * addr = (struct sockaddr_in *)&server_addr;
            port = ntohs(addr->sin_port);
        }
        printf("Listening on port %hu\n", port);
        fflush(stdout);
    }

    /* write expected file */
    if(expected_file_name) {
        FILE * f;
        f = fopen(expected_file_name, "wb");
        if(f) {
            char * buffer;
            buffer = malloc(16*1024);
            if(buffer == NULL) {
                fprintf(stderr, "memory allocation error\n");
            } else {
                build_content(buffer, 16*1024);
                i = fwrite(buffer, 1, 16*1024, f);
                if(i != 16*1024) {
                    fprintf(stderr, "error writing to file %s : %dbytes written (out of %d)\n", expected_file_name, i, 16*1024);
                }
                free(buffer);
            }
            fclose(f);
        } else {
            fprintf(stderr, "error opening file %s for writing\n", expected_file_name);
        }
    }

    /* fork() loop */
    while(!child && !quit) {
        while(child_to_wait_for > 0) {
            pid = wait(&status);
            if(pid < 0) {
                perror("wait");
            } else {
                printf("child(%d) terminated with status %d\n", (int)pid, status);
            }
            --child_to_wait_for;
        }
        client_addrlen = sizeof(struct sockaddr_storage);
        c = accept(s, (struct sockaddr *)&client_addr,
                   &client_addrlen);
        if(c < 0) {
            if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
                continue;
            perror("accept");
            return 1;
        }
        printf("accept...\n");
        pid = fork();
        if(pid < 0) {
            perror("fork");
            return 1;
        } else if(pid == 0) {
            /* child */
            child = 1;
            close(s);
            s = -1;
            handle_http_connection(c);
        }
        close(c);
    }
    if(s >= 0) {
        close(s);
        s = -1;
    }
    if(!child) {
        while(child_to_wait_for > 0) {
            pid = wait(&status);
            if(pid < 0) {
                perror("wait");
            } else {
                printf("child(%d) terminated with status %d\n", (int)pid, status);
            }
            --child_to_wait_for;
        }
        printf("Bye...\n");
    }
    return 0;
}

