Browse Source

msh3: add support for QUIC and HTTP/3 using msh3

Considered experimental, as the other HTTP/3 backends.

Closes #8517
Nick Banks 2 years ago
parent
commit
37492ebbfa
18 changed files with 850 additions and 7 deletions
  1. 1 0
      .gitignore
  2. 68 0
      CMake/FindMSH3.cmake
  3. 10 0
      CMakeLists.txt
  4. 1 0
      Makefile.am
  5. 75 2
      configure.ac
  6. 49 0
      docs/HTTP3.md
  7. 2 0
      lib/Makefile.inc
  8. 2 0
      lib/altsvc.c
  9. 3 2
      lib/config-os400.h
  10. 4 1
      lib/curl_config.h.cmake
  11. 1 1
      lib/curl_setup.h
  12. 44 0
      lib/http.h
  13. 4 1
      lib/quic.h
  14. 498 0
      lib/vquic/msh3.c
  15. 38 0
      lib/vquic/msh3.h
  16. 22 0
      winbuild/Makefile.vc
  17. 26 0
      winbuild/MakefileBuild.vc
  18. 2 0
      winbuild/README.md

+ 1 - 0
.gitignore

@@ -18,6 +18,7 @@
 .project
 .settings
 /.vs
+/bld/
 /build/
 /builds/
 /stats/

+ 68 - 0
CMake/FindMSH3.cmake

@@ -0,0 +1,68 @@
+#***************************************************************************
+#                                  _   _ ____  _
+#  Project                     ___| | | |  _ \| |
+#                             / __| | | | |_) | |
+#                            | (__| |_| |  _ <| |___
+#                             \___|\___/|_| \_\_____|
+#
+# Copyright (C) 1998 - 2022, Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+###########################################################################
+
+#[=======================================================================[.rst:
+FindMSH3
+----------
+
+Find the msh3 library
+
+Result Variables
+^^^^^^^^^^^^^^^^
+
+``MSH3_FOUND``
+  System has msh3
+``MSH3_INCLUDE_DIRS``
+  The msh3 include directories.
+``MSH3_LIBRARIES``
+  The libraries needed to use msh3
+#]=======================================================================]
+if(UNIX)
+  find_package(PkgConfig QUIET)
+  pkg_search_module(PC_MSH3 libmsh3)
+endif()
+
+find_path(MSH3_INCLUDE_DIR msh3.h
+  HINTS
+    ${PC_MSH3_INCLUDEDIR}
+    ${PC_MSH3_INCLUDE_DIRS}
+)
+
+find_library(MSH3_LIBRARY NAMES msh3
+  HINTS
+    ${PC_MSH3_LIBDIR}
+    ${PC_MSH3_LIBRARY_DIRS}
+)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(MSH3
+  REQUIRED_VARS
+    MSH3_LIBRARY
+    MSH3_INCLUDE_DIR
+)
+
+if(MSH3_FOUND)
+  set(MSH3_LIBRARIES    ${MSH3_LIBRARY})
+  set(MSH3_INCLUDE_DIRS ${MSH3_INCLUDE_DIR})
+endif()
+
+mark_as_advanced(MSH3_INCLUDE_DIRS MSH3_LIBRARIES)

+ 10 - 0
CMakeLists.txt

@@ -556,6 +556,16 @@ if(USE_QUICHE)
   cmake_pop_check_state()
 endif()
 
+option(USE_MSH3 "Use msquic library for HTTP/3 support" OFF)
+if(USE_MSH3)
+  if(USE_NGTCP2 OR USE_QUICHE)
+    message(FATAL_ERROR "Only one HTTP/3 backend can be selected!")
+  endif()
+  set(USE_MSH3 ON)
+  include_directories(${MSH3_INCLUDE_DIRS})
+  list(APPEND CURL_LIBS ${MSH3_LIBRARIES})
+endif()
+
 if(NOT CURL_DISABLE_LDAP)
   if(WIN32)
     option(USE_WIN32_LDAP "Use Windows LDAP implementation" ON)

+ 1 - 0
Makefile.am

@@ -36,6 +36,7 @@ CMAKE_DIST =                                    \
  CMake/FindGSS.cmake                            \
  CMake/FindLibSSH2.cmake                        \
  CMake/FindMbedTLS.cmake                        \
+ CMake/FindMSH3.cmake                           \
  CMake/FindNGHTTP2.cmake                        \
  CMake/FindNGHTTP3.cmake                        \
  CMake/FindNGTCP2.cmake                         \

+ 75 - 2
configure.ac

@@ -170,7 +170,7 @@ curl_verbose_msg="enabled (--disable-verbose)"
     ssl_backends=
      curl_h1_msg="enabled (internal)"
      curl_h2_msg="no      (--with-nghttp2, --with-hyper)"
-     curl_h3_msg="no      (--with-ngtcp2, --with-quiche)"
+     curl_h3_msg="no      (--with-ngtcp2, --with-quiche --with-msh3)"
 
 enable_altsvc="yes"
 hsts="yes"
@@ -3036,6 +3036,78 @@ AC_INCLUDES_DEFAULT
   fi
 fi
 
