/* * Copyright (c) 2001-2002 Packet Design, LLC. * All rights reserved. * * Subject to the following obligations and disclaimer of warranty, * use and redistribution of this software, in source or object code * forms, with or without modifications are expressly permitted by * Packet Design; provided, however, that: * * (i) Any and all reproductions of the source or object code * must include the copyright notice above and the following * disclaimer of warranties; and * (ii) No rights are granted, in any manner or form, to use * Packet Design trademarks, including the mark "PACKET DESIGN" * on advertising, endorsements, or otherwise except as such * appears in the above copyright notice or in the software. * * THIS SOFTWARE IS BEING PROVIDED BY PACKET DESIGN "AS IS", AND * TO THE MAXIMUM EXTENT PERMITTED BY LAW, PACKET DESIGN MAKES NO * REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, REGARDING * THIS SOFTWARE, INCLUDING WITHOUT LIMITATION, ANY AND ALL IMPLIED * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, * OR NON-INFRINGEMENT. PACKET DESIGN DOES NOT WARRANT, GUARANTEE, * OR MAKE ANY REPRESENTATIONS REGARDING THE USE OF, OR THE RESULTS * OF THE USE OF THIS SOFTWARE IN TERMS OF ITS CORRECTNESS, ACCURACY, * RELIABILITY OR OTHERWISE. IN NO EVENT SHALL PACKET DESIGN BE * LIABLE FOR ANY DAMAGES RESULTING FROM OR ARISING OUT OF ANY USE * OF THIS SOFTWARE, INCLUDING WITHOUT LIMITATION, ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, PUNITIVE, OR CONSEQUENTIAL * DAMAGES, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, LOSS OF * USE, DATA OR PROFITS, HOWEVER CAUSED AND UNDER ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF * THE USE OF THIS SOFTWARE, EVEN IF PACKET DESIGN IS ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. * * Author: Archie Cobbs */ #include #include #include #include #include #ifdef __linux__ #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "structs/structs.h" #include "structs/type/array.h" #include "sys/alog.h" #include "io/string_fp.h" #include "tmpl/tmpl.h" #include "util/ghash.h" #include "util/typed_mem.h" #include "http/http_defs.h" #include "http/http_server.h" #include "http/http_servlet.h" #include "http/servlet/tmpl.h" #include "http/servlet/file.h" #define MEM_TYPE "http_servlet_file" #define TMPL_SUFFIX ".tmpl" /* Cleanup state for http_servlet_file_serve() */ struct http_servlet_file_serve_state { int fd; }; /* One cached template */ struct tmpl_cache { char *path; /* file pathname */ struct http_servlet *servlet; /* tmpl servlet */ }; /* Servlet state */ struct file_private { struct http_servlet_file_info *info; struct ghash *tmpls; /* cached tmpl servlets */ http_servlet_tmpl_free_t *freer; /* freer for tmpl arg */ }; /* Directory -> file redirects */ static const char *http_servlet_file_dirindex[][2] = { { "index.tmpl", "index" }, { "index.html", "index.html" }, { "index.htm", "index.htm" }, { NULL, NULL } }; /* * Internal functions */ static char *http_servlet_file_gen_filename( struct http_servlet_file_info *finfo, const char *url, const char *mtype); static void http_servlet_file_tmpl(struct http_servlet *servlet, const char *path, struct http_request *req, struct http_response *resp); static void http_servlet_file_serve_cleanup(void *arg); static http_servlet_run_t http_servlet_file_run; static http_servlet_destroy_t http_servlet_file_destroy; static ghash_hash_t tmpl_cache_hash; static ghash_equal_t tmpl_cache_equal; static ghash_del_t tmpl_cache_del; /* * Create a new file servlet. */ struct http_servlet * http_servlet_file_create(const struct http_servlet_file_info *info) { struct http_servlet *servlet = NULL; struct file_private *priv = NULL; /* Create servlet */ if ((servlet = MALLOC(MEM_TYPE, sizeof(*servlet))) == NULL) goto fail; memset(servlet, 0, sizeof(*servlet)); servlet->run = http_servlet_file_run; servlet->destroy = http_servlet_file_destroy; /* Initialize private info */ if ((priv = MALLOC(MEM_TYPE, sizeof(*priv))) == NULL) goto fail; memset(priv, 0, sizeof(*priv)); /* Copy "info" */ if ((priv->info = MALLOC(MEM_TYPE, sizeof(*priv->info))) == NULL) goto fail; memset(priv->info, 0, sizeof(*priv->info)); if (info->docroot != NULL && (priv->info->docroot = STRDUP(MEM_TYPE, info->docroot)) == NULL) goto fail; priv->info->allow_escape = info->allow_escape; if (info->filename != NULL && (priv->info->filename = STRDUP(MEM_TYPE, info->filename)) == NULL) goto fail; if (info->prefix != NULL && (priv->info->prefix = STRDUP(MEM_TYPE, info->prefix)) == NULL) goto fail; if (info->mime_type != NULL && (priv->info->mime_type = STRDUP(MEM_TYPE, info->mime_type)) == NULL) goto fail; if (info->mime_encoding != NULL && (priv->info->mime_encoding = STRDUP(MEM_TYPE, info->mime_encoding)) == NULL) goto fail; priv->info->logger = info->logger; priv->info->hide = info->hide; if (_http_servlet_tmpl_copy_tinfo(&priv->info->tinfo, &info->tinfo) == -1) goto fail; /* Only free the template argument once */ priv->freer = priv->info->tinfo.freer; priv->info->tinfo.freer = NULL; /* Create template hash table */ if ((priv->tmpls = ghash_create(priv, 0, 200, MEM_TYPE, tmpl_cache_hash, tmpl_cache_equal, NULL, tmpl_cache_del)) == NULL) goto fail; servlet->arg = priv; /* OK */ return (servlet); fail: /* Clean up after failure */ if (priv != NULL) { if (priv->info != NULL) { FREE(MEM_TYPE, (char *)priv->info->filename); FREE(MEM_TYPE, (char *)priv->info->docroot); FREE(MEM_TYPE, (char *)priv->info->prefix); FREE(MEM_TYPE, (char *)priv->info->mime_type); FREE(MEM_TYPE, (char *)priv->info->mime_encoding); _http_servlet_tmpl_free_tinfo(&priv->info->tinfo); FREE(MEM_TYPE, priv->info); } ghash_destroy(&priv->tmpls); FREE(MEM_TYPE, priv); } if (servlet != NULL) FREE(MEM_TYPE, servlet); return (NULL); } /* * Destroy a file servlet. */ static void http_servlet_file_destroy(struct http_servlet *servlet) { struct file_private *const priv = servlet->arg; /* Free template argument */ if (priv->freer != NULL) (*priv->freer)(priv->info->tinfo.arg); /* Free private info */ FREE(MEM_TYPE, (char *)priv->info->filename); FREE(MEM_TYPE, (char *)priv->info->docroot); FREE(MEM_TYPE, (char *)priv->info->prefix); FREE(MEM_TYPE, (char *)priv->info->mime_type); FREE(MEM_TYPE, (char *)priv->info->mime_encoding); _http_servlet_tmpl_free_tinfo(&priv->info->tinfo); FREE(MEM_TYPE, priv->info); ghash_destroy(&priv->tmpls); FREE(MEM_TYPE, priv); /* Free servlet */ FREE(MEM_TYPE, servlet); } /* * Execute file servlet. */ static int http_servlet_file_run(struct http_servlet *servlet, struct http_request *req, struct http_response *resp) { struct file_private *const priv = servlet->arg; struct http_servlet_file_info *const info = priv->info; const char *const urlpath = http_request_get_path(req); char *path = NULL; int got_tmpl = 0; size_t len; /* Generate file name from URL; first try to find a template file */ if (info->tinfo.handler != NULL && (len = strlen(urlpath)) < MAXPATHLEN - sizeof(TMPL_SUFFIX)) { char tp[MAXPATHLEN]; struct stat sb; memcpy(tp, urlpath, len); memcpy(tp + len, TMPL_SUFFIX, sizeof(TMPL_SUFFIX)); path = http_servlet_file_gen_filename(info, tp, TYPED_MEM_TEMP); if (stat(path, &sb) == 0 && (sb.st_mode & S_IFMT) == S_IFREG) got_tmpl = 1; else { FREE(TYPED_MEM_TEMP, path); path = NULL; } } /* Generate file name from URL; now try a normal file */ if (!got_tmpl && (path = http_servlet_file_gen_filename(info, urlpath, TYPED_MEM_TEMP)) == NULL) { http_response_send_errno_error(resp); return (1); } /* Handle templates */ if (got_tmpl) { http_servlet_file_tmpl(servlet, path, req, resp); goto done; } /* Check whether to hide this file */ if (info->hide != NULL && (*info->hide)(info, req, resp, path)) { FREE(TYPED_MEM_TEMP, path); return (0); /* continue with next servlet */ } /* Use supplied MIME info, if any */ if (info->mime_type != NULL) { http_response_set_header(resp, 0, HTTP_HEADER_CONTENT_TYPE, "%s", info->mime_type); if (info->mime_encoding != NULL) { http_response_set_header(resp, 0, HTTP_HEADER_CONTENT_ENCODING, "%s", info->mime_encoding); } } /* Serve up file */ http_servlet_file_serve(path, info->logger, req, resp); done: /* Done */ FREE(TYPED_MEM_TEMP, path); return (1); } #define MAX_ENCODINGS 10 /* * Serve up a file. * * This is a public function usable by other servlets. */ void http_servlet_file_serve(const char *path, http_logger_t *logger, struct http_request *req, struct http_response *resp) { const char *hval; struct stat sb; char buf[1024]; FILE *output; struct tm tm; time_t when; int sock; /* Stat file */ if (stat(path, &sb) == -1) { fail_errno: http_response_send_errno_error(resp); return; } /* If file is a directory, redirect to default file if it exists */ if ((sb.st_mode & S_IFMT) == S_IFDIR) { int i; for (i = 0; http_servlet_file_dirindex[i][0] != NULL; i++) { const char *qs = http_request_get_query_string(req); char *urlpath; char *dfile; if (qs == NULL) qs = ""; ASPRINTF(TYPED_MEM_TEMP, &dfile, "%s/%s", path, http_servlet_file_dirindex[i][0]); if (dfile == NULL) goto fail_errno; if (stat(dfile, &sb) == -1) { FREE(TYPED_MEM_TEMP, dfile); continue; } if ((urlpath = http_request_url_encode(TYPED_MEM_TEMP, http_request_get_path(req))) == NULL) { FREE(TYPED_MEM_TEMP, dfile); goto fail_errno; } if (http_response_set_header(resp, 0, HTTP_HEADER_LOCATION, "%s%s%s%s%s", urlpath, "/" + (urlpath[strlen(urlpath) - 1] == '/'), http_servlet_file_dirindex[i][1], (*qs != '\0') ? "?" : "", qs) == -1) { FREE(TYPED_MEM_TEMP, urlpath); FREE(TYPED_MEM_TEMP, dfile); goto fail_errno; } FREE(TYPED_MEM_TEMP, urlpath); FREE(TYPED_MEM_TEMP, dfile); http_response_send_error(resp, HTTP_STATUS_FOUND, NULL); return; } } /* File must be regular */ if ((sb.st_mode & S_IFMT) != S_IFREG) { errno = ENOENT; /* hide non-regular files */ goto fail_errno; } /* Set timestamp from stat(2) info */ strftime(buf, sizeof(buf), HTTP_TIME_FMT_RFC1123, gmtime_r(&sb.st_mtime, &tm)); http_response_set_header(resp, 0, HTTP_HEADER_DATE, "%s", buf); /* Check for If-Modified-Since: header */ if ((hval = http_request_get_header(req, HTTP_HEADER_IF_MODIFIED_SINCE)) != NULL) { if ((when = http_request_parse_time(hval)) != (time_t)-1 && sb.st_mtime <= when) { http_response_send_error(resp, HTTP_STATUS_NOT_MODIFIED, NULL); return; } } /* Set MIME type if not set already */ if (http_response_get_header(resp, HTTP_HEADER_CONTENT_TYPE) == NULL) { const char *cencs[MAX_ENCODINGS]; const char *ctype; int i; http_response_guess_mime(path, &ctype, cencs, MAX_ENCODINGS); http_response_set_header(resp, 0, HTTP_HEADER_CONTENT_TYPE, "%s", ctype); for (i = 0; i < MAX_ENCODINGS && cencs[i] != NULL; i++) { http_response_set_header(resp, i > 0, HTTP_HEADER_CONTENT_ENCODING, "%s", cencs[i]); } } /* Set content length */ http_response_set_header(resp, 0, HTTP_HEADER_CONTENT_LENGTH, "%lu", (u_long)sb.st_size); /* Get servlet output stream (unbuffered) */ if ((output = http_response_get_output(resp, 0)) == NULL) { (*logger)(LOG_ERR, "can't get response output: %s", strerror(errno)); return; } /* Send file contents, using sendfile(2) if possible */ if ((sock = http_response_get_raw_socket(resp)) != -1) { struct http_servlet_file_serve_state state; /* Open file */ if ((state.fd = open(path, O_RDONLY)) == -1) goto fail_errno; /* Set cleanup hook in case thread is canceled */ pthread_cleanup_push(http_servlet_file_serve_cleanup, &state); /* Make sure headers are sent first */ http_response_send_headers(resp, 1); fflush(output); /* Send file directly using sendfile(2) */ #ifndef __linux__ sendfile(state.fd, sock, 0, sb.st_size, NULL, NULL, 0); #else sendfile(sock, state.fd, NULL, sb.st_size); #endif /* Close file */ pthread_cleanup_pop(1); } else { FILE *fp; int ret; /* Open file */ if ((fp = fopen(path, "r")) == NULL) goto fail_errno; /* Set cleanup hook in case thread is canceled */ pthread_cleanup_push((void (*)(void *))fclose, fp); /* Tranfer file contents */ while (1) { if ((ret = fread(buf, 1, sizeof(buf), fp)) != 0) { if (fwrite(buf, 1, ret, output) < ret) break; } if (ret < sizeof(buf)) break; } /* Close file */ pthread_cleanup_pop(1); } } /* * Do a template file. */ static void http_servlet_file_tmpl(struct http_servlet *servlet, const char *path, struct http_request *req, struct http_response *resp) { struct file_private *const priv = servlet->arg; struct http_servlet_file_info *const info = priv->info; struct http_servlet_tmpl_info ti; char mimepath[MAXPATHLEN + 1]; struct tmpl_cache *t = NULL; struct tmpl_cache key; const char *s; /* See if template already cached */ key.path = (char *)path; if ((t = ghash_get(priv->tmpls, &key)) != NULL) goto found; /* Create new cached entry */ if ((t = MALLOC(MEM_TYPE, sizeof(*t))) == NULL) goto fail; memset(t, 0, sizeof(*t)); if ((t->path = STRDUP(MEM_TYPE, path)) == NULL) goto fail; /* Set info required by the template servlet */ memset(&ti, 0, sizeof(ti)); ti.path = t->path; ti.tinfo = info->tinfo; ti.logger = info->logger; /* Figure out templates's output MIME type */ strlcat(mimepath, path, sizeof(mimepath)); mimepath[strlen(mimepath) - strlen(TMPL_SUFFIX)] = '\0'; if ((s = strrchr(mimepath, '.')) == NULL || strchr(s, '/') != NULL) /* no suffix? assume html */ strlcat(mimepath, "x.html", sizeof(mimepath)); http_response_guess_mime(mimepath, &ti.mime_type, NULL, 0); /* Create template servlet */ if ((t->servlet = http_servlet_tmpl_create(&ti)) == NULL) goto fail; /* Add it to hash table */ if (ghash_put(priv->tmpls, t) == -1) { (*info->logger)(LOG_ERR, "%s: %s", "ghash_put", strerror(errno)); fail: FREE(MEM_TYPE, t); http_response_send_errno_error(resp); return; } found: /* Invoke servlet */ (*t->servlet->run)(t->servlet, req, resp); } static void http_servlet_file_serve_cleanup(void *arg) { const struct http_servlet_file_serve_state *const state = arg; close(state->fd); } static u_int32_t tmpl_cache_hash(struct ghash *g, const void *item) { const struct tmpl_cache *const t = item; u_int32_t hash; const char *s; for (hash = 0, s = t->path; *s != '\0'; s++) hash = (31 * hash) + (u_char)*s; return (hash); } static int tmpl_cache_equal(struct ghash *g, const void *item1, const void *item2) { const struct tmpl_cache *const t1 = item1; const struct tmpl_cache *const t2 = item2; return (strcmp(t1->path, t2->path) == 0); } static void tmpl_cache_del(struct ghash *g, void *item) { struct tmpl_cache *const t = item; http_server_destroy_servlet(&t->servlet); FREE(MEM_TYPE, t->path); FREE(MEM_TYPE, t); } /* * Compute a filename from supplied info and URL. * * Caller must free returned string, which is in a buffer of size MAXPATHLEN. */ static char * http_servlet_file_gen_filename(struct http_servlet_file_info *info, const char *urlpath, const char *mtype) { char path[MAXPATHLEN]; char *rpath; char *tok; char *s; /* Sanity check */ assert(*urlpath == '/'); /* Disallow all ".", "..", and empty components within urlpath */ strlcpy(path, urlpath, sizeof(path)); for (s = path + 1; (tok = strsep(&s, "/")) != NULL; ) { if ((*tok == '\0' && s != NULL) || strcmp(tok, ".") == 0 || strcmp(tok, "..") == 0) { errno = ENOENT; return (NULL); } } /* Prepend root directory, if any */ if (info->docroot != NULL) { strlcpy(path, info->docroot, sizeof(path) - 1); if (path[strlen(path) - 1] != '/') strlcat(path, "/", sizeof(path)); } else *path = '\0'; /* Add fixed filename, if any */ if (info->filename != NULL) { if (*info->filename == '/') *path = '\0'; strlcat(path, info->filename, sizeof(path)); goto normalize; } /* Strip URL prefix, if it matches */ if (info->prefix != NULL && strncmp(urlpath, info->prefix, strlen(info->prefix)) == 0) urlpath += strlen(info->prefix); /* Derive remainder of pathname from URL */ strlcat(path, urlpath + (*urlpath == '/'), sizeof(path)); normalize: /* Normalize path */ if ((rpath = MALLOC(mtype, MAXPATHLEN)) == NULL) return (NULL); if (realpath(path, rpath) == NULL) { FREE(mtype, rpath); return (NULL); } rpath[MAXPATHLEN - 1] = '\0'; /* Verify that file is within the document root directory hierarchy */ if (!info->allow_escape) { const char *docroot; char *dpath; size_t rlen; int within; /* Use current working directory if info->docroot is NULL */ if (info->docroot == NULL) { getcwd(path, sizeof(path)); path[sizeof(path) - 1] = '\0'; docroot = path; } else docroot = info->docroot; /* Normalize docroot path */ if ((dpath = MALLOC(mtype, MAXPATHLEN)) == NULL) { FREE(mtype, rpath); return (NULL); } if (realpath(docroot, dpath) == NULL) { FREE(mtype, dpath); FREE(mtype, rpath); return (NULL); } dpath[MAXPATHLEN - 1] = '\0'; /* Verify that path is within the root */ rlen = strlen(dpath); within = strncmp(rpath, dpath, rlen) == 0 && (rpath[rlen] == '\0' || rpath[rlen] == '/'); FREE(mtype, dpath); if (!within) { FREE(mtype, rpath); errno = ENOENT; return (NULL); } } /* Done */ return (rpath); }