[libsoup/carlosgc/server-http2: 11/13] server: add initial support for HTTP/2




commit 21b3ec4490dd5a05c25ec052b95cf0006d7148b8
Author: Carlos Garcia Campos <cgarcia igalia com>
Date:   Wed Jul 27 15:15:11 2022 +0200

    server: add initial support for HTTP/2
    
    For now the plan is to use it only for testing the client APIs.

 libsoup/meson.build                                |   2 +
 .../server/http2/soup-server-message-io-http2.c    | 793 +++++++++++++++++++++
 .../server/http2/soup-server-message-io-http2.h    |  14 +
 libsoup/server/soup-server-connection.c            |  57 +-
 libsoup/server/soup-server-connection.h            |   2 +
 libsoup/server/soup-server-message.c               |   3 +
 libsoup/server/soup-server-private.h               |  14 +
 libsoup/server/soup-server.c                       |  19 +-
 8 files changed, 895 insertions(+), 9 deletions(-)
---
diff --git a/libsoup/meson.build b/libsoup/meson.build
index 48217952..a82a49fd 100644
--- a/libsoup/meson.build
+++ b/libsoup/meson.build
@@ -43,6 +43,7 @@ soup_sources = [
   'http2/soup-body-input-stream-http2.c',
 
   'server/http1/soup-server-message-io-http1.c',
+  'server/http2/soup-server-message-io-http2.c',
   'server/soup-auth-domain.c',
   'server/soup-auth-domain-basic.c',
   'server/soup-auth-domain-digest.c',
@@ -203,6 +204,7 @@ libsoup_includes = [
     'http2',
     'server',
     'server/http1',
+    'server/http2',
     'websocket',
     '.'
   ]),
