/** * httpd.C: Generic HTTPd * * Copyright © 2007 Pippijn van Steenhoven / The Ermyth Team * Rights to this code are as documented in COPYING. * * * Portions of this file were derived from sources bearing the following license: * Copyright © 2007 Pippijn van Steenhoven / The Ermyth Team * Copyright © 2005-2007 Jilles Tjoelker et al. * Rights to this code are as documented in doc/pod/license.pod. * * $Id: httpd.C,v 1.10 2007/09/22 14:27:28 pippijn Exp $ */ #include "atheme.h" #include #include #include "httpd.h" #include "datastream.h" #include "connection.h" #include "confparse.h" #define REQUEST_MAX 65536 /* maximum size of one call */ static char const rcsid[] = "$Id: httpd.C,v 1.10 2007/09/22 14:27:28 pippijn Exp $"; static void on_config_ready (void); REGISTER_MODULE ("rpc/httpd", false, "The Ermyth Team "); static connection_t *listener; pathvec pathhandlers; /* conf stuff */ ConfTable::list_type conf_httpd_table; static struct httpd_configuration { char *host; char *www_root; int port; void clear () { if (host != NULL) sfree (host); if (www_root != NULL) sfree (www_root); host = NULL; www_root = NULL; } } httpd_config; static int conf_httpd_host (config_entry_t *ce) { if (!ce->valid ()) return -1; if (httpd_config.host != NULL) sfree (httpd_config.host); httpd_config.host = sstrdup (ce->vardata ()); return 0; } static int conf_httpd_www_root (config_entry_t *ce) { if (!ce->valid ()) return -1; if (httpd_config.www_root != NULL) sfree (httpd_config.www_root); httpd_config.www_root = sstrdup (ce->vardata ()); return 0; } static int conf_httpd_port (config_entry_t *ce) { if (!ce->valid ()) return -1; httpd_config.port = ce->vardata (); return 0; } static int conf_httpd (config_entry_t *ce) { subblock_handler (ce, conf_httpd_table); return 0; } void httpddata::clear (void) { method[0] = '\0'; filename[0] = '\0'; if (requestbuf != NULL) { sfree (requestbuf); requestbuf = NULL; } if (replybuf != NULL) { sfree (replybuf); replybuf = NULL; } length = 0; lengthdone = 0; correct_content_type = false; expect_100_continue = false; sent_reply = false; } static int open_file (char const *filename) { char fname[256]; if (strstr (filename, "..")) return -1; if (!strcmp (filename, "/")) filename = "/index.html"; snprintf (fname, sizeof fname, "%s/%s", httpd_config.www_root, filename); return open (fname, O_RDONLY); } static void process_header (connection_t *cptr, char *line) { httpddata *hd; char *p; hd = static_cast (cptr->userdata); p = strchr (line, ':'); if (p == NULL) return; *p = '\0'; p++; while (*p == ' ') p++; if (!strcasecmp (line, "Connection")) { p = strtok (p, ", \t"); while (p != NULL) { if (!strcasecmp (p, "close")) { slog (LG_DEBUG, "process_header(): Connection: close requested by fd %d", cptr->fd); hd->connection_close = true; } p = strtok (NULL, ", \t"); } } else if (!strcasecmp (line, "Content-Length")) hd->length = atoi (p); else if (!strcasecmp (line, "Content-Type")) { p = strtok (p, "; \t"); hd->correct_content_type = p != NULL && (!strcasecmp (p, "text/xml") || !strcasecmp (p, "application/json")); } else if (!strcasecmp (line, "Expect")) hd->expect_100_continue = !strcasecmp (p, "100-continue"); } static void check_close (connection_t *cptr) { httpddata *hd; hd = static_cast (cptr->userdata); if (hd->connection_close) sendq_add_eof (cptr); } static void send_error (connection_t *cptr, int errorcode, char const * const text, bool const sendentity) { httpddata *hd; char buf1[300]; char buf2[700]; hd = static_cast (cptr->userdata); if (errorcode < 100 || errorcode > 999) errorcode = 500; snprintf (buf2, sizeof buf2, "HTTP/1.1 %d %s\r\n", errorcode, text); snprintf (buf1, sizeof buf1, "HTTP/1.1 %d %s\r\n" "Server: " PACKAGE_NAME "/%s\r\n" "Content-Type: text/plain\r\n" "Content-Length: %" PRIsize "\r\n\r\n%s", errorcode, text, version, strlen (buf2), sendentity ? buf2 : ""); sendq_add (cptr, buf1, strlen (buf1)); } static char const * const content_type (char const * const filename) { char const *p; if (!strcmp (filename, "/")) return "text/html"; p = strrchr (filename, '.'); if (p == NULL) return "text/plain"; p++; if (!strcasecmp (p, "html") || !strcasecmp (p, "htm")) return "text/html"; else if (!strcasecmp (p, "txt")) return "text/plain"; else if (!strcasecmp (p, "jpg") || !strcasecmp (p, "jpeg")) return "image/jpeg"; else if (!strcasecmp (p, "gif")) return "image/gif"; else if (!strcasecmp (p, "png")) return "image/png"; return "application/octet-stream"; } static void httpd_recvqhandler (connection_t *cptr) { char buf[BUFSIZE * 2]; char outbuf[BUFSIZE * 2]; int count; httpddata *hd; char *p; int in; struct stat sb; off_t count1; path_handler_t ph; bool is_get, is_post, handling_done = false; pathvec::iterator found, pvend = pathhandlers.end (); hd = static_cast (cptr->userdata); if ((found = (std::find_if (pathhandlers.begin (), pvend, pathhandler_eq (hd->filename)))) != pvend) { handling_done = true; ph = *found; } if (handling_done == true) { if (hd->requestbuf != NULL) { count = recvq_get (cptr, hd->requestbuf + hd->lengthdone, hd->length - hd->lengthdone); if (count <= 0) return; hd->lengthdone += count; if (hd->lengthdone != hd->length) return; hd->requestbuf[hd->length] = '\0'; ph.handler (cptr, hd->requestbuf); hd->clear (); return; } } count = recvq_getline (cptr, buf, sizeof buf - 1); if (count <= 0) return; if (cptr->flags & CF_NONEWLINE) { slog (LG_INFO, "httpd_recvqhandler(): throwing out fd %d (%s) for excessive line length", cptr->fd, cptr->hbuf); send_error (cptr, 400, "Bad request", true); sendq_add_eof (cptr); return; } cnt.bin += count; if (buf[count - 1] == '\n') count--; if (count > 0 && buf[count - 1] == '\r') count--; buf[count] = '\0'; if (hd->method[0] == '\0') { /* make sure they're not sending more requests after * declaring they're not sending any more */ if (hd->connection_close) return; p = strtok (buf, " "); if (p == NULL) return; strlcpy (hd->method, p, sizeof hd->method); p = strtok (NULL, " "); if (p == NULL) return; strlcpy (hd->filename, p, sizeof hd->filename); p = strtok (NULL, ""); if (p == NULL || !strcmp (p, "HTTP/1.0")) hd->connection_close = true; slog (LG_DEBUG, "httpd_recvqhandler(): request %s for %s", hd->method, hd->filename); } else if (count == 0) { is_get = !strcmp (hd->method, "GET"); is_post = !strcmp (hd->method, "POST"); if (!is_post && !is_get) { send_error (cptr, 501, "Method Not Implemented", true); sendq_add_eof (cptr); return; } hd->method[0] = '\0'; if (!handling_done) { in = open_file (hd->filename); if (in == -1 || fstat (in, &sb) == -1 || !S_ISREG (sb.st_mode)) { if (in != -1) close (in); slog (LG_INFO, "httpd_recvqhandler(): 404 for %s", hd->filename); send_error (cptr, 404, "Not Found", is_get); check_close (cptr); return; } slog (LG_INFO, "httpd_recvqhandler(): 200 for %s", hd->filename); snprintf (outbuf, sizeof outbuf, "HTTP/1.1 200 OK\r\n" "Server: " PACKAGE_NAME "/%s\r\n" "Content-Type: %s\r\n" "Content-Length: %lu\r\n" "\r\n", version, content_type (hd->filename), sb.st_size); sendq_add (cptr, outbuf, strlen (outbuf)); count1 = is_get ? sb.st_size : 0; while (count1 > 0) { count = sizeof outbuf; if (count > count1) count = count1; count = read (in, outbuf, count); if (count <= 0) break; sendq_add (cptr, outbuf, count); count1 -= count; } close (in); if (count1 > 0) { slog (LG_INFO, "httpd_recvqhandler(): disconnecting fd %d (%s), read failed on %s", cptr->fd, cptr->hbuf, hd->filename); cptr->flags |= CF_DEAD; } else check_close (cptr); } else { if (hd->length <= 0) { send_error (cptr, 411, "Length Required", true); sendq_add_eof (cptr); return; } if (hd->length > REQUEST_MAX) { send_error (cptr, 413, "Request Entity Too Large", true); sendq_add_eof (cptr); return; } if (!hd->correct_content_type) { send_error (cptr, 415, "Unsupported Media Type", true); sendq_add_eof (cptr); return; } if (hd->expect_100_continue) { snprintf (outbuf, sizeof outbuf, "HTTP/1.1 100 Continue\r\n" "Server: " PACKAGE_NAME "/%s\r\n" "\r\n", version); sendq_add (cptr, outbuf, strlen (outbuf)); } hd->requestbuf = salloc (hd->length + 1); } } else process_header (cptr, buf); } static void httpd_closehandler (connection_t *cptr) { httpddata *hd; slog (LG_DEBUG, "httpd_closehandler(): fd %d (%s) closed", cptr->fd, cptr->hbuf); hd = static_cast (cptr->userdata); if (hd != NULL) { sfree (hd->requestbuf); delete hd; } cptr->userdata = NULL; } static void do_listen (connection_t *cptr) { connection_t *newptr; httpddata *hd; newptr = connection_t::accept_tcp (cptr, recvq_put, sendq_flush); slog (LG_DEBUG, "do_listen(): accepted httpd from %s fd %d", newptr->hbuf, newptr->fd); hd = new httpddata; newptr->userdata = hd; newptr->recvq_handler = httpd_recvqhandler; newptr->close_handler = httpd_closehandler; } static void httpd_checkidle (void *arg) { connection_t::list_type::iterator it, it_end = connection_t::list.end (); connection_t *cptr; if (listener == NULL) return; for (it = connection_t::list.begin (); it != it_end; ++it) { cptr = *it; if (cptr->listener == listener && cptr->last_recv + 300 < NOW) { if (sendq_nonempty (cptr)) cptr->last_recv = NOW; else /* from a timeout function, * connection_t::close_soon() may take quite * a while, and connection_t::close() is safe * -- jilles */ cptr->close (); } } } static void on_config_ready (void) { if (httpd_config.host != NULL && httpd_config.port != 0) { listener = connection_t::open_listener_tcp (httpd_config.host, httpd_config.port, do_listen); if (listener == NULL) slog (LG_ERROR, "httpd_config_ready(): failed to open listener on host %s port %d", httpd_config.host, httpd_config.port); } else slog (LG_ERROR, "httpd_config_ready(): httpd {} block missing or invalid"); httpd_config.clear (); } bool _modinit (module *m) { event_add ("httpd_checkidle", httpd_checkidle, NULL, 60); /* This module needs a rehash to initialize fully if loaded * at run time */ ConfTable::callback.ready.attach (on_config_ready); add_top_conf ("HTTPD", conf_httpd); add_conf_item ("HOST", conf_httpd_table, conf_httpd_host); add_conf_item ("WWW_ROOT", conf_httpd_table, conf_httpd_www_root); add_conf_item ("PORT", conf_httpd_table, conf_httpd_port); return true; } void _moddeinit (void) { event_delete (httpd_checkidle, NULL); listener->close_soon_children (); del_conf_item ("HOST", conf_httpd_table); del_conf_item ("WWW_ROOT", conf_httpd_table); del_conf_item ("PORT", conf_httpd_table); del_top_conf ("HTTPD"); httpd_config.clear (); ConfTable::callback.ready.detach (on_config_ready); }