/* darkstat 3 * copyright (c) 2001-2008 Emil Mikulic. * * http.c: embedded webserver. * This borrows a lot of code from darkhttpd. * * You may use, modify and redistribute this file under the terms of the * GNU General Public License version 2. (see COPYING.GPL) */ #include "darkstat.h" #include "http.h" #include "conv.h" #include "hosts_db.h" #include "graph_db.h" #include "err.h" #include "queue.h" #include "now.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const char mime_type_xml[] = "text/xml"; static const char mime_type_html[] = "text/html; charset=us-ascii"; static const char mime_type_css[] = "text/css"; static const char mime_type_js[] = "text/javascript"; static const char encoding_gzip[] = "Vary: Accept-Encoding\r\n" "Content-Encoding: gzip\r\n"; static const char server[] = PACKAGE_NAME "/" PACKAGE_VERSION; static int idletime = 60; static int sockin = -1; /* socket to accept connections from */ #define MAX_REQUEST_LENGTH 4000 #ifndef min #define min(a,b) (((a) < (b)) ? (a) : (b)) #endif struct connection { LIST_ENTRY(connection) entries; int socket; in_addr_t client; time_t last_active; enum { RECV_REQUEST, /* receiving request */ SEND_HEADER_AND_REPLY, /* try to send header+reply together */ SEND_HEADER, /* sending generated header */ SEND_REPLY, /* sending reply */ DONE /* conn closed, need to remove from queue */ } state; /* char request[request_length+1] is null-terminated */ char *request; size_t request_length; int accept_gzip; /* request fields */ char *method, *uri, *query; /* query can be NULL */ char *header; const char *mime_type, *encoding, *header_extra; size_t header_length, header_sent; int header_dont_free, header_only, http_code; char *reply; int reply_dont_free; size_t reply_length, reply_sent; unsigned int total_sent; /* header + body = total, for logging */ }; static LIST_HEAD(conn_list_head, connection) connlist = LIST_HEAD_INITIALIZER(conn_list_head); /* --------------------------------------------------------------------------- * Decode URL by converting %XX (where XX are hexadecimal digits) to the * character it represents. Don't forget to free the return value. */ static char *urldecode(const char *url) { size_t i, len = strlen(url); char *out = xmalloc(len+1); int pos; for (i=0, pos=0; i= 'A' && (hex) <= 'F') ? ((hex)-'A'+10): \ ((hex) >= 'a' && (hex) <= 'f') ? ((hex)-'a'+10): \ ((hex)-'0') ) out[pos++] = HEX_TO_DIGIT(url[i+1]) * 16 + HEX_TO_DIGIT(url[i+2]); i += 2; #undef HEX_TO_DIGIT } else { /* straight copy */ out[pos++] = url[i]; } } out[pos] = 0; #if 0 /* don't really need to realloc here - it's probably a performance hit */ out = xrealloc(out, strlen(out)+1); /* dealloc what we don't need */ #endif return (out); } /* --------------------------------------------------------------------------- * Consolidate slashes in-place by shifting parts of the string over repeated * slashes. */ static void consolidate_slashes(char *s) { size_t left = 0, right = 0; int saw_slash = 0; assert(s != NULL); while (s[right] != '\0') { if (saw_slash) { if (s[right] == '/') right++; else { saw_slash = 0; s[left++] = s[right++]; } } else { if (s[right] == '/') saw_slash++; s[left++] = s[right++]; } } s[left] = '\0'; } /* --------------------------------------------------------------------------- * Resolve /./ and /../ in a URI, returing a new, safe URI, or NULL if the URI * is invalid/unsafe. Returned buffer needs to be deallocated. */ static char *make_safe_uri(char *uri) { char **elem, *out; unsigned int slashes = 0, elements = 0; size_t urilen, i, j, pos; assert(uri != NULL); if (uri[0] != '/') return (NULL); consolidate_slashes(uri); urilen = strlen(uri); /* count the slashes */ for (i=0, slashes=0; isocket = -1; conn->client = INADDR_ANY; conn->last_active = now; conn->request = NULL; conn->request_length = 0; conn->accept_gzip = 0; conn->method = NULL; conn->uri = NULL; conn->query = NULL; conn->header = NULL; conn->mime_type = NULL; conn->encoding = ""; conn->header_extra = ""; conn->header_length = 0; conn->header_sent = 0; conn->header_dont_free = 0; conn->header_only = 0; conn->http_code = 0; conn->reply = NULL; conn->reply_dont_free = 0; conn->reply_length = 0; conn->reply_sent = 0; conn->total_sent = 0; /* Make it harmless so it gets garbage-collected if it should, for some * reason, fail to be correctly filled out. */ conn->state = DONE; return (conn); } /* --------------------------------------------------------------------------- * Accept a connection from sockin and add it to the connection queue. */ static void accept_connection(void) { struct sockaddr_in addrin; socklen_t sin_size; struct connection *conn; int sock; sin_size = (socklen_t)sizeof(struct sockaddr); sock = accept(sockin, (struct sockaddr *)&addrin, &sin_size); if (sock == -1) { if (errno == ECONNABORTED || errno == EINTR) { verbosef("accept() failed: %s", strerror(errno)); return; } /* else */ err(1, "accept()"); } fd_set_nonblock(sock); /* allocate and initialise struct connection */ conn = new_connection(); conn->socket = sock; conn->state = RECV_REQUEST; conn->client = addrin.sin_addr.s_addr; LIST_INSERT_HEAD(&connlist, conn, entries); verbosef("accepted connection from %s:%u", inet_ntoa(addrin.sin_addr), ntohs(addrin.sin_port) ); } /* --------------------------------------------------------------------------- * Log a connection, then cleanly deallocate its internals. */ static void free_connection(struct connection *conn) { dverbosef("free_connection(%d)", conn->socket); if (conn->socket != -1) close(conn->socket); if (conn->request != NULL) free(conn->request); if (conn->method != NULL) free(conn->method); if (conn->uri != NULL) free(conn->uri); if (conn->query != NULL) free(conn->query); if (conn->header != NULL && !conn->header_dont_free) free(conn->header); if (conn->reply != NULL && !conn->reply_dont_free) free(conn->reply); } /* --------------------------------------------------------------------------- * Format [when] as an RFC1123 date, stored in the specified buffer. The same * buffer is returned for convenience. */ #define DATE_LEN 30 /* strlen("Fri, 28 Feb 2003 00:02:08 GMT")+1 */ static char *rfc1123_date(char *dest, const time_t when) { time_t tmp = when; if (strftime(dest, DATE_LEN, "%a, %d %b %Y %H:%M:%S %Z", gmtime(&tmp) ) == 0) errx(1, "strftime() failed [%s]", dest); return (dest); } /* --------------------------------------------------------------------------- * A default reply for any (erroneous) occasion. */ static void default_reply(struct connection *conn, const int errcode, const char *errname, const char *format, ...) { char *reason, date[DATE_LEN]; va_list va; va_start(va, format); xvasprintf(&reason, format, va); va_end(va); /* Only really need to calculate the date once. */ (void)rfc1123_date(date, now); conn->reply_length = xasprintf(&(conn->reply), "%d %s\n" "