diff --git a/libsoup/server/http2/soup-server-message-io-http2.c 
b/libsoup/server/http2/soup-server-message-io-http2.c
new file mode 100644
index 00000000..c3f457a3
--- /dev/null
+++ b/libsoup/server/http2/soup-server-message-io-http2.c
@@ -0,0 +1,793 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
+/*
+ * soup-server-message-io-http1.c: HTTP message I/O
+ *
+ * Copyright (C) 2022, Igalia S.L.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <glib/gi18n-lib.h>
+
+#include "soup-server-message-io-http2.h"
+#include "soup.h"
+#include "soup-body-input-stream.h"
+#include "soup-body-output-stream.h"
+#include "soup-filter-input-stream.h"
+#include "soup-message-io-data.h"
+#include "soup-message-headers-private.h"
+#include "soup-server-message-private.h"
+#include "soup-misc.h"
+
+#include <nghttp2/nghttp2.h>
+
+typedef enum {
+        STATE_NONE,
+        STATE_READ_HEADERS,
+        STATE_READ_DATA,
+        STATE_READ_DONE,
+        STATE_WRITE_HEADERS,
+        STATE_WRITE_DATA,
+        STATE_WRITE_DONE,
+} SoupHTTP2IOState;
+
+typedef struct {
+        SoupServerMessage *msg;
+        guint32 stream_id;
+        SoupHTTP2IOState state;
+        GSource *unpause_source;
+        gboolean paused;
+
+        SoupMessageIOCompletionFn completion_cb;
+        gpointer completion_data;
+
+        char *scheme;
+        char *authority;
+        char *path;
+
+        GBytes *write_chunk;
+        goffset write_offset;
+        goffset chunk_written;
+} SoupMessageIOHTTP2;
+
+typedef struct {
+        SoupServerMessageIO iface;
+
+        SoupServerConnection *conn;
+        GIOStream *iostream;
+        GInputStream *istream;
+        GOutputStream *ostream;
+
+        GSource *read_source;
+        GSource *write_source;
+
+        nghttp2_session *session;
+
+        /* Owned by nghttp2 */
+        guint8 *write_buffer;
+        gssize write_buffer_size;
+        gssize written_bytes;
+
+        SoupMessageIOStartedFn started_cb;
+        gpointer started_user_data;
+
+        GHashTable *messages;
+} SoupServerMessageIOHTTP2;
+
+static void soup_server_message_io_http2_send_response (SoupServerMessageIOHTTP2 *io,
+                                                        SoupMessageIOHTTP2       *msg_io);
+
+static const char *
+state_to_string (SoupHTTP2IOState state)
+{
+        switch (state) {
+        case STATE_NONE:
+                return "NONE";
+        case STATE_READ_HEADERS:
+                return "READ_HEADERS";
+        case STATE_READ_DATA:
+                return "READ_DATA";
+        case STATE_READ_DONE:
+                return "READ_DONE";
+        case STATE_WRITE_HEADERS:
+                return "WRITE_HEADERS";
+        case STATE_WRITE_DATA:
+                return "WRITE_DATA";
+        case STATE_WRITE_DONE:
+                return "WRITE_DONE";
+        default:
+                g_assert_not_reached ();
+                return "";
+        }
+}
+
+static void
+advance_state_from (SoupMessageIOHTTP2 *msg_io,
+                    SoupHTTP2IOState    from,
+                    SoupHTTP2IOState    to)
+{
+        if (msg_io->state != from) {
+                g_warning ("Unexpected state changed %s -> %s, expected to be from %s",
+                           state_to_string (msg_io->state), state_to_string (to),
+                           state_to_string (from));
+        }
+
+        /* State never goes backwards */
+        if (to < msg_io->state) {
+                g_warning ("Unexpected state changed %s -> %s, expected %s -> %s\n",
+                           state_to_string (msg_io->state), state_to_string (to),
+                           state_to_string (from), state_to_string (to));
+                return;
+        }
+
+        msg_io->state = to;
+}
+
+static SoupMessageIOHTTP2 *
+soup_message_io_http2_new (SoupServerMessage *msg)
+{
+        SoupMessageIOHTTP2 *msg_io;
+
+        msg_io = g_new0 (SoupMessageIOHTTP2, 1);
+        msg_io->msg = msg;
+
+        return msg_io;
+}
+
+static void
+soup_message_io_http2_free (SoupMessageIOHTTP2 *msg_io)
+{
+        if (msg_io->unpause_source) {
+                g_source_destroy (msg_io->unpause_source);
+                g_source_unref (msg_io->unpause_source);
+        }
+        g_clear_object (&msg_io->msg);
+        g_free (msg_io->scheme);
+        g_free (msg_io->authority);
+        g_free (msg_io->path);
+        g_clear_pointer (&msg_io->write_chunk, g_bytes_unref);
+        g_free (msg_io);
+}
+
+static void
+soup_server_message_io_http2_destroy (SoupServerMessageIO *iface)
+{
+        SoupServerMessageIOHTTP2 *io = (SoupServerMessageIOHTTP2 *)iface;
+
+        if (io->read_source) {
+                g_source_destroy (io->read_source);
+                g_source_unref (io->read_source);
+        }
+        if (io->write_source) {
+                g_source_destroy (io->write_source);
+                g_source_unref (io->write_source);
+        }
+
+        g_clear_object (&io->iostream);
+        g_clear_pointer (&io->session, nghttp2_session_del);
+        g_clear_pointer (&io->messages, g_hash_table_unref);
+
+        g_free (io);
+}
+
+static void
+soup_server_message_io_http2_finished (SoupServerMessageIO *iface,
+                                       SoupServerMessage   *msg)
+{
+        SoupServerMessageIOHTTP2 *io = (SoupServerMessageIOHTTP2 *)iface;
+        SoupMessageIOHTTP2 *msg_io = NULL;
+        SoupMessageIOCompletionFn completion_cb;
+        gpointer completion_data;
+        SoupMessageIOCompletion completion;
+
+        g_hash_table_steal_extended (io->messages, msg, NULL, (gpointer *)&msg_io);
+        completion = msg_io->state < STATE_WRITE_DONE ? SOUP_MESSAGE_IO_INTERRUPTED : 
SOUP_MESSAGE_IO_COMPLETE;
+
+        completion_cb = msg_io->completion_cb;
+        completion_data = msg_io->completion_data;
+
+        g_object_ref (msg);
+        soup_message_io_http2_free (msg_io);
+
+        if (completion_cb)
+                completion_cb (G_OBJECT (msg), completion, completion_data);
+
+        g_object_unref (msg);
+}
+
+static GIOStream *
+soup_server_message_io_http2_steal (SoupServerMessageIO *iface)
+{
+        g_assert_not_reached ();
+        return NULL;
+}
+
+static void
+soup_server_message_io_http2_read_request (SoupServerMessageIO      *iface,
+                                           SoupServerMessage        *msg,
+                                           SoupMessageIOCompletionFn completion_cb,
+                                           gpointer                  user_data)
+{
+        SoupServerMessageIOHTTP2 *io = (SoupServerMessageIOHTTP2 *)iface;
+        SoupMessageIOHTTP2 *msg_io;
+
+        msg_io = g_hash_table_lookup (io->messages, msg);
+        g_assert (msg_io);
+
+        msg_io->completion_cb = completion_cb;
+        msg_io->completion_data = user_data;
+}
+
+static void
+soup_server_message_io_http2_pause (SoupServerMessageIO *iface,
+                                    SoupServerMessage   *msg)
+{
+        SoupServerMessageIOHTTP2 *io = (SoupServerMessageIOHTTP2 *)iface;
+        SoupMessageIOHTTP2 *msg_io;
+
+        msg_io = g_hash_table_lookup (io->messages, msg);
+        g_assert (msg_io);
+
+        if (msg_io->paused)
+                g_warn_if_reached ();
+
+        if (msg_io->unpause_source) {
+                g_source_destroy (msg_io->unpause_source);
+                g_source_unref (msg_io->unpause_source);
+                msg_io->unpause_source = NULL;
+        }
+
+        msg_io->paused = TRUE;
+}
+
+typedef struct {
+        SoupServerMessageIOHTTP2 *io;
+        SoupMessageIOHTTP2 *msg_io;
+} UnpauseSourceData;
+
+static gboolean
+io_unpause_internal (UnpauseSourceData *data)
+{
+        SoupMessageIOHTTP2 *msg_io = data->msg_io;
+
+        g_clear_pointer (&msg_io->unpause_source, g_source_unref);
+        if (msg_io->paused)
+                return FALSE;
+
+        if (!nghttp2_session_get_stream_user_data (data->io->session, msg_io->stream_id)) {
+                soup_server_message_finish (msg_io->msg);
+                return FALSE;
+        }
+
+        switch (msg_io->state) {
+        case STATE_READ_DONE:
+                soup_server_message_io_http2_send_response (data->io, msg_io);
+                break;
+        default:
+                g_warn_if_reached ();
+        }
+        return FALSE;
+}
+
+static void
+soup_server_message_io_http2_unpause (SoupServerMessageIO *iface,
+                                      SoupServerMessage   *msg)
+{
+        SoupServerMessageIOHTTP2 *io = (SoupServerMessageIOHTTP2 *)iface;
+        SoupMessageIOHTTP2 *msg_io;
+
+        msg_io = g_hash_table_lookup (io->messages, msg);
+        g_assert (msg_io);
+
+        if (!msg_io->paused)
+                g_warn_if_reached ();
+
+        msg_io->paused = FALSE;
+
+        if (!msg_io->unpause_source) {
+                UnpauseSourceData *data = g_new (UnpauseSourceData, 1);
+
+                data->io = io;
+                data->msg_io = msg_io;
+                msg_io->unpause_source = soup_add_completion_reffed (g_main_context_get_thread_default (),
+                                                                     (GSourceFunc)io_unpause_internal,
+                                                                     data, g_free);
+        }
+}
+
+static gboolean
+soup_server_message_io_http2_is_paused (SoupServerMessageIO *iface,
+                                        SoupServerMessage   *msg)
+{
+        SoupServerMessageIOHTTP2 *io = (SoupServerMessageIOHTTP2 *)iface;
+        SoupMessageIOHTTP2 *msg_io;
+
+        msg_io = g_hash_table_lookup (io->messages, msg);
+        g_assert (msg_io);
+
+        return msg_io->paused;
+}
+
+static const SoupServerMessageIOFuncs io_funcs = {
+        soup_server_message_io_http2_destroy,
+        soup_server_message_io_http2_finished,
+        soup_server_message_io_http2_steal,
+        soup_server_message_io_http2_read_request,
+        soup_server_message_io_http2_pause,
+        soup_server_message_io_http2_unpause,
+        soup_server_message_io_http2_is_paused
+};
+
+static gboolean
+io_write (SoupServerMessageIOHTTP2 *io,
+          GError                  **error)
+{
+        /* We must write all of nghttp2's buffer before we ask for more */
+        if (io->written_bytes == io->write_buffer_size)
+                io->write_buffer = NULL;
+
+        if (io->write_buffer == NULL) {
+                io->written_bytes = 0;
+                io->write_buffer_size = nghttp2_session_mem_send (io->session, (const 
guint8**)&io->write_buffer);
+                if (io->write_buffer_size == 0) {
+                        /* Done */
+                        io->write_buffer = NULL;
+                        return TRUE;
+                }
+        }
+
+        gssize ret = g_pollable_stream_write (io->ostream,
+                                              io->write_buffer + io->written_bytes,
+                                              io->write_buffer_size - io->written_bytes,
+                                              FALSE, NULL, error);
+        if (ret < 0)
+                return FALSE;
+
+        io->written_bytes += ret;
+        return TRUE;
+}
+
+static gboolean
+io_write_ready (GObject                  *stream,
+                SoupServerMessageIOHTTP2 *io)
+{
+        GError *error = NULL;
+
+        while (nghttp2_session_want_write (io->session) && !error)
+                io_write (io, &error);
+
+        if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
+                g_error_free (error);
+                return G_SOURCE_CONTINUE;
+        }
+
+        g_clear_error (&error);
+        g_clear_pointer (&io->write_source, g_source_unref);
+
+        return G_SOURCE_REMOVE;
+}
+
+static void
+io_try_write (SoupServerMessageIOHTTP2 *io)
+{
+        GError *error = NULL;
+
+        if (io->write_source)
+                return;
+
+        while (nghttp2_session_want_write (io->session) && !error)
+                io_write (io, &error);
+
+        if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
+                g_clear_error (&error);
+                io->write_source = g_pollable_output_stream_create_source (G_POLLABLE_OUTPUT_STREAM 
(io->ostream), NULL);
+                g_source_set_name (io->write_source, "Soup server HTTP/2 write source");
+                g_source_set_callback (io->write_source, (GSourceFunc)io_write_ready, io, NULL);
+                g_source_attach (io->write_source, g_main_context_get_thread_default ());
+        }
+
+        g_clear_error (&error);
+}
+
+static gboolean
+io_read (SoupServerMessageIOHTTP2 *io,
+         GError                  **error)
+{
+        guint8 buffer[8192];
+        gssize read;
+
+        if ((read = g_pollable_stream_read (io->istream, buffer, sizeof (buffer), FALSE, NULL, error)) < 0)
+                return FALSE;
+
+        return nghttp2_session_mem_recv (io->session, buffer, read) != 0;
+}
+
+static gboolean
+io_read_ready (GObject                  *stream,
+               SoupServerMessageIOHTTP2 *io)
+{
+        gboolean progress = TRUE;
+        GError *error = NULL;
+
+        while (nghttp2_session_want_read (io->session) && progress)
+                progress = io_read (io, &error);
+
+        if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
+                g_error_free (error);
+                return G_SOURCE_CONTINUE;
+        }
+
+        g_clear_error (&error);
+
+        return G_SOURCE_REMOVE;
+}
+
+static SoupMessageIOHTTP2 *
+soup_server_message_io_http2_get_or_create_msg_io (SoupServerMessageIOHTTP2 *io)
+{
+        SoupMessageIOHTTP2 *msg_io;
+
+        if (g_hash_table_size (io->messages) == 1) {
+                GList *values = g_hash_table_get_values (io->messages);
+
+                msg_io = (SoupMessageIOHTTP2 *)values->data;
+                g_list_free (values);
+
+                if (msg_io->stream_id == 0)
+                        return msg_io;
+        }
+
+        msg_io = soup_message_io_http2_new (soup_server_message_new (io->conn));
+        soup_server_message_set_http_version (msg_io->msg, SOUP_HTTP_2_0);
+        g_hash_table_insert (io->messages, msg_io->msg, msg_io);
+
+        return msg_io;
+}
+
+static int
+on_begin_headers_callback (nghttp2_session     *session,
+                           const nghttp2_frame *frame,
+                           void                *user_data)
+{
+        SoupServerMessageIOHTTP2 *io = (SoupServerMessageIOHTTP2 *)user_data;
+        SoupMessageIOHTTP2 *msg_io;
+
+        if (frame->hd.type != NGHTTP2_HEADERS || frame->headers.cat != NGHTTP2_HCAT_REQUEST)
+                return 0;
+
+        msg_io = soup_server_message_io_http2_get_or_create_msg_io (io);
+        msg_io->stream_id = frame->hd.stream_id;
+        nghttp2_session_set_stream_user_data (session, frame->hd.stream_id, msg_io);
+
+        if (!msg_io->completion_cb)
+                io->started_cb (msg_io->msg, io->started_user_data);
+
+        advance_state_from (msg_io, STATE_NONE, STATE_READ_HEADERS);
+
+        return 0;
+}
+
+static int
+on_header_callback (nghttp2_session     *session,
+                    const nghttp2_frame *frame,
+                    const uint8_t       *name,
+                    size_t               namelen,
+                    const uint8_t       *value,
+                    size_t               valuelen,
+                    uint8_t              flags,
+                    void                *user_data)
+{
+        SoupMessageIOHTTP2 *msg_io;
+        SoupServerMessage *msg;
+
+        if (frame->hd.type != NGHTTP2_HEADERS)
+                return 0;
+
+        if (frame->headers.cat != NGHTTP2_HCAT_REQUEST)
+                return 0;
+
+        msg_io = nghttp2_session_get_stream_user_data (session, frame->hd.stream_id);
+        if (!msg_io)
+                return 0;
+
+        msg = msg_io->msg;
+        if (name[0] == ':') {
+                if (strcmp ((char *)name, ":method") == 0)
+                        soup_server_message_set_method (msg, (char *)value);
+                else if (strcmp ((char *)name, ":scheme") == 0)
+                        msg_io->scheme = g_strndup ((char *)value, valuelen);
+                else if (strcmp ((char *)name, ":authority") == 0)
+                        msg_io->authority = g_strndup ((char *)value, valuelen);
+                else if (strcmp ((char *)name, ":path") == 0)
+                        msg_io->path = g_strndup ((char *)value, valuelen);
+                else
+                        g_debug ("Unknown header: %s = %s", name, value);
+                return 0;
+        }
+
+        soup_message_headers_append_untrusted_data (soup_server_message_get_request_headers (msg),
+                                                    (const char*)name, (const char*)value);
+        return 0;
+}
+
+static int
+on_data_chunk_recv_callback (nghttp2_session *session,
+                             uint8_t          flags,
+                             int32_t          stream_id,
+                             const uint8_t   *data,
+                             size_t           len,
+                             void            *user_data)
+{
+        SoupMessageIOHTTP2 *msg_io;
+        GBytes *bytes;
+
+        msg_io = nghttp2_session_get_stream_user_data (session, stream_id);
+        if (!msg_io)
+                return NGHTTP2_ERR_CALLBACK_FAILURE;
+
+        bytes = g_bytes_new (data, len);
+        soup_message_body_got_chunk (soup_server_message_get_request_body (msg_io->msg), bytes);
+        soup_server_message_got_chunk (msg_io->msg, bytes);
+        g_bytes_unref (bytes);
+
+        return 0;
+}
+
+static ssize_t
+on_data_source_read_callback (nghttp2_session     *session,
+                              int32_t              stream_id,
+                              uint8_t             *buf,
+                              size_t               length,
+                              uint32_t            *data_flags,
+                              nghttp2_data_source *source,
+                              void                *user_data)
+{
+        SoupMessageIOHTTP2 *msg_io;
+        gsize bytes_written = 0;
+        SoupMessageBody *response_body = (SoupMessageBody *)source->ptr;
+
+        msg_io = nghttp2_session_get_stream_user_data (session, stream_id);
+
+        while (bytes_written < length && msg_io->write_offset < response_body->length) {
+                gconstpointer data;
+                gsize data_length;
+                gsize bytes_to_write;
+
+                if (!msg_io->write_chunk)
+                        msg_io->write_chunk = soup_message_body_get_chunk (response_body, 
msg_io->write_offset);
+
+                data = g_bytes_get_data (msg_io->write_chunk, &data_length);
+                bytes_to_write = MIN (length - bytes_written, data_length - msg_io->chunk_written);
+                memcpy (buf + bytes_written, (uint8_t *)data + msg_io->chunk_written, bytes_to_write);
+                bytes_written += bytes_to_write;
+                msg_io->chunk_written += bytes_to_write;
+                msg_io->write_offset += bytes_to_write;
+                soup_server_message_wrote_body_data (msg_io->msg, bytes_to_write);
+
+                if (msg_io->chunk_written == data_length) {
+                        soup_message_body_wrote_chunk (response_body, msg_io->write_chunk);
+                        g_clear_pointer (&msg_io->write_chunk, g_bytes_unref);
+                        soup_server_message_wrote_chunk (msg_io->msg);
+                        msg_io->chunk_written = 0;
+                }
+        }
+
+        if (msg_io->write_offset == response_body->length) {
+                soup_server_message_wrote_body (msg_io->msg);
+                *data_flags |= NGHTTP2_DATA_FLAG_EOF;
+        }
+
+        return bytes_written;
+}
+
+#define MAKE_NV(NAME, VALUE, VALUELEN)                                      \
+        {                                                                   \
+                (uint8_t *)NAME, (uint8_t *)VALUE, strlen (NAME), VALUELEN, \
+                    NGHTTP2_NV_FLAG_NONE                                    \
+        }
+
+#define MAKE_NV2(NAME, VALUE)                                                     \
+        {                                                                         \
+                (uint8_t *)NAME, (uint8_t *)VALUE, strlen (NAME), strlen (VALUE), \
+                    NGHTTP2_NV_FLAG_NONE                                          \
+        }
+
+#define MAKE_NV3(NAME, VALUE, FLAGS)                                              \
+        {                                                                         \
+                (uint8_t *)NAME, (uint8_t *)VALUE, strlen (NAME), strlen (VALUE), \
+                    FLAGS                                                         \
+        }
+
+static void
+soup_server_message_io_http2_send_response (SoupServerMessageIOHTTP2 *io,
+                                            SoupMessageIOHTTP2       *msg_io)
+{
+        if (msg_io->paused)
+                return;
+
+        SoupServerMessage *msg = msg_io->msg;
+        GArray *headers = g_array_new (FALSE, FALSE, sizeof (nghttp2_nv));
+        guint status_code = soup_server_message_get_status (msg);
+        if (status_code == 0) {
+                status_code = SOUP_STATUS_INTERNAL_SERVER_ERROR;
+                soup_server_message_set_status (msg, status_code, NULL);
+        }
+        char *status = g_strdup_printf ("%u", status_code);
+        const nghttp2_nv status_nv = MAKE_NV2 (":status", status);
+        g_array_append_val (headers, status_nv);
+
+        SoupMessageHeaders *response_headers = soup_server_message_get_response_headers (msg);
+        if (status_code == SOUP_STATUS_NO_CONTENT || SOUP_STATUS_IS_INFORMATIONAL (status_code)) {
+                soup_message_headers_remove (response_headers, "Content-Length");
+        } else if (!soup_message_headers_get_content_length (response_headers)) {
+                SoupMessageBody *response_body;
+
+                response_body = soup_server_message_get_response_body (msg);
+                soup_message_headers_set_content_length (response_headers, response_body->length);
+        }
+
+        SoupMessageHeadersIter iter;
+        const char *name, *value;
+        soup_message_headers_iter_init (&iter, response_headers);
+        while (soup_message_headers_iter_next (&iter, &name, &value)) {
+                const nghttp2_nv nv = MAKE_NV2 (name, value);
+                g_array_append_val (headers, nv);
+        }
+
+        advance_state_from (msg_io, STATE_READ_DONE, STATE_WRITE_HEADERS);
+
+        nghttp2_data_provider data_provider;
+        data_provider.source.ptr = soup_server_message_get_response_body (msg);
+        data_provider.read_callback = on_data_source_read_callback;
+        nghttp2_submit_response (io->session, msg_io->stream_id, (const nghttp2_nv *)headers->data, 
headers->len, &data_provider);
+        io_try_write (io);
+        g_array_free (headers, TRUE);
+        g_free (status);
+}
+
+static int
+on_frame_recv_callback (nghttp2_session     *session,
+                        const nghttp2_frame *frame,
+                        void                *user_data)
+{
+        SoupServerMessageIOHTTP2 *io = (SoupServerMessageIOHTTP2 *)user_data;
+        SoupMessageIOHTTP2 *msg_io;
+
+        msg_io = nghttp2_session_get_stream_user_data (session, frame->hd.stream_id);
+        if (!msg_io)
+                return 0;
+
+        switch (frame->hd.type) {
+        case NGHTTP2_HEADERS: {
+                char *uri_string;
+                GUri *uri;
+
+                uri_string = g_strdup_printf ("%s://%s%s", msg_io->scheme, msg_io->authority, msg_io->path);
+                uri = g_uri_parse (uri_string, SOUP_HTTP_URI_FLAGS, NULL);
+                g_free (uri_string);
+                soup_server_message_set_uri (msg_io->msg, uri);
+                g_uri_unref (uri);
+
+                advance_state_from (msg_io, STATE_READ_HEADERS, STATE_READ_DATA);
+                soup_server_message_got_headers (msg_io->msg);
+                break;
+        }
+        case NGHTTP2_DATA:
+                break;
+        default:
+                return 0;
+        }
+
+        if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
+                advance_state_from (msg_io, STATE_READ_DATA, STATE_READ_DONE);
+                soup_server_message_got_body (msg_io->msg);
+                soup_server_message_io_http2_send_response (io, msg_io);
+        }
+
+        return 0;
+}
+
+static int
+on_frame_send_callback (nghttp2_session     *session,
+                        const nghttp2_frame *frame,
+                        void                *user_data)
+{
+        SoupMessageIOHTTP2 *msg_io;
+
+        msg_io = nghttp2_session_get_stream_user_data (session, frame->hd.stream_id);
+
+        switch (frame->hd.type) {
+        case NGHTTP2_HEADERS:
+                if (frame->hd.flags & NGHTTP2_FLAG_END_HEADERS) {
+                        advance_state_from (msg_io, STATE_WRITE_HEADERS, STATE_WRITE_DATA);
+                        soup_server_message_wrote_headers (msg_io->msg);
+                }
+                break;
+        case NGHTTP2_DATA:
+                if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
+                        advance_state_from (msg_io, STATE_WRITE_DATA, STATE_WRITE_DONE);
+                        soup_server_message_wrote_body (msg_io->msg);
+                }
+                break;
+        default:
+                break;
+        }
+
+        return 0;
+}
+
+static int
+on_stream_close_callback (nghttp2_session *session,
+                          int32_t          stream_id,
+                          uint32_t         error_code,
+                          void            *user_data)
+{
+        SoupMessageIOHTTP2 *msg_io;
+
+        msg_io = nghttp2_session_get_stream_user_data (session, stream_id);
+        if (!msg_io)
+                return 0;
+
+        if (!msg_io->paused)
+                soup_server_message_finish (msg_io->msg);
+
+        return 0;
+}
+
+static void
+soup_server_message_io_http2_init (SoupServerMessageIOHTTP2 *io)
+{
+        nghttp2_session_callbacks *callbacks;
+
+        nghttp2_session_callbacks_new (&callbacks);
+        nghttp2_session_callbacks_set_on_begin_headers_callback (callbacks, on_begin_headers_callback);
+        nghttp2_session_callbacks_set_on_header_callback (callbacks, on_header_callback);
+        nghttp2_session_callbacks_set_on_data_chunk_recv_callback (callbacks, on_data_chunk_recv_callback);
+        nghttp2_session_callbacks_set_on_frame_recv_callback (callbacks, on_frame_recv_callback);
+        nghttp2_session_callbacks_set_on_frame_send_callback (callbacks, on_frame_send_callback);
+        nghttp2_session_callbacks_set_on_stream_close_callback (callbacks, on_stream_close_callback);
+
+        nghttp2_session_server_new (&io->session, callbacks, io);
+        nghttp2_session_callbacks_del (callbacks);
+}
+
+SoupServerMessageIO *
+soup_server_message_io_http2_new (SoupServerConnection  *conn,
+                                  SoupServerMessage     *msg,
+                                  SoupMessageIOStartedFn started_cb,
+                                  gpointer               user_data)
+{
+        SoupServerMessageIOHTTP2 *io;
+
+        io = g_new0 (SoupServerMessageIOHTTP2, 1);
+        io->conn = conn; /* FIXME: weak */
+        io->iostream = g_object_ref (soup_server_connection_get_iostream (io->conn));
+        io->istream = g_io_stream_get_input_stream (io->iostream);
+        io->ostream = g_io_stream_get_output_stream (io->iostream);
+
+        io->started_cb = started_cb;
+        io->started_user_data = user_data;
+
+        soup_server_message_io_http2_init (io);
+
+        io->read_source = g_pollable_input_stream_create_source (G_POLLABLE_INPUT_STREAM (io->istream), 
NULL);
+        g_source_set_name (io->read_source, "Soup server HTTP/2 read source");
+        g_source_set_callback (io->read_source, (GSourceFunc)io_read_ready, io, NULL);
+        g_source_attach (io->read_source, g_main_context_get_thread_default ());
+
+        io->iface.funcs = &io_funcs;
+
+        io->messages = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, 
(GDestroyNotify)soup_message_io_http2_free);
+        g_hash_table_insert (io->messages, msg, soup_message_io_http2_new (msg));
+        soup_server_message_set_http_version (msg, SOUP_HTTP_2_0);
+
+        const nghttp2_settings_entry settings[] = {
+                { NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100 }
+        };
+        nghttp2_submit_settings (io->session, NGHTTP2_FLAG_NONE, settings, G_N_ELEMENTS (settings));
+        io_try_write (io);
+
+        return (SoupServerMessageIO *)io;
+}
diff --git a/libsoup/server/http2/soup-server-message-io-http2.h 
b/libsoup/server/http2/soup-server-message-io-http2.h
new file mode 100644
index 00000000..031a7fce
--- /dev/null
+++ b/libsoup/server/http2/soup-server-message-io-http2.h
@@ -0,0 +1,14 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2022 Igalia S.L.
+ */
+
+#pragma once
+
+#include "soup-server-connection.h"
+#include "soup-server-message-io.h"
+
+SoupServerMessageIO *soup_server_message_io_http2_new (SoupServerConnection  *conn,
+                                                       SoupServerMessage     *msg,
+                                                       SoupMessageIOStartedFn started_cb,
+                                                       gpointer               user_data);
diff --git a/libsoup/server/soup-server-connection.c b/libsoup/server/soup-server-connection.c
index ff37e8c9..5d5bd251 100644
--- a/libsoup/server/soup-server-connection.c
+++ b/libsoup/server/soup-server-connection.c
@@ -20,6 +20,7 @@
 #include "soup-io-stream.h"
 #include "soup-server-message-private.h"
 #include "soup-server-message-io-http1.h"
