/* Main HTTP server module. * * IRC Services is copyright (c) 1996-2007 Andrew Church. * E-mail: * Parts written by Andrew Kempe and others. * This program is free but copyrighted software; see the file COPYING for * details. */ #include "services.h" #include "modules.h" #include "conffile.h" #include "timeout.h" #include "http.h" #include #include #include /*************************************************************************/ static Module *module; static int cb_auth = -1; static int cb_request = -1; static int32 ListenBacklog; static int32 RequestBufferSize; static int32 MaxConnections; static int32 MaxRequests; static time_t IdleTimeout; static int LogConnections; /* List of ports to listen to (set by ListenTo configuration directive) */ static struct listento_ { char ip[16]; /* aaa.bbb.ccc.ddd\0 */ uint16 port; } *ListenTo; static int ListenTo_count; #define MAX_LISTENTO 32767 /* Array of listen sockets (corresponding to ListenTo[] entries) */ static Socket **listen_sockets; /* Array of clients */ static Client *clients; int clients_count; /*************************************************************************/ static void do_accept(Socket *listener, void *param); static void do_disconnect(Socket *socket, void *param_unused); static void do_readline(Socket *s, void *param_unused); static void do_readdata(Socket *s, void *param); static void do_timeout(Timeout *t); static Client *find_client(Socket *s); static void set_timeout(Client *c); static void clear_timeout(Client *c); static void parse_header(Client *c, char *linestart); static void handle_request(Client *c); /*************************************************************************/ /*************************** HTTP server core ****************************/ /*************************************************************************/ /* Accept a connection and perform IP address checks on it; if it doesn't * pass, disconnect it. */ static void do_accept(Socket *listener, void *param) { Socket *new = param; struct sockaddr_in sin; int sin_len = sizeof(sin); if (sock_remote(new, (struct sockaddr *)&sin, &sin_len) < 0) { module_log_perror("sock_remote() failed"); } else if (sin_len > sizeof(sin)) { module_log("sock_remote() returned oversize address (%d)", sin_len); } else if (sin.sin_family != AF_INET) { module_log("sock_remote() returned bad address family (%d)", sin.sin_family); } else { int i = clients_count; ARRAY_EXTEND(clients); snprintf(clients[i].address, sizeof(clients[i].address), "%s:%u", unpack_ip((uint8 *)&sin.sin_addr), ntohs(sin.sin_port)); clients[i].socket = new; clients[i].ip = sin.sin_addr.s_addr; clients[i].port = sin.sin_port; clients[i].timeout = NULL; clients[i].request_count = 0; clients[i].in_request = 0; clients[i].request_buf = smalloc(RequestBufferSize); clients[i].request_len = 0; clients[i].version_major = 0; clients[i].version_minor = 0; clients[i].method = -1; clients[i].url = NULL; clients[i].data = NULL; clients[i].data_len = 0; clients[i].headers = NULL; clients[i].headers_count = 0; clients[i].variables = NULL; clients[i].variables_count = 0; if (clients_count >= MaxConnections) { module_log("Dropping connection (exceeded MaxConnections: %d)" " from %s", MaxConnections, clients[i].address); http_error(&clients[i], HTTP_F_SERVICE_UNAVAILABLE, NULL); /* http_error() closes socket for us, so don't try to * disconnect it below */ } else { set_timeout(&clients[i]); sock_setcb(new, SCB_READLINE, do_readline); sock_setcb(new, SCB_DISCONNECT, do_disconnect); sock_set_blocking(new, 1); if (LogConnections) module_log("Accepted connection from %s", clients[i].address); } return; } disconn(new); } /*************************************************************************/ static void do_disconnect(Socket *socket, void *param_unused) { Client *c = find_client(socket); int index = c - clients; #ifdef CLEAN_COMPILE param_unused = param_unused; #endif if (!c) { module_log("BUG: unexpected disconnect callback for socket %p",socket); return; } clear_timeout(c); free(c->headers); free(c->variables); free(c->request_buf); ARRAY_REMOVE(clients, index); } /*************************************************************************/ /* Read a line from a client socket. If the end of the request data is * reached, the request is passed to handle_request(); if the request data * length exceeds the buffer size, an error is sent to the client and the * connection is closed. */ static void do_readline(Socket *socket, void *param_unused) { Client *c = find_client(socket); char line[HTTP_LINEMAX], *linestart, *s, *t; int32 i; #ifdef CLEAN_COMPILE param_unused = param_unused; #endif if (!c) { module_log("BUG: unexpected readline callback for socket %p", socket); disconn(socket); return; } if (!sgets2(line, sizeof(line), socket)) { module_log("BUG: sgets2() failed in readline callback for socket %p", socket); return; } i = strlen(line) + 1; /* include trailing \0 */ if (c->request_len + i > RequestBufferSize) { module_log("%s: Request too large, closing connection", c->address); http_error(c, HTTP_E_REQUEST_ENTITY_TOO_LARGE, NULL); return; } linestart = c->request_buf + c->request_len; memcpy(linestart, line, i); c->request_len += i; if (!c->url) { char *method, *url, *version; if (!*linestart) { /* RFC2616 4.2: servers SHOULD ignore initial empty lines (OK) */ c->request_len = 0; set_timeout(c); return; } method = strtok(linestart, " "); url = strtok(NULL, " "); version = strtok(NULL, " "); if (!method || !url || !version) { /* Note that we don't support REALLY old clients (HTTP 0.9) * which don't send version strings */ /* RFC2616 10.4: SHOULD ensure client has received error before * closing connection (NG) -- in this case the client is so * broken that it doesn't deserve to be worried about */ module_log("%s: Invalid HTTP request", c->address); http_error(c, HTTP_E_BAD_REQUEST, NULL); return; } if (strcmp(method, "GET") == 0) { c->method = METHOD_GET; } else if (strcmp(method, "HEAD") == 0) { c->method = METHOD_HEAD; } else if (strcmp(method, "POST") == 0) { c->method = METHOD_POST; } else { module_log("%s: Unimplemented/unsupported method `%s' requested", c->address, method); http_error(c, HTTP_F_NOT_IMPLEMENTED, NULL); return; } if (strncmp(version, "HTTP/", 5) != 0 || !(s = strchr(version+5,'.'))){ module_log("%s: Bad HTTP version string: %s", c->address, version); http_error(c, HTTP_E_BAD_REQUEST, NULL); return; } *s++ = 0; c->version_major = strtol(version+5, &t, 10); if (!*t) c->version_minor = strtol(s, &t, 10); if (*t) { module_log("%s: Bad HTTP version string: %s.%s", c->address, version, t); http_error(c, HTTP_E_BAD_REQUEST, NULL); return; } if (c->version_major != 1) { module_log("%s: Unsupported HTTP version: %d.%d", c->address, c->version_major, c->version_minor); http_error(c, HTTP_F_HTTP_VER_NOT_SUPPORTED, NULL); return; } if (strnicmp(url, "http://", 7) == 0) { /* RFC2616 5.1.2: MUST accept absolute URIs (OK, but we ignore * the hostname and just pretend it's us) */ s = strchr(url+7, '/'); if (s) { strmove(url, s); } else { url[0] = '/'; url[1] = 0; } } c->url = url; set_timeout(c); return; } /* if (!url) */ /* We already have the URL, so this must be a header or blank line. */ if (*linestart) { /* Header line: process it */ parse_header(c, linestart); set_timeout(c); return; } /* End of headers. For GET/HEAD, handle any query string present in * the URL and process the request immediately. For POST, handle * Expect: 100-continue, then deal with the body. */ if (c->method == METHOD_GET || c->method == METHOD_HEAD) { char *s = strchr(c->url, '?'); if (s) { *s++ = 0; c->data = s; c->data_len = strlen(s); } handle_request(c); } else if (c->method == METHOD_POST) { long length; char *t; s = http_get_header(c, "Content-Length"); if (!s) { module_log("%s: Missing Content-Length header for POST", c->address); http_error(c, HTTP_E_LENGTH_REQUIRED, NULL); return; } errno = 0; length = strtol(s, &t, 10); if (*t) { module_log("%s: Invalid Content-Length header: %s", c->address, s); http_error(c, HTTP_E_BAD_REQUEST, NULL); return; } else if (errno==ERANGE || c->request_len+length>RequestBufferSize) { module_log("%s: Request too large, closing connection",c->address); http_error(c, HTTP_E_REQUEST_ENTITY_TOO_LARGE, NULL); return; } c->data = c->request_buf + c->request_len; c->data_len = (int32)length; if (length > 0) { s = http_get_header(c, "Expect"); for (s = strtok(s, ", \t"); s; s = strtok(NULL, ", \t")) { if (strcmp(s, "100-continue") == 0) { sockprintf(socket, "HTTP/1.1 100 Continue\r\n\r\n"); break; } } /* Set up to read POST data */ sock_setcb(socket, SCB_READ, do_readdata); sock_setcb(socket, SCB_READLINE, NULL); set_timeout(c); } else { /* length == 0: do it just like GET */ handle_request(c); } } else { module_log("BUG: do_readline(): unsupported method %d", c->method); http_error(c, HTTP_F_INTERNAL_SERVER_ERROR, NULL); } } /*************************************************************************/ /* Read data from a client socket. When the end of the data is reached, * reached, the request is passed to handle_request(). The request is * assumed to have already been checked for exceeding the buffer size. */ static void do_readdata(Socket *socket, void *param) { Client *c = find_client(socket); int32 available = (int32)(long)param, needed, nread; if (!c) { module_log("BUG: unexpected readdata callback for socket %p", socket); disconn(socket); return; } needed = c->data_len - (c->request_len - (c->data - c->request_buf)); if (available > needed) available = needed; if (c->request_len + available > RequestBufferSize) { module_log("BUG: do_readdata(%s[%s]): data size exceeded buffer limit", c->address, c->url); http_error(c, HTTP_F_INTERNAL_SERVER_ERROR, NULL); return; } nread = sread(socket, c->request_buf + c->request_len, available); if (nread != available) { module_log("BUG: do_readdata(%s[%s]): nread (%d) != available (%d)", c->address, c->url, nread, available); } c->request_len += nread; needed -= nread; if (needed <= 0) { /* Prepare for next request, if any */ sock_setcb(socket, SCB_READ, NULL); sock_setcb(socket, SCB_READLINE, do_readline); /* Process this request */ handle_request(c); } } /*************************************************************************/ /* Handle an idle timeout on a client. */ static void do_timeout(Timeout *t) { Client *c = find_client(t->data); if (!c) { module_log("BUG: do_timeout(): client not found for timeout %p!", t); return; } c->timeout = NULL; disconn(c->socket); } /*************************************************************************/ /*************************************************************************/ /* Return the client structure corresponding to the given socket, or NULL * if not found. */ static Client *find_client(Socket *s) { int i; ARRAY_FOREACH (i, clients) { if (clients[i].socket == s) return &clients[i]; } return NULL; } /*************************************************************************/ /* Start an idle timer running on the given client. If a timer was already * running, clear it and start a new one. */ static void set_timeout(Client *c) { if (!c->socket) { module_log("BUG: attempt to set timeout for client %d with no" " socket!", (int)(c-clients)); return; } if (IdleTimeout) { clear_timeout(c); c->timeout = add_timeout(IdleTimeout, do_timeout, 0); c->timeout->data = c->socket; } } /*************************************************************************/ /* Cancel the idle timer for the given client. */ static void clear_timeout(Client *c) { if (c->timeout) { del_timeout(c->timeout); c->timeout = NULL; } } /*************************************************************************/ /* Do appropriate things with the given header line. */ static void parse_header(Client *c, char *linestart) { char *s; /* Check for whitespace-started line and no headers yet */ if ((*linestart == ' ' || *linestart == '\t') && !c->headers_count) { http_error(c, HTTP_E_BAD_REQUEST, NULL); return; } /* Remove all trailing whitespace */ s = linestart + strlen(linestart) - 1; while (s > linestart && (*s == ' ' || *s == '\t')) { *s-- = 0; c->request_len--; } if (*linestart == ' ' || *linestart == '\t') { /* If it starts with whitespace, just tack it onto the end of the * previous line (convert all leading whitespace to a single space) */ linestart[-1] = ' '; s = linestart; while (*s == ' ' || *s == '\t') s++; strmove(linestart, s); c->request_len -= s-linestart; } else { /* New header: split into name and value, and create a new * headers[] entry */ int i = c->headers_count; ARRAY_EXTEND(c->headers); c->headers[i] = linestart; s = strchr(linestart, ':'); if (!s) { http_error(c, HTTP_E_BAD_REQUEST, NULL); return; } *s++ = 0; linestart = s; while (*s == ' ' || *s == '\t') s++; strmove(linestart, s); c->request_len -= s - linestart; } } /*************************************************************************/ /* Parse the data given by `buf' (null-terminated) into variables and * create an array of them in c->variables. Assume standard * x-www-form-urlencoding encoding (for multipart data, use * parse_data_multipart() below). */ static void parse_data(Client *c, char *buf) { char *dest = buf; int found_equals = 0; char hexbuf[3]; hexbuf[2] = 0; free(c->variables); c->variables = NULL; c->variables_count = 0; ARRAY_EXTEND(c->variables); c->variables[0] = dest; while (*buf) { switch (*buf) { case '=': if (!found_equals) { *dest++ = 0; found_equals = 1; } break; case '&': *dest++ = 0; ARRAY_EXTEND(c->variables); c->variables[c->variables_count-1] = dest; found_equals = 0; break; case '+': *dest++ = ' '; break; case '%': if (!buf[1]) break; buf++; if (!buf[1]) break; hexbuf[0] = *buf++; hexbuf[1] = *buf; *dest++ = (char)strtol(hexbuf, NULL, 16); break; default: *dest++ = *buf; break; } buf++; } *dest = 0; } /*************************************************************************/ /* Parse the data given by `buf' (null-terminated) into variables and * create an array of them in c->variables, using the boundary string * given by `boundary'. */ static void parse_data_multipart(Client *c, char *buf, const char *boundary) { char *dest = buf; int boundarylen = strlen(boundary); char *varname = NULL; free(c->variables); c->variables = NULL; c->variables_count = 0; buf = strstr(buf, boundary); if (!buf) return; /* boundary string not found */ while (*buf && (buf[boundarylen+2] != '-' || buf[boundarylen+3] != '-')) { char *s; /* Read in header for this part */ s = buf + strcspn(buf, "\r\n"); if (!*s) return; buf = s + strspn(s, "\r") + 1; while (*buf != '\r' && *buf != '\n') { s = buf + strcspn(buf, "\r\n"); if (!*s) return; if (*s == '\r') *s++ = 0; *s++ = 0; if (strnicmp(buf,"Content-Disposition:",20) == 0) { buf += 20; while (*buf && isspace(*buf)) buf++; if (*buf && strnicmp(buf,"form-data;",10) == 0) { buf += 10; while (*buf && isspace(*buf)) buf++; if (*buf && strnicmp(buf,"name=",5) == 0) { buf += 5; if (*buf == '"') { char *t = strchr(++buf, '"'); if (t) *t = 0; } else { char *t = strchr(buf, ';'); if (t) *t = 0; } varname = dest; strmove(dest, buf); dest += strlen(buf)+1; } } } buf = s; } /* while (*buf != '\r' && *buf != '\n') */ if (*buf == '\r') buf++; buf++; /* Read in data (variable contents) */ if (varname) { ARRAY_EXTEND(c->variables); c->variables[c->variables_count-1] = varname; varname = NULL; } s = buf + strcspn(buf, "\r\n"); if (s > buf) { memmove(dest, buf, s-buf); dest += s-buf; } if (*s == '\r') s++; buf = s; /* *buf is always pointing to a \n or \0 at the top of this loop */ while (*buf && (buf[1] != '-' || buf[2] != '-' || strncmp(buf+3, boundary, boundarylen) != 0)) { s = buf+1 + strcspn(buf+1, "\r\n"); if (!s) s = buf + strlen(buf); memmove(dest, buf, s-buf); dest += s-buf; if (*s == '\r') s++; buf = s; } /* Null-terminate variable contents */ *dest++ = 0; /* Skip over newline */ if (*buf) buf++; } /* while not final boundary line */ } /*************************************************************************/ static void handle_request(Client *c) { int res; int close = 0; /* Parse GET query string or POST data into variables */ if (c->data && c->data_len) { char *s; if (c->method == METHOD_POST) { /* There were at least two newlines before the beginning of the * data, so it's safe to move it back a byte (to add a trailing * null) */ memmove(c->data-1, c->data, c->data_len); c->data--; c->data[c->data_len] = 0; } /* Check the content type, and extract the boundary string if it's * multipart data */ s = http_get_header(c, "Content-Type"); if (s && strnicmp(s, "multipart/form-data;", 20) == 0) { s += 20; while (isspace(*s)) s++; if (strnicmp(s, "boundary=", 9) == 0) { s += 9; if (*s == '"') { char *t = strchr(++s, '"'); if (t) *t = 0; } } else { s = NULL; } } else { s = NULL; } /* Parse data into variables */ if (s) parse_data_multipart(c, c->data, s); else parse_data(c, c->data); } c->request_count++; c->in_request = 1; if (c->version_major == 1 && c->version_minor == 0) { close = 1; } else { const char *s = http_get_header(c, "Connection"); if (s && strstr(s, "close")) { /* This might accidentally trigger on something like "abcloseyz", * but that's okay; all it means is the client has to reconnect */ close = 1; } } res = call_callback_2(module, cb_auth, c, &close); if (res < 0) { module_log("handle_request(): call_callback(cb_request) failed"); http_error(c, HTTP_F_INTERNAL_SERVER_ERROR, NULL); close = 1; } else if (res != HTTP_AUTH_DENY) { res = call_callback_2(module, cb_request, c, &close); if (res < 0) { module_log("handle_request(): call_callback(cb_request) failed"); http_error(c, HTTP_F_INTERNAL_SERVER_ERROR, NULL); close = 1; } else if (res == 0) { http_error(c, HTTP_E_NOT_FOUND, NULL); } } if (close || (MaxRequests && c->request_count >= MaxRequests) || c->in_request < 0 /* flag from http_error */ ) { disconn(c->socket); } else { free(c->headers); free(c->variables); c->in_request = 0; c->request_len = 0; c->version_major = 0; c->version_minor = 0; c->method = -1; c->url = NULL; c->data = NULL; c->data_len = 0; c->headers = NULL; c->headers_count = 0; c->variables = NULL; c->variables_count = 0; set_timeout(c); } } /*************************************************************************/ /***************************** Module stuff ******************************/ /*************************************************************************/ const int32 module_version = MODULE_VERSION_CODE; static int do_ListenTo(const char *filename, int linenum, char *param); ConfigDirective module_config[] = { { "IdleTimeout", { { CD_TIME, 0, &IdleTimeout } } }, { "ListenBacklog", { { CD_POSINT, CF_DIRREQ, &ListenBacklog } } }, { "ListenTo", { { CD_FUNC, CF_DIRREQ, do_ListenTo } } }, { "LogConnections", { { CD_SET, 0, &LogConnections } } }, { "MaxConnections", { { CD_POSINT, 0, &MaxConnections } } }, { "MaxRequests", { { CD_POSINT, 0, &MaxRequests } } }, { "RequestBufferSize",{ { CD_POSINT, 0, &RequestBufferSize } } }, { NULL } }; /*************************************************************************/ static int do_ListenTo(const char *filename, int linenum, char *param) { char *s, *t; long port; uint8 *ip; char *ipstr; char ipbuf[15+1]; /* aaa.bbb.ccc.ddd\0 */ int recursing = 0, i; static struct listento_ *new_ListenTo; static int new_ListenTo_count; if (!filename) { /* filename == NULL, perform special operations */ switch (linenum) { case CDFUNC_INIT: /* prepare to read new data */ free(new_ListenTo); new_ListenTo = NULL; new_ListenTo_count = 0; break; case CDFUNC_SET: /* store new data in config variable */ free(ListenTo); ListenTo = new_ListenTo; ListenTo_count = new_ListenTo_count; new_ListenTo = NULL; new_ListenTo_count = 0; break; case CDFUNC_DECONFIG: /* clear any stored data */ free(ListenTo); ListenTo = NULL; ListenTo_count = 0; break; } /* switch (linenum) */ return 1; } /* if (!filename) */ /* filename != NULL, process directive */ if (linenum < 0) { recursing = 1; linenum = -linenum; } if (ListenTo_count >= MAX_LISTENTO) { config_error(filename, linenum, "Too many ListenTo addresses (maximum %d)", MAX_LISTENTO); return 0; } s = strchr(param, ':'); if (!s) { config_error(filename, linenum, "ListenTo address requires both address and port"); return 0; } *s++ = 0; port = strtol(s, &t, 10); if (*t || port <= 0 || port > 65535) { config_error(filename, linenum, "Invalid port number `%s'", s); return 0; } if (strcmp(param, "*") == 0) { /* "*" -> all addresses (NULL string) */ ipstr = NULL; } else if ((ip = pack_ip(param)) != NULL) { /* IP address -> normalize (no leading zeros, etc.) */ snprintf(ipbuf, sizeof(ipbuf), "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]); if (strlen(ipbuf) > 15) { config_error(filename, linenum, "BUG: strlen(ipbuf) > 15 [%s]", ipbuf); return 0; } ipstr = ipbuf; } else { /* hostname -> check for double recursion, then look up and * recursively add addresses */ #ifdef HAVE_GETHOSTBYNAME struct hostent *hp; #endif if (recursing) { config_error(filename, linenum, "BUG: double recursion (param=%s)", param); return 0; } #ifdef HAVE_GETHOSTBYNAME if ((hp = gethostbyname(param)) != NULL) { if (hp->h_addrtype == AF_INET) { for (i = 0; hp->h_addr_list[i]; i++) { ip = (uint8 *)hp->h_addr_list[i]; snprintf(ipbuf, sizeof(ipbuf), "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]); if (strlen(ipbuf) > 15) { config_error(filename, linenum, "BUG: strlen(ipbuf) > 15 [%s]", ipbuf); return 0; } if (!do_ListenTo(filename, -linenum, ipbuf)) return 0; } return 1; /* Success */ } else { config_error(filename, linenum, "%s: no IPv4 addresses found", param); } } else { config_error(filename, linenum, "%s: %s", param, hstrerror(h_errno)); } #else config_error(filename, linenum, "gethostbyname() not available, hostnames may not be" " used"); #endif return 0; } i = new_ListenTo_count; ARRAY_EXTEND(new_ListenTo); if (ipstr) strcpy(new_ListenTo[i].ip, ipstr);/*safe: strlen(ip)<16 checked above*/ else memset(new_ListenTo[i].ip, 0, sizeof(new_ListenTo[i].ip)); new_ListenTo[i].port = port; return 1; } /*************************************************************************/ int init_module(Module *module_) { int i, opencount; module = module_; init_http_util(module); cb_auth = register_callback(module, "auth"); cb_request = register_callback(module, "request"); if (cb_auth < 0 || cb_request < 0) { module_log("Unable to register callbacks"); exit_module(0); return 0; } listen_sockets = smalloc(sizeof(*listen_sockets) * ListenTo_count); opencount = 0; ARRAY_FOREACH (i, ListenTo) { listen_sockets[i] = sock_new(); if (listen_sockets[i]) { if (open_listener(listen_sockets[i], *ListenTo[i].ip ? ListenTo[i].ip : NULL, ListenTo[i].port, ListenBacklog) == 0) { sock_setcb(listen_sockets[i], SCB_ACCEPT, do_accept); module_log("Listening on %s:%u", ListenTo[i].ip, ListenTo[i].port); opencount++; } else { module_log_perror("Failed to open listen socket for %s:%u", ListenTo[i].ip, ListenTo[i].port); } } else { module_log("Failed to create listen socket for %s:%u", *ListenTo[i].ip ? ListenTo[i].ip : "*", ListenTo[i].port); } } if (!opencount) { module_log("No ports could be opened, aborting"); return 0; } return 1; } /*************************************************************************/ int exit_module(int shutdown_unused) { int i; #ifdef CLEAN_COMPILE shutdown_unused = shutdown_unused; #endif ARRAY_FOREACH (i, ListenTo) { if (listen_sockets[i]) { close_listener(listen_sockets[i]); sock_free(listen_sockets[i]); } } free(ListenTo); ListenTo = NULL; ListenTo_count = 0; free(listen_sockets); listen_sockets = NULL; unregister_callback(module, cb_request); unregister_callback(module, cb_auth); return 1; } /*************************************************************************/