diff --git a/Makefile-ostree.am b/Makefile-ostree.am index d46fc22528..05fec15520 100644 --- a/Makefile-ostree.am +++ b/Makefile-ostree.am @@ -103,7 +103,8 @@ EXTRA_DIST += src/ostree/parse-datetime.y CLEANFILES += src/ostree/parse-datetime.c ostree_bin_shared_cflags = $(AM_CFLAGS) -I$(srcdir)/src/libotutil -I$(srcdir)/src/libostree \ - -I$(srcdir)/src/ostree -I$(srcdir)/libglnx $(OT_INTERNAL_GIO_UNIX_CFLAGS) + -I$(srcdir)/src/ostree -I$(srcdir)/libglnx $(OT_INTERNAL_GIO_UNIX_CFLAGS) \ + -DPKGLIBEXECDIR=\"$(pkglibexecdir)\" ostree_bin_shared_ldadd = $(AM_LDFLAGS) libglnx.la libotutil.la libostree-1.la \ $(OT_INTERNAL_GIO_UNIX_LIBS) @@ -112,12 +113,14 @@ ostree_LDADD = $(ostree_bin_shared_ldadd) libbsdiff.la libostree-kernel-args.la if USE_LIBSOUP -ostree_SOURCES += \ - src/ostree/ot-builtin-pull.c \ - src/ostree/ot-builtin-trivial-httpd.c \ - $(NULL) +ostree_SOURCES += src/ostree/ot-builtin-pull.c src/ostree/ot-builtin-trivial-httpd.c ostree_CFLAGS += $(OT_INTERNAL_SOUP_CFLAGS) ostree_LDADD += $(OT_INTERNAL_SOUP_LIBS) + +pkglibexec_PROGRAMS += ostree-trivial-httpd +ostree_trivial_httpd_SOURCES = src/ostree/ostree-trivial-httpd.c +ostree_trivial_httpd_CFLAGS = $(ostree_bin_shared_cflags) $(OT_INTERNAL_SOUP_CFLAGS) +ostree_trivial_httpd_LDADD = $(ostree_bin_shared_ldadd) $(OT_INTERNAL_SOUP_LIBS) endif if USE_LIBARCHIVE diff --git a/Makefile-tests.am b/Makefile-tests.am index 1f9cad48b5..63ceea1eb1 100644 --- a/Makefile-tests.am +++ b/Makefile-tests.am @@ -28,6 +28,7 @@ EXTRA_DIST += \ # include the builddir in $PATH so we find our just-built ostree # binary. TESTS_ENVIRONMENT += OT_TESTS_DEBUG=1 \ + OSTREE_UNINSTALLED=$(abs_top_builddir) \ G_DEBUG=fatal-warnings \ GI_TYPELIB_PATH=$$(cd $(top_builddir) && pwd)$${GI_TYPELIB_PATH:+:$$GI_TYPELIB_PATH} \ LD_LIBRARY_PATH=$$(cd $(top_builddir)/.libs && pwd)$${LD_LIBRARY_PATH:+:$${LD_LIBRARY_PATH}} \ diff --git a/src/ostree/ostree-trivial-httpd.c b/src/ostree/ostree-trivial-httpd.c new file mode 100644 index 0000000000..ef297af6a2 --- /dev/null +++ b/src/ostree/ostree-trivial-httpd.c @@ -0,0 +1,683 @@ +/* -*- 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 + +#include "ot-main.h" +#include "ot-builtins.h" +#include "ostree.h" +#include "otutil.h" + +#include +#include +#include +#include + +static char *opt_port_file = NULL; +static char *opt_log = NULL; +static gboolean opt_daemonize; +static gboolean opt_autoexit; +static gboolean opt_force_ranges; +static int opt_random_500s_percentage; +/* We have a strong upper bound for any unlikely + * cases involving repeated random 500s. */ +static int opt_random_500s_max = 100; +static gint opt_port = 0; +static gchar **opt_expected_cookies; +static gchar **opt_expected_headers; + +static guint emitted_random_500s_count = 0; + +typedef struct { + int root_dfd; + gboolean running; + GOutputStream *log; +} 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", 'P', 0, G_OPTION_ARG_INT, &opt_port, "Use the specified TCP port", NULL }, + { "port-file", 'p', 0, G_OPTION_ARG_FILENAME, &opt_port_file, "Write port number to PATH (- for standard output)", "PATH" }, + { "force-range-requests", 0, 0, G_OPTION_ARG_NONE, &opt_force_ranges, "Force range requests by only serving half of files", NULL }, + { "random-500s", 0, 0, G_OPTION_ARG_INT, &opt_random_500s_percentage, "Generate random HTTP 500 errors approximately for PERCENTAGE requests", "PERCENTAGE" }, + { "random-500s-max", 0, 0, G_OPTION_ARG_INT, &opt_random_500s_max, "Limit HTTP 500 errors to MAX (default 100)", "MAX" }, + { "log-file", 0, 0, G_OPTION_ARG_FILENAME, &opt_log, "Put logs here", "PATH" }, + { "expected-cookies", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_expected_cookies, "Expect given cookies in the http request", "KEY=VALUE" }, + { "expected-header", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_expected_headers, "Expect given headers in the http request", "KEY=VALUE" }, + { NULL } +}; + +static void +httpd_log (OtTrivialHttpd *httpd, const gchar *format, ...) __attribute__ ((format(printf, 2, 3))); + +static void +httpd_log (OtTrivialHttpd *httpd, const gchar *format, ...) +{ + g_autoptr(GString) str = NULL; + va_list args; + gsize written; + + if (!httpd->log) + return; + + { + g_autoptr(GDateTime) now = g_date_time_new_now_local (); + g_autofree char *timestamp = g_date_time_format (now, "%F %T"); + str = g_string_new (timestamp); + g_string_append_printf (str, ".%06d - ", g_date_time_get_microsecond (now)); + } + + va_start (args, format); + g_string_append_vprintf (str, format, args); + va_end (args); + + g_output_stream_write_all (httpd->log, str->str, str->len, &written, NULL, 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 (int dfd, + const char *path) +{ + g_autoptr(GPtrArray) entries = g_ptr_array_new_with_free_func (g_free); + g_auto(GLnxDirFdIterator) dfd_iter = { 0, }; + g_autoptr(GError) local_error = NULL; + GError **error = &local_error; + guint i; + char *escaped; + GString *listing; + + listing = g_string_new ("\r\n"); + + if (!glnx_dirfd_iterator_init_at (dfd, path, FALSE, &dfd_iter, error)) + goto out; + + while (TRUE) + { + struct dirent *dent; + + if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, NULL, error)) + goto out; + + if (dent == NULL) + break; + + escaped = g_markup_escape_text (dent->d_name, -1); + g_ptr_array_add (entries, escaped); + } + + g_ptr_array_sort (entries, (GCompareFunc)compare_strings); + + 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 (g_steal_pointer (&entries->pdata[i])); + } + g_string_append (listing, "\r\n\r\n"); + out: + if (local_error) + g_printerr ("%s\n", local_error->message); + 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 +close_socket (SoupMessage *msg, gpointer user_data) +{ + SoupSocket *sock = user_data; + int sockfd; + + /* Actually calling soup_socket_disconnect() here would cause + * us to leak memory, so just shutdown the socket instead. + */ + sockfd = soup_socket_get_fd (sock); +#ifdef G_OS_WIN32 + shutdown (sockfd, SD_SEND); +#else + shutdown (sockfd, SHUT_WR); +#endif +} + +static void +do_get (OtTrivialHttpd *self, + SoupServer *server, + SoupMessage *msg, + const char *path, + SoupClientContext *context) +{ + char *slash; + int ret; + struct stat stbuf; + + httpd_log (self, "serving %s\n", path); + + if (opt_expected_cookies) + { + GSList *cookies = soup_cookies_from_request (msg); + GSList *l; + int i; + + for (i = 0 ; opt_expected_cookies[i] != NULL; i++) + { + gboolean found = FALSE; + gchar *k = opt_expected_cookies[i]; + gchar *v = strchr (k, '=') + 1; + + for (l = cookies; l != NULL ; l = g_slist_next (l)) + { + SoupCookie *c = l->data; + + if (!strncmp (k, soup_cookie_get_name (c), v - k - 1) && + !strcmp (v, soup_cookie_get_value (c))) + { + found = TRUE; + break; + } + } + + if (!found) + { + httpd_log (self, "Expected cookie not found %s\n", k); + soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); + soup_cookies_free (cookies); + goto out; + } + } + soup_cookies_free (cookies); + } + + if (opt_expected_headers) + { + for (int i = 0 ; opt_expected_headers[i] != NULL; i++) + { + const gchar *kv = opt_expected_headers[i]; + const gchar *eq = strchr (kv, '='); + + g_assert (eq); + + { + g_autofree char *k = g_strndup (kv, eq - kv); + const gchar *expected_v = eq + 1; + const gchar *found_v = soup_message_headers_get_one (msg->request_headers, k); + + if (!found_v) + { + httpd_log (self, "Expected header not found %s\n", k); + soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); + goto out; + } + if (strcmp (found_v, expected_v) != 0) + { + httpd_log (self, "Expected header %s: %s but found %s\n", k, expected_v, found_v); + soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); + goto out; + } + } + } + } + + if (strstr (path, "../") != NULL) + { + soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); + goto out; + } + + if (opt_random_500s_percentage > 0 && + emitted_random_500s_count < opt_random_500s_max && + g_random_int_range (0, 100) < opt_random_500s_percentage) + { + emitted_random_500s_count++; + soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR); + goto out; + } + + while (path[0] == '/') + path++; + + do + ret = fstatat (self->root_dfd, path, &stbuf, 0); + 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 (path, '/'); + if (!slash || slash[1]) + { + g_autofree 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 + { + g_autofree char *index_realpath = g_strconcat (path, "/index.html", NULL); + if (fstatat (self->root_dfd, index_realpath, &stbuf, 0) != -1) + { + g_autofree char *index_path = g_strconcat (path, "/index.html", NULL); + do_get (self, server, msg, index_path, context); + } + else + { + GString *listing = get_directory_listing (self->root_dfd, path); + soup_message_set_response (msg, "text/html", + SOUP_MEMORY_TAKE, + listing->str, listing->len); + soup_message_set_status (msg, SOUP_STATUS_OK); + 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) + { + glnx_fd_close int fd = -1; + g_autoptr(GMappedFile) mapping = NULL; + gsize buffer_length, file_size; + SoupRange *ranges; + int ranges_length; + gboolean have_ranges; + + fd = openat (self->root_dfd, path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + { + soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR); + goto out; + } + + mapping = g_mapped_file_new_from_fd (fd, FALSE, NULL); + if (!mapping) + { + soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR); + goto out; + } + (void) close (fd); fd = -1; + + file_size = g_mapped_file_get_length (mapping); + have_ranges = soup_message_headers_get_ranges(msg->request_headers, file_size, &ranges, &ranges_length); + if (opt_force_ranges && !have_ranges && g_strrstr (path, "/objects") != NULL) + { + SoupSocket *sock; + buffer_length = file_size/2; + soup_message_headers_set_content_length (msg->response_headers, file_size); + soup_message_headers_append (msg->response_headers, + "Connection", "close"); + + /* soup-message-io will wait for us to add + * another chunk after the first, to fill out + * the declared Content-Length. Instead, we + * forcibly close the socket at that point. + */ + sock = soup_client_context_get_socket (context); + g_signal_connect (msg, "wrote-chunk", G_CALLBACK (close_socket), sock); + } + else + buffer_length = file_size; + + if (have_ranges) + { + if (ranges_length > 0 && ranges[0].start >= file_size) + { + soup_message_set_status (msg, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE); + soup_message_headers_free_ranges (msg->request_headers, ranges); + goto out; + } + soup_message_headers_free_ranges (msg->request_headers, ranges); + } + if (buffer_length > 0) + { + SoupBuffer *buffer; + + buffer = soup_buffer_new_with_owner (g_mapped_file_get_contents (mapping), + buffer_length, + g_mapped_file_ref (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 */ + { + g_autofree 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: + { + guint status = 0; + g_autofree gchar *reason = NULL; + + g_object_get (msg, + "status-code", &status, + "reason-phrase", &reason, + NULL); + httpd_log (self, " status: %s (%u)\n", reason, status); + } + return; +} + +static void +httpd_callback (SoupServer *server, SoupMessage *msg, + const char *path, GHashTable *query, + SoupClientContext *context, gpointer data) +{ + OtTrivialHttpd *self = data; + + if (msg->method == SOUP_METHOD_GET || msg->method == SOUP_METHOD_HEAD) + do_get (self, server, msg, path, context); + 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); + } +} + +static gboolean +run (int argc, char **argv, GCancellable *cancellable, GError **error) +{ + gboolean ret = FALSE; + g_autoptr(GOptionContext) context = NULL; + const char *dirpath; + OtTrivialHttpd appstruct = { 0, }; + OtTrivialHttpd *app = &appstruct; + glnx_unref_object SoupServer *server = NULL; + g_autoptr(GFileMonitor) dirmon = NULL; + + context = g_option_context_new ("[DIR] - Simple webserver"); + g_option_context_add_main_entries (context, options, NULL); + + app->root_dfd = -1; + + if (!g_option_context_parse (context, &argc, &argv, error)) + goto out; + + if (argc > 1) + dirpath = argv[1]; + else + dirpath = "."; + + if (!glnx_opendirat (AT_FDCWD, dirpath, TRUE, &app->root_dfd, error)) + goto out; + + if (!(opt_random_500s_percentage >= 0 && opt_random_500s_percentage <= 99)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Invalid --random-500s=%u", opt_random_500s_percentage); + goto out; + } + + if (opt_log) + { + GOutputStream *stream = NULL; + + if (g_strcmp0 (opt_log, "-") == 0) + { + if (opt_daemonize) + { + ot_util_usage_error (context, "Cannot use --log-file=- and --daemonize at the same time", error); + goto out; + } + stream = G_OUTPUT_STREAM (g_unix_output_stream_new (STDOUT_FILENO, FALSE)); + } + else + { + g_autoptr(GFile) log_file; + GFileOutputStream* log_stream; + + log_file = g_file_new_for_path (opt_log); + log_stream = g_file_create (log_file, + G_FILE_CREATE_PRIVATE, + cancellable, + error); + if (!log_stream) + goto out; + stream = G_OUTPUT_STREAM (log_stream); + } + + app->log = stream; + } + +#if SOUP_CHECK_VERSION(2, 48, 0) + server = soup_server_new (SOUP_SERVER_SERVER_HEADER, "ostree-httpd ", NULL); + if (!soup_server_listen_all (server, opt_port, 0, error)) + goto out; +#else + server = soup_server_new (SOUP_SERVER_PORT, opt_port, + SOUP_SERVER_SERVER_HEADER, "ostree-httpd ", + NULL); +#endif + + soup_server_add_handler (server, NULL, httpd_callback, app, NULL); + if (opt_port_file) + { + g_autofree char *portstr = NULL; +#if SOUP_CHECK_VERSION(2, 48, 0) + GSList *listeners = soup_server_get_listeners (server); + g_autoptr(GSocket) listener = NULL; + g_autoptr(GSocketAddress) addr = NULL; + + g_assert (listeners); + listener = g_object_ref (listeners->data); + g_slist_free (listeners); + listeners = NULL; + addr = g_socket_get_local_address (listener, error); + if (!addr) + goto out; + + g_assert (G_IS_INET_SOCKET_ADDRESS (addr)); + + portstr = g_strdup_printf ("%u\n", g_inet_socket_address_get_port ((GInetSocketAddress*)addr)); +#else + portstr = g_strdup_printf ("%u\n", soup_server_get_port (server)); +#endif + + if (g_strcmp0 ("-", opt_port_file) == 0) + { + fputs (portstr, stdout); // not g_print - this must go to stdout, not a handler + fflush (stdout); + } + else if (!g_file_set_contents (opt_port_file, portstr, strlen (portstr), error)) + goto out; + } +#if !SOUP_CHECK_VERSION(2, 48, 0) + soup_server_run_async (server); +#endif + + 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) + { + ret = TRUE; + goto out; + } + /* Child, continue */ + /* Daemonising: close stdout/stderr so $() et al work on us */ + fclose (stdout); + fclose (stdin); + } + else + { + /* Since we're used for testing purposes, let's just do this by + * default. This ensures we exit when our parent does. + */ + if (prctl (PR_SET_PDEATHSIG, SIGTERM) != 0) + { + if (errno != ENOSYS) + { + glnx_set_error_from_errno (error); + goto out; + } + } + } + + app->running = TRUE; + if (opt_autoexit) + { + gboolean is_symlink = FALSE; + g_autoptr(GFile) root = NULL; + g_autoptr(GFileInfo) info = NULL; + + root = g_file_new_for_path (dirpath); + info = g_file_query_info (root, + G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable, error); + if (!info) + goto out; + + is_symlink = g_file_info_get_is_symlink (info); + + if (is_symlink) + dirmon = g_file_monitor_file (root, 0, cancellable, error); + else + dirmon = g_file_monitor_directory (root, 0, cancellable, error); + + if (!dirmon) + goto out; + g_signal_connect (dirmon, "changed", G_CALLBACK (on_dir_changed), app); + } + httpd_log (app, "serving at root %s\n", dirpath); + while (app->running) + g_main_context_iteration (NULL, TRUE); + + ret = TRUE; + out: + if (app->root_dfd != -1) + (void) close (app->root_dfd); + g_clear_object (&app->log); + return ret; +} + +int +main (int argc, + char **argv) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GCancellable) cancellable = NULL; + + setlocale (LC_ALL, ""); + + g_set_prgname (argv[0]); + + if (!run (argc, argv, cancellable, &error)) + { + int is_tty = isatty (1); + const char *prefix = ""; + const char *suffix = ""; + if (is_tty) + { + prefix = "\x1b[31m\x1b[1m"; /* red, bold */ + suffix = "\x1b[22m\x1b[0m"; /* bold off, color reset */ + } + g_printerr ("%serror: %s%s\n", prefix, suffix, error->message); + return 1; + } + + return 0; +} diff --git a/src/ostree/ot-builtin-trivial-httpd.c b/src/ostree/ot-builtin-trivial-httpd.c index 0a55385895..206970c56d 100644 --- a/src/ostree/ot-builtin-trivial-httpd.c +++ b/src/ostree/ot-builtin-trivial-httpd.c @@ -1,6 +1,6 @@ /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * - * Copyright (C) 2011,2013 Colin Walters + * Copyright (C) 2016 Colin Walters * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,634 +20,22 @@ #include "config.h" -#include - -#include - #include "ot-main.h" #include "ot-builtins.h" #include "ostree.h" #include "otutil.h" -#include -#include -#include - -static char *opt_port_file = NULL; -static char *opt_log = NULL; -static gboolean opt_daemonize; -static gboolean opt_autoexit; -static gboolean opt_force_ranges; -static int opt_random_500s_percentage; -/* We have a strong upper bound for any unlikely - * cases involving repeated random 500s. */ -static int opt_random_500s_max = 100; -static gint opt_port = 0; -static gchar **opt_expected_cookies; -static gchar **opt_expected_headers; - -static guint emitted_random_500s_count = 0; - -typedef struct { - int root_dfd; - gboolean running; - GOutputStream *log; -} 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", 'P', 0, G_OPTION_ARG_INT, &opt_port, "Use the specified TCP port", NULL }, - { "port-file", 'p', 0, G_OPTION_ARG_FILENAME, &opt_port_file, "Write port number to PATH (- for standard output)", "PATH" }, - { "force-range-requests", 0, 0, G_OPTION_ARG_NONE, &opt_force_ranges, "Force range requests by only serving half of files", NULL }, - { "random-500s", 0, 0, G_OPTION_ARG_INT, &opt_random_500s_percentage, "Generate random HTTP 500 errors approximately for PERCENTAGE requests", "PERCENTAGE" }, - { "random-500s-max", 0, 0, G_OPTION_ARG_INT, &opt_random_500s_max, "Limit HTTP 500 errors to MAX (default 100)", "MAX" }, - { "log-file", 0, 0, G_OPTION_ARG_FILENAME, &opt_log, "Put logs here", "PATH" }, - { "expected-cookies", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_expected_cookies, "Expect given cookies in the http request", "KEY=VALUE" }, - { "expected-header", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_expected_headers, "Expect given headers in the http request", "KEY=VALUE" }, - { NULL } -}; - -static void -httpd_log (OtTrivialHttpd *httpd, const gchar *format, ...) __attribute__ ((format(printf, 2, 3))); - -static void -httpd_log (OtTrivialHttpd *httpd, const gchar *format, ...) -{ - g_autoptr(GString) str = NULL; - va_list args; - gsize written; - - if (!httpd->log) - return; - - { - g_autoptr(GDateTime) now = g_date_time_new_now_local (); - g_autofree char *timestamp = g_date_time_format (now, "%F %T"); - str = g_string_new (timestamp); - g_string_append_printf (str, ".%06d - ", g_date_time_get_microsecond (now)); - } - - va_start (args, format); - g_string_append_vprintf (str, format, args); - va_end (args); - - g_output_stream_write_all (httpd->log, str->str, str->len, &written, NULL, 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 (int dfd, - const char *path) -{ - g_autoptr(GPtrArray) entries = g_ptr_array_new_with_free_func (g_free); - g_auto(GLnxDirFdIterator) dfd_iter = { 0, }; - g_autoptr(GError) local_error = NULL; - GError **error = &local_error; - guint i; - char *escaped; - GString *listing; - - listing = g_string_new ("\r\n"); - - if (!glnx_dirfd_iterator_init_at (dfd, path, FALSE, &dfd_iter, error)) - goto out; - - while (TRUE) - { - struct dirent *dent; - - if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, NULL, error)) - goto out; - - if (dent == NULL) - break; - - escaped = g_markup_escape_text (dent->d_name, -1); - g_ptr_array_add (entries, escaped); - } - - g_ptr_array_sort (entries, (GCompareFunc)compare_strings); - - 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 (g_steal_pointer (&entries->pdata[i])); - } - g_string_append (listing, "\r\n\r\n"); - out: - if (local_error) - g_printerr ("%s\n", local_error->message); - 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 -close_socket (SoupMessage *msg, gpointer user_data) -{ - SoupSocket *sock = user_data; - int sockfd; - - /* Actually calling soup_socket_disconnect() here would cause - * us to leak memory, so just shutdown the socket instead. - */ - sockfd = soup_socket_get_fd (sock); -#ifdef G_OS_WIN32 - shutdown (sockfd, SD_SEND); -#else - shutdown (sockfd, SHUT_WR); -#endif -} - -static void -do_get (OtTrivialHttpd *self, - SoupServer *server, - SoupMessage *msg, - const char *path, - SoupClientContext *context) -{ - char *slash; - int ret; - struct stat stbuf; - - httpd_log (self, "serving %s\n", path); - - if (opt_expected_cookies) - { - GSList *cookies = soup_cookies_from_request (msg); - GSList *l; - int i; - - for (i = 0 ; opt_expected_cookies[i] != NULL; i++) - { - gboolean found = FALSE; - gchar *k = opt_expected_cookies[i]; - gchar *v = strchr (k, '=') + 1; - - for (l = cookies; l != NULL ; l = g_slist_next (l)) - { - SoupCookie *c = l->data; - - if (!strncmp (k, soup_cookie_get_name (c), v - k - 1) && - !strcmp (v, soup_cookie_get_value (c))) - { - found = TRUE; - break; - } - } - - if (!found) - { - httpd_log (self, "Expected cookie not found %s\n", k); - soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); - soup_cookies_free (cookies); - goto out; - } - } - soup_cookies_free (cookies); - } - - if (opt_expected_headers) - { - for (int i = 0 ; opt_expected_headers[i] != NULL; i++) - { - const gchar *kv = opt_expected_headers[i]; - const gchar *eq = strchr (kv, '='); - - g_assert (eq); - - { - g_autofree char *k = g_strndup (kv, eq - kv); - const gchar *expected_v = eq + 1; - const gchar *found_v = soup_message_headers_get_one (msg->request_headers, k); - - if (!found_v) - { - httpd_log (self, "Expected header not found %s\n", k); - soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); - goto out; - } - if (strcmp (found_v, expected_v) != 0) - { - httpd_log (self, "Expected header %s: %s but found %s\n", k, expected_v, found_v); - soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); - goto out; - } - } - } - } - - if (strstr (path, "../") != NULL) - { - soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); - goto out; - } - - if (opt_random_500s_percentage > 0 && - emitted_random_500s_count < opt_random_500s_max && - g_random_int_range (0, 100) < opt_random_500s_percentage) - { - emitted_random_500s_count++; - soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR); - goto out; - } - - while (path[0] == '/') - path++; - - do - ret = fstatat (self->root_dfd, path, &stbuf, 0); - 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 (path, '/'); - if (!slash || slash[1]) - { - g_autofree 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 - { - g_autofree char *index_realpath = g_strconcat (path, "/index.html", NULL); - if (fstatat (self->root_dfd, index_realpath, &stbuf, 0) != -1) - { - g_autofree char *index_path = g_strconcat (path, "/index.html", NULL); - do_get (self, server, msg, index_path, context); - } - else - { - GString *listing = get_directory_listing (self->root_dfd, path); - soup_message_set_response (msg, "text/html", - SOUP_MEMORY_TAKE, - listing->str, listing->len); - soup_message_set_status (msg, SOUP_STATUS_OK); - 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) - { - glnx_fd_close int fd = -1; - g_autoptr(GMappedFile) mapping = NULL; - gsize buffer_length, file_size; - SoupRange *ranges; - int ranges_length; - gboolean have_ranges; - - fd = openat (self->root_dfd, path, O_RDONLY | O_CLOEXEC); - if (fd < 0) - { - soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR); - goto out; - } - - mapping = g_mapped_file_new_from_fd (fd, FALSE, NULL); - if (!mapping) - { - soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR); - goto out; - } - (void) close (fd); fd = -1; - - file_size = g_mapped_file_get_length (mapping); - have_ranges = soup_message_headers_get_ranges(msg->request_headers, file_size, &ranges, &ranges_length); - if (opt_force_ranges && !have_ranges && g_strrstr (path, "/objects") != NULL) - { - SoupSocket *sock; - buffer_length = file_size/2; - soup_message_headers_set_content_length (msg->response_headers, file_size); - soup_message_headers_append (msg->response_headers, - "Connection", "close"); - - /* soup-message-io will wait for us to add - * another chunk after the first, to fill out - * the declared Content-Length. Instead, we - * forcibly close the socket at that point. - */ - sock = soup_client_context_get_socket (context); - g_signal_connect (msg, "wrote-chunk", G_CALLBACK (close_socket), sock); - } - else - buffer_length = file_size; - - if (have_ranges) - { - if (ranges_length > 0 && ranges[0].start >= file_size) - { - soup_message_set_status (msg, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE); - soup_message_headers_free_ranges (msg->request_headers, ranges); - goto out; - } - soup_message_headers_free_ranges (msg->request_headers, ranges); - } - if (buffer_length > 0) - { - SoupBuffer *buffer; - - buffer = soup_buffer_new_with_owner (g_mapped_file_get_contents (mapping), - buffer_length, - g_mapped_file_ref (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 */ - { - g_autofree 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: - { - guint status = 0; - g_autofree gchar *reason = NULL; - - g_object_get (msg, - "status-code", &status, - "reason-phrase", &reason, - NULL); - httpd_log (self, " status: %s (%u)\n", reason, status); - } - return; -} - -static void -httpd_callback (SoupServer *server, SoupMessage *msg, - const char *path, GHashTable *query, - SoupClientContext *context, gpointer data) -{ - OtTrivialHttpd *self = data; - - if (msg->method == SOUP_METHOD_GET || msg->method == SOUP_METHOD_HEAD) - do_get (self, server, msg, path, context); - 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, GCancellable *cancellable, GError **error) { - gboolean ret = FALSE; - g_autoptr(GOptionContext) context = NULL; - const char *dirpath; - OtTrivialHttpd appstruct = { 0, }; - OtTrivialHttpd *app = &appstruct; - glnx_unref_object SoupServer *server = NULL; - g_autoptr(GFileMonitor) dirmon = NULL; - - context = g_option_context_new ("[DIR] - Simple webserver"); - - app->root_dfd = -1; - - if (!ostree_option_context_parse (context, options, &argc, &argv, OSTREE_BUILTIN_FLAG_NO_REPO, NULL, cancellable, error)) - goto out; - - if (argc > 1) - dirpath = argv[1]; - else - dirpath = "."; - - if (!glnx_opendirat (AT_FDCWD, dirpath, TRUE, &app->root_dfd, error)) - goto out; - - if (!(opt_random_500s_percentage >= 0 && opt_random_500s_percentage <= 99)) - { - g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, - "Invalid --random-500s=%u", opt_random_500s_percentage); - goto out; - } - - if (opt_log) - { - GOutputStream *stream = NULL; - - if (g_strcmp0 (opt_log, "-") == 0) - { - if (opt_daemonize) - { - ot_util_usage_error (context, "Cannot use --log-file=- and --daemonize at the same time", error); - goto out; - } - stream = G_OUTPUT_STREAM (g_unix_output_stream_new (STDOUT_FILENO, FALSE)); - } - else - { - g_autoptr(GFile) log_file; - GFileOutputStream* log_stream; - - log_file = g_file_new_for_path (opt_log); - log_stream = g_file_create (log_file, - G_FILE_CREATE_PRIVATE, - cancellable, - error); - if (!log_stream) - goto out; - stream = G_OUTPUT_STREAM (log_stream); - } - - app->log = stream; - } - -#if SOUP_CHECK_VERSION(2, 48, 0) - server = soup_server_new (SOUP_SERVER_SERVER_HEADER, "ostree-httpd ", NULL); - if (!soup_server_listen_all (server, opt_port, 0, error)) - goto out; -#else - server = soup_server_new (SOUP_SERVER_PORT, opt_port, - SOUP_SERVER_SERVER_HEADER, "ostree-httpd ", - NULL); -#endif - - soup_server_add_handler (server, NULL, httpd_callback, app, NULL); - if (opt_port_file) - { - g_autofree char *portstr = NULL; -#if SOUP_CHECK_VERSION(2, 48, 0) - GSList *listeners = soup_server_get_listeners (server); - g_autoptr(GSocket) listener = NULL; - g_autoptr(GSocketAddress) addr = NULL; - - g_assert (listeners); - listener = g_object_ref (listeners->data); - g_slist_free (listeners); - listeners = NULL; - addr = g_socket_get_local_address (listener, error); - if (!addr) - goto out; - - g_assert (G_IS_INET_SOCKET_ADDRESS (addr)); - - portstr = g_strdup_printf ("%u\n", g_inet_socket_address_get_port ((GInetSocketAddress*)addr)); -#else - portstr = g_strdup_printf ("%u\n", soup_server_get_port (server)); -#endif - - if (g_strcmp0 ("-", opt_port_file) == 0) - { - fputs (portstr, stdout); // not g_print - this must go to stdout, not a handler - fflush (stdout); - } - else if (!g_file_set_contents (opt_port_file, portstr, strlen (portstr), error)) - goto out; - } -#if !SOUP_CHECK_VERSION(2, 48, 0) - soup_server_run_async (server); -#endif - - 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) - { - ret = TRUE; - goto out; - } - /* Child, continue */ - /* Daemonising: close stdout/stderr so $() et al work on us */ - fclose (stdout); - fclose (stdin); - } - else - { - /* Since we're used for testing purposes, let's just do this by - * default. This ensures we exit when our parent does. - */ - if (prctl (PR_SET_PDEATHSIG, SIGTERM) != 0) - { - if (errno != ENOSYS) - { - glnx_set_error_from_errno (error); - goto out; - } - } - } - - app->running = TRUE; - if (opt_autoexit) - { - gboolean is_symlink = FALSE; - g_autoptr(GFile) root = NULL; - g_autoptr(GFileInfo) info = NULL; - - root = g_file_new_for_path (dirpath); - info = g_file_query_info (root, - G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK, - G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, - cancellable, error); - if (!info) - goto out; - - is_symlink = g_file_info_get_is_symlink (info); - - if (is_symlink) - dirmon = g_file_monitor_file (root, 0, cancellable, error); - else - dirmon = g_file_monitor_directory (root, 0, cancellable, error); - - if (!dirmon) - goto out; - g_signal_connect (dirmon, "changed", G_CALLBACK (on_dir_changed), app); - } - httpd_log (app, "serving at root %s\n", dirpath); - while (app->running) - g_main_context_iteration (NULL, TRUE); + g_autoptr(GPtrArray) new_argv = g_ptr_array_new (); - ret = TRUE; - out: - if (app->root_dfd != -1) - (void) close (app->root_dfd); - g_clear_object (&app->log); - return ret; + g_ptr_array_add (new_argv, PKGLIBEXECDIR "/ostree-trivial-httpd"); + for (int i = 1; i < argc; i++) + g_ptr_array_add (new_argv, argv[i]); + g_ptr_array_add (new_argv, NULL); + execvp (new_argv->pdata[0], (char**)new_argv->pdata); + /* Fall through on error */ + glnx_set_error_from_errno (error); + return FALSE; } diff --git a/tests/libtest.sh b/tests/libtest.sh index c0bf8d0dba..137d9534f2 100755 --- a/tests/libtest.sh +++ b/tests/libtest.sh @@ -102,6 +102,12 @@ else fi fi +if test -n "${OSTREE_UNINSTALLED:-}"; then + OSTREE_HTTPD=${OSTREE_UNINSTALLED}/ostree-trivial-httpd +else + OSTREE_HTTPD="${CMD_PREFIX} ostree trivial-httpd" +fi + assert_streq () { test "$1" = "$2" || (echo 1>&2 "$1 != $2"; exit 1) } @@ -257,7 +263,7 @@ setup_fake_remote_repo1() { mkdir ${test_tmpdir}/httpd cd httpd ln -s ${test_tmpdir}/ostree-srv ostree - ${CMD_PREFIX} ostree trivial-httpd --autoexit --daemonize -p ${test_tmpdir}/httpd-port $args + ${OSTREE_HTTPD} --autoexit --daemonize -p ${test_tmpdir}/httpd-port $args port=$(cat ${test_tmpdir}/httpd-port) echo "http://127.0.0.1:${port}" > ${test_tmpdir}/httpd-address cd ${oldpwd} @@ -379,7 +385,7 @@ EOF mkdir ${test_tmpdir}/httpd cd httpd ln -s ${test_tmpdir} ostree - ${CMD_PREFIX} ostree trivial-httpd --autoexit --daemonize -p ${test_tmpdir}/httpd-port + ${OSTREE_HTTPD} --autoexit --daemonize -p ${test_tmpdir}/httpd-port port=$(cat ${test_tmpdir}/httpd-port) echo "http://127.0.0.1:${port}" > ${test_tmpdir}/httpd-address cd ${oldpwd} diff --git a/tests/test-commit-sign.sh b/tests/test-commit-sign.sh index 60265c1a99..f963b1045b 100755 --- a/tests/test-commit-sign.sh +++ b/tests/test-commit-sign.sh @@ -53,7 +53,7 @@ cd ${test_tmpdir} mkdir ${test_tmpdir}/httpd cd httpd ln -s ${test_tmpdir}/ostree-srv ostree -${CMD_PREFIX} ostree trivial-httpd --autoexit --daemonize -P 18081 -p ${test_tmpdir}/httpd-port +${OSTREE_HTTPD} --autoexit --daemonize -P 18081 -p ${test_tmpdir}/httpd-port port=$(cat ${test_tmpdir}/httpd-port) assert_streq $port 18081 echo "http://127.0.0.1:${port}" > ${test_tmpdir}/httpd-address diff --git a/tests/test-pull-contenturl.sh b/tests/test-pull-contenturl.sh index 16dcbe4fc4..d74d619b3f 100755 --- a/tests/test-pull-contenturl.sh +++ b/tests/test-pull-contenturl.sh @@ -51,8 +51,7 @@ fi find ${test_tmpdir}/ostree-srv/gnomerepo/objects \ ! -name '*.commitmeta' -type f | xargs rm -${CMD_PREFIX} ostree trivial-httpd --autoexit --daemonize \ - -p ${test_tmpdir}/httpd-content-port +${OSTREE_HTTPD} --autoexit --daemonize -p ${test_tmpdir}/httpd-content-port content_port=$(cat ${test_tmpdir}/httpd-content-port) echo "http://127.0.0.1:${content_port}" > ${test_tmpdir}/httpd-content-address diff --git a/tests/test-pull-metalink.sh b/tests/test-pull-metalink.sh index 2a1a73e776..07d619dfe2 100755 --- a/tests/test-pull-metalink.sh +++ b/tests/test-pull-metalink.sh @@ -29,7 +29,7 @@ echo '1..9' cd ${test_tmpdir} mkdir metalink-data cd metalink-data -${CMD_PREFIX} ostree trivial-httpd --autoexit --daemonize -p ${test_tmpdir}/metalink-httpd-port +${OSTREE_HTTPD} --autoexit --daemonize -p ${test_tmpdir}/metalink-httpd-port metalink_port=$(cat ${test_tmpdir}/metalink-httpd-port) echo "http://127.0.0.1:${metalink_port}" > ${test_tmpdir}/metalink-httpd-address diff --git a/tests/test-pull-mirrorlist.sh b/tests/test-pull-mirrorlist.sh index 454014ca78..13f40e7a6b 100755 --- a/tests/test-pull-mirrorlist.sh +++ b/tests/test-pull-mirrorlist.sh @@ -33,7 +33,7 @@ setup_mirror () { cd $name cp -a ${test_tmpdir}/ostree-srv ostree - ${CMD_PREFIX} ostree trivial-httpd --autoexit --daemonize \ + ${OSTREE_HTTPD} --autoexit --daemonize \ -p ${test_tmpdir}/${name}-port port=$(cat ${test_tmpdir}/${name}-port) echo "http://127.0.0.1:${port}" > ${test_tmpdir}/${name}-address diff --git a/tests/test-pull-override-url.sh b/tests/test-pull-override-url.sh index d81b345494..928013a574 100755 --- a/tests/test-pull-override-url.sh +++ b/tests/test-pull-override-url.sh @@ -50,7 +50,7 @@ mkdir mirror-httpd cd mirror-httpd ln -s ${test_tmpdir}/mirror-srv ostree mirror_log="${test_tmpdir}/mirror_log" -${CMD_PREFIX} ostree trivial-httpd --log-file=${mirror_log} --autoexit --daemonize -p ${test_tmpdir}/mirror-httpd-port +${OSTREE_HTTPD} --log-file=${mirror_log} --autoexit --daemonize -p ${test_tmpdir}/mirror-httpd-port port=$(cat ${test_tmpdir}/mirror-httpd-port) echo "http://127.0.0.1:${port}" > ${test_tmpdir}/mirror-httpd-address