[libsoup] Add a method to override the remote connection.



commit 8e7652a2f96ea78b546e7b3fd04d4969c49b0085
Author: Robert Ancell <robert ancell canonical com>
Date:   Wed Feb 24 12:28:08 2021 +1300

    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(-)
---
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
 <SUBSECTION>
 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 <libsoup/soup.h>
+
+#include <gio/gunixsocketaddress.h>
+
+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 <libsoup/soup.h>
+#include <glib/gstdio.h>
+
+#include <gio/gunixsocketaddress.h>
+
+#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 <glib/gprintf.h>
+#ifdef G_OS_UNIX
+#include <gio/gunixsocketaddress.h>
+#endif
 
 #include <locale.h>
 #include <signal.h>
@@ -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 <gio/gunixsocketaddress.h>
+
+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;
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]