+#include "soup-server-message-io-http2.h"
 
 enum {
         CONNECTED,
@@ -58,6 +59,8 @@ typedef struct {
         GIOStream *conn;
         GIOStream *iostream;
         SoupServerMessage *initial_msg;
+        gboolean advertise_http2;
+        SoupHTTPVersion http_version;
         SoupServerMessageIO *io_data;
 
         GSocketAddress *local_addr;
@@ -80,6 +83,9 @@ request_started_cb (SoupServerMessage    *msg,
 static void
 soup_server_connection_init (SoupServerConnection *conn)
 {
+        SoupServerConnectionPrivate *priv = soup_server_connection_get_instance_private (conn);
+
+        priv->http_version = SOUP_HTTP_1_1;
 }
 
 static void
@@ -377,10 +383,21 @@ soup_server_connection_connected (SoupServerConnection *conn)
         SoupServerConnectionPrivate *priv = soup_server_connection_get_instance_private (conn);
 
         g_assert (!priv->io_data);
-        priv->io_data = soup_server_message_io_http1_new (conn,
-                                                          g_steal_pointer (&priv->initial_msg),
-                                                          (SoupMessageIOStartedFn)request_started_cb,
-                                                          conn);
+        switch (priv->http_version) {
+        case SOUP_HTTP_1_0:
+        case SOUP_HTTP_1_1:
+                priv->io_data = soup_server_message_io_http1_new (conn,
+                                                                  g_steal_pointer (&priv->initial_msg),
+                                                                  (SoupMessageIOStartedFn)request_started_cb,
+                                                                  conn);
+                break;
+        case SOUP_HTTP_2_0:
+                priv->io_data = soup_server_message_io_http2_new (conn,
+                                                                  g_steal_pointer (&priv->initial_msg),
+                                                                  (SoupMessageIOStartedFn)request_started_cb,
+                                                                  conn);
+                break;
+        }
         g_signal_emit (conn, signals[CONNECTED], 0);
 }
 
@@ -407,10 +424,34 @@ tls_connection_handshake_ready_cb (GTlsConnection       *tls_conn,
                                    GAsyncResult         *result,
                                    SoupServerConnection *conn)
 {
-        if (g_tls_connection_handshake_finish (tls_conn, result, NULL))
+        SoupServerConnectionPrivate *priv = soup_server_connection_get_instance_private (conn);
+
+        if (g_tls_connection_handshake_finish (tls_conn, result, NULL)) {
+                const char *protocol = g_tls_connection_get_negotiated_protocol (tls_conn);
+
+                if (g_strcmp0 (protocol, "h2") == 0)
+                        priv->http_version = SOUP_HTTP_2_0;
+                else if (g_strcmp0 (protocol, "http/1.0") == 0)
+                        priv->http_version = SOUP_HTTP_1_0;
+                else if (g_strcmp0 (protocol, "http/1.1") == 0)
+                        priv->http_version = SOUP_HTTP_1_1;
+
                 soup_server_connection_connected (conn);
-        else
+        } else {
                 soup_server_connection_disconnect (conn);
+        }
+}
+
+void
+soup_server_connection_set_advertise_http2 (SoupServerConnection *conn,
+                                            gboolean              advertise_http2)
+{
+        SoupServerConnectionPrivate *priv;
+
+        g_return_if_fail (SOUP_IS_SERVER_CONNECTION (conn));
+
+        priv = soup_server_connection_get_instance_private (conn);
+        priv->advertise_http2 = advertise_http2;
 }
 
 void
@@ -441,7 +482,9 @@ soup_server_connection_accepted (SoupServerConnection *conn)
         if (priv->tls_certificate) {
                 GPtrArray *advertised_protocols;
 
-                advertised_protocols = g_ptr_array_sized_new (3);
+                advertised_protocols = g_ptr_array_sized_new (4);
+                if (priv->advertise_http2 && !priv->tls_database)
+                        g_ptr_array_add (advertised_protocols, "h2");
                 g_ptr_array_add (advertised_protocols, "http/1.1");
                 g_ptr_array_add (advertised_protocols, "http/1.0");
                 g_ptr_array_add (advertised_protocols, NULL);
diff --git a/libsoup/server/soup-server-connection.h b/libsoup/server/soup-server-connection.h
index 7fd54388..50127cf8 100644
--- a/libsoup/server/soup-server-connection.h
+++ b/libsoup/server/soup-server-connection.h
@@ -22,6 +22,8 @@ SoupServerConnection *soup_server_connection_new                             (GS
 SoupServerConnection *soup_server_connection_new_for_connection              (GIOStream             
*connection,
                                                                               GSocketAddress        
*local_addr,
                                                                               GSocketAddress        
*remote_addr);
+void                  soup_server_connection_set_advertise_http2             (SoupServerConnection *conn,
+                                                                              gboolean              
advertise_http2);
 void                  soup_server_connection_accepted                        (SoupServerConnection  *conn);
 SoupServerMessageIO  *soup_server_connection_get_io_data                     (SoupServerConnection  *conn);
 gboolean              soup_server_connection_is_ssl                          (SoupServerConnection  *conn);
diff --git a/libsoup/server/soup-server-message.c b/libsoup/server/soup-server-message.c
index 9ef34be0..61d62a53 100644
--- a/libsoup/server/soup-server-message.c
+++ b/libsoup/server/soup-server-message.c
@@ -517,6 +517,9 @@ soup_server_message_set_auth (SoupServerMessage *msg,
 gboolean
 soup_server_message_is_keepalive (SoupServerMessage *msg)
 {
+        if (msg->http_version == SOUP_HTTP_2_0)
+                return TRUE;
+
         if (msg->status_code == SOUP_STATUS_OK && msg->method == SOUP_METHOD_CONNECT)
                 return TRUE;
 
diff --git a/libsoup/server/soup-server-private.h b/libsoup/server/soup-server-private.h
new file mode 100644
index 00000000..7a90d7df
--- /dev/null
+++ b/libsoup/server/soup-server-private.h
@@ -0,0 +1,14 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2022, Igalia S.L.
+ */
+
+#ifndef __SOUP_SERVER_PRIVATE_H__
+#define __SOUP_SERVER_PRIVATE_H__ 1
+
+#include "soup-server.h"
+
+void soup_server_set_http2_enabled (SoupServer *server,
+                                    gboolean    enabled);
+
+#endif /* __SOUP_SERVER_PRIVATE_H__ */
diff --git a/libsoup/server/soup-server.c b/libsoup/server/soup-server.c
index 0b2abb1b..6b486f56 100644
--- a/libsoup/server/soup-server.c
+++ b/libsoup/server/soup-server.c
@@ -13,7 +13,7 @@
 
 #include <glib/gi18n-lib.h>
 
-#include "soup-server.h"
+#include "soup-server-private.h"
 #include "soup-server-message-private.h"
 #include "soup-message-headers-private.h"
 #include "soup.h"
@@ -164,6 +164,7 @@ typedef struct {
        GPtrArray         *websocket_extension_types;
 
        gboolean           disposed;
+        gboolean           http2_enabled;
 
 } SoupServerPrivate;
 
@@ -210,6 +211,7 @@ soup_server_init (SoupServer *server)
 {
        SoupServerPrivate *priv = soup_server_get_instance_private (server);
 
+        priv->http2_enabled = !!g_getenv ("SOUP_SERVER_HTTP2");
        priv->handlers = soup_path_map_new ((GDestroyNotify)free_handler);
 
        priv->websocket_extension_types = g_ptr_array_new_with_free_func ((GDestroyNotify)g_type_class_unref);
@@ -1085,7 +1087,8 @@ request_finished (SoupServerMessage      *msg,
            priv->listeners)
                return;
 
-       soup_server_connection_disconnect (conn);
+        if (soup_server_message_get_http_version (msg) < SOUP_HTTP_2_0)
+                soup_server_connection_disconnect (conn);
 }
 
 /**
@@ -1125,6 +1128,9 @@ new_connection (SoupListener         *listener,
                 SoupServerConnection *conn,
                 SoupServer           *server)
 {
+        SoupServerPrivate *priv = soup_server_get_instance_private (server);
+
+        soup_server_connection_set_advertise_http2 (conn, priv->http2_enabled);
        soup_server_accept_connection (server, conn);
 }
 
@@ -1986,3 +1992,12 @@ soup_server_remove_websocket_extension (SoupServer *server, GType extension_type
                 }
         }
 }
+
+void
+soup_server_set_http2_enabled (SoupServer *server,
+                               gboolean    enabled)
+{
+        SoupServerPrivate *priv = soup_server_get_instance_private (server);
+
+        priv->http2_enabled = enabled;
+}


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