>From e31f2e767228e7bb238d6716d2d9b6e788feed52 Mon Sep 17 00:00:00 2001 From: Joseph Artsimovich Date: Thu, 22 Aug 2013 16:13:46 +0100 Subject: [PATCH] Make it possible to provide a custom listening socket to SoupServer The patch makes it possible to support systemd socket activation [1] mechanism in applications using SoupServer. The idea behind systemd socket activation is to launch a server application on demand, when someone tries to connect to it. The application gets passed a listening socket with a connection pending on it, and from that point it's the application's responsibility to accept new connections. Instead of adding support for systemd specifically, this patch introduces a more general ability to provide a custom listening socket to SoupServer. This works like this: sock = g_socket_new(...); g_socket_bind(sock, ...); g_socket_listen(sock, ...); server = soup_server_new(SOUP_SERVER_LISTEN_SOCKET, sock, NULL); [1]: http://0pointer.de/blog/projects/socket-activation.html --- libsoup/soup-server.c | 24 +++++++ libsoup/soup-server.h | 1 + libsoup/soup-socket.c | 102 ++++++++++++++++++++++--------- libsoup/soup-socket.h | 1 + tests/Makefile.am | 1 + tests/listen-socket-test.c | 145 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 244 insertions(+), 30 deletions(-) create mode 100644 tests/listen-socket-test.c diff --git a/libsoup/soup-server.c b/libsoup/soup-server.c index 07d801d..fb90b44 100644 --- a/libsoup/soup-server.c +++ b/libsoup/soup-server.c @@ -97,6 +97,7 @@ typedef struct { GMainLoop *loop; SoupSocket *listen_sock; + GSocket *listen_sock_prop; GSList *clients; gboolean raw_paths; @@ -126,6 +127,7 @@ enum { PROP_SERVER_HEADER, PROP_HTTP_ALIASES, PROP_HTTPS_ALIASES, + PROP_LISTEN_SOCKET, LAST_PROP }; @@ -167,6 +169,7 @@ soup_server_finalize (GObject *object) g_free (priv->server_header); g_clear_object (&priv->listen_sock); + g_clear_object (&priv->listen_sock_prop); while (priv->clients) { SoupClientContext *client = priv->clients->data; @@ -245,6 +248,7 @@ soup_server_constructor (GType type, soup_socket_new (SOUP_SOCKET_LOCAL_ADDRESS, priv->iface, SOUP_SOCKET_SSL_CREDENTIALS, priv->ssl_cert, SOUP_SOCKET_ASYNC_CONTEXT, priv->async_context, + SOUP_SOCKET_LISTEN_SOCKET, priv->listen_sock_prop, NULL); if (!soup_socket_listen (priv->listen_sock)) { g_object_unref (server); @@ -345,6 +349,9 @@ soup_server_set_property (GObject *object, guint prop_id, case PROP_HTTPS_ALIASES: set_aliases (&priv->https_aliases, g_value_get_boxed (value)); break; + case PROP_LISTEN_SOCKET: + priv->listen_sock_prop = g_value_dup_object (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -737,6 +744,23 @@ soup_server_class_init (SoupServerClass *server_class) "URI schemes that are considered aliases for 'https'", G_TYPE_STRV, G_PARAM_READWRITE)); + + /** SOUP_SERVER_LISTEN_SOCKET: + * + * Alias for the #SoupServer:listen-socket property. Makes it possible + * to provide a socket to be used for listening for incoming connections. + * The provided socket has to be in listening state already. Passing a + * null pointer as a value of this property is allowed and has no effect. + * When #SoupServer:listen-socket property is set, #SoupServer:interface + * and #SoupServer:port are ignored. + */ + g_object_class_install_property ( + object_class, PROP_LISTEN_SOCKET, + g_param_spec_object (SOUP_SERVER_LISTEN_SOCKET, + "Listening socket", + "Socket in listening state", + G_TYPE_SOCKET, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); } /** diff --git a/libsoup/soup-server.h b/libsoup/soup-server.h index 0d09322..9d82d54 100644 --- a/libsoup/soup-server.h +++ b/libsoup/soup-server.h @@ -66,6 +66,7 @@ typedef void (*SoupServerCallback) (SoupServer *server, #define SOUP_SERVER_SERVER_HEADER "server-header" #define SOUP_SERVER_HTTP_ALIASES "http-aliases" #define SOUP_SERVER_HTTPS_ALIASES "https-aliases" +#define SOUP_SERVER_LISTEN_SOCKET "listen-socket" SoupServer *soup_server_new (const char *optname1, ...) G_GNUC_NULL_TERMINATED; diff --git a/libsoup/soup-socket.c b/libsoup/soup-socket.c index b9f1dfc..c399b07 100644 --- a/libsoup/soup-socket.c +++ b/libsoup/soup-socket.c @@ -60,6 +60,7 @@ enum { PROP_TLS_CERTIFICATE, PROP_TLS_ERRORS, PROP_PROXY_RESOLVER, + PROP_LISTEN_SOCKET, LAST_PROP }; @@ -68,6 +69,7 @@ typedef struct { SoupAddress *local_addr, *remote_addr; GIOStream *conn, *iostream; GSocket *gsock; + GSocket *listen_sock; GInputStream *istream; GOutputStream *ostream; GTlsCertificateFlags tls_errors; @@ -142,6 +144,7 @@ soup_socket_finalize (GObject *object) disconnect_internal (SOUP_SOCKET (object), TRUE); } + g_clear_object (&priv->listen_sock); g_clear_object (&priv->conn); g_clear_object (&priv->iostream); g_clear_object (&priv->istream); @@ -229,6 +232,9 @@ soup_socket_set_property (GObject *object, guint prop_id, case PROP_CLEAN_DISPOSE: priv->clean_dispose = g_value_get_boolean (value); break; + case PROP_LISTEN_SOCKET: + priv->listen_sock = g_value_dup_object (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -628,6 +634,24 @@ soup_socket_class_init (SoupSocketClass *socket_class) "GProxyResolver to use", G_TYPE_PROXY_RESOLVER, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); + + /** + * SOUP_SOCKET_LISTEN_SOCKET: + * + * Alias for the #SoupSocket:listen-socket property. Makes it possible + * to provide a socket to be used for listening for incoming connections. + * The provided socket has to be in listening state already. Passing a + * null pointer as a value of this property is allowed and has no effect. + * When #SoupSocket:listen-socket property is set, #SoupSocket:local-address + * is ignored. + **/ + g_object_class_install_property ( + object_class, PROP_LISTEN_SOCKET, + g_param_spec_object (SOUP_SOCKET_LISTEN_SOCKET, + "Listening socket", + "Socket in listening state", + G_TYPE_SOCKET, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); } @@ -1025,8 +1049,12 @@ listen_watch (GObject *pollable, gpointer data) * @sock: a server #SoupSocket (which must not already be connected or * listening) * - * Makes @sock start listening on its local address. When connections - * come in, @sock will emit #SoupSocket::new_connection. + * Makes @sock start listening on its local address, or whatever address + * #SoupSocket::listen-socket is bound to, if it was provided. + * Note than even though #SoupSocket::listen-socket must already be + * in listening state, a call to soup_socket_listen() is still required. + * + * When connections come in, @sock will emit #SoupSocket::new_connection. * * Return value: whether or not @sock is now listening. **/ @@ -1035,53 +1063,67 @@ soup_socket_listen (SoupSocket *sock) { SoupSocketPrivate *priv; - GSocketAddress *addr; + GSocketAddress *addr = NULL; g_return_val_if_fail (SOUP_IS_SOCKET (sock), FALSE); priv = SOUP_SOCKET_GET_PRIVATE (sock); g_return_val_if_fail (priv->gsock == NULL, FALSE); - g_return_val_if_fail (priv->local_addr != NULL, FALSE); + if (!priv->listen_sock) + g_return_val_if_fail (priv->local_addr != NULL, FALSE); priv->is_server = TRUE; - /* @local_addr may have its port set to 0. So we intentionally - * don't store it in priv->local_addr, so that if the - * caller calls soup_socket_get_local_address() later, we'll - * have to make a new addr by calling getsockname(), which - * will have the right port number. - */ - addr = soup_address_get_gsockaddr (priv->local_addr); - g_return_val_if_fail (addr != NULL, FALSE); + if (priv->listen_sock) { + priv->gsock = priv->listen_sock; + g_object_ref(priv->gsock); + finish_socket_setup (priv); + } else { + /* @local_addr may have its port set to 0. So we intentionally + * don't store it in priv->local_addr, so that if the + * caller calls soup_socket_get_local_address() later, we'll + * have to make a new addr by calling getsockname(), which + * will have the right port number. + */ + addr = soup_address_get_gsockaddr (priv->local_addr); + g_return_val_if_fail (addr != NULL, FALSE); - priv->gsock = g_socket_new (g_socket_address_get_family (addr), - G_SOCKET_TYPE_STREAM, - G_SOCKET_PROTOCOL_DEFAULT, - NULL); - if (!priv->gsock) - goto cant_listen; - finish_socket_setup (priv); + priv->gsock = g_socket_new (g_socket_address_get_family (addr), + G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_DEFAULT, + NULL); - /* Bind */ - if (!g_socket_bind (priv->gsock, addr, TRUE, NULL)) - goto cant_listen; - /* Force local_addr to be re-resolved now */ - g_object_unref (priv->local_addr); - priv->local_addr = NULL; + if (!priv->gsock) + goto cant_listen; + finish_socket_setup (priv); - /* Listen */ - if (!g_socket_listen (priv->gsock, NULL)) - goto cant_listen; + /* Bind */ + if (!g_socket_bind (priv->gsock, addr, TRUE, NULL)) + goto cant_listen; + + /* Listen */ + if (!g_socket_listen (priv->gsock, NULL)) + goto cant_listen; + } + + /* Force local_addr to be re-resolved now */ + if (priv->local_addr) { + g_object_unref (priv->local_addr); + priv->local_addr = NULL; + } priv->watch_src = soup_socket_create_watch (priv, G_IO_IN, listen_watch, sock, NULL); - g_object_unref (addr); + if (addr) + g_object_unref (addr); + return TRUE; cant_listen: if (priv->conn) disconnect_internal (sock, TRUE); - g_object_unref (addr); + if (addr) + g_object_unref (addr); return FALSE; } diff --git a/libsoup/soup-socket.h b/libsoup/soup-socket.h index 5c1264f..a2a90dd 100644 --- a/libsoup/soup-socket.h +++ b/libsoup/soup-socket.h @@ -52,6 +52,7 @@ typedef struct { #define SOUP_SOCKET_TIMEOUT "timeout" #define SOUP_SOCKET_TLS_CERTIFICATE "tls-certificate" #define SOUP_SOCKET_TLS_ERRORS "tls-errors" +#define SOUP_SOCKET_LISTEN_SOCKET "listen-socket" typedef void (*SoupSocketCallback) (SoupSocket *sock, guint status, diff --git a/tests/Makefile.am b/tests/Makefile.am index a8b9d01..e8bc1e6 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -21,6 +21,7 @@ test_programs = \ date \ forms-test \ header-parsing \ + listen-socket-test \ misc-test \ multipart-test \ no-ssl-test \ diff --git a/tests/listen-socket-test.c b/tests/listen-socket-test.c new file mode 100644 index 0000000..1a26dc1 --- /dev/null +++ b/tests/listen-socket-test.c @@ -0,0 +1,145 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright 2014 YouView TV Ltd + */ + +#include "test-utils.h" + +#include +#include + +static GSocket* create_listening_socket (void) +{ + gboolean bret; + GSocket* sock; + GInetAddress* inet_addr; + GSocketAddress* sock_addr; + + inet_addr = g_inet_address_new_loopback(G_SOCKET_FAMILY_IPV4); + sock_addr = g_inet_socket_address_new(inet_addr, 0); + + + sock = g_socket_new (G_SOCKET_FAMILY_IPV4, G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_TCP, NULL); + g_assert(sock); + + bret = g_socket_bind (sock, sock_addr, /*allow_reuse*/TRUE, NULL); + g_assert(bret); + + bret = g_socket_listen (sock, NULL); + g_assert(bret); + + g_object_unref(sock_addr); + g_object_unref(inet_addr); + + return sock; +} + +static void +do_listen_socket_low_level_test (void) +{ + GSocket* listen_sock; + SoupSocket* server_sock, *client_sock; + SoupAddress* server_addr; + gboolean bret; + guint uret; + + listen_sock = create_listening_socket (); + g_assert(listen_sock); + + server_sock = soup_socket_new (SOUP_SOCKET_LISTEN_SOCKET, listen_sock, NULL); + g_assert (server_sock); + + bret = soup_socket_listen (server_sock); + g_assert (bret); + + server_addr = soup_socket_get_local_address (server_sock); + g_assert (server_addr); + g_assert (g_socket_get_fd (listen_sock) == soup_socket_get_fd (server_sock)); + + client_sock = soup_socket_new (SOUP_SOCKET_REMOTE_ADDRESS, server_addr, NULL); + g_assert (client_sock); + + uret = soup_socket_connect_sync (client_sock, NULL); + g_assert_cmpuint (uret, ==, SOUP_STATUS_OK); + + g_object_unref (client_sock); + g_object_unref (server_sock); + g_object_unref (listen_sock); +} + +struct RequestContext +{ + GMainLoop* main_loop; + guint response_status; +}; + +static void +response_handler(SoupSession *session, SoupMessage *msg, gpointer user_data) +{ + struct RequestContext* ctx = user_data; + ctx->response_status = msg->status_code; + g_main_loop_quit (ctx->main_loop); +} + +static void +do_listen_socket_server_test (void) +{ + GSocket* listen_sock; + SoupServer* server; + SoupSocket* server_listener; + SoupSession* session; + SoupMessage* message; + char http_addr[100]; + guint server_port; + struct RequestContext ctx; + + ctx.main_loop = g_main_loop_new (NULL, FALSE); + ctx.response_status = 0; + g_assert (ctx.main_loop); + + listen_sock = create_listening_socket (); + g_assert (listen_sock); + + server = soup_server_new (SOUP_SERVER_LISTEN_SOCKET, listen_sock, NULL); + g_assert (server); + server_port = soup_server_get_port (server); + + server_listener = soup_server_get_listener (server); + g_assert(server_listener); + g_assert (g_socket_get_fd (listen_sock) == soup_socket_get_fd (server_listener)); + + session = soup_session_async_new (); + g_assert (session); + + g_snprintf (http_addr, sizeof(http_addr), "http://127.0.0.1:%u/", server_port); + message = soup_message_new ("GET", http_addr); + + soup_session_queue_message (session, message, &response_handler, &ctx); + + soup_server_run_async (server); + + g_main_loop_run (ctx.main_loop); + + g_object_unref (session); + g_object_unref (server); + g_object_unref (listen_sock); + g_main_loop_unref (ctx.main_loop); + + g_assert_cmpuint (ctx.response_status, ==, SOUP_STATUS_NOT_FOUND); +} + +int +main (int argc, char **argv) +{ + int ret; + test_init (argc, argv, NULL); + + g_test_add_func ("/listen-socket/low-level", do_listen_socket_low_level_test); + g_test_add_func ("/listen-socket/server", do_listen_socket_server_test); + + ret = g_test_run (); + + test_cleanup (); + return ret; +} -- 1.7.1