From 8e7652a2f96ea78b546e7b3fd04d4969c49b0085 Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Wed, 24 Feb 2021 12:28:08 +1300 Subject: Add a method to override the remote connection. This allows using a Unix socket to communicate on. Fixes https://gitlab.gnome.org/GNOME/libsoup/-/issues/75 --- docs/reference/libsoup-3.0-sections.txt | 1 + examples/meson.build | 6 +++ examples/unix-socket-client.c | 46 +++++++++++++++++++ examples/unix-socket-server.c | 79 ++++++++++++++++++++++++++++++++ libsoup/soup-session.c | 67 ++++++++++++++++++++++++--- libsoup/soup-session.h | 3 ++ meson.build | 6 +++ tests/meson.build | 10 +++- tests/test-utils.c | 54 +++++++++++++++++++++- tests/test-utils.h | 4 +- tests/unix-socket-test.c | 81 +++++++++++++++++++++++++++++++++ 11 files changed, 347 insertions(+), 10 deletions(-) create mode 100644 examples/unix-socket-client.c create mode 100644 examples/unix-socket-server.c create mode 100644 tests/unix-socket-test.c diff --git a/docs/reference/libsoup-3.0-sections.txt b/docs/reference/libsoup-3.0-sections.txt index 24a8a688..3c481939 100644 --- a/docs/reference/libsoup-3.0-sections.txt +++ b/docs/reference/libsoup-3.0-sections.txt @@ -384,6 +384,7 @@ soup_session_get_accept_language soup_session_set_accept_language_auto soup_session_get_accept_language_auto soup_session_get_async_result_message +soup_session_get_remote_connectable soup_session_send soup_session_send_async diff --git a/examples/meson.build b/examples/meson.build index a553aac3..189616b2 100644 --- a/examples/meson.build +++ b/examples/meson.build @@ -9,6 +9,12 @@ examples = [ 'simple-proxy' ] +if unix_socket_dep.found() + examples += [ 'unix-socket-client' ] + examples += [ 'unix-socket-server' ] + deps += [ unix_socket_dep ] +endif + foreach example: examples executable(example, example + '.c', dependencies: deps) endforeach diff --git a/examples/unix-socket-client.c b/examples/unix-socket-client.c new file mode 100644 index 00000000..df62422c --- /dev/null +++ b/examples/unix-socket-client.c @@ -0,0 +1,46 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2021 Canonical Ltd. + */ + +#include + +#include + +int +main (int argc, char **argv) +{ + SoupSession *session; + GSocketAddress *address; + SoupMessage *msg; + GBytes *body; + const char *content_type; + char *text; + GError *error = NULL; + + /* Create a session that uses a unix socket */ + address = g_unix_socket_address_new ("/tmp/libsoup-unix-server"); + session = soup_session_new_with_options ("remote-connectable", address, NULL); + g_object_unref (address); + + /* Do a GET across the unix socket */ + msg = soup_message_new (SOUP_METHOD_GET, "http://locahost"); + body = soup_session_send_and_read (session, msg, NULL, &error); + if (body == NULL) { + g_printerr ("Failed to contact HTTP server: %s\n", error->message); + return 1; + } + content_type = soup_message_headers_get_one (soup_message_get_response_headers (msg), "Content-Type"); + if (g_strcmp0 (content_type, "text/plain") != 0) { + g_printerr ("Server returned unexpected content-type: %s\n", content_type); + return 1; + } + text = g_strndup (g_bytes_get_data (body, NULL), g_bytes_get_size (body)); + g_printerr ("%s\n", text); + + g_object_unref (msg); + g_bytes_unref (body); + g_object_unref (session); + + return 0; +} diff --git a/examples/unix-socket-server.c b/examples/unix-socket-server.c new file mode 100644 index 00000000..2db481c8 --- /dev/null +++ b/examples/unix-socket-server.c @@ -0,0 +1,79 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2021 Canonical Ltd. + */ + +#include +#include + +#include + +#define SOCKET_PATH "/tmp/libsoup-unix-server" + +static void +server_callback (SoupServer *server, + SoupServerMessage *msg, + const char *path, + GHashTable *query, + gpointer data) +{ + const char *method; + + method = soup_server_message_get_method (msg); + if (method != SOUP_METHOD_GET) { + soup_server_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED, NULL); + return; + } + + soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL); + soup_server_message_set_response (msg, "text/plain", + SOUP_MEMORY_STATIC, "Hello World!", 12); +} + +int +main (int argc, char **argv) +{ + GSocket *listen_socket; + GSocketAddress *listen_address; + SoupServer *server; + GMainLoop *loop; + GError *error = NULL; + + /* Remove an existing socket */ + g_unlink (SOCKET_PATH); + + /* Create a server that uses a unix socket */ + listen_socket = g_socket_new (G_SOCKET_FAMILY_UNIX, + G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_DEFAULT, + &error); + if (listen_socket == NULL) { + g_printerr ("Unable to create unix socket: %s\n", error->message); + return 1; + } + listen_address = g_unix_socket_address_new (SOCKET_PATH); + if (!g_socket_bind (listen_socket, listen_address, TRUE, &error)) { + g_printerr ("Unable to bind unix socket to %s: %s\n", SOCKET_PATH, error->message); + return 1; + } + g_object_unref (listen_address); + if (!g_socket_listen (listen_socket, &error)) { + g_printerr ("Unable to listen on unix socket: %s\n", error->message); + return 1; + } + server = soup_server_new ("server-header", "unix-socket-server", NULL); + soup_server_add_handler (server, NULL, server_callback, NULL, NULL); + + if (!soup_server_listen_socket (server, listen_socket, 0, &error)) { + g_printerr ("Unable to listen on unix socket: %s\n", error->message); + return 1; + } + g_object_unref (listen_socket); + + loop = g_main_loop_new (NULL, TRUE); + g_main_loop_run (loop); + + g_object_unref (server); + + return 0; +} diff --git a/libsoup/soup-session.c b/libsoup/soup-session.c index 2b050169..6686afff 100644 --- a/libsoup/soup-session.c +++ b/libsoup/soup-session.c @@ -112,6 +112,8 @@ typedef struct { char *accept_language; gboolean accept_language_auto; + GSocketConnectable *remote_connectable; + GSList *features; GHashTable *features_cache; @@ -168,6 +170,7 @@ enum { PROP_USER_AGENT, PROP_ACCEPT_LANGUAGE, PROP_ACCEPT_LANGUAGE_AUTO, + PROP_REMOTE_CONNECTABLE, PROP_IDLE_TIMEOUT, PROP_LOCAL_ADDRESS, PROP_TLS_INTERACTION, @@ -299,6 +302,8 @@ soup_session_finalize (GObject *object) g_queue_free (priv->queue); g_source_unref (priv->queue_source); + g_clear_object (&priv->remote_connectable); + g_hash_table_destroy (priv->http_hosts); g_hash_table_destroy (priv->https_hosts); g_hash_table_destroy (priv->conns); @@ -390,6 +395,9 @@ soup_session_set_property (GObject *object, guint prop_id, case PROP_ACCEPT_LANGUAGE_AUTO: soup_session_set_accept_language_auto (session, g_value_get_boolean (value)); break; + case PROP_REMOTE_CONNECTABLE: + priv->remote_connectable = g_value_dup_object (value); + break; case PROP_IDLE_TIMEOUT: soup_session_set_idle_timeout (session, g_value_get_uint (value)); break; @@ -436,6 +444,9 @@ soup_session_get_property (GObject *object, guint prop_id, case PROP_ACCEPT_LANGUAGE_AUTO: g_value_set_boolean (value, soup_session_get_accept_language_auto (session)); break; + case PROP_REMOTE_CONNECTABLE: + g_value_set_object (value, soup_session_get_remote_connectable (session)); + break; case PROP_IDLE_TIMEOUT: g_value_set_uint (value, soup_session_get_idle_timeout (session)); break; @@ -946,6 +957,25 @@ soup_session_get_accept_language_auto (SoupSession *session) return priv->accept_language_auto; } +/** + * soup_session_get_remote_connectable: + * @session: a #SoupSession + * + * Get the remote connectable if one set. + * + * Returns: (transfer none) (nullable): the #GSocketConnectable or %NULL + */ +GSocketConnectable * +soup_session_get_remote_connectable (SoupSession *session) +{ + SoupSessionPrivate *priv; + + g_return_val_if_fail (SOUP_IS_SESSION (session), NULL); + + priv = soup_session_get_instance_private (session); + return priv->remote_connectable; +} + /* Hosts */ /* Note that we can't use soup_uri_host_hash() and soup_uri_host_equal() @@ -1819,12 +1849,16 @@ get_connection_for_host (SoupSession *session, return NULL; } - remote_connectable = - g_object_new (G_TYPE_NETWORK_ADDRESS, - "hostname", g_uri_get_host (host->uri), - "port", g_uri_get_port (host->uri), - "scheme", g_uri_get_scheme (host->uri), - NULL); + if (priv->remote_connectable == NULL) { + remote_connectable = + g_object_new (G_TYPE_NETWORK_ADDRESS, + "hostname", g_uri_get_host (host->uri), + "port", g_uri_get_port (host->uri), + "scheme", g_uri_get_scheme (host->uri), + NULL); + } else { + remote_connectable = g_object_ref (priv->remote_connectable); + } ensure_socket_props (session); conn = g_object_new (SOUP_TYPE_CONNECTION, @@ -2734,6 +2768,27 @@ soup_session_class_init (SoupSessionClass *session_class) G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + /** + * SoupSession:remote-connectable: + * + * Sets a socket to make outgoing connections on. This will override the default + * behaviour of opening TCP/IP sockets to the hosts specified in the URIs. + * + * This function is not required for common HTTP usage, but only when connecting + * to a HTTP service that is not using standard TCP/IP sockets. An example of + * this is a local service that uses HTTP over UNIX-domain sockets, in that case + * a #GUnixSocketAddress can be passed to this function. + * + **/ + g_object_class_install_property ( + object_class, PROP_REMOTE_CONNECTABLE, + g_param_spec_object ("remote-connectable", + "Remote Connectable", + "Socket to connect to make outgoing connections on", + G_TYPE_SOCKET_CONNECTABLE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + /** * SoupSession:local-address: * diff --git a/libsoup/soup-session.h b/libsoup/soup-session.h index 8266c9e7..1b4ae2ab 100644 --- a/libsoup/soup-session.h +++ b/libsoup/soup-session.h @@ -100,6 +100,9 @@ void soup_session_set_accept_language_auto (SoupSession *sess SOUP_AVAILABLE_IN_ALL gboolean soup_session_get_accept_language_auto (SoupSession *session); +SOUP_AVAILABLE_IN_ALL +GSocketConnectable *soup_session_get_remote_connectable (SoupSession *session); + SOUP_AVAILABLE_IN_ALL void soup_session_abort (SoupSession *session); diff --git a/meson.build b/meson.build index 24506ad1..0162a59d 100644 --- a/meson.build +++ b/meson.build @@ -128,6 +128,11 @@ if brotlidec_dep.found() cdata.set('WITH_BROTLI', true) endif +unix_socket_dep = dependency('gio-unix-2.0', + version : glib_required_version, + fallback: ['glib', 'libgiounix_dep'], + required : false) + platform_deps = [] is_static_library = get_option('default_library') == 'static' if not is_static_library @@ -396,6 +401,7 @@ summary({ 'Tests requiring Apache' : have_apache, 'Fuzzing tests' : get_option('fuzzing').enabled(), 'Install tests': get_option('installed_tests'), + 'Unix sockets' : unix_socket_dep.found(), }, section : 'Testing' ) diff --git a/tests/meson.build b/tests/meson.build index 1cae853d..c847a254 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -7,10 +7,10 @@ abs_installed_tests_execdir = join_paths(prefix, installed_tests_execdir) if cc.get_id() == 'msvc' test_utils = static_library(test_utils_name, test_utils_name + '.c', - dependencies : libsoup_static_dep) + dependencies : [ libsoup_static_dep, unix_socket_dep ]) else test_utils = library(test_utils_name, test_utils_name + '.c', - dependencies : libsoup_static_dep, + dependencies : [ libsoup_static_dep, unix_socket_dep ], install : installed_tests_enabled, install_dir : installed_tests_execdir, ) @@ -81,6 +81,12 @@ if brotlidec_dep.found() endif endif +if unix_socket_dep.found() + tests += [ + ['unix-socket', true, [ unix_socket_dep ]], + ] +endif + if have_apache tests += [ ['auth', false, []], diff --git a/tests/test-utils.c b/tests/test-utils.c index 697106a5..88247a98 100644 --- a/tests/test-utils.c +++ b/tests/test-utils.c @@ -4,6 +4,9 @@ #include "soup-misc.h" #include +#ifdef G_OS_UNIX +#include +#endif #include #include @@ -370,16 +373,27 @@ soup_test_session_send_message (SoupSession *session, return soup_message_get_status (msg); } +const char * +soup_test_server_get_unix_path (SoupServer *server) +{ + return g_object_get_data (G_OBJECT (server), "unix-socket-path"); +} + static void server_listen (SoupServer *server) { GError *error = NULL; SoupServerListenOptions options = 0; + GSocket *socket; if (g_getenv ("SOUP_TEST_NO_IPV6")) options = SOUP_SERVER_LISTEN_IPV4_ONLY; - soup_server_listen_local (server, 0, options, &error); + socket = g_object_get_data (G_OBJECT (server), "listen-socket"); + if (socket != NULL) + soup_server_listen_socket (server, socket, 0, &error); + else + soup_server_listen_local (server, 0, options, &error); if (error) { g_printerr ("Unable to create server: %s\n", error->message); exit (1); @@ -464,6 +478,44 @@ soup_test_server_new (SoupTestServerOptions options) g_object_set_data (G_OBJECT (server), "options", GUINT_TO_POINTER (options)); + if (options & SOUP_TEST_SERVER_UNIX_SOCKET) { +#ifdef G_OS_UNIX + char *socket_dir, *socket_path; + GSocket *listen_socket; + GSocketAddress *listen_address; + + socket_dir = g_dir_make_tmp ("unix-socket-test-XXXXXX", NULL); + socket_path = g_build_filename (socket_dir, "socket", NULL); + g_object_set_data_full (G_OBJECT (server), "unix-socket-path", socket_path, g_free); + g_free (socket_dir); + + listen_socket = g_socket_new (G_SOCKET_FAMILY_UNIX, + G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_DEFAULT, + &error); + if (listen_socket == NULL) { + g_printerr ("Unable to create unix socket: %s\n", error->message); + exit (1); + } + + listen_address = g_unix_socket_address_new (socket_path); + if (!g_socket_bind (listen_socket, listen_address, TRUE, &error)) { + g_printerr ("Unable to bind unix socket to %s: %s\n", socket_path, error->message); + exit (1); + } + g_object_unref (listen_address); + if (!g_socket_listen (listen_socket, &error)) { + g_printerr ("Unable to listen on unix socket: %s\n", error->message); + exit (1); + } + + g_object_set_data_full (G_OBJECT (server), "listen-socket", listen_socket, g_object_unref); +#else + g_printerr ("Unix socket support not available\n"); + exit (1); +#endif + } + if (options & SOUP_TEST_SERVER_IN_THREAD) soup_test_server_run_in_thread (server); else if (!(options & SOUP_TEST_SERVER_NO_DEFAULT_LISTENER)) diff --git a/tests/test-utils.h b/tests/test-utils.h index f3dc8798..74d77644 100644 --- a/tests/test-utils.h +++ b/tests/test-utils.h @@ -69,10 +69,12 @@ guint soup_test_session_send_message (SoupSession *session, typedef enum { SOUP_TEST_SERVER_DEFAULT = 0, SOUP_TEST_SERVER_IN_THREAD = (1 << 0), - SOUP_TEST_SERVER_NO_DEFAULT_LISTENER = (1 << 1) + SOUP_TEST_SERVER_NO_DEFAULT_LISTENER = (1 << 1), + SOUP_TEST_SERVER_UNIX_SOCKET = (1 << 2) } SoupTestServerOptions; SoupServer *soup_test_server_new (SoupTestServerOptions options); +const char *soup_test_server_get_unix_path (SoupServer *server); void soup_test_server_run_in_thread (SoupServer *server); GUri *soup_test_server_get_uri (SoupServer *server, const char *scheme, diff --git a/tests/unix-socket-test.c b/tests/unix-socket-test.c new file mode 100644 index 00000000..29ce3422 --- /dev/null +++ b/tests/unix-socket-test.c @@ -0,0 +1,81 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +#include "test-utils.h" + +#include + +static SoupServer *server; + +static void +server_callback (SoupServer *server, + SoupServerMessage *msg, + const char *path, + GHashTable *query, + gpointer data) +{ + const char *method; + + method = soup_server_message_get_method (msg); + if (method != SOUP_METHOD_GET) { + soup_server_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED, NULL); + return; + } + + soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL); + soup_server_message_set_response (msg, "application/json", + SOUP_MEMORY_STATIC, "{\"count\":42}", 12); +} + +static void +do_load_uri_test (void) +{ + SoupSession *session; + GSocketAddress *address; + SoupMessage *msg; + GBytes *body; + const char *content_type; + char *json; + GError *error = NULL; + + address = g_unix_socket_address_new (soup_test_server_get_unix_path (server)); + session = soup_test_session_new ("remote-connectable", address, NULL); + g_object_unref (address); + + msg = soup_message_new (SOUP_METHOD_GET, "http://locahost/foo"); + body = soup_session_send_and_read (session, msg, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (body); + + content_type = soup_message_headers_get_one (soup_message_get_response_headers (msg), "Content-Type"); + g_assert_cmpstr (content_type, ==, "application/json"); + g_object_unref (msg); + + json = g_strndup (g_bytes_get_data (body, NULL), g_bytes_get_size (body)); + g_assert_cmpstr (json, ==, "{\"count\":42}"); + g_free (json); + g_bytes_unref (body); + + soup_test_session_abort_unref (session); +} + +int +main (int argc, + char *argv[]) +{ + int ret; + + test_init (argc, argv, NULL); + + server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD | SOUP_TEST_SERVER_UNIX_SOCKET); + soup_server_add_handler (server, NULL, + server_callback, NULL, NULL); + + g_test_add_func ("/unix-socket/load-uri", do_load_uri_test); + + ret = g_test_run (); + + soup_test_server_quit_unref (server); + + test_cleanup (); + return ret; +} -- cgit v1.2.1