diff --git a/src/modules/mod_dirlist.c b/src/modules/mod_dirlist.c new file mode 100644 index 0000000..432998e --- /dev/null +++ b/src/modules/mod_dirlist.c @@ -0,0 +1,490 @@ +/* + * mod_dirlist - directory listing + * + * Description: + * mod_vhost offers various ways to implement virtual webhosts. + * It can map hostnames to document roots or even actions and offers multiple ways to do so. + * These ways differ in the flexibility of mapping (what to map and what to map to) as well as performance. + * + * Setups: + * none + * Options: + * vhost.debug = - enable debug output + * Actions: + * dirlist [options] - show directory listing + * options: optional (not required), array, can contain any of the following string => value pairs: + * "sort" => criterium - string, one of "name", "size" or "type" + * "css" => url - string, external css to use for styling, default: use internal css + * "hide-dotfiles" => bool - hide entries beginning with a dot, default: true + * "hide-tildefiles" => bool - hide entries ending with a tilde (~), often used for backups, default: true + * "include-header" => bool - include HEADER.txt above the directory listing, default: false + * "hide-header" => bool - hide HEADER.txt from the directory listing, default: false + * "include-readme" => bool - include README.txt below the directory listing, default: false + * "hide-header" => bool - hide README.txt from the directory listing, default: false + * + * Example config: + * dirlist ("include-header" => true, "hide-header" => true); + * - shows a directory listing including the content of HEADER.txt above the list and hiding itself from it + * + * Tip: + * xyz + * + * Todo: + * - filters for entries (pattern, regex) + * - include-* parameters + * - caching + * - javascript for sorting + * - sort parameter + * + * Author: + * Copyright (c) 2009 Thomas Porzelt + * License: + * MIT, see COPYING file in the lighttpd 2 tree + */ + +#include +#include + +LI_API gboolean mod_dirlist_init(modules *mods, module *mod); +LI_API gboolean mod_dirlist_free(modules *mods, module *mod); + +/* html snippet constants */ +static const gchar html_header[] = + "\n" + "\n" + "\n" + " \n" + " Index of %s\n"; + +static const gchar html_table_start[] = + " \n" + " \n" + "

Index of %s

