diff --git a/Makefile-ostree.am b/Makefile-ostree.am index 76cd0479..94d5d088 100644 --- a/Makefile-ostree.am +++ b/Makefile-ostree.am @@ -44,6 +44,7 @@ ostree_SOURCES = src/ostree/main.c \ src/ostree/ot-builtin-remote.c \ src/ostree/ot-builtin-rev-parse.c \ src/ostree/ot-builtin-show.c \ + src/ostree/ot-builtin-trivial-httpd.c \ src/ostree/ot-builtin-write-refs.c \ src/ostree/ot-main.h \ src/ostree/ot-main.c \ diff --git a/src/ostree/main.c b/src/ostree/main.c index 2648ff49..da6db638 100644 --- a/src/ostree/main.c +++ b/src/ostree/main.c @@ -49,6 +49,7 @@ static OstreeCommand commands[] = { { "remote", ostree_builtin_remote, 0 }, { "rev-parse", ostree_builtin_rev_parse, 0 }, { "show", ostree_builtin_show, 0 }, + { "trivial-httpd", ostree_builtin_trivial_httpd, OSTREE_BUILTIN_FLAG_NO_REPO }, { "write-refs", ostree_builtin_write_refs, 0 }, { NULL } }; diff --git a/src/ostree/ot-builtin-trivial-httpd.c b/src/ostree/ot-builtin-trivial-httpd.c new file mode 100644 index 00000000..83484fc9 --- /dev/null +++ b/src/ostree/ot-builtin-trivial-httpd.c @@ -0,0 +1,348 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2011,2013 Colin Walters + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include + +#include "ot-builtins.h" +#include "ot-admin-builtins.h" +#include "ot-admin-functions.h" +#include "ot-main.h" +#include "ostree.h" +#include "ostree-repo-file.h" + +#include + +static char *opt_port_file = NULL; +static gboolean opt_daemonize; +static gboolean opt_autoexit; + +typedef struct { + GFile *root; + gboolean running; +} OtTrivialHttpd; + +static GOptionEntry options[] = { + { "daemonize", 'd', 0, G_OPTION_ARG_NONE, &opt_daemonize, "Fork into background when ready", NULL }, + { "autoexit", 0, 0, G_OPTION_ARG_NONE, &opt_autoexit, "Automatically exit when directory is deleted", NULL }, + { "port-file", 'p', 0, G_OPTION_ARG_FILENAME, &opt_port_file, "Write port number to PATH", "PATH" }, + { NULL } +}; + +static int +compare_strings (gconstpointer a, gconstpointer b) +{ + const char **sa = (const char **)a; + const char **sb = (const char **)b; + + return strcmp (*sa, *sb); +} + +static GString * +get_directory_listing (const char *path) +{ + GPtrArray *entries; + GString *listing; + char *escaped; + DIR *dir; + struct dirent *dent; + int i; + + entries = g_ptr_array_new (); + dir = opendir (path); + if (dir) + { + while ((dent = readdir (dir))) + { + if (!strcmp (dent->d_name, ".") || + (!strcmp (dent->d_name, "..") && + !strcmp (path, "./"))) + continue; + escaped = g_markup_escape_text (dent->d_name, -1); + g_ptr_array_add (entries, escaped); + } + closedir (dir); + } + + g_ptr_array_sort (entries, (GCompareFunc)compare_strings); + + listing = g_string_new ("\r\n"); + escaped = g_markup_escape_text (strchr (path, '/'), -1); + g_string_append_printf (listing, "Index of %s\r\n", escaped); + g_string_append_printf (listing, "

Index of %s

\r\n

