#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <syslog.h>
#include <time.h>
#include <ctype.h>
#include <pwd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include "httpd.h"

/* ---------------------------------------------------------------------- */

void
read_request(struct REQUEST *req, int pipelined)
{
    int             rc;
    char            *h;

 restart:
#ifdef USE_SSL
    if (with_ssl)
	rc = ssl_read(req, req->hreq + req->hdata, MAX_HEADER - req->hdata);
    else
#endif
	rc = read(req->fd, req->hreq + req->hdata, MAX_HEADER - req->hdata);
    switch (rc) {
    case -1:
	if (errno == EAGAIN) {
	    if (pipelined)
		break; /* check if there is already a full request */
	    else
		return;
	}
	if (errno == EINTR)
	    goto restart;
	xperror(LOG_INFO,"read",req->peerhost);
	/* fall through */
    case 0:
	req->state = STATE_CLOSE;
	return;
    default:
	req->hdata += rc;
	req->hreq[req->hdata] = 0;
    }

    /* check if this looks like a http request after
       the first few bytes... */
    if (req->hdata < 5)
	return;
    if (strncmp(req->hreq,"GET ",4)  != 0  &&
	strncmp(req->hreq,"PUT ",4)  != 0  &&
	strncmp(req->hreq,"HEAD ",5) != 0  &&
	strncmp(req->hreq,"POST ",5) != 0) {
	mkerror(req,400,0);
	return;
    }
    
    /* header complete ?? */
    if (NULL != (h = strstr(req->hreq,"\r\n\r\n")) ||
	NULL != (h = strstr(req->hreq,"\n\n"))) {
	if (*h == '\r') {
	    h += 4;
	    *(h-2) = 0;
	} else {
	    h += 2;
	    *(h-1) = 0;
	}
	req->lreq  = h - req->hreq;
	req->state = STATE_PARSE_HEADER;
	return;
    }

    if (req->hdata == MAX_HEADER) {
	/* oops: buffer full, but found no complete request ... */
	mkerror(req,400,0);
	return;
    }
    return;
}

/* ---------------------------------------------------------------------- */

