You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

629 lines
14 KiB
C

#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;
}