+dnl **********************************************************************
+dnl Check for msh3 (QUIC)
+dnl **********************************************************************
+
+OPT_MSH3="no"
+
+if test "x$disable_http" = "xyes" -o "x$USE_NGTCP" = "x1"; then
+  # without HTTP or with ngtcp2, msh3 is no use
+  OPT_MSH3="no"
+fi
+
+AC_ARG_WITH(msh3,
+AS_HELP_STRING([--with-msh3=PATH],[Enable msh3 usage])
+AS_HELP_STRING([--without-msh3],[Disable msh3 usage]),
+  [OPT_MSH3=$withval])
+case "$OPT_MSH3" in
+  no)
+    dnl --without-msh3 option used
+    want_msh3="no"
+    ;;
+  yes)
+    dnl --with-msh3 option used without path
+    want_msh3="default"
+    want_msh3_path=""
+    ;;
+  *)
+    dnl --with-msh3 option used with path
+    want_msh3="yes"
+    want_msh3_path="$withval"
+    ;;
+esac
+
+if test X"$want_msh3" != Xno; then
+
+  if test "$NGHTTP3_ENABLED" = 1; then
+    AC_MSG_ERROR([--with-msh3 and --with-ngtcp2 are mutually exclusive])
+  fi
+
+  dnl backup the pre-msh3 variables
+  CLEANLDFLAGS="$LDFLAGS"
+  CLEANCPPFLAGS="$CPPFLAGS"
+  CLEANLIBS="$LIBS"
+
+  if test -n "$want_msh3_path"; then
+    LD_MSH3="-L$want_msh3_path/lib"
+    CPP_MSH3="-I$want_msh3_path/include"
+    DIR_MSH3="$want_msh3_path/lib"
+    LDFLAGS="$LDFLAGS $LD_MSH3"
+    CPPFLAGS="$CPPFLAGS $CPP_MSH3"
+  fi
+  LIBS="-lmsh3 $LIBS"
+
+  AC_CHECK_LIB(msh3, MsH3ApiOpen,
+    [
+    AC_CHECK_HEADERS(msh3.h,
+        curl_h3_msg="enabled (msh3)"
+        MSH3_ENABLED=1
+        AC_DEFINE(USE_MSH3, 1, [if msh3 is in use])
+        AC_SUBST(USE_MSH3, [1])
+        CURL_LIBRARY_PATH="$CURL_LIBRARY_PATH:$DIR_MSH3"
+        export CURL_LIBRARY_PATH
+        AC_MSG_NOTICE([Added $DIR_MSH3 to CURL_LIBRARY_PATH]),
+        experimental="$experimental HTTP3"
+     )
+    ],
+      dnl not found, revert back to clean variables
+      LDFLAGS=$CLEANLDFLAGS
+      CPPFLAGS=$CLEANCPPFLAGS
+      LIBS=$CLEANLIBS
+  )
+fi
+
 dnl **********************************************************************
 dnl Check for zsh completion path
 dnl **********************************************************************
@@ -4146,7 +4218,8 @@ if test "x$USE_NGHTTP2" = "x1" -o "x$USE_HYPER" = "x1"; then
   SUPPORT_FEATURES="$SUPPORT_FEATURES HTTP2"
 fi
 
-if test "x$USE_NGTCP2" = "x1" -o "x$USE_QUICHE" = "x1"; then
+if test "x$USE_NGTCP2" = "x1" -o "x$USE_QUICHE" = "x1" \
+    -o "x$USE_MSH3" = "x1"; then
   SUPPORT_FEATURES="$SUPPORT_FEATURES HTTP3"
 fi
 

+ 49 - 0
docs/HTTP3.md