\n" + "
\n" + " \n" + " \n" + " \n"; + +static const gchar html_table_row[] = + " " + "" + "" + "\n"; + +static const gchar html_table_end[] = + " \n" + "
NameLast ModifiedSizeType
%s%s%s%s
\n" + "
\n"; + +static const gchar html_footer[] = + "
%s
\n" + " \n" + ""; + +static const gchar html_css[] = + "\n"; + +struct dirlist_data { + plugin *plugin; + GString *css; + gboolean hide_dotfiles; + gboolean hide_tildefiles; +}; +typedef struct dirlist_data dirlist_data; + +struct dirlist_plugin_data { + GHashTable *cache; + GMutex *mutex; +}; +typedef struct dirlist_plugin_data dirlist_plugin_data; + +static void dirlist_format_size(gchar *buf, goffset size) { + const gchar unit[] = "BKMGTPE"; /* Kilo, Mega, Tera, Peta, Exa */ + gchar *u = (gchar*)unit; + guint remaining = 0; + + while (size > 1024) { + remaining = size & 1023; /* % 1024 */ + size >>= 10; /* /= 1024 */ + u++; + } + + remaining /= 100; + if (remaining > 9) + remaining = 9; + if (size > 999) { + size = 0; + remaining = 9; + u++; + } + + if (size > 99) { + *buf++ = size / 100 + '0'; + size = size % 100; + *buf++ = size / 10 + '0'; + *buf++ = (size % 10) + '0'; + } else if (size > 9) { + *buf++ = size / 10 + '0'; + *buf++ = (size % 10) + '0'; + } else { + *buf++ = size + '0'; + } + + buf[0] = '.'; + buf[1] = remaining + '0'; + buf[2] = *u; + buf[3] = '\0'; +} + +static handler_t dirlist(vrequest *vr, gpointer param, gpointer *context) { + GString *listing; + stat_cache_entry *sce; + dirlist_data *dd; + dirlist_plugin_data *pd; + + UNUSED(context); + + if (!vr->stat_cache_entry) { + if (vr->physical.path->len == 0) return HANDLER_GO_ON; + + if (!vrequest_handle_direct(vr)) return HANDLER_GO_ON; + } + + dd = param; + pd = dd->plugin->data; + + /* redirect to scheme + host + path + / + querystring if directory without trailing slash */ + /* TODO: local addr if HTTP 1.0 without host header, url encoding */ + if (vr->request.uri.path->str[vr->request.uri.path->len-1] != G_DIR_SEPARATOR) { + GString *host = vr->request.uri.authority->len ? vr->request.uri.authority : vr->con->local_addr_str; + GString *uri = g_string_sized_new( + 8 /* https:// */ + host->len + + vr->request.uri.orig_path->len + 2 /* /? */ + vr->request.uri.query->len + ); + + if (vr->con->is_ssl) + g_string_append_len(uri, CONST_STR_LEN("https://")); + else + g_string_append_len(uri, CONST_STR_LEN("http://")); + g_string_append_len(uri, GSTR_LEN(host)); + g_string_append_len(uri, GSTR_LEN(vr->request.uri.orig_path)); + g_string_append_c(uri, '/'); + if (vr->request.uri.query->len) { + g_string_append_c(uri, '?'); + g_string_append_len(uri, GSTR_LEN(vr->request.uri.query)); + } + + vr->response.http_status = 301; + http_header_overwrite(vr->response.headers, CONST_STR_LEN("Location"), GSTR_LEN(uri)); + g_string_free(uri, TRUE); + return HANDLER_GO_ON; + } + + sce = stat_cache_get_dir(vr, vr->physical.path); + if (!sce) + return HANDLER_WAIT_FOR_EVENT; + + if (sce->data.failed) { + /* stat failed */ + VR_DEBUG(vr, "stat(\"%s\") failed: %s (%d)", sce->data.path->str, g_strerror(sce->data.err), sce->data.err); + + switch (errno) { + case ENOENT: + vr->response.http_status = 404; break; + case EACCES: + case EFAULT: + vr->response.http_status = 403; break; + default: + vr->response.http_status = 500; + } + } else { + /* everything ok, we have the directory listing */ + vr->response.http_status = 200; + guint i; + stat_cache_entry_data *sced; + GString *mime_str; + GArray *directories; + GArray *files; + GString *encoded; + gchar sizebuf[sizeof("999.9K")+1]; + gchar datebuf[sizeof("2005-Jan-01 22:23:24")+1]; + struct tm tm; + + /* temporary string for encoded names */ + encoded = g_string_sized_new(64); + + http_header_overwrite(vr->response.headers, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/html")); + + /* seperate directories from other files */ + directories = g_array_sized_new(FALSE, FALSE, sizeof(guint), 16); + files = g_array_sized_new(FALSE, FALSE, sizeof(guint), sce->dirlist->len); + for (i = 0; i < sce->dirlist->len; i++) { + sced = &g_array_index(sce->dirlist, stat_cache_entry_data, i); + + /* ingore entries where the stat() failed */ + if (sced->failed) + continue; + + if (dd->hide_dotfiles && sced->path->str[0] == '.') + continue; + + if (dd->hide_tildefiles && sced->path->str[sced->path->len-1] == '~') + continue; + + if (S_ISDIR(sced->st.st_mode)) + g_array_append_val(directories, i); + else + g_array_append_val(files, i); + + } + + listing = g_string_sized_new(4*1024); + g_string_append_printf(listing, html_header, vr->request.uri.path->str); + + if (dd->css) { + /* custom css */ + g_string_append_len(listing, CONST_STR_LEN(" css)); + g_string_append_len(listing, CONST_STR_LEN("\" />\n")); + } else { + /* default css */ + g_string_append_len(listing, CONST_STR_LEN(html_css)); + } + + g_string_append_printf(listing, html_table_start, vr->request.uri.path->str); + + g_string_append_printf(listing, html_table_row, "../", + "Parent Directory", (gint64)0, "", (gint64)0, "-", "Directory"); + + /* list directories */ + for (i = 0; i < directories->len; i++) { + sced = &g_array_index(sce->dirlist, stat_cache_entry_data, g_array_index(directories, guint, i)); + + localtime_r(&(sced->st.st_mtime), &tm); + datebuf[strftime(datebuf, sizeof(datebuf), "%Y-%b-%d %H:%M:%S", &tm)] = '\0'; + + g_string_append_len(listing, CONST_STR_LEN(" path->str, encoded, ENCODING_URI); + g_string_append_len(listing, GSTR_LEN(encoded)); + g_string_append_len(listing, CONST_STR_LEN("/\">")); + string_encode(sced->path->str, encoded, ENCODING_HTML); + g_string_append_len(listing, GSTR_LEN(encoded)); + g_string_append_printf(listing, + "" + "%s" + "%s" + "%s\n", + (gint64)sced->st.st_mtime, datebuf, + (gint64)0, "-", + "Directory" + ); + + /* + g_string_append_printf(listing, html_table_row, + vr->request.uri.path->str, sced->path->str, sced->path->str, + (gint64)sced->st.st_mtime, datebuf, + (gint64)0, "-", + "Directory"); + */ + } + + g_string_append_len(listing, CONST_STR_LEN(" \n")); + + /* list files */ + for (i = 0; i < files->len; i++) { + sced = &g_array_index(sce->dirlist, stat_cache_entry_data, g_array_index(files, guint, i)); + mime_str = mimetype_get(vr, sced->path); + + localtime_r(&(sced->st.st_mtime), &tm); + datebuf[strftime(datebuf, sizeof(datebuf), "%Y-%b-%d %H:%M:%S", &tm)] = '\0'; + + dirlist_format_size(sizebuf, sced->st.st_size); + + + g_string_append_len(listing, CONST_STR_LEN(" path->str, encoded, ENCODING_URI); + g_string_append_len(listing, GSTR_LEN(encoded)); + g_string_append_len(listing, CONST_STR_LEN("\">")); + string_encode(sced->path->str, encoded, ENCODING_HTML); + g_string_append_len(listing, GSTR_LEN(encoded)); + g_string_append_printf(listing, + "" + "%s" + "%s" + "%s\n", + (gint64)sced->st.st_mtime, datebuf, + (gint64)0, sizebuf, + mime_str ? mime_str->str : "application/octet-stream" + ); + + /* + g_string_append_printf(listing, html_table_row, + sced->path->str, sced->path->str, + (gint64)sced->st.st_mtime, datebuf, + sced->st.st_size, sizebuf, + mime_str ? mime_str->str : "application/octet-stream"); + */ + } + + g_string_append_len(listing, CONST_STR_LEN(html_table_end)); + + g_string_append_printf(listing, html_footer, CORE_OPTION(CORE_OPTION_SERVER_TAG).string->str); + + chunkqueue_append_string(vr->out, listing); + g_array_free(directories, TRUE); + g_array_free(files, TRUE); + } + + stat_cache_entry_release(vr); + + return HANDLER_GO_ON; +} + +static void dirlist_free(server *srv, gpointer param) { + dirlist_data *data = param; + + UNUSED(srv); + + if (data->css) + g_string_free(data->css, TRUE); + + g_slice_free(dirlist_data, data); +} + +static action* dirlist_create(server *srv, plugin* p, value *val) { + dirlist_data *data; + guint i; + value *v, *tmpval; + GString *k; + + if (val && val->type != VALUE_LIST) { + ERROR(srv, "%s", "dirlist expects an optional list of string-value pairs"); + return NULL; + } + + data = g_slice_new0(dirlist_data); + data->plugin = p; + data->hide_dotfiles = TRUE; + data->hide_tildefiles = TRUE; + + if (val) { + for (i = 0; i < val->data.list->len; i++) { + tmpval = g_array_index(val->data.list, value*, i); + if (tmpval->type != VALUE_LIST || tmpval->data.list->len != 2 || + g_array_index(tmpval->data.list, value*, 0)->type != VALUE_STRING) { + ERROR(srv, "%s", "dirlist expects an optional list of string-value pairs"); + dirlist_free(srv, data); + return NULL; + } + + k = g_array_index(tmpval->data.list, value*, 0)->data.string; + v = g_array_index(tmpval->data.list, value*, 1); + + if (g_str_equal(k->str, "css")) { + if (v->type != VALUE_STRING) { + ERROR(srv, "%s", "dirlisting: css parameter must be a string"); + dirlist_free(srv, data); + return NULL; + } + data->css = g_string_new_len(GSTR_LEN(v->data.string)); + } else if (g_str_equal(k->str, "hide-dotfiles")) { + if (v->type != VALUE_BOOLEAN) { + ERROR(srv, "%s", "dirlisting: hide-dotfiles parameter must be a boolean (true or false)"); + dirlist_free(srv, data); + return NULL; + } + data->hide_dotfiles = v->data.boolean; + } else if (g_str_equal(k->str, "hide-tildefiles")) { + if (v->type != VALUE_BOOLEAN) { + ERROR(srv, "%s", "dirlisting: hide-tildefiles parameter must be a boolean (true or false)"); + dirlist_free(srv, data); + return NULL; + } + data->hide_tildefiles = v->data.boolean; + } else { + ERROR(srv, "dirlisting: unknown parameter \"%s\"", k->str); + dirlist_free(srv, data); + return NULL; + } + } + } + + return action_new_function(dirlist, NULL, dirlist_free, data); +} + +static const plugin_option options[] = { + { "dirlist.debug", VALUE_BOOLEAN, NULL, NULL, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; + +static const plugin_action actions[] = { + { "dirlist", dirlist_create }, + + { NULL, NULL } +}; + +static const plugin_setup setups[] = { + { NULL, NULL } +}; + + +static void plugin_dirlist_free(server *srv, plugin *p) { + dirlist_plugin_data *pd; + + UNUSED(srv); + + pd = p->data; + g_hash_table_destroy(pd->cache); + g_mutex_free(pd->mutex); + g_slice_free(dirlist_plugin_data, pd); +} + + +static void plugin_dirlist_init(server *srv, plugin *p) { + dirlist_plugin_data *pd; + + UNUSED(srv); + + p->options = options; + p->actions = actions; + p->setups = setups; + p->free = plugin_dirlist_free; + + pd = g_slice_new0(dirlist_plugin_data); + pd->cache = g_hash_table_new_full((GHashFunc)g_string_hash, (GEqualFunc)g_string_equal, string_destroy_notify, string_destroy_notify); + pd->mutex = g_mutex_new(); + p->data = pd; +} + + +gboolean mod_dirlist_init(modules *mods, module *mod) { + UNUSED(mod); + + MODULE_VERSION_CHECK(mods); + + mod->config = plugin_register(mods->main, "mod_dirlist", plugin_dirlist_init); + + return mod->config != NULL; +} + +gboolean mod_dirlist_free(modules *mods, module *mod) { + if (mod->config) + plugin_free(mods->main, mod->config); + + return TRUE; +} \ No newline at end of file