summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarlos Garcia Campos <cgarcia@igalia.com>2022-07-27 15:15:11 +0200
committerCarlos Garcia Campos <cgarcia@igalia.com>2022-08-12 12:20:02 +0200
commit5fb25e7810498170dd3458c9509035cef945e299 (patch)
tree998f3fcbbe0438e11d0ea6ecd1ab635ab53f64f3
parentb0988100dc962e6700a4b0ede3afba353e0a5f4d (diff)
downloadlibsoup-5fb25e7810498170dd3458c9509035cef945e299.tar.gz
server: add initial support for HTTP/2
For now the plan is to use it only for testing the client APIs.
-rw-r--r--libsoup/meson.build2
-rw-r--r--libsoup/server/http2/soup-server-message-io-http2.c800
-rw-r--r--libsoup/server/http2/soup-server-message-io-http2.h14
-rw-r--r--libsoup/server/soup-server-connection.c57
-rw-r--r--libsoup/server/soup-server-connection.h2
-rw-r--r--libsoup/server/soup-server-message.c3
-rw-r--r--libsoup/server/soup-server-private.h14
-rw-r--r--libsoup/server/soup-server.c19
8 files changed, 902 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..c1682e04
--- /dev/null
+++ b/libsoup/server/http2/soup-server-message-io-http2.c
@@ -0,0 +1,800 @@
+/* -*- 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_clear_pointer (&msg_io->unpause_source, g_source_unref);
+ }
+
+ 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,
+ int32_t stream_id)
+{
+ SoupMessageIOHTTP2 *msg_io;
+
+ /* The initial message is created earlier to handle the TLS certificate.
+ * If there's only one message without a stream id, that means it's the
+ * initial message and should be used now.
+ */
+ 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) {
+ msg_io->stream_id = stream_id;
+ return msg_io;
+ }
+ }
+
+ msg_io = soup_message_io_http2_new (soup_server_message_new (io->conn));
+ msg_io->stream_id = stream_id;
+ 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, 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;
+ 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_SETTINGS_ENABLE_PUSH, 0 }
+ };
+ 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..f14058c2 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_auth_mode == G_TLS_AUTHENTICATION_NONE)
+ 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;
+}