@@ -19,6 +19,8 @@ QUIC libraries we are experimenting with:
 
 [quiche](https://github.com/cloudflare/quiche)
 
+[msquic](https://github.com/microsoft/msquic) & [msh3](https://github.com/nibanks/msh3)
+
 ## Experimental
 
 HTTP/3 and QUIC support in curl is considered **EXPERIMENTAL** until further
@@ -136,6 +138,53 @@ Build curl:
 
  If `make install` results in `Permission denied` error, you will need to prepend it with `sudo`.
 
+# msh3 (msquic) version
+
+## Build Linux (with quictls fork of OpenSSL)
+
+Build msh3:
+
+     % git clone -b v0.1.0 --single-branch --recursive https://github.com/nibanks/msh3
+     % cd msh3 && mkdir build && cd build
+     % cmake -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=RelWithDebInfo ..
+     % cmake --build .
+     % cmake --install .
+
+Build curl:
+
+     % git clone https://github.com/curl/curl
+     % cd curl
+     % autoreconf -fi
+     % ./configure LDFLAGS="-Wl,-rpath,/usr/local/lib" --with-msh3=/usr/local --with-openssl
+     % make
+     % make install
+
+Run from `/usr/local/bin/curl`.
+
+## Build Windows
+
+Build msh3:
+
+     % git clone -b v0.2.0 --single-branch --recursive https://github.com/nibanks/msh3
+     % cd msh3 && mkdir build && cd build
+     % cmake -G 'Visual Studio 17 2022' -DCMAKE_BUILD_TYPE=RelWithDebInfo ..
+     % cmake --build . --config Release
+     % cmake --install . --config Release
+
+> **Note** - On Windows, Schannel will be used for TLS support by default. If you with to use (the quictls fork of) OpenSSL, specify the `-DQUIC_TLS=openssl` option to the generate command above. Also note that OpenSSL brings with it an additional set of build dependencies not specified here.
+
+Build curl (in [Visual Studio Command prompt](../winbuild/README.md#open-a-command-prompt)):
+
+     % git clone https://github.com/curl/curl
+     % cd curl/winbuild
+     % nmake /f Makefile.vc mode=dll WITH_MSH3=dll MSH3_PATH="C:/Program Files/msh3" MACHINE=x64
+
+Note: If you encouter a build error with `tool_hugehelp.c` being missing, rename `tool_hugehelp.c.cvs` in the same directory to `tool_hugehelp.c` and then run `nmake` again.
+
+Run in the `C:/Program Files/msh3/lib` directory, copy `curl.exe` to that directory, or copy `msquic.dll` and `msh3.dll` from that directory to the `curl.exe` directory. For example:
+
+     % C:\Program Files\msh3\lib> F:\curl\builds\libcurl-vc-x64-release-dll-ipv6-sspi-schannel-msh3\bin\curl.exe --http3 https://www.google.com
+
 # `--http3`
 
 Use HTTP/3 directly:

+ 2 - 0
lib/Makefile.inc

@@ -76,11 +76,13 @@ LIB_VTLS_HFILES =           \
   vtls/x509asn1.h
 
 LIB_VQUIC_CFILES = \
+  vquic/msh3.c   \
   vquic/ngtcp2.c   \
   vquic/quiche.c   \
   vquic/vquic.c
 
 LIB_VQUIC_HFILES = \
+  vquic/msh3.h   \
   vquic/ngtcp2.h   \
   vquic/quiche.h   \
   vquic/vquic.h

+ 2 - 0
lib/altsvc.c

@@ -54,6 +54,8 @@
 #define H3VERSION "h3-29"
 #elif defined(USE_NGTCP2) && !defined(UNITTESTS)
 #define H3VERSION "h3-29"
+#elif defined(USE_MSH3) && !defined(UNITTESTS)
+#define H3VERSION "h3-29"
 #else
 #define H3VERSION "h3"
 #endif

+ 3 - 2
lib/config-os400.h

@@ -7,7 +7,7 @@
  *                            | (__| |_| |  _ <| |___
  *                             \___|\___/|_| \_\_____|
  *
- * Copyright (C) 1998 - 2021, Daniel Stenberg, <daniel@haxx.se>, et al.
+ * Copyright (C) 1998 - 2022, Daniel Stenberg, <daniel@haxx.se>, et al.
  *
  * This software is licensed as described in the file COPYING, which
  * you should have received as part of this distribution. The terms
@@ -371,7 +371,8 @@
 /* Define if you can safely include both <sys/time.h> and <time.h>. */
 #define TIME_WITH_SYS_TIME
 
-/* Define to enable HTTP3 support (experimental, requires NGTCP2 or QUICHE) */
+/* Define to enable HTTP3 support (experimental, requires NGTCP2, QUICHE or
+   MSH3) */
 #undef ENABLE_QUIC
 
 /* Version number of package */

+ 4 - 1
lib/curl_config.h.cmake

@@ -5,7 +5,7 @@
  *                            | (__| |_| |  _ <| |___
  *                             \___|\___/|_| \_\_____|
  *
- * Copyright (C) 1998 - 2021, Daniel Stenberg, <daniel@haxx.se>, et al.
+ * Copyright (C) 1998 - 2022, Daniel Stenberg, <daniel@haxx.se>, et al.
  *
  * This software is licensed as described in the file COPYING, which
  * you should have received as part of this distribution. The terms
@@ -949,6 +949,9 @@ ${SIZEOF_TIME_T_CODE}
 /* Define to 1 if you have the quiche_conn_set_qlog_fd function. */
 #cmakedefine HAVE_QUICHE_CONN_SET_QLOG_FD 1
 
+/* to enable msh3 */
+#cmakedefine USE_MSH3 1
+
 /* if Unix domain sockets are enabled  */
 #cmakedefine USE_UNIX_SOCKETS
 

+ 1 - 1
lib/curl_setup.h

@@ -794,7 +794,7 @@ int getpwuid_r(uid_t uid, struct passwd *pwd, char *buf,
 #define USE_HTTP2
 #endif
 
-#if defined(USE_NGTCP2) || defined(USE_QUICHE)
+#if defined(USE_NGTCP2) || defined(USE_QUICHE) || defined(USE_MSH3)
 #define ENABLE_QUIC
 #endif
 

+ 44 - 0
lib/http.h

@@ -38,6 +38,10 @@ typedef enum {
 #include <nghttp2/nghttp2.h>
 #endif
 
+#if defined(_WIN32) && defined(ENABLE_QUIC)
+#include <stdint.h>
+#endif
+
 extern const struct Curl_handler Curl_handler_http;
 
 #ifdef USE_SSL
@@ -163,6 +167,29 @@ CURLcode Curl_http_auth_act(struct Curl_easy *data);
 struct h3out; /* see ngtcp2 */
 #endif
 
+#ifdef USE_MSH3
+#ifdef _WIN32
+#define msh3_lock CRITICAL_SECTION
+#define msh3_lock_initialize(lock) InitializeCriticalSection(lock)
+#define msh3_lock_uninitialize(lock) DeleteCriticalSection(lock)
+#define msh3_lock_acquire(lock) EnterCriticalSection(lock)
+#define msh3_lock_release(lock) LeaveCriticalSection(lock)
+#else /* !_WIN32 */
+#include <pthread.h>
+#define msh3_lock pthread_mutex_t
+#define msh3_lock_initialize(lock) { \
+  pthread_mutexattr_t attr; \
+  pthread_mutexattr_init(&attr); \
+  pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); \
+  pthread_mutex_init(lock, &attr); \
+  pthread_mutexattr_destroy(&attr); \
+}
+#define msh3_lock_uninitialize(lock) pthread_mutex_destroy(lock)
+#define msh3_lock_acquire(lock) pthread_mutex_lock(lock)
+#define msh3_lock_release(lock) pthread_mutex_unlock(lock)
+#endif /* _WIN32 */
+#endif /* USE_MSH3 */
+
 /****************************************************************************
  * HTTP unique setup
  ***************************************************************************/
@@ -228,11 +255,13 @@ struct HTTP {
 #endif
 
 #ifdef ENABLE_QUIC
+#ifndef USE_MSH3
   /*********** for HTTP/3 we store stream-local data here *************/
   int64_t stream3_id; /* stream we are interested in */
   bool firstheader;  /* FALSE until headers arrive */
   bool firstbody;  /* FALSE until body arrives */
   bool h3req;    /* FALSE until request is issued */
+#endif
   bool upload_done;
 #endif
 #ifdef USE_NGHTTP3
@@ -240,6 +269,21 @@ struct HTTP {
   struct h3out *h3out; /* per-stream buffers for upload */
   struct dynbuf overflow; /* excess data received during a single Curl_read */
 #endif
+#ifdef USE_MSH3
+  struct MSH3_REQUEST *req;
+  msh3_lock recv_lock;
+  /* Receive Buffer (Headers and Data) */
+  uint8_t* recv_buf;
+  size_t recv_buf_alloc;
+  /* Receive Headers */
+  size_t recv_header_len;
+  bool recv_header_complete;
+  /* Receive Data */
+  size_t recv_data_len;
+  bool recv_data_complete;
+  /* General Receive Error */
+  CURLcode recv_error;
+#endif
 };
 
 #ifdef USE_NGHTTP2

+ 4 - 1
lib/quic.h

@@ -7,7 +7,7 @@
  *                            | (__| |_| |  _ <| |___
  *                             \___|\___/|_| \_\_____|
  *
- * Copyright (C) 1998 - 2021, Daniel Stenberg, <daniel@haxx.se>, et al.
+ * Copyright (C) 1998 - 2022, Daniel Stenberg, <daniel@haxx.se>, et al.
  *
  * This software is licensed as described in the file COPYING, which
  * you should have received as part of this distribution. The terms
@@ -31,6 +31,9 @@
 #ifdef USE_QUICHE
 #include "vquic/quiche.h"
 #endif
+#ifdef USE_MSH3
+#include "vquic/msh3.h"
+#endif
 
 #include "urldata.h"
 

+ 498 - 0
lib/vquic/msh3.c

@@ -0,0 +1,498 @@
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) 1998 - 2022, Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ***************************************************************************/
+
+#include "curl_setup.h"
+
+#ifdef USE_MSH3
+
+#include "urldata.h"
+#include "curl_printf.h"
+#include "timeval.h"
+#include "multiif.h"
+#include "sendf.h"
+#include "connect.h"
+#include "h2h3.h"
+#include "msh3.h"
+
+/* #define DEBUG_HTTP3 1 */
+#ifdef DEBUG_HTTP3
+#define H3BUGF(x) x
+#else
+#define H3BUGF(x) do { } while(0)
+#endif
+
+#define MSH3_REQ_INIT_BUF_LEN 8192
+
+static CURLcode msh3_do_it(struct Curl_easy *data, bool *done);
+static int msh3_getsock(struct Curl_easy *data,
+                        struct connectdata *conn, curl_socket_t *socks);
+static CURLcode msh3_disconnect(struct Curl_easy *data,
+                                struct connectdata *conn,
+                                bool dead_connection);
+static unsigned int msh3_conncheck(struct Curl_easy *data,
+                                   struct connectdata *conn,
+                                   unsigned int checks_to_perform);
+static Curl_recv msh3_stream_recv;
+static Curl_send msh3_stream_send;
+static void MSH3_CALL msh3_header_received(MSH3_REQUEST *Request,
+                                           void *IfContext,
+                                           const MSH3_HEADER *Header);
+static void MSH3_CALL msh3_data_received(MSH3_REQUEST *Request,
+                                        void *IfContext, uint32_t Length,
+                                        const uint8_t *Data);
+static void MSH3_CALL msh3_complete(MSH3_REQUEST *Request, void *IfContext,
+                                    bool Aborted, uint64_t AbortError);
+static void MSH3_CALL msh3_shutdown(MSH3_REQUEST *Request, void *IfContext);
+
+static const struct Curl_handler msh3_curl_handler_http3 = {
+  "HTTPS",                              /* scheme */
+  ZERO_NULL,                            /* setup_connection */
+  msh3_do_it,                           /* do_it */
+  Curl_http_done,                       /* done */
+  ZERO_NULL,                            /* do_more */
+  ZERO_NULL,                            /* connect_it */
+  ZERO_NULL,                            /* connecting */
+  ZERO_NULL,                            /* doing */
+  msh3_getsock,                         /* proto_getsock */
+  msh3_getsock,                         /* doing_getsock */
+  ZERO_NULL,                            /* domore_getsock */
+  msh3_getsock,                         /* perform_getsock */
+  msh3_disconnect,                      /* disconnect */
+  ZERO_NULL,                            /* readwrite */
+  msh3_conncheck,                       /* connection_check */
+  ZERO_NULL,                            /* attach connection */
+  PORT_HTTP,                            /* defport */
+  CURLPROTO_HTTPS,                      /* protocol */
+  CURLPROTO_HTTP,                       /* family */
+  PROTOPT_SSL | PROTOPT_STREAM          /* flags */
+};
+
+static const MSH3_REQUEST_IF msh3_request_if = {
+  msh3_header_received,
+  msh3_data_received,
+  msh3_complete,
+  msh3_shutdown
+};
+
+void Curl_quic_ver(char *p, size_t len)
+{
+  (void)msnprintf(p, len, "msh3/%s", "0.0.1");
+}
+
+CURLcode Curl_quic_connect(struct Curl_easy *data,
+                           struct connectdata *conn,
+                           curl_socket_t sockfd,
+                           int sockindex,
+                           const struct sockaddr *addr,
+                           socklen_t addrlen)
+{
+  struct quicsocket *qs = &conn->hequic[sockindex];
+  bool unsecure = !conn->ssl_config.verifypeer;
+  memset(qs, 0, sizeof(*qs));
+
+  (void)sockfd;
+  (void)addr; /* TODO - Pass address along */
+  (void)addrlen;
+
+  H3BUGF(infof(data, "creating new api/connection"));
+
+  qs->api = MsH3ApiOpen();
+  if(!qs->api) {
+    failf(data, "can't create msh3 api");
+    return CURLE_FAILED_INIT;
+  }
+
+  qs->conn = MsH3ConnectionOpen(qs->api, conn->host.name, unsecure);
+  if(!qs->conn) {
+    failf(data, "can't create msh3 connection");
+    if(qs->api) {
+      MsH3ApiClose(qs->api);
+    }
+    return CURLE_FAILED_INIT;
+  }
+
+  return CURLE_OK;
+}
+
+CURLcode Curl_quic_is_connected(struct Curl_easy *data,
+                                struct connectdata *conn,
+                                int sockindex,
+                                bool *connected)
+{
+  struct quicsocket *qs = &conn->hequic[sockindex];
+  MSH3_CONNECTION_STATE state;
+
+  state = MsH3ConnectionGetState(qs->conn, false);
+  if(state == MSH3_CONN_HANDSHAKE_FAILED || state == MSH3_CONN_DISCONNECTED) {
+    failf(data, "failed to connect, state=%u", (uint32_t)state);
+    return CURLE_COULDNT_CONNECT;
+  }
+
+  if(state == MSH3_CONN_CONNECTED) {
+    H3BUGF(infof(data, "connection connected"));
+    *connected = true;
+    conn->quic = qs;
+    conn->recv[sockindex] = msh3_stream_recv;
+    conn->send[sockindex] = msh3_stream_send;
+    conn->handler = &msh3_curl_handler_http3;
+    conn->bits.multiplex = TRUE; /* at least potentially multiplexed */
+    conn->httpversion = 30;
+    conn->bundle->multiuse = BUNDLE_MULTIPLEX;
+    /* TODO - Clean up other happy-eyeballs connection(s)? */
+  }
+
+  return CURLE_OK;
+}
+
+static int msh3_getsock(struct Curl_easy *data,
+                        struct connectdata *conn, curl_socket_t *socks)
+{
+  struct HTTP *stream = data->req.p.http;
+  int bitmap = GETSOCK_BLANK;
+
+  socks[0] = conn->sock[FIRSTSOCKET];
+
+  if(stream->recv_error) {
+    bitmap |= GETSOCK_READSOCK(FIRSTSOCKET);
+    data->state.drain++;
+  }
+  else if(stream->recv_header_len || stream->recv_data_len) {
+    bitmap |= GETSOCK_READSOCK(FIRSTSOCKET);
+    data->state.drain++;
+  }
+
+  H3BUGF(infof(data, "msh3_getsock %u", (uint32_t)data->state.drain));
+
+  return bitmap;
+}
+
+static CURLcode msh3_do_it(struct Curl_easy *data, bool *done)
+{
+  struct HTTP *stream = data->req.p.http;
+  H3BUGF(infof(data, "msh3_do_it"));
+  stream->recv_buf = malloc(MSH3_REQ_INIT_BUF_LEN);
+  if(!stream->recv_buf) {
+    return CURLE_OUT_OF_MEMORY;
+  }
+  stream->req = ZERO_NULL;
+  msh3_lock_initialize(&stream->recv_lock);
+  stream->recv_buf_alloc = MSH3_REQ_INIT_BUF_LEN;
+  stream->recv_header_len = 0;
+  stream->recv_header_complete = false;
+  stream->recv_data_len = 0;
+  stream->recv_data_complete = false;
+  stream->recv_error = CURLE_OK;
+  return Curl_http(data, done);
+}
+
+static unsigned int msh3_conncheck(struct Curl_easy *data,
+                                   struct connectdata *conn,
+                                   unsigned int checks_to_perform)
+{
+  (void)data;
+  (void)conn;
+  (void)checks_to_perform;
+  H3BUGF(infof(data, "msh3_conncheck"));
+  return CONNRESULT_NONE;
+}
+
+static void msh3_cleanup(struct quicsocket *qs, struct HTTP *stream)
+{
+  if(stream && stream->recv_buf) {
+    free(stream->recv_buf);
+    stream->recv_buf = ZERO_NULL;
+    msh3_lock_uninitialize(&stream->recv_lock);
+  }
+  if(qs->conn) {
+    MsH3ConnectionClose(qs->conn);
+    qs->conn = ZERO_NULL;
+  }
+  if(qs->api) {
+    MsH3ApiClose(qs->api);
+    qs->api = ZERO_NULL;
+  }
+}
+
+static CURLcode msh3_disconnect(struct Curl_easy *data,
+                                struct connectdata *conn, bool dead_connection)
+{
+  (void)dead_connection;
+  H3BUGF(infof(data, "disconnecting (msh3)"));
+  msh3_cleanup(conn->quic, data->req.p.http);
+  return CURLE_OK;
+}
+
+void Curl_quic_disconnect(struct Curl_easy *data, struct connectdata *conn,
+                          int tempindex)
+{
+  if(conn->transport == TRNSPRT_QUIC) {
+    H3BUGF(infof(data, "disconnecting (curl)"));
+    msh3_cleanup(&conn->hequic[tempindex], data->req.p.http);
+  }
+}
+
+/* Requires stream->recv_lock to be held */
+static bool msh3request_ensure_room(struct HTTP *stream, size_t len)
+{
+  uint8_t *new_recv_buf;
+  const size_t cur_recv_len = stream->recv_header_len + stream->recv_data_len;
+  if(cur_recv_len + len > stream->recv_buf_alloc) {
+    size_t new_recv_buf_alloc_len = stream->recv_buf_alloc;
+    do {
+      new_recv_buf_alloc_len <<= 1; /* TODO - handle overflow */
+    } while(cur_recv_len + len > new_recv_buf_alloc_len);
+    new_recv_buf = malloc(new_recv_buf_alloc_len);
+    if(!new_recv_buf) {
+      return false;
+    }
+    if(cur_recv_len) {
+      memcpy(new_recv_buf, stream->recv_buf, cur_recv_len);
+    }
+    stream->recv_buf_alloc = new_recv_buf_alloc_len;
+    free(stream->recv_buf);
+    stream->recv_buf = new_recv_buf;
+  }
+  return true;
+}
+
+static void MSH3_CALL msh3_header_received(MSH3_REQUEST *Request,
+                                           void *IfContext,
+                                           const MSH3_HEADER *Header)
+{
+  struct HTTP *stream = IfContext;
+  size_t total_len;
+  (void)Request;
+  H3BUGF(printf("* msh3_header_received\n"));
+
+  if(stream->recv_header_complete) {
+    H3BUGF(printf("* ignoring header after data\n"));
+    return;
+  }
+
+  msh3_lock_acquire(&stream->recv_lock);
+
+  if((Header->NameLength == 7) &&
+     !strncmp(H2H3_PSEUDO_STATUS, (char *)Header->Name, 7)) {
+     total_len = 9 + Header->ValueLength;
+    if(!msh3request_ensure_room(stream, total_len)) {
+      /* TODO - handle error */
+      goto release_lock;
+    }
+    msnprintf((char *)stream->recv_buf + stream->recv_header_len,
+              stream->recv_buf_alloc - stream->recv_header_len,
+              "HTTP/3 %.*s\n", (int)Header->ValueLength, Header->Value);
+  }
+  else {
+    total_len = Header->NameLength + 4 + Header->ValueLength;
+    if(!msh3request_ensure_room(stream, total_len)) {
+      /* TODO - handle error */
+      goto release_lock;
+    }
+    msnprintf((char *)stream->recv_buf + stream->recv_header_len,
+              stream->recv_buf_alloc - stream->recv_header_len,
+              "%.*s: %.*s\n",
+              (int)Header->NameLength, Header->Name,
+              (int)Header->ValueLength, Header->Value);
+  }
+
+  stream->recv_header_len += total_len - 1; /* don't include null-terminator */
+
+release_lock:
+  msh3_lock_release(&stream->recv_lock);
+}
+
+static void MSH3_CALL msh3_data_received(MSH3_REQUEST *Request,
+                                         void *IfContext, uint32_t Length,
+                                         const uint8_t *Data)
+{
+  struct HTTP *stream = IfContext;
+  size_t cur_recv_len = stream->recv_header_len + stream->recv_data_len;
+  (void)Request;
+  H3BUGF(printf("* msh3_data_received %u. %zu buffered, %zu allocated\n",
+                Length, cur_recv_len, stream->recv_buf_alloc));
+  msh3_lock_acquire(&stream->recv_lock);
+  if(!stream->recv_header_complete) {
+    H3BUGF(printf("* Headers complete!\n"));
+    if(!msh3request_ensure_room(stream, 2)) {
+      /* TODO - handle error */
+      goto release_lock;
+    }
+    stream->recv_buf[stream->recv_header_len++] = '\r';
+    stream->recv_buf[stream->recv_header_len++] = '\n';
+    stream->recv_header_complete = true;
+    cur_recv_len += 2;
+  }
+  if(!msh3request_ensure_room(stream, Length)) {
+    /* TODO - handle error */
+    goto release_lock;
+  }
+  memcpy(stream->recv_buf + cur_recv_len, Data, Length);
+  stream->recv_data_len += (size_t)Length;
+release_lock:
+  msh3_lock_release(&stream->recv_lock);
+}
+
+static void MSH3_CALL msh3_complete(MSH3_REQUEST *Request, void *IfContext,
+                                    bool Aborted, uint64_t AbortError)
+{
+  struct HTTP *stream = IfContext;
+  (void)Request;
+  (void)AbortError;
+  H3BUGF(printf("* msh3_complete, aborted=%hhu\n", Aborted));
+  msh3_lock_acquire(&stream->recv_lock);
+  if(Aborted) {
+    stream->recv_error = CURLE_HTTP3; /* TODO - how do we pass AbortError? */
+  }
+  stream->recv_header_complete = true;
+  stream->recv_data_complete = true;
+  msh3_lock_release(&stream->recv_lock);
+}
+
+static void MSH3_CALL msh3_shutdown(MSH3_REQUEST *Request, void *IfContext)
+{
+  struct HTTP *stream = IfContext;
+  (void)Request;
+  (void)stream;
+}
+
+static_assert(sizeof(MSH3_HEADER) == sizeof(struct h2h3pseudo),
+              "Sizes must match for cast below to work");
+
+static ssize_t msh3_stream_send(struct Curl_easy *data,
+                                int sockindex,
+                                const void *mem,
+                                size_t len,
+                                CURLcode *curlcode)
+{
+  struct connectdata *conn = data->conn;
+  struct HTTP *stream = data->req.p.http;
+  struct quicsocket *qs = conn->quic;
+  struct h2h3req *hreq;
+
+  (void)sockindex;
+  H3BUGF(infof(data, "msh3_stream_send %zu", len));
+
+  if(!stream->req) {
+    *curlcode = Curl_pseudo_headers(data, mem, len, &hreq);
+    if(*curlcode) {
+      failf(data, "Curl_pseudo_headers failed");
+      return -1;
+    }
+    H3BUGF(infof(data, "starting request with %zu headers", hreq->entries));
+    stream->req = MsH3RequestOpen(qs->conn, &msh3_request_if, stream,
+                                 (MSH3_HEADER*)hreq->header, hreq->entries);
+    Curl_pseudo_free(hreq);
+    if(!stream->req) {
+      failf(data, "request open failed");
+      *curlcode = CURLE_SEND_ERROR;
+      return -1;
+    }
+    *curlcode = CURLE_OK;
+    return len;
+  }
+  H3BUGF(infof(data, "send %zd body bytes on request %p", len,
+               (void *)stream->req));
+  *curlcode = CURLE_SEND_ERROR;
+  return -1;
+}
+
+static ssize_t msh3_stream_recv(struct Curl_easy *data,
+                                int sockindex,
+                                char *buf,
+                                size_t buffersize,
+                                CURLcode *curlcode)
+{
+  struct HTTP *stream = data->req.p.http;
+  size_t outsize = 0;
+  (void)sockindex;
+  H3BUGF(infof(data, "msh3_stream_recv %zu", buffersize));
+
+  if(stream->recv_error) {
+    failf(data, "request aborted");
+    *curlcode = stream->recv_error;
+    return -1;
+  }
+
+  msh3_lock_acquire(&stream->recv_lock);
+
+  if(stream->recv_header_len) {
+    outsize = buffersize;
+    if(stream->recv_header_len < outsize) {
+      outsize = stream->recv_header_len;
+    }
+    memcpy(buf, stream->recv_buf, outsize);
+    if(outsize < stream->recv_header_len + stream->recv_data_len) {
+      memmove(stream->recv_buf, stream->recv_buf + outsize,
+              stream->recv_header_len + stream->recv_data_len - outsize);
+    }
+    stream->recv_header_len -= outsize;
+    H3BUGF(infof(data, "returned %zu bytes of headers", outsize));
+  }
+  else if(stream->recv_data_len) {
+    outsize = buffersize;
+    if(stream->recv_data_len < outsize) {
+      outsize = stream->recv_data_len;
+    }
+    memcpy(buf, stream->recv_buf, outsize);
+    if(outsize < stream->recv_data_len) {
+      memmove(stream->recv_buf, stream->recv_buf + outsize,
+              stream->recv_data_len - outsize);
+    }
+    stream->recv_data_len -= outsize;
+    H3BUGF(infof(data, "returned %zu bytes of data", outsize));
+  }
+  else if(stream->recv_data_complete) {
+    H3BUGF(infof(data, "receive complete"));
+  }
+
+  msh3_lock_release(&stream->recv_lock);
+
+  return (ssize_t)outsize;
+}
+
+CURLcode Curl_quic_done_sending(struct Curl_easy *data)
+{
+  struct connectdata *conn = data->conn;
+  H3BUGF(infof(data, "Curl_quic_done_sending"));
+  if(conn->handler == &msh3_curl_handler_http3) {
+    struct HTTP *stream = data->req.p.http;
+    stream->upload_done = TRUE;
+  }
+
+  return CURLE_OK;
+}
+
+void Curl_quic_done(struct Curl_easy *data, bool premature)
+{
+  (void)data;
+  (void)premature;
+  H3BUGF(infof(data, "Curl_quic_done"));
+}
+
+bool Curl_quic_data_pending(const struct Curl_easy *data)
+{
+  struct HTTP *stream = data->req.p.http;
+  H3BUGF(infof((struct Curl_easy *)data, "Curl_quic_data_pending"));
+  return stream->recv_header_len || stream->recv_data_len;
+}
+
+#endif /* USE_MSH3 */

+ 38 - 0
lib/vquic/msh3.h

@@ -0,0 +1,38 @@
+#ifndef HEADER_CURL_VQUIC_MSH3_H
+#define HEADER_CURL_VQUIC_MSH3_H
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) 1998 - 2022, Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ***************************************************************************/
+
+#include "curl_setup.h"
+
+#ifdef USE_MSH3
+
+#include <msh3.h>
+
+struct quicsocket {
+  MSH3_API* api;
+  MSH3_CONNECTION* conn;
+};
+
+#endif /* USE_MSQUIC */
+
+#endif /* HEADER_CURL_VQUIC_MSH3_H */

+ 22 - 0
winbuild/Makefile.vc

@@ -149,6 +149,23 @@ NGHTTP2     = static
 USE_NGHTTP2 = false
 !ENDIF
 
+!IF "$(ENABLE_MSH3)"=="yes"
+# compatibility bit, WITH_MSH3 is the correct flag
+WITH_MSH3    = dll
+USE_MSH3     = true
+MSH3         = dll
+!ELSEIF "$(WITH_MSH3)"=="dll"
+USE_MSH3     = true
+MSH3         = dll
+!ELSEIF "$(WITH_MSH3)"=="static"
+USE_MSH3     = true
+MSH3         = static
+!ENDIF
+
+!IFNDEF USE_MSH3
+USE_MSH3 = false
+!ENDIF
+
 !IF "$(WITH_MBEDTLS)"=="dll" || "$(WITH_MBEDTLS)"=="static"
 USE_MBEDTLS = true
 MBEDTLS     = $(WITH_MBEDTLS)
@@ -240,6 +257,10 @@ CONFIG_NAME_LIB = $(CONFIG_NAME_LIB)-schannel
 CONFIG_NAME_LIB = $(CONFIG_NAME_LIB)-nghttp2-$(NGHTTP2)
 !ENDIF
 
+!IF "$(USE_MSH3)"=="true"
+CONFIG_NAME_LIB = $(CONFIG_NAME_LIB)-msh3
+!ENDIF
+
 !MESSAGE configuration name: $(CONFIG_NAME_LIB)
 
 BUILD_DIR=../builds/$(CONFIG_NAME_LIB)
@@ -261,6 +282,7 @@ $(MODE):
 	@SET CONFIG_NAME_LIB=$(CONFIG_NAME_LIB)
 	@SET MACHINE=$(MACHINE)
 	@SET USE_NGHTTP2=$(USE_NGHTTP2)
+	@SET USE_MSH3=$(USE_MSH3)
 	@SET USE_IDN=$(USE_IDN)
 	@SET USE_IPV6=$(USE_IPV6)
 	@SET USE_SSPI=$(USE_SSPI)

+ 26 - 0
winbuild/MakefileBuild.vc

@@ -164,6 +164,26 @@ NGHTTP2_LIBS     = nghttp2.lib
 !ENDIF
 !ENDIF
 
+!IFDEF MSH3_PATH
+MSH3_INC_DIR     = $(MSH3_PATH)\include
+MSH3_LIB_DIR     = $(MSH3_PATH)\lib
+MSH3_LFLAGS      = $(MSH3_LFLAGS) "/LIBPATH:$(MSH3_LIB_DIR)"
+!ELSE
+MSH3_INC_DIR     = $(DEVEL_INCLUDE)
+MSH3_LIB_DIR     = $(DEVEL_LIB)
+!ENDIF
+
+!IF "$(WITH_MSH3)"=="dll"
+MSH3_CFLAGS      = /DUSE_MSH3 /I"$(MSH3_INC_DIR)"
+MSH3_LIBS        = msh3.lib
+!ELSEIF "$(WITH_MSH3)"=="static"
+MSH3_CFLAGS      = /DUSE_MSH3 /DMSH3_STATICLIB /I"$(MSH3_INC_DIR)"
+!IF EXISTS("$(NGHTTP2_LIB_DIR)\msh3_static.lib")
+MSH3_LIBS        = msh3_static.lib
+!ELSE
+MSH3_LIBS        = msh3.lib
+!ENDIF
+!ENDIF
 
 !IFDEF MBEDTLS_PATH
 MBEDTLS_INC_DIR  = $(MBEDTLS_PATH)\include
@@ -492,6 +512,11 @@ CFLAGS = $(CFLAGS) $(NGHTTP2_CFLAGS)
 LFLAGS = $(LFLAGS) $(NGHTTP2_LFLAGS) $(NGHTTP2_LIBS)
 !ENDIF
 
+!IF "$(USE_MSH3)"=="true"
+CFLAGS = $(CFLAGS) $(MSH3_CFLAGS)
+LFLAGS = $(LFLAGS) $(MSH3_LFLAGS) $(MSH3_LIBS)
+!ENDIF
+
 !IF "$(GEN_PDB)"=="true"
 CFLAGS = $(CFLAGS) $(CFLAGS_PDB) /Fd"$(LIB_DIROBJ)\$(PDB)"
 LFLAGS = $(LFLAGS) $(LFLAGS_PDB)
@@ -545,6 +570,7 @@ package: $(TARGET)
 $(TARGET): $(LIB_OBJS) $(LIB_DIROBJ) $(DIRDIST)
 	@echo Using SSL: $(USE_SSL)
 	@echo Using NGHTTP2: $(USE_NGHTTP2)
+	@echo Using MSH3: $(USE_MSH3)
 	@echo Using c-ares: $(USE_CARES)
 	@echo Using SSH2: $(USE_SSH2)
 	@echo Using SSH: $(USE_SSH)

+ 2 - 0
winbuild/README.md

@@ -80,6 +80,7 @@ where `<options>` is one or many of:
                                    Uncompress them into the deps folder.
  - `WITH_SSL=<dll/static>`       - Enable OpenSSL support, DLL or static
  - `WITH_NGHTTP2=<dll/static>`   - Enable HTTP/2 support, DLL or static
+ - `WITH_MSH3=<dll/static>`      - Enable (experimental) HTTP/3 support, DLL or static
  - `WITH_MBEDTLS=<dll/static>`   - Enable mbedTLS support, DLL or static
  - `WITH_CARES=<dll/static>`     - Enable c-ares support, DLL or static
  - `WITH_ZLIB=<dll/static>`      - Enable zlib support, DLL or static
@@ -103,6 +104,7 @@ where `<options>` is one or many of:
  - `CARES_PATH=<path>`           - Custom path for c-ares
  - `MBEDTLS_PATH=<path>`         - Custom path for mbedTLS
  - `NGHTTP2_PATH=<path>`         - Custom path for nghttp2
+ - `MSH3_PATH=<path>`            - Custom path for msh3
  - `SSH2_PATH=<path>`            - Custom path for libSSH2
  - `SSL_PATH=<path>`             - Custom path for OpenSSL
  - `ZLIB_PATH=<path>`            - Custom path for zlib