%s

\n" /* errname */ "%s\n" /* reason */ "
\n" "Generated by %s on %s\n" "\n", errcode, errname, errname, reason, server, date); free(reason); conn->header_length = xasprintf(&(conn->header), "HTTP/1.1 %d %s\r\n" "Date: %s\r\n" "Server: %s\r\n" "Content-Length: %d\r\n" "Content-Type: text/html\r\n" "\r\n", errcode, errname, date, server, conn->reply_length); conn->http_code = errcode; } /* --------------------------------------------------------------------------- * Parses a single HTTP request field. Returns string from end of [field] to * first \r, \n or end of request string. Returns NULL if [field] can't be * matched. * * You need to remember to deallocate the result. * example: parse_field(conn, "Referer: "); */ static char *parse_field(const struct connection *conn, const char *field) { size_t bound1, bound2; char *pos; /* find start */ pos = strstr(conn->request, field); if (pos == NULL) return (NULL); bound1 = pos - conn->request + strlen(field); /* find end */ for (bound2 = bound1; conn->request[bound2] != '\r' && bound2 < conn->request_length; bound2++) ; /* copy to buffer */ return (split_string(conn->request, bound1, bound2)); } /* --------------------------------------------------------------------------- * Parse an HTTP request like "GET /hosts/?sort=in HTTP/1.1" to get the method * (GET), the uri (/hosts/), the query (sort=in) and whether the UA will * accept gzip encoding. Remember to deallocate all these buffers. Query * can be NULL. The method will be returned in uppercase. */ static int parse_request(struct connection *conn) { size_t bound1, bound2, mid; char *accept_enc; /* parse method */ for (bound1 = 0; bound1 < conn->request_length && conn->request[bound1] != ' '; bound1++) ; conn->method = split_string(conn->request, 0, bound1); strntoupper(conn->method, bound1); /* parse uri */ for (; bound1 < conn->request_length && conn->request[bound1] == ' '; bound1++) ; if (bound1 == conn->request_length) return (0); /* fail */ for (bound2=bound1+1; bound2 < conn->request_length && conn->request[bound2] != ' ' && conn->request[bound2] != '\r'; bound2++) ; /* find query string */ for (mid=bound1; midrequest[mid] != '?'; mid++) ; if (conn->request[mid] == '?') { conn->query = split_string(conn->request, mid+1, bound2); bound2 = mid; } conn->uri = split_string(conn->request, bound1, bound2); /* parse important fields */ accept_enc = parse_field(conn, "Accept-Encoding: "); if (accept_enc != NULL) { if (strstr(accept_enc, "gzip") != NULL) conn->accept_gzip = 1; free(accept_enc); } return (1); } /* FIXME: maybe we need a smarter way of doing static pages: */ /* --------------------------------------------------------------------------- * Web interface: static stylesheet. */ static void static_style_css(struct connection *conn) { #include "stylecss.h" conn->reply = style_css; conn->reply_length = style_css_len; conn->reply_dont_free = 1; conn->mime_type = mime_type_css; } /* --------------------------------------------------------------------------- * Web interface: static JavaScript. */ static void static_graph_js(struct connection *conn) { #include "graphjs.h" conn->reply = graph_js; conn->reply_length = graph_js_len; conn->reply_dont_free = 1; conn->mime_type = mime_type_js; } /* --------------------------------------------------------------------------- * gzip a reply, if requested and possible. Don't bother with a minimum * length requirement, I've never seen a page fail to compress. */ static void process_gzip(struct connection *conn) { char *buf; size_t len; z_stream zs; if (!conn->accept_gzip) return; buf = xmalloc(conn->reply_length); len = conn->reply_length; zs.zalloc = Z_NULL; zs.zfree = Z_NULL; zs.opaque = Z_NULL; if (deflateInit2(&zs, Z_BEST_COMPRESSION, Z_DEFLATED, 15+16, /* 15 = biggest window, 16 = add gzip header+trailer */ 8 /* default */, Z_DEFAULT_STRATEGY) != Z_OK) return; zs.avail_in = conn->reply_length; zs.next_in = (unsigned char *)conn->reply; zs.avail_out = conn->reply_length; zs.next_out = (unsigned char *)buf; if (deflate(&zs, Z_FINISH) != Z_STREAM_END) { deflateEnd(&zs); free(buf); verbosef("failed to compress %u bytes", (unsigned int)len); return; } if (conn->reply_dont_free) conn->reply_dont_free = 0; else free(conn->reply); conn->reply = buf; conn->reply_length -= zs.avail_out; conn->encoding = encoding_gzip; deflateEnd(&zs); } /* --------------------------------------------------------------------------- * Process a GET/HEAD request */ static void process_get(struct connection *conn) { char *decoded_url, *safe_url; char date[DATE_LEN]; verbosef("http: %s \"%s\" %s", conn->method, conn->uri, (conn->query == NULL)?"":conn->query); /* work out path of file being requested */ decoded_url = urldecode(conn->uri); /* make sure it's safe */ safe_url = make_safe_uri(decoded_url); free(decoded_url); if (safe_url == NULL) { default_reply(conn, 400, "Bad Request", "You requested an invalid URI: %s", conn->uri); return; } if (strcmp(safe_url, "/") == 0) { struct str *buf = html_front_page(); str_extract(buf, &(conn->reply_length), &(conn->reply)); conn->mime_type = mime_type_html; } else if (str_starts_with(safe_url, "/hosts/")) { /* FIXME here - make this saner */ struct str *buf = html_hosts(safe_url, conn->query); if (buf == NULL) { default_reply(conn, 404, "Not Found", "The page you requested could not be found."); return; } str_extract(buf, &(conn->reply_length), &(conn->reply)); conn->mime_type = mime_type_html; } else if (str_starts_with(safe_url, "/graphs.xml")) { struct str *buf = xml_graphs(); str_extract(buf, &(conn->reply_length), &(conn->reply)); conn->mime_type = mime_type_xml; /* hack around Opera caching the XML */ conn->header_extra = "Pragma: no-cache\r\n"; } else if (strcmp(safe_url, "/style.css") == 0) static_style_css(conn); else if (strcmp(safe_url, "/graph.js") == 0) static_graph_js(conn); else { default_reply(conn, 404, "Not Found", "The page you requested could not be found."); return; } free(safe_url); process_gzip(conn); assert(conn->mime_type != NULL); conn->header_length = xasprintf(&(conn->header), "HTTP/1.1 200 OK\r\n" "Date: %s\r\n" "Server: %s\r\n" "Content-Length: %d\r\n" "Content-Type: %s\r\n" "%s" "%s" "\r\n" , rfc1123_date(date, now), server, conn->reply_length, conn->mime_type, conn->encoding, conn->header_extra); conn->http_code = 200; } /* --------------------------------------------------------------------------- * Process a request: build the header and reply, advance state. */ static void process_request(struct connection *conn) { if (!parse_request(conn)) { default_reply(conn, 400, "Bad Request", "You sent a request that the server couldn't understand."); } else if (strcmp(conn->method, "GET") == 0) { process_get(conn); } else if (strcmp(conn->method, "HEAD") == 0) { process_get(conn); conn->header_only = 1; } else { default_reply(conn, 501, "Not Implemented", "The method you specified (%s) is not implemented.", conn->method); } /* advance state */ if (conn->header_only) conn->state = SEND_HEADER; else conn->state = SEND_HEADER_AND_REPLY; /* request not needed anymore */ free(conn->request); conn->request = NULL; /* important: don't free it again later */ } /* --------------------------------------------------------------------------- * Receiving request. */ static void poll_recv_request(struct connection *conn) { #define BUFSIZE 65536 char buf[BUFSIZE]; ssize_t recvd; recvd = recv(conn->socket, buf, BUFSIZE, 0); dverbosef("poll_recv_request(%d) got %d bytes", conn->socket, (int)recvd); if (recvd <= 0) { if (recvd == -1) verbosef("recv(%d) error: %s", conn->socket, strerror(errno)); conn->state = DONE; return; } conn->last_active = now; #undef BUFSIZE /* append to conn->request */ conn->request = xrealloc(conn->request, conn->request_length+recvd+1); memcpy(conn->request+conn->request_length, buf, (size_t)recvd); conn->request_length += recvd; conn->request[conn->request_length] = 0; /* process request if we have all of it */ if (conn->request_length > 4 && memcmp(conn->request+conn->request_length-4, "\r\n\r\n", 4) == 0) process_request(conn); /* die if it's too long */ if (conn->request_length > MAX_REQUEST_LENGTH) { default_reply(conn, 413, "Request Entity Too Large", "Your request was dropped because it was too long."); conn->state = SEND_HEADER; } } /* --------------------------------------------------------------------------- * Try to send header and [a part of the] reply in one packet. */ static void poll_send_header_and_reply(struct connection *conn) { ssize_t sent; struct iovec iov[2]; assert(!conn->header_only); assert(conn->reply_length > 0); assert(conn->header_sent == 0); assert(conn->reply_sent == 0); /* Fill out iovec */ iov[0].iov_base = conn->header; iov[0].iov_len = conn->header_length; iov[1].iov_base = conn->reply + conn->reply_sent; iov[1].iov_len = conn->reply_length - conn->reply_sent; sent = writev(conn->socket, iov, 2); conn->last_active = now; verbosef("poll_send_header_and_reply(%d) sent %d bytes", conn->socket, (int)sent); /* handle any errors (-1) or closure (0) in send() */ if (sent < 1) { if (sent == -1) verbosef("writev(%d) error: %s", conn->socket, strerror(errno)); conn->state = DONE; return; } /* Figure out what we've sent. */ conn->total_sent += (unsigned int)sent; if (sent < (ssize_t)conn->header_length) { verbosef("partially sent header"); conn->header_sent = sent; conn->state = SEND_HEADER; return; } /* else */ conn->header_sent = conn->header_length; sent -= conn->header_length; if (conn->reply_sent + sent < conn->reply_length) { verbosef("partially sent reply"); conn->reply_sent += sent; conn->state = SEND_REPLY; return; } /* else */ conn->reply_sent = conn->reply_length; conn->state = DONE; } /* --------------------------------------------------------------------------- * Sending header. Assumes conn->header is not NULL. */ static void poll_send_header(struct connection *conn) { ssize_t sent; sent = send(conn->socket, conn->header + conn->header_sent, conn->header_length - conn->header_sent, 0); conn->last_active = now; dverbosef("poll_send_header(%d) sent %d bytes", conn->socket, (int)sent); /* handle any errors (-1) or closure (0) in send() */ if (sent < 1) { if (sent == -1) verbosef("send(%d) error: %s", conn->socket, strerror(errno)); conn->state = DONE; return; } conn->header_sent += (unsigned int)sent; conn->total_sent += (unsigned int)sent; /* check if we're done sending */ if (conn->header_sent == conn->header_length) { if (conn->header_only) conn->state = DONE; else conn->state = SEND_REPLY; } } /* --------------------------------------------------------------------------- * Sending reply. */ static void poll_send_reply(struct connection *conn) { ssize_t sent; sent = send(conn->socket, conn->reply + conn->reply_sent, conn->reply_length - conn->reply_sent, 0); conn->last_active = now; dverbosef("poll_send_reply(%d) sent %d: [%d-%d] of %d", conn->socket, (int)sent, (int)conn->reply_sent, (int)(conn->reply_sent + sent - 1), (int)conn->reply_length); /* handle any errors (-1) or closure (0) in send() */ if (sent < 1) { if (sent == -1) verbosef("send(%d) error: %s", conn->socket, strerror(errno)); else if (sent == 0) verbosef("send(%d) closure", conn->socket); conn->state = DONE; return; } conn->reply_sent += (unsigned int)sent; conn->total_sent += (unsigned int)sent; /* check if we're done sending */ if (conn->reply_sent == conn->reply_length) conn->state = DONE; } /* --------------------------------------------------------------------------- * Initialize the sockin global. This is the socket that we accept * connections from. Pass -1 as max_conn for system limit. */ void http_init(const in_addr_t bindaddr, const unsigned short bindport, const int max_conn) { struct sockaddr_in addrin; int sockopt; /* create incoming socket */ sockin = socket(PF_INET, SOCK_STREAM, 0); if (sockin == -1) err(1, "socket()"); /* reuse address */ sockopt = 1; if (setsockopt(sockin, SOL_SOCKET, SO_REUSEADDR, &sockopt, sizeof(sockopt)) == -1) err(1, "setsockopt(SO_REUSEADDR)"); /* bind socket */ addrin.sin_family = (u_char)PF_INET; addrin.sin_port = htons(bindport); addrin.sin_addr.s_addr = bindaddr; memset(&(addrin.sin_zero), 0, 8); if (bind(sockin, (struct sockaddr *)&addrin, sizeof(struct sockaddr)) == -1) err(1, "bind(%s:%u)", inet_ntoa(addrin.sin_addr), bindport); verbosef("listening on %s:%u", inet_ntoa(addrin.sin_addr), bindport); /* listen on socket */ if (listen(sockin, max_conn) == -1) err(1, "listen()"); /* ignore SIGPIPE */ if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) err(1, "can't ignore SIGPIPE"); } /* --------------------------------------------------------------------------- * Set recv/send fd_sets and calculate timeout length. */ void http_fd_set(fd_set *recv_set, fd_set *send_set, int *max_fd, struct timeval *timeout, int *need_timeout) { struct connection *conn, *next; int minidle = idletime + 1; #define MAX_FD_SET(sock, fdset) do { \ FD_SET(sock, fdset); *max_fd = max(*max_fd, sock); } while(0) MAX_FD_SET(sockin, recv_set); LIST_FOREACH_SAFE(conn, &connlist, entries, next) { int idlefor = now - conn->last_active; /* Time out dead connections. */ if (idlefor >= idletime) { struct sockaddr_in addrin; addrin.sin_addr.s_addr = conn->client; verbosef("http socket timeout from %s (fd %d)", inet_ntoa(addrin.sin_addr), conn->socket); conn->state = DONE; } /* Connections that need a timeout. */ if (conn->state != DONE) minidle = min(minidle, (idletime - idlefor)); switch (conn->state) { case DONE: /* clean out stale connection */ LIST_REMOVE(conn, entries); free_connection(conn); free(conn); break; case RECV_REQUEST: MAX_FD_SET(conn->socket, recv_set); break; case SEND_HEADER_AND_REPLY: case SEND_HEADER: case SEND_REPLY: MAX_FD_SET(conn->socket, send_set); break; default: errx(1, "invalid state"); } } #undef MAX_FD_SET /* Only set timeout if cap hasn't already. */ if ((*need_timeout == 0) && (minidle <= idletime)) { *need_timeout = 1; timeout->tv_sec = minidle; timeout->tv_usec = 0; } } /* --------------------------------------------------------------------------- * poll connections that select() says need attention */ void http_poll(fd_set *recv_set, fd_set *send_set) { struct connection *conn; if (FD_ISSET(sockin, recv_set)) accept_connection(); LIST_FOREACH(conn, &connlist, entries) switch (conn->state) { case RECV_REQUEST: if (FD_ISSET(conn->socket, recv_set)) poll_recv_request(conn); break; case SEND_HEADER_AND_REPLY: if (FD_ISSET(conn->socket, send_set)) poll_send_header_and_reply(conn); break; case SEND_HEADER: if (FD_ISSET(conn->socket, send_set)) poll_send_header(conn); break; case SEND_REPLY: if (FD_ISSET(conn->socket, send_set)) poll_send_reply(conn); break; default: errx(1, "invalid state"); } } /* vim:set ts=4 sw=4 et tw=78: */