\r\n", escaped); + g_free (escaped); + for (i = 0; i < entries->len; i++) + { + g_string_append_printf (listing, "%s
\r\n", + (char *)entries->pdata[i], + (char *)entries->pdata[i]); + g_free (entries->pdata[i]); + } + g_string_append (listing, "\r\n\r\n"); + + g_ptr_array_free (entries, TRUE); + return listing; +} + +/* Only allow reading files that have o+r, and for directories, o+x. + * This makes this server relatively safe to use on multiuser + * machines. + */ +static gboolean +is_safe_to_access (struct stat *stbuf) +{ + /* Only regular files or directores */ + if (!(S_ISREG (stbuf->st_mode) || S_ISDIR (stbuf->st_mode))) + return FALSE; + /* Must be o+r */ + if (!(stbuf->st_mode & S_IROTH)) + return FALSE; + /* For directories, must be o+x */ + if (S_ISDIR (stbuf->st_mode) && !(stbuf->st_mode & S_IXOTH)) + return FALSE; + return TRUE; +} + +static void +do_get (OtTrivialHttpd *self, + SoupServer *server, + SoupMessage *msg, + const char *path) +{ + char *slash; + int ret; + struct stat stbuf; + gs_free char *safepath = NULL; + + if (strstr (path, "../") != NULL) + { + soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); + goto out; + } + + if (path[0] == '/') + path++; + + safepath = g_build_filename (gs_file_get_path_cached (self->root), path, NULL); + + do + ret = stat (safepath, &stbuf); + while (ret == -1 && errno == EINTR); + if (ret == -1) + { + if (errno == EPERM) + soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); + else if (errno == ENOENT) + soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND); + else + soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR); + goto out; + } + + if (!is_safe_to_access (&stbuf)) + { + soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); + goto out; + } + + if (S_ISDIR (stbuf.st_mode)) + { + slash = strrchr (safepath, '/'); + if (!slash || slash[1]) + { + gs_free char *redir_uri = NULL; + + redir_uri = g_strdup_printf ("%s/", soup_message_get_uri (msg)->path); + soup_message_set_redirect (msg, SOUP_STATUS_MOVED_PERMANENTLY, + redir_uri); + } + else + { + gs_free char *index_realpath = g_strconcat (safepath, "/index.html", NULL); + if (stat (index_realpath, &stbuf) != -1) + { + gs_free char *index_path = g_strconcat (path, "/index.html", NULL); + do_get (self, server, msg, index_path); + } + else + { + GString *listing = get_directory_listing (safepath); + soup_message_set_response (msg, "text/html", + SOUP_MEMORY_TAKE, + listing->str, listing->len); + g_string_free (listing, FALSE); + } + } + } + else + { + if (!S_ISREG (stbuf.st_mode)) + { + soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); + goto out; + } + + if (msg->method == SOUP_METHOD_GET) + { + GMappedFile *mapping; + SoupBuffer *buffer; + + mapping = g_mapped_file_new (safepath, FALSE, NULL); + if (!mapping) + { + soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR); + goto out; + } + + buffer = soup_buffer_new_with_owner (g_mapped_file_get_contents (mapping), + g_mapped_file_get_length (mapping), + mapping, (GDestroyNotify)g_mapped_file_unref); + soup_message_body_append_buffer (msg->response_body, buffer); + soup_buffer_free (buffer); + } + else /* msg->method == SOUP_METHOD_HEAD */ + { + gs_free char *length = NULL; + + /* We could just use the same code for both GET and + * HEAD (soup-message-server-io.c will fix things up). + * But we'll optimize and avoid the extra I/O. + */ + length = g_strdup_printf ("%lu", (gulong)stbuf.st_size); + soup_message_headers_append (msg->response_headers, + "Content-Length", length); + } + soup_message_set_status (msg, SOUP_STATUS_OK); + } + out: + return; +} + +static void +httpd_callback (SoupServer *server, SoupMessage *msg, + const char *path, GHashTable *query, + SoupClientContext *context, gpointer data) +{ + OtTrivialHttpd *self = data; + SoupMessageHeadersIter iter; + + soup_message_headers_iter_init (&iter, msg->request_headers); + + if (msg->method == SOUP_METHOD_GET || msg->method == SOUP_METHOD_HEAD) + do_get (self, server, msg, path); + else + soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED); +} + +static void +on_dir_changed (GFileMonitor *mon, + GFile *file, + GFile *other, + GFileMonitorEvent event, + gpointer user_data) +{ + OtTrivialHttpd *self = user_data; + + if (event == G_FILE_MONITOR_EVENT_DELETED) + { + self->running = FALSE; + g_main_context_wakeup (NULL); + } +} + +gboolean +ostree_builtin_trivial_httpd (int argc, char **argv, GFile *repo_path, GError **error) +{ + gboolean ret = FALSE; + GCancellable *cancellable = NULL; + GOptionContext *context; + const char *dirpath; + OtTrivialHttpd appstruct; + OtTrivialHttpd *app = &appstruct; + gs_unref_object GFile *dir = NULL; + gs_unref_object SoupServer *server = NULL; + gs_unref_object GFileMonitor *dirmon = NULL; + + memset (&appstruct, 0, sizeof (appstruct)); + + context = g_option_context_new ("[DIR] - Simple webserver"); + + g_option_context_add_main_entries (context, options, NULL); + + if (!g_option_context_parse (context, &argc, &argv, error)) + goto out; + + if (argc > 1) + dirpath = argv[1]; + else + dirpath = "."; + + app->root = g_file_new_for_path (dirpath); + + server = soup_server_new (SOUP_SERVER_PORT, 0, + SOUP_SERVER_SERVER_HEADER, "ostree-httpd ", + NULL); + soup_server_add_handler (server, NULL, httpd_callback, app, NULL); + if (opt_port_file) + { + gs_free char *portstr = g_strdup_printf ("%u\n", soup_server_get_port (server)); + if (!g_file_set_contents (opt_port_file, portstr, strlen (portstr), error)) + goto out; + } + soup_server_run_async (server); + + if (opt_daemonize) + { + pid_t pid = fork(); + if (pid == -1) + { + int errsv = errno; + g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (errsv), + g_strerror (errsv)); + goto out; + } + else if (pid > 0) + { + /* Parent */ + _exit (0); + } + /* Child, continue */ + } + + app->running = TRUE; + if (opt_autoexit) + { + dirmon = g_file_monitor_directory (app->root, 0, cancellable, error); + if (!dirmon) + goto out; + g_signal_connect (dirmon, "changed", G_CALLBACK (on_dir_changed), app); + } + + while (app->running) + g_main_context_iteration (NULL, TRUE); + + ret = TRUE; + out: + g_clear_object (&app->root); + if (context) + g_option_context_free (context); + return ret; +} diff --git a/src/ostree/ot-builtins.h b/src/ostree/ot-builtins.h index e8748640..9847c5a4 100644 --- a/src/ostree/ot-builtins.h +++ b/src/ostree/ot-builtins.h @@ -46,6 +46,7 @@ gboolean ostree_builtin_show (int argc, char **argv, GFile *repo_path, GError ** gboolean ostree_builtin_rev_parse (int argc, char **argv, GFile *repo_path, GError **error); gboolean ostree_builtin_remote (int argc, char **argv, GFile *repo_path, GError **error); gboolean ostree_builtin_write_refs (int argc, char **argv, GFile *repo_path, GError **error); +gboolean ostree_builtin_trivial_httpd (int argc, char **argv, GFile *repo_path, GError **error); G_END_DECLS diff --git a/tests/Makefile b/tests/Makefile deleted file mode 100644 index 18d4d822..00000000 --- a/tests/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -# Toplevel tests Makefile -# -# Copyright (C) 2011 Colin Walters -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the -# Free Software Foundation, Inc., 59 Temple Place - Suite 330, -# Boston, MA 02111-1307, USA. - -TESTS = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh) - -all: tmpdir-lifecycle run-apache - -tmpdir-lifecycle: tmpdir-lifecycle.c Makefile - gcc $(CFLAGS) `pkg-config --cflags --libs gio-unix-2.0` -o $@ $< - -run-apache: run-apache.c Makefile - gcc $(CFLAGS) `pkg-config --cflags --libs gio-unix-2.0` -o $@ $< - -check: - @for test in $(TESTS); do \ - echo $$test; \ - ./$$test; \ - done - diff --git a/tests/libtest.sh b/tests/libtest.sh index 12ddd646..0da1564a 100644 --- a/tests/libtest.sh +++ b/tests/libtest.sh @@ -102,9 +102,6 @@ setup_test_repository () { } setup_fake_remote_repo1() { - if ! test -x ${SRCDIR}/run-apache; then - exit 77 - fi mode=$1 shift oldpwd=`pwd` @@ -131,36 +128,10 @@ setup_fake_remote_repo1() { cd ${test_tmpdir} mkdir ${test_tmpdir}/httpd cd httpd - cat >httpd.conf < ${test_tmpdir}/httpd-address cd ${oldpwd} export OSTREE="ostree --repo=repo" diff --git a/tests/run-apache.c b/tests/run-apache.c deleted file mode 100644 index cabccb94..00000000 --- a/tests/run-apache.c +++ /dev/null @@ -1,168 +0,0 @@ -/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- - * - * Copyright (C) 2011 Colin Walters - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - * - * Author: Colin Walters - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* Taken from gnome-user-share src/httpd.c under the GPLv2 */ -static int -get_port (void) -{ - int sock; - int saved_errno; - struct sockaddr_in addr; - int reuse; - socklen_t len; - - sock = socket (PF_INET, SOCK_STREAM, 0); - if (sock < 0) - { - return -1; - } - - memset (&addr, 0, sizeof (addr)); - addr.sin_family = AF_INET; - addr.sin_port = 0; - addr.sin_addr.s_addr = htonl (INADDR_LOOPBACK); - - reuse = 1; - setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof (reuse)); - if (bind (sock, (struct sockaddr *)&addr, sizeof (addr)) == -1) - { - saved_errno = errno; - close (sock); - errno = saved_errno; - return -1; - } - - len = sizeof (addr); - if (getsockname (sock, (struct sockaddr *)&addr, &len) == -1) - { - saved_errno = errno; - close (sock); - errno = saved_errno; - return -1; - } - -#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__) - /* XXX This exposes a potential race condition, but without this, - * httpd will not start on the above listed platforms due to the fact - * that SO_REUSEADDR is also needed when Apache binds to the listening - * socket. At this time, Apache does not support that socket option. - */ - close (sock); -#endif - return ntohs (addr.sin_port); -} - -static const char *known_httpd_modules_locations [] = { - "/usr/libexec/apache2", - "/usr/lib/apache2/modules", - "/usr/lib64/httpd/modules", - "/usr/lib/httpd/modules", - NULL -}; - -static gchar* -get_httpd_modules_path () -{ - int i; - - for (i = 0; known_httpd_modules_locations[i]; i++) - { - if (g_file_test (known_httpd_modules_locations[i], G_FILE_TEST_IS_EXECUTABLE) - && g_file_test (known_httpd_modules_locations[i], G_FILE_TEST_IS_DIR)) - { - return g_strdup (known_httpd_modules_locations[i]); - } - } - return NULL; -} - -int -main (int argc, - char **argv) -{ - int port; - char *listen; - char *address_string; - GError *error = NULL; - GPtrArray *httpd_argv; - char *modules; - - if (argc != 3) - { - fprintf (stderr, "usage: run-apache CONF PORTFILE"); - return 1; - } - - g_type_init (); - - port = get_port (); - if (port == -1) - { - perror ("Failed to bind port"); - return 1; - } - - httpd_argv = g_ptr_array_new (); - g_ptr_array_add (httpd_argv, "httpd"); - g_ptr_array_add (httpd_argv, "-DFOREGROUND"); - g_ptr_array_add (httpd_argv, "-f"); - g_ptr_array_add (httpd_argv, argv[1]); - g_ptr_array_add (httpd_argv, "-C"); - listen = g_strdup_printf ("Listen 127.0.0.1:%d", port); - g_ptr_array_add (httpd_argv, listen); - g_ptr_array_add (httpd_argv, NULL); - - address_string = g_strdup_printf ("http://127.0.0.1:%d\n", port); - - if (!g_file_set_contents (argv[2], address_string, -1, &error)) - { - g_printerr ("%s\n", error->message); - return 1; - } - - setenv ("LANG", "C", 1); - modules = get_httpd_modules_path (); - if (modules == NULL) - { - g_printerr ("Failed to find httpd modules\n"); - return 1; - } - if (symlink (modules, "modules") < 0) - { - perror ("failed to make modules symlink"); - return 1; - } - execvp ("httpd", (char**)httpd_argv->pdata); - perror ("Failed to run httpd"); - return 1; -} diff --git a/tests/tmpdir-lifecycle.c b/tests/tmpdir-lifecycle.c deleted file mode 100644 index 7345df18..00000000 --- a/tests/tmpdir-lifecycle.c +++ /dev/null @@ -1,103 +0,0 @@ -/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- - * - * Kill a child process when the current directory is deleted - * - * Copyright (C) 2011 Colin Walters - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - * - * Author: Colin Walters - */ - -#include -#include -#include -#include - -struct TmpdirLifecyleData { - GMainLoop *loop; - GPid pid; - gboolean exited; -}; - -static void -on_dir_changed (GFileMonitor *mon, - GFile *file, - GFile *other, - GFileMonitorEvent event, - gpointer user_data) -{ - struct TmpdirLifecyleData *data = user_data; - - if (event == G_FILE_MONITOR_EVENT_DELETED) - g_main_loop_quit (data->loop); -} - -static void -on_child_exited (GPid pid, - int status, - gpointer user_data) -{ - struct TmpdirLifecyleData *data = user_data; - - data->exited = TRUE; - g_main_loop_quit (data->loop); -} - -int -main (int argc, - char **argv) -{ - GFileMonitor *monitor; - GFile *curdir; - GError *error = NULL; - GPtrArray *new_argv; - int i; - struct TmpdirLifecyleData data; - - g_type_init (); - - memset (&data, 0, sizeof (data)); - - data.loop = g_main_loop_new (NULL, TRUE); - - curdir = g_file_new_for_path ("."); - monitor = g_file_monitor_directory (curdir, 0, NULL, &error); - if (!monitor) - exit (1); - g_signal_connect (monitor, "changed", - G_CALLBACK (on_dir_changed), &data); - - new_argv = g_ptr_array_new (); - for (i = 1; i < argc; i++) - g_ptr_array_add (new_argv, argv[i]); - g_ptr_array_add (new_argv, NULL); - - if (!g_spawn_async (NULL, (char**)new_argv->pdata, NULL, G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, - NULL, NULL, &data.pid, &error)) - { - g_printerr ("%s\n", error->message); - return 1; - } - g_child_watch_add (data.pid, on_child_exited, &data); - - g_main_loop_run (data.loop); - - if (!data.exited) - kill (data.pid, SIGTERM); - - return 0; -}