#if 0
static time_t
parse_date(char *line)
{
    static char *m[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
			 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
    char month[4];
    struct tm tm;
    int i;

    line = strchr(line,' '); /* skip weekday */
    if (NULL == line)
	return -1;
    line++;

    /* first: RFC 1123 date ... */
    if (6 != sscanf(line,"%2d %3s %4d %2d:%2d:%2d GMT",
		    &tm.tm_mday,month,&tm.tm_year,
		    &tm.tm_hour,&tm.tm_min,&tm.tm_sec))
	/* second: RFC 1036 date ... */
	if (6 != sscanf(line,"%2d-%3s-%2d %2d:%2d:%2d GMT",
			&tm.tm_mday,month,&tm.tm_year,
			&tm.tm_hour,&tm.tm_min,&tm.tm_sec))
	    /* third: asctime() format */
	    if (6 != sscanf(line,"%3s %2d %2d:%2d:%2d %4d",
			    month,&tm.tm_mday,
			    &tm.tm_hour,&tm.tm_min,&tm.tm_sec,
			    &tm.tm_year))
		/* none worked :-( */
		return -1;
    for (i = 0; i <= 11; i++)
	if (0 == strcmp(month,m[i]))
	    break;
    tm.tm_mon = i;
    if (tm.tm_year > 1900)
	tm.tm_year -= 1900;

    return mktime(&tm);
}
#endif

static off_t
parse_off_t(char *str, int *pos)
{
    off_t value = 0;

    while (isdigit(str[*pos])) {
	value *= 10;
	value += str[*pos] - '0';
	(*pos)++;
    }
    return value;
}

static int
parse_ranges(struct REQUEST *req)
{
    char *h,*line = req->range_hdr;
    int  i,off;

    for (h = line, req->ranges=1; *h != '\n' && *h != '\0'; h++)
	if (*h == ',')
	    req->ranges++;
    if (debug)
	fprintf(stderr,"%03d: %d ranges:",req->fd,req->ranges);
    req->r_start = malloc(req->ranges*sizeof(off_t));
    req->r_end   = malloc(req->ranges*sizeof(off_t));
    req->r_head  = malloc((req->ranges+1)*BR_HEADER);
    req->r_hlen  = malloc((req->ranges+1)*sizeof(int));
    if (NULL == req->r_start || NULL == req->r_end ||
	NULL == req->r_head  || NULL == req->r_hlen) {
	if (req->r_start) free(req->r_start);
	if (req->r_end)   free(req->r_end);
	if (req->r_head)  free(req->r_head);
	if (req->r_hlen)  free(req->r_hlen);
	if (debug)
	    fprintf(stderr,"oom\n");
	return 500;
    }
    for (i = 0, off=0; i < req->ranges; i++) {
	if (line[off] == '-') {
	    off++;
	    if (!isdigit(line[off]))
		goto parse_error;
	    req->r_start[i] = req->bst.st_size - parse_off_t(line,&off);
	    req->r_end[i]   = req->bst.st_size;
	} else {
	    if (!isdigit(line[off]))
		goto parse_error;
	    req->r_start[i] = parse_off_t(line,&off);
	    if (line[off] != '-')
		goto parse_error;
	    off++;
	    if (isdigit(line[off]))
		req->r_end[i] = parse_off_t(line,&off) +1;
	    else
		req->r_end[i] = req->bst.st_size;
	}
	off++; /* skip "," */
	/* ranges ok? */
	if (debug)
	    fprintf(stderr," %d-%d",
		    (int)(req->r_start[i]),
		    (int)(req->r_end[i]));
	if (req->r_start[i] > req->r_end[i] ||
	    req->r_end[i]   > req->bst.st_size)
	    goto parse_error;
    }
    if (debug)
	fprintf(stderr," ok\n");
    return 0;

 parse_error:
    req->ranges = 0;
    if (debug)
	fprintf(stderr," range error\n");
    return 400;
}

static int
unhex(unsigned char c)
{
    if (c < '@')
	return c - '0';
    return (c & 0x0f) + 9;
}

/* handle %hex quoting, also split path / querystring */
static void
unquote(unsigned char *path, unsigned char *qs, unsigned char *src)
{
    int q;
    unsigned char *dst;

    q=0;
    dst = path;
    while (src[0] != 0) {
	if (!q && *src == '?') {
	    q = 1;
	    *dst = 0;
	    dst = qs;
	    src++;
	    continue;
	}
	if (q && *src == '+') {
	    *dst = ' ';
	} else if ((*src == '%') && isxdigit(src[1]) && isxdigit(src[2])) {
	    *dst = (unhex(src[1]) << 4) | unhex(src[2]);
	    src += 2;
	} else {
	    *dst = *src;
	}
	dst++;
	src++;
    }
    *dst = 0;
}

/* delete unneeded path elements */
static void
fixpath(char *path)
{
    char *dst = path;
    char *src = path;

    for (;*src;) {
	if (0 == strncmp(src,"//",2)) {
	    src++;
	    continue;
	}
	if (0 == strncmp(src,"/./",3)) {
	    src+=2;
	    continue;
	}
	*(dst++) = *(src++);
    }
    *dst = 0;
}

static int base64_table[] = {
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, 
    -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, 
    -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, 
};

static void
decode_base64(unsigned char *dest, unsigned char *src, int maxlen)
{
    int a,b,d;

    for (a=0, b=0, d=0; *src != 0 && d < maxlen; src++) {
	if (*src >= 128 || -1 == base64_table[*src])
	    break;
	a = (a<<6) | base64_table[*src];
	b += 6;
	if (b >= 8) {
	    b -= 8;
	    dest[d++] = (a >> b) & 0xff;
	}
    }
    dest[d] = 0;
}

static int sanity_checks(struct REQUEST *req)
{
    int i;
		
    /* path: must start with a '/' */
    if (req->path[0] != '/') {
	mkerror(req,400,0);
	return -1;
    }

    /* path: must not contain "/../" */
    if (strstr(req->path,"/../")) {
	mkerror(req,403,1);
	return -1;
    }

    if (req->hostname[0] == '\0')
	/* no hostname specified */
	return 0;

    /* validate hostname */
    for (i = 0; req->hostname[i] != '\0'; i++) {
	switch (req->hostname[i]) {
	case 'A' ... 'Z':
	    req->hostname[i] += 32; /* lowercase */
	case 'a' ... 'z':
	case '0' ... '9':
	case '-':
	    /* these are fine as-is */
	    break;
	case '.':
	    /* some extra checks */
	    if (0 == i) {
		/* don't allow a dot as first character */
		mkerror(req,400,0);
		return -1;
	    }
	    if ('.' == req->hostname[i-1]) {
		/* don't allow two dots in sequence */
		mkerror(req,400,0);
		return -1;
	    }
	    break;
	default:
	    /* invalid character */
	    mkerror(req,400,0);
	    return -1;
	}
    }
    return 0;
}

void
parse_request(struct REQUEST *req)
{
    char filename[MAX_PATH+1], proto[MAX_MISC+1], *h;
    int  port, rc, len;
    struct passwd *pw=NULL;
    
    if (debug > 2)
	fprintf(stderr,"%s\n",req->hreq);

    /* parse request. Hehe, scanf is powerfull :-) */
    if (4 != sscanf(req->hreq,
		    "%" S(MAX_MISC) "[A-Z] "
		    "%" S(MAX_PATH) "[^ \t\r\n] HTTP/%d.%d",
		    req->type, filename, &(req->major),&(req->minor))) {
	mkerror(req,400,0);
	return;
    }
    if (filename[0] == '/') {
	strncpy(req->uri,filename,sizeof(req->uri)-1);
    } else {
	port = 0;
	*proto = 0;
	if (4 != sscanf(filename,
			"%" S(MAX_MISC) "[a-zA-Z]://"
			"%" S(MAX_HOST) "[a-zA-Z0-9.-]:%d"
			"%" S(MAX_PATH) "[^ \t\r\n]",
			proto, req->hostname, &port, req->uri) &&
	    3 != sscanf(filename,
			"%" S(MAX_MISC) "[a-zA-Z]://"
			"%" S(MAX_HOST) "[a-zA-Z0-9.-]"
			"%" S(MAX_PATH) "[^ \t\r\n]",
			proto, req->hostname, req->uri)) {
	    mkerror(req,400,0);
	    return;
	}
	if (*proto != 0 && 0 != strcasecmp(proto,"http")) {
	    mkerror(req,400,0);
	    return;
	}
    }

    unquote(req->path,req->query,req->uri);
    fixpath(req->path);
    if (debug)
	fprintf(stderr,"%03d: %s \"%s\" HTTP/%d.%d\n",
		req->fd, req->type, req->path, req->major, req->minor);

    if (0 != strcmp(req->type,"GET") &&
	0 != strcmp(req->type,"HEAD")) {
	mkerror(req,501,0);
	return;
    }

    if (0 == strcmp(req->type,"HEAD")) {
	req->head_only = 1;
    }

    /* parse header lines */
    req->keep_alive = req->minor;
    for (h = req->hreq; h - req->hreq < req->lreq;) {
	h = strchr(h,'\n');
	if (NULL == h)
	    break;
	h++;

	h[-2] = 0;
	h[-1] = 0;
	list_add(&req->header,h,0);

	if (0 == strncasecmp(h,"Connection: ",12)) {
	    req->keep_alive = (0 == strncasecmp(h+12,"Keep-Alive",10));

	} else if (0 == strncasecmp(h,"Host: ",6)) {
	    if (2 != sscanf(h+6,"%" S(MAX_HOST) "[a-zA-Z0-9.-]:%d",
			    req->hostname,&port))
		sscanf(h+6,"%" S(MAX_HOST) "[a-zA-Z0-9.-]",
		       req->hostname);

	} else if (0 == strncasecmp(h,"If-Modified-Since: ",19)) {
	    req->if_modified = h+19;

	} else if (0 == strncasecmp(h,"If-Unmodified-Since: ",21)) {
	    req->if_unmodified = h+21;

	} else if (0 == strncasecmp(h,"If-Range: ",10)) {
	    req->if_range = h+10;

	} else if (0 == strncasecmp(h,"Authorization: Basic ",21)) {
	    decode_base64(req->auth,h+21,sizeof(req->auth)-1);
	    if (debug)
		fprintf(stderr,"%03d: auth: %s\n",req->fd,req->auth);
	    
	} else if (0 == strncasecmp(h,"Range: bytes=",13)) {
	    /* parsing must be done after fstat, we need the file size
	       for the boundary checks */
	    req->range_hdr = h+13;
	}
    }
    if (debug) {
	if (req->if_modified)
	    fprintf(stderr,"%03d: if-modified-since: \"%s\"\n",
		    req->fd, req->if_modified);
	if (req->if_unmodified)
	    fprintf(stderr,"%03d: if-unmodified-since: \"%s\"\n",
		    req->fd, req->if_unmodified);
	if (req->if_range)
	    fprintf(stderr,"%03d: if-range: \"%s\"\n",
		    req->fd, req->if_range);
    }

    /* take care about the hostname */
    if (virtualhosts) {
	if (req->hostname[0] == 0) {
	    if (req->minor > 0) {
		/* HTTP/1.1 clients MUST specify a hostname */
		mkerror(req,400,0);
		return;
	    }
	    strncpy(req->hostname,server_host,sizeof(req->hostname)-1);
	}
    } else {
	if (req->hostname[0] == '\0' || canonicalhost)
	    strncpy(req->hostname,server_host,sizeof(req->hostname)-1);
    }

    /* checks */
    if (0 != sanity_checks(req))
	return;

    /* check basic auth */
    if (NULL != userpass && 0 != strcmp(userpass,req->auth)) {
	mkerror(req,401,1);
	return;
    }

    /* is CGI ? */
    if (NULL != cgipath &&
	0 == strncmp(req->path,cgipath,strlen(cgipath))) {
	cgi_request(req);
	return;
    }

    /* build filename */
    if (userdir  &&  '~' == req->path[1]) {
	/* expand user directories, i.e.
	   /~user/path/file => $HOME/public_html/path/file */
	h = strchr(req->path+2,'/');
	if (NULL == h) {
	    mkerror(req,404,1);
	    return;
	}
	*h = 0;
	pw = getpwnam(req->path+2);
	*h = '/';
	if (NULL == pw) {
	    mkerror(req,404,1);
	    return;
	}
	len = snprintf(filename, sizeof(filename)-1,
		       "%s/%s/%s", pw->pw_dir, userdir, h+1);
    } else {
	len = snprintf(filename, sizeof(filename)-1,
		       "%s%s%s%s",
		       do_chroot ? "" : doc_root,
		       virtualhosts ? "/" : "",
		       virtualhosts ? req->hostname : "",
		       req->path);
    }

    h = filename +len -1;
    if (*h == '/') {
	/* looks like the client asks for a directory */
	if (indexhtml) {
	    /* check for index file */
	    strncpy(h+1, indexhtml, sizeof(filename) -len -1);
	    if (-1 != (req->bfd = open(filename,O_RDONLY))) {
		/* ok, we have one */
	    	close_on_exec(req->bfd);
		goto regular_file;
	    } else {
		if (errno == ENOENT) {
		    /* no such file or directory => listing */
		    h[1] = '\0';
		} else {
		    mkerror(req,403,1);
		    return;
		}
	    }
	}

	if (no_listing) {
	    mkerror(req,403,1);
	    return;
	};
	
	if (-1 == stat(filename,&(req->bst))) {
	    if (errno == EACCES) {
		mkerror(req,403,1);
	    } else {
		mkerror(req,404,1);
	    }
	    return;
	}
	strftime(req->mtime, sizeof(req->mtime), RFC1123, gmtime(&req->bst.st_mtime));
	req->mime = "text/html";
	req->dir = get_dir(req,filename);
	if (NULL == req->body) {
	    /* We arrive here if opendir failed, probably due to -EPERM
	     * It does exist (see the stat() call above) */
	    mkerror(req,403,1);
	    return;
	} else if (NULL != req->if_modified &&
		   0 == strcmp(req->if_modified, req->mtime)) {
	    /* 304 not modified */
	    mkheader(req,304);
	    req->head_only = 1;
	} else {
	    /* 200 OK */
	    mkheader(req,200);
	}
	return;
    }

    /* it is /probably/ a regular file */
    if (-1 == (req->bfd = open(filename,O_RDONLY))) {
	if (errno == EACCES) {
	    mkerror(req,403,1);
	} else {
	    mkerror(req,404,1);
	}
	return;
    }

 regular_file:
    fstat(req->bfd,&(req->bst));
    if (req->range_hdr)
	if (0 != (rc = parse_ranges(req))) {
	    mkerror(req,rc,1);
	    return;
	}

    if (!S_ISREG(req->bst.st_mode)) {
	/* /not/ a regular file */
	close(req->bfd);
	req->bfd = -1;
	if (S_ISDIR(req->bst.st_mode)) {
	    /* oops: a directory without trailing slash */
	    strcat(req->path,"/");
	    mkredirect(req);
	} else {
	    /* anything else is'nt allowed here */
	    mkerror(req,403,1);
	}
	return;
    }

    /* it is /really/ a regular file */
    req->mime = get_mime(filename);
    strftime(req->mtime, sizeof(req->mtime), RFC1123, gmtime(&req->bst.st_mtime));
    if (NULL != req->if_range  &&  0 != strcmp(req->if_range, req->mtime))
	/* mtime mismatch -> no ranges */
	req->ranges = 0;
    if (NULL != req->if_unmodified && 0 != strcmp(req->if_unmodified, req->mtime)) {
	/* 412 precondition failed */
	mkerror(req,412,1);
    } else if (NULL != req->if_modified && 0 == strcmp(req->if_modified, req->mtime)) {
	/* 304 not modified */
	mkheader(req,304);
	req->head_only = 1;
    } else if (req->ranges > 0) {
	/* send byte range(s) */
	mkheader(req,206);
    } else {
	/* normal */
	mkheader(req,200);
    }
    return;
}
