diff options
Diffstat (limited to 'examples')
102 files changed, 26227 insertions, 0 deletions
diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..b2e3553 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,15 @@ +client +server +examplestest +h09client +h09server +gtlsclient +gtlsserver +bsslclient +bsslserver +ptlsclient +ptlsserver +simpleclient +wsslclient +wsslserver +gtlssimpleclient diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..09701b8 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,359 @@ +# ngtcp2 + +# Copyright (c) 2017 ngtcp2 contributors + +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +if(LIBEV_FOUND AND HAVE_OPENSSL AND LIBNGHTTP3_FOUND) + set(client_SOURCES + client.cc + client_base.cc + debug.cc + util.cc + shared.cc + tls_client_context_openssl.cc + tls_client_session_openssl.cc + tls_session_base_openssl.cc + util_openssl.cc + ) + + set(server_SOURCES + server.cc + server_base.cc + debug.cc + util.cc + http.cc + shared.cc + tls_server_context_openssl.cc + tls_server_session_openssl.cc + tls_session_base_openssl.cc + util_openssl.cc + ) + + set(ossl_INCLUDE_DIRS + ${CMAKE_SOURCE_DIR}/lib/includes + ${CMAKE_BINARY_DIR}/lib/includes + ${CMAKE_SOURCE_DIR}/third-party + ${CMAKE_SOURCE_DIR}/crypto/includes + + ${JEMALLOC_INCLUDE_DIRS} + ${OPENSSL_INCLUDE_DIRS} + ${LIBEV_INCLUDE_DIRS} + ${LIBNGHTTP3_INCLUDE_DIRS} + ) + + set(ossl_LIBS + ngtcp2_crypto_openssl + ngtcp2 + ${JEMALLOC_LIBRARIES} + ${OPENSSL_LIBRARIES} + ${LIBEV_LIBRARIES} + ${LIBNGHTTP3_LIBRARIES} + ) + + add_executable(client ${client_SOURCES} $<TARGET_OBJECTS:http-parser>) + add_executable(server ${server_SOURCES} $<TARGET_OBJECTS:http-parser>) + set_target_properties(client PROPERTIES + COMPILE_FLAGS "${WARNCXXFLAGS} -DENABLE_EXAMPLE_OPENSSL -DWITH_EXAMPLE_OPENSSL" + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + set_target_properties(server PROPERTIES + COMPILE_FLAGS "${WARNCXXFLAGS} -DENABLE_EXAMPLE_OPENSSL -DWITH_EXAMPLE_OPENSSL" + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + target_include_directories(client PUBLIC ${ossl_INCLUDE_DIRS}) + target_include_directories(server PUBLIC ${ossl_INCLUDE_DIRS}) + target_link_libraries(client ${ossl_LIBS}) + target_link_libraries(server ${ossl_LIBS}) + + # TODO prevent client and example servers from being installed? +endif() + +if(LIBEV_FOUND AND HAVE_GNUTLS AND LIBNGHTTP3_FOUND) + set(gtlsclient_SOURCES + client.cc + client_base.cc + debug.cc + util.cc + shared.cc + tls_client_context_gnutls.cc + tls_client_session_gnutls.cc + tls_session_base_gnutls.cc + util_gnutls.cc + ) + + set(gtlsserver_SOURCES + server.cc + server_base.cc + debug.cc + util.cc + http.cc + shared.cc + tls_server_context_gnutls.cc + tls_server_session_gnutls.cc + tls_session_base_gnutls.cc + util_gnutls.cc + ) + + set(gtls_INCLUDE_DIRS + ${CMAKE_SOURCE_DIR}/lib/includes + ${CMAKE_BINARY_DIR}/lib/includes + ${CMAKE_SOURCE_DIR}/third-party + ${CMAKE_SOURCE_DIR}/crypto/includes + + ${JEMALLOC_INCLUDE_DIRS} + ${GNUTLS_INCLUDE_DIRS} + ${LIBEV_INCLUDE_DIRS} + ${LIBNGHTTP3_INCLUDE_DIRS} + ) + + set(gtls_LIBS + ngtcp2_crypto_gnutls + ngtcp2 + ${JEMALLOC_LIBRARIES} + ${GNUTLS_LIBRARIES} + ${LIBEV_LIBRARIES} + ${LIBNGHTTP3_LIBRARIES} + ) + + add_executable(gtlsclient ${gtlsclient_SOURCES} $<TARGET_OBJECTS:http-parser>) + add_executable(gtlsserver ${gtlsserver_SOURCES} $<TARGET_OBJECTS:http-parser>) + set_target_properties(gtlsclient PROPERTIES + COMPILE_FLAGS "${WARNCXXFLAGS} -DENABLE_EXAMPLE_GNUTLS -DWITH_EXAMPLE_GNUTLS" + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + set_target_properties(gtlsserver PROPERTIES + COMPILE_FLAGS "${WARNCXXFLAGS} -DENABLE_EXAMPLE_GNUTLS -DWITH_EXAMPLE_GNUTLS" + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + target_include_directories(gtlsclient PUBLIC ${gtls_INCLUDE_DIRS}) + target_include_directories(gtlsserver PUBLIC ${gtls_INCLUDE_DIRS}) + target_link_libraries(gtlsclient ${gtls_LIBS}) + target_link_libraries(gtlsserver ${gtls_LIBS}) + + # TODO prevent gtlsclient and example gtlsservers from being installed? +endif() + +if(LIBEV_FOUND AND HAVE_BORINGSSL AND LIBNGHTTP3_FOUND) + set(bsslclient_SOURCES + client.cc + client_base.cc + debug.cc + util.cc + shared.cc + tls_client_context_boringssl.cc + tls_client_session_boringssl.cc + tls_session_base_openssl.cc + util_openssl.cc + ) + + set(bsslserver_SOURCES + server.cc + server_base.cc + debug.cc + util.cc + http.cc + shared.cc + tls_server_context_boringssl.cc + tls_server_session_boringssl.cc + tls_session_base_openssl.cc + util_openssl.cc + ) + + set(bssl_INCLUDE_DIRS + ${CMAKE_SOURCE_DIR}/lib/includes + ${CMAKE_BINARY_DIR}/lib/includes + ${CMAKE_SOURCE_DIR}/third-party + ${CMAKE_SOURCE_DIR}/crypto/includes + + ${JEMALLOC_INCLUDE_DIRS} + ${BORINGSSL_INCLUDE_DIRS} + ${LIBEV_INCLUDE_DIRS} + ${LIBNGHTTP3_INCLUDE_DIRS} + ) + + set(bssl_LIBS + ngtcp2_crypto_boringssl_static + ngtcp2 + ${JEMALLOC_LIBRARIES} + ${BORINGSSL_LIBRARIES} + ${LIBEV_LIBRARIES} + ${LIBNGHTTP3_LIBRARIES} + ) + + add_executable(bsslclient ${bsslclient_SOURCES} $<TARGET_OBJECTS:http-parser>) + add_executable(bsslserver ${bsslserver_SOURCES} $<TARGET_OBJECTS:http-parser>) + set_target_properties(bsslclient PROPERTIES + COMPILE_FLAGS "${WARNCXXFLAGS} -DENABLE_EXAMPLE_BORINGSSL -DWITH_EXAMPLE_BORINGSSL" + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + set_target_properties(bsslserver PROPERTIES + COMPILE_FLAGS "${WARNCXXFLAGS} -DENABLE_EXAMPLE_BORINGSSL -DWITH_EXAMPLE_BORINGSSL" + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + target_include_directories(bsslclient PUBLIC ${bssl_INCLUDE_DIRS}) + target_include_directories(bsslserver PUBLIC ${bssl_INCLUDE_DIRS}) + target_link_libraries(bsslclient ${bssl_LIBS}) + target_link_libraries(bsslserver ${bssl_LIBS}) + + # TODO prevent bsslclient and example bsslservers from being installed? +endif() + +if(LIBEV_FOUND AND HAVE_PICOTLS AND LIBNGHTTP3_FOUND) + set(ptlsclient_SOURCES + client.cc + client_base.cc + debug.cc + util.cc + shared.cc + tls_client_context_picotls.cc + tls_client_session_picotls.cc + tls_session_base_picotls.cc + util_openssl.cc + ) + + set(ptlsserver_SOURCES + server.cc + server_base.cc + debug.cc + util.cc + http.cc + shared.cc + tls_server_context_picotls.cc + tls_server_session_picotls.cc + tls_session_base_picotls.cc + util_openssl.cc + ) + + set(ptls_INCLUDE_DIRS + ${CMAKE_SOURCE_DIR}/lib/includes + ${CMAKE_BINARY_DIR}/lib/includes + ${CMAKE_SOURCE_DIR}/third-party + ${CMAKE_SOURCE_DIR}/crypto/includes + + ${JEMALLOC_INCLUDE_DIRS} + ${PICOTLS_INCLUDE_DIRS} + ${VANILLA_OPENSSL_INCLUDE_DIRS} + ${LIBEV_INCLUDE_DIRS} + ${LIBNGHTTP3_INCLUDE_DIRS} + ) + + set(ptls_LIBS + ngtcp2_crypto_picotls_static + ngtcp2 + ${JEMALLOC_LIBRARIES} + ${PICOTLS_LIBRARIES} + ${VANILLA_OPENSSL_LIBRARIES} + ${LIBEV_LIBRARIES} + ${LIBNGHTTP3_LIBRARIES} + ) + + add_executable(ptlsclient ${ptlsclient_SOURCES} $<TARGET_OBJECTS:http-parser>) + add_executable(ptlsserver ${ptlsserver_SOURCES} $<TARGET_OBJECTS:http-parser>) + set_target_properties(ptlsclient PROPERTIES + COMPILE_FLAGS "${WARNCXXFLAGS} -DENABLE_EXAMPLE_PICOTLS -DWITH_EXAMPLE_PICOTLS" + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + set_target_properties(ptlsserver PROPERTIES + COMPILE_FLAGS "${WARNCXXFLAGS} -DENABLE_EXAMPLE_PICOTLS -DWITH_EXAMPLE_PICOTLS" + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + target_include_directories(ptlsclient PUBLIC ${ptls_INCLUDE_DIRS}) + target_include_directories(ptlsserver PUBLIC ${ptls_INCLUDE_DIRS}) + target_link_libraries(ptlsclient ${ptls_LIBS}) + target_link_libraries(ptlsserver ${ptls_LIBS}) + + # TODO prevent ptlsclient and example ptlsservers from being installed? +endif() + +if(LIBEV_FOUND AND HAVE_WOLFSSL AND LIBNGHTTP3_FOUND) + set(wsslclient_SOURCES + client.cc + client_base.cc + debug.cc + util.cc + shared.cc + tls_client_context_wolfssl.cc + tls_client_session_wolfssl.cc + tls_session_base_wolfssl.cc + util_wolfssl.cc + ) + + set(wsslserver_SOURCES + server.cc + server_base.cc + debug.cc + util.cc + http.cc + shared.cc + tls_server_context_wolfssl.cc + tls_server_session_wolfssl.cc + tls_session_base_wolfssl.cc + util_wolfssl.cc + ) + + set(wolfssl_INCLUDE_DIRS + ${CMAKE_SOURCE_DIR}/lib/includes + ${CMAKE_BINARY_DIR}/lib/includes + ${CMAKE_SOURCE_DIR}/third-party + ${CMAKE_SOURCE_DIR}/crypto/includes + + ${JEMALLOC_INCLUDE_DIRS} + ${WOLFSSL_INCLUDE_DIRS} + ${LIBEV_INCLUDE_DIRS} + ${LIBNGHTTP3_INCLUDE_DIRS} + ) + + set(wolfssl_LIBS + ngtcp2_crypto_wolfssl_static + ngtcp2 + ${JEMALLOC_LIBRARIES} + ${WOLFSSL_LIBRARIES} + ${LIBEV_LIBRARIES} + ${LIBNGHTTP3_LIBRARIES} + ) + + add_executable(wsslclient ${wsslclient_SOURCES} $<TARGET_OBJECTS:http-parser>) + add_executable(wsslserver ${wsslserver_SOURCES} $<TARGET_OBJECTS:http-parser>) + set_target_properties(wsslclient PROPERTIES + COMPILE_FLAGS "${WARNCXXFLAGS} -DENABLE_EXAMPLE_WOLFSSL -DWITH_EXAMPLE_WOLFSSL" + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + set_target_properties(wsslserver PROPERTIES + COMPILE_FLAGS "${WARNCXXFLAGS} -DENABLE_EXAMPLE_WOLFSSL -DWITH_EXAMPLE_WOLFSSL" + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + target_include_directories(wsslclient PUBLIC ${wolfssl_INCLUDE_DIRS}) + target_include_directories(wsslserver PUBLIC ${wolfssl_INCLUDE_DIRS}) + target_link_libraries(wsslclient ${wolfssl_LIBS}) + target_link_libraries(wsslserver ${wolfssl_LIBS}) + + # TODO prevent wsslclient and example wsslserver from being installed? +endif() diff --git a/examples/Makefile.am b/examples/Makefile.am new file mode 100644 index 0000000..66bfbe5 --- /dev/null +++ b/examples/Makefile.am @@ -0,0 +1,226 @@ +# ngtcp2 + +# Copyright (c) 2017 ngtcp2 contributors + +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +EXTRA_DIST = CMakeLists.txt + +AM_CFLAGS = $(WARNCFLAGS) $(DEBUGCFLAGS) +AM_CXXFLAGS = $(WARNCXXFLAGS) $(DEBUGCFLAGS) +AM_CPPFLAGS = \ + -I$(top_srcdir)/lib/includes \ + -I$(top_builddir)/lib/includes \ + -I$(top_srcdir)/crypto/includes \ + -I$(top_srcdir)/third-party \ + @LIBEV_CFLAGS@ \ + @LIBNGHTTP3_CFLAGS@ \ + @DEFS@ \ + @EXTRA_DEFS@ +AM_LDFLAGS = -no-install \ + @LIBTOOL_LDFLAGS@ +LDADD = $(top_builddir)/lib/libngtcp2.la \ + $(top_builddir)/third-party/libhttp-parser.la \ + @LIBEV_LIBS@ \ + @LIBNGHTTP3_LIBS@ + +SERVER_SRCS = \ + server_base.cc server_base.h \ + tls_server_context.h \ + tls_server_session.h \ + template.h \ + debug.cc debug.h \ + util.cc util.h \ + shared.cc shared.h \ + http.cc http.h \ + network.h + +CLIENT_SRCS = \ + client_base.cc client_base.h \ + tls_client_context.h \ + tls_client_session.h \ + template.h \ + debug.cc debug.h \ + util.cc util.h \ + shared.cc shared.h \ + network.h + +noinst_PROGRAMS = + +if ENABLE_EXAMPLE_OPENSSL +noinst_PROGRAMS += client server h09client h09server simpleclient + +simpleclient_CPPFLAGS = ${AM_CPPFLAGS} \ + @JEMALLOC_CFLAGS@ @OPENSSL_CFLAGS@ -DWITH_EXAMPLE_OPENSSL +simpleclient_LDADD = ${LDADD} \ + $(top_builddir)/crypto/openssl/libngtcp2_crypto_openssl.la \ + @OPENSSL_LIBS@ \ + @JEMALLOC_LIBS@ +simpleclient_SOURCES = simpleclient.c + +client_CPPFLAGS = ${AM_CPPFLAGS} \ + @JEMALLOC_CFLAGS@ @OPENSSL_CFLAGS@ -DWITH_EXAMPLE_OPENSSL +client_LDADD = ${LDADD} \ + $(top_builddir)/crypto/openssl/libngtcp2_crypto_openssl.la \ + @OPENSSL_LIBS@ \ + @JEMALLOC_LIBS@ +client_SOURCES = client.cc client.h ${CLIENT_SRCS} \ + tls_client_context_openssl.cc tls_client_context_openssl.h \ + tls_client_session_openssl.cc tls_client_session_openssl.h \ + tls_session_base_openssl.cc tls_session_base_openssl.h \ + util_openssl.cc + +server_CPPFLAGS = ${client_CPPFLAGS} +server_LDADD = ${client_LDADD} +server_SOURCES = server.cc server.h ${SERVER_SRCS} \ + tls_server_context_openssl.cc tls_server_context_openssl.h \ + tls_server_session_openssl.cc tls_server_session_openssl.h \ + tls_session_base_openssl.cc tls_session_base_openssl.h \ + util_openssl.cc + +h09client_CPPFLAGS = ${client_CPPFLAGS} +h09client_LDADD = ${client_LDADD} +h09client_SOURCES = h09client.cc h09client.h ${CLIENT_SRCS} \ + tls_client_context_openssl.cc tls_client_context_openssl.h \ + tls_client_session_openssl.cc tls_client_session_openssl.h \ + tls_session_base_openssl.cc tls_session_base_openssl.h \ + util_openssl.cc + +h09server_CPPFLAGS = ${client_CPPFLAGS} +h09server_LDADD = ${client_LDADD} +h09server_SOURCES = h09server.cc h09server.h ${SERVER_SRCS} \ + tls_server_context_openssl.cc tls_server_context_openssl.h \ + tls_server_session_openssl.cc tls_server_session_openssl.h \ + tls_session_base_openssl.cc tls_session_base_openssl.h \ + util_openssl.cc +endif # ENABLE_EXAMPLE_OPENSSL + +if ENABLE_EXAMPLE_GNUTLS +noinst_PROGRAMS += gtlsclient gtlsserver gtlssimpleclient + +gtlssimpleclient_CPPFLAGS = ${AM_CPPFLAGS} \ + @JEMALLOC_CFLAGS@ @GNUTLS_CFLAGS@ -DWITH_EXAMPLE_OPENSSL +gtlssimpleclient_LDADD = ${LDADD} \ + $(top_builddir)/crypto/gnutls/libngtcp2_crypto_gnutls.la \ + @GNUTLS_LIBS@ \ + @JEMALLOC_LIBS@ +gtlssimpleclient_SOURCES = gtlssimpleclient.c + +gtlsclient_CPPFLAGS = ${AM_CPPFLAGS} \ + @JEMALLOC_CFLAGS@ @GNUTLS_CFLAGS@ -DWITH_EXAMPLE_GNUTLS +gtlsclient_LDADD = ${LDADD} \ + $(top_builddir)/crypto/gnutls/libngtcp2_crypto_gnutls.la \ + @GNUTLS_LIBS@ \ + @JEMALLOC_LIBS@ +gtlsclient_SOURCES = client.cc client.h ${CLIENT_SRCS} \ + tls_client_context_gnutls.cc tls_client_context_gnutls.h \ + tls_client_session_gnutls.cc tls_client_session_gnutls.h \ + tls_session_base_gnutls.cc tls_session_base_gnutls.h \ + util_gnutls.cc + +gtlsserver_CPPFLAGS = ${gtlsclient_CPPFLAGS} +gtlsserver_LDADD = ${gtlsclient_LDADD} \ + $(top_builddir)/crypto/gnutls/libngtcp2_crypto_gnutls.la \ + @GNUTLS_LIBS@ +gtlsserver_SOURCES = server.cc server.h ${SERVER_SRCS} \ + tls_server_context_gnutls.cc tls_server_context_gnutls.h \ + tls_server_session_gnutls.cc tls_server_session_gnutls.h \ + tls_session_base_gnutls.cc tls_session_base_gnutls.h \ + util_gnutls.cc +endif # ENABLE_EXAMPLE_GNUTLS + +if ENABLE_EXAMPLE_BORINGSSL +noinst_PROGRAMS += bsslclient bsslserver + +bsslclient_CPPFLAGS = ${AM_CPPFLAGS} @BORINGSSL_CFLAGS@ -DWITH_EXAMPLE_BORINGSSL +bsslclient_LDADD = ${LDADD} \ + $(top_builddir)/crypto/boringssl/libngtcp2_crypto_boringssl.a \ + @BORINGSSL_LIBS@ \ + @JEMALLOC_LIBS@ +bsslclient_SOURCES = client.cc client.h ${CLIENT_SRCS} \ + tls_client_context_boringssl.cc tls_client_context_boringssl.h \ + tls_client_session_boringssl.cc tls_client_session_boringssl.h \ + tls_session_base_openssl.cc tls_session_base_openssl.h \ + util_openssl.cc + +bsslserver_CPPFLAGS = ${bsslclient_CPPFLAGS} +bsslserver_LDADD = ${bsslclient_LDADD} +bsslserver_SOURCES = server.cc server.h ${SERVER_SRCS} \ + tls_server_context_boringssl.cc tls_server_context_boringssl.h \ + tls_server_session_boringssl.cc tls_server_session_boringssl.h \ + tls_session_base_openssl.cc tls_session_base_openssl.h \ + util_openssl.cc +endif # ENABLE_EXAMPLE_BORINGSSL + +if ENABLE_EXAMPLE_PICOTLS +noinst_PROGRAMS += ptlsclient ptlsserver + +ptlsclient_CPPFLAGS = ${AM_CPPFLAGS} @PICOTLS_CFLAGS@ @VANILLA_OPENSSL_CFLAGS@ \ + -DWITH_EXAMPLE_PICOTLS +ptlsclient_LDADD = ${LDADD} \ + $(top_builddir)/crypto/picotls/libngtcp2_crypto_picotls.a \ + @PICOTLS_LIBS@ @VANILLA_OPENSSL_LIBS@ \ + @JEMALLOC_LIBS@ +ptlsclient_SOURCES = client.cc client.h ${CLIENT_SRCS} \ + tls_client_context_picotls.cc tls_client_context_picotls.h \ + tls_client_session_picotls.cc tls_client_session_picotls.h \ + tls_session_base_picotls.cc tls_session_base_picotls.h \ + util_openssl.cc + +ptlsserver_CPPFLAGS = ${ptlsclient_CPPFLAGS} +ptlsserver_LDADD = ${ptlsclient_LDADD} +ptlsserver_SOURCES = server.cc server.h ${SERVER_SRCS} \ + tls_server_context_picotls.cc tls_server_context_picotls.h \ + tls_server_session_picotls.cc tls_server_session_picotls.h \ + tls_session_base_picotls.cc tls_session_base_picotls.h \ + util_openssl.cc +endif # ENABLE_EXAMPLE_PICOTLS + +if ENABLE_EXAMPLE_WOLFSSL +noinst_PROGRAMS += wsslclient wsslserver + +wsslclient_CPPFLAGS = ${AM_CPPFLAGS} @WOLFSSL_CFLAGS@ -DWITH_EXAMPLE_WOLFSSL +wsslclient_LDADD = ${LDADD} \ + $(top_builddir)/crypto/wolfssl/libngtcp2_crypto_wolfssl.la \ + @WOLFSSL_LIBS@ \ + @JEMALLOC_LIBS@ +wsslclient_SOURCES = client.cc client.h ${CLIENT_SRCS} \ + tls_client_context_wolfssl.cc tls_client_context_wolfssl.h \ + tls_client_session_wolfssl.cc tls_client_session_wolfssl.h \ + tls_session_base_wolfssl.cc tls_session_base_wolfssl.h \ + util_wolfssl.cc + +wsslserver_CPPFLAGS = ${wsslclient_CPPFLAGS} +wsslserver_LDADD = ${wsslclient_LDADD} +wsslserver_SOURCES = server.cc server.h ${SERVER_SRCS} \ + tls_server_context_wolfssl.cc tls_server_context_wolfssl.h \ + tls_server_session_wolfssl.cc tls_server_session_wolfssl.h \ + tls_session_base_wolfssl.cc tls_session_base_wolfssl.h \ + util_wolfssl.cc +endif # ENABLE_EXAMPLE_WOLFSSL + +if HAVE_CUNIT +check_PROGRAMS = examplestest +examplestest_SOURCES = examplestest.cc \ + util_test.cc util_test.h util.cc util.h +examplestest_CPPFLAGS = ${AM_CPPFLAGS} @JEMALLOC_CFLAGS@ +examplestest_LDADD = ${LDADD} @CUNIT_LIBS@ @JEMALLOC_LIBS@ + +TESTS = examplestest +endif # HAVE_CUNIT diff --git a/examples/client.cc b/examples/client.cc new file mode 100644 index 0000000..2a2e2b3 --- /dev/null +++ b/examples/client.cc @@ -0,0 +1,3052 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include <cstdlib> +#include <cassert> +#include <cerrno> +#include <iostream> +#include <algorithm> +#include <memory> +#include <fstream> +#include <iomanip> + +#include <unistd.h> +#include <getopt.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <sys/socket.h> +#include <netdb.h> +#include <sys/mman.h> + +#include <http-parser/http_parser.h> + +#include "client.h" +#include "network.h" +#include "debug.h" +#include "util.h" +#include "shared.h" + +using namespace ngtcp2; +using namespace std::literals; + +namespace { +auto randgen = util::make_mt19937(); +} // namespace + +namespace { +constexpr size_t max_preferred_versionslen = 4; +} // namespace + +Config config{}; + +Stream::Stream(const Request &req, int64_t stream_id) + : req(req), stream_id(stream_id), fd(-1) {} + +Stream::~Stream() { + if (fd != -1) { + close(fd); + } +} + +int Stream::open_file(const std::string_view &path) { + assert(fd == -1); + + std::string_view filename; + + auto it = std::find(std::rbegin(path), std::rend(path), '/').base(); + if (it == std::end(path)) { + filename = "index.html"sv; + } else { + filename = std::string_view{it, static_cast<size_t>(std::end(path) - it)}; + if (filename == ".."sv || filename == "."sv) { + std::cerr << "Invalid file name: " << filename << std::endl; + return -1; + } + } + + auto fname = std::string{config.download}; + fname += '/'; + fname += filename; + + fd = open(fname.c_str(), O_WRONLY | O_CREAT | O_TRUNC, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (fd == -1) { + std::cerr << "open: Could not open file " << fname << ": " + << strerror(errno) << std::endl; + return -1; + } + + return 0; +} + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + auto c = static_cast<Client *>(w->data); + + c->on_write(); +} +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + auto ep = static_cast<Endpoint *>(w->data); + auto c = ep->client; + + if (c->on_read(*ep) != 0) { + return; + } + + c->on_write(); +} +} // namespace + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + int rv; + auto c = static_cast<Client *>(w->data); + + rv = c->handle_expiry(); + if (rv != 0) { + return; + } + + c->on_write(); +} +} // namespace + +namespace { +void change_local_addrcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto c = static_cast<Client *>(w->data); + + c->change_local_addr(); +} +} // namespace + +namespace { +void key_updatecb(struct ev_loop *loop, ev_timer *w, int revents) { + auto c = static_cast<Client *>(w->data); + + if (c->initiate_key_update() != 0) { + c->disconnect(); + } +} +} // namespace + +namespace { +void delay_streamcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto c = static_cast<Client *>(w->data); + + ev_timer_stop(loop, w); + c->on_extend_max_streams(); + c->on_write(); +} +} // namespace + +namespace { +void siginthandler(struct ev_loop *loop, ev_signal *w, int revents) { + ev_break(loop, EVBREAK_ALL); +} +} // namespace + +Client::Client(struct ev_loop *loop, uint32_t client_chosen_version, + uint32_t original_version) + : remote_addr_{}, + loop_(loop), + httpconn_(nullptr), + addr_(nullptr), + port_(nullptr), + nstreams_done_(0), + nstreams_closed_(0), + nkey_update_(0), + client_chosen_version_(client_chosen_version), + original_version_(original_version), + early_data_(false), + should_exit_(false), + should_exit_on_handshake_confirmed_(false), + handshake_confirmed_(false), + tx_{} { + ev_io_init(&wev_, writecb, 0, EV_WRITE); + wev_.data = this; + ev_timer_init(&timer_, timeoutcb, 0., 0.); + timer_.data = this; + ev_timer_init(&change_local_addr_timer_, change_local_addrcb, + static_cast<double>(config.change_local_addr) / NGTCP2_SECONDS, + 0.); + change_local_addr_timer_.data = this; + ev_timer_init(&key_update_timer_, key_updatecb, + static_cast<double>(config.key_update) / NGTCP2_SECONDS, 0.); + key_update_timer_.data = this; + ev_timer_init(&delay_stream_timer_, delay_streamcb, + static_cast<double>(config.delay_stream) / NGTCP2_SECONDS, 0.); + delay_stream_timer_.data = this; + ev_signal_init(&sigintev_, siginthandler, SIGINT); +} + +Client::~Client() { + disconnect(); + + if (httpconn_) { + nghttp3_conn_del(httpconn_); + httpconn_ = nullptr; + } +} + +void Client::disconnect() { + tx_.send_blocked = false; + + handle_error(); + + config.tx_loss_prob = 0; + + ev_timer_stop(loop_, &delay_stream_timer_); + ev_timer_stop(loop_, &key_update_timer_); + ev_timer_stop(loop_, &change_local_addr_timer_); + ev_timer_stop(loop_, &timer_); + + ev_io_stop(loop_, &wev_); + + for (auto &ep : endpoints_) { + ev_io_stop(loop_, &ep.rev); + close(ep.fd); + } + + endpoints_.clear(); + + ev_signal_stop(loop_, &sigintev_); +} + +namespace { +int recv_crypto_data(ngtcp2_conn *conn, ngtcp2_crypto_level crypto_level, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_crypto_data(crypto_level, data, datalen); + } + + return ngtcp2_crypto_recv_crypto_data_cb(conn, crypto_level, offset, data, + datalen, user_data); +} +} // namespace + +namespace { +int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data, void *stream_user_data) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_stream_data(stream_id, data, datalen); + } + + auto c = static_cast<Client *>(user_data); + + if (c->recv_stream_data(flags, stream_id, data, datalen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int acked_stream_data_offset(ngtcp2_conn *conn, int64_t stream_id, + uint64_t offset, uint64_t datalen, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + if (c->acked_stream_data_offset(stream_id, datalen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +namespace { +int handshake_completed(ngtcp2_conn *conn, void *user_data) { + auto c = static_cast<Client *>(user_data); + + if (!config.quiet) { + debug::handshake_completed(conn, user_data); + } + + if (c->handshake_completed() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Client::handshake_completed() { + if (early_data_ && !tls_session_.get_early_data_accepted()) { + if (!config.quiet) { + std::cerr << "Early data was rejected by server" << std::endl; + } + + // Some TLS backends only report early data rejection after + // handshake completion (e.g., OpenSSL). For TLS backends which + // report it early (e.g., BoringSSL and PicoTLS), the following + // functions are noop. + if (auto rv = ngtcp2_conn_early_data_rejected(conn_); rv != 0) { + std::cerr << "ngtcp2_conn_early_data_rejected: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + if (setup_httpconn() != 0) { + return -1; + } + } + + if (!config.quiet) { + std::cerr << "Negotiated cipher suite is " << tls_session_.get_cipher_name() + << std::endl; + std::cerr << "Negotiated ALPN is " << tls_session_.get_selected_alpn() + << std::endl; + } + + if (config.tp_file) { + auto params = ngtcp2_conn_get_remote_transport_params(conn_); + + if (write_transport_params(config.tp_file, params) != 0) { + std::cerr << "Could not write transport parameters in " << config.tp_file + << std::endl; + } + } + + return 0; +} + +namespace { +int handshake_confirmed(ngtcp2_conn *conn, void *user_data) { + auto c = static_cast<Client *>(user_data); + + if (!config.quiet) { + debug::handshake_confirmed(conn, user_data); + } + + if (c->handshake_confirmed() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Client::handshake_confirmed() { + handshake_confirmed_ = true; + + if (config.change_local_addr) { + start_change_local_addr_timer(); + } + if (config.key_update) { + start_key_update_timer(); + } + if (config.delay_stream) { + start_delay_stream_timer(); + } + + if (should_exit_on_handshake_confirmed_) { + should_exit_ = true; + } + + return 0; +} + +namespace { +int recv_version_negotiation(ngtcp2_conn *conn, const ngtcp2_pkt_hd *hd, + const uint32_t *sv, size_t nsv, void *user_data) { + auto c = static_cast<Client *>(user_data); + + c->recv_version_negotiation(sv, nsv); + + return 0; +} +} // namespace + +void Client::recv_version_negotiation(const uint32_t *sv, size_t nsv) { + offered_versions_.resize(nsv); + std::copy_n(sv, nsv, std::begin(offered_versions_)); +} + +namespace { +int stream_close(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + + if (!(flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET)) { + app_error_code = NGHTTP3_H3_NO_ERROR; + } + + if (c->on_stream_close(stream_id, app_error_code) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int stream_reset(ngtcp2_conn *conn, int64_t stream_id, uint64_t final_size, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + + if (c->on_stream_reset(stream_id) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int stream_stop_sending(ngtcp2_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + + if (c->on_stream_stop_sending(stream_id) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int extend_max_streams_bidi(ngtcp2_conn *conn, uint64_t max_streams, + void *user_data) { + auto c = static_cast<Client *>(user_data); + + if (c->on_extend_max_streams() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +void rand(uint8_t *dest, size_t destlen, const ngtcp2_rand_ctx *rand_ctx) { + auto dis = std::uniform_int_distribution<uint8_t>(0, 255); + std::generate(dest, dest + destlen, [&dis]() { return dis(randgen); }); +} +} // namespace + +namespace { +int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, uint8_t *token, + size_t cidlen, void *user_data) { + if (util::generate_secure_random(cid->data, cidlen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + cid->datalen = cidlen; + if (ngtcp2_crypto_generate_stateless_reset_token( + token, config.static_secret.data(), config.static_secret.size(), + cid) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int do_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, + const ngtcp2_crypto_cipher_ctx *hp_ctx, const uint8_t *sample) { + if (ngtcp2_crypto_hp_mask(dest, hp, hp_ctx, sample) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + if (!config.quiet && config.show_secret) { + debug::print_hp_mask(dest, NGTCP2_HP_MASKLEN, sample, NGTCP2_HP_SAMPLELEN); + } + + return 0; +} +} // namespace + +namespace { +int update_key(ngtcp2_conn *conn, uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen, + void *user_data) { + auto c = static_cast<Client *>(user_data); + + if (c->update_key(rx_secret, tx_secret, rx_aead_ctx, rx_iv, tx_aead_ctx, + tx_iv, current_rx_secret, current_tx_secret, + secretlen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int path_validation(ngtcp2_conn *conn, uint32_t flags, const ngtcp2_path *path, + ngtcp2_path_validation_result res, void *user_data) { + if (!config.quiet) { + debug::path_validation(path, res); + } + + if (flags & NGTCP2_PATH_VALIDATION_FLAG_PREFERRED_ADDR) { + auto c = static_cast<Client *>(user_data); + + c->set_remote_addr(path->remote); + } + + return 0; +} +} // namespace + +void Client::set_remote_addr(const ngtcp2_addr &remote_addr) { + memcpy(&remote_addr_.su, remote_addr.addr, remote_addr.addrlen); + remote_addr_.len = remote_addr.addrlen; +} + +namespace { +int select_preferred_address(ngtcp2_conn *conn, ngtcp2_path *dest, + const ngtcp2_preferred_addr *paddr, + void *user_data) { + auto c = static_cast<Client *>(user_data); + Address remote_addr; + + if (config.no_preferred_addr) { + return 0; + } + + if (c->select_preferred_address(remote_addr, paddr) != 0) { + return 0; + } + + auto ep = c->endpoint_for(remote_addr); + if (!ep) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + ngtcp2_addr_copy_byte(&dest->local, &(*ep)->addr.su.sa, (*ep)->addr.len); + ngtcp2_addr_copy_byte(&dest->remote, &remote_addr.su.sa, remote_addr.len); + dest->user_data = *ep; + + return 0; +} +} // namespace + +namespace { +int extend_max_stream_data(ngtcp2_conn *conn, int64_t stream_id, + uint64_t max_data, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + if (c->extend_max_stream_data(stream_id, max_data) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Client::extend_max_stream_data(int64_t stream_id, uint64_t max_data) { + if (auto rv = nghttp3_conn_unblock_stream(httpconn_, stream_id); rv != 0) { + std::cerr << "nghttp3_conn_unblock_stream: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + return 0; +} + +namespace { +int recv_new_token(ngtcp2_conn *conn, const ngtcp2_vec *token, + void *user_data) { + if (config.token_file.empty()) { + return 0; + } + + util::write_token(config.token_file, token->base, token->len); + + return 0; +} +} // namespace + +namespace { +int recv_rx_key(ngtcp2_conn *conn, ngtcp2_crypto_level level, void *user_data) { + if (level != NGTCP2_CRYPTO_LEVEL_APPLICATION) { + return 0; + } + + auto c = static_cast<Client *>(user_data); + if ((!c->get_early_data() || ngtcp2_conn_get_early_data_rejected(conn)) && + c->setup_httpconn() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int early_data_rejected(ngtcp2_conn *conn, void *user_data) { + auto c = static_cast<Client *>(user_data); + + c->early_data_rejected(); + + return 0; +} +} // namespace + +void Client::early_data_rejected() { + nghttp3_conn_del(httpconn_); + httpconn_ = nullptr; + + nstreams_done_ = 0; + streams_.clear(); +} + +int Client::init(int fd, const Address &local_addr, const Address &remote_addr, + const char *addr, const char *port, + TLSClientContext &tls_ctx) { + endpoints_.reserve(4); + + endpoints_.emplace_back(); + auto &ep = endpoints_.back(); + ep.addr = local_addr; + ep.client = this; + ep.fd = fd; + ev_io_init(&ep.rev, readcb, fd, EV_READ); + ep.rev.data = &ep; + + remote_addr_ = remote_addr; + addr_ = addr; + port_ = port; + + auto callbacks = ngtcp2_callbacks{ + ngtcp2_crypto_client_initial_cb, + nullptr, // recv_client_initial + ::recv_crypto_data, + ::handshake_completed, + ::recv_version_negotiation, + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + do_hp_mask, + ::recv_stream_data, + ::acked_stream_data_offset, + nullptr, // stream_open + stream_close, + nullptr, // recv_stateless_reset + ngtcp2_crypto_recv_retry_cb, + extend_max_streams_bidi, + nullptr, // extend_max_streams_uni + rand, + get_new_connection_id, + nullptr, // remove_connection_id + ::update_key, + path_validation, + ::select_preferred_address, + stream_reset, + nullptr, // extend_max_remote_streams_bidi, + nullptr, // extend_max_remote_streams_uni, + ::extend_max_stream_data, + nullptr, // dcid_status + ::handshake_confirmed, + ::recv_new_token, + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + nullptr, // recv_datagram + nullptr, // ack_datagram + nullptr, // lost_datagram + ngtcp2_crypto_get_path_challenge_data_cb, + stream_stop_sending, + ngtcp2_crypto_version_negotiation_cb, + ::recv_rx_key, + nullptr, // recv_tx_key + ::early_data_rejected, + }; + + ngtcp2_cid scid, dcid; + if (config.scid_present) { + scid = config.scid; + } else { + scid.datalen = 17; + if (util::generate_secure_random(scid.data, scid.datalen) != 0) { + std::cerr << "Could not generate source connection ID" << std::endl; + return -1; + } + } + if (config.dcid.datalen == 0) { + dcid.datalen = 18; + if (util::generate_secure_random(dcid.data, dcid.datalen) != 0) { + std::cerr << "Could not generate destination connection ID" << std::endl; + return -1; + } + } else { + dcid = config.dcid; + } + + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + settings.log_printf = config.quiet ? nullptr : debug::log_printf; + if (!config.qlog_file.empty() || !config.qlog_dir.empty()) { + std::string path; + if (!config.qlog_file.empty()) { + path = config.qlog_file; + } else { + path = std::string{config.qlog_dir}; + path += '/'; + path += util::format_hex(scid.data, scid.datalen); + path += ".sqlog"; + } + qlog_ = fopen(path.c_str(), "w"); + if (qlog_ == nullptr) { + std::cerr << "Could not open qlog file " << std::quoted(path) << ": " + << strerror(errno) << std::endl; + return -1; + } + settings.qlog.write = qlog_write_cb; + } + + settings.cc_algo = config.cc_algo; + settings.initial_ts = util::timestamp(loop_); + settings.initial_rtt = config.initial_rtt; + settings.max_window = config.max_window; + settings.max_stream_window = config.max_stream_window; + if (config.max_udp_payload_size) { + settings.max_tx_udp_payload_size = config.max_udp_payload_size; + settings.no_tx_udp_payload_size_shaping = 1; + } + settings.handshake_timeout = config.handshake_timeout; + settings.no_pmtud = config.no_pmtud; + settings.ack_thresh = config.ack_thresh; + + std::string token; + + if (!config.token_file.empty()) { + std::cerr << "Reading token file " << config.token_file << std::endl; + + auto t = util::read_token(config.token_file); + if (t) { + token = std::move(*t); + settings.token.base = reinterpret_cast<uint8_t *>(token.data()); + settings.token.len = token.size(); + } + } + + if (!config.other_versions.empty()) { + settings.other_versions = config.other_versions.data(); + settings.other_versionslen = config.other_versions.size(); + } + + if (!config.preferred_versions.empty()) { + settings.preferred_versions = config.preferred_versions.data(); + settings.preferred_versionslen = config.preferred_versions.size(); + } + + settings.original_version = original_version_; + + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + params.initial_max_stream_data_bidi_local = config.max_stream_data_bidi_local; + params.initial_max_stream_data_bidi_remote = + config.max_stream_data_bidi_remote; + params.initial_max_stream_data_uni = config.max_stream_data_uni; + params.initial_max_data = config.max_data; + params.initial_max_streams_bidi = config.max_streams_bidi; + params.initial_max_streams_uni = config.max_streams_uni; + params.max_idle_timeout = config.timeout; + params.active_connection_id_limit = 7; + + auto path = ngtcp2_path{ + { + const_cast<sockaddr *>(&ep.addr.su.sa), + ep.addr.len, + }, + { + const_cast<sockaddr *>(&remote_addr.su.sa), + remote_addr.len, + }, + &ep, + }; + auto rv = ngtcp2_conn_client_new(&conn_, &dcid, &scid, &path, + client_chosen_version_, &callbacks, + &settings, ¶ms, nullptr, this); + + if (rv != 0) { + std::cerr << "ngtcp2_conn_client_new: " << ngtcp2_strerror(rv) << std::endl; + return -1; + } + + if (tls_session_.init(early_data_, tls_ctx, addr_, this, + client_chosen_version_, AppProtocol::H3) != 0) { + return -1; + } + + ngtcp2_conn_set_tls_native_handle(conn_, tls_session_.get_native_handle()); + + if (early_data_ && config.tp_file) { + ngtcp2_transport_params params; + if (read_transport_params(config.tp_file, ¶ms) != 0) { + std::cerr << "Could not read transport parameters from " << config.tp_file + << std::endl; + early_data_ = false; + } else { + ngtcp2_conn_set_early_remote_transport_params(conn_, ¶ms); + if (make_stream_early() != 0) { + return -1; + } + } + } + + ev_io_start(loop_, &ep.rev); + + ev_signal_start(loop_, &sigintev_); + + return 0; +} + +int Client::feed_data(const Endpoint &ep, const sockaddr *sa, socklen_t salen, + const ngtcp2_pkt_info *pi, uint8_t *data, + size_t datalen) { + auto path = ngtcp2_path{ + { + const_cast<sockaddr *>(&ep.addr.su.sa), + ep.addr.len, + }, + { + const_cast<sockaddr *>(sa), + salen, + }, + const_cast<Endpoint *>(&ep), + }; + if (auto rv = ngtcp2_conn_read_pkt(conn_, &path, pi, data, datalen, + util::timestamp(loop_)); + rv != 0) { + std::cerr << "ngtcp2_conn_read_pkt: " << ngtcp2_strerror(rv) << std::endl; + if (!last_error_.error_code) { + if (rv == NGTCP2_ERR_CRYPTO) { + ngtcp2_connection_close_error_set_transport_error_tls_alert( + &last_error_, ngtcp2_conn_get_tls_alert(conn_), nullptr, 0); + } else { + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, rv, nullptr, 0); + } + } + disconnect(); + return -1; + } + return 0; +} + +int Client::on_read(const Endpoint &ep) { + std::array<uint8_t, 64_k> buf; + sockaddr_union su; + size_t pktcnt = 0; + ngtcp2_pkt_info pi; + + iovec msg_iov; + msg_iov.iov_base = buf.data(); + msg_iov.iov_len = buf.size(); + + msghdr msg{}; + msg.msg_name = &su; + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + + uint8_t msg_ctrl[CMSG_SPACE(sizeof(uint8_t))]; + msg.msg_control = msg_ctrl; + + for (;;) { + msg.msg_namelen = sizeof(su); + msg.msg_controllen = sizeof(msg_ctrl); + + auto nread = recvmsg(ep.fd, &msg, 0); + + if (nread == -1) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + std::cerr << "recvmsg: " << strerror(errno) << std::endl; + } + break; + } + + pi.ecn = msghdr_get_ecn(&msg, su.storage.ss_family); + + if (!config.quiet) { + std::cerr << "Received packet: local=" + << util::straddr(&ep.addr.su.sa, ep.addr.len) + << " remote=" << util::straddr(&su.sa, msg.msg_namelen) + << " ecn=0x" << std::hex << pi.ecn << std::dec << " " << nread + << " bytes" << std::endl; + } + + if (debug::packet_lost(config.rx_loss_prob)) { + if (!config.quiet) { + std::cerr << "** Simulated incoming packet loss **" << std::endl; + } + break; + } + + if (feed_data(ep, &su.sa, msg.msg_namelen, &pi, buf.data(), nread) != 0) { + return -1; + } + + if (++pktcnt >= 10) { + break; + } + } + + if (should_exit_) { + ngtcp2_connection_close_error_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(0), nullptr, 0); + disconnect(); + return -1; + } + + update_timer(); + + return 0; +} + +int Client::handle_expiry() { + auto now = util::timestamp(loop_); + if (auto rv = ngtcp2_conn_handle_expiry(conn_, now); rv != 0) { + std::cerr << "ngtcp2_conn_handle_expiry: " << ngtcp2_strerror(rv) + << std::endl; + ngtcp2_connection_close_error_set_transport_error_liberr(&last_error_, rv, + nullptr, 0); + disconnect(); + return -1; + } + + return 0; +} + +int Client::on_write() { + if (tx_.send_blocked) { + if (auto rv = send_blocked_packet(); rv != 0) { + return rv; + } + + if (tx_.send_blocked) { + return 0; + } + } + + if (auto rv = write_streams(); rv != 0) { + return rv; + } + + if (should_exit_) { + ngtcp2_connection_close_error_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(0), nullptr, 0); + disconnect(); + return -1; + } + + update_timer(); + return 0; +} + +int Client::write_streams() { + std::array<nghttp3_vec, 16> vec; + ngtcp2_path_storage ps; + size_t pktcnt = 0; + auto max_udp_payload_size = ngtcp2_conn_get_max_tx_udp_payload_size(conn_); + auto max_pktcnt = ngtcp2_conn_get_send_quantum(conn_) / max_udp_payload_size; + auto ts = util::timestamp(loop_); + + ngtcp2_path_storage_zero(&ps); + + for (;;) { + int64_t stream_id = -1; + int fin = 0; + nghttp3_ssize sveccnt = 0; + + if (httpconn_ && ngtcp2_conn_get_max_data_left(conn_)) { + sveccnt = nghttp3_conn_writev_stream(httpconn_, &stream_id, &fin, + vec.data(), vec.size()); + if (sveccnt < 0) { + std::cerr << "nghttp3_conn_writev_stream: " << nghttp3_strerror(sveccnt) + << std::endl; + ngtcp2_connection_close_error_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(sveccnt), + nullptr, 0); + disconnect(); + return -1; + } + } + + ngtcp2_ssize ndatalen; + auto v = vec.data(); + auto vcnt = static_cast<size_t>(sveccnt); + + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + if (fin) { + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + ngtcp2_pkt_info pi; + + auto nwrite = ngtcp2_conn_writev_stream( + conn_, &ps.path, &pi, tx_.data.data(), max_udp_payload_size, &ndatalen, + flags, stream_id, reinterpret_cast<const ngtcp2_vec *>(v), vcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + assert(ndatalen == -1); + nghttp3_conn_block_stream(httpconn_, stream_id); + continue; + case NGTCP2_ERR_STREAM_SHUT_WR: + assert(ndatalen == -1); + nghttp3_conn_shutdown_stream_write(httpconn_, stream_id); + continue; + case NGTCP2_ERR_WRITE_MORE: + assert(ndatalen >= 0); + if (auto rv = + nghttp3_conn_add_write_offset(httpconn_, stream_id, ndatalen); + rv != 0) { + std::cerr << "nghttp3_conn_add_write_offset: " << nghttp3_strerror(rv) + << std::endl; + ngtcp2_connection_close_error_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, + 0); + disconnect(); + return -1; + } + continue; + } + + assert(ndatalen == -1); + + std::cerr << "ngtcp2_conn_write_stream: " << ngtcp2_strerror(nwrite) + << std::endl; + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, nwrite, nullptr, 0); + disconnect(); + return -1; + } else if (ndatalen >= 0) { + if (auto rv = + nghttp3_conn_add_write_offset(httpconn_, stream_id, ndatalen); + rv != 0) { + std::cerr << "nghttp3_conn_add_write_offset: " << nghttp3_strerror(rv) + << std::endl; + ngtcp2_connection_close_error_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, + 0); + disconnect(); + return -1; + } + } + + if (nwrite == 0) { + // We are congestion limited. + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + ev_io_stop(loop_, &wev_); + return 0; + } + + auto &ep = *static_cast<Endpoint *>(ps.path.user_data); + + if (auto rv = + send_packet(ep, ps.path.remote, pi.ecn, tx_.data.data(), nwrite); + rv != NETWORK_ERR_OK) { + if (rv != NETWORK_ERR_SEND_BLOCKED) { + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, NGTCP2_ERR_INTERNAL, nullptr, 0); + disconnect(); + + return rv; + } + + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + on_send_blocked(ep, ps.path.remote, pi.ecn, nwrite); + + return 0; + } + + if (++pktcnt == max_pktcnt) { + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + start_wev_endpoint(ep); + return 0; + } + } +} + +void Client::update_timer() { + auto expiry = ngtcp2_conn_get_expiry(conn_); + auto now = util::timestamp(loop_); + + if (expiry <= now) { + if (!config.quiet) { + auto t = static_cast<ev_tstamp>(now - expiry) / NGTCP2_SECONDS; + std::cerr << "Timer has already expired: " << t << "s" << std::endl; + } + + ev_feed_event(loop_, &timer_, EV_TIMER); + + return; + } + + auto t = static_cast<ev_tstamp>(expiry - now) / NGTCP2_SECONDS; + if (!config.quiet) { + std::cerr << "Set timer=" << std::fixed << t << "s" << std::defaultfloat + << std::endl; + } + timer_.repeat = t; + ev_timer_again(loop_, &timer_); +} + +#ifdef HAVE_LINUX_RTNETLINK_H +namespace { +int bind_addr(Address &local_addr, int fd, const in_addr_union *iau, + int family) { + addrinfo hints{}; + addrinfo *res, *rp; + + hints.ai_family = family; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_PASSIVE; + + char *node; + std::array<char, NI_MAXHOST> nodebuf; + + if (iau) { + if (inet_ntop(family, iau, nodebuf.data(), nodebuf.size()) == nullptr) { + std::cerr << "inet_ntop: " << strerror(errno) << std::endl; + return -1; + } + + node = nodebuf.data(); + } else { + node = nullptr; + } + + if (auto rv = getaddrinfo(node, "0", &hints, &res); rv != 0) { + std::cerr << "getaddrinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + auto res_d = defer(freeaddrinfo, res); + + for (rp = res; rp; rp = rp->ai_next) { + if (bind(fd, rp->ai_addr, rp->ai_addrlen) != -1) { + break; + } + } + + if (!rp) { + std::cerr << "Could not bind" << std::endl; + return -1; + } + + socklen_t len = sizeof(local_addr.su.storage); + if (getsockname(fd, &local_addr.su.sa, &len) == -1) { + std::cerr << "getsockname: " << strerror(errno) << std::endl; + return -1; + } + local_addr.len = len; + local_addr.ifindex = 0; + + return 0; +} +} // namespace +#endif // HAVE_LINUX_RTNETLINK_H + +#ifndef HAVE_LINUX_RTNETLINK_H +namespace { +int connect_sock(Address &local_addr, int fd, const Address &remote_addr) { + if (connect(fd, &remote_addr.su.sa, remote_addr.len) != 0) { + std::cerr << "connect: " << strerror(errno) << std::endl; + return -1; + } + + socklen_t len = sizeof(local_addr.su.storage); + if (getsockname(fd, &local_addr.su.sa, &len) == -1) { + std::cerr << "getsockname: " << strerror(errno) << std::endl; + return -1; + } + local_addr.len = len; + local_addr.ifindex = 0; + + return 0; +} +} // namespace +#endif // !HAVE_LINUX_RTNETLINK_H + +namespace { +int udp_sock(int family) { + auto fd = util::create_nonblock_socket(family, SOCK_DGRAM, IPPROTO_UDP); + if (fd == -1) { + return -1; + } + + fd_set_recv_ecn(fd, family); + fd_set_ip_mtu_discover(fd, family); + fd_set_ip_dontfrag(fd, family); + + return fd; +} +} // namespace + +namespace { +int create_sock(Address &remote_addr, const char *addr, const char *port) { + addrinfo hints{}; + addrinfo *res, *rp; + + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + + if (auto rv = getaddrinfo(addr, port, &hints, &res); rv != 0) { + std::cerr << "getaddrinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + auto res_d = defer(freeaddrinfo, res); + + int fd = -1; + + for (rp = res; rp; rp = rp->ai_next) { + fd = udp_sock(rp->ai_family); + if (fd == -1) { + continue; + } + + break; + } + + if (!rp) { + std::cerr << "Could not create socket" << std::endl; + return -1; + } + + remote_addr.len = rp->ai_addrlen; + memcpy(&remote_addr.su, rp->ai_addr, rp->ai_addrlen); + + return fd; +} +} // namespace + +std::optional<Endpoint *> Client::endpoint_for(const Address &remote_addr) { +#ifdef HAVE_LINUX_RTNETLINK_H + in_addr_union iau; + + if (get_local_addr(iau, remote_addr) != 0) { + std::cerr << "Could not get local address for a selected preferred address" + << std::endl; + return nullptr; + } + + auto current_path = ngtcp2_conn_get_path(conn_); + auto current_ep = static_cast<Endpoint *>(current_path->user_data); + if (addreq(¤t_ep->addr.su.sa, iau)) { + return current_ep; + } +#endif // HAVE_LINUX_RTNETLINK_H + + auto fd = udp_sock(remote_addr.su.sa.sa_family); + if (fd == -1) { + return nullptr; + } + + Address local_addr; + +#ifdef HAVE_LINUX_RTNETLINK_H + if (bind_addr(local_addr, fd, &iau, remote_addr.su.sa.sa_family) != 0) { + close(fd); + return nullptr; + } +#else // !HAVE_LINUX_RTNETLINK_H + if (connect_sock(local_addr, fd, remote_addr) != 0) { + close(fd); + return nullptr; + } +#endif // !HAVE_LINUX_RTNETLINK_H + + endpoints_.emplace_back(); + auto &ep = endpoints_.back(); + ep.addr = local_addr; + ep.client = this; + ep.fd = fd; + ev_io_init(&ep.rev, readcb, fd, EV_READ); + ep.rev.data = &ep; + + ev_io_start(loop_, &ep.rev); + + return &ep; +} + +void Client::start_change_local_addr_timer() { + ev_timer_start(loop_, &change_local_addr_timer_); +} + +int Client::change_local_addr() { + Address local_addr; + + if (!config.quiet) { + std::cerr << "Changing local address" << std::endl; + } + + auto nfd = udp_sock(remote_addr_.su.sa.sa_family); + if (nfd == -1) { + return -1; + } + +#ifdef HAVE_LINUX_RTNETLINK_H + in_addr_union iau; + + if (get_local_addr(iau, remote_addr_) != 0) { + std::cerr << "Could not get local address" << std::endl; + close(nfd); + return -1; + } + + if (bind_addr(local_addr, nfd, &iau, remote_addr_.su.sa.sa_family) != 0) { + close(nfd); + return -1; + } +#else // !HAVE_LINUX_RTNETLINK_H + if (connect_sock(local_addr, nfd, remote_addr_) != 0) { + close(nfd); + return -1; + } +#endif // !HAVE_LINUX_RTNETLINK_H + + if (!config.quiet) { + std::cerr << "Local address is now " + << util::straddr(&local_addr.su.sa, local_addr.len) << std::endl; + } + + endpoints_.emplace_back(); + auto &ep = endpoints_.back(); + ep.addr = local_addr; + ep.client = this; + ep.fd = nfd; + ev_io_init(&ep.rev, readcb, nfd, EV_READ); + ep.rev.data = &ep; + + ngtcp2_addr addr; + ngtcp2_addr_init(&addr, &local_addr.su.sa, local_addr.len); + + if (config.nat_rebinding) { + ngtcp2_conn_set_local_addr(conn_, &addr); + ngtcp2_conn_set_path_user_data(conn_, &ep); + } else { + auto path = ngtcp2_path{ + addr, + { + const_cast<sockaddr *>(&remote_addr_.su.sa), + remote_addr_.len, + }, + &ep, + }; + if (auto rv = ngtcp2_conn_initiate_immediate_migration( + conn_, &path, util::timestamp(loop_)); + rv != 0) { + std::cerr << "ngtcp2_conn_initiate_immediate_migration: " + << ngtcp2_strerror(rv) << std::endl; + } + } + + ev_io_start(loop_, &ep.rev); + + return 0; +} + +void Client::start_key_update_timer() { + ev_timer_start(loop_, &key_update_timer_); +} + +int Client::update_key(uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen) { + if (!config.quiet) { + std::cerr << "Updating traffic key" << std::endl; + } + + auto crypto_ctx = ngtcp2_conn_get_crypto_ctx(conn_); + auto aead = &crypto_ctx->aead; + auto keylen = ngtcp2_crypto_aead_keylen(aead); + auto ivlen = ngtcp2_crypto_packet_protection_ivlen(aead); + + ++nkey_update_; + + std::array<uint8_t, 64> rx_key, tx_key; + + if (ngtcp2_crypto_update_key(conn_, rx_secret, tx_secret, rx_aead_ctx, + rx_key.data(), rx_iv, tx_aead_ctx, tx_key.data(), + tx_iv, current_rx_secret, current_tx_secret, + secretlen) != 0) { + return -1; + } + + if (!config.quiet && config.show_secret) { + std::cerr << "application_traffic rx secret " << nkey_update_ << std::endl; + debug::print_secrets(rx_secret, secretlen, rx_key.data(), keylen, rx_iv, + ivlen); + std::cerr << "application_traffic tx secret " << nkey_update_ << std::endl; + debug::print_secrets(tx_secret, secretlen, tx_key.data(), keylen, tx_iv, + ivlen); + } + + return 0; +} + +int Client::initiate_key_update() { + if (!config.quiet) { + std::cerr << "Initiate key update" << std::endl; + } + + if (auto rv = ngtcp2_conn_initiate_key_update(conn_, util::timestamp(loop_)); + rv != 0) { + std::cerr << "ngtcp2_conn_initiate_key_update: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + return 0; +} + +void Client::start_delay_stream_timer() { + ev_timer_start(loop_, &delay_stream_timer_); +} + +int Client::send_packet(const Endpoint &ep, const ngtcp2_addr &remote_addr, + unsigned int ecn, const uint8_t *data, size_t datalen) { + if (debug::packet_lost(config.tx_loss_prob)) { + if (!config.quiet) { + std::cerr << "** Simulated outgoing packet loss **" << std::endl; + } + return NETWORK_ERR_OK; + } + + iovec msg_iov; + msg_iov.iov_base = const_cast<uint8_t *>(data); + msg_iov.iov_len = datalen; + + msghdr msg{}; +#ifdef HAVE_LINUX_RTNETLINK_H + msg.msg_name = const_cast<sockaddr *>(remote_addr.addr); + msg.msg_namelen = remote_addr.addrlen; +#endif // HAVE_LINUX_RTNETLINK_H + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + + fd_set_ecn(ep.fd, remote_addr.addr->sa_family, ecn); + + ssize_t nwrite = 0; + + do { + nwrite = sendmsg(ep.fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return NETWORK_ERR_SEND_BLOCKED; + } + std::cerr << "sendmsg: " << strerror(errno) << std::endl; + if (errno == EMSGSIZE) { + return 0; + } + return NETWORK_ERR_FATAL; + } + + assert(static_cast<size_t>(nwrite) == datalen); + + if (!config.quiet) { + std::cerr << "Sent packet: local=" + << util::straddr(&ep.addr.su.sa, ep.addr.len) << " remote=" + << util::straddr(remote_addr.addr, remote_addr.addrlen) + << " ecn=0x" << std::hex << ecn << std::dec << " " << nwrite + << " bytes" << std::endl; + } + + return NETWORK_ERR_OK; +} + +void Client::on_send_blocked(const Endpoint &ep, const ngtcp2_addr &remote_addr, + unsigned int ecn, size_t datalen) { + assert(!tx_.send_blocked); + + tx_.send_blocked = true; + + memcpy(&tx_.blocked.remote_addr.su, remote_addr.addr, remote_addr.addrlen); + tx_.blocked.remote_addr.len = remote_addr.addrlen; + tx_.blocked.ecn = ecn; + tx_.blocked.datalen = datalen; + tx_.blocked.endpoint = &ep; + + start_wev_endpoint(ep); +} + +void Client::start_wev_endpoint(const Endpoint &ep) { + // We do not close ep.fd, so we can expect that each Endpoint has + // unique fd. + if (ep.fd != wev_.fd) { + if (ev_is_active(&wev_)) { + ev_io_stop(loop_, &wev_); + } + + ev_io_set(&wev_, ep.fd, EV_WRITE); + } + + ev_io_start(loop_, &wev_); +} + +int Client::send_blocked_packet() { + assert(tx_.send_blocked); + + ngtcp2_addr remote_addr{ + .addr = &tx_.blocked.remote_addr.su.sa, + .addrlen = tx_.blocked.remote_addr.len, + }; + + auto rv = send_packet(*tx_.blocked.endpoint, remote_addr, tx_.blocked.ecn, + tx_.data.data(), tx_.blocked.datalen); + if (rv != 0) { + if (rv == NETWORK_ERR_SEND_BLOCKED) { + assert(wev_.fd == tx_.blocked.endpoint->fd); + + ev_io_start(loop_, &wev_); + + return 0; + } + + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, NGTCP2_ERR_INTERNAL, nullptr, 0); + disconnect(); + + return rv; + } + + tx_.send_blocked = false; + + return 0; +} + +int Client::handle_error() { + if (!conn_ || ngtcp2_conn_is_in_closing_period(conn_) || + ngtcp2_conn_is_in_draining_period(conn_)) { + return 0; + } + + std::array<uint8_t, NGTCP2_MAX_UDP_PAYLOAD_SIZE> buf; + + ngtcp2_path_storage ps; + + ngtcp2_path_storage_zero(&ps); + + ngtcp2_pkt_info pi; + + auto nwrite = ngtcp2_conn_write_connection_close( + conn_, &ps.path, &pi, buf.data(), buf.size(), &last_error_, + util::timestamp(loop_)); + if (nwrite < 0) { + std::cerr << "ngtcp2_conn_write_connection_close: " + << ngtcp2_strerror(nwrite) << std::endl; + return -1; + } + + if (nwrite == 0) { + return 0; + } + + return send_packet(*static_cast<Endpoint *>(ps.path.user_data), + ps.path.remote, pi.ecn, buf.data(), nwrite); +} + +int Client::on_stream_close(int64_t stream_id, uint64_t app_error_code) { + if (httpconn_) { + if (app_error_code == 0) { + app_error_code = NGHTTP3_H3_NO_ERROR; + } + auto rv = nghttp3_conn_close_stream(httpconn_, stream_id, app_error_code); + switch (rv) { + case 0: + break; + case NGHTTP3_ERR_STREAM_NOT_FOUND: + // We have to handle the case when stream opened but no data is + // transferred. In this case, nghttp3_conn_close_stream might + // return error. + if (!ngtcp2_is_bidi_stream(stream_id)) { + assert(!ngtcp2_conn_is_local_stream(conn_, stream_id)); + ngtcp2_conn_extend_max_streams_uni(conn_, 1); + } + break; + default: + std::cerr << "nghttp3_conn_close_stream: " << nghttp3_strerror(rv) + << std::endl; + ngtcp2_connection_close_error_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, 0); + return -1; + } + } + + return 0; +} + +int Client::on_stream_reset(int64_t stream_id) { + if (httpconn_) { + if (auto rv = nghttp3_conn_shutdown_stream_read(httpconn_, stream_id); + rv != 0) { + std::cerr << "nghttp3_conn_shutdown_stream_read: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + } + return 0; +} + +int Client::on_stream_stop_sending(int64_t stream_id) { + if (!httpconn_) { + return 0; + } + + if (auto rv = nghttp3_conn_shutdown_stream_read(httpconn_, stream_id); + rv != 0) { + std::cerr << "nghttp3_conn_shutdown_stream_read: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + return 0; +} + +int Client::make_stream_early() { + if (setup_httpconn() != 0) { + return -1; + } + + return on_extend_max_streams(); +} + +int Client::on_extend_max_streams() { + int64_t stream_id; + + if ((config.delay_stream && !handshake_confirmed_) || + ev_is_active(&delay_stream_timer_)) { + return 0; + } + + for (; nstreams_done_ < config.nstreams; ++nstreams_done_) { + if (auto rv = ngtcp2_conn_open_bidi_stream(conn_, &stream_id, nullptr); + rv != 0) { + assert(NGTCP2_ERR_STREAM_ID_BLOCKED == rv); + break; + } + + auto stream = std::make_unique<Stream>( + config.requests[nstreams_done_ % config.requests.size()], stream_id); + + if (submit_http_request(stream.get()) != 0) { + break; + } + + if (!config.download.empty()) { + stream->open_file(stream->req.path); + } + streams_.emplace(stream_id, std::move(stream)); + } + return 0; +} + +namespace { +nghttp3_ssize read_data(nghttp3_conn *conn, int64_t stream_id, nghttp3_vec *vec, + size_t veccnt, uint32_t *pflags, void *user_data, + void *stream_user_data) { + vec[0].base = config.data; + vec[0].len = config.datalen; + *pflags |= NGHTTP3_DATA_FLAG_EOF; + + return 1; +} +} // namespace + +int Client::submit_http_request(const Stream *stream) { + std::string content_length_str; + + const auto &req = stream->req; + + std::array<nghttp3_nv, 6> nva{ + util::make_nv_nn(":method", config.http_method), + util::make_nv_nn(":scheme", req.scheme), + util::make_nv_nn(":authority", req.authority), + util::make_nv_nn(":path", req.path), + util::make_nv_nn("user-agent", "nghttp3/ngtcp2 client"), + }; + size_t nvlen = 5; + if (config.fd != -1) { + content_length_str = util::format_uint(config.datalen); + nva[nvlen++] = util::make_nv_nc("content-length", content_length_str); + } + + if (!config.quiet) { + debug::print_http_request_headers(stream->stream_id, nva.data(), nvlen); + } + + nghttp3_data_reader dr{}; + dr.read_data = read_data; + + if (auto rv = nghttp3_conn_submit_request( + httpconn_, stream->stream_id, nva.data(), nvlen, + config.fd == -1 ? nullptr : &dr, nullptr); + rv != 0) { + std::cerr << "nghttp3_conn_submit_request: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + return 0; +} + +int Client::recv_stream_data(uint32_t flags, int64_t stream_id, + const uint8_t *data, size_t datalen) { + auto nconsumed = nghttp3_conn_read_stream( + httpconn_, stream_id, data, datalen, flags & NGTCP2_STREAM_DATA_FLAG_FIN); + if (nconsumed < 0) { + std::cerr << "nghttp3_conn_read_stream: " << nghttp3_strerror(nconsumed) + << std::endl; + ngtcp2_connection_close_error_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(nconsumed), nullptr, + 0); + return -1; + } + + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, nconsumed); + ngtcp2_conn_extend_max_offset(conn_, nconsumed); + + return 0; +} + +int Client::acked_stream_data_offset(int64_t stream_id, uint64_t datalen) { + if (auto rv = nghttp3_conn_add_ack_offset(httpconn_, stream_id, datalen); + rv != 0) { + std::cerr << "nghttp3_conn_add_ack_offset: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + return 0; +} + +int Client::select_preferred_address(Address &selected_addr, + const ngtcp2_preferred_addr *paddr) { + auto path = ngtcp2_conn_get_path(conn_); + + switch (path->local.addr->sa_family) { + case AF_INET: + if (!paddr->ipv4_present) { + return -1; + } + selected_addr.su.in = paddr->ipv4; + selected_addr.len = sizeof(paddr->ipv4); + break; + case AF_INET6: + if (!paddr->ipv6_present) { + return -1; + } + selected_addr.su.in6 = paddr->ipv6; + selected_addr.len = sizeof(paddr->ipv6); + break; + default: + return -1; + } + + char host[NI_MAXHOST], service[NI_MAXSERV]; + if (auto rv = getnameinfo(&selected_addr.su.sa, selected_addr.len, host, + sizeof(host), service, sizeof(service), + NI_NUMERICHOST | NI_NUMERICSERV); + rv != 0) { + std::cerr << "getnameinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + if (!config.quiet) { + std::cerr << "selected server preferred_address is [" << host + << "]:" << service << std::endl; + } + + return 0; +} + +namespace { +int http_recv_data(nghttp3_conn *conn, int64_t stream_id, const uint8_t *data, + size_t datalen, void *user_data, void *stream_user_data) { + if (!config.quiet && !config.no_http_dump) { + debug::print_http_data(stream_id, data, datalen); + } + auto c = static_cast<Client *>(user_data); + c->http_consume(stream_id, datalen); + c->http_write_data(stream_id, data, datalen); + return 0; +} +} // namespace + +namespace { +int http_deferred_consume(nghttp3_conn *conn, int64_t stream_id, + size_t nconsumed, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + c->http_consume(stream_id, nconsumed); + return 0; +} +} // namespace + +void Client::http_consume(int64_t stream_id, size_t nconsumed) { + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, nconsumed); + ngtcp2_conn_extend_max_offset(conn_, nconsumed); +} + +void Client::http_write_data(int64_t stream_id, const uint8_t *data, + size_t datalen) { + auto it = streams_.find(stream_id); + if (it == std::end(streams_)) { + return; + } + + auto &stream = (*it).second; + + if (stream->fd == -1) { + return; + } + + ssize_t nwrite; + do { + nwrite = write(stream->fd, data, datalen); + } while (nwrite == -1 && errno == EINTR); +} + +namespace { +int http_begin_headers(nghttp3_conn *conn, int64_t stream_id, void *user_data, + void *stream_user_data) { + if (!config.quiet) { + debug::print_http_begin_response_headers(stream_id); + } + return 0; +} +} // namespace + +namespace { +int http_recv_header(nghttp3_conn *conn, int64_t stream_id, int32_t token, + nghttp3_rcbuf *name, nghttp3_rcbuf *value, uint8_t flags, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_header(stream_id, name, value, flags); + } + return 0; +} +} // namespace + +namespace { +int http_end_headers(nghttp3_conn *conn, int64_t stream_id, int fin, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_end_headers(stream_id); + } + return 0; +} +} // namespace + +namespace { +int http_begin_trailers(nghttp3_conn *conn, int64_t stream_id, void *user_data, + void *stream_user_data) { + if (!config.quiet) { + debug::print_http_begin_trailers(stream_id); + } + return 0; +} +} // namespace + +namespace { +int http_recv_trailer(nghttp3_conn *conn, int64_t stream_id, int32_t token, + nghttp3_rcbuf *name, nghttp3_rcbuf *value, uint8_t flags, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_header(stream_id, name, value, flags); + } + return 0; +} +} // namespace + +namespace { +int http_end_trailers(nghttp3_conn *conn, int64_t stream_id, int fin, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_end_trailers(stream_id); + } + return 0; +} +} // namespace + +namespace { +int http_stop_sending(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + if (c->stop_sending(stream_id, app_error_code) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Client::stop_sending(int64_t stream_id, uint64_t app_error_code) { + if (auto rv = + ngtcp2_conn_shutdown_stream_read(conn_, stream_id, app_error_code); + rv != 0) { + std::cerr << "ngtcp2_conn_shutdown_stream_read: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + return 0; +} + +namespace { +int http_reset_stream(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + if (c->reset_stream(stream_id, app_error_code) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Client::reset_stream(int64_t stream_id, uint64_t app_error_code) { + if (auto rv = + ngtcp2_conn_shutdown_stream_write(conn_, stream_id, app_error_code); + rv != 0) { + std::cerr << "ngtcp2_conn_shutdown_stream_write: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + return 0; +} + +namespace { +int http_stream_close(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *conn_user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(conn_user_data); + if (c->http_stream_close(stream_id, app_error_code) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Client::http_stream_close(int64_t stream_id, uint64_t app_error_code) { + if (ngtcp2_is_bidi_stream(stream_id)) { + assert(ngtcp2_conn_is_local_stream(conn_, stream_id)); + + ++nstreams_closed_; + + if (config.exit_on_first_stream_close || + (config.exit_on_all_streams_close && + config.nstreams == nstreams_done_ && + nstreams_closed_ == nstreams_done_)) { + if (handshake_confirmed_) { + should_exit_ = true; + } else { + should_exit_on_handshake_confirmed_ = true; + } + } + } else { + assert(!ngtcp2_conn_is_local_stream(conn_, stream_id)); + ngtcp2_conn_extend_max_streams_uni(conn_, 1); + } + + if (auto it = streams_.find(stream_id); it != std::end(streams_)) { + if (!config.quiet) { + std::cerr << "HTTP stream " << stream_id << " closed with error code " + << app_error_code << std::endl; + } + streams_.erase(it); + } + + return 0; +} + +int Client::setup_httpconn() { + if (httpconn_) { + return 0; + } + + if (ngtcp2_conn_get_max_local_streams_uni(conn_) < 3) { + std::cerr << "peer does not allow at least 3 unidirectional streams." + << std::endl; + return -1; + } + + nghttp3_callbacks callbacks{ + nullptr, // acked_stream_data + ::http_stream_close, + ::http_recv_data, + ::http_deferred_consume, + ::http_begin_headers, + ::http_recv_header, + ::http_end_headers, + ::http_begin_trailers, + ::http_recv_trailer, + ::http_end_trailers, + ::http_stop_sending, + nullptr, // end_stream + ::http_reset_stream, + nullptr, // shutdown + }; + nghttp3_settings settings; + nghttp3_settings_default(&settings); + settings.qpack_max_dtable_capacity = 4_k; + settings.qpack_blocked_streams = 100; + + auto mem = nghttp3_mem_default(); + + if (auto rv = + nghttp3_conn_client_new(&httpconn_, &callbacks, &settings, mem, this); + rv != 0) { + std::cerr << "nghttp3_conn_client_new: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + int64_t ctrl_stream_id; + + if (auto rv = ngtcp2_conn_open_uni_stream(conn_, &ctrl_stream_id, nullptr); + rv != 0) { + std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + if (auto rv = nghttp3_conn_bind_control_stream(httpconn_, ctrl_stream_id); + rv != 0) { + std::cerr << "nghttp3_conn_bind_control_stream: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + if (!config.quiet) { + fprintf(stderr, "http: control stream=%" PRIx64 "\n", ctrl_stream_id); + } + + int64_t qpack_enc_stream_id, qpack_dec_stream_id; + + if (auto rv = + ngtcp2_conn_open_uni_stream(conn_, &qpack_enc_stream_id, nullptr); + rv != 0) { + std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + if (auto rv = + ngtcp2_conn_open_uni_stream(conn_, &qpack_dec_stream_id, nullptr); + rv != 0) { + std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + if (auto rv = nghttp3_conn_bind_qpack_streams(httpconn_, qpack_enc_stream_id, + qpack_dec_stream_id); + rv != 0) { + std::cerr << "nghttp3_conn_bind_qpack_streams: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + if (!config.quiet) { + fprintf(stderr, + "http: QPACK streams encoder=%" PRIx64 " decoder=%" PRIx64 "\n", + qpack_enc_stream_id, qpack_dec_stream_id); + } + + return 0; +} + +const std::vector<uint32_t> &Client::get_offered_versions() const { + return offered_versions_; +} + +bool Client::get_early_data() const { return early_data_; }; + +namespace { +int run(Client &c, const char *addr, const char *port, + TLSClientContext &tls_ctx) { + Address remote_addr, local_addr; + + auto fd = create_sock(remote_addr, addr, port); + if (fd == -1) { + return -1; + } + +#ifdef HAVE_LINUX_RTNETLINK_H + in_addr_union iau; + + if (get_local_addr(iau, remote_addr) != 0) { + std::cerr << "Could not get local address" << std::endl; + close(fd); + return -1; + } + + if (bind_addr(local_addr, fd, &iau, remote_addr.su.sa.sa_family) != 0) { + close(fd); + return -1; + } +#else // !HAVE_LINUX_RTNETLINK_H + if (connect_sock(local_addr, fd, remote_addr) != 0) { + close(fd); + return -1; + } +#endif // !HAVE_LINUX_RTNETLINK_H + + if (c.init(fd, local_addr, remote_addr, addr, port, tls_ctx) != 0) { + return -1; + } + + // TODO Do we need this ? + if (auto rv = c.on_write(); rv != 0) { + return rv; + } + + ev_run(EV_DEFAULT, 0); + + return 0; +} +} // namespace + +namespace { +std::string_view get_string(const char *uri, const http_parser_url &u, + http_parser_url_fields f) { + auto p = &u.field_data[f]; + return {uri + p->off, p->len}; +} +} // namespace + +namespace { +int parse_uri(Request &req, const char *uri) { + http_parser_url u; + + http_parser_url_init(&u); + if (http_parser_parse_url(uri, strlen(uri), /* is_connect = */ 0, &u) != 0) { + return -1; + } + + if (!(u.field_set & (1 << UF_SCHEMA)) || !(u.field_set & (1 << UF_HOST))) { + return -1; + } + + req.scheme = get_string(uri, u, UF_SCHEMA); + + req.authority = get_string(uri, u, UF_HOST); + if (util::numeric_host(req.authority.c_str(), AF_INET6)) { + req.authority = '[' + req.authority + ']'; + } + if (u.field_set & (1 << UF_PORT)) { + req.authority += ':'; + req.authority += get_string(uri, u, UF_PORT); + } + + if (u.field_set & (1 << UF_PATH)) { + req.path = get_string(uri, u, UF_PATH); + } else { + req.path = "/"; + } + + if (u.field_set & (1 << UF_QUERY)) { + req.path += '?'; + req.path += get_string(uri, u, UF_QUERY); + } + + return 0; +} +} // namespace + +namespace { +int parse_requests(char **argv, size_t argvlen) { + for (size_t i = 0; i < argvlen; ++i) { + auto uri = argv[i]; + Request req; + if (parse_uri(req, uri) != 0) { + std::cerr << "Could not parse URI: " << uri << std::endl; + return -1; + } + config.requests.emplace_back(std::move(req)); + } + return 0; +} +} // namespace + +std::ofstream keylog_file; + +namespace { +void print_usage() { + std::cerr << "Usage: client [OPTIONS] <HOST> <PORT> [<URI>...]" << std::endl; +} +} // namespace + +namespace { +void config_set_default(Config &config) { + config = Config{}; + config.tx_loss_prob = 0.; + config.rx_loss_prob = 0.; + config.fd = -1; + config.ciphers = util::crypto_default_ciphers(); + config.groups = util::crypto_default_groups(); + config.nstreams = 0; + config.data = nullptr; + config.datalen = 0; + config.version = NGTCP2_PROTO_VER_V1; + config.timeout = 30 * NGTCP2_SECONDS; + config.http_method = "GET"sv; + config.max_data = 15_m; + config.max_stream_data_bidi_local = 6_m; + config.max_stream_data_bidi_remote = 6_m; + config.max_stream_data_uni = 6_m; + config.max_window = 24_m; + config.max_stream_window = 16_m; + config.max_streams_uni = 100; + config.cc_algo = NGTCP2_CC_ALGO_CUBIC; + config.initial_rtt = NGTCP2_DEFAULT_INITIAL_RTT; + config.handshake_timeout = NGTCP2_DEFAULT_HANDSHAKE_TIMEOUT; + config.ack_thresh = 2; +} +} // namespace + +namespace { +void print_help() { + print_usage(); + + config_set_default(config); + + std::cout << R"( + <HOST> Remote server host (DNS name or IP address). In case of + DNS name, it will be sent in TLS SNI extension. + <PORT> Remote server port + <URI> Remote URI +Options: + -t, --tx-loss=<P> + The probability of losing outgoing packets. <P> must be + [0.0, 1.0], inclusive. 0.0 means no packet loss. 1.0 + means 100% packet loss. + -r, --rx-loss=<P> + The probability of losing incoming packets. <P> must be + [0.0, 1.0], inclusive. 0.0 means no packet loss. 1.0 + means 100% packet loss. + -d, --data=<PATH> + Read data from <PATH>, and send them as STREAM data. + -n, --nstreams=<N> + The number of requests. <URI>s are used in the order of + appearance in the command-line. If the number of <URI> + list is less than <N>, <URI> list is wrapped. It + defaults to 0 which means the number of <URI> specified. + -v, --version=<HEX> + Specify QUIC version to use in hex string. If the given + version is not supported by libngtcp2, client will use + QUIC v1 long packet types. Instead of specifying hex + string, there are special aliases available: "v1" + indicates QUIC v1, and "v2draft" indicates QUIC v2 + draft. + Default: )" + << std::hex << "0x" << config.version << std::dec << R"( + --preferred-versions=<HEX>[[,<HEX>]...] + Specify QUIC versions in hex string in the order of + preference. Client chooses one of those versions if + client received Version Negotiation packet from server. + These versions must be supported by libngtcp2. Instead + of specifying hex string, there are special aliases + available: "v1" indicates QUIC v1, and "v2draft" + indicates QUIC v2 draft. + --other-versions=<HEX>[[,<HEX>]...] + Specify QUIC versions in hex string that are sent in + other_versions field of version_information transport + parameter. This list can include a version which is not + supported by libngtcp2. Instead of specifying hex + string, there are special aliases available: "v1" + indicates QUIC v1, and "v2draft" indicates QUIC v2 + draft. + -q, --quiet Suppress debug output. + -s, --show-secret + Print out secrets unless --quiet is used. + --timeout=<DURATION> + Specify idle timeout. + Default: )" + << util::format_duration(config.timeout) << R"( + --ciphers=<CIPHERS> + Specify the cipher suite list to enable. + Default: )" + << config.ciphers << R"( + --groups=<GROUPS> + Specify the supported groups. + Default: )" + << config.groups << R"( + --session-file=<PATH> + Read/write TLS session from/to <PATH>. To resume a + session, the previous session must be supplied with this + option. + --tp-file=<PATH> + Read/write QUIC transport parameters from/to <PATH>. To + send 0-RTT data, the transport parameters received from + the previous session must be supplied with this option. + --dcid=<DCID> + Specify initial DCID. <DCID> is hex string. When + decoded as binary, it should be at least 8 bytes and at + most 18 bytes long. + --scid=<SCID> + Specify source connection ID. <SCID> is hex string. If + an empty string is given, zero length connection ID is + assumed. + --change-local-addr=<DURATION> + Client changes local address when <DURATION> elapse + after handshake completes. + --nat-rebinding + When used with --change-local-addr, simulate NAT + rebinding. In other words, client changes local + address, but it does not start path validation. + --key-update=<DURATION> + Client initiates key update when <DURATION> elapse after + handshake completes. + -m, --http-method=<METHOD> + Specify HTTP method. Default: )" + << config.http_method << R"( + --delay-stream=<DURATION> + Delay sending STREAM data in 1-RTT for <DURATION> after + handshake completes. + --no-preferred-addr + Do not try to use preferred address offered by server. + --key=<PATH> + The path to client private key PEM file. + --cert=<PATH> + The path to client certificate PEM file. + --download=<PATH> + The path to the directory to save a downloaded content. + It is undefined if 2 concurrent requests write to the + same file. If a request path does not contain a path + component usable as a file name, it defaults to + "index.html". + --no-quic-dump + Disables printing QUIC STREAM and CRYPTO frame data out. + --no-http-dump + Disables printing HTTP response body out. + --qlog-file=<PATH> + The path to write qlog. This option and --qlog-dir are + mutually exclusive. + --qlog-dir=<PATH> + Path to the directory where qlog file is stored. The + file name of each qlog is the Source Connection ID of + client. This option and --qlog-file are mutually + exclusive. + --max-data=<SIZE> + The initial connection-level flow control window. + Default: )" + << util::format_uint_iec(config.max_data) << R"( + --max-stream-data-bidi-local=<SIZE> + The initial stream-level flow control window for a + bidirectional stream that the local endpoint initiates. + Default: )" + << util::format_uint_iec(config.max_stream_data_bidi_local) << R"( + --max-stream-data-bidi-remote=<SIZE> + The initial stream-level flow control window for a + bidirectional stream that the remote endpoint initiates. + Default: )" + << util::format_uint_iec(config.max_stream_data_bidi_remote) << R"( + --max-stream-data-uni=<SIZE> + The initial stream-level flow control window for a + unidirectional stream. + Default: )" + << util::format_uint_iec(config.max_stream_data_uni) << R"( + --max-streams-bidi=<N> + The number of the concurrent bidirectional streams. + Default: )" + << config.max_streams_bidi << R"( + --max-streams-uni=<N> + The number of the concurrent unidirectional streams. + Default: )" + << config.max_streams_uni << R"( + --exit-on-first-stream-close + Exit when a first client initialted HTTP stream is + closed. + --exit-on-all-streams-close + Exit when all client initiated HTTP streams are closed. + --disable-early-data + Disable early data. + --cc=(cubic|reno|bbr|bbr2) + The name of congestion controller algorithm. + Default: )" + << util::strccalgo(config.cc_algo) << R"( + --token-file=<PATH> + Read/write token from/to <PATH>. Token is obtained from + NEW_TOKEN frame from server. + --sni=<DNSNAME> + Send <DNSNAME> in TLS SNI, overriding the DNS name + specified in <HOST>. + --initial-rtt=<DURATION> + Set an initial RTT. + Default: )" + << util::format_duration(config.initial_rtt) << R"( + --max-window=<SIZE> + Maximum connection-level flow control window size. The + window auto-tuning is enabled if nonzero value is given, + and window size is scaled up to this value. + Default: )" + << util::format_uint_iec(config.max_window) << R"( + --max-stream-window=<SIZE> + Maximum stream-level flow control window size. The + window auto-tuning is enabled if nonzero value is given, + and window size is scaled up to this value. + Default: )" + << util::format_uint_iec(config.max_stream_window) << R"( + --max-udp-payload-size=<SIZE> + Override maximum UDP payload size that client transmits. + --handshake-timeout=<DURATION> + Set the QUIC handshake timeout. + Default: )" + << util::format_duration(config.handshake_timeout) << R"( + --no-pmtud Disables Path MTU Discovery. + --ack-thresh=<N> + The minimum number of the received ACK eliciting packets + that triggers immediate acknowledgement. + Default: )" + << config.ack_thresh << R"( + -h, --help Display this help and exit. + +--- + + The <SIZE> argument is an integer and an optional unit (e.g., 10K is + 10 * 1024). Units are K, M and G (powers of 1024). + + The <DURATION> argument is an integer and an optional unit (e.g., 1s + is 1 second and 500ms is 500 milliseconds). Units are h, m, s, ms, + us, or ns (hours, minutes, seconds, milliseconds, microseconds, and + nanoseconds respectively). If a unit is omitted, a second is used + as unit. + + The <HEX> argument is an hex string which must start with "0x" + (e.g., 0x00000001).)" + << std::endl; +} +} // namespace + +int main(int argc, char **argv) { + config_set_default(config); + char *data_path = nullptr; + const char *private_key_file = nullptr; + const char *cert_file = nullptr; + + for (;;) { + static int flag = 0; + constexpr static option long_opts[] = { + {"help", no_argument, nullptr, 'h'}, + {"tx-loss", required_argument, nullptr, 't'}, + {"rx-loss", required_argument, nullptr, 'r'}, + {"data", required_argument, nullptr, 'd'}, + {"http-method", required_argument, nullptr, 'm'}, + {"nstreams", required_argument, nullptr, 'n'}, + {"version", required_argument, nullptr, 'v'}, + {"quiet", no_argument, nullptr, 'q'}, + {"show-secret", no_argument, nullptr, 's'}, + {"ciphers", required_argument, &flag, 1}, + {"groups", required_argument, &flag, 2}, + {"timeout", required_argument, &flag, 3}, + {"session-file", required_argument, &flag, 4}, + {"tp-file", required_argument, &flag, 5}, + {"dcid", required_argument, &flag, 6}, + {"change-local-addr", required_argument, &flag, 7}, + {"key-update", required_argument, &flag, 8}, + {"nat-rebinding", no_argument, &flag, 9}, + {"delay-stream", required_argument, &flag, 10}, + {"no-preferred-addr", no_argument, &flag, 11}, + {"key", required_argument, &flag, 12}, + {"cert", required_argument, &flag, 13}, + {"download", required_argument, &flag, 14}, + {"no-quic-dump", no_argument, &flag, 15}, + {"no-http-dump", no_argument, &flag, 16}, + {"qlog-file", required_argument, &flag, 17}, + {"max-data", required_argument, &flag, 18}, + {"max-stream-data-bidi-local", required_argument, &flag, 19}, + {"max-stream-data-bidi-remote", required_argument, &flag, 20}, + {"max-stream-data-uni", required_argument, &flag, 21}, + {"max-streams-bidi", required_argument, &flag, 22}, + {"max-streams-uni", required_argument, &flag, 23}, + {"exit-on-first-stream-close", no_argument, &flag, 24}, + {"disable-early-data", no_argument, &flag, 25}, + {"qlog-dir", required_argument, &flag, 26}, + {"cc", required_argument, &flag, 27}, + {"exit-on-all-streams-close", no_argument, &flag, 28}, + {"token-file", required_argument, &flag, 29}, + {"sni", required_argument, &flag, 30}, + {"initial-rtt", required_argument, &flag, 31}, + {"max-window", required_argument, &flag, 32}, + {"max-stream-window", required_argument, &flag, 33}, + {"scid", required_argument, &flag, 34}, + {"max-udp-payload-size", required_argument, &flag, 35}, + {"handshake-timeout", required_argument, &flag, 36}, + {"other-versions", required_argument, &flag, 37}, + {"no-pmtud", no_argument, &flag, 38}, + {"preferred-versions", required_argument, &flag, 39}, + {"ack-thresh", required_argument, &flag, 40}, + {nullptr, 0, nullptr, 0}, + }; + + auto optidx = 0; + auto c = getopt_long(argc, argv, "d:him:n:qr:st:v:", long_opts, &optidx); + if (c == -1) { + break; + } + switch (c) { + case 'd': + // --data + data_path = optarg; + break; + case 'h': + // --help + print_help(); + exit(EXIT_SUCCESS); + case 'm': + // --http-method + config.http_method = optarg; + break; + case 'n': + // --streams + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "streams: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else if (*n > NGTCP2_MAX_VARINT) { + std::cerr << "streams: must not exceed " << NGTCP2_MAX_VARINT + << std::endl; + exit(EXIT_FAILURE); + } else { + config.nstreams = *n; + } + break; + case 'q': + // --quiet + config.quiet = true; + break; + case 'r': + // --rx-loss + config.rx_loss_prob = strtod(optarg, nullptr); + break; + case 's': + // --show-secret + config.show_secret = true; + break; + case 't': + // --tx-loss + config.tx_loss_prob = strtod(optarg, nullptr); + break; + case 'v': { + // --version + if (optarg == "v1"sv) { + config.version = NGTCP2_PROTO_VER_V1; + break; + } + if (optarg == "v2draft"sv) { + config.version = NGTCP2_PROTO_VER_V2_DRAFT; + break; + } + auto rv = util::parse_version(optarg); + if (!rv) { + std::cerr << "version: invalid version " << std::quoted(optarg) + << std::endl; + exit(EXIT_FAILURE); + } + config.version = *rv; + break; + } + case '?': + print_usage(); + exit(EXIT_FAILURE); + case 0: + switch (flag) { + case 1: + // --ciphers + config.ciphers = optarg; + break; + case 2: + // --groups + config.groups = optarg; + break; + case 3: + // --timeout + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "timeout: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.timeout = *t; + } + break; + case 4: + // --session-file + config.session_file = optarg; + break; + case 5: + // --tp-file + config.tp_file = optarg; + break; + case 6: { + // --dcid + auto dcidlen2 = strlen(optarg); + if (dcidlen2 % 2 || dcidlen2 / 2 < 8 || dcidlen2 / 2 > 18) { + std::cerr << "dcid: wrong length" << std::endl; + exit(EXIT_FAILURE); + } + auto dcid = util::decode_hex(optarg); + ngtcp2_cid_init(&config.dcid, + reinterpret_cast<const uint8_t *>(dcid.c_str()), + dcid.size()); + break; + } + case 7: + // --change-local-addr + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "change-local-addr: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.change_local_addr = *t; + } + break; + case 8: + // --key-update + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "key-update: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.key_update = *t; + } + break; + case 9: + // --nat-rebinding + config.nat_rebinding = true; + break; + case 10: + // --delay-stream + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "delay-stream: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.delay_stream = *t; + } + break; + case 11: + // --no-preferred-addr + config.no_preferred_addr = true; + break; + case 12: + // --key + private_key_file = optarg; + break; + case 13: + // --cert + cert_file = optarg; + break; + case 14: + // --download + config.download = optarg; + break; + case 15: + // --no-quic-dump + config.no_quic_dump = true; + break; + case 16: + // --no-http-dump + config.no_http_dump = true; + break; + case 17: + // --qlog-file + config.qlog_file = optarg; + break; + case 18: + // --max-data + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-data: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_data = *n; + } + break; + case 19: + // --max-stream-data-bidi-local + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-data-bidi-local: invalid argument" + << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_data_bidi_local = *n; + } + break; + case 20: + // --max-stream-data-bidi-remote + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-data-bidi-remote: invalid argument" + << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_data_bidi_remote = *n; + } + break; + case 21: + // --max-stream-data-uni + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-data-uni: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_data_uni = *n; + } + break; + case 22: + // --max-streams-bidi + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "max-streams-bidi: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_streams_bidi = *n; + } + break; + case 23: + // --max-streams-uni + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "max-streams-uni: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_streams_uni = *n; + } + break; + case 24: + // --exit-on-first-stream-close + config.exit_on_first_stream_close = true; + break; + case 25: + // --disable-early-data + config.disable_early_data = true; + break; + case 26: + // --qlog-dir + config.qlog_dir = optarg; + break; + case 27: + // --cc + if (strcmp("cubic", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_CUBIC; + break; + } + if (strcmp("reno", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_RENO; + break; + } + if (strcmp("bbr", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_BBR; + break; + } + if (strcmp("bbr2", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_BBR2; + break; + } + std::cerr << "cc: specify cubic, reno, bbr, or bbr2" << std::endl; + exit(EXIT_FAILURE); + case 28: + // --exit-on-all-streams-close + config.exit_on_all_streams_close = true; + break; + case 29: + // --token-file + config.token_file = optarg; + break; + case 30: + // --sni + config.sni = optarg; + break; + case 31: + // --initial-rtt + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "initial-rtt: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.initial_rtt = *t; + } + break; + case 32: + // --max-window + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-window: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_window = *n; + } + break; + case 33: + // --max-stream-window + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-window: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_window = *n; + } + break; + case 34: { + // --scid + auto scid = util::decode_hex(optarg); + ngtcp2_cid_init(&config.scid, + reinterpret_cast<const uint8_t *>(scid.c_str()), + scid.size()); + config.scid_present = true; + break; + } + case 35: + // --max-udp-payload-size + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-udp-payload-size: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else if (*n > 64_k) { + std::cerr << "max-udp-payload-size: must not exceed 65536" + << std::endl; + exit(EXIT_FAILURE); + } else if (*n == 0) { + std::cerr << "max-udp-payload-size: must not be 0" << std::endl; + } else { + config.max_udp_payload_size = *n; + } + break; + case 36: + // --handshake-timeout + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "handshake-timeout: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.handshake_timeout = *t; + } + break; + case 37: { + // --other-versions + if (strlen(optarg) == 0) { + config.other_versions.resize(0); + break; + } + auto l = util::split_str(optarg); + config.other_versions.resize(l.size()); + auto it = std::begin(config.other_versions); + for (const auto &k : l) { + if (k == "v1"sv) { + *it++ = NGTCP2_PROTO_VER_V1; + continue; + } + if (k == "v2draft"sv) { + *it++ = NGTCP2_PROTO_VER_V2_DRAFT; + continue; + } + auto rv = util::parse_version(k); + if (!rv) { + std::cerr << "other-versions: invalid version " << std::quoted(k) + << std::endl; + exit(EXIT_FAILURE); + } + *it++ = *rv; + } + break; + } + case 38: + // --no-pmtud + config.no_pmtud = true; + break; + case 39: { + // --preferred-versions + auto l = util::split_str(optarg); + if (l.size() > max_preferred_versionslen) { + std::cerr << "preferred-versions: too many versions > " + << max_preferred_versionslen << std::endl; + } + config.preferred_versions.resize(l.size()); + auto it = std::begin(config.preferred_versions); + for (const auto &k : l) { + if (k == "v1"sv) { + *it++ = NGTCP2_PROTO_VER_V1; + continue; + } + if (k == "v2draft"sv) { + *it++ = NGTCP2_PROTO_VER_V2_DRAFT; + continue; + } + auto rv = util::parse_version(k); + if (!rv) { + std::cerr << "preferred-versions: invalid version " + << std::quoted(k) << std::endl; + exit(EXIT_FAILURE); + } + if (!ngtcp2_is_supported_version(*rv)) { + std::cerr << "preferred-versions: unsupported version " + << std::quoted(k) << std::endl; + exit(EXIT_FAILURE); + } + *it++ = *rv; + } + break; + } + case 40: + // --ack-thresh + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "ack-thresh: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else if (*n > 100) { + std::cerr << "ack-thresh: must not exceed 100" << std::endl; + exit(EXIT_FAILURE); + } else { + config.ack_thresh = *n; + } + break; + } + break; + default: + break; + }; + } + + if (argc - optind < 2) { + std::cerr << "Too few arguments" << std::endl; + print_usage(); + exit(EXIT_FAILURE); + } + + if (!config.qlog_file.empty() && !config.qlog_dir.empty()) { + std::cerr << "qlog-file and qlog-dir are mutually exclusive" << std::endl; + exit(EXIT_FAILURE); + } + + if (config.exit_on_first_stream_close && config.exit_on_all_streams_close) { + std::cerr << "exit-on-first-stream-close and exit-on-all-streams-close are " + "mutually exclusive" + << std::endl; + exit(EXIT_FAILURE); + } + + if (data_path) { + auto fd = open(data_path, O_RDONLY); + if (fd == -1) { + std::cerr << "data: Could not open file " << data_path << ": " + << strerror(errno) << std::endl; + exit(EXIT_FAILURE); + } + struct stat st; + if (fstat(fd, &st) != 0) { + std::cerr << "data: Could not stat file " << data_path << ": " + << strerror(errno) << std::endl; + exit(EXIT_FAILURE); + } + config.fd = fd; + config.datalen = st.st_size; + auto addr = mmap(nullptr, config.datalen, PROT_READ, MAP_SHARED, fd, 0); + if (addr == MAP_FAILED) { + std::cerr << "data: Could not mmap file " << data_path << ": " + << strerror(errno) << std::endl; + exit(EXIT_FAILURE); + } + config.data = static_cast<uint8_t *>(addr); + } + + auto addr = argv[optind++]; + auto port = argv[optind++]; + + if (parse_requests(&argv[optind], argc - optind) != 0) { + exit(EXIT_FAILURE); + } + + if (!ngtcp2_is_reserved_version(config.version)) { + if (!config.preferred_versions.empty() && + std::find(std::begin(config.preferred_versions), + std::end(config.preferred_versions), + config.version) == std::end(config.preferred_versions)) { + std::cerr << "preferred-version: must include version " + << "0x" << config.version << std::endl; + exit(EXIT_FAILURE); + } + + if (!config.other_versions.empty() && + std::find(std::begin(config.other_versions), + std::end(config.other_versions), + config.version) == std::end(config.other_versions)) { + std::cerr << "other-versions: must include version " + << "0x" << config.version << std::endl; + exit(EXIT_FAILURE); + } + } + + if (config.nstreams == 0) { + config.nstreams = config.requests.size(); + } + + TLSClientContext tls_ctx; + if (tls_ctx.init(private_key_file, cert_file) != 0) { + exit(EXIT_FAILURE); + } + + auto ev_loop_d = defer(ev_loop_destroy, EV_DEFAULT); + + auto keylog_filename = getenv("SSLKEYLOGFILE"); + if (keylog_filename) { + keylog_file.open(keylog_filename, std::ios_base::app); + if (keylog_file) { + tls_ctx.enable_keylog(); + } + } + + if (util::generate_secret(config.static_secret.data(), + config.static_secret.size()) != 0) { + std::cerr << "Unable to generate static secret" << std::endl; + exit(EXIT_FAILURE); + } + + auto client_chosen_version = config.version; + + for (;;) { + Client c(EV_DEFAULT, client_chosen_version, config.version); + + if (run(c, addr, port, tls_ctx) != 0) { + exit(EXIT_FAILURE); + } + + if (config.preferred_versions.empty()) { + break; + } + + auto &offered_versions = c.get_offered_versions(); + if (offered_versions.empty()) { + break; + } + + client_chosen_version = ngtcp2_select_version( + config.preferred_versions.data(), config.preferred_versions.size(), + offered_versions.data(), offered_versions.size()); + + if (client_chosen_version == 0) { + std::cerr << "Unable to select a version" << std::endl; + exit(EXIT_FAILURE); + } + + if (!config.quiet) { + std::cerr << "Client selected version " << std::hex << "0x" + << client_chosen_version << std::dec << std::endl; + } + } + + return EXIT_SUCCESS; +} diff --git a/examples/client.h b/examples/client.h new file mode 100644 index 0000000..d861917 --- /dev/null +++ b/examples/client.h @@ -0,0 +1,192 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef CLIENT_H +#define CLIENT_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <vector> +#include <deque> +#include <map> +#include <string_view> +#include <memory> + +#include <ngtcp2/ngtcp2.h> +#include <ngtcp2/ngtcp2_crypto.h> +#include <nghttp3/nghttp3.h> + +#include <ev.h> + +#include "client_base.h" +#include "tls_client_context.h" +#include "tls_client_session.h" +#include "network.h" +#include "shared.h" +#include "template.h" + +using namespace ngtcp2; + +struct Stream { + Stream(const Request &req, int64_t stream_id); + ~Stream(); + + int open_file(const std::string_view &path); + + Request req; + int64_t stream_id; + int fd; +}; + +class Client; + +struct Endpoint { + Address addr; + ev_io rev; + Client *client; + int fd; +}; + +class Client : public ClientBase { +public: + Client(struct ev_loop *loop, uint32_t client_chosen_version, + uint32_t original_version); + ~Client(); + + int init(int fd, const Address &local_addr, const Address &remote_addr, + const char *addr, const char *port, TLSClientContext &tls_ctx); + void disconnect(); + + int on_read(const Endpoint &ep); + int on_write(); + int write_streams(); + int feed_data(const Endpoint &ep, const sockaddr *sa, socklen_t salen, + const ngtcp2_pkt_info *pi, uint8_t *data, size_t datalen); + int handle_expiry(); + void update_timer(); + int handshake_completed(); + int handshake_confirmed(); + void recv_version_negotiation(const uint32_t *sv, size_t nsv); + + int send_packet(const Endpoint &ep, const ngtcp2_addr &remote_addr, + unsigned int ecn, const uint8_t *data, size_t datalen); + int on_stream_close(int64_t stream_id, uint64_t app_error_code); + int on_extend_max_streams(); + int handle_error(); + int make_stream_early(); + int change_local_addr(); + void start_change_local_addr_timer(); + int update_key(uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen); + int initiate_key_update(); + void start_key_update_timer(); + void start_delay_stream_timer(); + + int select_preferred_address(Address &selected_addr, + const ngtcp2_preferred_addr *paddr); + + std::optional<Endpoint *> endpoint_for(const Address &remote_addr); + + void set_remote_addr(const ngtcp2_addr &remote_addr); + + int setup_httpconn(); + int submit_http_request(const Stream *stream); + int recv_stream_data(uint32_t flags, int64_t stream_id, const uint8_t *data, + size_t datalen); + int acked_stream_data_offset(int64_t stream_id, uint64_t datalen); + void http_consume(int64_t stream_id, size_t nconsumed); + void http_write_data(int64_t stream_id, const uint8_t *data, size_t datalen); + int on_stream_reset(int64_t stream_id); + int on_stream_stop_sending(int64_t stream_id); + int extend_max_stream_data(int64_t stream_id, uint64_t max_data); + int stop_sending(int64_t stream_id, uint64_t app_error_code); + int reset_stream(int64_t stream_id, uint64_t app_error_code); + int http_stream_close(int64_t stream_id, uint64_t app_error_code); + + void on_send_blocked(const Endpoint &ep, const ngtcp2_addr &remote_addr, + unsigned int ecn, size_t datalen); + void start_wev_endpoint(const Endpoint &ep); + int send_blocked_packet(); + + const std::vector<uint32_t> &get_offered_versions() const; + + bool get_early_data() const; + void early_data_rejected(); + +private: + std::vector<Endpoint> endpoints_; + Address remote_addr_; + ev_io wev_; + ev_timer timer_; + ev_timer change_local_addr_timer_; + ev_timer key_update_timer_; + ev_timer delay_stream_timer_; + ev_signal sigintev_; + struct ev_loop *loop_; + std::map<int64_t, std::unique_ptr<Stream>> streams_; + std::vector<uint32_t> offered_versions_; + nghttp3_conn *httpconn_; + // addr_ is the server host address. + const char *addr_; + // port_ is the server port. + const char *port_; + // nstreams_done_ is the number of streams opened. + size_t nstreams_done_; + // nstreams_closed_ is the number of streams get closed. + size_t nstreams_closed_; + // nkey_update_ is the number of key update occurred. + size_t nkey_update_; + uint32_t client_chosen_version_; + uint32_t original_version_; + // early_data_ is true if client attempts to do 0RTT data transfer. + bool early_data_; + // should_exit_ is true if client should exit rather than waiting + // for timeout. + bool should_exit_; + // should_exit_on_handshake_confirmed_ is true if client should exit + // when handshake confirmed. + bool should_exit_on_handshake_confirmed_; + // handshake_confirmed_ gets true after handshake has been + // confirmed. + bool handshake_confirmed_; + + struct { + bool send_blocked; + // blocked field is effective only when send_blocked is true. + struct { + const Endpoint *endpoint; + Address remote_addr; + unsigned int ecn; + size_t datalen; + } blocked; + std::array<uint8_t, 64_k> data; + } tx_; +}; + +#endif // CLIENT_H diff --git a/examples/client_base.cc b/examples/client_base.cc new file mode 100644 index 0000000..aa35b4b --- /dev/null +++ b/examples/client_base.cc @@ -0,0 +1,202 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "client_base.h" + +#include <cassert> +#include <array> +#include <iostream> +#include <fstream> + +#include "debug.h" +#include "template.h" +#include "util.h" + +using namespace ngtcp2; +using namespace std::literals; + +extern Config config; + +static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { + auto c = static_cast<ClientBase *>(conn_ref->user_data); + return c->conn(); +} + +ClientBase::ClientBase() + : conn_ref_{get_conn, this}, qlog_(nullptr), conn_(nullptr) { + ngtcp2_connection_close_error_default(&last_error_); +} + +ClientBase::~ClientBase() { + if (conn_) { + ngtcp2_conn_del(conn_); + } + + if (qlog_) { + fclose(qlog_); + } +} + +int ClientBase::write_transport_params(const char *path, + const ngtcp2_transport_params *params) { + auto f = std::ofstream(path); + if (!f) { + return -1; + } + + f << "initial_max_streams_bidi=" << params->initial_max_streams_bidi << '\n' + << "initial_max_streams_uni=" << params->initial_max_streams_uni << '\n' + << "initial_max_stream_data_bidi_local=" + << params->initial_max_stream_data_bidi_local << '\n' + << "initial_max_stream_data_bidi_remote=" + << params->initial_max_stream_data_bidi_remote << '\n' + << "initial_max_stream_data_uni=" << params->initial_max_stream_data_uni + << '\n' + << "initial_max_data=" << params->initial_max_data << '\n' + << "active_connection_id_limit=" << params->active_connection_id_limit + << '\n' + << "max_datagram_frame_size=" << params->max_datagram_frame_size << '\n'; + + f.close(); + if (!f) { + return -1; + } + + return 0; +} + +int ClientBase::read_transport_params(const char *path, + ngtcp2_transport_params *params) { + auto f = std::ifstream(path); + if (!f) { + return -1; + } + + for (std::string line; std::getline(f, line);) { + if (util::istarts_with(line, "initial_max_streams_bidi="sv)) { + if (auto n = util::parse_uint(line.c_str() + + "initial_max_streams_bidi="sv.size()); + !n) { + return -1; + } else { + params->initial_max_streams_bidi = *n; + } + continue; + } + + if (util::istarts_with(line, "initial_max_streams_uni="sv)) { + if (auto n = util::parse_uint(line.c_str() + + "initial_max_streams_uni="sv.size()); + !n) { + return -1; + } else { + params->initial_max_streams_uni = *n; + } + continue; + } + + if (util::istarts_with(line, "initial_max_stream_data_bidi_local="sv)) { + if (auto n = util::parse_uint( + line.c_str() + "initial_max_stream_data_bidi_local="sv.size()); + !n) { + return -1; + } else { + params->initial_max_stream_data_bidi_local = *n; + } + continue; + } + + if (util::istarts_with(line, "initial_max_stream_data_bidi_remote="sv)) { + if (auto n = util::parse_uint( + line.c_str() + "initial_max_stream_data_bidi_remote="sv.size()); + !n) { + return -1; + } else { + params->initial_max_stream_data_bidi_remote = *n; + } + continue; + } + + if (util::istarts_with(line, "initial_max_stream_data_uni="sv)) { + if (auto n = util::parse_uint(line.c_str() + + "initial_max_stream_data_uni="sv.size()); + !n) { + return -1; + } else { + params->initial_max_stream_data_uni = *n; + } + continue; + } + + if (util::istarts_with(line, "initial_max_data="sv)) { + if (auto n = + util::parse_uint(line.c_str() + "initial_max_data="sv.size()); + !n) { + return -1; + } else { + params->initial_max_data = *n; + } + continue; + } + + if (util::istarts_with(line, "active_connection_id_limit="sv)) { + if (auto n = util::parse_uint(line.c_str() + + "active_connection_id_limit="sv.size()); + !n) { + return -1; + } else { + params->active_connection_id_limit = *n; + } + continue; + } + + if (util::istarts_with(line, "max_datagram_frame_size="sv)) { + if (auto n = util::parse_uint(line.c_str() + + "max_datagram_frame_size="sv.size()); + !n) { + return -1; + } else { + params->max_datagram_frame_size = *n; + } + continue; + } + } + + return 0; +} + +ngtcp2_conn *ClientBase::conn() const { return conn_; } + +void qlog_write_cb(void *user_data, uint32_t flags, const void *data, + size_t datalen) { + auto c = static_cast<ClientBase *>(user_data); + c->write_qlog(data, datalen); +} + +void ClientBase::write_qlog(const void *data, size_t datalen) { + assert(qlog_); + fwrite(data, 1, datalen, qlog_); +} + +ngtcp2_crypto_conn_ref *ClientBase::conn_ref() { return &conn_ref_; } diff --git a/examples/client_base.h b/examples/client_base.h new file mode 100644 index 0000000..ef364ea --- /dev/null +++ b/examples/client_base.h @@ -0,0 +1,212 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef CLIENT_BASE_H +#define CLIENT_BASE_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <vector> +#include <deque> +#include <string> +#include <string_view> +#include <functional> + +#include <ngtcp2/ngtcp2_crypto.h> + +#include "tls_client_session.h" +#include "network.h" +#include "shared.h" + +using namespace ngtcp2; + +struct Request { + std::string_view scheme; + std::string authority; + std::string path; +}; + +struct Config { + ngtcp2_cid dcid; + ngtcp2_cid scid; + bool scid_present; + // tx_loss_prob is probability of losing outgoing packet. + double tx_loss_prob; + // rx_loss_prob is probability of losing incoming packet. + double rx_loss_prob; + // fd is a file descriptor to read input for streams. + int fd; + // ciphers is the list of enabled ciphers. + const char *ciphers; + // groups is the list of supported groups. + const char *groups; + // nstreams is the number of streams to open. + size_t nstreams; + // data is the pointer to memory region which maps file denoted by + // fd. + uint8_t *data; + // datalen is the length of file denoted by fd. + size_t datalen; + // version is a QUIC version to use. + uint32_t version; + // quiet suppresses the output normally shown except for the error + // messages. + bool quiet; + // timeout is an idle timeout for QUIC connection. + ngtcp2_duration timeout; + // session_file is a path to a file to write, and read TLS session. + const char *session_file; + // tp_file is a path to a file to write, and read QUIC transport + // parameters. + const char *tp_file; + // show_secret is true if transport secrets should be printed out. + bool show_secret; + // change_local_addr is the duration after which client changes + // local address. + ngtcp2_duration change_local_addr; + // key_update is the duration after which client initiates key + // update. + ngtcp2_duration key_update; + // delay_stream is the duration after which client sends the first + // 1-RTT stream. + ngtcp2_duration delay_stream; + // nat_rebinding is true if simulated NAT rebinding is enabled. + bool nat_rebinding; + // no_preferred_addr is true if client do not follow preferred + // address offered by server. + bool no_preferred_addr; + std::string_view http_method; + // download is a path to a directory where a downloaded file is + // saved. If it is empty, no file is saved. + std::string_view download; + // requests contains URIs to request. + std::vector<Request> requests; + // no_quic_dump is true if hexdump of QUIC STREAM and CRYPTO data + // should be disabled. + bool no_quic_dump; + // no_http_dump is true if hexdump of HTTP response body should be + // disabled. + bool no_http_dump; + // qlog_file is the path to write qlog. + std::string_view qlog_file; + // qlog_dir is the path to directory where qlog is stored. qlog_dir + // and qlog_file are mutually exclusive. + std::string_view qlog_dir; + // max_data is the initial connection-level flow control window. + uint64_t max_data; + // max_stream_data_bidi_local is the initial stream-level flow + // control window for a bidirectional stream that the local endpoint + // initiates. + uint64_t max_stream_data_bidi_local; + // max_stream_data_bidi_remote is the initial stream-level flow + // control window for a bidirectional stream that the remote + // endpoint initiates. + uint64_t max_stream_data_bidi_remote; + // max_stream_data_uni is the initial stream-level flow control + // window for a unidirectional stream. + uint64_t max_stream_data_uni; + // max_streams_bidi is the number of the concurrent bidirectional + // streams. + uint64_t max_streams_bidi; + // max_streams_uni is the number of the concurrent unidirectional + // streams. + uint64_t max_streams_uni; + // max_window is the maximum connection-level flow control window + // size if auto-tuning is enabled. + uint64_t max_window; + // max_stream_window is the maximum stream-level flow control window + // size if auto-tuning is enabled. + uint64_t max_stream_window; + // exit_on_first_stream_close is the flag that if it is true, client + // exits when a first HTTP stream gets closed. It is not + // necessarily the same time when the underlying QUIC stream closes + // due to the QPACK synchronization. + bool exit_on_first_stream_close; + // exit_on_all_streams_close is the flag that if it is true, client + // exits when all HTTP streams get closed. + bool exit_on_all_streams_close; + // disable_early_data disables early data. + bool disable_early_data; + // static_secret is used to derive keying materials for Stateless + // Retry token. + std::array<uint8_t, 32> static_secret; + // cc_algo is the congestion controller algorithm. + ngtcp2_cc_algo cc_algo; + // token_file is a path to file to read or write token from + // NEW_TOKEN frame. + std::string_view token_file; + // sni is the value sent in TLS SNI, overriding DNS name of the + // remote host. + std::string_view sni; + // initial_rtt is an initial RTT. + ngtcp2_duration initial_rtt; + // max_udp_payload_size is the maximum UDP payload size that client + // transmits. + size_t max_udp_payload_size; + // handshake_timeout is the period of time before giving up QUIC + // connection establishment. + ngtcp2_duration handshake_timeout; + // preferred_versions includes QUIC versions in the order of + // preference. Client uses this field to select a version from the + // version set offered in Version Negotiation packet. + std::vector<uint32_t> preferred_versions; + // other_versions includes QUIC versions that are sent in + // other_versions field of version_information transport_parameter. + std::vector<uint32_t> other_versions; + // no_pmtud disables Path MTU Discovery. + bool no_pmtud; + // ack_thresh is the minimum number of the received ACK eliciting + // packets that triggers immediate acknowledgement. + size_t ack_thresh; +}; + +class ClientBase { +public: + ClientBase(); + ~ClientBase(); + + ngtcp2_conn *conn() const; + + int write_transport_params(const char *path, + const ngtcp2_transport_params *params); + int read_transport_params(const char *path, ngtcp2_transport_params *params); + + void write_qlog(const void *data, size_t datalen); + + ngtcp2_crypto_conn_ref *conn_ref(); + +protected: + ngtcp2_crypto_conn_ref conn_ref_; + TLSClientSession tls_session_; + FILE *qlog_; + ngtcp2_conn *conn_; + ngtcp2_connection_close_error last_error_; +}; + +void qlog_write_cb(void *user_data, uint32_t flags, const void *data, + size_t datalen); + +#endif // CLIENT_BASE_H diff --git a/examples/debug.cc b/examples/debug.cc new file mode 100644 index 0000000..98561fb --- /dev/null +++ b/examples/debug.cc @@ -0,0 +1,298 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "debug.h" + +#include <cassert> +#include <random> +#include <iostream> + +#include "util.h" + +using namespace std::literals; + +namespace ngtcp2 { + +namespace debug { + +namespace { +auto randgen = util::make_mt19937(); +} // namespace + +namespace { +auto *outfile = stderr; +} // namespace + +int handshake_completed(ngtcp2_conn *conn, void *user_data) { + fprintf(outfile, "QUIC handshake has completed\n"); + return 0; +} + +int handshake_confirmed(ngtcp2_conn *conn, void *user_data) { + fprintf(outfile, "QUIC handshake has been confirmed\n"); + return 0; +} + +bool packet_lost(double prob) { + auto p = std::uniform_real_distribution<>(0, 1)(randgen); + return p < prob; +} + +void print_crypto_data(ngtcp2_crypto_level crypto_level, const uint8_t *data, + size_t datalen) { + const char *crypto_level_str; + switch (crypto_level) { + case NGTCP2_CRYPTO_LEVEL_INITIAL: + crypto_level_str = "Initial"; + break; + case NGTCP2_CRYPTO_LEVEL_HANDSHAKE: + crypto_level_str = "Handshake"; + break; + case NGTCP2_CRYPTO_LEVEL_APPLICATION: + crypto_level_str = "Application"; + break; + default: + assert(0); + abort(); + } + fprintf(outfile, "Ordered CRYPTO data in %s crypto level\n", + crypto_level_str); + util::hexdump(outfile, data, datalen); +} + +void print_stream_data(int64_t stream_id, const uint8_t *data, size_t datalen) { + fprintf(outfile, "Ordered STREAM data stream_id=0x%" PRIx64 "\n", stream_id); + util::hexdump(outfile, data, datalen); +} + +void print_initial_secret(const uint8_t *data, size_t len) { + fprintf(outfile, "initial_secret=%s\n", util::format_hex(data, len).c_str()); +} + +void print_client_in_secret(const uint8_t *data, size_t len) { + fprintf(outfile, "client_in_secret=%s\n", + util::format_hex(data, len).c_str()); +} + +void print_server_in_secret(const uint8_t *data, size_t len) { + fprintf(outfile, "server_in_secret=%s\n", + util::format_hex(data, len).c_str()); +} + +void print_handshake_secret(const uint8_t *data, size_t len) { + fprintf(outfile, "handshake_secret=%s\n", + util::format_hex(data, len).c_str()); +} + +void print_client_hs_secret(const uint8_t *data, size_t len) { + fprintf(outfile, "client_hs_secret=%s\n", + util::format_hex(data, len).c_str()); +} + +void print_server_hs_secret(const uint8_t *data, size_t len) { + fprintf(outfile, "server_hs_secret=%s\n", + util::format_hex(data, len).c_str()); +} + +void print_client_0rtt_secret(const uint8_t *data, size_t len) { + fprintf(outfile, "client_0rtt_secret=%s\n", + util::format_hex(data, len).c_str()); +} + +void print_client_1rtt_secret(const uint8_t *data, size_t len) { + fprintf(outfile, "client_1rtt_secret=%s\n", + util::format_hex(data, len).c_str()); +} + +void print_server_1rtt_secret(const uint8_t *data, size_t len) { + fprintf(outfile, "server_1rtt_secret=%s\n", + util::format_hex(data, len).c_str()); +} + +void print_client_pp_key(const uint8_t *data, size_t len) { + fprintf(outfile, "+ client_pp_key=%s\n", util::format_hex(data, len).c_str()); +} + +void print_server_pp_key(const uint8_t *data, size_t len) { + fprintf(outfile, "+ server_pp_key=%s\n", util::format_hex(data, len).c_str()); +} + +void print_client_pp_iv(const uint8_t *data, size_t len) { + fprintf(outfile, "+ client_pp_iv=%s\n", util::format_hex(data, len).c_str()); +} + +void print_server_pp_iv(const uint8_t *data, size_t len) { + fprintf(outfile, "+ server_pp_iv=%s\n", util::format_hex(data, len).c_str()); +} + +void print_client_pp_hp(const uint8_t *data, size_t len) { + fprintf(outfile, "+ client_pp_hp=%s\n", util::format_hex(data, len).c_str()); +} + +void print_server_pp_hp(const uint8_t *data, size_t len) { + fprintf(outfile, "+ server_pp_hp=%s\n", util::format_hex(data, len).c_str()); +} + +void print_secrets(const uint8_t *secret, size_t secretlen, const uint8_t *key, + size_t keylen, const uint8_t *iv, size_t ivlen, + const uint8_t *hp, size_t hplen) { + std::cerr << "+ secret=" << util::format_hex(secret, secretlen) << "\n" + << "+ key=" << util::format_hex(key, keylen) << "\n" + << "+ iv=" << util::format_hex(iv, ivlen) << "\n" + << "+ hp=" << util::format_hex(hp, hplen) << std::endl; +} + +void print_secrets(const uint8_t *secret, size_t secretlen, const uint8_t *key, + size_t keylen, const uint8_t *iv, size_t ivlen) { + std::cerr << "+ secret=" << util::format_hex(secret, secretlen) << "\n" + << "+ key=" << util::format_hex(key, keylen) << "\n" + << "+ iv=" << util::format_hex(iv, ivlen) << std::endl; +} + +void print_hp_mask(const uint8_t *mask, size_t masklen, const uint8_t *sample, + size_t samplelen) { + fprintf(outfile, "mask=%s sample=%s\n", + util::format_hex(mask, masklen).c_str(), + util::format_hex(sample, samplelen).c_str()); +} + +void log_printf(void *user_data, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + fprintf(stderr, "\n"); +} + +void path_validation(const ngtcp2_path *path, + ngtcp2_path_validation_result res) { + auto local_addr = util::straddr( + reinterpret_cast<sockaddr *>(path->local.addr), path->local.addrlen); + auto remote_addr = util::straddr( + reinterpret_cast<sockaddr *>(path->remote.addr), path->remote.addrlen); + + std::cerr << "Path validation against path {local:" << local_addr + << ", remote:" << remote_addr << "} " + << (res == NGTCP2_PATH_VALIDATION_RESULT_SUCCESS ? "succeeded" + : "failed") + << std::endl; +} + +void print_http_begin_request_headers(int64_t stream_id) { + fprintf(outfile, "http: stream 0x%" PRIx64 " request headers started\n", + stream_id); +} + +void print_http_begin_response_headers(int64_t stream_id) { + fprintf(outfile, "http: stream 0x%" PRIx64 " response headers started\n", + stream_id); +} + +namespace { +void print_header(const uint8_t *name, size_t namelen, const uint8_t *value, + size_t valuelen, uint8_t flags) { + fprintf(outfile, "[%.*s: %.*s]%s\n", static_cast<int>(namelen), name, + static_cast<int>(valuelen), value, + (flags & NGHTTP3_NV_FLAG_NEVER_INDEX) ? "(sensitive)" : ""); +} +} // namespace + +namespace { +void print_header(const nghttp3_rcbuf *name, const nghttp3_rcbuf *value, + uint8_t flags) { + auto namebuf = nghttp3_rcbuf_get_buf(name); + auto valuebuf = nghttp3_rcbuf_get_buf(value); + print_header(namebuf.base, namebuf.len, valuebuf.base, valuebuf.len, flags); +} +} // namespace + +namespace { +void print_header(const nghttp3_nv &nv) { + print_header(nv.name, nv.namelen, nv.value, nv.valuelen, nv.flags); +} +} // namespace + +void print_http_header(int64_t stream_id, const nghttp3_rcbuf *name, + const nghttp3_rcbuf *value, uint8_t flags) { + fprintf(outfile, "http: stream 0x%" PRIx64 " ", stream_id); + print_header(name, value, flags); +} + +void print_http_end_headers(int64_t stream_id) { + fprintf(outfile, "http: stream 0x%" PRIx64 " headers ended\n", stream_id); +} + +void print_http_data(int64_t stream_id, const uint8_t *data, size_t datalen) { + fprintf(outfile, "http: stream 0x%" PRIx64 " body %zu bytes\n", stream_id, + datalen); + util::hexdump(outfile, data, datalen); +} + +void print_http_begin_trailers(int64_t stream_id) { + fprintf(outfile, "http: stream 0x%" PRIx64 " trailers started\n", stream_id); +} + +void print_http_end_trailers(int64_t stream_id) { + fprintf(outfile, "http: stream 0x%" PRIx64 " trailers ended\n", stream_id); +} + +void print_http_request_headers(int64_t stream_id, const nghttp3_nv *nva, + size_t nvlen) { + fprintf(outfile, "http: stream 0x%" PRIx64 " submit request headers\n", + stream_id); + for (size_t i = 0; i < nvlen; ++i) { + auto &nv = nva[i]; + print_header(nv); + } +} + +void print_http_response_headers(int64_t stream_id, const nghttp3_nv *nva, + size_t nvlen) { + fprintf(outfile, "http: stream 0x%" PRIx64 " submit response headers\n", + stream_id); + for (size_t i = 0; i < nvlen; ++i) { + auto &nv = nva[i]; + print_header(nv); + } +} + +std::string_view secret_title(ngtcp2_crypto_level level) { + switch (level) { + case NGTCP2_CRYPTO_LEVEL_EARLY: + return "early_traffic"sv; + case NGTCP2_CRYPTO_LEVEL_HANDSHAKE: + return "handshake_traffic"sv; + case NGTCP2_CRYPTO_LEVEL_APPLICATION: + return "application_traffic"sv; + default: + assert(0); + abort(); + } +} + +} // namespace debug + +} // namespace ngtcp2 diff --git a/examples/debug.h b/examples/debug.h new file mode 100644 index 0000000..5b31388 --- /dev/null +++ b/examples/debug.h @@ -0,0 +1,124 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef DEBUG_H +#define DEBUG_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#ifndef __STDC_FORMAT_MACROS +// For travis and PRIu64 +# define __STDC_FORMAT_MACROS +#endif // __STDC_FORMAT_MACROS + +#include <cinttypes> +#include <string_view> + +#include <ngtcp2/ngtcp2.h> +#include <nghttp3/nghttp3.h> + +namespace ngtcp2 { + +namespace debug { + +int handshake_completed(ngtcp2_conn *conn, void *user_data); + +int handshake_confirmed(ngtcp2_conn *conn, void *user_data); + +bool packet_lost(double prob); + +void print_crypto_data(ngtcp2_crypto_level crypto_level, const uint8_t *data, + size_t datalen); + +void print_stream_data(int64_t stream_id, const uint8_t *data, size_t datalen); + +void print_initial_secret(const uint8_t *data, size_t len); + +void print_client_in_secret(const uint8_t *data, size_t len); +void print_server_in_secret(const uint8_t *data, size_t len); + +void print_handshake_secret(const uint8_t *data, size_t len); + +void print_client_hs_secret(const uint8_t *data, size_t len); +void print_server_hs_secret(const uint8_t *data, size_t len); + +void print_client_0rtt_secret(const uint8_t *data, size_t len); + +void print_client_1rtt_secret(const uint8_t *data, size_t len); +void print_server_1rtt_secret(const uint8_t *data, size_t len); + +void print_client_pp_key(const uint8_t *data, size_t len); +void print_server_pp_key(const uint8_t *data, size_t len); + +void print_client_pp_iv(const uint8_t *data, size_t len); +void print_server_pp_iv(const uint8_t *data, size_t len); + +void print_client_pp_hp(const uint8_t *data, size_t len); +void print_server_pp_hp(const uint8_t *data, size_t len); + +void print_secrets(const uint8_t *secret, size_t secretlen, const uint8_t *key, + size_t keylen, const uint8_t *iv, size_t ivlen, + const uint8_t *hp, size_t hplen); + +void print_secrets(const uint8_t *secret, size_t secretlen, const uint8_t *key, + size_t keylen, const uint8_t *iv, size_t ivlen); + +void print_hp_mask(const uint8_t *mask, size_t masklen, const uint8_t *sample, + size_t samplelen); + +void log_printf(void *user_data, const char *fmt, ...); + +void path_validation(const ngtcp2_path *path, + ngtcp2_path_validation_result res); + +void print_http_begin_request_headers(int64_t stream_id); + +void print_http_begin_response_headers(int64_t stream_id); + +void print_http_header(int64_t stream_id, const nghttp3_rcbuf *name, + const nghttp3_rcbuf *value, uint8_t flags); + +void print_http_end_headers(int64_t stream_id); + +void print_http_data(int64_t stream_id, const uint8_t *data, size_t datalen); + +void print_http_begin_trailers(int64_t stream_id); + +void print_http_end_trailers(int64_t stream_id); + +void print_http_request_headers(int64_t stream_id, const nghttp3_nv *nva, + size_t nvlen); + +void print_http_response_headers(int64_t stream_id, const nghttp3_nv *nva, + size_t nvlen); + +std::string_view secret_title(ngtcp2_crypto_level level); + +} // namespace debug + +} // namespace ngtcp2 + +#endif // DEBUG_H diff --git a/examples/examplestest.cc b/examples/examplestest.cc new file mode 100644 index 0000000..6487f81 --- /dev/null +++ b/examples/examplestest.cc @@ -0,0 +1,84 @@ +/* + * ngtcp2 + * + * Copyright (c) 2018 ngtcp2 contributors + * Copyright (c) 2013 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <stdio.h> +#include <CUnit/Basic.h> +// include test cases' include files here +#include "util_test.h" + +static int init_suite1(void) { return 0; } + +static int clean_suite1(void) { return 0; } + +int main(int argc, char *argv[]) { + CU_pSuite pSuite = nullptr; + unsigned int num_tests_failed; + + // initialize the CUnit test registry + if (CUE_SUCCESS != CU_initialize_registry()) + return CU_get_error(); + + // add a suite to the registry + pSuite = CU_add_suite("TestSuite", init_suite1, clean_suite1); + if (nullptr == pSuite) { + CU_cleanup_registry(); + return CU_get_error(); + } + + // add the tests to the suite + if (!CU_add_test(pSuite, "util_format_durationf", + ngtcp2::test_util_format_durationf) || + !CU_add_test(pSuite, "util_format_uint", ngtcp2::test_util_format_uint) || + !CU_add_test(pSuite, "util_format_uint_iec", + ngtcp2::test_util_format_uint_iec) || + !CU_add_test(pSuite, "util_format_duration", + ngtcp2::test_util_format_duration) || + !CU_add_test(pSuite, "util_parse_uint", ngtcp2::test_util_parse_uint) || + !CU_add_test(pSuite, "util_parse_uint_iec", + ngtcp2::test_util_parse_uint_iec) || + !CU_add_test(pSuite, "util_parse_duration", + ngtcp2::test_util_parse_duration) || + !CU_add_test(pSuite, "util_normalize_path", + ngtcp2::test_util_normalize_path)) { + CU_cleanup_registry(); + return CU_get_error(); + } + + // Run all tests using the CUnit Basic interface + CU_basic_set_mode(CU_BRM_VERBOSE); + CU_basic_run_tests(); + num_tests_failed = CU_get_number_of_tests_failed(); + CU_cleanup_registry(); + if (CU_get_error() == CUE_SUCCESS) { + return num_tests_failed; + } else { + printf("CUnit Error: %s\n", CU_get_error_msg()); + return CU_get_error(); + } +} diff --git a/examples/gtlssimpleclient.c b/examples/gtlssimpleclient.c new file mode 100644 index 0000000..60c0121 --- /dev/null +++ b/examples/gtlssimpleclient.c @@ -0,0 +1,720 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021-2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif /* HAVE_CONFIG_H */ + +#include <time.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> +#include <arpa/inet.h> +#include <string.h> +#include <stdio.h> +#include <errno.h> + +#include <ngtcp2/ngtcp2.h> +#include <ngtcp2/ngtcp2_crypto.h> +#include <ngtcp2/ngtcp2_crypto_gnutls.h> + +#include <gnutls/crypto.h> +#include <gnutls/gnutls.h> + +#include <ev.h> + +#define REMOTE_HOST "127.0.0.1" +#define REMOTE_PORT "4433" +#define ALPN "hq-interop" +#define MESSAGE "GET /\r\n" + +/* + * Example 1: Handshake with www.google.com + * + * #define REMOTE_HOST "www.google.com" + * #define REMOTE_PORT "443" + * #define ALPN "h3" + * + * and undefine MESSAGE macro. + */ + +static uint64_t timestamp(void) { + struct timespec tp; + + if (clock_gettime(CLOCK_MONOTONIC, &tp) != 0) { + fprintf(stderr, "clock_gettime: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + + return (uint64_t)tp.tv_sec * NGTCP2_SECONDS + (uint64_t)tp.tv_nsec; +} + +static int create_sock(struct sockaddr *addr, socklen_t *paddrlen, + const char *host, const char *port) { + struct addrinfo hints = {0}; + struct addrinfo *res, *rp; + int rv; + int fd = -1; + + hints.ai_flags = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + + rv = getaddrinfo(host, port, &hints, &res); + if (rv != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); + return -1; + } + + for (rp = res; rp; rp = rp->ai_next) { + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) { + continue; + } + + break; + } + + if (fd == -1) { + goto end; + } + + *paddrlen = rp->ai_addrlen; + memcpy(addr, rp->ai_addr, rp->ai_addrlen); + +end: + freeaddrinfo(res); + + return fd; +} + +static int connect_sock(struct sockaddr *local_addr, socklen_t *plocal_addrlen, + int fd, const struct sockaddr *remote_addr, + size_t remote_addrlen) { + socklen_t len; + + if (connect(fd, remote_addr, (socklen_t)remote_addrlen) != 0) { + fprintf(stderr, "connect: %s\n", strerror(errno)); + return -1; + } + + len = *plocal_addrlen; + + if (getsockname(fd, local_addr, &len) == -1) { + fprintf(stderr, "getsockname: %s\n", strerror(errno)); + return -1; + } + + *plocal_addrlen = len; + + return 0; +} + +struct client { + ngtcp2_crypto_conn_ref conn_ref; + int fd; + struct sockaddr_storage local_addr; + socklen_t local_addrlen; + gnutls_certificate_credentials_t cred; + gnutls_session_t session; + ngtcp2_conn *conn; + + struct { + int64_t stream_id; + const uint8_t *data; + size_t datalen; + size_t nwrite; + } stream; + + ngtcp2_connection_close_error last_error; + + ev_io rev; + ev_timer timer; +}; + +static int hook_func(gnutls_session_t session, unsigned int htype, + unsigned when, unsigned int incoming, + const gnutls_datum_t *msg) { + (void)session; + (void)htype; + (void)when; + (void)incoming; + (void)msg; + /* we could save session data here */ + + return 0; +} + +static int numeric_host_family(const char *hostname, int family) { + uint8_t dst[sizeof(struct in6_addr)]; + return inet_pton(family, hostname, dst) == 1; +} + +static int numeric_host(const char *hostname) { + return numeric_host_family(hostname, AF_INET) || + numeric_host_family(hostname, AF_INET6); +} + +static const char priority[] = + "NORMAL:-VERS-ALL:+VERS-TLS1.3:-CIPHER-ALL:+AES-128-GCM:+AES-256-GCM:" + "+CHACHA20-POLY1305:+AES-128-CCM:-GROUP-ALL:+GROUP-SECP256R1:+GROUP-X25519:" + "+GROUP-SECP384R1:" + "+GROUP-SECP521R1:%DISABLE_TLS13_COMPAT_MODE"; + +static const gnutls_datum_t alpn = {(uint8_t *)ALPN, sizeof(ALPN) - 1}; + +static int client_gnutls_init(struct client *c) { + int rv = gnutls_certificate_allocate_credentials(&c->cred); + + if (rv == 0) + rv = gnutls_certificate_set_x509_system_trust(c->cred); + if (rv < 0) { + fprintf(stderr, "cred init failed: %d: %s\n", rv, gnutls_strerror(rv)); + return -1; + } + + rv = gnutls_init(&c->session, GNUTLS_CLIENT | GNUTLS_ENABLE_EARLY_DATA | + GNUTLS_NO_END_OF_EARLY_DATA); + if (rv != 0) { + fprintf(stderr, "gnutls_init: %s\n", gnutls_strerror(rv)); + return -1; + } + + if (ngtcp2_crypto_gnutls_configure_client_session(c->session) != 0) { + fprintf(stderr, "ngtcp2_crypto_gnutls_configure_client_session failed\n"); + return -1; + } + + rv = gnutls_priority_set_direct(c->session, priority, NULL); + if (rv != 0) { + fprintf(stderr, "gnutls_priority_set_direct: %s\n", gnutls_strerror(rv)); + return -1; + } + + gnutls_handshake_set_hook_function(c->session, GNUTLS_HANDSHAKE_ANY, + GNUTLS_HOOK_POST, hook_func); + + gnutls_session_set_ptr(c->session, &c->conn_ref); + + rv = gnutls_credentials_set(c->session, GNUTLS_CRD_CERTIFICATE, c->cred); + + if (rv != 0) { + fprintf(stderr, "gnutls_credentials_set: %s\n", gnutls_strerror(rv)); + return -1; + } + + gnutls_alpn_set_protocols(c->session, &alpn, 1, GNUTLS_ALPN_MANDATORY); + + if (!numeric_host(REMOTE_HOST)) { + gnutls_server_name_set(c->session, GNUTLS_NAME_DNS, REMOTE_HOST, + strlen(REMOTE_HOST)); + } else { + gnutls_server_name_set(c->session, GNUTLS_NAME_DNS, "localhost", + strlen("localhost")); + } + + return 0; +} + +static void rand_cb(uint8_t *dest, size_t destlen, + const ngtcp2_rand_ctx *rand_ctx) { + (void)rand_ctx; + + (void)gnutls_rnd(GNUTLS_RND_RANDOM, dest, destlen); +} + +static int get_new_connection_id_cb(ngtcp2_conn *conn, ngtcp2_cid *cid, + uint8_t *token, size_t cidlen, + void *user_data) { + (void)conn; + (void)user_data; + + if (gnutls_rnd(GNUTLS_RND_RANDOM, cid->data, cidlen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + cid->datalen = cidlen; + + if (gnutls_rnd(GNUTLS_RND_RANDOM, token, NGTCP2_STATELESS_RESET_TOKENLEN) != + 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static int extend_max_local_streams_bidi(ngtcp2_conn *conn, + uint64_t max_streams, + void *user_data) { +#ifdef MESSAGE + struct client *c = user_data; + int rv; + int64_t stream_id; + (void)max_streams; + + if (c->stream.stream_id != -1) { + return 0; + } + + rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL); + if (rv != 0) { + return 0; + } + + c->stream.stream_id = stream_id; + c->stream.data = (const uint8_t *)MESSAGE; + c->stream.datalen = sizeof(MESSAGE) - 1; + + return 0; +#else /* !MESSAGE */ + (void)conn; + (void)max_streams; + (void)user_data; + + return 0; +#endif /* !MESSAGE */ +} + +static void log_printf(void *user_data, const char *fmt, ...) { + va_list ap; + (void)user_data; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + fprintf(stderr, "\n"); +} + +static int client_quic_init(struct client *c, + const struct sockaddr *remote_addr, + socklen_t remote_addrlen, + const struct sockaddr *local_addr, + socklen_t local_addrlen) { + ngtcp2_path path = { + { + (struct sockaddr *)local_addr, + local_addrlen, + }, + { + (struct sockaddr *)remote_addr, + remote_addrlen, + }, + NULL, + }; + ngtcp2_callbacks callbacks = { + ngtcp2_crypto_client_initial_cb, + NULL, /* recv_client_initial */ + ngtcp2_crypto_recv_crypto_data_cb, + NULL, /* handshake_completed */ + NULL, /* recv_version_negotiation */ + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + NULL, /* recv_stream_data */ + NULL, /* acked_stream_data_offset */ + NULL, /* stream_open */ + NULL, /* stream_close */ + NULL, /* recv_stateless_reset */ + ngtcp2_crypto_recv_retry_cb, + extend_max_local_streams_bidi, + NULL, /* extend_max_local_streams_uni */ + rand_cb, + get_new_connection_id_cb, + NULL, /* remove_connection_id */ + ngtcp2_crypto_update_key_cb, + NULL, /* path_validation */ + NULL, /* select_preferred_address */ + NULL, /* stream_reset */ + NULL, /* extend_max_remote_streams_bidi */ + NULL, /* extend_max_remote_streams_uni */ + NULL, /* extend_max_stream_data */ + NULL, /* dcid_status */ + NULL, /* handshake_confirmed */ + NULL, /* recv_new_token */ + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + NULL, /* recv_datagram */ + NULL, /* ack_datagram */ + NULL, /* lost_datagram */ + ngtcp2_crypto_get_path_challenge_data_cb, + NULL, /* stream_stop_sending */ + ngtcp2_crypto_version_negotiation_cb, + NULL, /* recv_rx_key */ + NULL, /* recv_tx_key */ + NULL, /* early_data_rejected */ + }; + ngtcp2_cid dcid, scid; + ngtcp2_settings settings; + ngtcp2_transport_params params; + int rv; + + dcid.datalen = NGTCP2_MIN_INITIAL_DCIDLEN; + if (gnutls_rnd(GNUTLS_RND_RANDOM, dcid.data, dcid.datalen) != 0) { + fprintf(stderr, "gnutls_rnd failed\n"); + return -1; + } + + scid.datalen = 8; + if (gnutls_rnd(GNUTLS_RND_RANDOM, scid.data, scid.datalen) != 0) { + fprintf(stderr, "gnutls_rnd failed\n"); + return -1; + } + + ngtcp2_settings_default(&settings); + + settings.initial_ts = timestamp(); + settings.log_printf = log_printf; + + ngtcp2_transport_params_default(¶ms); + + params.initial_max_streams_uni = 3; + params.initial_max_stream_data_bidi_local = 128 * 1024; + params.initial_max_data = 1024 * 1024; + + rv = + ngtcp2_conn_client_new(&c->conn, &dcid, &scid, &path, NGTCP2_PROTO_VER_V1, + &callbacks, &settings, ¶ms, NULL, c); + if (rv != 0) { + fprintf(stderr, "ngtcp2_conn_client_new: %s\n", ngtcp2_strerror(rv)); + return -1; + } + + ngtcp2_conn_set_tls_native_handle(c->conn, c->session); + + return 0; +} + +static int client_read(struct client *c) { + uint8_t buf[65536]; + struct sockaddr_storage addr; + struct iovec iov = {buf, sizeof(buf)}; + struct msghdr msg = {0}; + ssize_t nread; + ngtcp2_path path; + ngtcp2_pkt_info pi = {0}; + int rv; + + msg.msg_name = &addr; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + for (;;) { + msg.msg_namelen = sizeof(addr); + + nread = recvmsg(c->fd, &msg, MSG_DONTWAIT); + + if (nread == -1) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + fprintf(stderr, "recvmsg: %s\n", strerror(errno)); + } + + break; + } + + path.local.addrlen = c->local_addrlen; + path.local.addr = (struct sockaddr *)&c->local_addr; + path.remote.addrlen = msg.msg_namelen; + path.remote.addr = msg.msg_name; + + rv = ngtcp2_conn_read_pkt(c->conn, &path, &pi, buf, (size_t)nread, + timestamp()); + if (rv != 0) { + fprintf(stderr, "ngtcp2_conn_read_pkt: %s\n", ngtcp2_strerror(rv)); + if (!c->last_error.error_code) { + if (rv == NGTCP2_ERR_CRYPTO) { + ngtcp2_connection_close_error_set_transport_error_tls_alert( + &c->last_error, ngtcp2_conn_get_tls_alert(c->conn), NULL, 0); + } else { + ngtcp2_connection_close_error_set_transport_error_liberr( + &c->last_error, rv, NULL, 0); + } + } + return -1; + } + } + + return 0; +} + +static int client_send_packet(struct client *c, const uint8_t *data, + size_t datalen) { + struct iovec iov = {(uint8_t *)data, datalen}; + struct msghdr msg = {0}; + ssize_t nwrite; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + do { + nwrite = sendmsg(c->fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + fprintf(stderr, "sendmsg: %s\n", strerror(errno)); + + return -1; + } + + return 0; +} + +static size_t client_get_message(struct client *c, int64_t *pstream_id, + int *pfin, ngtcp2_vec *datav, + size_t datavcnt) { + if (datavcnt == 0) { + return 0; + } + + if (c->stream.stream_id != -1 && c->stream.nwrite < c->stream.datalen) { + *pstream_id = c->stream.stream_id; + *pfin = 1; + datav->base = (uint8_t *)c->stream.data + c->stream.nwrite; + datav->len = c->stream.datalen - c->stream.nwrite; + return 1; + } + + *pstream_id = -1; + *pfin = 0; + datav->base = NULL; + datav->len = 0; + + return 0; +} + +static int client_write_streams(struct client *c) { + ngtcp2_tstamp ts = timestamp(); + ngtcp2_pkt_info pi; + ngtcp2_ssize nwrite; + uint8_t buf[1280]; + ngtcp2_path_storage ps; + ngtcp2_vec datav; + size_t datavcnt; + int64_t stream_id; + ngtcp2_ssize wdatalen; + uint32_t flags; + int fin; + + ngtcp2_path_storage_zero(&ps); + + for (;;) { + datavcnt = client_get_message(c, &stream_id, &fin, &datav, 1); + + flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + if (fin) { + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + nwrite = ngtcp2_conn_writev_stream(c->conn, &ps.path, &pi, buf, sizeof(buf), + &wdatalen, flags, stream_id, &datav, + datavcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_WRITE_MORE: + c->stream.nwrite += (size_t)wdatalen; + continue; + default: + fprintf(stderr, "ngtcp2_conn_writev_stream: %s\n", + ngtcp2_strerror((int)nwrite)); + ngtcp2_connection_close_error_set_transport_error_liberr( + &c->last_error, (int)nwrite, NULL, 0); + return -1; + } + } + + if (nwrite == 0) { + return 0; + } + + if (wdatalen > 0) { + c->stream.nwrite += (size_t)wdatalen; + } + + if (client_send_packet(c, buf, (size_t)nwrite) != 0) { + break; + } + } + + return 0; +} + +static int client_write(struct client *c) { + ngtcp2_tstamp expiry, now; + ev_tstamp t; + + if (client_write_streams(c) != 0) { + return -1; + } + + expiry = ngtcp2_conn_get_expiry(c->conn); + now = timestamp(); + + t = expiry < now ? 1e-9 : (ev_tstamp)(expiry - now) / NGTCP2_SECONDS; + + c->timer.repeat = t; + ev_timer_again(EV_DEFAULT, &c->timer); + + return 0; +} + +static int client_handle_expiry(struct client *c) { + int rv = ngtcp2_conn_handle_expiry(c->conn, timestamp()); + if (rv != 0) { + fprintf(stderr, "ngtcp2_conn_handle_expiry: %s\n", ngtcp2_strerror(rv)); + return -1; + } + + return 0; +} + +static void client_close(struct client *c) { + ngtcp2_ssize nwrite; + ngtcp2_pkt_info pi; + ngtcp2_path_storage ps; + uint8_t buf[1280]; + + if (ngtcp2_conn_is_in_closing_period(c->conn) || + ngtcp2_conn_is_in_draining_period(c->conn)) { + goto fin; + } + + ngtcp2_path_storage_zero(&ps); + + nwrite = ngtcp2_conn_write_connection_close( + c->conn, &ps.path, &pi, buf, sizeof(buf), &c->last_error, timestamp()); + if (nwrite < 0) { + fprintf(stderr, "ngtcp2_conn_write_connection_close: %s\n", + ngtcp2_strerror((int)nwrite)); + goto fin; + } + + client_send_packet(c, buf, (size_t)nwrite); + +fin: + ev_break(EV_DEFAULT, EVBREAK_ALL); +} + +static void read_cb(struct ev_loop *loop, ev_io *w, int revents) { + struct client *c = w->data; + (void)loop; + (void)revents; + + if (client_read(c) != 0) { + client_close(c); + return; + } + + if (client_write(c) != 0) { + client_close(c); + } +} + +static void timer_cb(struct ev_loop *loop, ev_timer *w, int revents) { + struct client *c = w->data; + (void)loop; + (void)revents; + + if (client_handle_expiry(c) != 0) { + client_close(c); + return; + } + + if (client_write(c) != 0) { + client_close(c); + } +} + +static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { + struct client *c = conn_ref->user_data; + return c->conn; +} + +static int client_init(struct client *c) { + struct sockaddr_storage remote_addr, local_addr; + socklen_t remote_addrlen, local_addrlen = sizeof(local_addr); + + memset(c, 0, sizeof(*c)); + + ngtcp2_connection_close_error_default(&c->last_error); + + c->fd = create_sock((struct sockaddr *)&remote_addr, &remote_addrlen, + REMOTE_HOST, REMOTE_PORT); + if (c->fd == -1) { + return -1; + } + + if (connect_sock((struct sockaddr *)&local_addr, &local_addrlen, c->fd, + (struct sockaddr *)&remote_addr, remote_addrlen) != 0) { + return -1; + } + + memcpy(&c->local_addr, &local_addr, sizeof(c->local_addr)); + c->local_addrlen = local_addrlen; + + if (client_gnutls_init(c) != 0) { + return -1; + } + + if (client_quic_init(c, (struct sockaddr *)&remote_addr, remote_addrlen, + (struct sockaddr *)&local_addr, local_addrlen) != 0) { + return -1; + } + + c->stream.stream_id = -1; + + c->conn_ref.get_conn = get_conn; + c->conn_ref.user_data = c; + + ev_io_init(&c->rev, read_cb, c->fd, EV_READ); + c->rev.data = c; + ev_io_start(EV_DEFAULT, &c->rev); + + ev_timer_init(&c->timer, timer_cb, 0., 0.); + c->timer.data = c; + + return 0; +} + +static void client_free(struct client *c) { + ngtcp2_conn_del(c->conn); + gnutls_deinit(c->session); + gnutls_certificate_free_credentials(c->cred); +} + +int main(void) { + struct client c; + + if (client_init(&c) != 0) { + exit(EXIT_FAILURE); + } + + if (client_write(&c) != 0) { + exit(EXIT_FAILURE); + } + + ev_run(EV_DEFAULT, 0); + + client_free(&c); + + return 0; +} diff --git a/examples/h09client.cc b/examples/h09client.cc new file mode 100644 index 0000000..4df12ca --- /dev/null +++ b/examples/h09client.cc @@ -0,0 +1,2604 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include <cstdlib> +#include <cassert> +#include <cerrno> +#include <iostream> +#include <algorithm> +#include <memory> +#include <fstream> +#include <iomanip> + +#include <unistd.h> +#include <getopt.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <sys/socket.h> +#include <netdb.h> +#include <sys/mman.h> + +#include <http-parser/http_parser.h> + +#include "h09client.h" +#include "network.h" +#include "debug.h" +#include "util.h" +#include "shared.h" + +using namespace ngtcp2; +using namespace std::literals; + +namespace { +auto randgen = util::make_mt19937(); +} // namespace + +namespace { +constexpr size_t max_preferred_versionslen = 4; +} // namespace + +Config config{}; + +Stream::Stream(const Request &req, int64_t stream_id) + : req(req), stream_id(stream_id), fd(-1) { + nghttp3_buf_init(&reqbuf); +} + +Stream::~Stream() { + if (fd != -1) { + close(fd); + } +} + +int Stream::open_file(const std::string_view &path) { + assert(fd == -1); + + std::string_view filename; + + auto it = std::find(std::rbegin(path), std::rend(path), '/').base(); + if (it == std::end(path)) { + filename = "index.html"sv; + } else { + filename = std::string_view{it, static_cast<size_t>(std::end(path) - it)}; + if (filename == ".."sv || filename == "."sv) { + std::cerr << "Invalid file name: " << filename << std::endl; + return -1; + } + } + + auto fname = std::string{config.download}; + fname += '/'; + fname += filename; + + fd = open(fname.c_str(), O_WRONLY | O_CREAT | O_TRUNC, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (fd == -1) { + std::cerr << "open: Could not open file " << fname << ": " + << strerror(errno) << std::endl; + return -1; + } + + return 0; +} + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + auto c = static_cast<Client *>(w->data); + + c->on_write(); +} +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + auto ep = static_cast<Endpoint *>(w->data); + auto c = ep->client; + + if (c->on_read(*ep) != 0) { + return; + } + + c->on_write(); +} +} // namespace + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + int rv; + auto c = static_cast<Client *>(w->data); + + rv = c->handle_expiry(); + if (rv != 0) { + return; + } + + c->on_write(); +} +} // namespace + +namespace { +void change_local_addrcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto c = static_cast<Client *>(w->data); + + c->change_local_addr(); +} +} // namespace + +namespace { +void key_updatecb(struct ev_loop *loop, ev_timer *w, int revents) { + auto c = static_cast<Client *>(w->data); + + if (c->initiate_key_update() != 0) { + c->disconnect(); + } +} +} // namespace + +namespace { +void delay_streamcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto c = static_cast<Client *>(w->data); + + ev_timer_stop(loop, w); + c->on_extend_max_streams(); + c->on_write(); +} +} // namespace + +namespace { +void siginthandler(struct ev_loop *loop, ev_signal *w, int revents) { + ev_break(loop, EVBREAK_ALL); +} +} // namespace + +Client::Client(struct ev_loop *loop, uint32_t client_chosen_version, + uint32_t original_version) + : remote_addr_{}, + loop_(loop), + addr_(nullptr), + port_(nullptr), + nstreams_done_(0), + nstreams_closed_(0), + nkey_update_(0), + client_chosen_version_(client_chosen_version), + original_version_(original_version), + early_data_(false), + should_exit_(false), + should_exit_on_handshake_confirmed_(false), + handshake_confirmed_(false), + tx_{} { + ev_io_init(&wev_, writecb, 0, EV_WRITE); + wev_.data = this; + ev_timer_init(&timer_, timeoutcb, 0., 0.); + timer_.data = this; + ev_timer_init(&change_local_addr_timer_, change_local_addrcb, + static_cast<double>(config.change_local_addr) / NGTCP2_SECONDS, + 0.); + change_local_addr_timer_.data = this; + ev_timer_init(&key_update_timer_, key_updatecb, + static_cast<double>(config.key_update) / NGTCP2_SECONDS, 0.); + key_update_timer_.data = this; + ev_timer_init(&delay_stream_timer_, delay_streamcb, + static_cast<double>(config.delay_stream) / NGTCP2_SECONDS, 0.); + delay_stream_timer_.data = this; + ev_signal_init(&sigintev_, siginthandler, SIGINT); +} + +Client::~Client() { disconnect(); } + +void Client::disconnect() { + tx_.send_blocked = false; + + handle_error(); + + config.tx_loss_prob = 0; + + ev_timer_stop(loop_, &delay_stream_timer_); + ev_timer_stop(loop_, &key_update_timer_); + ev_timer_stop(loop_, &change_local_addr_timer_); + ev_timer_stop(loop_, &timer_); + + ev_io_stop(loop_, &wev_); + + for (auto &ep : endpoints_) { + ev_io_stop(loop_, &ep.rev); + close(ep.fd); + } + + endpoints_.clear(); + + ev_signal_stop(loop_, &sigintev_); +} + +namespace { +int recv_crypto_data(ngtcp2_conn *conn, ngtcp2_crypto_level crypto_level, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_crypto_data(crypto_level, data, datalen); + } + + return ngtcp2_crypto_recv_crypto_data_cb(conn, crypto_level, offset, data, + datalen, user_data); +} +} // namespace + +namespace { +int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data, void *stream_user_data) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_stream_data(stream_id, data, datalen); + } + + auto c = static_cast<Client *>(user_data); + + if (c->recv_stream_data(flags, stream_id, data, datalen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int acked_stream_data_offset(ngtcp2_conn *conn, int64_t stream_id, + uint64_t offset, uint64_t datalen, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + if (c->acked_stream_data_offset(stream_id, offset, datalen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +namespace { +int handshake_completed(ngtcp2_conn *conn, void *user_data) { + auto c = static_cast<Client *>(user_data); + + if (!config.quiet) { + debug::handshake_completed(conn, user_data); + } + + if (c->handshake_completed() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Client::handshake_completed() { + if (early_data_ && !tls_session_.get_early_data_accepted()) { + if (!config.quiet) { + std::cerr << "Early data was rejected by server" << std::endl; + } + + // Some TLS backends only report early data rejection after + // handshake completion (e.g., OpenSSL). For TLS backends which + // report it early (e.g., BoringSSL and PicoTLS), the following + // functions are noop. + if (auto rv = ngtcp2_conn_early_data_rejected(conn_); rv != 0) { + std::cerr << "ngtcp2_conn_early_data_rejected: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + } + + if (!config.quiet) { + std::cerr << "Negotiated cipher suite is " << tls_session_.get_cipher_name() + << std::endl; + std::cerr << "Negotiated ALPN is " << tls_session_.get_selected_alpn() + << std::endl; + } + + if (config.tp_file) { + auto params = ngtcp2_conn_get_remote_transport_params(conn_); + + if (write_transport_params(config.tp_file, params) != 0) { + std::cerr << "Could not write transport parameters in " << config.tp_file + << std::endl; + } + } + + return 0; +} + +namespace { +int handshake_confirmed(ngtcp2_conn *conn, void *user_data) { + auto c = static_cast<Client *>(user_data); + + if (!config.quiet) { + debug::handshake_confirmed(conn, user_data); + } + + if (c->handshake_confirmed() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Client::handshake_confirmed() { + handshake_confirmed_ = true; + + if (config.change_local_addr) { + start_change_local_addr_timer(); + } + if (config.key_update) { + start_key_update_timer(); + } + if (config.delay_stream) { + start_delay_stream_timer(); + } + + if (should_exit_on_handshake_confirmed_) { + should_exit_ = true; + } + + return 0; +} + +namespace { +int recv_version_negotiation(ngtcp2_conn *conn, const ngtcp2_pkt_hd *hd, + const uint32_t *sv, size_t nsv, void *user_data) { + auto c = static_cast<Client *>(user_data); + + c->recv_version_negotiation(sv, nsv); + + return 0; +} +} // namespace + +void Client::recv_version_negotiation(const uint32_t *sv, size_t nsv) { + offered_versions_.resize(nsv); + std::copy_n(sv, nsv, std::begin(offered_versions_)); +} + +namespace { +int stream_close(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + + if (c->on_stream_close(stream_id, app_error_code) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int extend_max_streams_bidi(ngtcp2_conn *conn, uint64_t max_streams, + void *user_data) { + auto c = static_cast<Client *>(user_data); + + if (c->on_extend_max_streams() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +void rand(uint8_t *dest, size_t destlen, const ngtcp2_rand_ctx *rand_ctx) { + auto dis = std::uniform_int_distribution<uint8_t>(0, 255); + std::generate(dest, dest + destlen, [&dis]() { return dis(randgen); }); +} +} // namespace + +namespace { +int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, uint8_t *token, + size_t cidlen, void *user_data) { + if (util::generate_secure_random(cid->data, cidlen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + cid->datalen = cidlen; + if (ngtcp2_crypto_generate_stateless_reset_token( + token, config.static_secret.data(), config.static_secret.size(), + cid) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int do_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, + const ngtcp2_crypto_cipher_ctx *hp_ctx, const uint8_t *sample) { + if (ngtcp2_crypto_hp_mask(dest, hp, hp_ctx, sample) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + if (!config.quiet && config.show_secret) { + debug::print_hp_mask(dest, NGTCP2_HP_MASKLEN, sample, NGTCP2_HP_SAMPLELEN); + } + + return 0; +} +} // namespace + +namespace { +int update_key(ngtcp2_conn *conn, uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen, + void *user_data) { + auto c = static_cast<Client *>(user_data); + + if (c->update_key(rx_secret, tx_secret, rx_aead_ctx, rx_iv, tx_aead_ctx, + tx_iv, current_rx_secret, current_tx_secret, + secretlen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int path_validation(ngtcp2_conn *conn, uint32_t flags, const ngtcp2_path *path, + ngtcp2_path_validation_result res, void *user_data) { + if (!config.quiet) { + debug::path_validation(path, res); + } + + if (flags & NGTCP2_PATH_VALIDATION_FLAG_PREFERRED_ADDR) { + auto c = static_cast<Client *>(user_data); + + c->set_remote_addr(path->remote); + } + + return 0; +} +} // namespace + +void Client::set_remote_addr(const ngtcp2_addr &remote_addr) { + memcpy(&remote_addr_.su, remote_addr.addr, remote_addr.addrlen); + remote_addr_.len = remote_addr.addrlen; +} + +namespace { +int select_preferred_address(ngtcp2_conn *conn, ngtcp2_path *dest, + const ngtcp2_preferred_addr *paddr, + void *user_data) { + auto c = static_cast<Client *>(user_data); + Address remote_addr; + + if (config.no_preferred_addr) { + return 0; + } + + if (c->select_preferred_address(remote_addr, paddr) != 0) { + return 0; + } + + auto ep = c->endpoint_for(remote_addr); + if (!ep) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + ngtcp2_addr_copy_byte(&dest->local, &(*ep)->addr.su.sa, (*ep)->addr.len); + ngtcp2_addr_copy_byte(&dest->remote, &remote_addr.su.sa, remote_addr.len); + dest->user_data = *ep; + + return 0; +} +} // namespace + +namespace { +int extend_max_stream_data(ngtcp2_conn *conn, int64_t stream_id, + uint64_t max_data, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + if (c->extend_max_stream_data(stream_id, max_data) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Client::extend_max_stream_data(int64_t stream_id, uint64_t max_data) { + auto it = streams_.find(stream_id); + assert(it != std::end(streams_)); + auto &stream = (*it).second; + + if (nghttp3_buf_len(&stream->reqbuf)) { + sendq_.emplace(stream.get()); + } + + return 0; +} + +namespace { +int recv_new_token(ngtcp2_conn *conn, const ngtcp2_vec *token, + void *user_data) { + if (config.token_file.empty()) { + return 0; + } + + auto f = BIO_new_file(config.token_file.data(), "w"); + if (f == nullptr) { + std::cerr << "Could not write token in " << config.token_file << std::endl; + return 0; + } + + PEM_write_bio(f, "QUIC TOKEN", "", token->base, token->len); + BIO_free(f); + + return 0; +} +} // namespace + +namespace { +int early_data_rejected(ngtcp2_conn *conn, void *user_data) { + auto c = static_cast<Client *>(user_data); + + c->early_data_rejected(); + + return 0; +} +} // namespace + +void Client::early_data_rejected() { + nstreams_done_ = 0; + streams_.clear(); +} + +int Client::init(int fd, const Address &local_addr, const Address &remote_addr, + const char *addr, const char *port, + TLSClientContext &tls_ctx) { + endpoints_.reserve(4); + + endpoints_.emplace_back(); + auto &ep = endpoints_.back(); + ep.addr = local_addr; + ep.client = this; + ep.fd = fd; + ev_io_init(&ep.rev, readcb, fd, EV_READ); + ep.rev.data = &ep; + + remote_addr_ = remote_addr; + addr_ = addr; + port_ = port; + + auto callbacks = ngtcp2_callbacks{ + ngtcp2_crypto_client_initial_cb, + nullptr, // recv_client_initial + ::recv_crypto_data, + ::handshake_completed, + ::recv_version_negotiation, + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + do_hp_mask, + ::recv_stream_data, + ::acked_stream_data_offset, + nullptr, // stream_open + stream_close, + nullptr, // recv_stateless_reset + ngtcp2_crypto_recv_retry_cb, + extend_max_streams_bidi, + nullptr, // extend_max_streams_uni + rand, + get_new_connection_id, + nullptr, // remove_connection_id + ::update_key, + path_validation, + ::select_preferred_address, + nullptr, // stream_reset + nullptr, // extend_max_remote_streams_bidi, + nullptr, // extend_max_remote_streams_uni, + ::extend_max_stream_data, + nullptr, // dcid_status + ::handshake_confirmed, + ::recv_new_token, + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + nullptr, // recv_datagram + nullptr, // ack_datagram + nullptr, // lost_datagram + ngtcp2_crypto_get_path_challenge_data_cb, + nullptr, // stream_stop_sending + ngtcp2_crypto_version_negotiation_cb, + nullptr, // recv_rx_key + nullptr, // recv_tx_key + ::early_data_rejected, + }; + + ngtcp2_cid scid, dcid; + scid.datalen = 17; + if (util::generate_secure_random(scid.data, scid.datalen) != 0) { + std::cerr << "Could not generate source connection ID" << std::endl; + return -1; + } + if (config.dcid.datalen == 0) { + dcid.datalen = 18; + if (util::generate_secure_random(dcid.data, dcid.datalen) != 0) { + std::cerr << "Could not generate destination connection ID" << std::endl; + return -1; + } + } else { + dcid = config.dcid; + } + + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + settings.log_printf = config.quiet ? nullptr : debug::log_printf; + if (!config.qlog_file.empty() || !config.qlog_dir.empty()) { + std::string path; + if (!config.qlog_file.empty()) { + path = config.qlog_file; + } else { + path = std::string{config.qlog_dir}; + path += '/'; + path += util::format_hex(scid.data, scid.datalen); + path += ".sqlog"; + } + qlog_ = fopen(path.c_str(), "w"); + if (qlog_ == nullptr) { + std::cerr << "Could not open qlog file " << std::quoted(path) << ": " + << strerror(errno) << std::endl; + return -1; + } + settings.qlog.write = qlog_write_cb; + } + + settings.cc_algo = config.cc_algo; + settings.initial_ts = util::timestamp(loop_); + settings.initial_rtt = config.initial_rtt; + settings.max_window = config.max_window; + settings.max_stream_window = config.max_stream_window; + if (config.max_udp_payload_size) { + settings.max_tx_udp_payload_size = config.max_udp_payload_size; + settings.no_tx_udp_payload_size_shaping = 1; + } + settings.handshake_timeout = config.handshake_timeout; + settings.no_pmtud = config.no_pmtud; + settings.ack_thresh = config.ack_thresh; + + std::string token; + + if (!config.token_file.empty()) { + std::cerr << "Reading token file " << config.token_file << std::endl; + + auto t = util::read_token(config.token_file); + if (t) { + token = std::move(*t); + settings.token.base = reinterpret_cast<uint8_t *>(token.data()); + settings.token.len = token.size(); + } + } + + if (!config.other_versions.empty()) { + settings.other_versions = config.other_versions.data(); + settings.other_versionslen = config.other_versions.size(); + } + + if (!config.preferred_versions.empty()) { + settings.preferred_versions = config.preferred_versions.data(); + settings.preferred_versionslen = config.preferred_versions.size(); + } + + settings.original_version = original_version_; + + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + params.initial_max_stream_data_bidi_local = config.max_stream_data_bidi_local; + params.initial_max_stream_data_bidi_remote = + config.max_stream_data_bidi_remote; + params.initial_max_stream_data_uni = config.max_stream_data_uni; + params.initial_max_data = config.max_data; + params.initial_max_streams_bidi = config.max_streams_bidi; + params.initial_max_streams_uni = 0; + params.max_idle_timeout = config.timeout; + params.active_connection_id_limit = 7; + + auto path = ngtcp2_path{ + { + const_cast<sockaddr *>(&ep.addr.su.sa), + ep.addr.len, + }, + { + const_cast<sockaddr *>(&remote_addr.su.sa), + remote_addr.len, + }, + &ep, + }; + auto rv = ngtcp2_conn_client_new(&conn_, &dcid, &scid, &path, + client_chosen_version_, &callbacks, + &settings, ¶ms, nullptr, this); + + if (rv != 0) { + std::cerr << "ngtcp2_conn_client_new: " << ngtcp2_strerror(rv) << std::endl; + return -1; + } + + if (tls_session_.init(early_data_, tls_ctx, addr_, this, + client_chosen_version_, AppProtocol::HQ) != 0) { + return -1; + } + + ngtcp2_conn_set_tls_native_handle(conn_, tls_session_.get_native_handle()); + + if (early_data_ && config.tp_file) { + ngtcp2_transport_params params; + if (read_transport_params(config.tp_file, ¶ms) != 0) { + std::cerr << "Could not read transport parameters from " << config.tp_file + << std::endl; + early_data_ = false; + } else { + ngtcp2_conn_set_early_remote_transport_params(conn_, ¶ms); + if (make_stream_early() != 0) { + return -1; + } + } + } + + ev_io_start(loop_, &ep.rev); + + ev_signal_start(loop_, &sigintev_); + + return 0; +} + +int Client::feed_data(const Endpoint &ep, const sockaddr *sa, socklen_t salen, + const ngtcp2_pkt_info *pi, uint8_t *data, + size_t datalen) { + auto path = ngtcp2_path{ + { + const_cast<sockaddr *>(&ep.addr.su.sa), + ep.addr.len, + }, + { + const_cast<sockaddr *>(sa), + salen, + }, + const_cast<Endpoint *>(&ep), + }; + if (auto rv = ngtcp2_conn_read_pkt(conn_, &path, pi, data, datalen, + util::timestamp(loop_)); + rv != 0) { + std::cerr << "ngtcp2_conn_read_pkt: " << ngtcp2_strerror(rv) << std::endl; + if (!last_error_.error_code) { + if (rv == NGTCP2_ERR_CRYPTO) { + ngtcp2_connection_close_error_set_transport_error_tls_alert( + &last_error_, ngtcp2_conn_get_tls_alert(conn_), nullptr, 0); + } else { + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, rv, nullptr, 0); + } + } + disconnect(); + return -1; + } + return 0; +} + +int Client::on_read(const Endpoint &ep) { + std::array<uint8_t, 64_k> buf; + sockaddr_union su; + size_t pktcnt = 0; + ngtcp2_pkt_info pi; + + iovec msg_iov; + msg_iov.iov_base = buf.data(); + msg_iov.iov_len = buf.size(); + + msghdr msg{}; + msg.msg_name = &su; + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + + uint8_t msg_ctrl[CMSG_SPACE(sizeof(uint8_t))]; + msg.msg_control = msg_ctrl; + + for (;;) { + msg.msg_namelen = sizeof(su); + msg.msg_controllen = sizeof(msg_ctrl); + + auto nread = recvmsg(ep.fd, &msg, 0); + + if (nread == -1) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + std::cerr << "recvmsg: " << strerror(errno) << std::endl; + } + break; + } + + pi.ecn = msghdr_get_ecn(&msg, su.storage.ss_family); + + if (!config.quiet) { + std::cerr << "Received packet: local=" + << util::straddr(&ep.addr.su.sa, ep.addr.len) + << " remote=" << util::straddr(&su.sa, msg.msg_namelen) + << " ecn=0x" << std::hex << pi.ecn << std::dec << " " << nread + << " bytes" << std::endl; + } + + if (debug::packet_lost(config.rx_loss_prob)) { + if (!config.quiet) { + std::cerr << "** Simulated incoming packet loss **" << std::endl; + } + break; + } + + if (feed_data(ep, &su.sa, msg.msg_namelen, &pi, buf.data(), nread) != 0) { + return -1; + } + + if (++pktcnt >= 10) { + break; + } + } + + if (should_exit_) { + disconnect(); + return -1; + } + + update_timer(); + + return 0; +} + +int Client::handle_expiry() { + auto now = util::timestamp(loop_); + if (auto rv = ngtcp2_conn_handle_expiry(conn_, now); rv != 0) { + std::cerr << "ngtcp2_conn_handle_expiry: " << ngtcp2_strerror(rv) + << std::endl; + ngtcp2_connection_close_error_set_transport_error_liberr(&last_error_, rv, + nullptr, 0); + disconnect(); + return -1; + } + + return 0; +} + +int Client::on_write() { + if (tx_.send_blocked) { + if (auto rv = send_blocked_packet(); rv != 0) { + return rv; + } + + if (tx_.send_blocked) { + return 0; + } + } + + if (auto rv = write_streams(); rv != 0) { + return rv; + } + + if (should_exit_) { + disconnect(); + return -1; + } + + update_timer(); + return 0; +} + +int Client::write_streams() { + ngtcp2_vec vec; + ngtcp2_path_storage ps; + size_t pktcnt = 0; + auto max_udp_payload_size = ngtcp2_conn_get_max_tx_udp_payload_size(conn_); + auto max_pktcnt = ngtcp2_conn_get_send_quantum(conn_) / max_udp_payload_size; + auto ts = util::timestamp(loop_); + + ngtcp2_path_storage_zero(&ps); + + for (;;) { + int64_t stream_id = -1; + size_t vcnt = 0; + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + Stream *stream = nullptr; + + if (!sendq_.empty() && ngtcp2_conn_get_max_data_left(conn_)) { + stream = *std::begin(sendq_); + + stream_id = stream->stream_id; + vec.base = stream->reqbuf.pos; + vec.len = nghttp3_buf_len(&stream->reqbuf); + vcnt = 1; + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + ngtcp2_ssize ndatalen; + ngtcp2_pkt_info pi; + + auto nwrite = ngtcp2_conn_writev_stream( + conn_, &ps.path, &pi, tx_.data.data(), max_udp_payload_size, &ndatalen, + flags, stream_id, &vec, vcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + case NGTCP2_ERR_STREAM_SHUT_WR: + assert(ndatalen == -1); + sendq_.erase(std::begin(sendq_)); + continue; + case NGTCP2_ERR_WRITE_MORE: + assert(ndatalen >= 0); + stream->reqbuf.pos += ndatalen; + if (nghttp3_buf_len(&stream->reqbuf) == 0) { + sendq_.erase(std::begin(sendq_)); + } + continue; + } + + assert(ndatalen == -1); + + std::cerr << "ngtcp2_conn_write_stream: " << ngtcp2_strerror(nwrite) + << std::endl; + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, nwrite, nullptr, 0); + disconnect(); + return -1; + } else if (ndatalen >= 0) { + stream->reqbuf.pos += ndatalen; + if (nghttp3_buf_len(&stream->reqbuf) == 0) { + sendq_.erase(std::begin(sendq_)); + } + } + + if (nwrite == 0) { + // We are congestion limited. + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + ev_io_stop(loop_, &wev_); + return 0; + } + + auto &ep = *static_cast<Endpoint *>(ps.path.user_data); + + if (auto rv = + send_packet(ep, ps.path.remote, pi.ecn, tx_.data.data(), nwrite); + rv != NETWORK_ERR_OK) { + if (rv != NETWORK_ERR_SEND_BLOCKED) { + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, NGTCP2_ERR_INTERNAL, nullptr, 0); + disconnect(); + + return rv; + } + + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + on_send_blocked(ep, ps.path.remote, pi.ecn, nwrite); + + return 0; + } + + if (++pktcnt == max_pktcnt) { + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + start_wev_endpoint(ep); + return 0; + } + } +} + +void Client::update_timer() { + auto expiry = ngtcp2_conn_get_expiry(conn_); + auto now = util::timestamp(loop_); + + if (expiry <= now) { + if (!config.quiet) { + auto t = static_cast<ev_tstamp>(now - expiry) / NGTCP2_SECONDS; + std::cerr << "Timer has already expired: " << t << "s" << std::endl; + } + + ev_feed_event(loop_, &timer_, EV_TIMER); + + return; + } + + auto t = static_cast<ev_tstamp>(expiry - now) / NGTCP2_SECONDS; + if (!config.quiet) { + std::cerr << "Set timer=" << std::fixed << t << "s" << std::defaultfloat + << std::endl; + } + timer_.repeat = t; + ev_timer_again(loop_, &timer_); +} + +#ifdef HAVE_LINUX_RTNETLINK_H +namespace { +int bind_addr(Address &local_addr, int fd, const in_addr_union *iau, + int family) { + addrinfo hints{}; + addrinfo *res, *rp; + + hints.ai_family = family; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_PASSIVE; + + char *node; + std::array<char, NI_MAXHOST> nodebuf; + + if (iau) { + if (inet_ntop(family, iau, nodebuf.data(), nodebuf.size()) == nullptr) { + std::cerr << "inet_ntop: " << strerror(errno) << std::endl; + return -1; + } + + node = nodebuf.data(); + } else { + node = nullptr; + } + + if (auto rv = getaddrinfo(node, "0", &hints, &res); rv != 0) { + std::cerr << "getaddrinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + auto res_d = defer(freeaddrinfo, res); + + for (rp = res; rp; rp = rp->ai_next) { + if (bind(fd, rp->ai_addr, rp->ai_addrlen) != -1) { + break; + } + } + + if (!rp) { + std::cerr << "Could not bind" << std::endl; + return -1; + } + + socklen_t len = sizeof(local_addr.su.storage); + if (getsockname(fd, &local_addr.su.sa, &len) == -1) { + std::cerr << "getsockname: " << strerror(errno) << std::endl; + return -1; + } + local_addr.len = len; + local_addr.ifindex = 0; + + return 0; +} +} // namespace +#endif // HAVE_LINUX_RTNETLINK_H + +#ifndef HAVE_LINUX_RTNETLINK_H +namespace { +int connect_sock(Address &local_addr, int fd, const Address &remote_addr) { + if (connect(fd, &remote_addr.su.sa, remote_addr.len) != 0) { + std::cerr << "connect: " << strerror(errno) << std::endl; + return -1; + } + + socklen_t len = sizeof(local_addr.su.storage); + if (getsockname(fd, &local_addr.su.sa, &len) == -1) { + std::cerr << "getsockname: " << strerror(errno) << std::endl; + return -1; + } + local_addr.len = len; + local_addr.ifindex = 0; + + return 0; +} +} // namespace +#endif // !HAVE_LINUX_RTNETLINK_H + +namespace { +int udp_sock(int family) { + auto fd = util::create_nonblock_socket(family, SOCK_DGRAM, IPPROTO_UDP); + if (fd == -1) { + return -1; + } + + fd_set_recv_ecn(fd, family); + fd_set_ip_mtu_discover(fd, family); + fd_set_ip_dontfrag(fd, family); + + return fd; +} +} // namespace + +namespace { +int create_sock(Address &remote_addr, const char *addr, const char *port) { + addrinfo hints{}; + addrinfo *res, *rp; + + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + + if (auto rv = getaddrinfo(addr, port, &hints, &res); rv != 0) { + std::cerr << "getaddrinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + auto res_d = defer(freeaddrinfo, res); + + int fd = -1; + + for (rp = res; rp; rp = rp->ai_next) { + fd = udp_sock(rp->ai_family); + if (fd == -1) { + continue; + } + + break; + } + + if (!rp) { + std::cerr << "Could not create socket" << std::endl; + return -1; + } + + remote_addr.len = rp->ai_addrlen; + memcpy(&remote_addr.su, rp->ai_addr, rp->ai_addrlen); + + return fd; +} +} // namespace + +std::optional<Endpoint *> Client::endpoint_for(const Address &remote_addr) { +#ifdef HAVE_LINUX_RTNETLINK_H + in_addr_union iau; + + if (get_local_addr(iau, remote_addr) != 0) { + std::cerr << "Could not get local address for a selected preferred address" + << std::endl; + return nullptr; + } + + auto current_path = ngtcp2_conn_get_path(conn_); + auto current_ep = static_cast<Endpoint *>(current_path->user_data); + if (addreq(¤t_ep->addr.su.sa, iau)) { + return current_ep; + } +#endif // HAVE_LINUX_RTNETLINK_H + + auto fd = udp_sock(remote_addr.su.sa.sa_family); + if (fd == -1) { + return nullptr; + } + + Address local_addr; + +#ifdef HAVE_LINUX_RTNETLINK_H + if (bind_addr(local_addr, fd, &iau, remote_addr.su.sa.sa_family) != 0) { + close(fd); + return nullptr; + } +#else // !HAVE_LINUX_RTNETLINK_H + if (connect_sock(local_addr, fd, remote_addr) != 0) { + close(fd); + return nullptr; + } +#endif // !HAVE_LINUX_RTNETLINK_H + + endpoints_.emplace_back(); + auto &ep = endpoints_.back(); + ep.addr = local_addr; + ep.client = this; + ep.fd = fd; + ev_io_init(&ep.rev, readcb, fd, EV_READ); + ep.rev.data = &ep; + + ev_io_start(loop_, &ep.rev); + + return &ep; +} + +void Client::start_change_local_addr_timer() { + ev_timer_start(loop_, &change_local_addr_timer_); +} + +int Client::change_local_addr() { + Address local_addr; + + if (!config.quiet) { + std::cerr << "Changing local address" << std::endl; + } + + auto nfd = udp_sock(remote_addr_.su.sa.sa_family); + if (nfd == -1) { + return -1; + } + +#ifdef HAVE_LINUX_RTNETLINK_H + in_addr_union iau; + + if (get_local_addr(iau, remote_addr_) != 0) { + std::cerr << "Could not get local address" << std::endl; + close(nfd); + return -1; + } + + if (bind_addr(local_addr, nfd, &iau, remote_addr_.su.sa.sa_family) != 0) { + close(nfd); + return -1; + } +#else // !HAVE_LINUX_RTNETLINK_H + if (connect_sock(local_addr, nfd, remote_addr_) != 0) { + close(nfd); + return -1; + } +#endif // !HAVE_LINUX_RTNETLINK_H + + if (!config.quiet) { + std::cerr << "Local address is now " + << util::straddr(&local_addr.su.sa, local_addr.len) << std::endl; + } + + endpoints_.emplace_back(); + auto &ep = endpoints_.back(); + ep.addr = local_addr; + ep.client = this; + ep.fd = nfd; + ev_io_init(&ep.rev, readcb, nfd, EV_READ); + ep.rev.data = &ep; + + ngtcp2_addr addr; + ngtcp2_addr_init(&addr, &local_addr.su.sa, local_addr.len); + + if (config.nat_rebinding) { + ngtcp2_conn_set_local_addr(conn_, &addr); + ngtcp2_conn_set_path_user_data(conn_, &ep); + } else { + auto path = ngtcp2_path{ + addr, + { + const_cast<sockaddr *>(&remote_addr_.su.sa), + remote_addr_.len, + }, + &ep, + }; + if (auto rv = ngtcp2_conn_initiate_immediate_migration( + conn_, &path, util::timestamp(loop_)); + rv != 0) { + std::cerr << "ngtcp2_conn_initiate_immediate_migration: " + << ngtcp2_strerror(rv) << std::endl; + } + } + + ev_io_start(loop_, &ep.rev); + + return 0; +} + +void Client::start_key_update_timer() { + ev_timer_start(loop_, &key_update_timer_); +} + +int Client::update_key(uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen) { + if (!config.quiet) { + std::cerr << "Updating traffic key" << std::endl; + } + + auto crypto_ctx = ngtcp2_conn_get_crypto_ctx(conn_); + auto aead = &crypto_ctx->aead; + auto keylen = ngtcp2_crypto_aead_keylen(aead); + auto ivlen = ngtcp2_crypto_packet_protection_ivlen(aead); + + ++nkey_update_; + + std::array<uint8_t, 64> rx_key, tx_key; + + if (ngtcp2_crypto_update_key(conn_, rx_secret, tx_secret, rx_aead_ctx, + rx_key.data(), rx_iv, tx_aead_ctx, tx_key.data(), + tx_iv, current_rx_secret, current_tx_secret, + secretlen) != 0) { + return -1; + } + + if (!config.quiet && config.show_secret) { + std::cerr << "application_traffic rx secret " << nkey_update_ << std::endl; + debug::print_secrets(rx_secret, secretlen, rx_key.data(), keylen, rx_iv, + ivlen); + std::cerr << "application_traffic tx secret " << nkey_update_ << std::endl; + debug::print_secrets(tx_secret, secretlen, tx_key.data(), keylen, tx_iv, + ivlen); + } + + return 0; +} + +int Client::initiate_key_update() { + if (!config.quiet) { + std::cerr << "Initiate key update" << std::endl; + } + + if (auto rv = ngtcp2_conn_initiate_key_update(conn_, util::timestamp(loop_)); + rv != 0) { + std::cerr << "ngtcp2_conn_initiate_key_update: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + return 0; +} + +void Client::start_delay_stream_timer() { + ev_timer_start(loop_, &delay_stream_timer_); +} + +int Client::send_packet(const Endpoint &ep, const ngtcp2_addr &remote_addr, + unsigned int ecn, const uint8_t *data, size_t datalen) { + if (debug::packet_lost(config.tx_loss_prob)) { + if (!config.quiet) { + std::cerr << "** Simulated outgoing packet loss **" << std::endl; + } + return NETWORK_ERR_OK; + } + + iovec msg_iov; + msg_iov.iov_base = const_cast<uint8_t *>(data); + msg_iov.iov_len = datalen; + + msghdr msg{}; +#ifdef HAVE_LINUX_RTNETLINK_H + msg.msg_name = const_cast<sockaddr *>(remote_addr.addr); + msg.msg_namelen = remote_addr.addrlen; +#endif // HAVE_LINUX_RTNETLINK_H + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + + fd_set_ecn(ep.fd, remote_addr.addr->sa_family, ecn); + + ssize_t nwrite = 0; + + do { + nwrite = sendmsg(ep.fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return NETWORK_ERR_SEND_BLOCKED; + } + std::cerr << "sendmsg: " << strerror(errno) << std::endl; + if (errno == EMSGSIZE) { + return 0; + } + return NETWORK_ERR_FATAL; + } + + assert(static_cast<size_t>(nwrite) == datalen); + + if (!config.quiet) { + std::cerr << "Sent packet: local=" + << util::straddr(&ep.addr.su.sa, ep.addr.len) << " remote=" + << util::straddr(remote_addr.addr, remote_addr.addrlen) + << " ecn=0x" << std::hex << ecn << std::dec << " " << nwrite + << " bytes" << std::endl; + } + + return NETWORK_ERR_OK; +} + +void Client::on_send_blocked(const Endpoint &ep, const ngtcp2_addr &remote_addr, + unsigned int ecn, size_t datalen) { + assert(!tx_.send_blocked); + + tx_.send_blocked = true; + + memcpy(&tx_.blocked.remote_addr.su, remote_addr.addr, remote_addr.addrlen); + tx_.blocked.remote_addr.len = remote_addr.addrlen; + tx_.blocked.ecn = ecn; + tx_.blocked.datalen = datalen; + tx_.blocked.endpoint = &ep; + + start_wev_endpoint(ep); +} + +void Client::start_wev_endpoint(const Endpoint &ep) { + // We do not close ep.fd, so we can expect that each Endpoint has + // unique fd. + if (ep.fd != wev_.fd) { + if (ev_is_active(&wev_)) { + ev_io_stop(loop_, &wev_); + } + + ev_io_set(&wev_, ep.fd, EV_WRITE); + } + + ev_io_start(loop_, &wev_); +} + +int Client::send_blocked_packet() { + assert(tx_.send_blocked); + + ngtcp2_addr remote_addr{ + .addr = &tx_.blocked.remote_addr.su.sa, + .addrlen = tx_.blocked.remote_addr.len, + }; + + auto rv = send_packet(*tx_.blocked.endpoint, remote_addr, tx_.blocked.ecn, + tx_.data.data(), tx_.blocked.datalen); + if (rv != 0) { + if (rv == NETWORK_ERR_SEND_BLOCKED) { + assert(wev_.fd == tx_.blocked.endpoint->fd); + + ev_io_start(loop_, &wev_); + + return 0; + } + + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, NGTCP2_ERR_INTERNAL, nullptr, 0); + disconnect(); + + return rv; + } + + tx_.send_blocked = false; + + return 0; +} + +int Client::handle_error() { + if (!conn_ || ngtcp2_conn_is_in_closing_period(conn_) || + ngtcp2_conn_is_in_draining_period(conn_)) { + return 0; + } + + std::array<uint8_t, NGTCP2_MAX_UDP_PAYLOAD_SIZE> buf; + + ngtcp2_path_storage ps; + + ngtcp2_path_storage_zero(&ps); + + ngtcp2_pkt_info pi; + + auto nwrite = ngtcp2_conn_write_connection_close( + conn_, &ps.path, &pi, buf.data(), buf.size(), &last_error_, + util::timestamp(loop_)); + if (nwrite < 0) { + std::cerr << "ngtcp2_conn_write_connection_close: " + << ngtcp2_strerror(nwrite) << std::endl; + return -1; + } + + if (nwrite == 0) { + return 0; + } + + return send_packet(*static_cast<Endpoint *>(ps.path.user_data), + ps.path.remote, pi.ecn, buf.data(), nwrite); +} + +int Client::on_stream_close(int64_t stream_id, uint64_t app_error_code) { + auto it = streams_.find(stream_id); + assert(it != std::end(streams_)); + auto &stream = (*it).second; + + sendq_.erase(stream.get()); + + ++nstreams_closed_; + + if (config.exit_on_first_stream_close || + (config.exit_on_all_streams_close && config.nstreams == nstreams_done_ && + nstreams_closed_ == nstreams_done_)) { + if (handshake_confirmed_) { + should_exit_ = true; + } else { + should_exit_on_handshake_confirmed_ = true; + } + } + + if (!ngtcp2_is_bidi_stream(stream_id)) { + assert(!ngtcp2_conn_is_local_stream(conn_, stream_id)); + ngtcp2_conn_extend_max_streams_uni(conn_, 1); + } + + if (!config.quiet) { + std::cerr << "HTTP stream " << stream_id << " closed with error code " + << app_error_code << std::endl; + } + streams_.erase(it); + + return 0; +} + +int Client::make_stream_early() { return on_extend_max_streams(); } + +int Client::on_extend_max_streams() { + int64_t stream_id; + + if ((config.delay_stream && !handshake_confirmed_) || + ev_is_active(&delay_stream_timer_)) { + return 0; + } + + for (; nstreams_done_ < config.nstreams; ++nstreams_done_) { + if (auto rv = ngtcp2_conn_open_bidi_stream(conn_, &stream_id, nullptr); + rv != 0) { + assert(NGTCP2_ERR_STREAM_ID_BLOCKED == rv); + break; + } + + auto stream = std::make_unique<Stream>( + config.requests[nstreams_done_ % config.requests.size()], stream_id); + + if (submit_http_request(stream.get()) != 0) { + break; + } + + if (!config.download.empty()) { + stream->open_file(stream->req.path); + } + streams_.emplace(stream_id, std::move(stream)); + } + return 0; +} + +int Client::submit_http_request(Stream *stream) { + const auto &req = stream->req; + + stream->rawreqbuf = config.http_method; + stream->rawreqbuf += ' '; + stream->rawreqbuf += req.path; + stream->rawreqbuf += "\r\n"; + + nghttp3_buf_init(&stream->reqbuf); + stream->reqbuf.begin = reinterpret_cast<uint8_t *>(stream->rawreqbuf.data()); + stream->reqbuf.pos = stream->reqbuf.begin; + stream->reqbuf.end = stream->reqbuf.last = + stream->reqbuf.begin + stream->rawreqbuf.size(); + + if (!config.quiet) { + auto nva = std::array<nghttp3_nv, 2>{ + util::make_nv_nn(":method", config.http_method), + util::make_nv_nn(":path", req.path), + }; + debug::print_http_request_headers(stream->stream_id, nva.data(), + nva.size()); + } + + sendq_.emplace(stream); + + return 0; +} + +int Client::recv_stream_data(uint32_t flags, int64_t stream_id, + const uint8_t *data, size_t datalen) { + auto it = streams_.find(stream_id); + assert(it != std::end(streams_)); + auto &stream = (*it).second; + + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, datalen); + ngtcp2_conn_extend_max_offset(conn_, datalen); + + if (stream->fd == -1) { + return 0; + } + + ssize_t nwrite; + do { + nwrite = write(stream->fd, data, datalen); + } while (nwrite == -1 && errno == EINTR); + + return 0; +} + +int Client::acked_stream_data_offset(int64_t stream_id, uint64_t offset, + uint64_t datalen) { + auto it = streams_.find(stream_id); + assert(it != std::end(streams_)); + auto &stream = (*it).second; + (void)stream; + assert(static_cast<uint64_t>(stream->reqbuf.end - stream->reqbuf.begin) >= + offset + datalen); + return 0; +} + +int Client::select_preferred_address(Address &selected_addr, + const ngtcp2_preferred_addr *paddr) { + auto path = ngtcp2_conn_get_path(conn_); + + switch (path->local.addr->sa_family) { + case AF_INET: + if (!paddr->ipv4_present) { + return -1; + } + selected_addr.su.in = paddr->ipv4; + selected_addr.len = sizeof(paddr->ipv4); + break; + case AF_INET6: + if (!paddr->ipv6_present) { + return -1; + } + selected_addr.su.in6 = paddr->ipv6; + selected_addr.len = sizeof(paddr->ipv6); + break; + default: + return -1; + } + + char host[NI_MAXHOST], service[NI_MAXSERV]; + if (auto rv = getnameinfo(&selected_addr.su.sa, selected_addr.len, host, + sizeof(host), service, sizeof(service), + NI_NUMERICHOST | NI_NUMERICSERV); + rv != 0) { + std::cerr << "getnameinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + if (!config.quiet) { + std::cerr << "selected server preferred_address is [" << host + << "]:" << service << std::endl; + } + + return 0; +} + +const std::vector<uint32_t> &Client::get_offered_versions() const { + return offered_versions_; +} + +namespace { +int run(Client &c, const char *addr, const char *port, + TLSClientContext &tls_ctx) { + Address remote_addr, local_addr; + + auto fd = create_sock(remote_addr, addr, port); + if (fd == -1) { + return -1; + } + +#ifdef HAVE_LINUX_RTNETLINK_H + in_addr_union iau; + + if (get_local_addr(iau, remote_addr) != 0) { + std::cerr << "Could not get local address" << std::endl; + close(fd); + return -1; + } + + if (bind_addr(local_addr, fd, &iau, remote_addr.su.sa.sa_family) != 0) { + close(fd); + return -1; + } +#else // !HAVE_LINUX_RTNETLINK_H + if (connect_sock(local_addr, fd, remote_addr) != 0) { + close(fd); + return -1; + } +#endif // !HAVE_LINUX_RTNETLINK_H + + if (c.init(fd, local_addr, remote_addr, addr, port, tls_ctx) != 0) { + return -1; + } + + // TODO Do we need this ? + if (auto rv = c.on_write(); rv != 0) { + return rv; + } + + ev_run(EV_DEFAULT, 0); + + return 0; +} +} // namespace + +namespace { +std::string_view get_string(const char *uri, const http_parser_url &u, + http_parser_url_fields f) { + auto p = &u.field_data[f]; + return {uri + p->off, p->len}; +} +} // namespace + +namespace { +int parse_uri(Request &req, const char *uri) { + http_parser_url u; + + http_parser_url_init(&u); + if (http_parser_parse_url(uri, strlen(uri), /* is_connect = */ 0, &u) != 0) { + return -1; + } + + if (!(u.field_set & (1 << UF_SCHEMA)) || !(u.field_set & (1 << UF_HOST))) { + return -1; + } + + req.scheme = get_string(uri, u, UF_SCHEMA); + + req.authority = get_string(uri, u, UF_HOST); + if (util::numeric_host(req.authority.c_str(), AF_INET6)) { + req.authority = '[' + req.authority + ']'; + } + if (u.field_set & (1 << UF_PORT)) { + req.authority += ':'; + req.authority += get_string(uri, u, UF_PORT); + } + + if (u.field_set & (1 << UF_PATH)) { + req.path = get_string(uri, u, UF_PATH); + } else { + req.path = "/"; + } + + if (u.field_set & (1 << UF_QUERY)) { + req.path += '?'; + req.path += get_string(uri, u, UF_QUERY); + } + + return 0; +} +} // namespace + +namespace { +int parse_requests(char **argv, size_t argvlen) { + for (size_t i = 0; i < argvlen; ++i) { + auto uri = argv[i]; + Request req; + if (parse_uri(req, uri) != 0) { + std::cerr << "Could not parse URI: " << uri << std::endl; + return -1; + } + config.requests.emplace_back(std::move(req)); + } + return 0; +} +} // namespace + +std::ofstream keylog_file; + +namespace { +void print_usage() { + std::cerr << "Usage: h09client [OPTIONS] <HOST> <PORT> [<URI>...]" + << std::endl; +} +} // namespace + +namespace { +void config_set_default(Config &config) { + config = Config{}; + config.tx_loss_prob = 0.; + config.rx_loss_prob = 0.; + config.fd = -1; + config.ciphers = util::crypto_default_ciphers(); + config.groups = util::crypto_default_groups(); + config.nstreams = 0; + config.data = nullptr; + config.datalen = 0; + config.version = NGTCP2_PROTO_VER_V1; + config.timeout = 30 * NGTCP2_SECONDS; + config.http_method = "GET"sv; + config.max_data = 15_m; + config.max_stream_data_bidi_local = 6_m; + config.max_stream_data_bidi_remote = 6_m; + config.max_stream_data_uni = 6_m; + config.max_window = 24_m; + config.max_stream_window = 16_m; + config.max_streams_uni = 100; + config.cc_algo = NGTCP2_CC_ALGO_CUBIC; + config.initial_rtt = NGTCP2_DEFAULT_INITIAL_RTT; + config.handshake_timeout = NGTCP2_DEFAULT_HANDSHAKE_TIMEOUT; + config.ack_thresh = 2; +} +} // namespace + +namespace { +void print_help() { + print_usage(); + + config_set_default(config); + + std::cout << R"( + <HOST> Remote server host (DNS name or IP address). In case of + DNS name, it will be sent in TLS SNI extension. + <PORT> Remote server port + <URI> Remote URI +Options: + -t, --tx-loss=<P> + The probability of losing outgoing packets. <P> must be + [0.0, 1.0], inclusive. 0.0 means no packet loss. 1.0 + means 100% packet loss. + -r, --rx-loss=<P> + The probability of losing incoming packets. <P> must be + [0.0, 1.0], inclusive. 0.0 means no packet loss. 1.0 + means 100% packet loss. + -d, --data=<PATH> + Read data from <PATH>, and send them as STREAM data. + -n, --nstreams=<N> + The number of requests. <URI>s are used in the order of + appearance in the command-line. If the number of <URI> + list is less than <N>, <URI> list is wrapped. It + defaults to 0 which means the number of <URI> specified. + -v, --version=<HEX> + Specify QUIC version to use in hex string. If the given + version is not supported by libngtcp2, client will use + QUIC v1 long packet types. Instead of specifying hex + string, there are special aliases available: "v1" + indicates QUIC v1, and "v2draft" indicates QUIC v2 + draft. + Default: )" + << std::hex << "0x" << config.version << std::dec << R"( + --preferred-versions=<HEX>[[,<HEX>]...] + Specify QUIC versions in hex string in the order of + preference. Client chooses one of those versions if + client received Version Negotiation packet from server. + These versions must be supported by libngtcp2. Instead + of specifying hex string, there are special aliases + available: "v1" indicates QUIC v1, and "v2draft" + indicates QUIC v2 draft. + --other-versions=<HEX>[[,<HEX>]...] + Specify QUIC versions in hex string that are sent in + other_versions field of version_information transport + parameter. This list can include a version which is not + supported by libngtcp2. Instead of specifying hex + string, there are special aliases available: "v1" + indicates QUIC v1, and "v2draft" indicates QUIC v2 + draft. + -q, --quiet Suppress debug output. + -s, --show-secret + Print out secrets unless --quiet is used. + --timeout=<DURATION> + Specify idle timeout. + Default: )" + << util::format_duration(config.timeout) << R"( + --ciphers=<CIPHERS> + Specify the cipher suite list to enable. + Default: )" + << config.ciphers << R"( + --groups=<GROUPS> + Specify the supported groups. + Default: )" + << config.groups << R"( + --session-file=<PATH> + Read/write TLS session from/to <PATH>. To resume a + session, the previous session must be supplied with this + option. + --tp-file=<PATH> + Read/write QUIC transport parameters from/to <PATH>. To + send 0-RTT data, the transport parameters received from + the previous session must be supplied with this option. + --dcid=<DCID> + Specify initial DCID. <DCID> is hex string. When + decoded as binary, it should be at least 8 bytes and at + most 18 bytes long. + --change-local-addr=<DURATION> + Client changes local address when <DURATION> elapse + after handshake completes. + --nat-rebinding + When used with --change-local-addr, simulate NAT + rebinding. In other words, client changes local + address, but it does not start path validation. + --key-update=<DURATION> + Client initiates key update when <DURATION> elapse after + handshake completes. + -m, --http-method=<METHOD> + Specify HTTP method. Default: )" + << config.http_method << R"( + --delay-stream=<DURATION> + Delay sending STREAM data in 1-RTT for <DURATION> after + handshake completes. + --no-preferred-addr + Do not try to use preferred address offered by server. + --key=<PATH> + The path to client private key PEM file. + --cert=<PATH> + The path to client certificate PEM file. + --download=<PATH> + The path to the directory to save a downloaded content. + It is undefined if 2 concurrent requests write to the + same file. If a request path does not contain a path + component usable as a file name, it defaults to + "index.html". + --no-quic-dump + Disables printing QUIC STREAM and CRYPTO frame data out. + --no-http-dump + Disables printing HTTP response body out. + --qlog-file=<PATH> + The path to write qlog. This option and --qlog-dir are + mutually exclusive. + --qlog-dir=<PATH> + Path to the directory where qlog file is stored. The + file name of each qlog is the Source Connection ID of + client. This option and --qlog-file are mutually + exclusive. + --max-data=<SIZE> + The initial connection-level flow control window. + Default: )" + << util::format_uint_iec(config.max_data) << R"( + --max-stream-data-bidi-local=<SIZE> + The initial stream-level flow control window for a + bidirectional stream that the local endpoint initiates. + Default: )" + << util::format_uint_iec(config.max_stream_data_bidi_local) << R"( + --max-stream-data-bidi-remote=<SIZE> + The initial stream-level flow control window for a + bidirectional stream that the remote endpoint initiates. + Default: )" + << util::format_uint_iec(config.max_stream_data_bidi_remote) << R"( + --max-stream-data-uni=<SIZE> + The initial stream-level flow control window for a + unidirectional stream. + Default: )" + << util::format_uint_iec(config.max_stream_data_uni) << R"( + --max-streams-bidi=<N> + The number of the concurrent bidirectional streams. + Default: )" + << config.max_streams_bidi << R"( + --max-streams-uni=<N> + The number of the concurrent unidirectional streams. + Default: )" + << config.max_streams_uni << R"( + --exit-on-first-stream-close + Exit when a first HTTP stream is closed. + --exit-on-all-streams-close + Exit when all HTTP streams are closed. + --disable-early-data + Disable early data. + --cc=(cubic|reno|bbr|bbr2) + The name of congestion controller algorithm. + Default: )" + << util::strccalgo(config.cc_algo) << R"( + --token-file=<PATH> + Read/write token from/to <PATH>. Token is obtained from + NEW_TOKEN frame from server. + --sni=<DNSNAME> + Send <DNSNAME> in TLS SNI, overriding the DNS name + specified in <HOST>. + --initial-rtt=<DURATION> + Set an initial RTT. + Default: )" + << util::format_duration(config.initial_rtt) << R"( + --max-window=<SIZE> + Maximum connection-level flow control window size. The + window auto-tuning is enabled if nonzero value is given, + and window size is scaled up to this value. + Default: )" + << util::format_uint_iec(config.max_window) << R"( + --max-stream-window=<SIZE> + Maximum stream-level flow control window size. The + window auto-tuning is enabled if nonzero value is given, + and window size is scaled up to this value. + Default: )" + << util::format_uint_iec(config.max_stream_window) << R"( + --max-udp-payload-size=<SIZE> + Override maximum UDP payload size that client transmits. + --handshake-timeout=<DURATION> + Set the QUIC handshake timeout. + Default: )" + << util::format_duration(config.handshake_timeout) << R"( + --no-pmtud Disables Path MTU Discovery. + --ack-thresh=<N> + The minimum number of the received ACK eliciting packets + that triggers immediate acknowledgement. + Default: )" + << config.ack_thresh << R"( + -h, --help Display this help and exit. + +--- + + The <SIZE> argument is an integer and an optional unit (e.g., 10K is + 10 * 1024). Units are K, M and G (powers of 1024). + + The <DURATION> argument is an integer and an optional unit (e.g., 1s + is 1 second and 500ms is 500 milliseconds). Units are h, m, s, ms, + us, or ns (hours, minutes, seconds, milliseconds, microseconds, and + nanoseconds respectively). If a unit is omitted, a second is used + as unit. + + The <HEX> argument is an hex string which must start with "0x" + (e.g., 0x00000001).)" + << std::endl; +} +} // namespace + +int main(int argc, char **argv) { + config_set_default(config); + char *data_path = nullptr; + const char *private_key_file = nullptr; + const char *cert_file = nullptr; + + for (;;) { + static int flag = 0; + constexpr static option long_opts[] = { + {"help", no_argument, nullptr, 'h'}, + {"tx-loss", required_argument, nullptr, 't'}, + {"rx-loss", required_argument, nullptr, 'r'}, + {"data", required_argument, nullptr, 'd'}, + {"http-method", required_argument, nullptr, 'm'}, + {"nstreams", required_argument, nullptr, 'n'}, + {"version", required_argument, nullptr, 'v'}, + {"quiet", no_argument, nullptr, 'q'}, + {"show-secret", no_argument, nullptr, 's'}, + {"ciphers", required_argument, &flag, 1}, + {"groups", required_argument, &flag, 2}, + {"timeout", required_argument, &flag, 3}, + {"session-file", required_argument, &flag, 4}, + {"tp-file", required_argument, &flag, 5}, + {"dcid", required_argument, &flag, 6}, + {"change-local-addr", required_argument, &flag, 7}, + {"key-update", required_argument, &flag, 8}, + {"nat-rebinding", no_argument, &flag, 9}, + {"delay-stream", required_argument, &flag, 10}, + {"no-preferred-addr", no_argument, &flag, 11}, + {"key", required_argument, &flag, 12}, + {"cert", required_argument, &flag, 13}, + {"download", required_argument, &flag, 14}, + {"no-quic-dump", no_argument, &flag, 15}, + {"no-http-dump", no_argument, &flag, 16}, + {"qlog-file", required_argument, &flag, 17}, + {"max-data", required_argument, &flag, 18}, + {"max-stream-data-bidi-local", required_argument, &flag, 19}, + {"max-stream-data-bidi-remote", required_argument, &flag, 20}, + {"max-stream-data-uni", required_argument, &flag, 21}, + {"max-streams-bidi", required_argument, &flag, 22}, + {"max-streams-uni", required_argument, &flag, 23}, + {"exit-on-first-stream-close", no_argument, &flag, 24}, + {"disable-early-data", no_argument, &flag, 25}, + {"qlog-dir", required_argument, &flag, 26}, + {"cc", required_argument, &flag, 27}, + {"exit-on-all-streams-close", no_argument, &flag, 28}, + {"token-file", required_argument, &flag, 29}, + {"sni", required_argument, &flag, 30}, + {"initial-rtt", required_argument, &flag, 31}, + {"max-window", required_argument, &flag, 32}, + {"max-stream-window", required_argument, &flag, 33}, + {"max-udp-payload-size", required_argument, &flag, 35}, + {"handshake-timeout", required_argument, &flag, 36}, + {"other-versions", required_argument, &flag, 37}, + {"no-pmtud", no_argument, &flag, 38}, + {"preferred-versions", required_argument, &flag, 39}, + {"ack-thresh", required_argument, &flag, 40}, + {nullptr, 0, nullptr, 0}, + }; + + auto optidx = 0; + auto c = getopt_long(argc, argv, "d:him:n:qr:st:v:", long_opts, &optidx); + if (c == -1) { + break; + } + switch (c) { + case 'd': + // --data + data_path = optarg; + break; + case 'h': + // --help + print_help(); + exit(EXIT_SUCCESS); + case 'm': + // --http-method + config.http_method = optarg; + break; + case 'n': + // --streams + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "streams: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else if (*n > NGTCP2_MAX_VARINT) { + std::cerr << "streams: must not exceed " << NGTCP2_MAX_VARINT + << std::endl; + exit(EXIT_FAILURE); + } else { + config.nstreams = *n; + } + break; + case 'q': + // --quiet + config.quiet = true; + break; + case 'r': + // --rx-loss + config.rx_loss_prob = strtod(optarg, nullptr); + break; + case 's': + // --show-secret + config.show_secret = true; + break; + case 't': + // --tx-loss + config.tx_loss_prob = strtod(optarg, nullptr); + break; + case 'v': { + // --version + if (optarg == "v1"sv) { + config.version = NGTCP2_PROTO_VER_V1; + break; + } + if (optarg == "v2draft"sv) { + config.version = NGTCP2_PROTO_VER_V2_DRAFT; + break; + } + auto rv = util::parse_version(optarg); + if (!rv) { + std::cerr << "version: invalid version " << std::quoted(optarg) + << std::endl; + exit(EXIT_FAILURE); + } + config.version = *rv; + break; + } + case '?': + print_usage(); + exit(EXIT_FAILURE); + case 0: + switch (flag) { + case 1: + // --ciphers + config.ciphers = optarg; + break; + case 2: + // --groups + config.groups = optarg; + break; + case 3: + // --timeout + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "timeout: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.timeout = *t; + } + break; + case 4: + // --session-file + config.session_file = optarg; + break; + case 5: + // --tp-file + config.tp_file = optarg; + break; + case 6: { + // --dcid + auto dcidlen2 = strlen(optarg); + if (dcidlen2 % 2 || dcidlen2 / 2 < 8 || dcidlen2 / 2 > 18) { + std::cerr << "dcid: wrong length" << std::endl; + exit(EXIT_FAILURE); + } + auto dcid = util::decode_hex(optarg); + ngtcp2_cid_init(&config.dcid, + reinterpret_cast<const uint8_t *>(dcid.c_str()), + dcid.size()); + break; + } + case 7: + // --change-local-addr + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "change-local-addr: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.change_local_addr = *t; + } + break; + case 8: + // --key-update + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "key-update: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.key_update = *t; + } + break; + case 9: + // --nat-rebinding + config.nat_rebinding = true; + break; + case 10: + // --delay-stream + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "delay-stream: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.delay_stream = *t; + } + break; + case 11: + // --no-preferred-addr + config.no_preferred_addr = true; + break; + case 12: + // --key + private_key_file = optarg; + break; + case 13: + // --cert + cert_file = optarg; + break; + case 14: + // --download + config.download = optarg; + break; + case 15: + // --no-quic-dump + config.no_quic_dump = true; + break; + case 16: + // --no-http-dump + config.no_http_dump = true; + break; + case 17: + // --qlog-file + config.qlog_file = optarg; + break; + case 18: + // --max-data + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-data: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_data = *n; + } + break; + case 19: + // --max-stream-data-bidi-local + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-data-bidi-local: invalid argument" + << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_data_bidi_local = *n; + } + break; + case 20: + // --max-stream-data-bidi-remote + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-data-bidi-remote: invalid argument" + << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_data_bidi_remote = *n; + } + break; + case 21: + // --max-stream-data-uni + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-data-uni: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_data_uni = *n; + } + break; + case 22: + // --max-streams-bidi + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "max-streams-bidi: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_streams_bidi = *n; + } + break; + case 23: + // --max-streams-uni + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "max-streams-uni: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_streams_uni = *n; + } + break; + case 24: + // --exit-on-first-stream-close + config.exit_on_first_stream_close = true; + break; + case 25: + // --disable-early-data + config.disable_early_data = true; + break; + case 26: + // --qlog-dir + config.qlog_dir = optarg; + break; + case 27: + // --cc + if (strcmp("cubic", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_CUBIC; + break; + } + if (strcmp("reno", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_RENO; + break; + } + if (strcmp("bbr", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_BBR; + break; + } + if (strcmp("bbr2", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_BBR2; + break; + } + std::cerr << "cc: specify cubic, reno, bbr, or bbr2" << std::endl; + exit(EXIT_FAILURE); + case 28: + // --exit-on-all-streams-close + config.exit_on_all_streams_close = true; + break; + case 29: + // --token-file + config.token_file = optarg; + break; + case 30: + // --sni + config.sni = optarg; + break; + case 31: + // --initial-rtt + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "initial-rtt: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.initial_rtt = *t; + } + break; + case 32: + // --max-window + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-window: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_window = *n; + } + break; + case 33: + // --max-stream-window + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-window: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_window = *n; + } + break; + case 35: + // --max-udp-payload-size + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-udp-payload-size: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else if (*n > 64_k) { + std::cerr << "max-udp-payload-size: must not exceed 65536" + << std::endl; + exit(EXIT_FAILURE); + } else if (*n == 0) { + std::cerr << "max-udp-payload-size: must not be 0" << std::endl; + } else { + config.max_udp_payload_size = *n; + } + break; + case 36: + // --handshake-timeout + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "handshake-timeout: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.handshake_timeout = *t; + } + break; + case 37: { + // --other-versions + if (strlen(optarg) == 0) { + config.other_versions.resize(0); + break; + } + auto l = util::split_str(optarg); + config.other_versions.resize(l.size()); + auto it = std::begin(config.other_versions); + for (const auto &k : l) { + if (k == "v1"sv) { + *it++ = NGTCP2_PROTO_VER_V1; + continue; + } + if (k == "v2draft"sv) { + *it++ = NGTCP2_PROTO_VER_V2_DRAFT; + continue; + } + auto rv = util::parse_version(k); + if (!rv) { + std::cerr << "other-versions: invalid version " << std::quoted(k) + << std::endl; + exit(EXIT_FAILURE); + } + *it++ = *rv; + } + break; + } + case 38: + // --no-pmtud + config.no_pmtud = true; + break; + case 39: { + // --preferred-versions + auto l = util::split_str(optarg); + if (l.size() > max_preferred_versionslen) { + std::cerr << "preferred-versions: too many versions > " + << max_preferred_versionslen << std::endl; + } + config.preferred_versions.resize(l.size()); + auto it = std::begin(config.preferred_versions); + for (const auto &k : l) { + if (k == "v1"sv) { + *it++ = NGTCP2_PROTO_VER_V1; + continue; + } + if (k == "v2draft"sv) { + *it++ = NGTCP2_PROTO_VER_V2_DRAFT; + continue; + } + auto rv = util::parse_version(k); + if (!rv) { + std::cerr << "preferred-versions: invalid version " + << std::quoted(k) << std::endl; + exit(EXIT_FAILURE); + } + if (!ngtcp2_is_supported_version(*rv)) { + std::cerr << "preferred-versions: unsupported version " + << std::quoted(k) << std::endl; + exit(EXIT_FAILURE); + } + *it++ = *rv; + } + break; + } + case 40: + // --ack-thresh + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "ack-thresh: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else if (*n > 100) { + std::cerr << "ack-thresh: must not exceed 100" << std::endl; + exit(EXIT_FAILURE); + } else { + config.ack_thresh = *n; + } + break; + } + break; + default: + break; + }; + } + + if (argc - optind < 2) { + std::cerr << "Too few arguments" << std::endl; + print_usage(); + exit(EXIT_FAILURE); + } + + if (!config.qlog_file.empty() && !config.qlog_dir.empty()) { + std::cerr << "qlog-file and qlog-dir are mutually exclusive" << std::endl; + exit(EXIT_FAILURE); + } + + if (config.exit_on_first_stream_close && config.exit_on_all_streams_close) { + std::cerr << "exit-on-first-stream-close and exit-on-all-streams-close are " + "mutually exclusive" + << std::endl; + exit(EXIT_FAILURE); + } + + if (data_path) { + auto fd = open(data_path, O_RDONLY); + if (fd == -1) { + std::cerr << "data: Could not open file " << data_path << ": " + << strerror(errno) << std::endl; + exit(EXIT_FAILURE); + } + struct stat st; + if (fstat(fd, &st) != 0) { + std::cerr << "data: Could not stat file " << data_path << ": " + << strerror(errno) << std::endl; + exit(EXIT_FAILURE); + } + config.fd = fd; + config.datalen = st.st_size; + auto addr = mmap(nullptr, config.datalen, PROT_READ, MAP_SHARED, fd, 0); + if (addr == MAP_FAILED) { + std::cerr << "data: Could not mmap file " << data_path << ": " + << strerror(errno) << std::endl; + exit(EXIT_FAILURE); + } + config.data = static_cast<uint8_t *>(addr); + } + + auto addr = argv[optind++]; + auto port = argv[optind++]; + + if (parse_requests(&argv[optind], argc - optind) != 0) { + exit(EXIT_FAILURE); + } + + if (!ngtcp2_is_reserved_version(config.version)) { + if (!config.preferred_versions.empty() && + std::find(std::begin(config.preferred_versions), + std::end(config.preferred_versions), + config.version) == std::end(config.preferred_versions)) { + std::cerr << "preferred-version: must include version " + << "0x" << config.version << std::endl; + exit(EXIT_FAILURE); + } + + if (!config.other_versions.empty() && + std::find(std::begin(config.other_versions), + std::end(config.other_versions), + config.version) == std::end(config.other_versions)) { + std::cerr << "other-versions: must include version " + << "0x" << config.version << std::endl; + exit(EXIT_FAILURE); + } + } + + if (config.nstreams == 0) { + config.nstreams = config.requests.size(); + } + + TLSClientContext tls_ctx; + if (tls_ctx.init(private_key_file, cert_file) != 0) { + exit(EXIT_FAILURE); + } + + auto ev_loop_d = defer(ev_loop_destroy, EV_DEFAULT); + + auto keylog_filename = getenv("SSLKEYLOGFILE"); + if (keylog_filename) { + keylog_file.open(keylog_filename, std::ios_base::app); + if (keylog_file) { + tls_ctx.enable_keylog(); + } + } + + if (util::generate_secret(config.static_secret.data(), + config.static_secret.size()) != 0) { + std::cerr << "Unable to generate static secret" << std::endl; + exit(EXIT_FAILURE); + } + + auto client_chosen_version = config.version; + + for (;;) { + Client c(EV_DEFAULT, client_chosen_version, config.version); + + if (run(c, addr, port, tls_ctx) != 0) { + exit(EXIT_FAILURE); + } + + if (config.preferred_versions.empty()) { + break; + } + + auto &offered_versions = c.get_offered_versions(); + if (offered_versions.empty()) { + break; + } + + client_chosen_version = ngtcp2_select_version( + config.preferred_versions.data(), config.preferred_versions.size(), + offered_versions.data(), offered_versions.size()); + + if (client_chosen_version == 0) { + std::cerr << "Unable to select a version" << std::endl; + exit(EXIT_FAILURE); + } + + if (!config.quiet) { + std::cerr << "Client selected version " << std::hex << "0x" + << client_chosen_version << std::dec << std::endl; + } + } + + return EXIT_SUCCESS; +} diff --git a/examples/h09client.h b/examples/h09client.h new file mode 100644 index 0000000..702eb17 --- /dev/null +++ b/examples/h09client.h @@ -0,0 +1,196 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef H09CLIENT_H +#define H09CLIENT_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <vector> +#include <deque> +#include <map> +#include <string_view> +#include <memory> +#include <set> + +#include <ngtcp2/ngtcp2.h> +#include <ngtcp2/ngtcp2_crypto.h> + +#include <nghttp3/nghttp3.h> + +#include <ev.h> + +#include "client_base.h" +#include "tls_client_context.h" +#include "tls_client_session.h" +#include "network.h" +#include "shared.h" +#include "template.h" + +using namespace ngtcp2; + +struct Stream { + Stream(const Request &req, int64_t stream_id); + ~Stream(); + + int open_file(const std::string_view &path); + + Request req; + int64_t stream_id; + int fd; + std::string rawreqbuf; + nghttp3_buf reqbuf; +}; + +struct StreamIDLess { + constexpr bool operator()(const Stream *lhs, const Stream *rhs) const { + return lhs->stream_id < rhs->stream_id; + } +}; + +class Client; + +struct Endpoint { + Address addr; + ev_io rev; + Client *client; + int fd; +}; + +class Client : public ClientBase { +public: + Client(struct ev_loop *loop, uint32_t client_chosen_version, + uint32_t original_version); + ~Client(); + + int init(int fd, const Address &local_addr, const Address &remote_addr, + const char *addr, const char *port, TLSClientContext &tls_ctx); + void disconnect(); + + int on_read(const Endpoint &ep); + int on_write(); + int write_streams(); + int feed_data(const Endpoint &ep, const sockaddr *sa, socklen_t salen, + const ngtcp2_pkt_info *pi, uint8_t *data, size_t datalen); + int handle_expiry(); + void update_timer(); + int handshake_completed(); + int handshake_confirmed(); + void recv_version_negotiation(const uint32_t *sv, size_t nsv); + + int send_packet(const Endpoint &ep, const ngtcp2_addr &remote_addr, + unsigned int ecn, const uint8_t *data, size_t datalen); + int on_stream_close(int64_t stream_id, uint64_t app_error_code); + int on_extend_max_streams(); + int handle_error(); + int make_stream_early(); + int change_local_addr(); + void start_change_local_addr_timer(); + int update_key(uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen); + int initiate_key_update(); + void start_key_update_timer(); + void start_delay_stream_timer(); + + int select_preferred_address(Address &selected_addr, + const ngtcp2_preferred_addr *paddr); + + std::optional<Endpoint *> endpoint_for(const Address &remote_addr); + + void set_remote_addr(const ngtcp2_addr &remote_addr); + + int submit_http_request(Stream *stream); + int recv_stream_data(uint32_t flags, int64_t stream_id, const uint8_t *data, + size_t datalen); + int acked_stream_data_offset(int64_t stream_id, uint64_t offset, + uint64_t datalen); + int extend_max_stream_data(int64_t stream_id, uint64_t max_data); + + void write_qlog(const void *data, size_t datalen); + + void on_send_blocked(const Endpoint &ep, const ngtcp2_addr &remote_addr, + unsigned int ecn, size_t datalen); + void start_wev_endpoint(const Endpoint &ep); + int send_blocked_packet(); + + const std::vector<uint32_t> &get_offered_versions() const; + + void early_data_rejected(); + +private: + std::vector<Endpoint> endpoints_; + Address remote_addr_; + ev_io wev_; + ev_timer timer_; + ev_timer change_local_addr_timer_; + ev_timer key_update_timer_; + ev_timer delay_stream_timer_; + ev_signal sigintev_; + struct ev_loop *loop_; + std::map<int64_t, std::unique_ptr<Stream>> streams_; + std::set<Stream *, StreamIDLess> sendq_; + std::vector<uint32_t> offered_versions_; + // addr_ is the server host address. + const char *addr_; + // port_ is the server port. + const char *port_; + // nstreams_done_ is the number of streams opened. + size_t nstreams_done_; + // nstreams_closed_ is the number of streams get closed. + size_t nstreams_closed_; + // nkey_update_ is the number of key update occurred. + size_t nkey_update_; + uint32_t client_chosen_version_; + uint32_t original_version_; + // early_data_ is true if client attempts to do 0RTT data transfer. + bool early_data_; + // should_exit_ is true if client should exit rather than waiting + // for timeout. + bool should_exit_; + // should_exit_on_handshake_confirmed_ is true if client should exit + // when handshake confirmed. + bool should_exit_on_handshake_confirmed_; + // handshake_confirmed_ gets true after handshake has been + // confirmed. + bool handshake_confirmed_; + + struct { + bool send_blocked; + // blocked field is effective only when send_blocked is true. + struct { + const Endpoint *endpoint; + Address remote_addr; + unsigned int ecn; + size_t datalen; + } blocked; + std::array<uint8_t, 64_k> data; + } tx_; +}; + +#endif // CLIENT_H diff --git a/examples/h09server.cc b/examples/h09server.cc new file mode 100644 index 0000000..4af67da --- /dev/null +++ b/examples/h09server.cc @@ -0,0 +1,3034 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include <chrono> +#include <cstdlib> +#include <cassert> +#include <cstring> +#include <iostream> +#include <algorithm> +#include <memory> +#include <fstream> +#include <iomanip> + +#include <unistd.h> +#include <getopt.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <sys/mman.h> +#include <netinet/udp.h> +#include <net/if.h> + +#include <http-parser/http_parser.h> + +#include "h09server.h" +#include "network.h" +#include "debug.h" +#include "util.h" +#include "shared.h" +#include "http.h" +#include "template.h" + +using namespace ngtcp2; +using namespace std::literals; + +namespace { +constexpr size_t NGTCP2_SV_SCIDLEN = 18; +} // namespace + +namespace { +constexpr size_t max_preferred_versionslen = 4; +} // namespace + +namespace { +auto randgen = util::make_mt19937(); +} // namespace + +Config config{}; + +Stream::Stream(int64_t stream_id, Handler *handler) + : stream_id(stream_id), handler(handler), eos(false) { + nghttp3_buf_init(&respbuf); + htp.data = this; + http_parser_init(&htp, HTTP_REQUEST); +} + +namespace { +constexpr auto NGTCP2_SERVER = "ngtcp2 server"sv; +} // namespace + +namespace { +std::string make_status_body(unsigned int status_code) { + auto status_string = util::format_uint(status_code); + auto reason_phrase = http::get_reason_phrase(status_code); + + std::string body; + body = "<html><head><title>"; + body += status_string; + body += ' '; + body += reason_phrase; + body += "</title></head><body><h1>"; + body += status_string; + body += ' '; + body += reason_phrase; + body += "</h1><hr><address>"; + body += NGTCP2_SERVER; + body += " at port "; + body += util::format_uint(config.port); + body += "</address>"; + body += "</body></html>"; + return body; +} +} // namespace + +struct Request { + std::string path; +}; + +namespace { +Request request_path(const std::string_view &uri) { + http_parser_url u; + Request req; + + http_parser_url_init(&u); + + if (auto rv = http_parser_parse_url(uri.data(), uri.size(), + /* is_connect = */ 0, &u); + rv != 0) { + return req; + } + + if (u.field_set & (1 << UF_PATH)) { + req.path = std::string(uri.data() + u.field_data[UF_PATH].off, + u.field_data[UF_PATH].len); + if (req.path.find('%') != std::string::npos) { + req.path = util::percent_decode(std::begin(req.path), std::end(req.path)); + } + if (!req.path.empty() && req.path.back() == '/') { + req.path += "index.html"; + } + } else { + req.path = "/index.html"; + } + + req.path = util::normalize_path(req.path); + if (req.path == "/") { + req.path = "/index.html"; + } + + return req; +} +} // namespace + +enum FileEntryFlag { + FILE_ENTRY_TYPE_DIR = 0x1, +}; + +struct FileEntry { + uint64_t len; + void *map; + int fd; + uint8_t flags; +}; + +namespace { +std::unordered_map<std::string, FileEntry> file_cache; +} // namespace + +std::pair<FileEntry, int> Stream::open_file(const std::string &path) { + auto it = file_cache.find(path); + if (it != std::end(file_cache)) { + return {(*it).second, 0}; + } + + auto fd = open(path.c_str(), O_RDONLY); + if (fd == -1) { + return {{}, -1}; + } + + struct stat st {}; + if (fstat(fd, &st) != 0) { + close(fd); + return {{}, -1}; + } + + FileEntry fe{}; + if (st.st_mode & S_IFDIR) { + fe.flags |= FILE_ENTRY_TYPE_DIR; + fe.fd = -1; + close(fd); + } else { + fe.fd = fd; + fe.len = st.st_size; + fe.map = mmap(nullptr, fe.len, PROT_READ, MAP_SHARED, fd, 0); + if (fe.map == MAP_FAILED) { + std::cerr << "mmap: " << strerror(errno) << std::endl; + close(fd); + return {{}, -1}; + } + } + + file_cache.emplace(path, fe); + + return {std::move(fe), 0}; +} + +void Stream::map_file(const FileEntry &fe) { + respbuf.begin = respbuf.pos = static_cast<uint8_t *>(fe.map); + respbuf.end = respbuf.last = respbuf.begin + fe.len; +} + +int Stream::send_status_response(unsigned int status_code) { + status_resp_body = make_status_body(status_code); + + respbuf.begin = respbuf.pos = + reinterpret_cast<uint8_t *>(status_resp_body.data()); + respbuf.end = respbuf.last = respbuf.begin + status_resp_body.size(); + + handler->add_sendq(this); + handler->shutdown_read(stream_id, 0); + + return 0; +} + +int Stream::start_response() { + if (uri.empty()) { + return send_status_response(400); + } + + auto req = request_path(uri); + if (req.path.empty()) { + return send_status_response(400); + } + + auto path = config.htdocs + req.path; + auto [fe, rv] = open_file(path); + if (rv != 0) { + send_status_response(404); + return 0; + } + + if (fe.flags & FILE_ENTRY_TYPE_DIR) { + send_status_response(308); + return 0; + } + + map_file(fe); + + if (!config.quiet) { + std::array<nghttp3_nv, 1> nva{ + util::make_nv_nn(":status", "200"), + }; + + debug::print_http_response_headers(stream_id, nva.data(), nva.size()); + } + + handler->add_sendq(this); + + return 0; +} + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + auto h = static_cast<Handler *>(w->data); + auto s = h->server(); + + switch (h->on_write()) { + case 0: + case NETWORK_ERR_CLOSE_WAIT: + return; + default: + s->remove(h); + } +} +} // namespace + +namespace { +void close_waitcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto h = static_cast<Handler *>(w->data); + auto s = h->server(); + auto conn = h->conn(); + + if (ngtcp2_conn_is_in_closing_period(conn)) { + if (!config.quiet) { + std::cerr << "Closing Period is over" << std::endl; + } + + s->remove(h); + return; + } + if (ngtcp2_conn_is_in_draining_period(conn)) { + if (!config.quiet) { + std::cerr << "Draining Period is over" << std::endl; + } + + s->remove(h); + return; + } + + assert(0); +} +} // namespace + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + int rv; + + auto h = static_cast<Handler *>(w->data); + auto s = h->server(); + + if (!config.quiet) { + std::cerr << "Timer expired" << std::endl; + } + + rv = h->handle_expiry(); + if (rv != 0) { + goto fail; + } + + rv = h->on_write(); + if (rv != 0) { + goto fail; + } + + return; + +fail: + switch (rv) { + case NETWORK_ERR_CLOSE_WAIT: + ev_timer_stop(loop, w); + return; + default: + s->remove(h); + return; + } +} +} // namespace + +Handler::Handler(struct ev_loop *loop, Server *server) + : loop_(loop), + server_(server), + qlog_(nullptr), + scid_{}, + nkey_update_(0), + no_gso_{ +#ifdef UDP_SEGMENT + false +#else // !UDP_SEGMENT + true +#endif // !UDP_SEGMENT + }, + tx_{ + .data = std::unique_ptr<uint8_t[]>(new uint8_t[64_k]), + } { + ev_io_init(&wev_, writecb, 0, EV_WRITE); + wev_.data = this; + ev_timer_init(&timer_, timeoutcb, 0., 0.); + timer_.data = this; +} + +Handler::~Handler() { + if (!config.quiet) { + std::cerr << scid_ << " Closing QUIC connection " << std::endl; + } + + ev_timer_stop(loop_, &timer_); + ev_io_stop(loop_, &wev_); + + if (qlog_) { + fclose(qlog_); + } +} + +namespace { +int handshake_completed(ngtcp2_conn *conn, void *user_data) { + auto h = static_cast<Handler *>(user_data); + + if (!config.quiet) { + debug::handshake_completed(conn, user_data); + } + + if (h->handshake_completed() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Handler::handshake_completed() { + if (!config.quiet) { + std::cerr << "Negotiated cipher suite is " << tls_session_.get_cipher_name() + << std::endl; + std::cerr << "Negotiated ALPN is " << tls_session_.get_selected_alpn() + << std::endl; + } + + std::array<uint8_t, NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN> token; + + auto path = ngtcp2_conn_get_path(conn_); + auto t = std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + auto tokenlen = ngtcp2_crypto_generate_regular_token( + token.data(), config.static_secret.data(), config.static_secret.size(), + path->remote.addr, path->remote.addrlen, t); + if (tokenlen < 0) { + if (!config.quiet) { + std::cerr << "Unable to generate token" << std::endl; + } + return 0; + } + + if (auto rv = ngtcp2_conn_submit_new_token(conn_, token.data(), tokenlen); + rv != 0) { + if (!config.quiet) { + std::cerr << "ngtcp2_conn_submit_new_token: " << ngtcp2_strerror(rv) + << std::endl; + } + return -1; + } + + return 0; +} + +namespace { +int do_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, + const ngtcp2_crypto_cipher_ctx *hp_ctx, const uint8_t *sample) { + if (ngtcp2_crypto_hp_mask(dest, hp, hp_ctx, sample) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + if (!config.quiet && config.show_secret) { + debug::print_hp_mask(dest, NGTCP2_HP_MASKLEN, sample, NGTCP2_HP_SAMPLELEN); + } + + return 0; +} +} // namespace + +namespace { +int recv_crypto_data(ngtcp2_conn *conn, ngtcp2_crypto_level crypto_level, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_crypto_data(crypto_level, data, datalen); + } + + return ngtcp2_crypto_recv_crypto_data_cb(conn, crypto_level, offset, data, + datalen, user_data); +} +} // namespace + +namespace { +int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data, void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + + if (h->recv_stream_data(flags, stream_id, data, datalen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int acked_stream_data_offset(ngtcp2_conn *conn, int64_t stream_id, + uint64_t offset, uint64_t datalen, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + if (h->acked_stream_data_offset(stream_id, offset, datalen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Handler::acked_stream_data_offset(int64_t stream_id, uint64_t offset, + uint64_t datalen) { + auto it = streams_.find(stream_id); + assert(it != std::end(streams_)); + auto &stream = (*it).second; + (void)stream; + + assert(static_cast<uint64_t>(stream->respbuf.end - stream->respbuf.begin) >= + offset + datalen); + + return 0; +} + +namespace { +int stream_open(ngtcp2_conn *conn, int64_t stream_id, void *user_data) { + auto h = static_cast<Handler *>(user_data); + h->on_stream_open(stream_id); + return 0; +} +} // namespace + +void Handler::on_stream_open(int64_t stream_id) { + if (!ngtcp2_is_bidi_stream(stream_id)) { + return; + } + auto it = streams_.find(stream_id); + (void)it; + assert(it == std::end(streams_)); + streams_.emplace(stream_id, std::make_unique<Stream>(stream_id, this)); +} + +namespace { +int stream_close(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + if (h->on_stream_close(stream_id, app_error_code) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +namespace { +void rand(uint8_t *dest, size_t destlen, const ngtcp2_rand_ctx *rand_ctx) { + auto dis = std::uniform_int_distribution<uint8_t>(0, 255); + std::generate(dest, dest + destlen, [&dis]() { return dis(randgen); }); +} +} // namespace + +namespace { +int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, uint8_t *token, + size_t cidlen, void *user_data) { + if (util::generate_secure_random(cid->data, cidlen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + cid->datalen = cidlen; + if (ngtcp2_crypto_generate_stateless_reset_token( + token, config.static_secret.data(), config.static_secret.size(), + cid) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + auto h = static_cast<Handler *>(user_data); + h->server()->associate_cid(cid, h); + + return 0; +} +} // namespace + +namespace { +int remove_connection_id(ngtcp2_conn *conn, const ngtcp2_cid *cid, + void *user_data) { + auto h = static_cast<Handler *>(user_data); + h->server()->dissociate_cid(cid); + return 0; +} +} // namespace + +namespace { +int update_key(ngtcp2_conn *conn, uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen, + void *user_data) { + auto h = static_cast<Handler *>(user_data); + if (h->update_key(rx_secret, tx_secret, rx_aead_ctx, rx_iv, tx_aead_ctx, + tx_iv, current_rx_secret, current_tx_secret, + secretlen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +namespace { +int path_validation(ngtcp2_conn *conn, uint32_t flags, const ngtcp2_path *path, + ngtcp2_path_validation_result res, void *user_data) { + if (!config.quiet) { + debug::path_validation(path, res); + } + return 0; +} +} // namespace + +namespace { +int extend_max_stream_data(ngtcp2_conn *conn, int64_t stream_id, + uint64_t max_data, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + if (h->extend_max_stream_data(stream_id, max_data) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Handler::extend_max_stream_data(int64_t stream_id, uint64_t max_data) { + auto it = streams_.find(stream_id); + assert(it != std::end(streams_)); + auto &stream = (*it).second; + + if (nghttp3_buf_len(&stream->respbuf)) { + sendq_.emplace(stream.get()); + } + + return 0; +} + +namespace { +void write_qlog(void *user_data, uint32_t flags, const void *data, + size_t datalen) { + auto h = static_cast<Handler *>(user_data); + h->write_qlog(data, datalen); +} +} // namespace + +void Handler::write_qlog(const void *data, size_t datalen) { + assert(qlog_); + fwrite(data, 1, datalen, qlog_); +} + +int Handler::init(const Endpoint &ep, const Address &local_addr, + const sockaddr *sa, socklen_t salen, const ngtcp2_cid *dcid, + const ngtcp2_cid *scid, const ngtcp2_cid *ocid, + const uint8_t *token, size_t tokenlen, uint32_t version, + TLSServerContext &tls_ctx) { + auto callbacks = ngtcp2_callbacks{ + nullptr, // client_initial + ngtcp2_crypto_recv_client_initial_cb, + ::recv_crypto_data, + ::handshake_completed, + nullptr, // recv_version_negotiation + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + do_hp_mask, + ::recv_stream_data, + ::acked_stream_data_offset, + stream_open, + stream_close, + nullptr, // recv_stateless_reset + nullptr, // recv_retry + nullptr, // extend_max_streams_bidi + nullptr, // extend_max_streams_uni + rand, + get_new_connection_id, + remove_connection_id, + ::update_key, + path_validation, + nullptr, // select_preferred_addr + nullptr, // stream_reset + nullptr, // extend_max_remote_streams_bidi + nullptr, // extend_max_remote_streams_uni + ::extend_max_stream_data, + nullptr, // dcid_status + nullptr, // handshake_confirmed + nullptr, // recv_new_token + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + nullptr, // recv_datagram + nullptr, // ack_datagram + nullptr, // lost_datagram + ngtcp2_crypto_get_path_challenge_data_cb, + nullptr, // stream_stop_sending + ngtcp2_crypto_version_negotiation_cb, + nullptr, // recv_rx_key + nullptr, // recv_tx_key + }; + + scid_.datalen = NGTCP2_SV_SCIDLEN; + if (util::generate_secure_random(scid_.data, scid_.datalen) != 0) { + std::cerr << "Could not generate connection ID" << std::endl; + return -1; + } + + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + settings.log_printf = config.quiet ? nullptr : debug::log_printf; + settings.initial_ts = util::timestamp(loop_); + settings.token = ngtcp2_vec{const_cast<uint8_t *>(token), tokenlen}; + settings.cc_algo = config.cc_algo; + settings.initial_rtt = config.initial_rtt; + settings.max_window = config.max_window; + settings.max_stream_window = config.max_stream_window; + settings.handshake_timeout = config.handshake_timeout; + settings.no_pmtud = config.no_pmtud; + settings.ack_thresh = config.ack_thresh; + if (config.max_udp_payload_size) { + settings.max_tx_udp_payload_size = config.max_udp_payload_size; + settings.no_tx_udp_payload_size_shaping = 1; + } + if (!config.qlog_dir.empty()) { + auto path = std::string{config.qlog_dir}; + path += '/'; + path += util::format_hex(scid_.data, scid_.datalen); + path += ".sqlog"; + qlog_ = fopen(path.c_str(), "w"); + if (qlog_ == nullptr) { + std::cerr << "Could not open qlog file " << std::quoted(path) << ": " + << strerror(errno) << std::endl; + return -1; + } + settings.qlog.write = ::write_qlog; + settings.qlog.odcid = *scid; + } + if (!config.preferred_versions.empty()) { + settings.preferred_versions = config.preferred_versions.data(); + settings.preferred_versionslen = config.preferred_versions.size(); + } + if (!config.other_versions.empty()) { + settings.other_versions = config.other_versions.data(); + settings.other_versionslen = config.other_versions.size(); + } + + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + params.initial_max_stream_data_bidi_local = config.max_stream_data_bidi_local; + params.initial_max_stream_data_bidi_remote = + config.max_stream_data_bidi_remote; + params.initial_max_stream_data_uni = config.max_stream_data_uni; + params.initial_max_data = config.max_data; + params.initial_max_streams_bidi = config.max_streams_bidi; + params.initial_max_streams_uni = 0; + params.max_idle_timeout = config.timeout; + params.stateless_reset_token_present = 1; + params.active_connection_id_limit = 7; + + if (ocid) { + params.original_dcid = *ocid; + params.retry_scid = *scid; + params.retry_scid_present = 1; + } else { + params.original_dcid = *scid; + } + + if (util::generate_secure_random(params.stateless_reset_token, + sizeof(params.stateless_reset_token)) != 0) { + std::cerr << "Could not generate stateless reset token" << std::endl; + return -1; + } + + if (config.preferred_ipv4_addr.len || config.preferred_ipv6_addr.len) { + params.preferred_address_present = 1; + + if (config.preferred_ipv4_addr.len) { + params.preferred_address.ipv4 = config.preferred_ipv4_addr.su.in; + params.preferred_address.ipv4_present = 1; + } + + if (config.preferred_ipv6_addr.len) { + params.preferred_address.ipv6 = config.preferred_ipv6_addr.su.in6; + params.preferred_address.ipv6_present = 1; + } + + auto &token = params.preferred_address.stateless_reset_token; + if (util::generate_secure_random(token, sizeof(token)) != 0) { + std::cerr << "Could not generate preferred address stateless reset token" + << std::endl; + return -1; + } + + params.preferred_address.cid.datalen = NGTCP2_SV_SCIDLEN; + if (util::generate_secure_random(params.preferred_address.cid.data, + params.preferred_address.cid.datalen) != + 0) { + std::cerr << "Could not generate preferred address connection ID" + << std::endl; + return -1; + } + } + + auto path = ngtcp2_path{ + { + const_cast<sockaddr *>(&local_addr.su.sa), + local_addr.len, + }, + { + const_cast<sockaddr *>(sa), + salen, + }, + const_cast<Endpoint *>(&ep), + }; + if (auto rv = + ngtcp2_conn_server_new(&conn_, dcid, &scid_, &path, version, + &callbacks, &settings, ¶ms, nullptr, this); + rv != 0) { + std::cerr << "ngtcp2_conn_server_new: " << ngtcp2_strerror(rv) << std::endl; + return -1; + } + + if (tls_session_.init(tls_ctx, this) != 0) { + return -1; + } + + tls_session_.enable_keylog(); + + ngtcp2_conn_set_tls_native_handle(conn_, tls_session_.get_native_handle()); + + ev_io_set(&wev_, ep.fd, EV_WRITE); + + return 0; +} + +int Handler::feed_data(const Endpoint &ep, const Address &local_addr, + const sockaddr *sa, socklen_t salen, + const ngtcp2_pkt_info *pi, uint8_t *data, + size_t datalen) { + auto path = ngtcp2_path{ + { + const_cast<sockaddr *>(&local_addr.su.sa), + local_addr.len, + }, + { + const_cast<sockaddr *>(sa), + salen, + }, + const_cast<Endpoint *>(&ep), + }; + + if (auto rv = ngtcp2_conn_read_pkt(conn_, &path, pi, data, datalen, + util::timestamp(loop_)); + rv != 0) { + std::cerr << "ngtcp2_conn_read_pkt: " << ngtcp2_strerror(rv) << std::endl; + switch (rv) { + case NGTCP2_ERR_DRAINING: + start_draining_period(); + return NETWORK_ERR_CLOSE_WAIT; + case NGTCP2_ERR_RETRY: + return NETWORK_ERR_RETRY; + case NGTCP2_ERR_DROP_CONN: + return NETWORK_ERR_DROP_CONN; + case NGTCP2_ERR_CRYPTO: + if (!last_error_.error_code) { + ngtcp2_connection_close_error_set_transport_error_tls_alert( + &last_error_, ngtcp2_conn_get_tls_alert(conn_), nullptr, 0); + } + break; + default: + if (!last_error_.error_code) { + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, rv, nullptr, 0); + } + } + return handle_error(); + } + + return 0; +} + +int Handler::on_read(const Endpoint &ep, const Address &local_addr, + const sockaddr *sa, socklen_t salen, + const ngtcp2_pkt_info *pi, uint8_t *data, size_t datalen) { + if (auto rv = feed_data(ep, local_addr, sa, salen, pi, data, datalen); + rv != 0) { + return rv; + } + + update_timer(); + + return 0; +} + +int Handler::handle_expiry() { + auto now = util::timestamp(loop_); + if (auto rv = ngtcp2_conn_handle_expiry(conn_, now); rv != 0) { + std::cerr << "ngtcp2_conn_handle_expiry: " << ngtcp2_strerror(rv) + << std::endl; + ngtcp2_connection_close_error_set_transport_error_liberr(&last_error_, rv, + nullptr, 0); + return handle_error(); + } + + return 0; +} + +int Handler::on_write() { + if (ngtcp2_conn_is_in_closing_period(conn_) || + ngtcp2_conn_is_in_draining_period(conn_)) { + return 0; + } + + if (tx_.send_blocked) { + if (auto rv = send_blocked_packet(); rv != 0) { + return rv; + } + + if (tx_.send_blocked) { + return 0; + } + } + + if (auto rv = write_streams(); rv != 0) { + return rv; + } + + update_timer(); + + return 0; +} + +int Handler::write_streams() { + ngtcp2_vec vec; + ngtcp2_path_storage ps, prev_ps; + uint32_t prev_ecn = 0; + size_t pktcnt = 0; + auto max_udp_payload_size = ngtcp2_conn_get_max_tx_udp_payload_size(conn_); + auto path_max_udp_payload_size = + ngtcp2_conn_get_path_max_tx_udp_payload_size(conn_); + auto max_pktcnt = ngtcp2_conn_get_send_quantum(conn_) / max_udp_payload_size; + uint8_t *bufpos = tx_.data.get(); + ngtcp2_pkt_info pi; + size_t gso_size = 0; + auto ts = util::timestamp(loop_); + + ngtcp2_path_storage_zero(&ps); + ngtcp2_path_storage_zero(&prev_ps); + + max_pktcnt = std::min(max_pktcnt, static_cast<size_t>(config.max_gso_dgrams)); + + for (;;) { + int64_t stream_id = -1; + size_t vcnt = 0; + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + Stream *stream = nullptr; + + if (!sendq_.empty() && ngtcp2_conn_get_max_data_left(conn_)) { + stream = *std::begin(sendq_); + + stream_id = stream->stream_id; + vec.base = stream->respbuf.pos; + vec.len = nghttp3_buf_len(&stream->respbuf); + vcnt = 1; + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + ngtcp2_ssize ndatalen; + + auto nwrite = ngtcp2_conn_writev_stream(conn_, &ps.path, &pi, bufpos, + max_udp_payload_size, &ndatalen, + flags, stream_id, &vec, vcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + case NGTCP2_ERR_STREAM_SHUT_WR: + assert(ndatalen == -1); + sendq_.erase(std::begin(sendq_)); + continue; + case NGTCP2_ERR_WRITE_MORE: + assert(ndatalen >= 0); + stream->respbuf.pos += ndatalen; + if (nghttp3_buf_len(&stream->respbuf) == 0) { + sendq_.erase(std::begin(sendq_)); + } + continue; + } + + assert(ndatalen == -1); + + std::cerr << "ngtcp2_conn_writev_stream: " << ngtcp2_strerror(nwrite) + << std::endl; + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, nwrite, nullptr, 0); + return handle_error(); + } else if (ndatalen >= 0) { + stream->respbuf.pos += ndatalen; + if (nghttp3_buf_len(&stream->respbuf) == 0) { + sendq_.erase(std::begin(sendq_)); + } + } + + if (nwrite == 0) { + if (bufpos - tx_.data.get()) { + auto &ep = *static_cast<Endpoint *>(prev_ps.path.user_data); + auto data = tx_.data.get(); + auto datalen = bufpos - data; + + if (auto [nsent, rv] = server_->send_packet( + ep, no_gso_, prev_ps.path.local, prev_ps.path.remote, prev_ecn, + data, datalen, gso_size); + rv != NETWORK_ERR_OK) { + assert(NETWORK_ERR_SEND_BLOCKED == rv); + + on_send_blocked(ep, prev_ps.path.local, prev_ps.path.remote, prev_ecn, + data + nsent, datalen - nsent, gso_size); + + start_wev_endpoint(ep); + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + return 0; + } + } + + ev_io_stop(loop_, &wev_); + + // We are congestion limited. + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + return 0; + } + + bufpos += nwrite; + + if (pktcnt == 0) { + ngtcp2_path_copy(&prev_ps.path, &ps.path); + prev_ecn = pi.ecn; + gso_size = nwrite; + } else if (!ngtcp2_path_eq(&prev_ps.path, &ps.path) || prev_ecn != pi.ecn || + static_cast<size_t>(nwrite) > gso_size || + (gso_size > path_max_udp_payload_size && + static_cast<size_t>(nwrite) != gso_size)) { + auto &ep = *static_cast<Endpoint *>(prev_ps.path.user_data); + auto data = tx_.data.get(); + auto datalen = bufpos - data - nwrite; + + if (auto [nsent, rv] = server_->send_packet( + ep, no_gso_, prev_ps.path.local, prev_ps.path.remote, prev_ecn, + data, datalen, gso_size); + rv != 0) { + assert(NETWORK_ERR_SEND_BLOCKED == rv); + + on_send_blocked(ep, prev_ps.path.local, prev_ps.path.remote, prev_ecn, + data + nsent, datalen - nsent, gso_size); + + on_send_blocked(*static_cast<Endpoint *>(ps.path.user_data), + ps.path.local, ps.path.remote, pi.ecn, bufpos - nwrite, + nwrite, 0); + + start_wev_endpoint(ep); + } else { + auto &ep = *static_cast<Endpoint *>(ps.path.user_data); + auto data = bufpos - nwrite; + + if (auto [nsent, rv] = + server_->send_packet(ep, no_gso_, ps.path.local, ps.path.remote, + pi.ecn, data, nwrite, nwrite); + rv != 0) { + assert(nsent == 0); + assert(NETWORK_ERR_SEND_BLOCKED == rv); + + on_send_blocked(ep, ps.path.local, ps.path.remote, pi.ecn, data, + nwrite, 0); + } + + start_wev_endpoint(ep); + } + + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + return 0; + } + + if (++pktcnt == max_pktcnt || static_cast<size_t>(nwrite) < gso_size) { + auto &ep = *static_cast<Endpoint *>(ps.path.user_data); + auto data = tx_.data.get(); + auto datalen = bufpos - data; + + if (auto [nsent, rv] = + server_->send_packet(ep, no_gso_, ps.path.local, ps.path.remote, + pi.ecn, data, datalen, gso_size); + rv != 0) { + assert(NETWORK_ERR_SEND_BLOCKED == rv); + + on_send_blocked(ep, ps.path.local, ps.path.remote, pi.ecn, data + nsent, + datalen - nsent, gso_size); + } + + start_wev_endpoint(ep); + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + return 0; + } + } +} + +void Handler::on_send_blocked(Endpoint &ep, const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, unsigned int ecn, + const uint8_t *data, size_t datalen, + size_t gso_size) { + assert(tx_.num_blocked || !tx_.send_blocked); + assert(tx_.num_blocked < 2); + + tx_.send_blocked = true; + + auto &p = tx_.blocked[tx_.num_blocked++]; + + memcpy(&p.local_addr.su, local_addr.addr, local_addr.addrlen); + memcpy(&p.remote_addr.su, remote_addr.addr, remote_addr.addrlen); + + p.local_addr.len = local_addr.addrlen; + p.remote_addr.len = remote_addr.addrlen; + p.endpoint = &ep; + p.ecn = ecn; + p.data = data; + p.datalen = datalen; + p.gso_size = gso_size; +} + +void Handler::start_wev_endpoint(const Endpoint &ep) { + // We do not close ep.fd, so we can expect that each Endpoint has + // unique fd. + if (ep.fd != wev_.fd) { + if (ev_is_active(&wev_)) { + ev_io_stop(loop_, &wev_); + } + + ev_io_set(&wev_, ep.fd, EV_WRITE); + } + + ev_io_start(loop_, &wev_); +} + +int Handler::send_blocked_packet() { + assert(tx_.send_blocked); + + for (; tx_.num_blocked_sent < tx_.num_blocked; ++tx_.num_blocked_sent) { + auto &p = tx_.blocked[tx_.num_blocked_sent]; + + ngtcp2_addr local_addr{ + .addr = &p.local_addr.su.sa, + .addrlen = p.local_addr.len, + }; + ngtcp2_addr remote_addr{ + .addr = &p.remote_addr.su.sa, + .addrlen = p.remote_addr.len, + }; + + auto [nsent, rv] = + server_->send_packet(*p.endpoint, no_gso_, local_addr, remote_addr, + p.ecn, p.data, p.datalen, p.gso_size); + if (rv != 0) { + assert(NETWORK_ERR_SEND_BLOCKED == rv); + + p.data += nsent; + p.datalen -= nsent; + + start_wev_endpoint(*p.endpoint); + + return 0; + } + } + + tx_.send_blocked = false; + tx_.num_blocked = 0; + tx_.num_blocked_sent = 0; + + return 0; +} + +void Handler::signal_write() { ev_io_start(loop_, &wev_); } + +void Handler::start_draining_period() { + ev_io_stop(loop_, &wev_); + + ev_set_cb(&timer_, close_waitcb); + timer_.repeat = + static_cast<ev_tstamp>(ngtcp2_conn_get_pto(conn_)) / NGTCP2_SECONDS * 3; + ev_timer_again(loop_, &timer_); + + if (!config.quiet) { + std::cerr << "Draining period has started (" << timer_.repeat << " seconds)" + << std::endl; + } +} + +int Handler::start_closing_period() { + if (!conn_ || ngtcp2_conn_is_in_closing_period(conn_) || + ngtcp2_conn_is_in_draining_period(conn_)) { + return 0; + } + + ev_io_stop(loop_, &wev_); + + ev_set_cb(&timer_, close_waitcb); + timer_.repeat = + static_cast<ev_tstamp>(ngtcp2_conn_get_pto(conn_)) / NGTCP2_SECONDS * 3; + ev_timer_again(loop_, &timer_); + + if (!config.quiet) { + std::cerr << "Closing period has started (" << timer_.repeat << " seconds)" + << std::endl; + } + + conn_closebuf_ = std::make_unique<Buffer>(NGTCP2_MAX_UDP_PAYLOAD_SIZE); + + ngtcp2_path_storage ps; + + ngtcp2_path_storage_zero(&ps); + + ngtcp2_pkt_info pi; + auto n = ngtcp2_conn_write_connection_close( + conn_, &ps.path, &pi, conn_closebuf_->wpos(), conn_closebuf_->left(), + &last_error_, util::timestamp(loop_)); + if (n < 0) { + std::cerr << "ngtcp2_conn_write_connection_close: " << ngtcp2_strerror(n) + << std::endl; + return -1; + } + + if (n == 0) { + return 0; + } + + conn_closebuf_->push(n); + + return 0; +} + +int Handler::handle_error() { + if (last_error_.type == + NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_TRANSPORT_IDLE_CLOSE) { + return -1; + } + + if (start_closing_period() != 0) { + return -1; + } + + if (ngtcp2_conn_is_in_draining_period(conn_)) { + return NETWORK_ERR_CLOSE_WAIT; + } + + if (auto rv = send_conn_close(); rv != NETWORK_ERR_OK) { + return rv; + } + + return NETWORK_ERR_CLOSE_WAIT; +} + +int Handler::send_conn_close() { + if (!config.quiet) { + std::cerr << "Closing Period: TX CONNECTION_CLOSE" << std::endl; + } + + assert(conn_closebuf_ && conn_closebuf_->size()); + assert(conn_); + assert(!ngtcp2_conn_is_in_draining_period(conn_)); + + auto path = ngtcp2_conn_get_path(conn_); + + return server_->send_packet( + *static_cast<Endpoint *>(path->user_data), path->local, path->remote, + /* ecn = */ 0, conn_closebuf_->rpos(), conn_closebuf_->size()); +} + +void Handler::update_timer() { + auto expiry = ngtcp2_conn_get_expiry(conn_); + auto now = util::timestamp(loop_); + + if (expiry <= now) { + if (!config.quiet) { + auto t = static_cast<ev_tstamp>(now - expiry) / NGTCP2_SECONDS; + std::cerr << "Timer has already expired: " << t << "s" << std::endl; + } + + ev_feed_event(loop_, &timer_, EV_TIMER); + + return; + } + + auto t = static_cast<ev_tstamp>(expiry - now) / NGTCP2_SECONDS; + if (!config.quiet) { + std::cerr << "Set timer=" << std::fixed << t << "s" << std::defaultfloat + << std::endl; + } + timer_.repeat = t; + ev_timer_again(loop_, &timer_); +} + +namespace { +int on_msg_begin(http_parser *htp) { + auto s = static_cast<Stream *>(htp->data); + if (s->eos) { + return -1; + } + return 0; +} +} // namespace + +namespace { +int on_url_cb(http_parser *htp, const char *data, size_t datalen) { + auto s = static_cast<Stream *>(htp->data); + s->uri.append(data, datalen); + return 0; +} +} // namespace + +namespace { +int on_msg_complete(http_parser *htp) { + auto s = static_cast<Stream *>(htp->data); + s->eos = true; + if (s->start_response() != 0) { + return -1; + } + return 0; +} +} // namespace + +auto htp_settings = http_parser_settings{ + on_msg_begin, // on_message_begin + on_url_cb, // on_url + nullptr, // on_status + nullptr, // on_header_field + nullptr, // on_header_value + nullptr, // on_headers_complete + nullptr, // on_body + on_msg_complete, // on_message_complete + nullptr, // on_chunk_header, + nullptr, // on_chunk_complete +}; + +int Handler::recv_stream_data(uint32_t flags, int64_t stream_id, + const uint8_t *data, size_t datalen) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_stream_data(stream_id, data, datalen); + } + + auto it = streams_.find(stream_id); + assert(it != std::end(streams_)); + auto &stream = (*it).second; + + if (!stream->eos) { + auto nread = + http_parser_execute(&stream->htp, &htp_settings, + reinterpret_cast<const char *>(data), datalen); + if (nread != datalen) { + if (auto rv = ngtcp2_conn_shutdown_stream(conn_, stream_id, + /* app error code */ 1); + rv != 0) { + std::cerr << "ngtcp2_conn_shutdown_stream: " << ngtcp2_strerror(rv) + << std::endl; + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, NGTCP2_ERR_INTERNAL, nullptr, 0); + return -1; + } + } + } + + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, datalen); + ngtcp2_conn_extend_max_offset(conn_, datalen); + + return 0; +} + +int Handler::update_key(uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen) { + auto crypto_ctx = ngtcp2_conn_get_crypto_ctx(conn_); + auto aead = &crypto_ctx->aead; + auto keylen = ngtcp2_crypto_aead_keylen(aead); + auto ivlen = ngtcp2_crypto_packet_protection_ivlen(aead); + + ++nkey_update_; + + std::array<uint8_t, 64> rx_key, tx_key; + + if (ngtcp2_crypto_update_key(conn_, rx_secret, tx_secret, rx_aead_ctx, + rx_key.data(), rx_iv, tx_aead_ctx, tx_key.data(), + tx_iv, current_rx_secret, current_tx_secret, + secretlen) != 0) { + return -1; + } + + if (!config.quiet && config.show_secret) { + std::cerr << "application_traffic rx secret " << nkey_update_ << std::endl; + debug::print_secrets(rx_secret, secretlen, rx_key.data(), keylen, rx_iv, + ivlen); + std::cerr << "application_traffic tx secret " << nkey_update_ << std::endl; + debug::print_secrets(tx_secret, secretlen, tx_key.data(), keylen, tx_iv, + ivlen); + } + + return 0; +} + +Server *Handler::server() const { return server_; } + +int Handler::on_stream_close(int64_t stream_id, uint64_t app_error_code) { + if (!config.quiet) { + std::cerr << "QUIC stream " << stream_id << " closed" << std::endl; + } + + auto it = streams_.find(stream_id); + assert(it != std::end(streams_)); + auto &stream = (*it).second; + + sendq_.erase(stream.get()); + + if (!config.quiet) { + std::cerr << "HTTP stream " << stream_id << " closed with error code " + << app_error_code << std::endl; + } + + streams_.erase(it); + + if (ngtcp2_is_bidi_stream(stream_id)) { + assert(!ngtcp2_conn_is_local_stream(conn_, stream_id)); + ngtcp2_conn_extend_max_streams_bidi(conn_, 1); + } + + return 0; +} + +void Handler::shutdown_read(int64_t stream_id, int app_error_code) { + ngtcp2_conn_shutdown_stream_read(conn_, stream_id, app_error_code); +} + +void Handler::add_sendq(Stream *stream) { sendq_.emplace(stream); } + +namespace { +void sreadcb(struct ev_loop *loop, ev_io *w, int revents) { + auto ep = static_cast<Endpoint *>(w->data); + + ep->server->on_read(*ep); +} +} // namespace + +namespace { +void siginthandler(struct ev_loop *loop, ev_signal *watcher, int revents) { + ev_break(loop, EVBREAK_ALL); +} +} // namespace + +Server::Server(struct ev_loop *loop, TLSServerContext &tls_ctx) + : loop_(loop), tls_ctx_(tls_ctx) { + ev_signal_init(&sigintev_, siginthandler, SIGINT); +} + +Server::~Server() { + disconnect(); + close(); +} + +void Server::disconnect() { + config.tx_loss_prob = 0; + + for (auto &ep : endpoints_) { + ev_io_stop(loop_, &ep.rev); + } + + ev_signal_stop(loop_, &sigintev_); + + while (!handlers_.empty()) { + auto it = std::begin(handlers_); + auto &h = (*it).second; + + h->handle_error(); + + remove(h); + } +} + +void Server::close() { + for (auto &ep : endpoints_) { + ::close(ep.fd); + } + + endpoints_.clear(); +} + +namespace { +int create_sock(Address &local_addr, const char *addr, const char *port, + int family) { + addrinfo hints{}; + addrinfo *res, *rp; + int val = 1; + + hints.ai_family = family; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_PASSIVE; + + if (strcmp(addr, "*") == 0) { + addr = nullptr; + } + + if (auto rv = getaddrinfo(addr, port, &hints, &res); rv != 0) { + std::cerr << "getaddrinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + auto res_d = defer(freeaddrinfo, res); + + int fd = -1; + + for (rp = res; rp; rp = rp->ai_next) { + fd = util::create_nonblock_socket(rp->ai_family, rp->ai_socktype, + rp->ai_protocol); + if (fd == -1) { + continue; + } + + if (rp->ai_family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + close(fd); + continue; + } + + if (setsockopt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + close(fd); + continue; + } + } else if (setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + close(fd); + continue; + } + + if (bind(fd, rp->ai_addr, rp->ai_addrlen) != -1) { + break; + } + + close(fd); + } + + if (!rp) { + std::cerr << "Could not bind" << std::endl; + return -1; + } + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + close(fd); + return -1; + } + + fd_set_recv_ecn(fd, rp->ai_family); + fd_set_ip_mtu_discover(fd, rp->ai_family); + fd_set_ip_dontfrag(fd, family); + + socklen_t len = sizeof(local_addr.su.storage); + if (getsockname(fd, &local_addr.su.sa, &len) == -1) { + std::cerr << "getsockname: " << strerror(errno) << std::endl; + close(fd); + return -1; + } + local_addr.len = len; + local_addr.ifindex = 0; + + return fd; +} + +} // namespace + +namespace { +int add_endpoint(std::vector<Endpoint> &endpoints, const char *addr, + const char *port, int af) { + Address dest; + auto fd = create_sock(dest, addr, port, af); + if (fd == -1) { + return -1; + } + + endpoints.emplace_back(); + auto &ep = endpoints.back(); + ep.addr = dest; + ep.fd = fd; + ev_io_init(&ep.rev, sreadcb, 0, EV_READ); + + return 0; +} +} // namespace + +namespace { +int add_endpoint(std::vector<Endpoint> &endpoints, const Address &addr) { + auto fd = util::create_nonblock_socket(addr.su.sa.sa_family, SOCK_DGRAM, 0); + if (fd == -1) { + std::cerr << "socket: " << strerror(errno) << std::endl; + return -1; + } + + int val = 1; + if (addr.su.sa.sa_family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + std::cerr << "setsockopt: " << strerror(errno) << std::endl; + close(fd); + return -1; + } + + if (setsockopt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + std::cerr << "setsockopt: " << strerror(errno) << std::endl; + close(fd); + return -1; + } + } else if (setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + std::cerr << "setsockopt: " << strerror(errno) << std::endl; + close(fd); + return -1; + } + + if (bind(fd, &addr.su.sa, addr.len) == -1) { + std::cerr << "bind: " << strerror(errno) << std::endl; + close(fd); + return -1; + } + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + close(fd); + return -1; + } + + fd_set_recv_ecn(fd, addr.su.sa.sa_family); + fd_set_ip_mtu_discover(fd, addr.su.sa.sa_family); + fd_set_ip_dontfrag(fd, addr.su.sa.sa_family); + + endpoints.emplace_back(Endpoint{}); + auto &ep = endpoints.back(); + ep.addr = addr; + ep.fd = fd; + ev_io_init(&ep.rev, sreadcb, 0, EV_READ); + + return 0; +} +} // namespace + +int Server::init(const char *addr, const char *port) { + endpoints_.reserve(4); + + auto ready = false; + if (!util::numeric_host(addr, AF_INET6) && + add_endpoint(endpoints_, addr, port, AF_INET) == 0) { + ready = true; + } + if (!util::numeric_host(addr, AF_INET) && + add_endpoint(endpoints_, addr, port, AF_INET6) == 0) { + ready = true; + } + if (!ready) { + return -1; + } + + if (config.preferred_ipv4_addr.len && + add_endpoint(endpoints_, config.preferred_ipv4_addr) != 0) { + return -1; + } + if (config.preferred_ipv6_addr.len && + add_endpoint(endpoints_, config.preferred_ipv6_addr) != 0) { + return -1; + } + + for (auto &ep : endpoints_) { + ep.server = this; + ep.rev.data = &ep; + + ev_io_set(&ep.rev, ep.fd, EV_READ); + + ev_io_start(loop_, &ep.rev); + } + + ev_signal_start(loop_, &sigintev_); + + return 0; +} + +int Server::on_read(Endpoint &ep) { + sockaddr_union su; + std::array<uint8_t, 64_k> buf; + ngtcp2_pkt_hd hd; + size_t pktcnt = 0; + ngtcp2_pkt_info pi; + + iovec msg_iov; + msg_iov.iov_base = buf.data(); + msg_iov.iov_len = buf.size(); + + msghdr msg{}; + msg.msg_name = &su; + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + + uint8_t + msg_ctrl[CMSG_SPACE(sizeof(uint8_t)) + CMSG_SPACE(sizeof(in6_pktinfo))]; + msg.msg_control = msg_ctrl; + + for (; pktcnt < 10;) { + msg.msg_namelen = sizeof(su); + msg.msg_controllen = sizeof(msg_ctrl); + + auto nread = recvmsg(ep.fd, &msg, 0); + if (nread == -1) { + if (!(errno == EAGAIN || errno == ENOTCONN)) { + std::cerr << "recvmsg: " << strerror(errno) << std::endl; + } + return 0; + } + + ++pktcnt; + + pi.ecn = msghdr_get_ecn(&msg, su.storage.ss_family); + auto local_addr = msghdr_get_local_addr(&msg, su.storage.ss_family); + if (!local_addr) { + std::cerr << "Unable to obtain local address" << std::endl; + continue; + } + + set_port(*local_addr, ep.addr); + + if (!config.quiet) { + std::array<char, IF_NAMESIZE> ifname; + std::cerr << "Received packet: local=" + << util::straddr(&local_addr->su.sa, local_addr->len) + << " remote=" << util::straddr(&su.sa, msg.msg_namelen) + << " if=" << if_indextoname(local_addr->ifindex, ifname.data()) + << " ecn=0x" << std::hex << pi.ecn << std::dec << " " << nread + << " bytes" << std::endl; + } + + if (debug::packet_lost(config.rx_loss_prob)) { + if (!config.quiet) { + std::cerr << "** Simulated incoming packet loss **" << std::endl; + } + continue; + } + + if (nread == 0) { + continue; + } + + ngtcp2_version_cid vc; + + switch (auto rv = ngtcp2_pkt_decode_version_cid(&vc, buf.data(), nread, + NGTCP2_SV_SCIDLEN); + rv) { + case 0: + break; + case NGTCP2_ERR_VERSION_NEGOTIATION: + send_version_negotiation(vc.version, vc.scid, vc.scidlen, vc.dcid, + vc.dcidlen, ep, *local_addr, &su.sa, + msg.msg_namelen); + continue; + default: + std::cerr << "Could not decode version and CID from QUIC packet header: " + << ngtcp2_strerror(rv) << std::endl; + continue; + } + + auto dcid_key = util::make_cid_key(vc.dcid, vc.dcidlen); + + auto handler_it = handlers_.find(dcid_key); + if (handler_it == std::end(handlers_)) { + switch (auto rv = ngtcp2_accept(&hd, buf.data(), nread); rv) { + case 0: + break; + case NGTCP2_ERR_RETRY: + send_retry(&hd, ep, *local_addr, &su.sa, msg.msg_namelen, nread * 3); + continue; + default: + if (!config.quiet) { + std::cerr << "Unexpected packet received: length=" << nread + << std::endl; + } + continue; + } + + ngtcp2_cid ocid; + ngtcp2_cid *pocid = nullptr; + + assert(hd.type == NGTCP2_PKT_INITIAL); + + if (config.validate_addr || hd.token.len) { + std::cerr << "Perform stateless address validation" << std::endl; + if (hd.token.len == 0) { + send_retry(&hd, ep, *local_addr, &su.sa, msg.msg_namelen, nread * 3); + continue; + } + + if (hd.token.base[0] != NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY && + hd.dcid.datalen < NGTCP2_MIN_INITIAL_DCIDLEN) { + send_stateless_connection_close(&hd, ep, *local_addr, &su.sa, + msg.msg_namelen); + continue; + } + + switch (hd.token.base[0]) { + case NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY: + if (verify_retry_token(&ocid, &hd, &su.sa, msg.msg_namelen) != 0) { + send_stateless_connection_close(&hd, ep, *local_addr, &su.sa, + msg.msg_namelen); + continue; + } + pocid = &ocid; + break; + case NGTCP2_CRYPTO_TOKEN_MAGIC_REGULAR: + if (verify_token(&hd, &su.sa, msg.msg_namelen) != 0) { + if (config.validate_addr) { + send_retry(&hd, ep, *local_addr, &su.sa, msg.msg_namelen, + nread * 3); + continue; + } + + hd.token.base = nullptr; + hd.token.len = 0; + } + break; + default: + if (!config.quiet) { + std::cerr << "Ignore unrecognized token" << std::endl; + } + if (config.validate_addr) { + send_retry(&hd, ep, *local_addr, &su.sa, msg.msg_namelen, + nread * 3); + continue; + } + + hd.token.base = nullptr; + hd.token.len = 0; + break; + } + } + + auto h = std::make_unique<Handler>(loop_, this); + if (h->init(ep, *local_addr, &su.sa, msg.msg_namelen, &hd.scid, &hd.dcid, + pocid, hd.token.base, hd.token.len, hd.version, + tls_ctx_) != 0) { + continue; + } + + switch (h->on_read(ep, *local_addr, &su.sa, msg.msg_namelen, &pi, + buf.data(), nread)) { + case 0: + break; + case NETWORK_ERR_RETRY: + send_retry(&hd, ep, *local_addr, &su.sa, msg.msg_namelen, nread * 3); + continue; + default: + continue; + } + + switch (h->on_write()) { + case 0: + break; + default: + continue; + } + + std::array<ngtcp2_cid, 2> scids; + auto conn = h->conn(); + + auto num_scid = ngtcp2_conn_get_num_scid(conn); + + assert(num_scid <= scids.size()); + + ngtcp2_conn_get_scid(conn, scids.data()); + + for (size_t i = 0; i < num_scid; ++i) { + handlers_.emplace(util::make_cid_key(&scids[i]), h.get()); + } + + handlers_.emplace(dcid_key, h.get()); + + h.release(); + + continue; + } + + auto h = (*handler_it).second; + auto conn = h->conn(); + if (ngtcp2_conn_is_in_closing_period(conn)) { + // TODO do exponential backoff. + switch (h->send_conn_close()) { + case 0: + break; + default: + remove(h); + } + continue; + } + if (ngtcp2_conn_is_in_draining_period(conn)) { + continue; + } + + if (auto rv = h->on_read(ep, *local_addr, &su.sa, msg.msg_namelen, &pi, + buf.data(), nread); + rv != 0) { + if (rv != NETWORK_ERR_CLOSE_WAIT) { + remove(h); + } + continue; + } + + h->signal_write(); + } + + return 0; +} + +namespace { +uint32_t generate_reserved_version(const sockaddr *sa, socklen_t salen, + uint32_t version) { + uint32_t h = 0x811C9DC5u; + const uint8_t *p = (const uint8_t *)sa; + const uint8_t *ep = p + salen; + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + version = htonl(version); + p = (const uint8_t *)&version; + ep = p + sizeof(version); + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + h &= 0xf0f0f0f0u; + h |= 0x0a0a0a0au; + return h; +} +} // namespace + +int Server::send_version_negotiation(uint32_t version, const uint8_t *dcid, + size_t dcidlen, const uint8_t *scid, + size_t scidlen, Endpoint &ep, + const Address &local_addr, + const sockaddr *sa, socklen_t salen) { + Buffer buf{NGTCP2_MAX_UDP_PAYLOAD_SIZE}; + std::array<uint32_t, 1 + max_preferred_versionslen> sv; + + auto p = std::begin(sv); + + *p++ = generate_reserved_version(sa, salen, version); + + if (config.preferred_versions.empty()) { + *p++ = NGTCP2_PROTO_VER_V1; + } else { + for (auto v : config.preferred_versions) { + *p++ = v; + } + } + + auto nwrite = ngtcp2_pkt_write_version_negotiation( + buf.wpos(), buf.left(), + std::uniform_int_distribution<uint8_t>( + 0, std::numeric_limits<uint8_t>::max())(randgen), + dcid, dcidlen, scid, scidlen, sv.data(), p - std::begin(sv)); + if (nwrite < 0) { + std::cerr << "ngtcp2_pkt_write_version_negotiation: " + << ngtcp2_strerror(nwrite) << std::endl; + return -1; + } + + buf.push(nwrite); + + ngtcp2_addr laddr{ + const_cast<sockaddr *>(&local_addr.su.sa), + local_addr.len, + }; + ngtcp2_addr raddr{ + const_cast<sockaddr *>(sa), + salen, + }; + + if (send_packet(ep, laddr, raddr, /* ecn = */ 0, buf.rpos(), buf.size()) != + NETWORK_ERR_OK) { + return -1; + } + + return 0; +} + +int Server::send_retry(const ngtcp2_pkt_hd *chd, Endpoint &ep, + const Address &local_addr, const sockaddr *sa, + socklen_t salen, size_t max_pktlen) { + std::array<char, NI_MAXHOST> host; + std::array<char, NI_MAXSERV> port; + + if (auto rv = getnameinfo(sa, salen, host.data(), host.size(), port.data(), + port.size(), NI_NUMERICHOST | NI_NUMERICSERV); + rv != 0) { + std::cerr << "getnameinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + if (!config.quiet) { + std::cerr << "Sending Retry packet to [" << host.data() + << "]:" << port.data() << std::endl; + } + + ngtcp2_cid scid; + + scid.datalen = NGTCP2_SV_SCIDLEN; + if (util::generate_secure_random(scid.data, scid.datalen) != 0) { + return -1; + } + + std::array<uint8_t, NGTCP2_CRYPTO_MAX_RETRY_TOKENLEN> token; + + auto t = std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + auto tokenlen = ngtcp2_crypto_generate_retry_token( + token.data(), config.static_secret.data(), config.static_secret.size(), + chd->version, sa, salen, &scid, &chd->dcid, t); + if (tokenlen < 0) { + return -1; + } + + if (!config.quiet) { + std::cerr << "Generated address validation token:" << std::endl; + util::hexdump(stderr, token.data(), tokenlen); + } + + Buffer buf{ + std::min(static_cast<size_t>(NGTCP2_MAX_UDP_PAYLOAD_SIZE), max_pktlen)}; + + auto nwrite = ngtcp2_crypto_write_retry(buf.wpos(), buf.left(), chd->version, + &chd->scid, &scid, &chd->dcid, + token.data(), tokenlen); + if (nwrite < 0) { + std::cerr << "ngtcp2_crypto_write_retry failed" << std::endl; + return -1; + } + + buf.push(nwrite); + + ngtcp2_addr laddr{ + const_cast<sockaddr *>(&local_addr.su.sa), + local_addr.len, + }; + ngtcp2_addr raddr{ + const_cast<sockaddr *>(sa), + salen, + }; + + if (send_packet(ep, laddr, raddr, /* ecn = */ 0, buf.rpos(), buf.size()) != + NETWORK_ERR_OK) { + return -1; + } + + return 0; +} + +int Server::send_stateless_connection_close(const ngtcp2_pkt_hd *chd, + Endpoint &ep, + const Address &local_addr, + const sockaddr *sa, + socklen_t salen) { + Buffer buf{NGTCP2_MAX_UDP_PAYLOAD_SIZE}; + + auto nwrite = ngtcp2_crypto_write_connection_close( + buf.wpos(), buf.left(), chd->version, &chd->scid, &chd->dcid, + NGTCP2_INVALID_TOKEN, nullptr, 0); + if (nwrite < 0) { + std::cerr << "ngtcp2_crypto_write_connection_close failed" << std::endl; + return -1; + } + + buf.push(nwrite); + + ngtcp2_addr laddr{ + const_cast<sockaddr *>(&local_addr.su.sa), + local_addr.len, + }; + ngtcp2_addr raddr{ + const_cast<sockaddr *>(sa), + salen, + }; + + if (send_packet(ep, laddr, raddr, /* ecn = */ 0, buf.rpos(), buf.size()) != + NETWORK_ERR_OK) { + return -1; + } + + return 0; +} + +int Server::verify_retry_token(ngtcp2_cid *ocid, const ngtcp2_pkt_hd *hd, + const sockaddr *sa, socklen_t salen) { + std::array<char, NI_MAXHOST> host; + std::array<char, NI_MAXSERV> port; + + if (auto rv = getnameinfo(sa, salen, host.data(), host.size(), port.data(), + port.size(), NI_NUMERICHOST | NI_NUMERICSERV); + rv != 0) { + std::cerr << "getnameinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + if (!config.quiet) { + std::cerr << "Verifying Retry token from [" << host.data() + << "]:" << port.data() << std::endl; + util::hexdump(stderr, hd->token.base, hd->token.len); + } + + auto t = std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + if (ngtcp2_crypto_verify_retry_token( + ocid, hd->token.base, hd->token.len, config.static_secret.data(), + config.static_secret.size(), hd->version, sa, salen, &hd->dcid, + 10 * NGTCP2_SECONDS, t) != 0) { + std::cerr << "Could not verify Retry token" << std::endl; + + return -1; + } + + if (!config.quiet) { + std::cerr << "Token was successfully validated" << std::endl; + } + + return 0; +} + +int Server::verify_token(const ngtcp2_pkt_hd *hd, const sockaddr *sa, + socklen_t salen) { + std::array<char, NI_MAXHOST> host; + std::array<char, NI_MAXSERV> port; + + if (auto rv = getnameinfo(sa, salen, host.data(), host.size(), port.data(), + port.size(), NI_NUMERICHOST | NI_NUMERICSERV); + rv != 0) { + std::cerr << "getnameinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + if (!config.quiet) { + std::cerr << "Verifying token from [" << host.data() << "]:" << port.data() + << std::endl; + util::hexdump(stderr, hd->token.base, hd->token.len); + } + + auto t = std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + if (ngtcp2_crypto_verify_regular_token(hd->token.base, hd->token.len, + config.static_secret.data(), + config.static_secret.size(), sa, salen, + 3600 * NGTCP2_SECONDS, t) != 0) { + std::cerr << "Could not verify token" << std::endl; + + return -1; + } + + if (!config.quiet) { + std::cerr << "Token was successfully validated" << std::endl; + } + + return 0; +} + +int Server::send_packet(Endpoint &ep, const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, unsigned int ecn, + const uint8_t *data, size_t datalen) { + auto no_gso = false; + auto [_, rv] = send_packet(ep, no_gso, local_addr, remote_addr, ecn, data, + datalen, datalen); + + return rv; +} + +std::pair<size_t, int> +Server::send_packet(Endpoint &ep, bool &no_gso, const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, unsigned int ecn, + const uint8_t *data, size_t datalen, size_t gso_size) { + assert(gso_size); + + if (debug::packet_lost(config.tx_loss_prob)) { + if (!config.quiet) { + std::cerr << "** Simulated outgoing packet loss **" << std::endl; + } + return {0, NETWORK_ERR_OK}; + } + + if (no_gso && datalen > gso_size) { + size_t nsent = 0; + + for (auto p = data; p < data + datalen; p += gso_size) { + auto len = std::min(gso_size, static_cast<size_t>(data + datalen - p)); + + auto [n, rv] = + send_packet(ep, no_gso, local_addr, remote_addr, ecn, p, len, len); + if (rv != 0) { + return {nsent, rv}; + } + + nsent += n; + } + + return {nsent, 0}; + } + + iovec msg_iov; + msg_iov.iov_base = const_cast<uint8_t *>(data); + msg_iov.iov_len = datalen; + + msghdr msg{}; + msg.msg_name = const_cast<sockaddr *>(remote_addr.addr); + msg.msg_namelen = remote_addr.addrlen; + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + + uint8_t + msg_ctrl[CMSG_SPACE(sizeof(uint16_t)) + CMSG_SPACE(sizeof(in6_pktinfo))]; + + memset(msg_ctrl, 0, sizeof(msg_ctrl)); + + msg.msg_control = msg_ctrl; + msg.msg_controllen = sizeof(msg_ctrl); + + size_t controllen = 0; + + auto cm = CMSG_FIRSTHDR(&msg); + + switch (local_addr.addr->sa_family) { + case AF_INET: { + controllen += CMSG_SPACE(sizeof(in_pktinfo)); + cm->cmsg_level = IPPROTO_IP; + cm->cmsg_type = IP_PKTINFO; + cm->cmsg_len = CMSG_LEN(sizeof(in_pktinfo)); + auto pktinfo = reinterpret_cast<in_pktinfo *>(CMSG_DATA(cm)); + memset(pktinfo, 0, sizeof(in_pktinfo)); + auto addrin = reinterpret_cast<sockaddr_in *>(local_addr.addr); + pktinfo->ipi_spec_dst = addrin->sin_addr; + break; + } + case AF_INET6: { + controllen += CMSG_SPACE(sizeof(in6_pktinfo)); + cm->cmsg_level = IPPROTO_IPV6; + cm->cmsg_type = IPV6_PKTINFO; + cm->cmsg_len = CMSG_LEN(sizeof(in6_pktinfo)); + auto pktinfo = reinterpret_cast<in6_pktinfo *>(CMSG_DATA(cm)); + memset(pktinfo, 0, sizeof(in6_pktinfo)); + auto addrin = reinterpret_cast<sockaddr_in6 *>(local_addr.addr); + pktinfo->ipi6_addr = addrin->sin6_addr; + break; + } + default: + assert(0); + } + +#ifdef UDP_SEGMENT + if (datalen > gso_size) { + controllen += CMSG_SPACE(sizeof(uint16_t)); + cm = CMSG_NXTHDR(&msg, cm); + cm->cmsg_level = SOL_UDP; + cm->cmsg_type = UDP_SEGMENT; + cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); + *(reinterpret_cast<uint16_t *>(CMSG_DATA(cm))) = gso_size; + } +#endif // UDP_SEGMENT + + msg.msg_controllen = controllen; + + if (ep.ecn != ecn) { + ep.ecn = ecn; + fd_set_ecn(ep.fd, ep.addr.su.storage.ss_family, ecn); + } + + ssize_t nwrite = 0; + + do { + nwrite = sendmsg(ep.fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + switch (errno) { + case EAGAIN: +#if EAGAIN != EWOULDBLOCK + case EWOULDBLOCK: +#endif // EAGAIN != EWOULDBLOCK + return {0, NETWORK_ERR_SEND_BLOCKED}; +#ifdef UDP_SEGMENT + case EIO: + if (datalen > gso_size) { + // GSO failure; send each packet in a separate sendmsg call. + std::cerr << "sendmsg: disabling GSO due to " << strerror(errno) + << std::endl; + + no_gso = true; + + return send_packet(ep, no_gso, local_addr, remote_addr, ecn, data, + datalen, gso_size); + } + break; +#endif // UDP_SEGMENT + } + + std::cerr << "sendmsg: " << strerror(errno) << std::endl; + // TODO We have packet which is expected to fail to send (e.g., + // path validation to old path). + return {0, NETWORK_ERR_OK}; + } + + if (!config.quiet) { + std::cerr << "Sent packet: local=" + << util::straddr(local_addr.addr, local_addr.addrlen) + << " remote=" + << util::straddr(remote_addr.addr, remote_addr.addrlen) + << " ecn=0x" << std::hex << ecn << std::dec << " " << nwrite + << " bytes" << std::endl; + } + + return {nwrite, NETWORK_ERR_OK}; +} + +void Server::associate_cid(const ngtcp2_cid *cid, Handler *h) { + handlers_.emplace(util::make_cid_key(cid), h); +} + +void Server::dissociate_cid(const ngtcp2_cid *cid) { + handlers_.erase(util::make_cid_key(cid)); +} + +void Server::remove(const Handler *h) { + auto conn = h->conn(); + + handlers_.erase( + util::make_cid_key(ngtcp2_conn_get_client_initial_dcid(conn))); + + std::vector<ngtcp2_cid> cids(ngtcp2_conn_get_num_scid(conn)); + ngtcp2_conn_get_scid(conn, cids.data()); + + for (auto &cid : cids) { + handlers_.erase(util::make_cid_key(&cid)); + } + + delete h; +} + +namespace { +int parse_host_port(Address &dest, int af, const char *first, + const char *last) { + if (std::distance(first, last) == 0) { + return -1; + } + + const char *host_begin, *host_end, *it; + if (*first == '[') { + host_begin = first + 1; + it = std::find(host_begin, last, ']'); + if (it == last) { + return -1; + } + host_end = it; + ++it; + if (it == last || *it != ':') { + return -1; + } + } else { + host_begin = first; + it = std::find(host_begin, last, ':'); + if (it == last) { + return -1; + } + host_end = it; + } + + if (++it == last) { + return -1; + } + auto svc_begin = it; + + std::array<char, NI_MAXHOST> host; + *std::copy(host_begin, host_end, std::begin(host)) = '\0'; + + addrinfo hints{}, *res; + hints.ai_family = af; + hints.ai_socktype = SOCK_DGRAM; + + if (auto rv = getaddrinfo(host.data(), svc_begin, &hints, &res); rv != 0) { + std::cerr << "getaddrinfo: [" << host.data() << "]:" << svc_begin << ": " + << gai_strerror(rv) << std::endl; + return -1; + } + + dest.len = res->ai_addrlen; + memcpy(&dest.su, res->ai_addr, res->ai_addrlen); + + freeaddrinfo(res); + + return 0; +} +} // namespace + +namespace { +void print_usage() { + std::cerr << "Usage: server [OPTIONS] <ADDR> <PORT> <PRIVATE_KEY_FILE> " + "<CERTIFICATE_FILE>" + << std::endl; +} +} // namespace + +namespace { +void config_set_default(Config &config) { + config = Config{}; + config.tx_loss_prob = 0.; + config.rx_loss_prob = 0.; + config.ciphers = util::crypto_default_ciphers(); + config.groups = util::crypto_default_groups(); + config.timeout = 30 * NGTCP2_SECONDS; + { + auto path = realpath(".", nullptr); + assert(path); + config.htdocs = path; + free(path); + } + config.mime_types_file = "/etc/mime.types"sv; + config.max_data = 1_m; + config.max_stream_data_bidi_local = 256_k; + config.max_stream_data_bidi_remote = 256_k; + config.max_stream_data_uni = 256_k; + config.max_window = 6_m; + config.max_stream_window = 6_m; + config.max_streams_bidi = 100; + config.max_streams_uni = 3; + config.max_dyn_length = 20_m; + config.cc_algo = NGTCP2_CC_ALGO_CUBIC; + config.initial_rtt = NGTCP2_DEFAULT_INITIAL_RTT; + config.max_gso_dgrams = 64; + config.handshake_timeout = NGTCP2_DEFAULT_HANDSHAKE_TIMEOUT; + config.ack_thresh = 2; +} +} // namespace + +namespace { +void print_help() { + print_usage(); + + config_set_default(config); + + std::cout << R"( + <ADDR> Address to listen to. '*' binds to any address. + <PORT> Port + <PRIVATE_KEY_FILE> + Path to private key file + <CERTIFICATE_FILE> + Path to certificate file +Options: + -t, --tx-loss=<P> + The probability of losing outgoing packets. <P> must be + [0.0, 1.0], inclusive. 0.0 means no packet loss. 1.0 + means 100% packet loss. + -r, --rx-loss=<P> + The probability of losing incoming packets. <P> must be + [0.0, 1.0], inclusive. 0.0 means no packet loss. 1.0 + means 100% packet loss. + --ciphers=<CIPHERS> + Specify the cipher suite list to enable. + Default: )" + << config.ciphers << R"( + --groups=<GROUPS> + Specify the supported groups. + Default: )" + << config.groups << R"( + -d, --htdocs=<PATH> + Specify document root. If this option is not specified, + the document root is the current working directory. + -q, --quiet Suppress debug output. + -s, --show-secret + Print out secrets unless --quiet is used. + --timeout=<DURATION> + Specify idle timeout. + Default: )" + << util::format_duration(config.timeout) << R"( + -V, --validate-addr + Perform address validation. + --preferred-ipv4-addr=<ADDR>:<PORT> + Specify preferred IPv4 address and port. + --preferred-ipv6-addr=<ADDR>:<PORT> + Specify preferred IPv6 address and port. A numeric IPv6 + address must be enclosed by '[' and ']' (e.g., + [::1]:8443) + --mime-types-file=<PATH> + Path to file that contains MIME media types and the + extensions. + Default: )" + << config.mime_types_file << R"( + --early-response + Start sending response when it receives HTTP header + fields without waiting for request body. If HTTP + response data is written before receiving request body, + STOP_SENDING is sent. + --verify-client + Request a client certificate. At the moment, we just + request a certificate and no verification is done. + --qlog-dir=<PATH> + Path to the directory where qlog file is stored. The + file name of each qlog is the Source Connection ID of + server. + --no-quic-dump + Disables printing QUIC STREAM and CRYPTO frame data out. + --no-http-dump + Disables printing HTTP response body out. + --max-data=<SIZE> + The initial connection-level flow control window. + Default: )" + << util::format_uint_iec(config.max_data) << R"( + --max-stream-data-bidi-local=<SIZE> + The initial stream-level flow control window for a + bidirectional stream that the local endpoint initiates. + Default: )" + << util::format_uint_iec(config.max_stream_data_bidi_local) << R"( + --max-stream-data-bidi-remote=<SIZE> + The initial stream-level flow control window for a + bidirectional stream that the remote endpoint initiates. + Default: )" + << util::format_uint_iec(config.max_stream_data_bidi_remote) << R"( + --max-stream-data-uni=<SIZE> + The initial stream-level flow control window for a + unidirectional stream. + Default: )" + << util::format_uint_iec(config.max_stream_data_uni) << R"( + --max-streams-bidi=<N> + The number of the concurrent bidirectional streams. + Default: )" + << config.max_streams_bidi << R"( + --max-streams-uni=<N> + The number of the concurrent unidirectional streams. + Default: )" + << config.max_streams_uni << R"( + --max-dyn-length=<SIZE> + The maximum length of a dynamically generated content. + Default: )" + << util::format_uint_iec(config.max_dyn_length) << R"( + --cc=(cubic|reno|bbr|bbr2) + The name of congestion controller algorithm. + Default: )" + << util::strccalgo(config.cc_algo) << R"( + --initial-rtt=<DURATION> + Set an initial RTT. + Default: )" + << util::format_duration(config.initial_rtt) << R"( + --max-udp-payload-size=<SIZE> + Override maximum UDP payload size that server transmits. + --max-window=<SIZE> + Maximum connection-level flow control window size. The + window auto-tuning is enabled if nonzero value is given, + and window size is scaled up to this value. + Default: )" + << util::format_uint_iec(config.max_window) << R"( + --max-stream-window=<SIZE> + Maximum stream-level flow control window size. The + window auto-tuning is enabled if nonzero value is given, + and window size is scaled up to this value. + Default: )" + << util::format_uint_iec(config.max_stream_window) << R"( + --send-trailers + Send trailer fields. + --max-gso-dgrams=<N> + Maximum number of UDP datagrams that are sent in a + single GSO sendmsg call. + Default: )" + << config.max_gso_dgrams << R"( + --handshake-timeout=<DURATION> + Set the QUIC handshake timeout. + Default: )" + << util::format_duration(config.handshake_timeout) << R"( + --preferred-versions=<HEX>[[,<HEX>]...] + Specify QUIC versions in hex string in the order of + preference. Server negotiates one of those versions if + client initially selects a less preferred version. + These versions must be supported by libngtcp2. Instead + of specifying hex string, there are special aliases + available: "v1" indicates QUIC v1, and "v2draft" + indicates QUIC v2 draft. + --other-versions=<HEX>[[,<HEX>]...] + Specify QUIC versions in hex string that are sent in + other_versions field of version_information transport + parameter. This list can include a version which is not + supported by libngtcp2. Instead of specifying hex + string, there are special aliases available: "v1" + indicates QUIC v1, and "v2draft" indicates QUIC v2 + draft. + --no-pmtud Disables Path MTU Discovery. + --ack-thresh=<N> + The minimum number of the received ACK eliciting packets + that triggers immediate acknowledgement. + Default: )" + << config.ack_thresh << R"( + -h, --help Display this help and exit. + +--- + + The <SIZE> argument is an integer and an optional unit (e.g., 10K is + 10 * 1024). Units are K, M and G (powers of 1024). + + The <DURATION> argument is an integer and an optional unit (e.g., 1s + is 1 second and 500ms is 500 milliseconds). Units are h, m, s, ms, + us, or ns (hours, minutes, seconds, milliseconds, microseconds, and + nanoseconds respectively). If a unit is omitted, a second is used + as unit. + + The <HEX> argument is an hex string which must start with "0x" + (e.g., 0x00000001).)" + << std::endl; +} +} // namespace + +std::ofstream keylog_file; + +int main(int argc, char **argv) { + config_set_default(config); + + for (;;) { + static int flag = 0; + constexpr static option long_opts[] = { + {"help", no_argument, nullptr, 'h'}, + {"tx-loss", required_argument, nullptr, 't'}, + {"rx-loss", required_argument, nullptr, 'r'}, + {"htdocs", required_argument, nullptr, 'd'}, + {"quiet", no_argument, nullptr, 'q'}, + {"show-secret", no_argument, nullptr, 's'}, + {"validate-addr", no_argument, nullptr, 'V'}, + {"ciphers", required_argument, &flag, 1}, + {"groups", required_argument, &flag, 2}, + {"timeout", required_argument, &flag, 3}, + {"preferred-ipv4-addr", required_argument, &flag, 4}, + {"preferred-ipv6-addr", required_argument, &flag, 5}, + {"mime-types-file", required_argument, &flag, 6}, + {"early-response", no_argument, &flag, 7}, + {"verify-client", no_argument, &flag, 8}, + {"qlog-dir", required_argument, &flag, 9}, + {"no-quic-dump", no_argument, &flag, 10}, + {"no-http-dump", no_argument, &flag, 11}, + {"max-data", required_argument, &flag, 12}, + {"max-stream-data-bidi-local", required_argument, &flag, 13}, + {"max-stream-data-bidi-remote", required_argument, &flag, 14}, + {"max-stream-data-uni", required_argument, &flag, 15}, + {"max-streams-bidi", required_argument, &flag, 16}, + {"max-streams-uni", required_argument, &flag, 17}, + {"max-dyn-length", required_argument, &flag, 18}, + {"cc", required_argument, &flag, 19}, + {"initial-rtt", required_argument, &flag, 20}, + {"max-udp-payload-size", required_argument, &flag, 21}, + {"send-trailers", no_argument, &flag, 22}, + {"max-window", required_argument, &flag, 23}, + {"max-stream-window", required_argument, &flag, 24}, + {"max-gso-dgrams", required_argument, &flag, 25}, + {"handshake-timeout", required_argument, &flag, 26}, + {"preferred-versions", required_argument, &flag, 27}, + {"other-versions", required_argument, &flag, 28}, + {"no-pmtud", no_argument, &flag, 29}, + {"ack-thresh", required_argument, &flag, 30}, + {nullptr, 0, nullptr, 0}}; + + auto optidx = 0; + auto c = getopt_long(argc, argv, "d:hqr:st:V", long_opts, &optidx); + if (c == -1) { + break; + } + switch (c) { + case 'd': { + // --htdocs + auto path = realpath(optarg, nullptr); + if (path == nullptr) { + std::cerr << "path: invalid path " << std::quoted(optarg) << std::endl; + exit(EXIT_FAILURE); + } + config.htdocs = path; + free(path); + break; + } + case 'h': + // --help + print_help(); + exit(EXIT_SUCCESS); + case 'q': + // --quiet + config.quiet = true; + break; + case 'r': + // --rx-loss + config.rx_loss_prob = strtod(optarg, nullptr); + break; + case 's': + // --show-secret + config.show_secret = true; + break; + case 't': + // --tx-loss + config.tx_loss_prob = strtod(optarg, nullptr); + break; + case 'V': + // --validate-addr + config.validate_addr = true; + break; + case '?': + print_usage(); + exit(EXIT_FAILURE); + case 0: + switch (flag) { + case 1: + // --ciphers + config.ciphers = optarg; + break; + case 2: + // --groups + config.groups = optarg; + break; + case 3: + // --timeout + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "timeout: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.timeout = *t; + } + break; + case 4: + // --preferred-ipv4-addr + if (parse_host_port(config.preferred_ipv4_addr, AF_INET, optarg, + optarg + strlen(optarg)) != 0) { + std::cerr << "preferred-ipv4-addr: could not use " + << std::quoted(optarg) << std::endl; + exit(EXIT_FAILURE); + } + break; + case 5: + // --preferred-ipv6-addr + if (parse_host_port(config.preferred_ipv6_addr, AF_INET6, optarg, + optarg + strlen(optarg)) != 0) { + std::cerr << "preferred-ipv6-addr: could not use " + << std::quoted(optarg) << std::endl; + exit(EXIT_FAILURE); + } + break; + case 6: + // --mime-types-file + config.mime_types_file = optarg; + break; + case 7: + // --early-response + config.early_response = true; + break; + case 8: + // --verify-client + config.verify_client = true; + break; + case 9: + // --qlog-dir + config.qlog_dir = optarg; + break; + case 10: + // --no-quic-dump + config.no_quic_dump = true; + break; + case 11: + // --no-http-dump + config.no_http_dump = true; + break; + case 12: + // --max-data + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-data: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_data = *n; + } + break; + case 13: + // --max-stream-data-bidi-local + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-data-bidi-local: invalid argument" + << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_data_bidi_local = *n; + } + break; + case 14: + // --max-stream-data-bidi-remote + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-data-bidi-remote: invalid argument" + << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_data_bidi_remote = *n; + } + break; + case 15: + // --max-stream-data-uni + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-data-uni: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_data_uni = *n; + } + break; + case 16: + // --max-streams-bidi + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "max-streams-bidi: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_streams_bidi = *n; + } + break; + case 17: + // --max-streams-uni + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "max-streams-uni: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_streams_uni = *n; + } + break; + case 18: + // --max-dyn-length + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-dyn-length: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_dyn_length = *n; + } + break; + case 19: + // --cc + if (strcmp("cubic", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_CUBIC; + break; + } + if (strcmp("reno", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_RENO; + break; + } + if (strcmp("bbr", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_BBR; + break; + } + if (strcmp("bbr2", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_BBR2; + break; + } + std::cerr << "cc: specify cubic, reno, bbr, or bbr2" << std::endl; + exit(EXIT_FAILURE); + case 20: + // --initial-rtt + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "initial-rtt: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.initial_rtt = *t; + } + break; + case 21: + // --max-udp-payload-size + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-udp-payload-size: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else if (*n > 64_k) { + std::cerr << "max-udp-payload-size: must not exceed 65536" + << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_udp_payload_size = *n; + } + break; + case 22: + // --send-trailers + config.send_trailers = true; + break; + case 23: + // --max-window + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-window: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_window = *n; + } + break; + case 24: + // --max-stream-window + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-window: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_window = *n; + } + break; + case 25: + // --max-gso-dgrams + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "max-gso-dgrams: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_gso_dgrams = *n; + } + break; + case 26: + // --handshake-timeout + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "handshake-timeout: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.handshake_timeout = *t; + } + break; + case 27: { + // --preferred-versions + auto l = util::split_str(optarg); + if (l.size() > max_preferred_versionslen) { + std::cerr << "preferred-versions: too many versions > " + << max_preferred_versionslen << std::endl; + } + config.preferred_versions.resize(l.size()); + auto it = std::begin(config.preferred_versions); + for (const auto &k : l) { + if (k == "v1"sv) { + *it++ = NGTCP2_PROTO_VER_V1; + continue; + } + if (k == "v2draft"sv) { + *it++ = NGTCP2_PROTO_VER_V2_DRAFT; + continue; + } + auto rv = util::parse_version(k); + if (!rv) { + std::cerr << "preferred-versions: invalid version " + << std::quoted(k) << std::endl; + exit(EXIT_FAILURE); + } + if (!ngtcp2_is_supported_version(*rv)) { + std::cerr << "preferred-versions: unsupported version " + << std::quoted(k) << std::endl; + exit(EXIT_FAILURE); + } + *it++ = *rv; + } + break; + } + case 28: { + // --other-versions + auto l = util::split_str(optarg); + config.other_versions.resize(l.size()); + auto it = std::begin(config.other_versions); + for (const auto &k : l) { + if (k == "v1"sv) { + *it++ = NGTCP2_PROTO_VER_V1; + continue; + } + if (k == "v2draft"sv) { + *it++ = NGTCP2_PROTO_VER_V2_DRAFT; + continue; + } + auto rv = util::parse_version(k); + if (!rv) { + std::cerr << "other-versions: invalid version " << std::quoted(k) + << std::endl; + exit(EXIT_FAILURE); + } + *it++ = *rv; + } + break; + } + case 29: + // --no-pmtud + config.no_pmtud = true; + break; + case 30: + // --ack-thresh + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "ack-thresh: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else if (*n > 100) { + std::cerr << "ack-thresh: must not exceed 100" << std::endl; + exit(EXIT_FAILURE); + } else { + config.ack_thresh = *n; + } + break; + } + break; + default: + break; + }; + } + + if (argc - optind < 4) { + std::cerr << "Too few arguments" << std::endl; + print_usage(); + exit(EXIT_FAILURE); + } + + auto addr = argv[optind++]; + auto port = argv[optind++]; + auto private_key_file = argv[optind++]; + auto cert_file = argv[optind++]; + + if (auto n = util::parse_uint(port); !n) { + std::cerr << "port: invalid port number" << std::endl; + exit(EXIT_FAILURE); + } else if (*n > 65535) { + std::cerr << "port: must not exceed 65535" << std::endl; + exit(EXIT_FAILURE); + } else { + config.port = *n; + } + + if (auto mt = util::read_mime_types(config.mime_types_file); !mt) { + std::cerr << "mime-types-file: Could not read MIME media types file " + << std::quoted(config.mime_types_file) << std::endl; + } else { + config.mime_types = std::move(*mt); + } + + TLSServerContext tls_ctx; + + if (tls_ctx.init(private_key_file, cert_file, AppProtocol::HQ) != 0) { + exit(EXIT_FAILURE); + } + + if (config.htdocs.back() != '/') { + config.htdocs += '/'; + } + + std::cerr << "Using document root " << config.htdocs << std::endl; + + auto ev_loop_d = defer(ev_loop_destroy, EV_DEFAULT); + + auto keylog_filename = getenv("SSLKEYLOGFILE"); + if (keylog_filename) { + keylog_file.open(keylog_filename, std::ios_base::app); + if (keylog_file) { + tls_ctx.enable_keylog(); + } + } + + if (util::generate_secret(config.static_secret.data(), + config.static_secret.size()) != 0) { + std::cerr << "Unable to generate static secret" << std::endl; + exit(EXIT_FAILURE); + } + + Server s(EV_DEFAULT, tls_ctx); + if (s.init(addr, port) != 0) { + exit(EXIT_FAILURE); + } + + ev_run(EV_DEFAULT, 0); + + s.disconnect(); + s.close(); + + return EXIT_SUCCESS; +} diff --git a/examples/h09server.h b/examples/h09server.h new file mode 100644 index 0000000..8ab11f5 --- /dev/null +++ b/examples/h09server.h @@ -0,0 +1,237 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SERVER_H +#define SERVER_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <vector> +#include <unordered_map> +#include <string> +#include <deque> +#include <string_view> +#include <memory> +#include <set> + +#include <ngtcp2/ngtcp2.h> +#include <ngtcp2/ngtcp2_crypto.h> +#include <nghttp3/nghttp3.h> + +#include <ev.h> + +#include "server_base.h" +#include "tls_server_context.h" +#include "network.h" +#include "shared.h" + +using namespace ngtcp2; + +struct HTTPHeader { + HTTPHeader(const std::string_view &name, const std::string_view &value) + : name(name), value(value) {} + + std::string_view name; + std::string_view value; +}; + +class Handler; +struct FileEntry; + +struct Stream { + Stream(int64_t stream_id, Handler *handler); + + int start_response(); + std::pair<FileEntry, int> open_file(const std::string &path); + void map_file(const FileEntry &fe); + int send_status_response(unsigned int status_code); + + int64_t stream_id; + Handler *handler; + // uri is request uri/path. + std::string uri; + std::string status_resp_body; + nghttp3_buf respbuf; + http_parser htp; + // eos gets true when one HTTP request message is seen. + bool eos; +}; + +struct StreamIDLess { + constexpr bool operator()(const Stream *lhs, const Stream *rhs) const { + return lhs->stream_id < rhs->stream_id; + } +}; + +class Server; + +// Endpoint is a local endpoint. +struct Endpoint { + Address addr; + ev_io rev; + Server *server; + int fd; + // ecn is the last ECN bits set to fd. + unsigned int ecn; +}; + +class Handler : public HandlerBase { +public: + Handler(struct ev_loop *loop, Server *server); + ~Handler(); + + int init(const Endpoint &ep, const Address &local_addr, const sockaddr *sa, + socklen_t salen, const ngtcp2_cid *dcid, const ngtcp2_cid *scid, + const ngtcp2_cid *ocid, const uint8_t *token, size_t tokenlen, + uint32_t version, TLSServerContext &tls_ctx); + + int on_read(const Endpoint &ep, const Address &local_addr, const sockaddr *sa, + socklen_t salen, const ngtcp2_pkt_info *pi, uint8_t *data, + size_t datalen); + int on_write(); + int write_streams(); + int feed_data(const Endpoint &ep, const Address &local_addr, + const sockaddr *sa, socklen_t salen, const ngtcp2_pkt_info *pi, + uint8_t *data, size_t datalen); + void update_timer(); + int handle_expiry(); + void signal_write(); + int handshake_completed(); + + Server *server() const; + int recv_stream_data(uint32_t flags, int64_t stream_id, const uint8_t *data, + size_t datalen); + int acked_stream_data_offset(int64_t stream_id, uint64_t offset, + uint64_t datalen); + uint32_t version() const; + void on_stream_open(int64_t stream_id); + int on_stream_close(int64_t stream_id, uint64_t app_error_code); + void start_draining_period(); + int start_closing_period(); + int handle_error(); + int send_conn_close(); + + int update_key(uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen); + + Stream *find_stream(int64_t stream_id); + int extend_max_stream_data(int64_t stream_id, uint64_t max_data); + void shutdown_read(int64_t stream_id, int app_error_code); + + void write_qlog(const void *data, size_t datalen); + void add_sendq(Stream *stream); + + void on_send_blocked(Endpoint &ep, const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, unsigned int ecn, + const uint8_t *data, size_t datalen, size_t gso_size); + void start_wev_endpoint(const Endpoint &ep); + int send_blocked_packet(); + +private: + struct ev_loop *loop_; + Server *server_; + ev_io wev_; + ev_timer timer_; + FILE *qlog_; + ngtcp2_cid scid_; + std::unordered_map<int64_t, std::unique_ptr<Stream>> streams_; + std::set<Stream *, StreamIDLess> sendq_; + // conn_closebuf_ contains a packet which contains CONNECTION_CLOSE. + // This packet is repeatedly sent as a response to the incoming + // packet in draining period. + std::unique_ptr<Buffer> conn_closebuf_; + // nkey_update_ is the number of key update occurred. + size_t nkey_update_; + bool no_gso_; + + struct { + bool send_blocked; + size_t num_blocked; + size_t num_blocked_sent; + // blocked field is effective only when send_blocked is true. + struct { + Endpoint *endpoint; + Address local_addr; + Address remote_addr; + unsigned int ecn; + const uint8_t *data; + size_t datalen; + size_t gso_size; + } blocked[2]; + std::unique_ptr<uint8_t[]> data; + } tx_; +}; + +class Server { +public: + Server(struct ev_loop *loop, TLSServerContext &tls_ctx); + ~Server(); + + int init(const char *addr, const char *port); + void disconnect(); + void close(); + + int on_read(Endpoint &ep); + int send_version_negotiation(uint32_t version, const uint8_t *dcid, + size_t dcidlen, const uint8_t *scid, + size_t scidlen, Endpoint &ep, + const Address &local_addr, const sockaddr *sa, + socklen_t salen); + int send_retry(const ngtcp2_pkt_hd *chd, Endpoint &ep, + const Address &local_addr, const sockaddr *sa, socklen_t salen, + size_t max_pktlen); + int send_stateless_connection_close(const ngtcp2_pkt_hd *chd, Endpoint &ep, + const Address &local_addr, + const sockaddr *sa, socklen_t salen); + int verify_retry_token(ngtcp2_cid *ocid, const ngtcp2_pkt_hd *hd, + const sockaddr *sa, socklen_t salen); + int verify_token(const ngtcp2_pkt_hd *hd, const sockaddr *sa, + socklen_t salen); + int send_packet(Endpoint &ep, const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, unsigned int ecn, + const uint8_t *data, size_t datalen); + std::pair<size_t, int> send_packet(Endpoint &ep, bool &no_gso, + const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, + unsigned int ecn, const uint8_t *data, + size_t datalen, size_t gso_size); + void remove(const Handler *h); + + void associate_cid(const ngtcp2_cid *cid, Handler *h); + void dissociate_cid(const ngtcp2_cid *cid); + +private: + std::unordered_map<std::string, Handler *> handlers_; + struct ev_loop *loop_; + std::vector<Endpoint> endpoints_; + TLSServerContext &tls_ctx_; + ev_signal sigintev_; +}; + +#endif // SERVER_H diff --git a/examples/http.cc b/examples/http.cc new file mode 100644 index 0000000..bfcd188 --- /dev/null +++ b/examples/http.cc @@ -0,0 +1,138 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * Copyright (c) 2012 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "http.h" + +namespace ngtcp2 { + +namespace http { + +std::string get_reason_phrase(unsigned int status_code) { + switch (status_code) { + case 100: + return "Continue"; + case 101: + return "Switching Protocols"; + case 200: + return "OK"; + case 201: + return "Created"; + case 202: + return "Accepted"; + case 203: + return "Non-Authoritative Information"; + case 204: + return "No Content"; + case 205: + return "Reset Content"; + case 206: + return "Partial Content"; + case 300: + return "Multiple Choices"; + case 301: + return "Moved Permanently"; + case 302: + return "Found"; + case 303: + return "See Other"; + case 304: + return "Not Modified"; + case 305: + return "Use Proxy"; + // case 306: return "(Unused)"; + case 307: + return "Temporary Redirect"; + case 308: + return "Permanent Redirect"; + case 400: + return "Bad Request"; + case 401: + return "Unauthorized"; + case 402: + return "Payment Required"; + case 403: + return "Forbidden"; + case 404: + return "Not Found"; + case 405: + return "Method Not Allowed"; + case 406: + return "Not Acceptable"; + case 407: + return "Proxy Authentication Required"; + case 408: + return "Request Timeout"; + case 409: + return "Conflict"; + case 410: + return "Gone"; + case 411: + return "Length Required"; + case 412: + return "Precondition Failed"; + case 413: + return "Payload Too Large"; + case 414: + return "URI Too Long"; + case 415: + return "Unsupported Media Type"; + case 416: + return "Requested Range Not Satisfiable"; + case 417: + return "Expectation Failed"; + case 421: + return "Misdirected Request"; + case 426: + return "Upgrade Required"; + case 428: + return "Precondition Required"; + case 429: + return "Too Many Requests"; + case 431: + return "Request Header Fields Too Large"; + case 451: + return "Unavailable For Legal Reasons"; + case 500: + return "Internal Server Error"; + case 501: + return "Not Implemented"; + case 502: + return "Bad Gateway"; + case 503: + return "Service Unavailable"; + case 504: + return "Gateway Timeout"; + case 505: + return "HTTP Version Not Supported"; + case 511: + return "Network Authentication Required"; + default: + return ""; + } +} + +} // namespace http + +} // namespace ngtcp2 diff --git a/examples/http.h b/examples/http.h new file mode 100644 index 0000000..6622b4c --- /dev/null +++ b/examples/http.h @@ -0,0 +1,44 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef HTTP_H +#define HTTP_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <string> + +namespace ngtcp2 { + +namespace http { + +std::string get_reason_phrase(unsigned int status_code); + +} // namespace http + +} // namespace ngtcp2 + +#endif // HTTP_H diff --git a/examples/network.h b/examples/network.h new file mode 100644 index 0000000..451e531 --- /dev/null +++ b/examples/network.h @@ -0,0 +1,80 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * Copyright (c) 2016 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NETWORK_H +#define NETWORK_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <sys/types.h> +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H +#include <sys/un.h> +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif // HAVE_NETINET_IN_H +#ifdef HAVE_ARPA_INET_H +# include <arpa/inet.h> +#endif // HAVE_ARPA_INET_H + +#include <array> + +#include <ngtcp2/ngtcp2.h> + +namespace ngtcp2 { + +enum network_error { + NETWORK_ERR_OK = 0, + NETWORK_ERR_FATAL = -10, + NETWORK_ERR_SEND_BLOCKED = -11, + NETWORK_ERR_CLOSE_WAIT = -12, + NETWORK_ERR_RETRY = -13, + NETWORK_ERR_DROP_CONN = -14, +}; + +union in_addr_union { + in_addr in; + in6_addr in6; +}; + +union sockaddr_union { + sockaddr_storage storage; + sockaddr sa; + sockaddr_in6 in6; + sockaddr_in in; +}; + +struct Address { + socklen_t len; + union sockaddr_union su; + uint32_t ifindex; +}; + +} // namespace ngtcp2 + +#endif // NETWORK_H diff --git a/examples/server.cc b/examples/server.cc new file mode 100644 index 0000000..30db269 --- /dev/null +++ b/examples/server.cc @@ -0,0 +1,3741 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include <chrono> +#include <cstdlib> +#include <cassert> +#include <cstring> +#include <iostream> +#include <algorithm> +#include <memory> +#include <fstream> +#include <iomanip> + +#include <unistd.h> +#include <getopt.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <sys/mman.h> +#include <netinet/udp.h> +#include <net/if.h> + +#include <http-parser/http_parser.h> + +#include "server.h" +#include "network.h" +#include "debug.h" +#include "util.h" +#include "shared.h" +#include "http.h" +#include "template.h" + +using namespace ngtcp2; +using namespace std::literals; + +namespace { +constexpr size_t NGTCP2_SV_SCIDLEN = 18; +} // namespace + +namespace { +constexpr size_t MAX_DYNBUFLEN = 10_m; +} // namespace + +namespace { +constexpr size_t max_preferred_versionslen = 4; +} // namespace + +namespace { +auto randgen = util::make_mt19937(); +} // namespace + +Config config{}; + +Stream::Stream(int64_t stream_id, Handler *handler) + : stream_id(stream_id), + handler(handler), + data(nullptr), + datalen(0), + dynresp(false), + dyndataleft(0), + dynbuflen(0) {} + +namespace { +constexpr auto NGTCP2_SERVER = "nghttp3/ngtcp2 server"sv; +} // namespace + +namespace { +std::string make_status_body(unsigned int status_code) { + auto status_string = util::format_uint(status_code); + auto reason_phrase = http::get_reason_phrase(status_code); + + std::string body; + body = "<html><head><title>"; + body += status_string; + body += ' '; + body += reason_phrase; + body += "</title></head><body><h1>"; + body += status_string; + body += ' '; + body += reason_phrase; + body += "</h1><hr><address>"; + body += NGTCP2_SERVER; + body += " at port "; + body += util::format_uint(config.port); + body += "</address>"; + body += "</body></html>"; + return body; +} +} // namespace + +struct Request { + std::string path; + struct { + int32_t urgency; + int inc; + } pri; +}; + +namespace { +Request request_path(const std::string_view &uri, bool is_connect) { + http_parser_url u; + Request req; + + req.pri.urgency = -1; + req.pri.inc = -1; + + http_parser_url_init(&u); + + if (auto rv = http_parser_parse_url(uri.data(), uri.size(), is_connect, &u); + rv != 0) { + return req; + } + + if (u.field_set & (1 << UF_PATH)) { + req.path = std::string(uri.data() + u.field_data[UF_PATH].off, + u.field_data[UF_PATH].len); + if (req.path.find('%') != std::string::npos) { + req.path = util::percent_decode(std::begin(req.path), std::end(req.path)); + } + if (!req.path.empty() && req.path.back() == '/') { + req.path += "index.html"; + } + } else { + req.path = "/index.html"; + } + + req.path = util::normalize_path(req.path); + if (req.path == "/") { + req.path = "/index.html"; + } + + if (u.field_set & (1 << UF_QUERY)) { + static constexpr auto urgency_prefix = "u="sv; + static constexpr auto inc_prefix = "i="sv; + auto q = std::string(uri.data() + u.field_data[UF_QUERY].off, + u.field_data[UF_QUERY].len); + for (auto p = std::begin(q); p != std::end(q);) { + if (util::istarts_with(p, std::end(q), std::begin(urgency_prefix), + std::end(urgency_prefix))) { + auto urgency_start = p + urgency_prefix.size(); + auto urgency_end = std::find(urgency_start, std::end(q), '&'); + if (urgency_start + 1 == urgency_end && '0' <= *urgency_start && + *urgency_start <= '7') { + req.pri.urgency = *urgency_start - '0'; + } + if (urgency_end == std::end(q)) { + break; + } + p = urgency_end + 1; + continue; + } + if (util::istarts_with(p, std::end(q), std::begin(inc_prefix), + std::end(inc_prefix))) { + auto inc_start = p + inc_prefix.size(); + auto inc_end = std::find(inc_start, std::end(q), '&'); + if (inc_start + 1 == inc_end && + (*inc_start == '0' || *inc_start == '1')) { + req.pri.inc = *inc_start - '0'; + } + if (inc_end == std::end(q)) { + break; + } + p = inc_end + 1; + continue; + } + + p = std::find(p, std::end(q), '&'); + if (p == std::end(q)) { + break; + } + ++p; + } + } + return req; +} +} // namespace + +enum FileEntryFlag { + FILE_ENTRY_TYPE_DIR = 0x1, +}; + +struct FileEntry { + uint64_t len; + void *map; + int fd; + uint8_t flags; +}; + +namespace { +std::unordered_map<std::string, FileEntry> file_cache; +} // namespace + +std::pair<FileEntry, int> Stream::open_file(const std::string &path) { + auto it = file_cache.find(path); + if (it != std::end(file_cache)) { + return {(*it).second, 0}; + } + + auto fd = open(path.c_str(), O_RDONLY); + if (fd == -1) { + return {{}, -1}; + } + + struct stat st {}; + if (fstat(fd, &st) != 0) { + close(fd); + return {{}, -1}; + } + + FileEntry fe{}; + if (st.st_mode & S_IFDIR) { + fe.flags |= FILE_ENTRY_TYPE_DIR; + fe.fd = -1; + close(fd); + } else { + fe.fd = fd; + fe.len = st.st_size; + fe.map = mmap(nullptr, fe.len, PROT_READ, MAP_SHARED, fd, 0); + if (fe.map == MAP_FAILED) { + std::cerr << "mmap: " << strerror(errno) << std::endl; + close(fd); + return {{}, -1}; + } + } + + file_cache.emplace(path, fe); + + return {std::move(fe), 0}; +} + +void Stream::map_file(const FileEntry &fe) { + data = static_cast<uint8_t *>(fe.map); + datalen = fe.len; +} + +int64_t Stream::find_dyn_length(const std::string_view &path) { + assert(path[0] == '/'); + + if (path.size() == 1) { + return -1; + } + + uint64_t n = 0; + + for (auto it = std::begin(path) + 1; it != std::end(path); ++it) { + if (*it < '0' || '9' < *it) { + return -1; + } + auto d = *it - '0'; + if (n > (((1ull << 62) - 1) - d) / 10) { + return -1; + } + n = n * 10 + d; + if (n > config.max_dyn_length) { + return -1; + } + } + + return static_cast<int64_t>(n); +} + +namespace { +nghttp3_ssize read_data(nghttp3_conn *conn, int64_t stream_id, nghttp3_vec *vec, + size_t veccnt, uint32_t *pflags, void *user_data, + void *stream_user_data) { + auto stream = static_cast<Stream *>(stream_user_data); + + vec[0].base = stream->data; + vec[0].len = stream->datalen; + *pflags |= NGHTTP3_DATA_FLAG_EOF; + if (config.send_trailers) { + *pflags |= NGHTTP3_DATA_FLAG_NO_END_STREAM; + } + + return 1; +} +} // namespace + +auto dyn_buf = std::make_unique<std::array<uint8_t, 16_k>>(); + +namespace { +nghttp3_ssize dyn_read_data(nghttp3_conn *conn, int64_t stream_id, + nghttp3_vec *vec, size_t veccnt, uint32_t *pflags, + void *user_data, void *stream_user_data) { + auto stream = static_cast<Stream *>(stream_user_data); + + if (stream->dynbuflen > MAX_DYNBUFLEN) { + return NGHTTP3_ERR_WOULDBLOCK; + } + + auto len = + std::min(dyn_buf->size(), static_cast<size_t>(stream->dyndataleft)); + + vec[0].base = dyn_buf->data(); + vec[0].len = len; + + stream->dynbuflen += len; + stream->dyndataleft -= len; + + if (stream->dyndataleft == 0) { + *pflags |= NGHTTP3_DATA_FLAG_EOF; + if (config.send_trailers) { + *pflags |= NGHTTP3_DATA_FLAG_NO_END_STREAM; + auto stream_id_str = util::format_uint(stream_id); + std::array<nghttp3_nv, 1> trailers{ + util::make_nv_nc("x-ngtcp2-stream-id"sv, stream_id_str), + }; + + if (auto rv = nghttp3_conn_submit_trailers( + conn, stream_id, trailers.data(), trailers.size()); + rv != 0) { + std::cerr << "nghttp3_conn_submit_trailers: " << nghttp3_strerror(rv) + << std::endl; + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + } + } + + return 1; +} +} // namespace + +void Stream::http_acked_stream_data(uint64_t datalen) { + if (!dynresp) { + return; + } + + assert(dynbuflen >= datalen); + + dynbuflen -= datalen; +} + +int Stream::send_status_response(nghttp3_conn *httpconn, + unsigned int status_code, + const std::vector<HTTPHeader> &extra_headers) { + status_resp_body = make_status_body(status_code); + + auto status_code_str = util::format_uint(status_code); + auto content_length_str = util::format_uint(status_resp_body.size()); + + std::vector<nghttp3_nv> nva(4 + extra_headers.size()); + nva[0] = util::make_nv_nc(":status"sv, status_code_str); + nva[1] = util::make_nv_nn("server"sv, NGTCP2_SERVER); + nva[2] = util::make_nv_nn("content-type"sv, "text/html; charset=utf-8"); + nva[3] = util::make_nv_nc("content-length"sv, content_length_str); + for (size_t i = 0; i < extra_headers.size(); ++i) { + auto &hdr = extra_headers[i]; + auto &nv = nva[4 + i]; + nv = util::make_nv_cc(hdr.name, hdr.value); + } + + data = (uint8_t *)status_resp_body.data(); + datalen = status_resp_body.size(); + + nghttp3_data_reader dr{}; + dr.read_data = read_data; + + if (auto rv = nghttp3_conn_submit_response(httpconn, stream_id, nva.data(), + nva.size(), &dr); + rv != 0) { + std::cerr << "nghttp3_conn_submit_response: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + if (config.send_trailers) { + auto stream_id_str = util::format_uint(stream_id); + std::array<nghttp3_nv, 1> trailers{ + util::make_nv_nc("x-ngtcp2-stream-id"sv, stream_id_str), + }; + + if (auto rv = nghttp3_conn_submit_trailers( + httpconn, stream_id, trailers.data(), trailers.size()); + rv != 0) { + std::cerr << "nghttp3_conn_submit_trailers: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + } + + handler->shutdown_read(stream_id, NGHTTP3_H3_NO_ERROR); + + return 0; +} + +int Stream::send_redirect_response(nghttp3_conn *httpconn, + unsigned int status_code, + const std::string_view &path) { + return send_status_response(httpconn, status_code, {{"location", path}}); +} + +int Stream::start_response(nghttp3_conn *httpconn) { + // TODO This should be handled by nghttp3 + if (uri.empty() || method.empty()) { + return send_status_response(httpconn, 400); + } + + auto req = request_path(uri, method == "CONNECT"); + if (req.path.empty()) { + return send_status_response(httpconn, 400); + } + + auto dyn_len = find_dyn_length(req.path); + + int64_t content_length = -1; + nghttp3_data_reader dr{}; + auto content_type = "text/plain"sv; + + if (dyn_len == -1) { + auto path = config.htdocs + req.path; + auto [fe, rv] = open_file(path); + if (rv != 0) { + send_status_response(httpconn, 404); + return 0; + } + + if (fe.flags & FILE_ENTRY_TYPE_DIR) { + send_redirect_response(httpconn, 308, + path.substr(config.htdocs.size() - 1) + '/'); + return 0; + } + + content_length = fe.len; + + if (method != "HEAD") { + map_file(fe); + } + + dr.read_data = read_data; + + auto ext = std::end(req.path) - 1; + for (; ext != std::begin(req.path) && *ext != '.' && *ext != '/'; --ext) + ; + if (*ext == '.') { + ++ext; + auto it = config.mime_types.find(std::string{ext, std::end(req.path)}); + if (it != std::end(config.mime_types)) { + content_type = (*it).second; + } + } + } else { + content_length = dyn_len; + dynresp = true; + dr.read_data = dyn_read_data; + + if (method != "HEAD") { + datalen = dyn_len; + dyndataleft = dyn_len; + } + + content_type = "application/octet-stream"sv; + } + + auto content_length_str = util::format_uint(content_length); + + std::array<nghttp3_nv, 5> nva{ + util::make_nv_nn(":status"sv, "200"sv), + util::make_nv_nn("server"sv, NGTCP2_SERVER), + util::make_nv_nn("content-type"sv, content_type), + util::make_nv_nc("content-length"sv, content_length_str), + }; + + size_t nvlen = 4; + + std::string prival; + + if (req.pri.urgency != -1 || req.pri.inc != -1) { + nghttp3_pri pri; + + if (auto rv = nghttp3_conn_get_stream_priority(httpconn, &pri, stream_id); + rv != 0) { + std::cerr << "nghttp3_conn_get_stream_priority: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + if (req.pri.urgency != -1) { + pri.urgency = req.pri.urgency; + } + if (req.pri.inc != -1) { + pri.inc = req.pri.inc; + } + + if (auto rv = nghttp3_conn_set_stream_priority(httpconn, stream_id, &pri); + rv != 0) { + std::cerr << "nghttp3_conn_set_stream_priority: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + prival = "u="; + prival += pri.urgency + '0'; + prival += ",i"; + if (!pri.inc) { + prival += "=?0"; + } + + nva[nvlen++] = util::make_nv_nc("priority"sv, prival); + } + + if (!config.quiet) { + debug::print_http_response_headers(stream_id, nva.data(), nvlen); + } + + if (auto rv = nghttp3_conn_submit_response(httpconn, stream_id, nva.data(), + nvlen, &dr); + rv != 0) { + std::cerr << "nghttp3_conn_submit_response: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + if (config.send_trailers && dyn_len == -1) { + auto stream_id_str = util::format_uint(stream_id); + std::array<nghttp3_nv, 1> trailers{ + util::make_nv_nc("x-ngtcp2-stream-id"sv, stream_id_str), + }; + + if (auto rv = nghttp3_conn_submit_trailers( + httpconn, stream_id, trailers.data(), trailers.size()); + rv != 0) { + std::cerr << "nghttp3_conn_submit_trailers: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + } + + return 0; +} + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + auto h = static_cast<Handler *>(w->data); + auto s = h->server(); + + switch (h->on_write()) { + case 0: + case NETWORK_ERR_CLOSE_WAIT: + return; + default: + s->remove(h); + } +} +} // namespace + +namespace { +void close_waitcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto h = static_cast<Handler *>(w->data); + auto s = h->server(); + auto conn = h->conn(); + + if (ngtcp2_conn_is_in_closing_period(conn)) { + if (!config.quiet) { + std::cerr << "Closing Period is over" << std::endl; + } + + s->remove(h); + return; + } + if (ngtcp2_conn_is_in_draining_period(conn)) { + if (!config.quiet) { + std::cerr << "Draining Period is over" << std::endl; + } + + s->remove(h); + return; + } + + assert(0); +} +} // namespace + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + int rv; + + auto h = static_cast<Handler *>(w->data); + auto s = h->server(); + + if (!config.quiet) { + std::cerr << "Timer expired" << std::endl; + } + + rv = h->handle_expiry(); + if (rv != 0) { + goto fail; + } + + rv = h->on_write(); + if (rv != 0) { + goto fail; + } + + return; + +fail: + switch (rv) { + case NETWORK_ERR_CLOSE_WAIT: + ev_timer_stop(loop, w); + return; + default: + s->remove(h); + return; + } +} +} // namespace + +Handler::Handler(struct ev_loop *loop, Server *server) + : loop_(loop), + server_(server), + qlog_(nullptr), + scid_{}, + httpconn_{nullptr}, + nkey_update_(0), + no_gso_{ +#ifdef UDP_SEGMENT + false +#else // !UDP_SEGMENT + true +#endif // !UDP_SEGMENT + }, + tx_{ + .data = std::unique_ptr<uint8_t[]>(new uint8_t[64_k]), + } { + ev_io_init(&wev_, writecb, 0, EV_WRITE); + wev_.data = this; + ev_timer_init(&timer_, timeoutcb, 0., 0.); + timer_.data = this; +} + +Handler::~Handler() { + if (!config.quiet) { + std::cerr << scid_ << " Closing QUIC connection " << std::endl; + } + + ev_timer_stop(loop_, &timer_); + ev_io_stop(loop_, &wev_); + + if (httpconn_) { + nghttp3_conn_del(httpconn_); + } + + if (qlog_) { + fclose(qlog_); + } +} + +namespace { +int handshake_completed(ngtcp2_conn *conn, void *user_data) { + auto h = static_cast<Handler *>(user_data); + + if (!config.quiet) { + debug::handshake_completed(conn, user_data); + } + + if (h->handshake_completed() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Handler::handshake_completed() { + if (!config.quiet) { + std::cerr << "Negotiated cipher suite is " << tls_session_.get_cipher_name() + << std::endl; + std::cerr << "Negotiated ALPN is " << tls_session_.get_selected_alpn() + << std::endl; + } + + if (tls_session_.send_session_ticket() != 0) { + std::cerr << "Unable to send session ticket" << std::endl; + } + + std::array<uint8_t, NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN> token; + + auto path = ngtcp2_conn_get_path(conn_); + auto t = std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + auto tokenlen = ngtcp2_crypto_generate_regular_token( + token.data(), config.static_secret.data(), config.static_secret.size(), + path->remote.addr, path->remote.addrlen, t); + if (tokenlen < 0) { + if (!config.quiet) { + std::cerr << "Unable to generate token" << std::endl; + } + return 0; + } + + if (auto rv = ngtcp2_conn_submit_new_token(conn_, token.data(), tokenlen); + rv != 0) { + if (!config.quiet) { + std::cerr << "ngtcp2_conn_submit_new_token: " << ngtcp2_strerror(rv) + << std::endl; + } + return -1; + } + + return 0; +} + +namespace { +int do_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, + const ngtcp2_crypto_cipher_ctx *hp_ctx, const uint8_t *sample) { + if (ngtcp2_crypto_hp_mask(dest, hp, hp_ctx, sample) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + if (!config.quiet && config.show_secret) { + debug::print_hp_mask(dest, NGTCP2_HP_MASKLEN, sample, NGTCP2_HP_SAMPLELEN); + } + + return 0; +} +} // namespace + +namespace { +int recv_crypto_data(ngtcp2_conn *conn, ngtcp2_crypto_level crypto_level, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_crypto_data(crypto_level, data, datalen); + } + + return ngtcp2_crypto_recv_crypto_data_cb(conn, crypto_level, offset, data, + datalen, user_data); +} +} // namespace + +namespace { +int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data, void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + + if (h->recv_stream_data(flags, stream_id, data, datalen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int acked_stream_data_offset(ngtcp2_conn *conn, int64_t stream_id, + uint64_t offset, uint64_t datalen, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + if (h->acked_stream_data_offset(stream_id, datalen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Handler::acked_stream_data_offset(int64_t stream_id, uint64_t datalen) { + if (!httpconn_) { + return 0; + } + + if (auto rv = nghttp3_conn_add_ack_offset(httpconn_, stream_id, datalen); + rv != 0) { + std::cerr << "nghttp3_conn_add_ack_offset: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + return 0; +} + +namespace { +int stream_open(ngtcp2_conn *conn, int64_t stream_id, void *user_data) { + auto h = static_cast<Handler *>(user_data); + h->on_stream_open(stream_id); + return 0; +} +} // namespace + +void Handler::on_stream_open(int64_t stream_id) { + if (!ngtcp2_is_bidi_stream(stream_id)) { + return; + } + auto it = streams_.find(stream_id); + (void)it; + assert(it == std::end(streams_)); + streams_.emplace(stream_id, std::make_unique<Stream>(stream_id, this)); +} + +namespace { +int stream_close(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + + if (!(flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET)) { + app_error_code = NGHTTP3_H3_NO_ERROR; + } + + if (h->on_stream_close(stream_id, app_error_code) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +namespace { +int stream_reset(ngtcp2_conn *conn, int64_t stream_id, uint64_t final_size, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + if (h->on_stream_reset(stream_id) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Handler::on_stream_reset(int64_t stream_id) { + if (httpconn_) { + if (auto rv = nghttp3_conn_shutdown_stream_read(httpconn_, stream_id); + rv != 0) { + std::cerr << "nghttp3_conn_shutdown_stream_read: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + } + return 0; +} + +namespace { +int stream_stop_sending(ngtcp2_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + if (h->on_stream_stop_sending(stream_id) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Handler::on_stream_stop_sending(int64_t stream_id) { + if (!httpconn_) { + return 0; + } + + if (auto rv = nghttp3_conn_shutdown_stream_read(httpconn_, stream_id); + rv != 0) { + std::cerr << "nghttp3_conn_shutdown_stream_read: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + return 0; +} + +namespace { +void rand(uint8_t *dest, size_t destlen, const ngtcp2_rand_ctx *rand_ctx) { + auto dis = std::uniform_int_distribution<uint8_t>(0, 255); + std::generate(dest, dest + destlen, [&dis]() { return dis(randgen); }); +} +} // namespace + +namespace { +int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, uint8_t *token, + size_t cidlen, void *user_data) { + if (util::generate_secure_random(cid->data, cidlen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + cid->datalen = cidlen; + if (ngtcp2_crypto_generate_stateless_reset_token( + token, config.static_secret.data(), config.static_secret.size(), + cid) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + auto h = static_cast<Handler *>(user_data); + h->server()->associate_cid(cid, h); + + return 0; +} +} // namespace + +namespace { +int remove_connection_id(ngtcp2_conn *conn, const ngtcp2_cid *cid, + void *user_data) { + auto h = static_cast<Handler *>(user_data); + h->server()->dissociate_cid(cid); + return 0; +} +} // namespace + +namespace { +int update_key(ngtcp2_conn *conn, uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen, + void *user_data) { + auto h = static_cast<Handler *>(user_data); + if (h->update_key(rx_secret, tx_secret, rx_aead_ctx, rx_iv, tx_aead_ctx, + tx_iv, current_rx_secret, current_tx_secret, + secretlen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +namespace { +int path_validation(ngtcp2_conn *conn, uint32_t flags, const ngtcp2_path *path, + ngtcp2_path_validation_result res, void *user_data) { + if (!config.quiet) { + debug::path_validation(path, res); + } + return 0; +} +} // namespace + +namespace { +int extend_max_remote_streams_bidi(ngtcp2_conn *conn, uint64_t max_streams, + void *user_data) { + auto h = static_cast<Handler *>(user_data); + h->extend_max_remote_streams_bidi(max_streams); + return 0; +} +} // namespace + +void Handler::extend_max_remote_streams_bidi(uint64_t max_streams) { + if (!httpconn_) { + return; + } + + nghttp3_conn_set_max_client_streams_bidi(httpconn_, max_streams); +} + +namespace { +int http_recv_data(nghttp3_conn *conn, int64_t stream_id, const uint8_t *data, + size_t datalen, void *user_data, void *stream_user_data) { + if (!config.quiet && !config.no_http_dump) { + debug::print_http_data(stream_id, data, datalen); + } + auto h = static_cast<Handler *>(user_data); + h->http_consume(stream_id, datalen); + return 0; +} +} // namespace + +namespace { +int http_deferred_consume(nghttp3_conn *conn, int64_t stream_id, + size_t nconsumed, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + h->http_consume(stream_id, nconsumed); + return 0; +} +} // namespace + +void Handler::http_consume(int64_t stream_id, size_t nconsumed) { + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, nconsumed); + ngtcp2_conn_extend_max_offset(conn_, nconsumed); +} + +namespace { +int http_begin_request_headers(nghttp3_conn *conn, int64_t stream_id, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_begin_request_headers(stream_id); + } + + auto h = static_cast<Handler *>(user_data); + h->http_begin_request_headers(stream_id); + return 0; +} +} // namespace + +void Handler::http_begin_request_headers(int64_t stream_id) { + auto it = streams_.find(stream_id); + assert(it != std::end(streams_)); + auto &stream = (*it).second; + + nghttp3_conn_set_stream_user_data(httpconn_, stream_id, stream.get()); +} + +namespace { +int http_recv_request_header(nghttp3_conn *conn, int64_t stream_id, + int32_t token, nghttp3_rcbuf *name, + nghttp3_rcbuf *value, uint8_t flags, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_header(stream_id, name, value, flags); + } + + auto h = static_cast<Handler *>(user_data); + auto stream = static_cast<Stream *>(stream_user_data); + h->http_recv_request_header(stream, token, name, value); + return 0; +} +} // namespace + +void Handler::http_recv_request_header(Stream *stream, int32_t token, + nghttp3_rcbuf *name, + nghttp3_rcbuf *value) { + auto v = nghttp3_rcbuf_get_buf(value); + + switch (token) { + case NGHTTP3_QPACK_TOKEN__PATH: + stream->uri = std::string{v.base, v.base + v.len}; + break; + case NGHTTP3_QPACK_TOKEN__METHOD: + stream->method = std::string{v.base, v.base + v.len}; + break; + case NGHTTP3_QPACK_TOKEN__AUTHORITY: + stream->authority = std::string{v.base, v.base + v.len}; + break; + } +} + +namespace { +int http_end_request_headers(nghttp3_conn *conn, int64_t stream_id, int fin, + void *user_data, void *stream_user_data) { + if (!config.quiet) { + debug::print_http_end_headers(stream_id); + } + + auto h = static_cast<Handler *>(user_data); + auto stream = static_cast<Stream *>(stream_user_data); + if (h->http_end_request_headers(stream) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Handler::http_end_request_headers(Stream *stream) { + if (config.early_response) { + if (start_response(stream) != 0) { + return -1; + } + + shutdown_read(stream->stream_id, NGHTTP3_H3_NO_ERROR); + } + return 0; +} + +namespace { +int http_end_stream(nghttp3_conn *conn, int64_t stream_id, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + auto stream = static_cast<Stream *>(stream_user_data); + if (h->http_end_stream(stream) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Handler::http_end_stream(Stream *stream) { + if (!config.early_response) { + return start_response(stream); + } + return 0; +} + +int Handler::start_response(Stream *stream) { + return stream->start_response(httpconn_); +} + +namespace { +int http_acked_stream_data(nghttp3_conn *conn, int64_t stream_id, + uint64_t datalen, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + auto stream = static_cast<Stream *>(stream_user_data); + h->http_acked_stream_data(stream, datalen); + return 0; +} +} // namespace + +void Handler::http_acked_stream_data(Stream *stream, uint64_t datalen) { + stream->http_acked_stream_data(datalen); + + if (stream->dynresp && stream->dynbuflen < MAX_DYNBUFLEN - 16_k) { + if (auto rv = nghttp3_conn_resume_stream(httpconn_, stream->stream_id); + rv != 0) { + // TODO Handle error + std::cerr << "nghttp3_conn_resume_stream: " << nghttp3_strerror(rv) + << std::endl; + } + } +} + +namespace { +int http_stream_close(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *conn_user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(conn_user_data); + h->http_stream_close(stream_id, app_error_code); + return 0; +} +} // namespace + +void Handler::http_stream_close(int64_t stream_id, uint64_t app_error_code) { + auto it = streams_.find(stream_id); + if (it == std::end(streams_)) { + return; + } + + if (!config.quiet) { + std::cerr << "HTTP stream " << stream_id << " closed with error code " + << app_error_code << std::endl; + } + + streams_.erase(it); + + if (ngtcp2_is_bidi_stream(stream_id)) { + assert(!ngtcp2_conn_is_local_stream(conn_, stream_id)); + ngtcp2_conn_extend_max_streams_bidi(conn_, 1); + } +} + +namespace { +int http_stop_sending(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + if (h->http_stop_sending(stream_id, app_error_code) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Handler::http_stop_sending(int64_t stream_id, uint64_t app_error_code) { + if (auto rv = + ngtcp2_conn_shutdown_stream_read(conn_, stream_id, app_error_code); + rv != 0) { + std::cerr << "ngtcp2_conn_shutdown_stream_read: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + return 0; +} + +namespace { +int http_reset_stream(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + if (h->http_reset_stream(stream_id, app_error_code) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Handler::http_reset_stream(int64_t stream_id, uint64_t app_error_code) { + if (auto rv = + ngtcp2_conn_shutdown_stream_write(conn_, stream_id, app_error_code); + rv != 0) { + std::cerr << "ngtcp2_conn_shutdown_stream_write: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + return 0; +} + +int Handler::setup_httpconn() { + if (httpconn_) { + return 0; + } + + if (ngtcp2_conn_get_max_local_streams_uni(conn_) < 3) { + std::cerr << "peer does not allow at least 3 unidirectional streams." + << std::endl; + return -1; + } + + nghttp3_callbacks callbacks{ + ::http_acked_stream_data, // acked_stream_data + ::http_stream_close, + ::http_recv_data, + ::http_deferred_consume, + ::http_begin_request_headers, + ::http_recv_request_header, + ::http_end_request_headers, + nullptr, // begin_trailers + nullptr, // recv_trailer + nullptr, // end_trailers + ::http_stop_sending, + ::http_end_stream, + ::http_reset_stream, + }; + nghttp3_settings settings; + nghttp3_settings_default(&settings); + settings.qpack_max_dtable_capacity = 4096; + settings.qpack_blocked_streams = 100; + + auto mem = nghttp3_mem_default(); + + if (auto rv = + nghttp3_conn_server_new(&httpconn_, &callbacks, &settings, mem, this); + rv != 0) { + std::cerr << "nghttp3_conn_server_new: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + auto params = ngtcp2_conn_get_local_transport_params(conn_); + + nghttp3_conn_set_max_client_streams_bidi(httpconn_, + params->initial_max_streams_bidi); + + int64_t ctrl_stream_id; + + if (auto rv = ngtcp2_conn_open_uni_stream(conn_, &ctrl_stream_id, nullptr); + rv != 0) { + std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + if (auto rv = nghttp3_conn_bind_control_stream(httpconn_, ctrl_stream_id); + rv != 0) { + std::cerr << "nghttp3_conn_bind_control_stream: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + if (!config.quiet) { + fprintf(stderr, "http: control stream=%" PRIx64 "\n", ctrl_stream_id); + } + + int64_t qpack_enc_stream_id, qpack_dec_stream_id; + + if (auto rv = + ngtcp2_conn_open_uni_stream(conn_, &qpack_enc_stream_id, nullptr); + rv != 0) { + std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + if (auto rv = + ngtcp2_conn_open_uni_stream(conn_, &qpack_dec_stream_id, nullptr); + rv != 0) { + std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + if (auto rv = nghttp3_conn_bind_qpack_streams(httpconn_, qpack_enc_stream_id, + qpack_dec_stream_id); + rv != 0) { + std::cerr << "nghttp3_conn_bind_qpack_streams: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + if (!config.quiet) { + fprintf(stderr, + "http: QPACK streams encoder=%" PRIx64 " decoder=%" PRIx64 "\n", + qpack_enc_stream_id, qpack_dec_stream_id); + } + + return 0; +} + +namespace { +int extend_max_stream_data(ngtcp2_conn *conn, int64_t stream_id, + uint64_t max_data, void *user_data, + void *stream_user_data) { + auto h = static_cast<Handler *>(user_data); + if (h->extend_max_stream_data(stream_id, max_data) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Handler::extend_max_stream_data(int64_t stream_id, uint64_t max_data) { + if (auto rv = nghttp3_conn_unblock_stream(httpconn_, stream_id); rv != 0) { + std::cerr << "nghttp3_conn_unblock_stream: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + return 0; +} + +namespace { +int recv_tx_key(ngtcp2_conn *conn, ngtcp2_crypto_level level, void *user_data) { + if (level != NGTCP2_CRYPTO_LEVEL_APPLICATION) { + return 0; + } + + auto h = static_cast<Handler *>(user_data); + if (h->setup_httpconn() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +void write_qlog(void *user_data, uint32_t flags, const void *data, + size_t datalen) { + auto h = static_cast<Handler *>(user_data); + h->write_qlog(data, datalen); +} +} // namespace + +void Handler::write_qlog(const void *data, size_t datalen) { + assert(qlog_); + fwrite(data, 1, datalen, qlog_); +} + +int Handler::init(const Endpoint &ep, const Address &local_addr, + const sockaddr *sa, socklen_t salen, const ngtcp2_cid *dcid, + const ngtcp2_cid *scid, const ngtcp2_cid *ocid, + const uint8_t *token, size_t tokenlen, uint32_t version, + TLSServerContext &tls_ctx) { + auto callbacks = ngtcp2_callbacks{ + nullptr, // client_initial + ngtcp2_crypto_recv_client_initial_cb, + ::recv_crypto_data, + ::handshake_completed, + nullptr, // recv_version_negotiation + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + do_hp_mask, + ::recv_stream_data, + ::acked_stream_data_offset, + stream_open, + stream_close, + nullptr, // recv_stateless_reset + nullptr, // recv_retry + nullptr, // extend_max_streams_bidi + nullptr, // extend_max_streams_uni + rand, + get_new_connection_id, + remove_connection_id, + ::update_key, + path_validation, + nullptr, // select_preferred_addr + ::stream_reset, + ::extend_max_remote_streams_bidi, + nullptr, // extend_max_remote_streams_uni + ::extend_max_stream_data, + nullptr, // dcid_status + nullptr, // handshake_confirmed + nullptr, // recv_new_token + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + nullptr, // recv_datagram + nullptr, // ack_datagram + nullptr, // lost_datagram + ngtcp2_crypto_get_path_challenge_data_cb, + stream_stop_sending, + ngtcp2_crypto_version_negotiation_cb, + nullptr, // recv_rx_key + ::recv_tx_key, + }; + + scid_.datalen = NGTCP2_SV_SCIDLEN; + if (util::generate_secure_random(scid_.data, scid_.datalen) != 0) { + std::cerr << "Could not generate connection ID" << std::endl; + return -1; + } + + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + settings.log_printf = config.quiet ? nullptr : debug::log_printf; + settings.initial_ts = util::timestamp(loop_); + settings.token = ngtcp2_vec{const_cast<uint8_t *>(token), tokenlen}; + settings.cc_algo = config.cc_algo; + settings.initial_rtt = config.initial_rtt; + settings.max_window = config.max_window; + settings.max_stream_window = config.max_stream_window; + settings.handshake_timeout = config.handshake_timeout; + settings.no_pmtud = config.no_pmtud; + settings.ack_thresh = config.ack_thresh; + if (config.max_udp_payload_size) { + settings.max_tx_udp_payload_size = config.max_udp_payload_size; + settings.no_tx_udp_payload_size_shaping = 1; + } + if (!config.qlog_dir.empty()) { + auto path = std::string{config.qlog_dir}; + path += '/'; + path += util::format_hex(scid_.data, scid_.datalen); + path += ".sqlog"; + qlog_ = fopen(path.c_str(), "w"); + if (qlog_ == nullptr) { + std::cerr << "Could not open qlog file " << std::quoted(path) << ": " + << strerror(errno) << std::endl; + return -1; + } + settings.qlog.write = ::write_qlog; + settings.qlog.odcid = *scid; + } + if (!config.preferred_versions.empty()) { + settings.preferred_versions = config.preferred_versions.data(); + settings.preferred_versionslen = config.preferred_versions.size(); + } + if (!config.other_versions.empty()) { + settings.other_versions = config.other_versions.data(); + settings.other_versionslen = config.other_versions.size(); + } + + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + params.initial_max_stream_data_bidi_local = config.max_stream_data_bidi_local; + params.initial_max_stream_data_bidi_remote = + config.max_stream_data_bidi_remote; + params.initial_max_stream_data_uni = config.max_stream_data_uni; + params.initial_max_data = config.max_data; + params.initial_max_streams_bidi = config.max_streams_bidi; + params.initial_max_streams_uni = config.max_streams_uni; + params.max_idle_timeout = config.timeout; + params.stateless_reset_token_present = 1; + params.active_connection_id_limit = 7; + + if (ocid) { + params.original_dcid = *ocid; + params.retry_scid = *scid; + params.retry_scid_present = 1; + } else { + params.original_dcid = *scid; + } + + if (util::generate_secure_random(params.stateless_reset_token, + sizeof(params.stateless_reset_token)) != 0) { + std::cerr << "Could not generate stateless reset token" << std::endl; + return -1; + } + + if (config.preferred_ipv4_addr.len || config.preferred_ipv6_addr.len) { + params.preferred_address_present = 1; + + if (config.preferred_ipv4_addr.len) { + params.preferred_address.ipv4 = config.preferred_ipv4_addr.su.in; + params.preferred_address.ipv4_present = 1; + } + + if (config.preferred_ipv6_addr.len) { + params.preferred_address.ipv6 = config.preferred_ipv6_addr.su.in6; + params.preferred_address.ipv6_present = 1; + } + + auto &token = params.preferred_address.stateless_reset_token; + if (util::generate_secure_random(token, sizeof(token)) != 0) { + std::cerr << "Could not generate preferred address stateless reset token" + << std::endl; + return -1; + } + + params.preferred_address.cid.datalen = NGTCP2_SV_SCIDLEN; + if (util::generate_secure_random(params.preferred_address.cid.data, + params.preferred_address.cid.datalen) != + 0) { + std::cerr << "Could not generate preferred address connection ID" + << std::endl; + return -1; + } + } + + auto path = ngtcp2_path{ + { + const_cast<sockaddr *>(&local_addr.su.sa), + local_addr.len, + }, + { + const_cast<sockaddr *>(sa), + salen, + }, + const_cast<Endpoint *>(&ep), + }; + if (auto rv = + ngtcp2_conn_server_new(&conn_, dcid, &scid_, &path, version, + &callbacks, &settings, ¶ms, nullptr, this); + rv != 0) { + std::cerr << "ngtcp2_conn_server_new: " << ngtcp2_strerror(rv) << std::endl; + return -1; + } + + if (tls_session_.init(tls_ctx, this) != 0) { + return -1; + } + + tls_session_.enable_keylog(); + + ngtcp2_conn_set_tls_native_handle(conn_, tls_session_.get_native_handle()); + + ev_io_set(&wev_, ep.fd, EV_WRITE); + + return 0; +} + +int Handler::feed_data(const Endpoint &ep, const Address &local_addr, + const sockaddr *sa, socklen_t salen, + const ngtcp2_pkt_info *pi, uint8_t *data, + size_t datalen) { + auto path = ngtcp2_path{ + { + const_cast<sockaddr *>(&local_addr.su.sa), + local_addr.len, + }, + { + const_cast<sockaddr *>(sa), + salen, + }, + const_cast<Endpoint *>(&ep), + }; + + if (auto rv = ngtcp2_conn_read_pkt(conn_, &path, pi, data, datalen, + util::timestamp(loop_)); + rv != 0) { + std::cerr << "ngtcp2_conn_read_pkt: " << ngtcp2_strerror(rv) << std::endl; + switch (rv) { + case NGTCP2_ERR_DRAINING: + start_draining_period(); + return NETWORK_ERR_CLOSE_WAIT; + case NGTCP2_ERR_RETRY: + return NETWORK_ERR_RETRY; + case NGTCP2_ERR_DROP_CONN: + return NETWORK_ERR_DROP_CONN; + case NGTCP2_ERR_CRYPTO: + if (!last_error_.error_code) { + ngtcp2_connection_close_error_set_transport_error_tls_alert( + &last_error_, ngtcp2_conn_get_tls_alert(conn_), nullptr, 0); + } + break; + default: + if (!last_error_.error_code) { + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, rv, nullptr, 0); + } + } + return handle_error(); + } + + return 0; +} + +int Handler::on_read(const Endpoint &ep, const Address &local_addr, + const sockaddr *sa, socklen_t salen, + const ngtcp2_pkt_info *pi, uint8_t *data, size_t datalen) { + if (auto rv = feed_data(ep, local_addr, sa, salen, pi, data, datalen); + rv != 0) { + return rv; + } + + update_timer(); + + return 0; +} + +int Handler::handle_expiry() { + auto now = util::timestamp(loop_); + if (auto rv = ngtcp2_conn_handle_expiry(conn_, now); rv != 0) { + std::cerr << "ngtcp2_conn_handle_expiry: " << ngtcp2_strerror(rv) + << std::endl; + ngtcp2_connection_close_error_set_transport_error_liberr(&last_error_, rv, + nullptr, 0); + return handle_error(); + } + + return 0; +} + +int Handler::on_write() { + if (ngtcp2_conn_is_in_closing_period(conn_) || + ngtcp2_conn_is_in_draining_period(conn_)) { + return 0; + } + + if (tx_.send_blocked) { + if (auto rv = send_blocked_packet(); rv != 0) { + return rv; + } + + if (tx_.send_blocked) { + return 0; + } + } + + if (auto rv = write_streams(); rv != 0) { + return rv; + } + + update_timer(); + + return 0; +} + +int Handler::write_streams() { + std::array<nghttp3_vec, 16> vec; + ngtcp2_path_storage ps, prev_ps; + uint32_t prev_ecn = 0; + size_t pktcnt = 0; + auto max_udp_payload_size = ngtcp2_conn_get_max_tx_udp_payload_size(conn_); + auto path_max_udp_payload_size = + ngtcp2_conn_get_path_max_tx_udp_payload_size(conn_); + auto max_pktcnt = ngtcp2_conn_get_send_quantum(conn_) / max_udp_payload_size; + uint8_t *bufpos = tx_.data.get(); + ngtcp2_pkt_info pi; + size_t gso_size = 0; + auto ts = util::timestamp(loop_); + + ngtcp2_path_storage_zero(&ps); + ngtcp2_path_storage_zero(&prev_ps); + + max_pktcnt = std::min(max_pktcnt, static_cast<size_t>(config.max_gso_dgrams)); + + for (;;) { + int64_t stream_id = -1; + int fin = 0; + nghttp3_ssize sveccnt = 0; + + if (httpconn_ && ngtcp2_conn_get_max_data_left(conn_)) { + sveccnt = nghttp3_conn_writev_stream(httpconn_, &stream_id, &fin, + vec.data(), vec.size()); + if (sveccnt < 0) { + std::cerr << "nghttp3_conn_writev_stream: " << nghttp3_strerror(sveccnt) + << std::endl; + ngtcp2_connection_close_error_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(sveccnt), + nullptr, 0); + return handle_error(); + } + } + + ngtcp2_ssize ndatalen; + auto v = vec.data(); + auto vcnt = static_cast<size_t>(sveccnt); + + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + if (fin) { + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + auto nwrite = ngtcp2_conn_writev_stream( + conn_, &ps.path, &pi, bufpos, max_udp_payload_size, &ndatalen, flags, + stream_id, reinterpret_cast<const ngtcp2_vec *>(v), vcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + assert(ndatalen == -1); + nghttp3_conn_block_stream(httpconn_, stream_id); + continue; + case NGTCP2_ERR_STREAM_SHUT_WR: + assert(ndatalen == -1); + nghttp3_conn_shutdown_stream_write(httpconn_, stream_id); + continue; + case NGTCP2_ERR_WRITE_MORE: + assert(ndatalen >= 0); + if (auto rv = + nghttp3_conn_add_write_offset(httpconn_, stream_id, ndatalen); + rv != 0) { + std::cerr << "nghttp3_conn_add_write_offset: " << nghttp3_strerror(rv) + << std::endl; + ngtcp2_connection_close_error_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, + 0); + return handle_error(); + } + continue; + } + + assert(ndatalen == -1); + + std::cerr << "ngtcp2_conn_writev_stream: " << ngtcp2_strerror(nwrite) + << std::endl; + ngtcp2_connection_close_error_set_transport_error_liberr( + &last_error_, nwrite, nullptr, 0); + return handle_error(); + } else if (ndatalen >= 0) { + if (auto rv = + nghttp3_conn_add_write_offset(httpconn_, stream_id, ndatalen); + rv != 0) { + std::cerr << "nghttp3_conn_add_write_offset: " << nghttp3_strerror(rv) + << std::endl; + ngtcp2_connection_close_error_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, + 0); + return handle_error(); + } + } + + if (nwrite == 0) { + if (bufpos - tx_.data.get()) { + auto &ep = *static_cast<Endpoint *>(prev_ps.path.user_data); + auto data = tx_.data.get(); + auto datalen = bufpos - data; + + if (auto [nsent, rv] = server_->send_packet( + ep, no_gso_, prev_ps.path.local, prev_ps.path.remote, prev_ecn, + data, datalen, gso_size); + rv != NETWORK_ERR_OK) { + assert(NETWORK_ERR_SEND_BLOCKED == rv); + + on_send_blocked(ep, prev_ps.path.local, prev_ps.path.remote, prev_ecn, + data + nsent, datalen - nsent, gso_size); + + start_wev_endpoint(ep); + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + return 0; + } + } + + ev_io_stop(loop_, &wev_); + + // We are congestion limited. + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + return 0; + } + + bufpos += nwrite; + + if (pktcnt == 0) { + ngtcp2_path_copy(&prev_ps.path, &ps.path); + prev_ecn = pi.ecn; + gso_size = nwrite; + } else if (!ngtcp2_path_eq(&prev_ps.path, &ps.path) || prev_ecn != pi.ecn || + static_cast<size_t>(nwrite) > gso_size || + (gso_size > path_max_udp_payload_size && + static_cast<size_t>(nwrite) != gso_size)) { + auto &ep = *static_cast<Endpoint *>(prev_ps.path.user_data); + auto data = tx_.data.get(); + auto datalen = bufpos - data - nwrite; + + if (auto [nsent, rv] = server_->send_packet( + ep, no_gso_, prev_ps.path.local, prev_ps.path.remote, prev_ecn, + data, datalen, gso_size); + rv != 0) { + assert(NETWORK_ERR_SEND_BLOCKED == rv); + + on_send_blocked(ep, prev_ps.path.local, prev_ps.path.remote, prev_ecn, + data + nsent, datalen - nsent, gso_size); + + on_send_blocked(*static_cast<Endpoint *>(ps.path.user_data), + ps.path.local, ps.path.remote, pi.ecn, bufpos - nwrite, + nwrite, 0); + + start_wev_endpoint(ep); + } else { + auto &ep = *static_cast<Endpoint *>(ps.path.user_data); + auto data = bufpos - nwrite; + + if (auto [nsent, rv] = + server_->send_packet(ep, no_gso_, ps.path.local, ps.path.remote, + pi.ecn, data, nwrite, nwrite); + rv != 0) { + assert(nsent == 0); + assert(NETWORK_ERR_SEND_BLOCKED == rv); + + on_send_blocked(ep, ps.path.local, ps.path.remote, pi.ecn, data, + nwrite, 0); + } + + start_wev_endpoint(ep); + } + + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + return 0; + } + + if (++pktcnt == max_pktcnt || static_cast<size_t>(nwrite) < gso_size) { + auto &ep = *static_cast<Endpoint *>(ps.path.user_data); + auto data = tx_.data.get(); + auto datalen = bufpos - data; + + if (auto [nsent, rv] = + server_->send_packet(ep, no_gso_, ps.path.local, ps.path.remote, + pi.ecn, data, datalen, gso_size); + rv != 0) { + assert(NETWORK_ERR_SEND_BLOCKED == rv); + + on_send_blocked(ep, ps.path.local, ps.path.remote, pi.ecn, data + nsent, + datalen - nsent, gso_size); + } + + start_wev_endpoint(ep); + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + return 0; + } + } +} + +void Handler::on_send_blocked(Endpoint &ep, const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, unsigned int ecn, + const uint8_t *data, size_t datalen, + size_t gso_size) { + assert(tx_.num_blocked || !tx_.send_blocked); + assert(tx_.num_blocked < 2); + + tx_.send_blocked = true; + + auto &p = tx_.blocked[tx_.num_blocked++]; + + memcpy(&p.local_addr.su, local_addr.addr, local_addr.addrlen); + memcpy(&p.remote_addr.su, remote_addr.addr, remote_addr.addrlen); + + p.local_addr.len = local_addr.addrlen; + p.remote_addr.len = remote_addr.addrlen; + p.endpoint = &ep; + p.ecn = ecn; + p.data = data; + p.datalen = datalen; + p.gso_size = gso_size; +} + +void Handler::start_wev_endpoint(const Endpoint &ep) { + // We do not close ep.fd, so we can expect that each Endpoint has + // unique fd. + if (ep.fd != wev_.fd) { + if (ev_is_active(&wev_)) { + ev_io_stop(loop_, &wev_); + } + + ev_io_set(&wev_, ep.fd, EV_WRITE); + } + + ev_io_start(loop_, &wev_); +} + +int Handler::send_blocked_packet() { + assert(tx_.send_blocked); + + for (; tx_.num_blocked_sent < tx_.num_blocked; ++tx_.num_blocked_sent) { + auto &p = tx_.blocked[tx_.num_blocked_sent]; + + ngtcp2_addr local_addr{ + .addr = &p.local_addr.su.sa, + .addrlen = p.local_addr.len, + }; + ngtcp2_addr remote_addr{ + .addr = &p.remote_addr.su.sa, + .addrlen = p.remote_addr.len, + }; + + auto [nsent, rv] = + server_->send_packet(*p.endpoint, no_gso_, local_addr, remote_addr, + p.ecn, p.data, p.datalen, p.gso_size); + if (rv != 0) { + assert(NETWORK_ERR_SEND_BLOCKED == rv); + + p.data += nsent; + p.datalen -= nsent; + + start_wev_endpoint(*p.endpoint); + + return 0; + } + } + + tx_.send_blocked = false; + tx_.num_blocked = 0; + tx_.num_blocked_sent = 0; + + return 0; +} + +void Handler::signal_write() { ev_io_start(loop_, &wev_); } + +void Handler::start_draining_period() { + ev_io_stop(loop_, &wev_); + + ev_set_cb(&timer_, close_waitcb); + timer_.repeat = + static_cast<ev_tstamp>(ngtcp2_conn_get_pto(conn_)) / NGTCP2_SECONDS * 3; + ev_timer_again(loop_, &timer_); + + if (!config.quiet) { + std::cerr << "Draining period has started (" << timer_.repeat << " seconds)" + << std::endl; + } +} + +int Handler::start_closing_period() { + if (!conn_ || ngtcp2_conn_is_in_closing_period(conn_) || + ngtcp2_conn_is_in_draining_period(conn_)) { + return 0; + } + + ev_io_stop(loop_, &wev_); + + ev_set_cb(&timer_, close_waitcb); + timer_.repeat = + static_cast<ev_tstamp>(ngtcp2_conn_get_pto(conn_)) / NGTCP2_SECONDS * 3; + ev_timer_again(loop_, &timer_); + + if (!config.quiet) { + std::cerr << "Closing period has started (" << timer_.repeat << " seconds)" + << std::endl; + } + + conn_closebuf_ = std::make_unique<Buffer>(NGTCP2_MAX_UDP_PAYLOAD_SIZE); + + ngtcp2_path_storage ps; + + ngtcp2_path_storage_zero(&ps); + + ngtcp2_pkt_info pi; + auto n = ngtcp2_conn_write_connection_close( + conn_, &ps.path, &pi, conn_closebuf_->wpos(), conn_closebuf_->left(), + &last_error_, util::timestamp(loop_)); + if (n < 0) { + std::cerr << "ngtcp2_conn_write_connection_close: " << ngtcp2_strerror(n) + << std::endl; + return -1; + } + + if (n == 0) { + return 0; + } + + conn_closebuf_->push(n); + + return 0; +} + +int Handler::handle_error() { + if (last_error_.type == + NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_TRANSPORT_IDLE_CLOSE) { + return -1; + } + + if (start_closing_period() != 0) { + return -1; + } + + if (ngtcp2_conn_is_in_draining_period(conn_)) { + return NETWORK_ERR_CLOSE_WAIT; + } + + if (auto rv = send_conn_close(); rv != NETWORK_ERR_OK) { + return rv; + } + + return NETWORK_ERR_CLOSE_WAIT; +} + +int Handler::send_conn_close() { + if (!config.quiet) { + std::cerr << "Closing Period: TX CONNECTION_CLOSE" << std::endl; + } + + assert(conn_closebuf_ && conn_closebuf_->size()); + assert(conn_); + assert(!ngtcp2_conn_is_in_draining_period(conn_)); + + auto path = ngtcp2_conn_get_path(conn_); + + return server_->send_packet( + *static_cast<Endpoint *>(path->user_data), path->local, path->remote, + /* ecn = */ 0, conn_closebuf_->rpos(), conn_closebuf_->size()); +} + +void Handler::update_timer() { + auto expiry = ngtcp2_conn_get_expiry(conn_); + auto now = util::timestamp(loop_); + + if (expiry <= now) { + if (!config.quiet) { + auto t = static_cast<ev_tstamp>(now - expiry) / NGTCP2_SECONDS; + std::cerr << "Timer has already expired: " << t << "s" << std::endl; + } + + ev_feed_event(loop_, &timer_, EV_TIMER); + + return; + } + + auto t = static_cast<ev_tstamp>(expiry - now) / NGTCP2_SECONDS; + if (!config.quiet) { + std::cerr << "Set timer=" << std::fixed << t << "s" << std::defaultfloat + << std::endl; + } + timer_.repeat = t; + ev_timer_again(loop_, &timer_); +} + +int Handler::recv_stream_data(uint32_t flags, int64_t stream_id, + const uint8_t *data, size_t datalen) { + if (!config.quiet && !config.no_quic_dump) { + debug::print_stream_data(stream_id, data, datalen); + } + + if (!httpconn_) { + return 0; + } + + auto nconsumed = nghttp3_conn_read_stream( + httpconn_, stream_id, data, datalen, flags & NGTCP2_STREAM_DATA_FLAG_FIN); + if (nconsumed < 0) { + std::cerr << "nghttp3_conn_read_stream: " << nghttp3_strerror(nconsumed) + << std::endl; + ngtcp2_connection_close_error_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(nconsumed), nullptr, + 0); + return -1; + } + + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, nconsumed); + ngtcp2_conn_extend_max_offset(conn_, nconsumed); + + return 0; +} + +int Handler::update_key(uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen) { + auto crypto_ctx = ngtcp2_conn_get_crypto_ctx(conn_); + auto aead = &crypto_ctx->aead; + auto keylen = ngtcp2_crypto_aead_keylen(aead); + auto ivlen = ngtcp2_crypto_packet_protection_ivlen(aead); + + ++nkey_update_; + + std::array<uint8_t, 64> rx_key, tx_key; + + if (ngtcp2_crypto_update_key(conn_, rx_secret, tx_secret, rx_aead_ctx, + rx_key.data(), rx_iv, tx_aead_ctx, tx_key.data(), + tx_iv, current_rx_secret, current_tx_secret, + secretlen) != 0) { + return -1; + } + + if (!config.quiet && config.show_secret) { + std::cerr << "application_traffic rx secret " << nkey_update_ << std::endl; + debug::print_secrets(rx_secret, secretlen, rx_key.data(), keylen, rx_iv, + ivlen); + std::cerr << "application_traffic tx secret " << nkey_update_ << std::endl; + debug::print_secrets(tx_secret, secretlen, tx_key.data(), keylen, tx_iv, + ivlen); + } + + return 0; +} + +Server *Handler::server() const { return server_; } + +int Handler::on_stream_close(int64_t stream_id, uint64_t app_error_code) { + if (!config.quiet) { + std::cerr << "QUIC stream " << stream_id << " closed" << std::endl; + } + + if (httpconn_) { + if (app_error_code == 0) { + app_error_code = NGHTTP3_H3_NO_ERROR; + } + auto rv = nghttp3_conn_close_stream(httpconn_, stream_id, app_error_code); + switch (rv) { + case 0: + break; + case NGHTTP3_ERR_STREAM_NOT_FOUND: + if (ngtcp2_is_bidi_stream(stream_id)) { + assert(!ngtcp2_conn_is_local_stream(conn_, stream_id)); + ngtcp2_conn_extend_max_streams_bidi(conn_, 1); + } + break; + default: + std::cerr << "nghttp3_conn_close_stream: " << nghttp3_strerror(rv) + << std::endl; + ngtcp2_connection_close_error_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, 0); + return -1; + } + } + + return 0; +} + +void Handler::shutdown_read(int64_t stream_id, int app_error_code) { + ngtcp2_conn_shutdown_stream_read(conn_, stream_id, app_error_code); +} + +namespace { +void sreadcb(struct ev_loop *loop, ev_io *w, int revents) { + auto ep = static_cast<Endpoint *>(w->data); + + ep->server->on_read(*ep); +} +} // namespace + +namespace { +void siginthandler(struct ev_loop *loop, ev_signal *watcher, int revents) { + ev_break(loop, EVBREAK_ALL); +} +} // namespace + +Server::Server(struct ev_loop *loop, TLSServerContext &tls_ctx) + : loop_(loop), tls_ctx_(tls_ctx) { + ev_signal_init(&sigintev_, siginthandler, SIGINT); +} + +Server::~Server() { + disconnect(); + close(); +} + +void Server::disconnect() { + config.tx_loss_prob = 0; + + for (auto &ep : endpoints_) { + ev_io_stop(loop_, &ep.rev); + } + + ev_signal_stop(loop_, &sigintev_); + + while (!handlers_.empty()) { + auto it = std::begin(handlers_); + auto &h = (*it).second; + + h->handle_error(); + + remove(h); + } +} + +void Server::close() { + for (auto &ep : endpoints_) { + ::close(ep.fd); + } + + endpoints_.clear(); +} + +namespace { +int create_sock(Address &local_addr, const char *addr, const char *port, + int family) { + addrinfo hints{}; + addrinfo *res, *rp; + int val = 1; + + hints.ai_family = family; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_PASSIVE; + + if (strcmp(addr, "*") == 0) { + addr = nullptr; + } + + if (auto rv = getaddrinfo(addr, port, &hints, &res); rv != 0) { + std::cerr << "getaddrinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + auto res_d = defer(freeaddrinfo, res); + + int fd = -1; + + for (rp = res; rp; rp = rp->ai_next) { + fd = util::create_nonblock_socket(rp->ai_family, rp->ai_socktype, + rp->ai_protocol); + if (fd == -1) { + continue; + } + + if (rp->ai_family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + close(fd); + continue; + } + + if (setsockopt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + close(fd); + continue; + } + } else if (setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + close(fd); + continue; + } + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + close(fd); + continue; + } + + fd_set_recv_ecn(fd, rp->ai_family); + fd_set_ip_mtu_discover(fd, rp->ai_family); + fd_set_ip_dontfrag(fd, family); + + if (bind(fd, rp->ai_addr, rp->ai_addrlen) != -1) { + break; + } + + close(fd); + } + + if (!rp) { + std::cerr << "Could not bind" << std::endl; + return -1; + } + + socklen_t len = sizeof(local_addr.su.storage); + if (getsockname(fd, &local_addr.su.sa, &len) == -1) { + std::cerr << "getsockname: " << strerror(errno) << std::endl; + close(fd); + return -1; + } + local_addr.len = len; + local_addr.ifindex = 0; + + return fd; +} + +} // namespace + +namespace { +int add_endpoint(std::vector<Endpoint> &endpoints, const char *addr, + const char *port, int af) { + Address dest; + auto fd = create_sock(dest, addr, port, af); + if (fd == -1) { + return -1; + } + + endpoints.emplace_back(); + auto &ep = endpoints.back(); + ep.addr = dest; + ep.fd = fd; + ev_io_init(&ep.rev, sreadcb, 0, EV_READ); + + return 0; +} +} // namespace + +namespace { +int add_endpoint(std::vector<Endpoint> &endpoints, const Address &addr) { + auto fd = util::create_nonblock_socket(addr.su.sa.sa_family, SOCK_DGRAM, 0); + if (fd == -1) { + std::cerr << "socket: " << strerror(errno) << std::endl; + return -1; + } + + int val = 1; + if (addr.su.sa.sa_family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + std::cerr << "setsockopt: " << strerror(errno) << std::endl; + close(fd); + return -1; + } + + if (setsockopt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + std::cerr << "setsockopt: " << strerror(errno) << std::endl; + close(fd); + return -1; + } + } else if (setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + std::cerr << "setsockopt: " << strerror(errno) << std::endl; + close(fd); + return -1; + } + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + close(fd); + return -1; + } + + fd_set_recv_ecn(fd, addr.su.sa.sa_family); + fd_set_ip_mtu_discover(fd, addr.su.sa.sa_family); + fd_set_ip_dontfrag(fd, addr.su.sa.sa_family); + + if (bind(fd, &addr.su.sa, addr.len) == -1) { + std::cerr << "bind: " << strerror(errno) << std::endl; + close(fd); + return -1; + } + + endpoints.emplace_back(Endpoint{}); + auto &ep = endpoints.back(); + ep.addr = addr; + ep.fd = fd; + ev_io_init(&ep.rev, sreadcb, 0, EV_READ); + + return 0; +} +} // namespace + +int Server::init(const char *addr, const char *port) { + endpoints_.reserve(4); + + auto ready = false; + if (!util::numeric_host(addr, AF_INET6) && + add_endpoint(endpoints_, addr, port, AF_INET) == 0) { + ready = true; + } + if (!util::numeric_host(addr, AF_INET) && + add_endpoint(endpoints_, addr, port, AF_INET6) == 0) { + ready = true; + } + if (!ready) { + return -1; + } + + if (config.preferred_ipv4_addr.len && + add_endpoint(endpoints_, config.preferred_ipv4_addr) != 0) { + return -1; + } + if (config.preferred_ipv6_addr.len && + add_endpoint(endpoints_, config.preferred_ipv6_addr) != 0) { + return -1; + } + + for (auto &ep : endpoints_) { + ep.server = this; + ep.rev.data = &ep; + + ev_io_set(&ep.rev, ep.fd, EV_READ); + + ev_io_start(loop_, &ep.rev); + } + + ev_signal_start(loop_, &sigintev_); + + return 0; +} + +int Server::on_read(Endpoint &ep) { + sockaddr_union su; + std::array<uint8_t, 64_k> buf; + ngtcp2_pkt_hd hd; + size_t pktcnt = 0; + ngtcp2_pkt_info pi; + + iovec msg_iov; + msg_iov.iov_base = buf.data(); + msg_iov.iov_len = buf.size(); + + msghdr msg{}; + msg.msg_name = &su; + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + + uint8_t + msg_ctrl[CMSG_SPACE(sizeof(uint8_t)) + CMSG_SPACE(sizeof(in6_pktinfo))]; + msg.msg_control = msg_ctrl; + + for (; pktcnt < 10;) { + msg.msg_namelen = sizeof(su); + msg.msg_controllen = sizeof(msg_ctrl); + + auto nread = recvmsg(ep.fd, &msg, 0); + if (nread == -1) { + if (!(errno == EAGAIN || errno == ENOTCONN)) { + std::cerr << "recvmsg: " << strerror(errno) << std::endl; + } + return 0; + } + + ++pktcnt; + + pi.ecn = msghdr_get_ecn(&msg, su.storage.ss_family); + auto local_addr = msghdr_get_local_addr(&msg, su.storage.ss_family); + if (!local_addr) { + std::cerr << "Unable to obtain local address" << std::endl; + continue; + } + + set_port(*local_addr, ep.addr); + + if (!config.quiet) { + std::array<char, IF_NAMESIZE> ifname; + std::cerr << "Received packet: local=" + << util::straddr(&local_addr->su.sa, local_addr->len) + << " remote=" << util::straddr(&su.sa, msg.msg_namelen) + << " if=" << if_indextoname(local_addr->ifindex, ifname.data()) + << " ecn=0x" << std::hex << pi.ecn << std::dec << " " << nread + << " bytes" << std::endl; + } + + if (debug::packet_lost(config.rx_loss_prob)) { + if (!config.quiet) { + std::cerr << "** Simulated incoming packet loss **" << std::endl; + } + continue; + } + + if (nread == 0) { + continue; + } + + ngtcp2_version_cid vc; + + switch (auto rv = ngtcp2_pkt_decode_version_cid(&vc, buf.data(), nread, + NGTCP2_SV_SCIDLEN); + rv) { + case 0: + break; + case NGTCP2_ERR_VERSION_NEGOTIATION: + send_version_negotiation(vc.version, vc.scid, vc.scidlen, vc.dcid, + vc.dcidlen, ep, *local_addr, &su.sa, + msg.msg_namelen); + continue; + default: + std::cerr << "Could not decode version and CID from QUIC packet header: " + << ngtcp2_strerror(rv) << std::endl; + continue; + } + + auto dcid_key = util::make_cid_key(vc.dcid, vc.dcidlen); + + auto handler_it = handlers_.find(dcid_key); + if (handler_it == std::end(handlers_)) { + switch (auto rv = ngtcp2_accept(&hd, buf.data(), nread); rv) { + case 0: + break; + case NGTCP2_ERR_RETRY: + send_retry(&hd, ep, *local_addr, &su.sa, msg.msg_namelen, nread * 3); + continue; + default: + if (!config.quiet) { + std::cerr << "Unexpected packet received: length=" << nread + << std::endl; + } + continue; + } + + ngtcp2_cid ocid; + ngtcp2_cid *pocid = nullptr; + + assert(hd.type == NGTCP2_PKT_INITIAL); + + if (config.validate_addr || hd.token.len) { + std::cerr << "Perform stateless address validation" << std::endl; + if (hd.token.len == 0) { + send_retry(&hd, ep, *local_addr, &su.sa, msg.msg_namelen, nread * 3); + continue; + } + + if (hd.token.base[0] != NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY && + hd.dcid.datalen < NGTCP2_MIN_INITIAL_DCIDLEN) { + send_stateless_connection_close(&hd, ep, *local_addr, &su.sa, + msg.msg_namelen); + continue; + } + + switch (hd.token.base[0]) { + case NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY: + if (verify_retry_token(&ocid, &hd, &su.sa, msg.msg_namelen) != 0) { + send_stateless_connection_close(&hd, ep, *local_addr, &su.sa, + msg.msg_namelen); + continue; + } + pocid = &ocid; + break; + case NGTCP2_CRYPTO_TOKEN_MAGIC_REGULAR: + if (verify_token(&hd, &su.sa, msg.msg_namelen) != 0) { + if (config.validate_addr) { + send_retry(&hd, ep, *local_addr, &su.sa, msg.msg_namelen, + nread * 3); + continue; + } + + hd.token.base = nullptr; + hd.token.len = 0; + } + break; + default: + if (!config.quiet) { + std::cerr << "Ignore unrecognized token" << std::endl; + } + if (config.validate_addr) { + send_retry(&hd, ep, *local_addr, &su.sa, msg.msg_namelen, + nread * 3); + continue; + } + + hd.token.base = nullptr; + hd.token.len = 0; + break; + } + } + + auto h = std::make_unique<Handler>(loop_, this); + if (h->init(ep, *local_addr, &su.sa, msg.msg_namelen, &hd.scid, &hd.dcid, + pocid, hd.token.base, hd.token.len, hd.version, + tls_ctx_) != 0) { + continue; + } + + switch (h->on_read(ep, *local_addr, &su.sa, msg.msg_namelen, &pi, + buf.data(), nread)) { + case 0: + break; + case NETWORK_ERR_RETRY: + send_retry(&hd, ep, *local_addr, &su.sa, msg.msg_namelen, nread * 3); + continue; + default: + continue; + } + + switch (h->on_write()) { + case 0: + break; + default: + continue; + } + + std::array<ngtcp2_cid, 2> scids; + auto conn = h->conn(); + + auto num_scid = ngtcp2_conn_get_num_scid(conn); + + assert(num_scid <= scids.size()); + + ngtcp2_conn_get_scid(conn, scids.data()); + + for (size_t i = 0; i < num_scid; ++i) { + handlers_.emplace(util::make_cid_key(&scids[i]), h.get()); + } + + handlers_.emplace(dcid_key, h.get()); + + h.release(); + + continue; + } + + auto h = (*handler_it).second; + auto conn = h->conn(); + if (ngtcp2_conn_is_in_closing_period(conn)) { + // TODO do exponential backoff. + switch (h->send_conn_close()) { + case 0: + break; + default: + remove(h); + } + continue; + } + if (ngtcp2_conn_is_in_draining_period(conn)) { + continue; + } + + if (auto rv = h->on_read(ep, *local_addr, &su.sa, msg.msg_namelen, &pi, + buf.data(), nread); + rv != 0) { + if (rv != NETWORK_ERR_CLOSE_WAIT) { + remove(h); + } + continue; + } + + h->signal_write(); + } + + return 0; +} + +namespace { +uint32_t generate_reserved_version(const sockaddr *sa, socklen_t salen, + uint32_t version) { + uint32_t h = 0x811C9DC5u; + const uint8_t *p = (const uint8_t *)sa; + const uint8_t *ep = p + salen; + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + version = htonl(version); + p = (const uint8_t *)&version; + ep = p + sizeof(version); + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + h &= 0xf0f0f0f0u; + h |= 0x0a0a0a0au; + return h; +} +} // namespace + +int Server::send_version_negotiation(uint32_t version, const uint8_t *dcid, + size_t dcidlen, const uint8_t *scid, + size_t scidlen, Endpoint &ep, + const Address &local_addr, + const sockaddr *sa, socklen_t salen) { + Buffer buf{NGTCP2_MAX_UDP_PAYLOAD_SIZE}; + std::array<uint32_t, 1 + max_preferred_versionslen> sv; + + auto p = std::begin(sv); + + *p++ = generate_reserved_version(sa, salen, version); + + if (config.preferred_versions.empty()) { + *p++ = NGTCP2_PROTO_VER_V1; + } else { + for (auto v : config.preferred_versions) { + *p++ = v; + } + } + + auto nwrite = ngtcp2_pkt_write_version_negotiation( + buf.wpos(), buf.left(), + std::uniform_int_distribution<uint8_t>( + 0, std::numeric_limits<uint8_t>::max())(randgen), + dcid, dcidlen, scid, scidlen, sv.data(), p - std::begin(sv)); + if (nwrite < 0) { + std::cerr << "ngtcp2_pkt_write_version_negotiation: " + << ngtcp2_strerror(nwrite) << std::endl; + return -1; + } + + buf.push(nwrite); + + ngtcp2_addr laddr{ + const_cast<sockaddr *>(&local_addr.su.sa), + local_addr.len, + }; + ngtcp2_addr raddr{ + const_cast<sockaddr *>(sa), + salen, + }; + + if (send_packet(ep, laddr, raddr, /* ecn = */ 0, buf.rpos(), buf.size()) != + NETWORK_ERR_OK) { + return -1; + } + + return 0; +} + +int Server::send_retry(const ngtcp2_pkt_hd *chd, Endpoint &ep, + const Address &local_addr, const sockaddr *sa, + socklen_t salen, size_t max_pktlen) { + std::array<char, NI_MAXHOST> host; + std::array<char, NI_MAXSERV> port; + + if (auto rv = getnameinfo(sa, salen, host.data(), host.size(), port.data(), + port.size(), NI_NUMERICHOST | NI_NUMERICSERV); + rv != 0) { + std::cerr << "getnameinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + if (!config.quiet) { + std::cerr << "Sending Retry packet to [" << host.data() + << "]:" << port.data() << std::endl; + } + + ngtcp2_cid scid; + + scid.datalen = NGTCP2_SV_SCIDLEN; + if (util::generate_secure_random(scid.data, scid.datalen) != 0) { + return -1; + } + + std::array<uint8_t, NGTCP2_CRYPTO_MAX_RETRY_TOKENLEN> token; + + auto t = std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + auto tokenlen = ngtcp2_crypto_generate_retry_token( + token.data(), config.static_secret.data(), config.static_secret.size(), + chd->version, sa, salen, &scid, &chd->dcid, t); + if (tokenlen < 0) { + return -1; + } + + if (!config.quiet) { + std::cerr << "Generated address validation token:" << std::endl; + util::hexdump(stderr, token.data(), tokenlen); + } + + Buffer buf{ + std::min(static_cast<size_t>(NGTCP2_MAX_UDP_PAYLOAD_SIZE), max_pktlen)}; + + auto nwrite = ngtcp2_crypto_write_retry(buf.wpos(), buf.left(), chd->version, + &chd->scid, &scid, &chd->dcid, + token.data(), tokenlen); + if (nwrite < 0) { + std::cerr << "ngtcp2_crypto_write_retry failed" << std::endl; + return -1; + } + + buf.push(nwrite); + + ngtcp2_addr laddr{ + const_cast<sockaddr *>(&local_addr.su.sa), + local_addr.len, + }; + ngtcp2_addr raddr{ + const_cast<sockaddr *>(sa), + salen, + }; + + if (send_packet(ep, laddr, raddr, /* ecn = */ 0, buf.rpos(), buf.size()) != + NETWORK_ERR_OK) { + return -1; + } + + return 0; +} + +int Server::send_stateless_connection_close(const ngtcp2_pkt_hd *chd, + Endpoint &ep, + const Address &local_addr, + const sockaddr *sa, + socklen_t salen) { + Buffer buf{NGTCP2_MAX_UDP_PAYLOAD_SIZE}; + + auto nwrite = ngtcp2_crypto_write_connection_close( + buf.wpos(), buf.left(), chd->version, &chd->scid, &chd->dcid, + NGTCP2_INVALID_TOKEN, nullptr, 0); + if (nwrite < 0) { + std::cerr << "ngtcp2_crypto_write_connection_close failed" << std::endl; + return -1; + } + + buf.push(nwrite); + + ngtcp2_addr laddr{ + const_cast<sockaddr *>(&local_addr.su.sa), + local_addr.len, + }; + ngtcp2_addr raddr{ + const_cast<sockaddr *>(sa), + salen, + }; + + if (send_packet(ep, laddr, raddr, /* ecn = */ 0, buf.rpos(), buf.size()) != + NETWORK_ERR_OK) { + return -1; + } + + return 0; +} + +int Server::verify_retry_token(ngtcp2_cid *ocid, const ngtcp2_pkt_hd *hd, + const sockaddr *sa, socklen_t salen) { + std::array<char, NI_MAXHOST> host; + std::array<char, NI_MAXSERV> port; + + if (auto rv = getnameinfo(sa, salen, host.data(), host.size(), port.data(), + port.size(), NI_NUMERICHOST | NI_NUMERICSERV); + rv != 0) { + std::cerr << "getnameinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + if (!config.quiet) { + std::cerr << "Verifying Retry token from [" << host.data() + << "]:" << port.data() << std::endl; + util::hexdump(stderr, hd->token.base, hd->token.len); + } + + auto t = std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + if (ngtcp2_crypto_verify_retry_token( + ocid, hd->token.base, hd->token.len, config.static_secret.data(), + config.static_secret.size(), hd->version, sa, salen, &hd->dcid, + 10 * NGTCP2_SECONDS, t) != 0) { + std::cerr << "Could not verify Retry token" << std::endl; + + return -1; + } + + if (!config.quiet) { + std::cerr << "Token was successfully validated" << std::endl; + } + + return 0; +} + +int Server::verify_token(const ngtcp2_pkt_hd *hd, const sockaddr *sa, + socklen_t salen) { + std::array<char, NI_MAXHOST> host; + std::array<char, NI_MAXSERV> port; + + if (auto rv = getnameinfo(sa, salen, host.data(), host.size(), port.data(), + port.size(), NI_NUMERICHOST | NI_NUMERICSERV); + rv != 0) { + std::cerr << "getnameinfo: " << gai_strerror(rv) << std::endl; + return -1; + } + + if (!config.quiet) { + std::cerr << "Verifying token from [" << host.data() << "]:" << port.data() + << std::endl; + util::hexdump(stderr, hd->token.base, hd->token.len); + } + + auto t = std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + if (ngtcp2_crypto_verify_regular_token(hd->token.base, hd->token.len, + config.static_secret.data(), + config.static_secret.size(), sa, salen, + 3600 * NGTCP2_SECONDS, t) != 0) { + if (!config.quiet) { + std::cerr << "Could not verify token" << std::endl; + } + return -1; + } + + if (!config.quiet) { + std::cerr << "Token was successfully validated" << std::endl; + } + + return 0; +} + +int Server::send_packet(Endpoint &ep, const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, unsigned int ecn, + const uint8_t *data, size_t datalen) { + auto no_gso = false; + auto [_, rv] = send_packet(ep, no_gso, local_addr, remote_addr, ecn, data, + datalen, datalen); + + return rv; +} + +std::pair<size_t, int> +Server::send_packet(Endpoint &ep, bool &no_gso, const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, unsigned int ecn, + const uint8_t *data, size_t datalen, size_t gso_size) { + assert(gso_size); + + if (debug::packet_lost(config.tx_loss_prob)) { + if (!config.quiet) { + std::cerr << "** Simulated outgoing packet loss **" << std::endl; + } + return {0, NETWORK_ERR_OK}; + } + + if (no_gso && datalen > gso_size) { + size_t nsent = 0; + + for (auto p = data; p < data + datalen; p += gso_size) { + auto len = std::min(gso_size, static_cast<size_t>(data + datalen - p)); + + auto [n, rv] = + send_packet(ep, no_gso, local_addr, remote_addr, ecn, p, len, len); + if (rv != 0) { + return {nsent, rv}; + } + + nsent += n; + } + + return {nsent, 0}; + } + + iovec msg_iov; + msg_iov.iov_base = const_cast<uint8_t *>(data); + msg_iov.iov_len = datalen; + + msghdr msg{}; + msg.msg_name = const_cast<sockaddr *>(remote_addr.addr); + msg.msg_namelen = remote_addr.addrlen; + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + + uint8_t + msg_ctrl[CMSG_SPACE(sizeof(uint16_t)) + CMSG_SPACE(sizeof(in6_pktinfo))]; + + memset(msg_ctrl, 0, sizeof(msg_ctrl)); + + msg.msg_control = msg_ctrl; + msg.msg_controllen = sizeof(msg_ctrl); + + size_t controllen = 0; + + auto cm = CMSG_FIRSTHDR(&msg); + + switch (local_addr.addr->sa_family) { + case AF_INET: { + controllen += CMSG_SPACE(sizeof(in_pktinfo)); + cm->cmsg_level = IPPROTO_IP; + cm->cmsg_type = IP_PKTINFO; + cm->cmsg_len = CMSG_LEN(sizeof(in_pktinfo)); + auto pktinfo = reinterpret_cast<in_pktinfo *>(CMSG_DATA(cm)); + memset(pktinfo, 0, sizeof(in_pktinfo)); + auto addrin = reinterpret_cast<sockaddr_in *>(local_addr.addr); + pktinfo->ipi_spec_dst = addrin->sin_addr; + break; + } + case AF_INET6: { + controllen += CMSG_SPACE(sizeof(in6_pktinfo)); + cm->cmsg_level = IPPROTO_IPV6; + cm->cmsg_type = IPV6_PKTINFO; + cm->cmsg_len = CMSG_LEN(sizeof(in6_pktinfo)); + auto pktinfo = reinterpret_cast<in6_pktinfo *>(CMSG_DATA(cm)); + memset(pktinfo, 0, sizeof(in6_pktinfo)); + auto addrin = reinterpret_cast<sockaddr_in6 *>(local_addr.addr); + pktinfo->ipi6_addr = addrin->sin6_addr; + break; + } + default: + assert(0); + } + +#ifdef UDP_SEGMENT + if (datalen > gso_size) { + controllen += CMSG_SPACE(sizeof(uint16_t)); + cm = CMSG_NXTHDR(&msg, cm); + cm->cmsg_level = SOL_UDP; + cm->cmsg_type = UDP_SEGMENT; + cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); + *(reinterpret_cast<uint16_t *>(CMSG_DATA(cm))) = gso_size; + } +#endif // UDP_SEGMENT + + msg.msg_controllen = controllen; + + if (ep.ecn != ecn) { + ep.ecn = ecn; + fd_set_ecn(ep.fd, ep.addr.su.storage.ss_family, ecn); + } + + ssize_t nwrite = 0; + + do { + nwrite = sendmsg(ep.fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + switch (errno) { + case EAGAIN: +#if EAGAIN != EWOULDBLOCK + case EWOULDBLOCK: +#endif // EAGAIN != EWOULDBLOCK + return {0, NETWORK_ERR_SEND_BLOCKED}; +#ifdef UDP_SEGMENT + case EIO: + if (datalen > gso_size) { + // GSO failure; send each packet in a separate sendmsg call. + std::cerr << "sendmsg: disabling GSO due to " << strerror(errno) + << std::endl; + + no_gso = true; + + return send_packet(ep, no_gso, local_addr, remote_addr, ecn, data, + datalen, gso_size); + } + break; +#endif // UDP_SEGMENT + } + + std::cerr << "sendmsg: " << strerror(errno) << std::endl; + // TODO We have packet which is expected to fail to send (e.g., + // path validation to old path). + return {0, NETWORK_ERR_OK}; + } + + if (!config.quiet) { + std::cerr << "Sent packet: local=" + << util::straddr(local_addr.addr, local_addr.addrlen) + << " remote=" + << util::straddr(remote_addr.addr, remote_addr.addrlen) + << " ecn=0x" << std::hex << ecn << std::dec << " " << nwrite + << " bytes" << std::endl; + } + + return {nwrite, NETWORK_ERR_OK}; +} + +void Server::associate_cid(const ngtcp2_cid *cid, Handler *h) { + handlers_.emplace(util::make_cid_key(cid), h); +} + +void Server::dissociate_cid(const ngtcp2_cid *cid) { + handlers_.erase(util::make_cid_key(cid)); +} + +void Server::remove(const Handler *h) { + auto conn = h->conn(); + + handlers_.erase( + util::make_cid_key(ngtcp2_conn_get_client_initial_dcid(conn))); + + std::vector<ngtcp2_cid> cids(ngtcp2_conn_get_num_scid(conn)); + ngtcp2_conn_get_scid(conn, cids.data()); + + for (auto &cid : cids) { + handlers_.erase(util::make_cid_key(&cid)); + } + + delete h; +} + +namespace { +int parse_host_port(Address &dest, int af, const char *first, + const char *last) { + if (std::distance(first, last) == 0) { + return -1; + } + + const char *host_begin, *host_end, *it; + if (*first == '[') { + host_begin = first + 1; + it = std::find(host_begin, last, ']'); + if (it == last) { + return -1; + } + host_end = it; + ++it; + if (it == last || *it != ':') { + return -1; + } + } else { + host_begin = first; + it = std::find(host_begin, last, ':'); + if (it == last) { + return -1; + } + host_end = it; + } + + if (++it == last) { + return -1; + } + auto svc_begin = it; + + std::array<char, NI_MAXHOST> host; + *std::copy(host_begin, host_end, std::begin(host)) = '\0'; + + addrinfo hints{}, *res; + hints.ai_family = af; + hints.ai_socktype = SOCK_DGRAM; + + if (auto rv = getaddrinfo(host.data(), svc_begin, &hints, &res); rv != 0) { + std::cerr << "getaddrinfo: [" << host.data() << "]:" << svc_begin << ": " + << gai_strerror(rv) << std::endl; + return -1; + } + + dest.len = res->ai_addrlen; + memcpy(&dest.su, res->ai_addr, res->ai_addrlen); + + freeaddrinfo(res); + + return 0; +} +} // namespace + +namespace { +void print_usage() { + std::cerr << "Usage: server [OPTIONS] <ADDR> <PORT> <PRIVATE_KEY_FILE> " + "<CERTIFICATE_FILE>" + << std::endl; +} +} // namespace + +namespace { +void config_set_default(Config &config) { + config = Config{}; + config.tx_loss_prob = 0.; + config.rx_loss_prob = 0.; + config.ciphers = util::crypto_default_ciphers(); + config.groups = util::crypto_default_groups(); + config.timeout = 30 * NGTCP2_SECONDS; + { + auto path = realpath(".", nullptr); + assert(path); + config.htdocs = path; + free(path); + } + config.mime_types_file = "/etc/mime.types"sv; + config.max_data = 1_m; + config.max_stream_data_bidi_local = 256_k; + config.max_stream_data_bidi_remote = 256_k; + config.max_stream_data_uni = 256_k; + config.max_window = 6_m; + config.max_stream_window = 6_m; + config.max_streams_bidi = 100; + config.max_streams_uni = 3; + config.max_dyn_length = 20_m; + config.cc_algo = NGTCP2_CC_ALGO_CUBIC; + config.initial_rtt = NGTCP2_DEFAULT_INITIAL_RTT; + config.max_gso_dgrams = 64; + config.handshake_timeout = NGTCP2_DEFAULT_HANDSHAKE_TIMEOUT; + config.ack_thresh = 2; +} +} // namespace + +namespace { +void print_help() { + print_usage(); + + config_set_default(config); + + std::cout << R"( + <ADDR> Address to listen to. '*' binds to any address. + <PORT> Port + <PRIVATE_KEY_FILE> + Path to private key file + <CERTIFICATE_FILE> + Path to certificate file +Options: + -t, --tx-loss=<P> + The probability of losing outgoing packets. <P> must be + [0.0, 1.0], inclusive. 0.0 means no packet loss. 1.0 + means 100% packet loss. + -r, --rx-loss=<P> + The probability of losing incoming packets. <P> must be + [0.0, 1.0], inclusive. 0.0 means no packet loss. 1.0 + means 100% packet loss. + --ciphers=<CIPHERS> + Specify the cipher suite list to enable. + Default: )" + << config.ciphers << R"( + --groups=<GROUPS> + Specify the supported groups. + Default: )" + << config.groups << R"( + -d, --htdocs=<PATH> + Specify document root. If this option is not specified, + the document root is the current working directory. + -q, --quiet Suppress debug output. + -s, --show-secret + Print out secrets unless --quiet is used. + --timeout=<DURATION> + Specify idle timeout. + Default: )" + << util::format_duration(config.timeout) << R"( + -V, --validate-addr + Perform address validation. + --preferred-ipv4-addr=<ADDR>:<PORT> + Specify preferred IPv4 address and port. + --preferred-ipv6-addr=<ADDR>:<PORT> + Specify preferred IPv6 address and port. A numeric IPv6 + address must be enclosed by '[' and ']' (e.g., + [::1]:8443) + --mime-types-file=<PATH> + Path to file that contains MIME media types and the + extensions. + Default: )" + << config.mime_types_file << R"( + --early-response + Start sending response when it receives HTTP header + fields without waiting for request body. If HTTP + response data is written before receiving request body, + STOP_SENDING is sent. + --verify-client + Request a client certificate. At the moment, we just + request a certificate and no verification is done. + --qlog-dir=<PATH> + Path to the directory where qlog file is stored. The + file name of each qlog is the Source Connection ID of + server. + --no-quic-dump + Disables printing QUIC STREAM and CRYPTO frame data out. + --no-http-dump + Disables printing HTTP response body out. + --max-data=<SIZE> + The initial connection-level flow control window. + Default: )" + << util::format_uint_iec(config.max_data) << R"( + --max-stream-data-bidi-local=<SIZE> + The initial stream-level flow control window for a + bidirectional stream that the local endpoint initiates. + Default: )" + << util::format_uint_iec(config.max_stream_data_bidi_local) << R"( + --max-stream-data-bidi-remote=<SIZE> + The initial stream-level flow control window for a + bidirectional stream that the remote endpoint initiates. + Default: )" + << util::format_uint_iec(config.max_stream_data_bidi_remote) << R"( + --max-stream-data-uni=<SIZE> + The initial stream-level flow control window for a + unidirectional stream. + Default: )" + << util::format_uint_iec(config.max_stream_data_uni) << R"( + --max-streams-bidi=<N> + The number of the concurrent bidirectional streams. + Default: )" + << config.max_streams_bidi << R"( + --max-streams-uni=<N> + The number of the concurrent unidirectional streams. + Default: )" + << config.max_streams_uni << R"( + --max-dyn-length=<SIZE> + The maximum length of a dynamically generated content. + Default: )" + << util::format_uint_iec(config.max_dyn_length) << R"( + --cc=(cubic|reno|bbr|bbr2) + The name of congestion controller algorithm. + Default: )" + << util::strccalgo(config.cc_algo) << R"( + --initial-rtt=<DURATION> + Set an initial RTT. + Default: )" + << util::format_duration(config.initial_rtt) << R"( + --max-udp-payload-size=<SIZE> + Override maximum UDP payload size that server transmits. + --send-trailers + Send trailer fields. + --max-window=<SIZE> + Maximum connection-level flow control window size. The + window auto-tuning is enabled if nonzero value is given, + and window size is scaled up to this value. + Default: )" + << util::format_uint_iec(config.max_window) << R"( + --max-stream-window=<SIZE> + Maximum stream-level flow control window size. The + window auto-tuning is enabled if nonzero value is given, + and window size is scaled up to this value. + Default: )" + << util::format_uint_iec(config.max_stream_window) << R"( + --max-gso-dgrams=<N> + Maximum number of UDP datagrams that are sent in a + single GSO sendmsg call. + Default: )" + << config.max_gso_dgrams << R"( + --handshake-timeout=<DURATION> + Set the QUIC handshake timeout. + Default: )" + << util::format_duration(config.handshake_timeout) << R"( + --preferred-versions=<HEX>[[,<HEX>]...] + Specify QUIC versions in hex string in the order of + preference. Server negotiates one of those versions if + client initially selects a less preferred version. + These versions must be supported by libngtcp2. Instead + of specifying hex string, there are special aliases + available: "v1" indicates QUIC v1, and "v2draft" + indicates QUIC v2 draft. + --other-versions=<HEX>[[,<HEX>]...] + Specify QUIC versions in hex string that are sent in + other_versions field of version_information transport + parameter. This list can include a version which is not + supported by libngtcp2. Instead of specifying hex + string, there are special aliases available: "v1" + indicates QUIC v1, and "v2draft" indicates QUIC v2 + draft. + --no-pmtud Disables Path MTU Discovery. + --ack-thresh=<N> + The minimum number of the received ACK eliciting packets + that triggers immediate acknowledgement. + Default: )" + << config.ack_thresh << R"( + -h, --help Display this help and exit. + +--- + + The <SIZE> argument is an integer and an optional unit (e.g., 10K is + 10 * 1024). Units are K, M and G (powers of 1024). + + The <DURATION> argument is an integer and an optional unit (e.g., 1s + is 1 second and 500ms is 500 milliseconds). Units are h, m, s, ms, + us, or ns (hours, minutes, seconds, milliseconds, microseconds, and + nanoseconds respectively). If a unit is omitted, a second is used + as unit. + + The <HEX> argument is an hex string which must start with "0x" + (e.g., 0x00000001).)" + << std::endl; +} +} // namespace + +std::ofstream keylog_file; + +int main(int argc, char **argv) { + config_set_default(config); + + for (;;) { + static int flag = 0; + constexpr static option long_opts[] = { + {"help", no_argument, nullptr, 'h'}, + {"tx-loss", required_argument, nullptr, 't'}, + {"rx-loss", required_argument, nullptr, 'r'}, + {"htdocs", required_argument, nullptr, 'd'}, + {"quiet", no_argument, nullptr, 'q'}, + {"show-secret", no_argument, nullptr, 's'}, + {"validate-addr", no_argument, nullptr, 'V'}, + {"ciphers", required_argument, &flag, 1}, + {"groups", required_argument, &flag, 2}, + {"timeout", required_argument, &flag, 3}, + {"preferred-ipv4-addr", required_argument, &flag, 4}, + {"preferred-ipv6-addr", required_argument, &flag, 5}, + {"mime-types-file", required_argument, &flag, 6}, + {"early-response", no_argument, &flag, 7}, + {"verify-client", no_argument, &flag, 8}, + {"qlog-dir", required_argument, &flag, 9}, + {"no-quic-dump", no_argument, &flag, 10}, + {"no-http-dump", no_argument, &flag, 11}, + {"max-data", required_argument, &flag, 12}, + {"max-stream-data-bidi-local", required_argument, &flag, 13}, + {"max-stream-data-bidi-remote", required_argument, &flag, 14}, + {"max-stream-data-uni", required_argument, &flag, 15}, + {"max-streams-bidi", required_argument, &flag, 16}, + {"max-streams-uni", required_argument, &flag, 17}, + {"max-dyn-length", required_argument, &flag, 18}, + {"cc", required_argument, &flag, 19}, + {"initial-rtt", required_argument, &flag, 20}, + {"max-udp-payload-size", required_argument, &flag, 21}, + {"send-trailers", no_argument, &flag, 22}, + {"max-window", required_argument, &flag, 23}, + {"max-stream-window", required_argument, &flag, 24}, + {"max-gso-dgrams", required_argument, &flag, 25}, + {"handshake-timeout", required_argument, &flag, 26}, + {"preferred-versions", required_argument, &flag, 27}, + {"other-versions", required_argument, &flag, 28}, + {"no-pmtud", no_argument, &flag, 29}, + {"ack-thresh", required_argument, &flag, 30}, + {nullptr, 0, nullptr, 0}}; + + auto optidx = 0; + auto c = getopt_long(argc, argv, "d:hqr:st:V", long_opts, &optidx); + if (c == -1) { + break; + } + switch (c) { + case 'd': { + // --htdocs + auto path = realpath(optarg, nullptr); + if (path == nullptr) { + std::cerr << "path: invalid path " << std::quoted(optarg) << std::endl; + exit(EXIT_FAILURE); + } + config.htdocs = path; + free(path); + break; + } + case 'h': + // --help + print_help(); + exit(EXIT_SUCCESS); + case 'q': + // --quiet + config.quiet = true; + break; + case 'r': + // --rx-loss + config.rx_loss_prob = strtod(optarg, nullptr); + break; + case 's': + // --show-secret + config.show_secret = true; + break; + case 't': + // --tx-loss + config.tx_loss_prob = strtod(optarg, nullptr); + break; + case 'V': + // --validate-addr + config.validate_addr = true; + break; + case '?': + print_usage(); + exit(EXIT_FAILURE); + case 0: + switch (flag) { + case 1: + // --ciphers + config.ciphers = optarg; + break; + case 2: + // --groups + config.groups = optarg; + break; + case 3: + // --timeout + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "timeout: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.timeout = *t; + } + break; + case 4: + // --preferred-ipv4-addr + if (parse_host_port(config.preferred_ipv4_addr, AF_INET, optarg, + optarg + strlen(optarg)) != 0) { + std::cerr << "preferred-ipv4-addr: could not use " + << std::quoted(optarg) << std::endl; + exit(EXIT_FAILURE); + } + break; + case 5: + // --preferred-ipv6-addr + if (parse_host_port(config.preferred_ipv6_addr, AF_INET6, optarg, + optarg + strlen(optarg)) != 0) { + std::cerr << "preferred-ipv6-addr: could not use " + << std::quoted(optarg) << std::endl; + exit(EXIT_FAILURE); + } + break; + case 6: + // --mime-types-file + config.mime_types_file = optarg; + break; + case 7: + // --early-response + config.early_response = true; + break; + case 8: + // --verify-client + config.verify_client = true; + break; + case 9: + // --qlog-dir + config.qlog_dir = optarg; + break; + case 10: + // --no-quic-dump + config.no_quic_dump = true; + break; + case 11: + // --no-http-dump + config.no_http_dump = true; + break; + case 12: + // --max-data + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-data: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_data = *n; + } + break; + case 13: + // --max-stream-data-bidi-local + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-data-bidi-local: invalid argument" + << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_data_bidi_local = *n; + } + break; + case 14: + // --max-stream-data-bidi-remote + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-data-bidi-remote: invalid argument" + << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_data_bidi_remote = *n; + } + break; + case 15: + // --max-stream-data-uni + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-data-uni: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_data_uni = *n; + } + break; + case 16: + // --max-streams-bidi + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "max-streams-bidi: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_streams_bidi = *n; + } + break; + case 17: + // --max-streams-uni + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "max-streams-uni: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_streams_uni = *n; + } + break; + case 18: + // --max-dyn-length + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-dyn-length: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_dyn_length = *n; + } + break; + case 19: + // --cc + if (strcmp("cubic", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_CUBIC; + break; + } + if (strcmp("reno", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_RENO; + break; + } + if (strcmp("bbr", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_BBR; + break; + } + if (strcmp("bbr2", optarg) == 0) { + config.cc_algo = NGTCP2_CC_ALGO_BBR2; + break; + } + std::cerr << "cc: specify cubic, reno, bbr, or bbr2" << std::endl; + exit(EXIT_FAILURE); + case 20: + // --initial-rtt + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "initial-rtt: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.initial_rtt = *t; + } + break; + case 21: + // --max-udp-payload-size + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-udp-payload-size: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else if (*n > 64_k) { + std::cerr << "max-udp-payload-size: must not exceed 65536" + << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_udp_payload_size = *n; + } + break; + case 22: + // --send-trailers + config.send_trailers = true; + break; + case 23: + // --max-window + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-window: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_window = *n; + } + break; + case 24: + // --max-stream-window + if (auto n = util::parse_uint_iec(optarg); !n) { + std::cerr << "max-stream-window: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_stream_window = *n; + } + break; + case 25: + // --max-gso-dgrams + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "max-gso-dgrams: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.max_gso_dgrams = *n; + } + break; + case 26: + // --handshake-timeout + if (auto t = util::parse_duration(optarg); !t) { + std::cerr << "handshake-timeout: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else { + config.handshake_timeout = *t; + } + break; + case 27: { + // --preferred-versions + auto l = util::split_str(optarg); + if (l.size() > max_preferred_versionslen) { + std::cerr << "preferred-versions: too many versions > " + << max_preferred_versionslen << std::endl; + } + config.preferred_versions.resize(l.size()); + auto it = std::begin(config.preferred_versions); + for (const auto &k : l) { + if (k == "v1"sv) { + *it++ = NGTCP2_PROTO_VER_V1; + continue; + } + if (k == "v2draft"sv) { + *it++ = NGTCP2_PROTO_VER_V2_DRAFT; + continue; + } + auto rv = util::parse_version(k); + if (!rv) { + std::cerr << "preferred-versions: invalid version " + << std::quoted(k) << std::endl; + exit(EXIT_FAILURE); + } + if (!ngtcp2_is_supported_version(*rv)) { + std::cerr << "preferred-versions: unsupported version " + << std::quoted(k) << std::endl; + exit(EXIT_FAILURE); + } + *it++ = *rv; + } + break; + } + case 28: { + // --other-versions + auto l = util::split_str(optarg); + config.other_versions.resize(l.size()); + auto it = std::begin(config.other_versions); + for (const auto &k : l) { + if (k == "v1"sv) { + *it++ = NGTCP2_PROTO_VER_V1; + continue; + } + if (k == "v2draft"sv) { + *it++ = NGTCP2_PROTO_VER_V2_DRAFT; + continue; + } + auto rv = util::parse_version(k); + if (!rv) { + std::cerr << "other-versions: invalid version " << std::quoted(k) + << std::endl; + exit(EXIT_FAILURE); + } + *it++ = *rv; + } + break; + } + case 29: + // --no-pmtud + config.no_pmtud = true; + break; + case 30: + // --ack-thresh + if (auto n = util::parse_uint(optarg); !n) { + std::cerr << "ack-thresh: invalid argument" << std::endl; + exit(EXIT_FAILURE); + } else if (*n > 100) { + std::cerr << "ack-thresh: must not exceed 100" << std::endl; + exit(EXIT_FAILURE); + } else { + config.ack_thresh = *n; + } + break; + } + break; + default: + break; + }; + } + + if (argc - optind < 4) { + std::cerr << "Too few arguments" << std::endl; + print_usage(); + exit(EXIT_FAILURE); + } + + auto addr = argv[optind++]; + auto port = argv[optind++]; + auto private_key_file = argv[optind++]; + auto cert_file = argv[optind++]; + + if (auto n = util::parse_uint(port); !n) { + std::cerr << "port: invalid port number" << std::endl; + exit(EXIT_FAILURE); + } else if (*n > 65535) { + std::cerr << "port: must not exceed 65535" << std::endl; + exit(EXIT_FAILURE); + } else { + config.port = *n; + } + + if (auto mt = util::read_mime_types(config.mime_types_file); !mt) { + std::cerr << "mime-types-file: Could not read MIME media types file " + << std::quoted(config.mime_types_file) << std::endl; + } else { + config.mime_types = std::move(*mt); + } + + TLSServerContext tls_ctx; + + if (tls_ctx.init(private_key_file, cert_file, AppProtocol::H3) != 0) { + exit(EXIT_FAILURE); + } + + if (config.htdocs.back() != '/') { + config.htdocs += '/'; + } + + std::cerr << "Using document root " << config.htdocs << std::endl; + + auto ev_loop_d = defer(ev_loop_destroy, EV_DEFAULT); + + auto keylog_filename = getenv("SSLKEYLOGFILE"); + if (keylog_filename) { + keylog_file.open(keylog_filename, std::ios_base::app); + if (keylog_file) { + tls_ctx.enable_keylog(); + } + } + + if (util::generate_secret(config.static_secret.data(), + config.static_secret.size()) != 0) { + std::cerr << "Unable to generate static secret" << std::endl; + exit(EXIT_FAILURE); + } + + Server s(EV_DEFAULT, tls_ctx); + if (s.init(addr, port) != 0) { + exit(EXIT_FAILURE); + } + + ev_run(EV_DEFAULT, 0); + + s.disconnect(); + s.close(); + + return EXIT_SUCCESS; +} diff --git a/examples/server.h b/examples/server.h new file mode 100644 index 0000000..51fdfd0 --- /dev/null +++ b/examples/server.h @@ -0,0 +1,256 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SERVER_H +#define SERVER_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <vector> +#include <unordered_map> +#include <string> +#include <deque> +#include <string_view> +#include <memory> + +#include <ngtcp2/ngtcp2.h> +#include <ngtcp2/ngtcp2_crypto.h> +#include <nghttp3/nghttp3.h> + +#include <ev.h> + +#include "server_base.h" +#include "tls_server_context.h" +#include "network.h" +#include "shared.h" + +using namespace ngtcp2; + +struct HTTPHeader { + HTTPHeader(const std::string_view &name, const std::string_view &value) + : name(name), value(value) {} + + std::string_view name; + std::string_view value; +}; + +class Handler; +struct FileEntry; + +struct Stream { + Stream(int64_t stream_id, Handler *handler); + + int start_response(nghttp3_conn *conn); + std::pair<FileEntry, int> open_file(const std::string &path); + void map_file(const FileEntry &fe); + int send_status_response(nghttp3_conn *conn, unsigned int status_code, + const std::vector<HTTPHeader> &extra_headers = {}); + int send_redirect_response(nghttp3_conn *conn, unsigned int status_code, + const std::string_view &path); + int64_t find_dyn_length(const std::string_view &path); + void http_acked_stream_data(uint64_t datalen); + + int64_t stream_id; + Handler *handler; + // uri is request uri/path. + std::string uri; + std::string method; + std::string authority; + std::string status_resp_body; + // data is a pointer to the memory which maps file denoted by fd. + uint8_t *data; + // datalen is the length of mapped file by data. + uint64_t datalen; + // dynresp is true if dynamic data response is enabled. + bool dynresp; + // dyndataleft is the number of dynamic data left to send. + uint64_t dyndataleft; + // dynbuflen is the number of bytes in-flight. + uint64_t dynbuflen; +}; + +class Server; + +// Endpoint is a local endpoint. +struct Endpoint { + Address addr; + ev_io rev; + Server *server; + int fd; + // ecn is the last ECN bits set to fd. + unsigned int ecn; +}; + +class Handler : public HandlerBase { +public: + Handler(struct ev_loop *loop, Server *server); + ~Handler(); + + int init(const Endpoint &ep, const Address &local_addr, const sockaddr *sa, + socklen_t salen, const ngtcp2_cid *dcid, const ngtcp2_cid *scid, + const ngtcp2_cid *ocid, const uint8_t *token, size_t tokenlen, + uint32_t version, TLSServerContext &tls_ctx); + + int on_read(const Endpoint &ep, const Address &local_addr, const sockaddr *sa, + socklen_t salen, const ngtcp2_pkt_info *pi, uint8_t *data, + size_t datalen); + int on_write(); + int write_streams(); + int feed_data(const Endpoint &ep, const Address &local_addr, + const sockaddr *sa, socklen_t salen, const ngtcp2_pkt_info *pi, + uint8_t *data, size_t datalen); + void update_timer(); + int handle_expiry(); + void signal_write(); + int handshake_completed(); + + Server *server() const; + int recv_stream_data(uint32_t flags, int64_t stream_id, const uint8_t *data, + size_t datalen); + int acked_stream_data_offset(int64_t stream_id, uint64_t datalen); + uint32_t version() const; + void on_stream_open(int64_t stream_id); + int on_stream_close(int64_t stream_id, uint64_t app_error_code); + void start_draining_period(); + int start_closing_period(); + int handle_error(); + int send_conn_close(); + + int update_key(uint8_t *rx_secret, uint8_t *tx_secret, + ngtcp2_crypto_aead_ctx *rx_aead_ctx, uint8_t *rx_iv, + ngtcp2_crypto_aead_ctx *tx_aead_ctx, uint8_t *tx_iv, + const uint8_t *current_rx_secret, + const uint8_t *current_tx_secret, size_t secretlen); + + int setup_httpconn(); + void http_consume(int64_t stream_id, size_t nconsumed); + void extend_max_remote_streams_bidi(uint64_t max_streams); + Stream *find_stream(int64_t stream_id); + void http_begin_request_headers(int64_t stream_id); + void http_recv_request_header(Stream *stream, int32_t token, + nghttp3_rcbuf *name, nghttp3_rcbuf *value); + int http_end_request_headers(Stream *stream); + int http_end_stream(Stream *stream); + int start_response(Stream *stream); + int on_stream_reset(int64_t stream_id); + int on_stream_stop_sending(int64_t stream_id); + int extend_max_stream_data(int64_t stream_id, uint64_t max_data); + void shutdown_read(int64_t stream_id, int app_error_code); + void http_acked_stream_data(Stream *stream, uint64_t datalen); + void http_stream_close(int64_t stream_id, uint64_t app_error_code); + int http_stop_sending(int64_t stream_id, uint64_t app_error_code); + int http_reset_stream(int64_t stream_id, uint64_t app_error_code); + + void write_qlog(const void *data, size_t datalen); + + void on_send_blocked(Endpoint &ep, const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, unsigned int ecn, + const uint8_t *data, size_t datalen, size_t gso_size); + void start_wev_endpoint(const Endpoint &ep); + int send_blocked_packet(); + +private: + struct ev_loop *loop_; + Server *server_; + ev_io wev_; + ev_timer timer_; + FILE *qlog_; + ngtcp2_cid scid_; + nghttp3_conn *httpconn_; + std::unordered_map<int64_t, std::unique_ptr<Stream>> streams_; + // conn_closebuf_ contains a packet which contains CONNECTION_CLOSE. + // This packet is repeatedly sent as a response to the incoming + // packet in draining period. + std::unique_ptr<Buffer> conn_closebuf_; + // nkey_update_ is the number of key update occurred. + size_t nkey_update_; + bool no_gso_; + + struct { + bool send_blocked; + size_t num_blocked; + size_t num_blocked_sent; + // blocked field is effective only when send_blocked is true. + struct { + Endpoint *endpoint; + Address local_addr; + Address remote_addr; + unsigned int ecn; + const uint8_t *data; + size_t datalen; + size_t gso_size; + } blocked[2]; + std::unique_ptr<uint8_t[]> data; + } tx_; +}; + +class Server { +public: + Server(struct ev_loop *loop, TLSServerContext &tls_ctx); + ~Server(); + + int init(const char *addr, const char *port); + void disconnect(); + void close(); + + int on_read(Endpoint &ep); + int send_version_negotiation(uint32_t version, const uint8_t *dcid, + size_t dcidlen, const uint8_t *scid, + size_t scidlen, Endpoint &ep, + const Address &local_addr, const sockaddr *sa, + socklen_t salen); + int send_retry(const ngtcp2_pkt_hd *chd, Endpoint &ep, + const Address &local_addr, const sockaddr *sa, socklen_t salen, + size_t max_pktlen); + int send_stateless_connection_close(const ngtcp2_pkt_hd *chd, Endpoint &ep, + const Address &local_addr, + const sockaddr *sa, socklen_t salen); + int verify_retry_token(ngtcp2_cid *ocid, const ngtcp2_pkt_hd *hd, + const sockaddr *sa, socklen_t salen); + int verify_token(const ngtcp2_pkt_hd *hd, const sockaddr *sa, + socklen_t salen); + int send_packet(Endpoint &ep, const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, unsigned int ecn, + const uint8_t *data, size_t datalen); + std::pair<size_t, int> send_packet(Endpoint &ep, bool &no_gso, + const ngtcp2_addr &local_addr, + const ngtcp2_addr &remote_addr, + unsigned int ecn, const uint8_t *data, + size_t datalen, size_t gso_size); + void remove(const Handler *h); + + void associate_cid(const ngtcp2_cid *cid, Handler *h); + void dissociate_cid(const ngtcp2_cid *cid); + +private: + std::unordered_map<std::string, Handler *> handlers_; + struct ev_loop *loop_; + std::vector<Endpoint> endpoints_; + TLSServerContext &tls_ctx_; + ev_signal sigintev_; +}; + +#endif // SERVER_H diff --git a/examples/server_base.cc b/examples/server_base.cc new file mode 100644 index 0000000..aea86bd --- /dev/null +++ b/examples/server_base.cc @@ -0,0 +1,58 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "server_base.h" + +#include <cassert> +#include <array> +#include <iostream> + +#include "debug.h" + +using namespace ngtcp2; + +extern Config config; + +Buffer::Buffer(const uint8_t *data, size_t datalen) + : buf{data, data + datalen}, begin(buf.data()), tail(begin + datalen) {} +Buffer::Buffer(size_t datalen) : buf(datalen), begin(buf.data()), tail(begin) {} + +static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { + auto h = static_cast<HandlerBase *>(conn_ref->user_data); + return h->conn(); +} + +HandlerBase::HandlerBase() : conn_ref_{get_conn, this}, conn_(nullptr) { + ngtcp2_connection_close_error_default(&last_error_); +} + +HandlerBase::~HandlerBase() { + if (conn_) { + ngtcp2_conn_del(conn_); + } +} + +ngtcp2_conn *HandlerBase::conn() const { return conn_; } + +ngtcp2_crypto_conn_ref *HandlerBase::conn_ref() { return &conn_ref_; } diff --git a/examples/server_base.h b/examples/server_base.h new file mode 100644 index 0000000..adbbd20 --- /dev/null +++ b/examples/server_base.h @@ -0,0 +1,194 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SERVER_BASE_H +#define SERVER_BASE_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <vector> +#include <deque> +#include <unordered_map> +#include <string> +#include <string_view> +#include <functional> + +#include <ngtcp2/ngtcp2_crypto.h> + +#include "tls_server_session.h" +#include "network.h" +#include "shared.h" + +using namespace ngtcp2; + +struct Config { + Address preferred_ipv4_addr; + Address preferred_ipv6_addr; + // tx_loss_prob is probability of losing outgoing packet. + double tx_loss_prob; + // rx_loss_prob is probability of losing incoming packet. + double rx_loss_prob; + // ciphers is the list of enabled ciphers. + const char *ciphers; + // groups is the list of supported groups. + const char *groups; + // htdocs is a root directory to serve documents. + std::string htdocs; + // mime_types_file is a path to "MIME media types and the + // extensions" file. Ubuntu mime-support package includes it in + // /etc/mime/types. + std::string_view mime_types_file; + // mime_types maps file extension to MIME media type. + std::unordered_map<std::string, std::string> mime_types; + // port is the port number which server listens on for incoming + // connections. + uint16_t port; + // quiet suppresses the output normally shown except for the error + // messages. + bool quiet; + // timeout is an idle timeout for QUIC connection. + ngtcp2_duration timeout; + // show_secret is true if transport secrets should be printed out. + bool show_secret; + // validate_addr is true if server requires address validation. + bool validate_addr; + // early_response is true if server starts sending response when it + // receives HTTP header fields without waiting for request body. If + // HTTP response data is written before receiving request body, + // STOP_SENDING is sent. + bool early_response; + // verify_client is true if server verifies client with X.509 + // certificate based authentication. + bool verify_client; + // qlog_dir is the path to directory where qlog is stored. + std::string_view qlog_dir; + // no_quic_dump is true if hexdump of QUIC STREAM and CRYPTO data + // should be disabled. + bool no_quic_dump; + // no_http_dump is true if hexdump of HTTP response body should be + // disabled. + bool no_http_dump; + // max_data is the initial connection-level flow control window. + uint64_t max_data; + // max_stream_data_bidi_local is the initial stream-level flow + // control window for a bidirectional stream that the local endpoint + // initiates. + uint64_t max_stream_data_bidi_local; + // max_stream_data_bidi_remote is the initial stream-level flow + // control window for a bidirectional stream that the remote + // endpoint initiates. + uint64_t max_stream_data_bidi_remote; + // max_stream_data_uni is the initial stream-level flow control + // window for a unidirectional stream. + uint64_t max_stream_data_uni; + // max_streams_bidi is the number of the concurrent bidirectional + // streams. + uint64_t max_streams_bidi; + // max_streams_uni is the number of the concurrent unidirectional + // streams. + uint64_t max_streams_uni; + // max_window is the maximum connection-level flow control window + // size if auto-tuning is enabled. + uint64_t max_window; + // max_stream_window is the maximum stream-level flow control window + // size if auto-tuning is enabled. + uint64_t max_stream_window; + // max_dyn_length is the maximum length of dynamically generated + // response. + uint64_t max_dyn_length; + // static_secret is used to derive keying materials for Retry and + // Stateless Retry token. + std::array<uint8_t, 32> static_secret; + // cc_algo is the congestion controller algorithm. + ngtcp2_cc_algo cc_algo; + // initial_rtt is an initial RTT. + ngtcp2_duration initial_rtt; + // max_udp_payload_size is the maximum UDP payload size that server + // transmits. + size_t max_udp_payload_size; + // send_trailers controls whether server sends trailer fields or + // not. + bool send_trailers; + // max_gso_dgrams is the maximum number of UDP datagrams in one GSO + // sendmsg call. + size_t max_gso_dgrams; + // handshake_timeout is the period of time before giving up QUIC + // connection establishment. + ngtcp2_duration handshake_timeout; + // preferred_versions includes QUIC versions in the order of + // preference. Server negotiates one of those versions if a client + // initially selects a less preferred version. + std::vector<uint32_t> preferred_versions; + // other_versions includes QUIC versions that are sent in + // other_versions field of version_information transport_parameter. + std::vector<uint32_t> other_versions; + // no_pmtud disables Path MTU Discovery. + bool no_pmtud; + // ack_thresh is the minimum number of the received ACK eliciting + // packets that triggers immediate acknowledgement. + size_t ack_thresh; +}; + +struct Buffer { + Buffer(const uint8_t *data, size_t datalen); + explicit Buffer(size_t datalen); + + size_t size() const { return tail - begin; } + size_t left() const { return buf.data() + buf.size() - tail; } + uint8_t *const wpos() { return tail; } + const uint8_t *rpos() const { return begin; } + void push(size_t len) { tail += len; } + void reset() { tail = begin; } + + std::vector<uint8_t> buf; + // begin points to the beginning of the buffer. This might point to + // buf.data() if a buffer space is allocated by this object. It is + // also allowed to point to the external shared buffer. + uint8_t *begin; + // tail points to the position of the buffer where write should + // occur. + uint8_t *tail; +}; + +class HandlerBase { +public: + HandlerBase(); + ~HandlerBase(); + + ngtcp2_conn *conn() const; + + TLSServerSession *get_session() { return &tls_session_; } + + ngtcp2_crypto_conn_ref *conn_ref(); + +protected: + ngtcp2_crypto_conn_ref conn_ref_; + TLSServerSession tls_session_; + ngtcp2_conn *conn_; + ngtcp2_connection_close_error last_error_; +}; + +#endif // SERVER_BASE_H diff --git a/examples/shared.cc b/examples/shared.cc new file mode 100644 index 0000000..d65819a --- /dev/null +++ b/examples/shared.cc @@ -0,0 +1,385 @@ +/* + * ngtcp2 + * + * Copyright (c) 2019 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shared.h" + +#include <nghttp3/nghttp3.h> + +#include <cstring> +#include <cassert> +#include <iostream> + +#include <unistd.h> +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif // HAVE_NETINET_IN_H +#ifdef HAVE_ASM_TYPES_H +# include <asm/types.h> +#endif // HAVE_ASM_TYPES_H +#ifdef HAVE_LINUX_NETLINK_H +# include <linux/netlink.h> +#endif // HAVE_LINUX_NETLINK_H +#ifdef HAVE_LINUX_RTNETLINK_H +# include <linux/rtnetlink.h> +#endif // HAVE_LINUX_RTNETLINK_H + +#include "template.h" + +namespace ngtcp2 { + +unsigned int msghdr_get_ecn(msghdr *msg, int family) { + switch (family) { + case AF_INET: + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_TOS && + cmsg->cmsg_len) { + return *reinterpret_cast<uint8_t *>(CMSG_DATA(cmsg)); + } + } + break; + case AF_INET6: + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IPV6 && cmsg->cmsg_type == IPV6_TCLASS && + cmsg->cmsg_len) { + return *reinterpret_cast<uint8_t *>(CMSG_DATA(cmsg)); + } + } + break; + } + + return 0; +} + +void fd_set_ecn(int fd, int family, unsigned int ecn) { + switch (family) { + case AF_INET: + if (setsockopt(fd, IPPROTO_IP, IP_TOS, &ecn, + static_cast<socklen_t>(sizeof(ecn))) == -1) { + std::cerr << "setsockopt: " << strerror(errno) << std::endl; + } + break; + case AF_INET6: + if (setsockopt(fd, IPPROTO_IPV6, IPV6_TCLASS, &ecn, + static_cast<socklen_t>(sizeof(ecn))) == -1) { + std::cerr << "setsockopt: " << strerror(errno) << std::endl; + } + break; + } +} + +void fd_set_recv_ecn(int fd, int family) { + unsigned int tos = 1; + switch (family) { + case AF_INET: + if (setsockopt(fd, IPPROTO_IP, IP_RECVTOS, &tos, + static_cast<socklen_t>(sizeof(tos))) == -1) { + std::cerr << "setsockopt: " << strerror(errno) << std::endl; + } + break; + case AF_INET6: + if (setsockopt(fd, IPPROTO_IPV6, IPV6_RECVTCLASS, &tos, + static_cast<socklen_t>(sizeof(tos))) == -1) { + std::cerr << "setsockopt: " << strerror(errno) << std::endl; + } + break; + } +} + +void fd_set_ip_mtu_discover(int fd, int family) { +#if defined(IP_MTU_DISCOVER) && defined(IPV6_MTU_DISCOVER) + int val; + + switch (family) { + case AF_INET: + val = IP_PMTUDISC_DO; + if (setsockopt(fd, IPPROTO_IP, IP_MTU_DISCOVER, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + std::cerr << "setsockopt: IP_MTU_DISCOVER: " << strerror(errno) + << std::endl; + } + break; + case AF_INET6: + val = IPV6_PMTUDISC_DO; + if (setsockopt(fd, IPPROTO_IPV6, IPV6_MTU_DISCOVER, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + std::cerr << "setsockopt: IPV6_MTU_DISCOVER: " << strerror(errno) + << std::endl; + } + break; + } +#endif // defined(IP_MTU_DISCOVER) && defined(IPV6_MTU_DISCOVER) +} + +void fd_set_ip_dontfrag(int fd, int family) { +#if defined(IP_DONTFRAG) && defined(IPV6_DONTFRAG) + int val = 1; + + switch (family) { + case AF_INET: + if (setsockopt(fd, IPPROTO_IP, IP_DONTFRAG, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + std::cerr << "setsockopt: IP_DONTFRAG: " << strerror(errno) << std::endl; + } + break; + case AF_INET6: + if (setsockopt(fd, IPPROTO_IPV6, IPV6_DONTFRAG, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + std::cerr << "setsockopt: IPV6_DONTFRAG: " << strerror(errno) + << std::endl; + } + break; + } +#endif // defined(IP_DONTFRAG) && defined(IPV6_DONTFRAG) +} + +std::optional<Address> msghdr_get_local_addr(msghdr *msg, int family) { + switch (family) { + case AF_INET: + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) { + auto pktinfo = reinterpret_cast<in_pktinfo *>(CMSG_DATA(cmsg)); + Address res{}; + res.ifindex = pktinfo->ipi_ifindex; + res.len = sizeof(res.su.in); + auto &sa = res.su.in; + sa.sin_family = AF_INET; + sa.sin_addr = pktinfo->ipi_addr; + return res; + } + } + return {}; + case AF_INET6: + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IPV6 && cmsg->cmsg_type == IPV6_PKTINFO) { + auto pktinfo = reinterpret_cast<in6_pktinfo *>(CMSG_DATA(cmsg)); + Address res{}; + res.ifindex = pktinfo->ipi6_ifindex; + res.len = sizeof(res.su.in6); + auto &sa = res.su.in6; + sa.sin6_family = AF_INET6; + sa.sin6_addr = pktinfo->ipi6_addr; + return res; + } + } + return {}; + } + return {}; +} + +void set_port(Address &dst, Address &src) { + switch (dst.su.storage.ss_family) { + case AF_INET: + assert(AF_INET == src.su.storage.ss_family); + dst.su.in.sin_port = src.su.in.sin_port; + return; + case AF_INET6: + assert(AF_INET6 == src.su.storage.ss_family); + dst.su.in6.sin6_port = src.su.in6.sin6_port; + return; + default: + assert(0); + } +} + +#ifdef HAVE_LINUX_RTNETLINK_H + +struct nlmsg { + nlmsghdr hdr; + rtmsg msg; + rtattr dst; + in_addr_union dst_addr; +}; + +namespace { +int send_netlink_msg(int fd, const Address &remote_addr) { + nlmsg nlmsg{}; + nlmsg.hdr.nlmsg_type = RTM_GETROUTE; + nlmsg.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + + nlmsg.msg.rtm_family = remote_addr.su.sa.sa_family; + + nlmsg.dst.rta_type = RTA_DST; + + switch (remote_addr.su.sa.sa_family) { + case AF_INET: + nlmsg.dst.rta_len = RTA_LENGTH(sizeof(remote_addr.su.in.sin_addr)); + memcpy(RTA_DATA(&nlmsg.dst), &remote_addr.su.in.sin_addr, + sizeof(remote_addr.su.in.sin_addr)); + break; + case AF_INET6: + nlmsg.dst.rta_len = RTA_LENGTH(sizeof(remote_addr.su.in6.sin6_addr)); + memcpy(RTA_DATA(&nlmsg.dst), &remote_addr.su.in6.sin6_addr, + sizeof(remote_addr.su.in6.sin6_addr)); + break; + default: + assert(0); + } + + nlmsg.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(nlmsg.msg) + nlmsg.dst.rta_len); + + sockaddr_nl sa{}; + sa.nl_family = AF_NETLINK; + + iovec iov{&nlmsg, nlmsg.hdr.nlmsg_len}; + msghdr msg{}; + msg.msg_name = &sa; + msg.msg_namelen = sizeof(sa); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + ssize_t nwrite; + + do { + nwrite = sendmsg(fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + std::cerr << "sendmsg: Could not write netlink message: " << strerror(errno) + << std::endl; + return -1; + } + + return 0; +} +} // namespace + +namespace { +int recv_netlink_msg(in_addr_union &iau, int fd) { + std::array<uint8_t, 8192> buf; + iovec iov = {buf.data(), buf.size()}; + sockaddr_nl sa{}; + msghdr msg{}; + + msg.msg_name = &sa; + msg.msg_namelen = sizeof(sa); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + ssize_t nread; + + do { + nread = recvmsg(fd, &msg, 0); + } while (nread == -1 && errno == EINTR); + + if (nread == -1) { + std::cerr << "recvmsg: Could not receive netlink message: " + << strerror(errno) << std::endl; + return -1; + } + + for (auto hdr = reinterpret_cast<nlmsghdr *>(buf.data()); + NLMSG_OK(hdr, nread); hdr = NLMSG_NEXT(hdr, nread)) { + switch (hdr->nlmsg_type) { + case NLMSG_DONE: + std::cerr << "netlink: no info returned from kernel" << std::endl; + return -1; + case NLMSG_NOOP: + continue; + case NLMSG_ERROR: + std::cerr << "netlink: " + << strerror(-static_cast<nlmsgerr *>(NLMSG_DATA(hdr))->error) + << std::endl; + return -1; + } + + auto attrlen = hdr->nlmsg_len - NLMSG_SPACE(sizeof(rtmsg)); + + for (auto rta = reinterpret_cast<rtattr *>( + static_cast<uint8_t *>(NLMSG_DATA(hdr)) + sizeof(rtmsg)); + RTA_OK(rta, attrlen); rta = RTA_NEXT(rta, attrlen)) { + if (rta->rta_type != RTA_PREFSRC) { + continue; + } + + size_t in_addrlen; + + switch (static_cast<rtmsg *>(NLMSG_DATA(hdr))->rtm_family) { + case AF_INET: + in_addrlen = sizeof(in_addr); + break; + case AF_INET6: + in_addrlen = sizeof(in6_addr); + break; + default: + assert(0); + abort(); + } + + if (RTA_LENGTH(in_addrlen) != rta->rta_len) { + return -1; + } + + memcpy(&iau, RTA_DATA(rta), in_addrlen); + + return 0; + } + } + + return -1; +} +} // namespace + +int get_local_addr(in_addr_union &iau, const Address &remote_addr) { + sockaddr_nl sa{}; + sa.nl_family = AF_NETLINK; + + auto fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (fd == -1) { + std::cerr << "socket: Could not create netlink socket: " << strerror(errno) + << std::endl; + return -1; + } + + auto fd_d = defer(close, fd); + + if (bind(fd, reinterpret_cast<sockaddr *>(&sa), sizeof(sa)) == -1) { + std::cerr << "bind: Could not bind netlink socket: " << strerror(errno) + << std::endl; + return -1; + } + + if (send_netlink_msg(fd, remote_addr) != 0) { + return -1; + } + + return recv_netlink_msg(iau, fd); +} + +#endif // HAVE_LINUX_NETLINK_H + +bool addreq(const sockaddr *sa, const in_addr_union &iau) { + switch (sa->sa_family) { + case AF_INET: + return memcmp(&reinterpret_cast<const sockaddr_in *>(sa)->sin_addr, &iau.in, + sizeof(iau.in)) == 0; + case AF_INET6: + return memcmp(&reinterpret_cast<const sockaddr_in6 *>(sa)->sin6_addr, + &iau.in6, sizeof(iau.in6)) == 0; + default: + assert(0); + abort(); + } +} + +} // namespace ngtcp2 diff --git a/examples/shared.h b/examples/shared.h new file mode 100644 index 0000000..e0e4cec --- /dev/null +++ b/examples/shared.h @@ -0,0 +1,96 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHARED_H +#define SHARED_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <optional> + +#include <ngtcp2/ngtcp2.h> + +#include "network.h" + +namespace ngtcp2 { + +enum class AppProtocol { + H3, + HQ, +}; + +constexpr uint8_t HQ_ALPN[] = "\xahq-interop\x5hq-29\x5hq-30\x5hq-31\x5hq-32"; +constexpr uint8_t HQ_ALPN_DRAFT29[] = "\x5hq-29"; +constexpr uint8_t HQ_ALPN_DRAFT30[] = "\x5hq-30"; +constexpr uint8_t HQ_ALPN_DRAFT31[] = "\x5hq-31"; +constexpr uint8_t HQ_ALPN_DRAFT32[] = "\x5hq-32"; +constexpr uint8_t HQ_ALPN_V1[] = "\xahq-interop"; + +constexpr uint8_t H3_ALPN[] = "\x2h3\x5h3-29\x5h3-30\x5h3-31\x5h3-32"; +constexpr uint8_t H3_ALPN_DRAFT29[] = "\x5h3-29"; +constexpr uint8_t H3_ALPN_DRAFT30[] = "\x5h3-30"; +constexpr uint8_t H3_ALPN_DRAFT31[] = "\x5h3-31"; +constexpr uint8_t H3_ALPN_DRAFT32[] = "\x5h3-32"; +constexpr uint8_t H3_ALPN_V1[] = "\x2h3"; + +constexpr uint32_t QUIC_VER_DRAFT29 = 0xff00001du; +constexpr uint32_t QUIC_VER_DRAFT30 = 0xff00001eu; +constexpr uint32_t QUIC_VER_DRAFT31 = 0xff00001fu; +constexpr uint32_t QUIC_VER_DRAFT32 = 0xff000020u; + +// msghdr_get_ecn gets ECN bits from |msg|. |family| is the address +// family from which packet is received. +unsigned int msghdr_get_ecn(msghdr *msg, int family); + +// fd_set_ecn sets ECN bits |ecn| to |fd|. |family| is the address +// family of |fd|. +void fd_set_ecn(int fd, int family, unsigned int ecn); + +// fd_set_recv_ecn sets socket option to |fd| so that it can receive +// ECN bits. +void fd_set_recv_ecn(int fd, int family); + +// fd_set_ip_mtu_discover sets IP(V6)_MTU_DISCOVER socket option to +// |fd|. +void fd_set_ip_mtu_discover(int fd, int family); + +// fd_set_ip_dontfrag sets IP(V6)_DONTFRAG socket option to |fd|. +void fd_set_ip_dontfrag(int fd, int family); + +std::optional<Address> msghdr_get_local_addr(msghdr *msg, int family); + +void set_port(Address &dst, Address &src); + +// get_local_addr stores preferred local address (interface address) +// in |iau| for a given destination address |remote_addr|. +int get_local_addr(in_addr_union &iau, const Address &remote_addr); + +// addreq returns true if |sa| and |iau| contain the same address. +bool addreq(const sockaddr *sa, const in_addr_union &iau); + +} // namespace ngtcp2 + +#endif // SHARED_H diff --git a/examples/simpleclient.c b/examples/simpleclient.c new file mode 100644 index 0000000..59321b5 --- /dev/null +++ b/examples/simpleclient.c @@ -0,0 +1,683 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif /* HAVE_CONFIG_H */ + +#include <time.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> +#include <arpa/inet.h> +#include <string.h> +#include <stdio.h> +#include <errno.h> + +#include <ngtcp2/ngtcp2.h> +#include <ngtcp2/ngtcp2_crypto.h> +#include <ngtcp2/ngtcp2_crypto_openssl.h> + +#include <openssl/ssl.h> +#include <openssl/rand.h> +#include <openssl/err.h> + +#include <ev.h> + +#define REMOTE_HOST "127.0.0.1" +#define REMOTE_PORT "4433" +#define ALPN "\xahq-interop" +#define MESSAGE "GET /\r\n" + +/* + * Example 1: Handshake with www.google.com + * + * #define REMOTE_HOST "www.google.com" + * #define REMOTE_PORT "443" + * #define ALPN "\x2h3" + * + * and undefine MESSAGE macro. + */ + +static uint64_t timestamp(void) { + struct timespec tp; + + if (clock_gettime(CLOCK_MONOTONIC, &tp) != 0) { + fprintf(stderr, "clock_gettime: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + + return (uint64_t)tp.tv_sec * NGTCP2_SECONDS + (uint64_t)tp.tv_nsec; +} + +static int create_sock(struct sockaddr *addr, socklen_t *paddrlen, + const char *host, const char *port) { + struct addrinfo hints = {0}; + struct addrinfo *res, *rp; + int rv; + int fd = -1; + + hints.ai_flags = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + + rv = getaddrinfo(host, port, &hints, &res); + if (rv != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); + return -1; + } + + for (rp = res; rp; rp = rp->ai_next) { + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) { + continue; + } + + break; + } + + if (fd == -1) { + goto end; + } + + *paddrlen = rp->ai_addrlen; + memcpy(addr, rp->ai_addr, rp->ai_addrlen); + +end: + freeaddrinfo(res); + + return fd; +} + +static int connect_sock(struct sockaddr *local_addr, socklen_t *plocal_addrlen, + int fd, const struct sockaddr *remote_addr, + size_t remote_addrlen) { + socklen_t len; + + if (connect(fd, remote_addr, (socklen_t)remote_addrlen) != 0) { + fprintf(stderr, "connect: %s\n", strerror(errno)); + return -1; + } + + len = *plocal_addrlen; + + if (getsockname(fd, local_addr, &len) == -1) { + fprintf(stderr, "getsockname: %s\n", strerror(errno)); + return -1; + } + + *plocal_addrlen = len; + + return 0; +} + +struct client { + ngtcp2_crypto_conn_ref conn_ref; + int fd; + struct sockaddr_storage local_addr; + socklen_t local_addrlen; + SSL_CTX *ssl_ctx; + SSL *ssl; + ngtcp2_conn *conn; + + struct { + int64_t stream_id; + const uint8_t *data; + size_t datalen; + size_t nwrite; + } stream; + + ngtcp2_connection_close_error last_error; + + ev_io rev; + ev_timer timer; +}; + +static int numeric_host_family(const char *hostname, int family) { + uint8_t dst[sizeof(struct in6_addr)]; + return inet_pton(family, hostname, dst) == 1; +} + +static int numeric_host(const char *hostname) { + return numeric_host_family(hostname, AF_INET) || + numeric_host_family(hostname, AF_INET6); +} + +static int client_ssl_init(struct client *c) { + c->ssl_ctx = SSL_CTX_new(TLS_client_method()); + if (!c->ssl_ctx) { + fprintf(stderr, "SSL_CTX_new: %s\n", + ERR_error_string(ERR_get_error(), NULL)); + return -1; + } + + if (ngtcp2_crypto_openssl_configure_client_context(c->ssl_ctx) != 0) { + fprintf(stderr, "ngtcp2_crypto_openssl_configure_client_context failed\n"); + return -1; + } + + c->ssl = SSL_new(c->ssl_ctx); + if (!c->ssl) { + fprintf(stderr, "SSL_new: %s\n", ERR_error_string(ERR_get_error(), NULL)); + return -1; + } + + SSL_set_app_data(c->ssl, &c->conn_ref); + SSL_set_connect_state(c->ssl); + SSL_set_alpn_protos(c->ssl, (const unsigned char *)ALPN, sizeof(ALPN) - 1); + if (!numeric_host(REMOTE_HOST)) { + SSL_set_tlsext_host_name(c->ssl, REMOTE_HOST); + } + + /* For NGTCP2_PROTO_VER_V1 */ + SSL_set_quic_transport_version(c->ssl, TLSEXT_TYPE_quic_transport_parameters); + + return 0; +} + +static void rand_cb(uint8_t *dest, size_t destlen, + const ngtcp2_rand_ctx *rand_ctx) { + size_t i; + (void)rand_ctx; + + for (i = 0; i < destlen; ++i) { + *dest = (uint8_t)random(); + } +} + +static int get_new_connection_id_cb(ngtcp2_conn *conn, ngtcp2_cid *cid, + uint8_t *token, size_t cidlen, + void *user_data) { + (void)conn; + (void)user_data; + + if (RAND_bytes(cid->data, (int)cidlen) != 1) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + cid->datalen = cidlen; + + if (RAND_bytes(token, NGTCP2_STATELESS_RESET_TOKENLEN) != 1) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static int extend_max_local_streams_bidi(ngtcp2_conn *conn, + uint64_t max_streams, + void *user_data) { +#ifdef MESSAGE + struct client *c = user_data; + int rv; + int64_t stream_id; + (void)max_streams; + + if (c->stream.stream_id != -1) { + return 0; + } + + rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL); + if (rv != 0) { + return 0; + } + + c->stream.stream_id = stream_id; + c->stream.data = (const uint8_t *)MESSAGE; + c->stream.datalen = sizeof(MESSAGE) - 1; + + return 0; +#else /* !MESSAGE */ + (void)conn; + (void)max_streams; + (void)user_data; + + return 0; +#endif /* !MESSAGE */ +} + +static void log_printf(void *user_data, const char *fmt, ...) { + va_list ap; + (void)user_data; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + fprintf(stderr, "\n"); +} + +static int client_quic_init(struct client *c, + const struct sockaddr *remote_addr, + socklen_t remote_addrlen, + const struct sockaddr *local_addr, + socklen_t local_addrlen) { + ngtcp2_path path = { + { + (struct sockaddr *)local_addr, + local_addrlen, + }, + { + (struct sockaddr *)remote_addr, + remote_addrlen, + }, + NULL, + }; + ngtcp2_callbacks callbacks = { + ngtcp2_crypto_client_initial_cb, + NULL, /* recv_client_initial */ + ngtcp2_crypto_recv_crypto_data_cb, + NULL, /* handshake_completed */ + NULL, /* recv_version_negotiation */ + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + NULL, /* recv_stream_data */ + NULL, /* acked_stream_data_offset */ + NULL, /* stream_open */ + NULL, /* stream_close */ + NULL, /* recv_stateless_reset */ + ngtcp2_crypto_recv_retry_cb, + extend_max_local_streams_bidi, + NULL, /* extend_max_local_streams_uni */ + rand_cb, + get_new_connection_id_cb, + NULL, /* remove_connection_id */ + ngtcp2_crypto_update_key_cb, + NULL, /* path_validation */ + NULL, /* select_preferred_address */ + NULL, /* stream_reset */ + NULL, /* extend_max_remote_streams_bidi */ + NULL, /* extend_max_remote_streams_uni */ + NULL, /* extend_max_stream_data */ + NULL, /* dcid_status */ + NULL, /* handshake_confirmed */ + NULL, /* recv_new_token */ + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + NULL, /* recv_datagram */ + NULL, /* ack_datagram */ + NULL, /* lost_datagram */ + ngtcp2_crypto_get_path_challenge_data_cb, + NULL, /* stream_stop_sending */ + ngtcp2_crypto_version_negotiation_cb, + NULL, /* recv_rx_key */ + NULL, /* recv_tx_key */ + NULL, /* early_data_rejected */ + }; + ngtcp2_cid dcid, scid; + ngtcp2_settings settings; + ngtcp2_transport_params params; + int rv; + + dcid.datalen = NGTCP2_MIN_INITIAL_DCIDLEN; + if (RAND_bytes(dcid.data, (int)dcid.datalen) != 1) { + fprintf(stderr, "RAND_bytes failed\n"); + return -1; + } + + scid.datalen = 8; + if (RAND_bytes(scid.data, (int)scid.datalen) != 1) { + fprintf(stderr, "RAND_bytes failed\n"); + return -1; + } + + ngtcp2_settings_default(&settings); + + settings.initial_ts = timestamp(); + settings.log_printf = log_printf; + + ngtcp2_transport_params_default(¶ms); + + params.initial_max_streams_uni = 3; + params.initial_max_stream_data_bidi_local = 128 * 1024; + params.initial_max_data = 1024 * 1024; + + rv = + ngtcp2_conn_client_new(&c->conn, &dcid, &scid, &path, NGTCP2_PROTO_VER_V1, + &callbacks, &settings, ¶ms, NULL, c); + if (rv != 0) { + fprintf(stderr, "ngtcp2_conn_client_new: %s\n", ngtcp2_strerror(rv)); + return -1; + } + + ngtcp2_conn_set_tls_native_handle(c->conn, c->ssl); + + return 0; +} + +static int client_read(struct client *c) { + uint8_t buf[65536]; + struct sockaddr_storage addr; + struct iovec iov = {buf, sizeof(buf)}; + struct msghdr msg = {0}; + ssize_t nread; + ngtcp2_path path; + ngtcp2_pkt_info pi = {0}; + int rv; + + msg.msg_name = &addr; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + for (;;) { + msg.msg_namelen = sizeof(addr); + + nread = recvmsg(c->fd, &msg, MSG_DONTWAIT); + + if (nread == -1) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + fprintf(stderr, "recvmsg: %s\n", strerror(errno)); + } + + break; + } + + path.local.addrlen = c->local_addrlen; + path.local.addr = (struct sockaddr *)&c->local_addr; + path.remote.addrlen = msg.msg_namelen; + path.remote.addr = msg.msg_name; + + rv = ngtcp2_conn_read_pkt(c->conn, &path, &pi, buf, (size_t)nread, + timestamp()); + if (rv != 0) { + fprintf(stderr, "ngtcp2_conn_read_pkt: %s\n", ngtcp2_strerror(rv)); + if (!c->last_error.error_code) { + if (rv == NGTCP2_ERR_CRYPTO) { + ngtcp2_connection_close_error_set_transport_error_tls_alert( + &c->last_error, ngtcp2_conn_get_tls_alert(c->conn), NULL, 0); + } else { + ngtcp2_connection_close_error_set_transport_error_liberr( + &c->last_error, rv, NULL, 0); + } + } + return -1; + } + } + + return 0; +} + +static int client_send_packet(struct client *c, const uint8_t *data, + size_t datalen) { + struct iovec iov = {(uint8_t *)data, datalen}; + struct msghdr msg = {0}; + ssize_t nwrite; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + do { + nwrite = sendmsg(c->fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + fprintf(stderr, "sendmsg: %s\n", strerror(errno)); + + return -1; + } + + return 0; +} + +static size_t client_get_message(struct client *c, int64_t *pstream_id, + int *pfin, ngtcp2_vec *datav, + size_t datavcnt) { + if (datavcnt == 0) { + return 0; + } + + if (c->stream.stream_id != -1 && c->stream.nwrite < c->stream.datalen) { + *pstream_id = c->stream.stream_id; + *pfin = 1; + datav->base = (uint8_t *)c->stream.data + c->stream.nwrite; + datav->len = c->stream.datalen - c->stream.nwrite; + return 1; + } + + *pstream_id = -1; + *pfin = 0; + datav->base = NULL; + datav->len = 0; + + return 0; +} + +static int client_write_streams(struct client *c) { + ngtcp2_tstamp ts = timestamp(); + ngtcp2_pkt_info pi; + ngtcp2_ssize nwrite; + uint8_t buf[1280]; + ngtcp2_path_storage ps; + ngtcp2_vec datav; + size_t datavcnt; + int64_t stream_id; + ngtcp2_ssize wdatalen; + uint32_t flags; + int fin; + + ngtcp2_path_storage_zero(&ps); + + for (;;) { + datavcnt = client_get_message(c, &stream_id, &fin, &datav, 1); + + flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + if (fin) { + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + nwrite = ngtcp2_conn_writev_stream(c->conn, &ps.path, &pi, buf, sizeof(buf), + &wdatalen, flags, stream_id, &datav, + datavcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_WRITE_MORE: + c->stream.nwrite += (size_t)wdatalen; + continue; + default: + fprintf(stderr, "ngtcp2_conn_writev_stream: %s\n", + ngtcp2_strerror((int)nwrite)); + ngtcp2_connection_close_error_set_transport_error_liberr( + &c->last_error, (int)nwrite, NULL, 0); + return -1; + } + } + + if (nwrite == 0) { + return 0; + } + + if (wdatalen > 0) { + c->stream.nwrite += (size_t)wdatalen; + } + + if (client_send_packet(c, buf, (size_t)nwrite) != 0) { + break; + } + } + + return 0; +} + +static int client_write(struct client *c) { + ngtcp2_tstamp expiry, now; + ev_tstamp t; + + if (client_write_streams(c) != 0) { + return -1; + } + + expiry = ngtcp2_conn_get_expiry(c->conn); + now = timestamp(); + + t = expiry < now ? 1e-9 : (ev_tstamp)(expiry - now) / NGTCP2_SECONDS; + + c->timer.repeat = t; + ev_timer_again(EV_DEFAULT, &c->timer); + + return 0; +} + +static int client_handle_expiry(struct client *c) { + int rv = ngtcp2_conn_handle_expiry(c->conn, timestamp()); + if (rv != 0) { + fprintf(stderr, "ngtcp2_conn_handle_expiry: %s\n", ngtcp2_strerror(rv)); + return -1; + } + + return 0; +} + +static void client_close(struct client *c) { + ngtcp2_ssize nwrite; + ngtcp2_pkt_info pi; + ngtcp2_path_storage ps; + uint8_t buf[1280]; + + if (ngtcp2_conn_is_in_closing_period(c->conn) || + ngtcp2_conn_is_in_draining_period(c->conn)) { + goto fin; + } + + ngtcp2_path_storage_zero(&ps); + + nwrite = ngtcp2_conn_write_connection_close( + c->conn, &ps.path, &pi, buf, sizeof(buf), &c->last_error, timestamp()); + if (nwrite < 0) { + fprintf(stderr, "ngtcp2_conn_write_connection_close: %s\n", + ngtcp2_strerror((int)nwrite)); + goto fin; + } + + client_send_packet(c, buf, (size_t)nwrite); + +fin: + ev_break(EV_DEFAULT, EVBREAK_ALL); +} + +static void read_cb(struct ev_loop *loop, ev_io *w, int revents) { + struct client *c = w->data; + (void)loop; + (void)revents; + + if (client_read(c) != 0) { + client_close(c); + return; + } + + if (client_write(c) != 0) { + client_close(c); + } +} + +static void timer_cb(struct ev_loop *loop, ev_timer *w, int revents) { + struct client *c = w->data; + (void)loop; + (void)revents; + + if (client_handle_expiry(c) != 0) { + client_close(c); + return; + } + + if (client_write(c) != 0) { + client_close(c); + } +} + +static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { + struct client *c = conn_ref->user_data; + return c->conn; +} + +static int client_init(struct client *c) { + struct sockaddr_storage remote_addr, local_addr; + socklen_t remote_addrlen, local_addrlen = sizeof(local_addr); + + memset(c, 0, sizeof(*c)); + + ngtcp2_connection_close_error_default(&c->last_error); + + c->fd = create_sock((struct sockaddr *)&remote_addr, &remote_addrlen, + REMOTE_HOST, REMOTE_PORT); + if (c->fd == -1) { + return -1; + } + + if (connect_sock((struct sockaddr *)&local_addr, &local_addrlen, c->fd, + (struct sockaddr *)&remote_addr, remote_addrlen) != 0) { + return -1; + } + + memcpy(&c->local_addr, &local_addr, sizeof(c->local_addr)); + c->local_addrlen = local_addrlen; + + if (client_ssl_init(c) != 0) { + return -1; + } + + if (client_quic_init(c, (struct sockaddr *)&remote_addr, remote_addrlen, + (struct sockaddr *)&local_addr, local_addrlen) != 0) { + return -1; + } + + c->stream.stream_id = -1; + + c->conn_ref.get_conn = get_conn; + c->conn_ref.user_data = c; + + ev_io_init(&c->rev, read_cb, c->fd, EV_READ); + c->rev.data = c; + ev_io_start(EV_DEFAULT, &c->rev); + + ev_timer_init(&c->timer, timer_cb, 0., 0.); + c->timer.data = c; + + return 0; +} + +static void client_free(struct client *c) { + ngtcp2_conn_del(c->conn); + SSL_free(c->ssl); + SSL_CTX_free(c->ssl_ctx); +} + +int main(void) { + struct client c; + + srandom((unsigned int)timestamp()); + + if (client_init(&c) != 0) { + exit(EXIT_FAILURE); + } + + if (client_write(&c) != 0) { + exit(EXIT_FAILURE); + } + + ev_run(EV_DEFAULT, 0); + + client_free(&c); + + return 0; +} diff --git a/examples/template.h b/examples/template.h new file mode 100644 index 0000000..e923fae --- /dev/null +++ b/examples/template.h @@ -0,0 +1,71 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * Copyright (c) 2015 ngttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TEMPLATE_H +#define TEMPLATE_H + +#include <functional> +#include <utility> +#include <type_traits> + +// inspired by <http://blog.korfuri.fr/post/go-defer-in-cpp/>, but our +// template can take functions returning other than void. +template <typename F, typename... T> struct Defer { + Defer(F &&f, T &&...t) + : f(std::bind(std::forward<F>(f), std::forward<T>(t)...)) {} + Defer(Defer &&o) noexcept : f(std::move(o.f)) {} + ~Defer() { f(); } + + using ResultType = std::invoke_result_t<F, T...>; + std::function<ResultType()> f; +}; + +template <typename F, typename... T> Defer<F, T...> defer(F &&f, T &&...t) { + return Defer<F, T...>(std::forward<F>(f), std::forward<T>(t)...); +} + +template <typename T, size_t N> constexpr size_t array_size(T (&)[N]) { + return N; +} + +template <typename T, size_t N> constexpr size_t str_size(T (&)[N]) { + return N - 1; +} + +// User-defined literals for K, M, and G (powers of 1024) + +constexpr unsigned long long operator"" _k(unsigned long long k) { + return k * 1024; +} + +constexpr unsigned long long operator"" _m(unsigned long long m) { + return m * 1024 * 1024; +} + +constexpr unsigned long long operator"" _g(unsigned long long g) { + return g * 1024 * 1024 * 1024; +} + +#endif // TEMPLATE_H diff --git a/examples/tests/.gitignore b/examples/tests/.gitignore new file mode 100644 index 0000000..ba92e76 --- /dev/null +++ b/examples/tests/.gitignore @@ -0,0 +1,3 @@ +config.ini +gen +*.bak diff --git a/examples/tests/README.rst b/examples/tests/README.rst new file mode 100644 index 0000000..f583ecf --- /dev/null +++ b/examples/tests/README.rst @@ -0,0 +1,60 @@ +Examples Tests +============== + +This is a ``pytest`` suite intended to verify interoperability between +the different example clients and servers built. + +You run it by executing ``pytest`` on top level project dir or in +the examples/tests directory. + +.. code-block:: text + + examples/test> pytest + ngtcp2-examples: [0.9.0-DEV, crypto_libs=['openssl', 'wolfssl']] + ... + +Requirements +------------ + +You need a Python3 (3.8 is probably sufficient), ``pytest`` and the +Python ``cryptography`` module installed. + +Usage +----- + +If you run ``pytest`` without arguments, it will print the test suite +and a ``.`` for every test case passed. Add ``-v`` and all test cases +will be listed in the full name. Adding several ``v`` will increase the +logging level on failure output. + +The name of test cases include the crypto libs of the server and client +used. For example: + +.. code-block:: text + + test_01_handshake.py::TestHandshake::test_01_01_get[openssl-openssl] PASSED [ 16%] + test_01_handshake.py::TestHandshake::test_01_01_get[openssl-wolfssl] PASSED + +Here, ``test_01_01`` is run first with the OpenSSL server and client and then +with the OpenSSL server and wolfSSL client. By default, the test suite runs +all combinations of servers and clients that have been configured in the project. + +To track down problems, you can restrict the test cases that are run by +matching patterns: + +.. code-block:: text + + # only tests with wolfSSL example server + > pytest -v -k 'wolfssl-' + # only tests with wolfSSL example client + > pytest -v -k 'test and -wolfssl' + # tests with a specific combination + > pytest -v -k 'openssl-wolfssl' + + +Analysing +--------- + +To make analysis of a broken test case easier, you best run only that +test case. Use ``pytest -vv`` (or more) to get more verbose logging. +Inspect server and client log files in ``examples/tests/gen``. diff --git a/examples/tests/__init__.py b/examples/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/examples/tests/__init__.py diff --git a/examples/tests/config.ini.in b/examples/tests/config.ini.in new file mode 100644 index 0000000..2d15dbb --- /dev/null +++ b/examples/tests/config.ini.in @@ -0,0 +1,32 @@ +[ngtcp2] +version = @PACKAGE_VERSION@ +examples = @EXAMPLES_ENABLED@ + +[crypto] +openssl = @have_openssl@ +gnutls = @have_gnutls@ +boringssl = @have_boringssl@ +picotls = @have_picotls@ +wolfssl = @have_wolfssl@ + +[examples] +port = 4433 +openssl = @EXAMPLES_OPENSSL@ +gnutls = @EXAMPLES_GNUTLS@ +boringssl = @EXAMPLES_BORINGSSL@ +picotls = @EXAMPLES_PICOTLS@ +wolfssl = @EXAMPLES_WOLFSSL@ + +[clients] +openssl = client +gnutls = gtlsclient +boringssl = bsslclient +picotls = ptlsclient +wolfssl = wsslclient + +[servers] +openssl = server +gnutls = gtlsserver +boringssl = bsslserver +picotls = ptlsserver +wolfssl = wsslserver diff --git a/examples/tests/conftest.py b/examples/tests/conftest.py new file mode 100644 index 0000000..a566ff0 --- /dev/null +++ b/examples/tests/conftest.py @@ -0,0 +1,28 @@ +import logging +import pytest + +from .ngtcp2test import Env + + +@pytest.mark.usefixtures("env") +def pytest_report_header(config): + env = Env() + return [ + f"ngtcp2-examples: [{env.version}, crypto_libs={env.crypto_libs}]", + f"example clients: {env.clients}", + f"example servers: {env.servers}", + ] + + +@pytest.fixture(scope="package") +def env(pytestconfig) -> Env: + console = logging.StreamHandler() + console.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + logging.getLogger('').addHandler(console) + env = Env(pytestconfig=pytestconfig) + level = logging.DEBUG if env.verbose > 0 else logging.INFO + console.setLevel(level) + logging.getLogger('').setLevel(level=level) + env.setup() + + return env diff --git a/examples/tests/ngtcp2test/__init__.py b/examples/tests/ngtcp2test/__init__.py new file mode 100644 index 0000000..65c61d8 --- /dev/null +++ b/examples/tests/ngtcp2test/__init__.py @@ -0,0 +1,6 @@ +from .env import Env, CryptoLib +from .log import LogFile +from .client import ExampleClient, ClientRun +from .server import ExampleServer, ServerRun +from .certs import Ngtcp2TestCA, Credentials +from .tls import HandShake, HSRecord diff --git a/examples/tests/ngtcp2test/certs.py b/examples/tests/ngtcp2test/certs.py new file mode 100644 index 0000000..3ab6260 --- /dev/null +++ b/examples/tests/ngtcp2test/certs.py @@ -0,0 +1,476 @@ +import os +import re +from datetime import timedelta, datetime +from typing import List, Any, Optional + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption, load_pem_private_key +from cryptography.x509 import ExtendedKeyUsageOID, NameOID + + +EC_SUPPORTED = {} +EC_SUPPORTED.update([(curve.name.upper(), curve) for curve in [ + ec.SECP192R1, + ec.SECP224R1, + ec.SECP256R1, + ec.SECP384R1, +]]) + + +def _private_key(key_type): + if isinstance(key_type, str): + key_type = key_type.upper() + m = re.match(r'^(RSA)?(\d+)$', key_type) + if m: + key_type = int(m.group(2)) + + if isinstance(key_type, int): + return rsa.generate_private_key( + public_exponent=65537, + key_size=key_type, + backend=default_backend() + ) + if not isinstance(key_type, ec.EllipticCurve) and key_type in EC_SUPPORTED: + key_type = EC_SUPPORTED[key_type] + return ec.generate_private_key( + curve=key_type, + backend=default_backend() + ) + + +class CertificateSpec: + + def __init__(self, name: str = None, domains: List[str] = None, + email: str = None, + key_type: str = None, single_file: bool = False, + valid_from: timedelta = timedelta(days=-1), + valid_to: timedelta = timedelta(days=89), + client: bool = False, + sub_specs: List['CertificateSpec'] = None): + self._name = name + self.domains = domains + self.client = client + self.email = email + self.key_type = key_type + self.single_file = single_file + self.valid_from = valid_from + self.valid_to = valid_to + self.sub_specs = sub_specs + + @property + def name(self) -> Optional[str]: + if self._name: + return self._name + elif self.domains: + return self.domains[0] + return None + + @property + def type(self) -> Optional[str]: + if self.domains and len(self.domains): + return "server" + elif self.client: + return "client" + elif self.name: + return "ca" + return None + + +class Credentials: + + def __init__(self, name: str, cert: Any, pkey: Any, issuer: 'Credentials' = None): + self._name = name + self._cert = cert + self._pkey = pkey + self._issuer = issuer + self._cert_file = None + self._pkey_file = None + self._store = None + + @property + def name(self) -> str: + return self._name + + @property + def subject(self) -> x509.Name: + return self._cert.subject + + @property + def key_type(self): + if isinstance(self._pkey, RSAPrivateKey): + return f"rsa{self._pkey.key_size}" + elif isinstance(self._pkey, EllipticCurvePrivateKey): + return f"{self._pkey.curve.name}" + else: + raise Exception(f"unknown key type: {self._pkey}") + + @property + def private_key(self) -> Any: + return self._pkey + + @property + def certificate(self) -> Any: + return self._cert + + @property + def cert_pem(self) -> bytes: + return self._cert.public_bytes(Encoding.PEM) + + @property + def pkey_pem(self) -> bytes: + return self._pkey.private_bytes( + Encoding.PEM, + PrivateFormat.TraditionalOpenSSL if self.key_type.startswith('rsa') else PrivateFormat.PKCS8, + NoEncryption()) + + @property + def issuer(self) -> Optional['Credentials']: + return self._issuer + + def set_store(self, store: 'CertStore'): + self._store = store + + def set_files(self, cert_file: str, pkey_file: str = None): + self._cert_file = cert_file + self._pkey_file = pkey_file + + @property + def cert_file(self) -> str: + return self._cert_file + + @property + def pkey_file(self) -> Optional[str]: + return self._pkey_file + + def get_first(self, name) -> Optional['Credentials']: + creds = self._store.get_credentials_for_name(name) if self._store else [] + return creds[0] if len(creds) else None + + def get_credentials_for_name(self, name) -> List['Credentials']: + return self._store.get_credentials_for_name(name) if self._store else [] + + def issue_certs(self, specs: List[CertificateSpec], + chain: List['Credentials'] = None) -> List['Credentials']: + return [self.issue_cert(spec=spec, chain=chain) for spec in specs] + + def issue_cert(self, spec: CertificateSpec, chain: List['Credentials'] = None) -> 'Credentials': + key_type = spec.key_type if spec.key_type else self.key_type + creds = None + if self._store: + creds = self._store.load_credentials( + name=spec.name, key_type=key_type, single_file=spec.single_file, issuer=self) + if creds is None: + creds = Ngtcp2TestCA.create_credentials(spec=spec, issuer=self, key_type=key_type, + valid_from=spec.valid_from, valid_to=spec.valid_to) + if self._store: + self._store.save(creds, single_file=spec.single_file) + if spec.type == "ca": + self._store.save_chain(creds, "ca", with_root=True) + + if spec.sub_specs: + if self._store: + sub_store = CertStore(fpath=os.path.join(self._store.path, creds.name)) + creds.set_store(sub_store) + subchain = chain.copy() if chain else [] + subchain.append(self) + creds.issue_certs(spec.sub_specs, chain=subchain) + return creds + + +class CertStore: + + def __init__(self, fpath: str): + self._store_dir = fpath + if not os.path.exists(self._store_dir): + os.makedirs(self._store_dir) + self._creds_by_name = {} + + @property + def path(self) -> str: + return self._store_dir + + def save(self, creds: Credentials, name: str = None, + chain: List[Credentials] = None, + single_file: bool = False) -> None: + name = name if name is not None else creds.name + cert_file = self.get_cert_file(name=name, key_type=creds.key_type) + pkey_file = self.get_pkey_file(name=name, key_type=creds.key_type) + if single_file: + pkey_file = None + with open(cert_file, "wb") as fd: + fd.write(creds.cert_pem) + if chain: + for c in chain: + fd.write(c.cert_pem) + if pkey_file is None: + fd.write(creds.pkey_pem) + if pkey_file is not None: + with open(pkey_file, "wb") as fd: + fd.write(creds.pkey_pem) + creds.set_files(cert_file, pkey_file) + self._add_credentials(name, creds) + + def save_chain(self, creds: Credentials, infix: str, with_root=False): + name = creds.name + chain = [creds] + while creds.issuer is not None: + creds = creds.issuer + chain.append(creds) + if not with_root and len(chain) > 1: + chain = chain[:-1] + chain_file = os.path.join(self._store_dir, f'{name}-{infix}.pem') + with open(chain_file, "wb") as fd: + for c in chain: + fd.write(c.cert_pem) + + def _add_credentials(self, name: str, creds: Credentials): + if name not in self._creds_by_name: + self._creds_by_name[name] = [] + self._creds_by_name[name].append(creds) + + def get_credentials_for_name(self, name) -> List[Credentials]: + return self._creds_by_name[name] if name in self._creds_by_name else [] + + def get_cert_file(self, name: str, key_type=None) -> str: + key_infix = ".{0}".format(key_type) if key_type is not None else "" + return os.path.join(self._store_dir, f'{name}{key_infix}.cert.pem') + + def get_pkey_file(self, name: str, key_type=None) -> str: + key_infix = ".{0}".format(key_type) if key_type is not None else "" + return os.path.join(self._store_dir, f'{name}{key_infix}.pkey.pem') + + def load_pem_cert(self, fpath: str) -> x509.Certificate: + with open(fpath) as fd: + return x509.load_pem_x509_certificate("".join(fd.readlines()).encode()) + + def load_pem_pkey(self, fpath: str): + with open(fpath) as fd: + return load_pem_private_key("".join(fd.readlines()).encode(), password=None) + + def load_credentials(self, name: str, key_type=None, single_file: bool = False, issuer: Credentials = None): + cert_file = self.get_cert_file(name=name, key_type=key_type) + pkey_file = cert_file if single_file else self.get_pkey_file(name=name, key_type=key_type) + if os.path.isfile(cert_file) and os.path.isfile(pkey_file): + cert = self.load_pem_cert(cert_file) + pkey = self.load_pem_pkey(pkey_file) + creds = Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer) + creds.set_store(self) + creds.set_files(cert_file, pkey_file) + self._add_credentials(name, creds) + return creds + return None + + +class Ngtcp2TestCA: + + @classmethod + def create_root(cls, name: str, store_dir: str, key_type: str = "rsa2048") -> Credentials: + store = CertStore(fpath=store_dir) + creds = store.load_credentials(name="ca", key_type=key_type, issuer=None) + if creds is None: + creds = Ngtcp2TestCA._make_ca_credentials(name=name, key_type=key_type) + store.save(creds, name="ca") + creds.set_store(store) + return creds + + @staticmethod + def create_credentials(spec: CertificateSpec, issuer: Credentials, key_type: Any, + valid_from: timedelta = timedelta(days=-1), + valid_to: timedelta = timedelta(days=89), + ) -> Credentials: + """Create a certificate signed by this CA for the given domains. + :returns: the certificate and private key PEM file paths + """ + if spec.domains and len(spec.domains): + creds = Ngtcp2TestCA._make_server_credentials(name=spec.name, domains=spec.domains, + issuer=issuer, valid_from=valid_from, + valid_to=valid_to, key_type=key_type) + elif spec.client: + creds = Ngtcp2TestCA._make_client_credentials(name=spec.name, issuer=issuer, + email=spec.email, valid_from=valid_from, + valid_to=valid_to, key_type=key_type) + elif spec.name: + creds = Ngtcp2TestCA._make_ca_credentials(name=spec.name, issuer=issuer, + valid_from=valid_from, valid_to=valid_to, + key_type=key_type) + else: + raise Exception(f"unrecognized certificate specification: {spec}") + return creds + + @staticmethod + def _make_x509_name(org_name: str = None, common_name: str = None, parent: x509.Name = None) -> x509.Name: + name_pieces = [] + if org_name: + oid = NameOID.ORGANIZATIONAL_UNIT_NAME if parent else NameOID.ORGANIZATION_NAME + name_pieces.append(x509.NameAttribute(oid, org_name)) + elif common_name: + name_pieces.append(x509.NameAttribute(NameOID.COMMON_NAME, common_name)) + if parent: + name_pieces.extend([rdn for rdn in parent]) + return x509.Name(name_pieces) + + @staticmethod + def _make_csr( + subject: x509.Name, + pkey: Any, + issuer_subject: Optional[Credentials], + valid_from_delta: timedelta = None, + valid_until_delta: timedelta = None + ): + pubkey = pkey.public_key() + issuer_subject = issuer_subject if issuer_subject is not None else subject + + valid_from = datetime.now() + if valid_until_delta is not None: + valid_from += valid_from_delta + valid_until = datetime.now() + if valid_until_delta is not None: + valid_until += valid_until_delta + + return ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer_subject) + .public_key(pubkey) + .not_valid_before(valid_from) + .not_valid_after(valid_until) + .serial_number(x509.random_serial_number()) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(pubkey), + critical=False, + ) + ) + + @staticmethod + def _add_ca_usages(csr: Any) -> Any: + return csr.add_extension( + x509.BasicConstraints(ca=True, path_length=9), + critical=True, + ).add_extension( + x509.KeyUsage( + digital_signature=True, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=True, + crl_sign=True, + encipher_only=False, + decipher_only=False), + critical=True + ).add_extension( + x509.ExtendedKeyUsage([ + ExtendedKeyUsageOID.CLIENT_AUTH, + ExtendedKeyUsageOID.SERVER_AUTH, + ExtendedKeyUsageOID.CODE_SIGNING, + ]), + critical=True + ) + + @staticmethod + def _add_leaf_usages(csr: Any, domains: List[str], issuer: Credentials) -> Any: + return csr.add_extension( + x509.BasicConstraints(ca=False, path_length=None), + critical=True, + ).add_extension( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( + issuer.certificate.extensions.get_extension_for_class( + x509.SubjectKeyIdentifier).value), + critical=False + ).add_extension( + x509.SubjectAlternativeName([x509.DNSName(domain) for domain in domains]), + critical=True, + ).add_extension( + x509.ExtendedKeyUsage([ + ExtendedKeyUsageOID.SERVER_AUTH, + ]), + critical=True + ) + + @staticmethod + def _add_client_usages(csr: Any, issuer: Credentials, rfc82name: str = None) -> Any: + cert = csr.add_extension( + x509.BasicConstraints(ca=False, path_length=None), + critical=True, + ).add_extension( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( + issuer.certificate.extensions.get_extension_for_class( + x509.SubjectKeyIdentifier).value), + critical=False + ) + if rfc82name: + cert.add_extension( + x509.SubjectAlternativeName([x509.RFC822Name(rfc82name)]), + critical=True, + ) + cert.add_extension( + x509.ExtendedKeyUsage([ + ExtendedKeyUsageOID.CLIENT_AUTH, + ]), + critical=True + ) + return cert + + @staticmethod + def _make_ca_credentials(name, key_type: Any, + issuer: Credentials = None, + valid_from: timedelta = timedelta(days=-1), + valid_to: timedelta = timedelta(days=89), + ) -> Credentials: + pkey = _private_key(key_type=key_type) + if issuer is not None: + issuer_subject = issuer.certificate.subject + issuer_key = issuer.private_key + else: + issuer_subject = None + issuer_key = pkey + subject = Ngtcp2TestCA._make_x509_name(org_name=name, parent=issuer.subject if issuer else None) + csr = Ngtcp2TestCA._make_csr(subject=subject, + issuer_subject=issuer_subject, pkey=pkey, + valid_from_delta=valid_from, valid_until_delta=valid_to) + csr = Ngtcp2TestCA._add_ca_usages(csr) + cert = csr.sign(private_key=issuer_key, + algorithm=hashes.SHA256(), + backend=default_backend()) + return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer) + + @staticmethod + def _make_server_credentials(name: str, domains: List[str], issuer: Credentials, + key_type: Any, + valid_from: timedelta = timedelta(days=-1), + valid_to: timedelta = timedelta(days=89), + ) -> Credentials: + name = name + pkey = _private_key(key_type=key_type) + subject = Ngtcp2TestCA._make_x509_name(common_name=name, parent=issuer.subject) + csr = Ngtcp2TestCA._make_csr(subject=subject, + issuer_subject=issuer.certificate.subject, pkey=pkey, + valid_from_delta=valid_from, valid_until_delta=valid_to) + csr = Ngtcp2TestCA._add_leaf_usages(csr, domains=domains, issuer=issuer) + cert = csr.sign(private_key=issuer.private_key, + algorithm=hashes.SHA256(), + backend=default_backend()) + return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer) + + @staticmethod + def _make_client_credentials(name: str, + issuer: Credentials, email: Optional[str], + key_type: Any, + valid_from: timedelta = timedelta(days=-1), + valid_to: timedelta = timedelta(days=89), + ) -> Credentials: + pkey = _private_key(key_type=key_type) + subject = Ngtcp2TestCA._make_x509_name(common_name=name, parent=issuer.subject) + csr = Ngtcp2TestCA._make_csr(subject=subject, + issuer_subject=issuer.certificate.subject, pkey=pkey, + valid_from_delta=valid_from, valid_until_delta=valid_to) + csr = Ngtcp2TestCA._add_client_usages(csr, issuer=issuer, rfc82name=email) + cert = csr.sign(private_key=issuer.private_key, + algorithm=hashes.SHA256(), + backend=default_backend()) + return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer) diff --git a/examples/tests/ngtcp2test/client.py b/examples/tests/ngtcp2test/client.py new file mode 100644 index 0000000..2676343 --- /dev/null +++ b/examples/tests/ngtcp2test/client.py @@ -0,0 +1,187 @@ +import logging +import os +import re +import subprocess +from typing import List + +import pytest + +from .server import ExampleServer, ServerRun +from .certs import Credentials +from .tls import HandShake, HSRecord +from .env import Env, CryptoLib +from .log import LogFile, HexDumpScanner + + +log = logging.getLogger(__name__) + + +class ClientRun: + + def __init__(self, env: Env, returncode, logfile: LogFile, srun: ServerRun): + self.env = env + self.returncode = returncode + self.logfile = logfile + self.log_lines = logfile.get_recent() + self._data_recs = None + self._hs_recs = None + self._srun = srun + if self.env.verbose > 1: + log.debug(f'read {len(self.log_lines)} lines from {logfile.path}') + + @property + def handshake(self) -> List[HSRecord]: + if self._data_recs is None: + crypto_line = re.compile(r'Ordered CRYPTO data in \S+ crypto level') + scanner = HexDumpScanner(source=self.log_lines, + leading_regex=crypto_line) + self._data_recs = [data for data in scanner] + if self.env.verbose > 1: + log.debug(f'detected {len(self._data_recs)} crypto hexdumps ' + f'in {self.logfile.path}') + if self._hs_recs is None: + self._hs_recs = [hrec for hrec in HandShake(source=self._data_recs, + verbose=self.env.verbose)] + if self.env.verbose > 1: + log.debug(f'detected {len(self._hs_recs)} crypto ' + f'records in {self.logfile.path}') + return self._hs_recs + + @property + def hs_stripe(self) -> str: + return ":".join([hrec.name for hrec in self.handshake]) + + @property + def early_data_rejected(self) -> bool: + for l in self.log_lines: + if re.match(r'^Early data was rejected by server.*', l): + return True + return False + + @property + def server(self) -> ServerRun: + return self._srun + + def norm_exp(self, c_hs, s_hs, allow_hello_retry=True): + if allow_hello_retry and self.hs_stripe.startswith('HelloRetryRequest:'): + c_hs = "HelloRetryRequest:" + c_hs + s_hs = "ClientHello:" + s_hs + return c_hs, s_hs + + def _assert_hs(self, c_hs, s_hs): + if not self.hs_stripe.startswith(c_hs): + # what happened? + if self.hs_stripe == '': + # server send nothing + if self.server.hs_stripe == '': + # client send nothing + pytest.fail(f'client did not send a ClientHello"') + else: + # client send sth, but server did not respond + pytest.fail(f'server did not respond to ClientHello: ' + f'{self.server.handshake[0].to_text()}"') + else: + pytest.fail(f'Expected "{c_hs}", got "{self.hs_stripe}"') + assert self.server.hs_stripe == s_hs, \ + f'Expected "{s_hs}", got "{self.server.hs_stripe}"\n' + + def assert_non_resume_handshake(self, allow_hello_retry=True): + # for client/server where KEY_SHARE do not match, the hello is retried + c_hs, s_hs = self.norm_exp( + "ServerHello:EncryptedExtensions:Certificate:CertificateVerify:Finished", + "ClientHello:Finished", allow_hello_retry=allow_hello_retry) + self._assert_hs(c_hs, s_hs) + + def assert_resume_handshake(self): + # for client/server where KEY_SHARE do not match, the hello is retried + c_hs, s_hs = self.norm_exp("ServerHello:EncryptedExtensions:Finished", + "ClientHello:Finished") + self._assert_hs(c_hs, s_hs) + + def assert_verify_null_handshake(self): + c_hs, s_hs = self.norm_exp( + "ServerHello:EncryptedExtensions:CertificateRequest:Certificate:CertificateVerify:Finished", + "ClientHello:Certificate:Finished") + self._assert_hs(c_hs, s_hs) + + def assert_verify_cert_handshake(self): + c_hs, s_hs = self.norm_exp( + "ServerHello:EncryptedExtensions:CertificateRequest:Certificate:CertificateVerify:Finished", + "ClientHello:Certificate:CertificateVerify:Finished") + self._assert_hs(c_hs, s_hs) + + +class ExampleClient: + + def __init__(self, env: Env, crypto_lib: str): + self.env = env + self._crypto_lib = crypto_lib + self._path = env.client_path(self._crypto_lib) + self._log_path = f'{self.env.gen_dir}/{self._crypto_lib}-client.log' + self._qlog_path = f'{self.env.gen_dir}/{self._crypto_lib}-client.qlog' + self._session_path = f'{self.env.gen_dir}/{self._crypto_lib}-client.session' + self._tp_path = f'{self.env.gen_dir}/{self._crypto_lib}-client.tp' + self._data_path = f'{self.env.gen_dir}/{self._crypto_lib}-client.data' + + @property + def path(self): + return self._path + + @property + def crypto_lib(self): + return self._crypto_lib + + @property + def uses_cipher_config(self): + return CryptoLib.uses_cipher_config(self.crypto_lib) + + def supports_cipher(self, cipher): + return CryptoLib.supports_cipher(self.crypto_lib, cipher) + + def exists(self): + return os.path.isfile(self.path) + + def clear_session(self): + if os.path.isfile(self._session_path): + os.remove(self._session_path) + if os.path.isfile(self._tp_path): + os.remove(self._tp_path) + + def http_get(self, server: ExampleServer, url: str, extra_args: List[str] = None, + use_session=False, data=None, + credentials: Credentials = None, + ciphers: str = None): + args = [ + self.path, '--exit-on-all-streams-close', + f'--qlog-file={self._qlog_path}' + ] + if use_session: + args.append(f'--session-file={self._session_path}') + args.append(f'--tp-file={self._tp_path}') + if data is not None: + with open(self._data_path, 'w') as fd: + fd.write(data) + args.append(f'--data={self._data_path}') + if ciphers is not None: + ciphers = CryptoLib.adjust_ciphers(self.crypto_lib, ciphers) + args.append(f'--ciphers={ciphers}') + if credentials is not None: + args.append(f'--key={credentials.pkey_file}') + args.append(f'--cert={credentials.cert_file}') + if extra_args is not None: + args.extend(extra_args) + args.extend([ + 'localhost', str(self.env.examples_port), + url + ]) + if os.path.isfile(self._qlog_path): + os.remove(self._qlog_path) + with open(self._log_path, 'w') as log_file: + logfile = LogFile(path=self._log_path) + server.log.advance() + process = subprocess.Popen(args=args, text=True, + stdout=log_file, stderr=log_file) + process.wait() + return ClientRun(env=self.env, returncode=process.returncode, + logfile=logfile, srun=server.get_run()) + diff --git a/examples/tests/ngtcp2test/env.py b/examples/tests/ngtcp2test/env.py new file mode 100644 index 0000000..9699d55 --- /dev/null +++ b/examples/tests/ngtcp2test/env.py @@ -0,0 +1,191 @@ +import logging +import os +from configparser import ConfigParser, ExtendedInterpolation +from typing import Dict, Optional + +from .certs import CertificateSpec, Ngtcp2TestCA, Credentials + +log = logging.getLogger(__name__) + + +class CryptoLib: + + IGNORES_CIPHER_CONFIG = [ + 'picotls', 'boringssl' + ] + UNSUPPORTED_CIPHERS = { + 'wolfssl': [ + 'TLS_AES_128_CCM_SHA256', # no plans to + ], + 'picotls': [ + 'TLS_AES_128_CCM_SHA256', # no plans to + ], + 'boringssl': [ + 'TLS_AES_128_CCM_SHA256', # no plans to + ] + } + GNUTLS_CIPHERS = { + 'TLS_AES_128_GCM_SHA256': 'AES-128-GCM', + 'TLS_AES_256_GCM_SHA384': 'AES-256-GCM', + 'TLS_CHACHA20_POLY1305_SHA256': 'CHACHA20-POLY1305', + 'TLS_AES_128_CCM_SHA256': 'AES-128-CCM', + } + + @classmethod + def uses_cipher_config(cls, crypto_lib): + return crypto_lib not in cls.IGNORES_CIPHER_CONFIG + + @classmethod + def supports_cipher(cls, crypto_lib, cipher): + return crypto_lib not in cls.UNSUPPORTED_CIPHERS or \ + cipher not in cls.UNSUPPORTED_CIPHERS[crypto_lib] + + @classmethod + def adjust_ciphers(cls, crypto_lib, ciphers: str) -> str: + if crypto_lib == 'gnutls': + gciphers = "NORMAL:-VERS-ALL:+VERS-TLS1.3:-CIPHER-ALL" + for cipher in ciphers.split(':'): + gciphers += f':+{cls.GNUTLS_CIPHERS[cipher]}' + return gciphers + return ciphers + + +def init_config_from(conf_path): + if os.path.isfile(conf_path): + config = ConfigParser(interpolation=ExtendedInterpolation()) + config.read(conf_path) + return config + return None + + +TESTS_PATH = os.path.dirname(os.path.dirname(__file__)) +EXAMPLES_PATH = os.path.dirname(TESTS_PATH) +DEF_CONFIG = init_config_from(os.path.join(TESTS_PATH, 'config.ini')) + + +class Env: + + @classmethod + def get_crypto_libs(cls, configurable_ciphers=None): + names = [name for name in DEF_CONFIG['examples'] + if DEF_CONFIG['examples'][name] == 'yes'] + if configurable_ciphers is not None: + names = [n for n in names if CryptoLib.uses_cipher_config(n)] + return names + + def __init__(self, examples_dir=None, tests_dir=None, config=None, + pytestconfig=None): + self._verbose = pytestconfig.option.verbose if pytestconfig is not None else 0 + self._examples_dir = examples_dir if examples_dir is not None else EXAMPLES_PATH + self._tests_dir = examples_dir if tests_dir is not None else TESTS_PATH + self._gen_dir = os.path.join(self._tests_dir, 'gen') + self.config = config if config is not None else DEF_CONFIG + self._version = self.config['ngtcp2']['version'] + self._crypto_libs = [name for name in self.config['examples'] + if self.config['examples'][name] == 'yes'] + self._clients = [self.config['clients'][lib] for lib in self._crypto_libs + if lib in self.config['clients']] + self._servers = [self.config['servers'][lib] for lib in self._crypto_libs + if lib in self.config['servers']] + self._examples_pem = { + 'key': 'xxx', + 'cert': 'xxx', + } + self._htdocs_dir = os.path.join(self._gen_dir, 'htdocs') + self._tld = 'tests.ngtcp2.nghttp2.org' + self._example_domain = f"one.{self._tld}" + self._ca = None + self._cert_specs = [ + CertificateSpec(domains=[self._example_domain], key_type='rsa2048'), + CertificateSpec(name="clientsX", sub_specs=[ + CertificateSpec(name="user1", client=True), + ]), + ] + + def issue_certs(self): + if self._ca is None: + self._ca = Ngtcp2TestCA.create_root(name=self._tld, + store_dir=os.path.join(self.gen_dir, 'ca'), + key_type="rsa2048") + self._ca.issue_certs(self._cert_specs) + + def setup(self): + os.makedirs(self._gen_dir, exist_ok=True) + os.makedirs(self._htdocs_dir, exist_ok=True) + self.issue_certs() + + def get_server_credentials(self) -> Optional[Credentials]: + creds = self.ca.get_credentials_for_name(self._example_domain) + if len(creds) > 0: + return creds[0] + return None + + @property + def verbose(self) -> int: + return self._verbose + + @property + def version(self) -> str: + return self._version + + @property + def gen_dir(self) -> str: + return self._gen_dir + + @property + def ca(self): + return self._ca + + @property + def htdocs_dir(self) -> str: + return self._htdocs_dir + + @property + def example_domain(self) -> str: + return self._example_domain + + @property + def examples_dir(self) -> str: + return self._examples_dir + + @property + def examples_port(self) -> int: + return int(self.config['examples']['port']) + + @property + def examples_pem(self) -> Dict[str, str]: + return self._examples_pem + + @property + def crypto_libs(self): + return self._crypto_libs + + @property + def clients(self): + return self._clients + + @property + def servers(self): + return self._servers + + def client_name(self, crypto_lib): + if crypto_lib in self.config['clients']: + return self.config['clients'][crypto_lib] + return None + + def client_path(self, crypto_lib): + cname = self.client_name(crypto_lib) + if cname is not None: + return os.path.join(self.examples_dir, cname) + return None + + def server_name(self, crypto_lib): + if crypto_lib in self.config['servers']: + return self.config['servers'][crypto_lib] + return None + + def server_path(self, crypto_lib): + sname = self.server_name(crypto_lib) + if sname is not None: + return os.path.join(self.examples_dir, sname) + return None diff --git a/examples/tests/ngtcp2test/log.py b/examples/tests/ngtcp2test/log.py new file mode 100644 index 0000000..9e8f399 --- /dev/null +++ b/examples/tests/ngtcp2test/log.py @@ -0,0 +1,101 @@ +import binascii +import os +import re +import sys +import time +from datetime import timedelta, datetime +from io import SEEK_END +from typing import List + + +class LogFile: + + def __init__(self, path: str): + self._path = path + self._start_pos = 0 + self._last_pos = self._start_pos + + @property + def path(self) -> str: + return self._path + + def reset(self): + self._start_pos = 0 + self._last_pos = self._start_pos + + def advance(self) -> None: + if os.path.isfile(self._path): + with open(self._path) as fd: + self._start_pos = fd.seek(0, SEEK_END) + + def get_recent(self, advance=True) -> List[str]: + lines = [] + if os.path.isfile(self._path): + with open(self._path) as fd: + fd.seek(self._last_pos, os.SEEK_SET) + for line in fd: + lines.append(line) + if advance: + self._last_pos = fd.tell() + return lines + + def scan_recent(self, pattern: re, timeout=10) -> bool: + if not os.path.isfile(self.path): + return False + with open(self.path) as fd: + end = datetime.now() + timedelta(seconds=timeout) + while True: + fd.seek(self._last_pos, os.SEEK_SET) + for line in fd: + if pattern.match(line): + return True + if datetime.now() > end: + raise TimeoutError(f"pattern not found in error log after {timeout} seconds") + time.sleep(.1) + return False + + +class HexDumpScanner: + + def __init__(self, source, leading_regex=None): + self._source = source + self._leading_regex = leading_regex + + def __iter__(self): + data = b'' + offset = 0 if self._leading_regex is None else -1 + idx = 0 + for l in self._source: + if offset == -1: + pass + elif offset == 0: + # possible start of a hex dump + m = re.match(r'^\s*0+(\s+-)?((\s+[0-9a-f]{2}){1,16})(\s+.*)$', + l, re.IGNORECASE) + if m: + data = binascii.unhexlify(re.sub(r'\s+', '', m.group(2))) + offset = 16 + idx = 1 + continue + else: + # possible continuation of a hexdump + m = re.match(r'^\s*([0-9a-f]+)(\s+-)?((\s+[0-9a-f]{2}){1,16})' + r'(\s+.*)$', l, re.IGNORECASE) + if m: + loffset = int(m.group(1), 16) + if loffset == offset or loffset == idx: + data += binascii.unhexlify(re.sub(r'\s+', '', + m.group(3))) + offset += 16 + idx += 1 + continue + else: + sys.stderr.write(f'wrong offset {loffset}, expected {offset} or {idx}\n') + # not a hexdump line, produce any collected data + if len(data) > 0: + yield data + data = b'' + offset = 0 if self._leading_regex is None \ + or self._leading_regex.match(l) else -1 + if len(data) > 0: + yield data diff --git a/examples/tests/ngtcp2test/server.py b/examples/tests/ngtcp2test/server.py new file mode 100644 index 0000000..9f4e9a0 --- /dev/null +++ b/examples/tests/ngtcp2test/server.py @@ -0,0 +1,137 @@ +import logging +import os +import re +import subprocess +import time +from datetime import datetime, timedelta +from threading import Thread + +from .tls import HandShake +from .env import Env, CryptoLib +from .log import LogFile, HexDumpScanner + + +log = logging.getLogger(__name__) + + +class ServerRun: + + def __init__(self, env: Env, logfile: LogFile): + self.env = env + self._logfile = logfile + self.log_lines = self._logfile.get_recent() + self._data_recs = None + self._hs_recs = None + if self.env.verbose > 1: + log.debug(f'read {len(self.log_lines)} lines from {logfile.path}') + + @property + def handshake(self): + if self._data_recs is None: + self._data_recs = [data for data in HexDumpScanner(source=self.log_lines)] + if self.env.verbose > 1: + log.debug(f'detected {len(self._data_recs)} hexdumps ' + f'in {self._logfile.path}') + if self._hs_recs is None: + self._hs_recs = [hrec for hrec in HandShake(source=self._data_recs, + verbose=self.env.verbose)] + if self.env.verbose > 1: + log.debug(f'detected {len(self._hs_recs)} crypto records ' + f'in {self._logfile.path}') + return self._hs_recs + + @property + def hs_stripe(self): + return ":".join([hrec.name for hrec in self.handshake]) + + +def monitor_proc(env: Env, proc): + _env = env + proc.wait() + + +class ExampleServer: + + def __init__(self, env: Env, crypto_lib: str, verify_client=False): + self.env = env + self._crypto_lib = crypto_lib + self._path = env.server_path(self._crypto_lib) + self._logpath = f'{self.env.gen_dir}/{self._crypto_lib}-server.log' + self._log = LogFile(path=self._logpath) + self._logfile = None + self._process = None + self._verify_client = verify_client + + @property + def path(self): + return self._path + + @property + def crypto_lib(self): + return self._crypto_lib + + @property + def uses_cipher_config(self): + return CryptoLib.uses_cipher_config(self.crypto_lib) + + def supports_cipher(self, cipher): + return CryptoLib.supports_cipher(self.crypto_lib, cipher) + + @property + def log(self): + return self._log + + def exists(self): + return os.path.isfile(self.path) + + def start(self): + if self._process is not None: + return False + creds = self.env.get_server_credentials() + assert creds + args = [ + self.path, + f'--htdocs={self.env.htdocs_dir}', + ] + if self._verify_client: + args.append('--verify-client') + args.extend([ + '*', str(self.env.examples_port), + creds.pkey_file, creds.cert_file + ]) + self._logfile = open(self._logpath, 'w') + self._process = subprocess.Popen(args=args, text=True, + stdout=self._logfile, stderr=self._logfile) + t = Thread(target=monitor_proc, daemon=True, args=(self.env, self._process)) + t.start() + timeout = 5 + end = datetime.now() + timedelta(seconds=timeout) + while True: + if self._process.poll(): + return False + try: + if self.log.scan_recent(pattern=re.compile(r'^Using document root'), timeout=0.5): + break + except TimeoutError: + pass + if datetime.now() > end: + raise TimeoutError(f"pattern not found in error log after {timeout} seconds") + self.log.advance() + return True + + def stop(self): + if self._process: + self._process.terminate() + self._process = None + if self._logfile: + self._logfile.close() + self._logfile = None + return True + + def restart(self): + self.stop() + self._log.reset() + return self.start() + + def get_run(self) -> ServerRun: + return ServerRun(env=self.env, logfile=self.log) diff --git a/examples/tests/ngtcp2test/tls.py b/examples/tests/ngtcp2test/tls.py new file mode 100644 index 0000000..f9bce62 --- /dev/null +++ b/examples/tests/ngtcp2test/tls.py @@ -0,0 +1,983 @@ +import binascii +import logging +import sys +from collections.abc import Iterator +from typing import Dict, Any, Iterable + + +log = logging.getLogger(__name__) + + +class ParseError(Exception): + pass + +def _get_int(d, n): + if len(d) < n: + raise ParseError(f'get_int: {n} bytes needed, but data is {d}') + if n == 1: + dlen = d[0] + else: + dlen = int.from_bytes(d[0:n], byteorder='big') + return d[n:], dlen + + +def _get_field(d, dlen): + if dlen > 0: + if len(d) < dlen: + raise ParseError(f'field len={dlen}, but data len={len(d)}') + field = d[0:dlen] + return d[dlen:], field + return d, b'' + + +def _get_len_field(d, n): + d, dlen = _get_int(d, n) + return _get_field(d, dlen) + + +# d are bytes that start with a quic variable length integer +def _get_qint(d): + i = d[0] & 0xc0 + if i == 0: + return d[1:], int(d[0]) + elif i == 0x40: + ndata = bytearray(d[0:2]) + d = d[2:] + ndata[0] = ndata[0] & ~0xc0 + return d, int.from_bytes(ndata, byteorder='big') + elif i == 0x80: + ndata = bytearray(d[0:4]) + d = d[4:] + ndata[0] = ndata[0] & ~0xc0 + return d, int.from_bytes(ndata, byteorder='big') + else: + ndata = bytearray(d[0:8]) + d = d[8:] + ndata[0] = ndata[0] & ~0xc0 + return d, int.from_bytes(ndata, byteorder='big') + + +class TlsSupportedGroups: + NAME_BY_ID = { + 0: 'Reserved', + 1: 'sect163k1', + 2: 'sect163r1', + 3: 'sect163r2', + 4: 'sect193r1', + 5: 'sect193r2', + 6: 'sect233k1', + 7: 'sect233r1', + 8: 'sect239k1', + 9: 'sect283k1', + 10: 'sect283r1', + 11: 'sect409k1', + 12: 'sect409r1', + 13: 'sect571k1', + 14: 'sect571r1', + 15: 'secp160k1', + 16: 'secp160r1', + 17: 'secp160r2', + 18: 'secp192k1', + 19: 'secp192r1', + 20: 'secp224k1', + 21: 'secp224r1', + 22: 'secp256k1', + 23: 'secp256r1', + 24: 'secp384r1', + 25: 'secp521r1', + 26: 'brainpoolP256r1', + 27: 'brainpoolP384r1', + 28: 'brainpoolP512r1', + 29: 'x25519', + 30: 'x448', + 31: 'brainpoolP256r1tls13', + 32: 'brainpoolP384r1tls13', + 33: 'brainpoolP512r1tls13', + 34: 'GC256A', + 35: 'GC256B', + 36: 'GC256C', + 37: 'GC256D', + 38: 'GC512A', + 39: 'GC512B', + 40: 'GC512C', + 41: 'curveSM2', + } + + @classmethod + def name(cls, gid): + if gid in cls.NAME_BY_ID: + return f'{cls.NAME_BY_ID[gid]}(0x{gid:0x})' + return f'0x{gid:0x}' + + +class TlsSignatureScheme: + NAME_BY_ID = { + 0x0201: 'rsa_pkcs1_sha1', + 0x0202: 'Reserved', + 0x0203: 'ecdsa_sha1', + 0x0401: 'rsa_pkcs1_sha256', + 0x0403: 'ecdsa_secp256r1_sha256', + 0x0420: 'rsa_pkcs1_sha256_legacy', + 0x0501: 'rsa_pkcs1_sha384', + 0x0503: 'ecdsa_secp384r1_sha384', + 0x0520: 'rsa_pkcs1_sha384_legacy', + 0x0601: 'rsa_pkcs1_sha512', + 0x0603: 'ecdsa_secp521r1_sha512', + 0x0620: 'rsa_pkcs1_sha512_legacy', + 0x0704: 'eccsi_sha256', + 0x0705: 'iso_ibs1', + 0x0706: 'iso_ibs2', + 0x0707: 'iso_chinese_ibs', + 0x0708: 'sm2sig_sm3', + 0x0709: 'gostr34102012_256a', + 0x070A: 'gostr34102012_256b', + 0x070B: 'gostr34102012_256c', + 0x070C: 'gostr34102012_256d', + 0x070D: 'gostr34102012_512a', + 0x070E: 'gostr34102012_512b', + 0x070F: 'gostr34102012_512c', + 0x0804: 'rsa_pss_rsae_sha256', + 0x0805: 'rsa_pss_rsae_sha384', + 0x0806: 'rsa_pss_rsae_sha512', + 0x0807: 'ed25519', + 0x0808: 'ed448', + 0x0809: 'rsa_pss_pss_sha256', + 0x080A: 'rsa_pss_pss_sha384', + 0x080B: 'rsa_pss_pss_sha512', + 0x081A: 'ecdsa_brainpoolP256r1tls13_sha256', + 0x081B: 'ecdsa_brainpoolP384r1tls13_sha384', + 0x081C: 'ecdsa_brainpoolP512r1tls13_sha512', + } + + @classmethod + def name(cls, gid): + if gid in cls.NAME_BY_ID: + return f'{cls.NAME_BY_ID[gid]}(0x{gid:0x})' + return f'0x{gid:0x}' + + +class TlsCipherSuites: + NAME_BY_ID = { + 0x1301: 'TLS_AES_128_GCM_SHA256', + 0x1302: 'TLS_AES_256_GCM_SHA384', + 0x1303: 'TLS_CHACHA20_POLY1305_SHA256', + 0x1304: 'TLS_AES_128_CCM_SHA256', + 0x1305: 'TLS_AES_128_CCM_8_SHA256', + 0x00ff: 'TLS_EMPTY_RENEGOTIATION_INFO_SCSV', + } + + @classmethod + def name(cls, cid): + if cid in cls.NAME_BY_ID: + return f'{cls.NAME_BY_ID[cid]}(0x{cid:0x})' + return f'Cipher(0x{cid:0x})' + + +class PskKeyExchangeMode: + NAME_BY_ID = { + 0x00: 'psk_ke', + 0x01: 'psk_dhe_ke', + } + + @classmethod + def name(cls, gid): + if gid in cls.NAME_BY_ID: + return f'{cls.NAME_BY_ID[gid]}(0x{gid:0x})' + return f'0x{gid:0x}' + + +class QuicTransportParam: + NAME_BY_ID = { + 0x00: 'original_destination_connection_id', + 0x01: 'max_idle_timeout', + 0x02: 'stateless_reset_token', + 0x03: 'max_udp_payload_size', + 0x04: 'initial_max_data', + 0x05: 'initial_max_stream_data_bidi_local', + 0x06: 'initial_max_stream_data_bidi_remote', + 0x07: 'initial_max_stream_data_uni', + 0x08: 'initial_max_streams_bidi', + 0x09: 'initial_max_streams_uni', + 0x0a: 'ack_delay_exponent', + 0x0b: 'max_ack_delay', + 0x0c: 'disable_active_migration', + 0x0d: 'preferred_address', + 0x0e: 'active_connection_id_limit', + 0x0f: 'initial_source_connection_id', + 0x10: 'retry_source_connection_id', + } + TYPE_BY_ID = { + 0x00: bytes, + 0x01: int, + 0x02: bytes, + 0x03: int, + 0x04: int, + 0x05: int, + 0x06: int, + 0x07: int, + 0x08: int, + 0x09: int, + 0x0a: int, + 0x0b: int, + 0x0c: int, + 0x0d: bytes, + 0x0e: int, + 0x0f: bytes, + 0x10: bytes, + } + + @classmethod + def name(cls, cid): + if cid in cls.NAME_BY_ID: + return f'{cls.NAME_BY_ID[cid]}(0x{cid:0x})' + return f'QuicTP(0x{cid:0x})' + + @classmethod + def is_qint(cls, cid): + if cid in cls.TYPE_BY_ID: + return cls.TYPE_BY_ID[cid] == int + return False + + +class Extension: + + def __init__(self, eid, name, edata, hsid): + self._eid = eid + self._name = name + self._edata = edata + self._hsid = hsid + + @property + def data(self): + return self._edata + + @property + def hsid(self): + return self._hsid + + def to_json(self): + jdata = { + 'id': self._eid, + 'name': self._name, + } + if len(self.data) > 0: + jdata['data'] = binascii.hexlify(self.data).decode() + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + s = f'{ind}{self._name}(0x{self._eid:0x})' + if len(self._edata): + s += f'\n{ind} data({len(self._edata)}): ' \ + f'{binascii.hexlify(self._edata).decode()}' + return s + + +class ExtSupportedGroups(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = edata + self._groups = [] + while len(d) > 0: + d, gid = _get_int(d, 2) + self._groups.append(gid) + + def to_json(self): + jdata = { + 'id': self._eid, + 'name': self._name, + } + if len(self._groups): + jdata['groups'] = [TlsSupportedGroups.name(gid) + for gid in self._groups] + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + gnames = [TlsSupportedGroups.name(gid) for gid in self._groups] + s = f'{ind}{self._name}(0x{self._eid:0x}): {", ".join(gnames)}' + return s + + +class ExtKeyShare(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + self._keys = [] + self._group = None + self._pubkey = None + if self.hsid == 2: # ServerHello + # single key share (group, pubkey) + d, self._group = _get_int(d, 2) + d, self._pubkey = _get_len_field(d, 2) + elif self.hsid == 6: # HelloRetryRequest + assert len(d) == 2 + d, self._group = _get_int(d, 2) + else: + # list if key shares (group, pubkey) + d, shares = _get_len_field(d, 2) + while len(shares) > 0: + shares, group = _get_int(shares, 2) + shares, pubkey = _get_len_field(shares, 2) + self._keys.append({ + 'group': TlsSupportedGroups.name(group), + 'pubkey': binascii.hexlify(pubkey).decode() + }) + + def to_json(self): + jdata = super().to_json() + if self._group is not None: + jdata['group'] = TlsSupportedGroups.name(self._group) + if self._pubkey is not None: + jdata['pubkey'] = binascii.hexlify(self._pubkey).decode() + if len(self._keys) > 0: + jdata['keys'] = self._keys + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + s = f'{ind}{self._name}(0x{self._eid:0x})' + if self._group is not None: + s += f'\n{ind} group: {TlsSupportedGroups.name(self._group)}' + if self._pubkey is not None: + s += f'\n{ind} pubkey: {binascii.hexlify(self._pubkey).decode()}' + if len(self._keys) > 0: + for idx, key in enumerate(self._keys): + s += f'\n{ind} {idx}: {key["group"]}, {key["pubkey"]}' + return s + + +class ExtSNI(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + self._indicators = [] + while len(d) > 0: + d, entry = _get_len_field(d, 2) + entry, stype = _get_int(entry, 1) + entry, sname = _get_len_field(entry, 2) + self._indicators.append({ + 'type': stype, + 'name': sname.decode() + }) + + def to_json(self): + jdata = super().to_json() + for i in self._indicators: + if i['type'] == 0: + jdata['host_name'] = i['name'] + else: + jdata[f'0x{i["type"]}'] = i['name'] + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + s = f'{ind}{self._name}(0x{self._eid:0x})' + if len(self._indicators) == 1 and self._indicators[0]['type'] == 0: + s += f': {self._indicators[0]["name"]}' + else: + for i in self._indicators: + ikey = 'host_name' if i["type"] == 0 else f'type(0x{i["type"]:0x}' + s += f'\n{ind} {ikey}: {i["name"]}' + return s + + +class ExtALPN(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + d, list_len = _get_int(d, 2) + self._protocols = [] + while len(d) > 0: + d, proto = _get_len_field(d, 1) + self._protocols.append(proto.decode()) + + def to_json(self): + jdata = super().to_json() + if len(self._protocols) == 1: + jdata['alpn'] = self._protocols[0] + else: + jdata['alpn'] = self._protocols + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return f'{ind}{self._name}(0x{self._eid:0x}): {", ".join(self._protocols)}' + + +class ExtEarlyData(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + self._max_size = None + d = self.data + if hsid == 4: # SessionTicket + assert len(d) == 4, f'expected 4, len is {len(d)} data={d}' + d, self._max_size = _get_int(d, 4) + else: + assert len(d) == 0 + + def to_json(self): + jdata = super().to_json() + if self._max_size is not None: + jdata['max_size'] = self._max_size + return jdata + + +class ExtSignatureAlgorithms(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + d, list_len = _get_int(d, 2) + self._algos = [] + while len(d) > 0: + d, algo = _get_int(d, 2) + self._algos.append(TlsSignatureScheme.name(algo)) + + def to_json(self): + jdata = super().to_json() + if len(self._algos) > 0: + jdata['algorithms'] = self._algos + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return f'{ind}{self._name}(0x{self._eid:0x}): {", ".join(self._algos)}' + + +class ExtPSKExchangeModes(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + d, list_len = _get_int(d, 1) + self._modes = [] + while len(d) > 0: + d, mode = _get_int(d, 1) + self._modes.append(PskKeyExchangeMode.name(mode)) + + def to_json(self): + jdata = super().to_json() + jdata['modes'] = self._modes + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return f'{ind}{self._name}(0x{self._eid:0x}): {", ".join(self._modes)}' + + +class ExtPreSharedKey(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + self._kid = None + self._identities = None + self._binders = None + d = self.data + if hsid == 1: # client hello + d, idata = _get_len_field(d, 2) + self._identities = [] + while len(idata): + idata, identity = _get_len_field(idata, 2) + idata, obfs_age = _get_int(idata, 4) + self._identities.append({ + 'id': binascii.hexlify(identity).decode(), + 'age': obfs_age, + }) + d, binders = _get_len_field(d, 2) + self._binders = [] + while len(binders) > 0: + binders, hmac = _get_len_field(binders, 1) + self._binders.append(binascii.hexlify(hmac).decode()) + assert len(d) == 0 + else: + d, self._kid = _get_int(d, 2) + + def to_json(self): + jdata = super().to_json() + if self.hsid == 1: + jdata['identities'] = self._identities + jdata['binders'] = self._binders + else: + jdata['identity'] = self._kid + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + s = f'{ind}{self._name}(0x{self._hsid:0x})' + if self.hsid == 1: + for idx, i in enumerate(self._identities): + s += f'\n{ind} {idx}: {i["id"]} ({i["age"]})' + s += f'\n{ind} binders: {self._binders}' + else: + s += f'\n{ind} identity: {self._kid}' + return s + + +class ExtSupportedVersions(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + self._versions = [] + if hsid == 1: # client hello + d, list_len = _get_int(d, 1) + while len(d) > 0: + d, version = _get_int(d, 2) + self._versions.append(f'0x{version:0x}') + else: + d, version = _get_int(d, 2) + self._versions.append(f'0x{version:0x}') + + def to_json(self): + jdata = super().to_json() + if len(self._versions) == 1: + jdata['version'] = self._versions[0] + else: + jdata['versions'] = self._versions + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return f'{ind}{self._name}(0x{self._eid:0x}): {", ".join(self._versions)}' + + +class ExtQuicTP(Extension): + + def __init__(self, eid, name, edata, hsid): + super().__init__(eid=eid, name=name, edata=edata, hsid=hsid) + d = self.data + self._params = [] + while len(d) > 0: + d, ptype = _get_qint(d) + d, plen = _get_qint(d) + d, pvalue = _get_field(d, plen) + if QuicTransportParam.is_qint(ptype): + _, pvalue = _get_qint(pvalue) + else: + pvalue = binascii.hexlify(pvalue).decode() + self._params.append({ + 'key': QuicTransportParam.name(ptype), + 'value': pvalue, + }) + + def to_json(self): + jdata = super().to_json() + jdata['params'] = self._params + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + s = f'{ind}{self._name}(0x{self._eid:0x})' + for p in self._params: + s += f'\n{ind} {p["key"]}: {p["value"]}' + return s + + +class TlsExtensions: + + EXT_TYPES = [ + (0x00, 'SNI', ExtSNI), + (0x01, 'MAX_FRAGMENT_LENGTH', Extension), + (0x03, 'TRUSTED_CA_KEYS', Extension), + (0x04, 'TRUNCATED_HMAC', Extension), + (0x05, 'OSCP_STATUS_REQUEST', Extension), + (0x0a, 'SUPPORTED_GROUPS', ExtSupportedGroups), + (0x0b, 'EC_POINT_FORMATS', Extension), + (0x0d, 'SIGNATURE_ALGORITHMS', ExtSignatureAlgorithms), + (0x0e, 'USE_SRTP', Extension), + (0x10, 'ALPN', ExtALPN), + (0x11, 'STATUS_REQUEST_V2', Extension), + (0x16, 'ENCRYPT_THEN_MAC', Extension), + (0x17, 'EXTENDED_MASTER_SECRET', Extension), + (0x23, 'SESSION_TICKET', Extension), + (0x29, 'PRE_SHARED_KEY', ExtPreSharedKey), + (0x2a, 'EARLY_DATA', ExtEarlyData), + (0x2b, 'SUPPORTED_VERSIONS', ExtSupportedVersions), + (0x2c, 'COOKIE', Extension), + (0x2d, 'PSK_KEY_EXCHANGE_MODES', ExtPSKExchangeModes), + (0x31, 'POST_HANDSHAKE_AUTH', Extension), + (0x32, 'SIGNATURE_ALGORITHMS_CERT', Extension), + (0x33, 'KEY_SHARE', ExtKeyShare), + (0x39, 'QUIC_TP_PARAMS', ExtQuicTP), + (0xff01, 'RENEGOTIATION_INFO', Extension), + (0xffa5, 'QUIC_TP_PARAMS_DRAFT', ExtQuicTP), + ] + NAME_BY_ID = {} + CLASS_BY_ID = {} + + @classmethod + def init(cls): + for (eid, name, ecls) in cls.EXT_TYPES: + cls.NAME_BY_ID[eid] = name + cls.CLASS_BY_ID[eid] = ecls + + @classmethod + def from_data(cls, hsid, data): + exts = [] + d = data + while len(d): + d, eid = _get_int(d, 2) + d, elen = _get_int(d, 2) + d, edata = _get_field(d, elen) + if eid in cls.NAME_BY_ID: + ename = cls.NAME_BY_ID[eid] + ecls = cls.CLASS_BY_ID[eid] + exts.append(ecls(eid=eid, name=ename, edata=edata, hsid=hsid)) + else: + exts.append(Extension(eid=eid, name=f'(0x{eid:0x})', + edata=edata, hsid=hsid)) + return exts + + +TlsExtensions.init() + + +class HSRecord: + + def __init__(self, hsid: int, name: str, data): + self._hsid = hsid + self._name = name + self._data = data + + @property + def hsid(self): + return self._hsid + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + + @property + def data(self): + return self._data + + def __repr__(self): + return f'{self.name}[{binascii.hexlify(self._data).decode()}]' + + def to_json(self) -> Dict[str, Any]: + return { + 'name': self.name, + 'data': binascii.hexlify(self._data).decode(), + } + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return f'{ind}{self._name}\n'\ + f'{ind} id: 0x{self._hsid:0x}\n'\ + f'{ind} data({len(self._data)}): '\ + f'{binascii.hexlify(self._data).decode()}' + + +class ClientHello(HSRecord): + + def __init__(self, hsid: int, name: str, data): + super().__init__(hsid=hsid, name=name, data=data) + d = data + d, self._version = _get_int(d, 2) + d, self._random = _get_field(d, 32) + d, self._session_id = _get_len_field(d, 1) + self._ciphers = [] + d, ciphers = _get_len_field(d, 2) + while len(ciphers): + ciphers, cipher = _get_int(ciphers, 2) + self._ciphers.append(TlsCipherSuites.name(cipher)) + d, comps = _get_len_field(d, 1) + self._compressions = [int(c) for c in comps] + d, edata = _get_len_field(d, 2) + self._extensions = TlsExtensions.from_data(hsid, edata) + + def to_json(self): + jdata = super().to_json() + jdata['version'] = f'0x{self._version:0x}' + jdata['random'] = f'{binascii.hexlify(self._random).decode()}' + jdata['session_id'] = binascii.hexlify(self._session_id).decode() + jdata['ciphers'] = self._ciphers + jdata['compressions'] = self._compressions + jdata['extensions'] = [ext.to_json() for ext in self._extensions] + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return super().to_text(indent=indent) + '\n'\ + f'{ind} version: 0x{self._version:0x}\n'\ + f'{ind} random: {binascii.hexlify(self._random).decode()}\n' \ + f'{ind} session_id: {binascii.hexlify(self._session_id).decode()}\n' \ + f'{ind} ciphers: {", ".join(self._ciphers)}\n'\ + f'{ind} compressions: {self._compressions}\n'\ + f'{ind} extensions: \n' + '\n'.join( + [ext.to_text(indent=indent+4) for ext in self._extensions]) + + +class ServerHello(HSRecord): + + HELLO_RETRY_RANDOM = binascii.unhexlify( + 'CF21AD74E59A6111BE1D8C021E65B891C2A211167ABB8C5E079E09E2C8A8339C' + ) + + def __init__(self, hsid: int, name: str, data): + super().__init__(hsid=hsid, name=name, data=data) + d = data + d, self._version = _get_int(d, 2) + d, self._random = _get_field(d, 32) + if self._random == self.HELLO_RETRY_RANDOM: + self.name = 'HelloRetryRequest' + hsid = 6 + d, self._session_id = _get_len_field(d, 1) + d, cipher = _get_int(d, 2) + self._cipher = TlsCipherSuites.name(cipher) + d, self._compression = _get_int(d, 1) + d, edata = _get_len_field(d, 2) + self._extensions = TlsExtensions.from_data(hsid, edata) + + def to_json(self): + jdata = super().to_json() + jdata['version'] = f'0x{self._version:0x}' + jdata['random'] = f'{binascii.hexlify(self._random).decode()}' + jdata['session_id'] = binascii.hexlify(self._session_id).decode() + jdata['cipher'] = self._cipher + jdata['compression'] = int(self._compression) + jdata['extensions'] = [ext.to_json() for ext in self._extensions] + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return super().to_text(indent=indent) + '\n'\ + f'{ind} version: 0x{self._version:0x}\n'\ + f'{ind} random: {binascii.hexlify(self._random).decode()}\n' \ + f'{ind} session_id: {binascii.hexlify(self._session_id).decode()}\n' \ + f'{ind} cipher: {self._cipher}\n'\ + f'{ind} compression: {int(self._compression)}\n'\ + f'{ind} extensions: \n' + '\n'.join( + [ext.to_text(indent=indent+4) for ext in self._extensions]) + + +class EncryptedExtensions(HSRecord): + + def __init__(self, hsid: int, name: str, data): + super().__init__(hsid=hsid, name=name, data=data) + d = data + d, edata = _get_len_field(d, 2) + self._extensions = TlsExtensions.from_data(hsid, edata) + + def to_json(self): + jdata = super().to_json() + jdata['extensions'] = [ext.to_json() for ext in self._extensions] + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return super().to_text(indent=indent) + '\n'\ + f'{ind} extensions: \n' + '\n'.join( + [ext.to_text(indent=indent+4) for ext in self._extensions]) + + +class CertificateRequest(HSRecord): + + def __init__(self, hsid: int, name: str, data): + super().__init__(hsid=hsid, name=name, data=data) + d = data + d, self._context = _get_int(d, 1) + d, edata = _get_len_field(d, 2) + self._extensions = TlsExtensions.from_data(hsid, edata) + + def to_json(self): + jdata = super().to_json() + jdata['context'] = self._context + jdata['extensions'] = [ext.to_json() for ext in self._extensions] + return jdata + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return super().to_text(indent=indent) + '\n'\ + f'{ind} context: {self._context}\n'\ + f'{ind} extensions: \n' + '\n'.join( + [ext.to_text(indent=indent+4) for ext in self._extensions]) + + +class Certificate(HSRecord): + + def __init__(self, hsid: int, name: str, data): + super().__init__(hsid=hsid, name=name, data=data) + d = data + d, self._context = _get_int(d, 1) + d, clist = _get_len_field(d, 3) + self._cert_entries = [] + while len(clist) > 0: + clist, cert_data = _get_len_field(clist, 3) + clist, cert_exts = _get_len_field(clist, 2) + exts = TlsExtensions.from_data(hsid, cert_exts) + self._cert_entries.append({ + 'cert': binascii.hexlify(cert_data).decode(), + 'extensions': exts, + }) + + def to_json(self): + jdata = super().to_json() + jdata['context'] = self._context + jdata['certificate_list'] = [{ + 'cert': e['cert'], + 'extensions': [x.to_json() for x in e['extensions']], + } for e in self._cert_entries] + return jdata + + def _enxtry_text(self, e, indent: int = 0): + ind = ' ' * (indent + 2) + return f'{ind} cert: {e["cert"]}\n'\ + f'{ind} extensions: \n' + '\n'.join( + [x.to_text(indent=indent + 4) for x in e['extensions']]) + + def to_text(self, indent: int = 0): + ind = ' ' * (indent + 2) + return super().to_text(indent=indent) + '\n'\ + f'{ind} context: {self._context}\n'\ + f'{ind} certificate_list: \n' + '\n'.join( + [self._enxtry_text(e, indent+4) for e in self._cert_entries]) + + +class SessionTicket(HSRecord): + + def __init__(self, hsid: int, name: str, data): + super().__init__(hsid=hsid, name=name, data=data) + d = data + d, self._lifetime = _get_int(d, 4) + d, self._age = _get_int(d, 4) + d, self._nonce = _get_len_field(d, 1) + d, self._ticket = _get_len_field(d, 2) + d, edata = _get_len_field(d, 2) + self._extensions = TlsExtensions.from_data(hsid, edata) + + def to_json(self): + jdata = super().to_json() + jdata['lifetime'] = self._lifetime + jdata['age'] = self._age + jdata['nonce'] = binascii.hexlify(self._nonce).decode() + jdata['ticket'] = binascii.hexlify(self._ticket).decode() + jdata['extensions'] = [ext.to_json() for ext in self._extensions] + return jdata + + +class HSIterator(Iterator): + + def __init__(self, recs): + self._recs = recs + self._index = 0 + + def __iter__(self): + return self + + def __next__(self): + try: + result = self._recs[self._index] + except IndexError: + raise StopIteration + self._index += 1 + return result + + +class HandShake: + REC_TYPES = [ + (1, 'ClientHello', ClientHello), + (2, 'ServerHello', ServerHello), + (3, 'HelloVerifyRequest', HSRecord), + (4, 'SessionTicket', SessionTicket), + (5, 'EndOfEarlyData', HSRecord), + (6, 'HelloRetryRequest', ServerHello), + (8, 'EncryptedExtensions', EncryptedExtensions), + (11, 'Certificate', Certificate), + (12, 'ServerKeyExchange ', HSRecord), + (13, 'CertificateRequest', CertificateRequest), + (14, 'ServerHelloDone', HSRecord), + (15, 'CertificateVerify', HSRecord), + (16, 'ClientKeyExchange', HSRecord), + (20, 'Finished', HSRecord), + (22, 'CertificateStatus', HSRecord), + (24, 'KeyUpdate', HSRecord), + ] + RT_NAME_BY_ID = {} + RT_CLS_BY_ID = {} + + @classmethod + def _parse_rec(cls, data): + d, hsid = _get_int(data, 1) + if hsid not in cls.RT_CLS_BY_ID: + raise ParseError(f'unknown type {hsid}') + d, rec_len = _get_int(d, 3) + if rec_len > len(d): + # incomplete, need more data + return data, None + d, rec_data = _get_field(d, rec_len) + if hsid in cls.RT_CLS_BY_ID: + name = cls.RT_NAME_BY_ID[hsid] + rcls = cls.RT_CLS_BY_ID[hsid] + else: + name = f'CryptoRecord(0x{hsid:0x})' + rcls = HSRecord + return d, rcls(hsid=hsid, name=name, data=rec_data) + + @classmethod + def _parse(cls, source, strict=False, verbose: int = 0): + d = b'' + hsid = 0 + hsrecs = [] + if verbose > 0: + log.debug(f'scanning for handshake records') + blocks = [d for d in source] + while len(blocks) > 0: + try: + total_data = b''.join(blocks) + remain, r = cls._parse_rec(total_data) + if r is None: + # if we could not recognize a record, skip the first + # data block and try again + blocks = blocks[1:] + continue + hsrecs.append(r) + cons_len = len(total_data) - len(remain) + while cons_len > 0 and len(blocks) > 0: + if cons_len >= len(blocks[0]): + cons_len -= len(blocks[0]) + blocks = blocks[1:] + else: + blocks[0] = blocks[0][cons_len:] + cons_len = 0 + if verbose > 2: + log.debug(f'added record: {r.to_text()}') + except ParseError as err: + # if we could not recognize a record, skip the first + # data block and try again + blocks = blocks[1:] + if len(blocks) > 0 and strict: + raise Exception(f'possibly incomplete handshake record ' + f'id={hsid}, from raw={blocks}\n') + return hsrecs + + + + @classmethod + def init(cls): + for (hsid, name, rcls) in cls.REC_TYPES: + cls.RT_NAME_BY_ID[hsid] = name + cls.RT_CLS_BY_ID[hsid] = rcls + + def __init__(self, source: Iterable[bytes], strict: bool = False, + verbose: int = 0): + self._source = source + self._strict = strict + self._verbose = verbose + + def __iter__(self): + return HSIterator(recs=self._parse(self._source, strict=self._strict, + verbose=self._verbose)) + + +HandShake.init() diff --git a/examples/tests/test_01_handshake.py b/examples/tests/test_01_handshake.py new file mode 100644 index 0000000..f1a01d1 --- /dev/null +++ b/examples/tests/test_01_handshake.py @@ -0,0 +1,30 @@ +import pytest + +from .ngtcp2test import ExampleClient +from .ngtcp2test import ExampleServer +from .ngtcp2test import Env + + +@pytest.mark.skipif(condition=len(Env.get_crypto_libs()) == 0, + reason="no crypto lib examples configured") +class TestHandshake: + + @pytest.fixture(scope='class', params=Env.get_crypto_libs()) + def server(self, env, request) -> ExampleServer: + s = ExampleServer(env=env, crypto_lib=request.param) + assert s.exists(), f'server not found: {s.path}' + assert s.start() + yield s + s.stop() + + @pytest.fixture(scope='function', params=Env.get_crypto_libs()) + def client(self, env, request) -> ExampleClient: + client = ExampleClient(env=env, crypto_lib=request.param) + assert client.exists() + yield client + + def test_01_01_get(self, env: Env, server, client): + # run simple GET, no sessions, needs to give full handshake + cr = client.http_get(server, url=f'https://{env.example_domain}/') + assert cr.returncode == 0 + cr.assert_non_resume_handshake() diff --git a/examples/tests/test_02_resume.py b/examples/tests/test_02_resume.py new file mode 100644 index 0000000..3de1344 --- /dev/null +++ b/examples/tests/test_02_resume.py @@ -0,0 +1,46 @@ +import pytest + +from .ngtcp2test import ExampleClient +from .ngtcp2test import ExampleServer +from .ngtcp2test import Env + + +@pytest.mark.skipif(condition=len(Env.get_crypto_libs()) == 0, + reason="no crypto lib examples configured") +class TestResume: + + @pytest.fixture(scope='class', params=Env.get_crypto_libs()) + def server(self, env, request) -> ExampleServer: + s = ExampleServer(env=env, crypto_lib=request.param) + assert s.exists(), f'server not found: {s.path}' + assert s.start() + yield s + s.stop() + + @pytest.fixture(scope='function', params=Env.get_crypto_libs()) + def client(self, env, request) -> ExampleClient: + client = ExampleClient(env=env, crypto_lib=request.param) + assert client.exists() + yield client + + def test_02_01(self, env: Env, server, client): + # run GET with sessions but no early data, cleared first, then reused + client.clear_session() + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, + extra_args=['--disable-early-data']) + assert cr.returncode == 0 + cr.assert_non_resume_handshake() + # Now do this again and we expect a resumption, meaning no certificate + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, + extra_args=['--disable-early-data']) + assert cr.returncode == 0 + cr.assert_resume_handshake() + # restart the server, do it again + server.restart() + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, + extra_args=['--disable-early-data']) + assert cr.returncode == 0 + cr.assert_non_resume_handshake() diff --git a/examples/tests/test_03_earlydata.py b/examples/tests/test_03_earlydata.py new file mode 100644 index 0000000..a0170c3 --- /dev/null +++ b/examples/tests/test_03_earlydata.py @@ -0,0 +1,56 @@ +import pytest + +from .ngtcp2test import ExampleClient +from .ngtcp2test import ExampleServer +from .ngtcp2test import Env + + +@pytest.mark.skipif(condition=len(Env.get_crypto_libs()) == 0, + reason="no crypto lib examples configured") +class TestEarlyData: + + @pytest.fixture(scope='class', params=Env.get_crypto_libs()) + def server(self, env, request) -> ExampleServer: + s = ExampleServer(env=env, crypto_lib=request.param) + assert s.exists(), f'server not found: {s.path}' + assert s.start() + yield s + s.stop() + + @pytest.fixture(scope='function', params=Env.get_crypto_libs()) + def client(self, env, request) -> ExampleClient: + client = ExampleClient(env=env, crypto_lib=request.param) + assert client.exists() + yield client + + def test_03_01(self, env: Env, server, client): + # run GET with sessions, cleared first, without a session, early + # data will not even be attempted + client.clear_session() + edata = 'This is the early data. It is not much.' + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, data=edata) + assert cr.returncode == 0 + cr.assert_non_resume_handshake() + # resume session, early data is sent and accepted + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, data=edata) + assert cr.returncode == 0 + cr.assert_resume_handshake() + assert not cr.early_data_rejected + # restart the server, resume, early data is attempted but will not work + server.restart() + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, data=edata) + assert cr.returncode == 0 + assert cr.early_data_rejected + cr.assert_non_resume_handshake() + # restart again, sent data, but not as early data + server.restart() + cr = client.http_get(server, url=f'https://{env.example_domain}/', + use_session=True, data=edata, + extra_args=['--disable-early-data']) + assert cr.returncode == 0 + # we see no rejection, since it was not used + assert not cr.early_data_rejected + cr.assert_non_resume_handshake() diff --git a/examples/tests/test_04_clientcert.py b/examples/tests/test_04_clientcert.py new file mode 100644 index 0000000..bde1b18 --- /dev/null +++ b/examples/tests/test_04_clientcert.py @@ -0,0 +1,57 @@ +import pytest + +from .ngtcp2test import ExampleClient +from .ngtcp2test import ExampleServer +from .ngtcp2test import Env + + +@pytest.mark.skipif(condition=len(Env.get_crypto_libs()) == 0, + reason="no crypto lib examples configured") +class TestClientCert: + + @pytest.fixture(scope='class', params=Env.get_crypto_libs()) + def server(self, env, request) -> ExampleServer: + s = ExampleServer(env=env, crypto_lib=request.param, + verify_client=True) + assert s.exists(), f'server not found: {s.path}' + assert s.start() + yield s + s.stop() + + @pytest.fixture(scope='function', params=Env.get_crypto_libs()) + def client(self, env, request) -> ExampleClient: + client = ExampleClient(env=env, crypto_lib=request.param) + assert client.exists() + yield client + + def test_04_01(self, env: Env, server, client): + # run GET with a server requesting a cert, client has none to offer + cr = client.http_get(server, url=f'https://{env.example_domain}/') + assert cr.returncode == 0 + cr.assert_verify_null_handshake() + creqs = [r for r in cr.handshake if r.hsid == 13] # CertificateRequest + assert len(creqs) == 1 + creq = creqs[0].to_json() + certs = [r for r in cr.server.handshake if r.hsid == 11] # Certificate + assert len(certs) == 1 + crec = certs[0].to_json() + assert len(crec['certificate_list']) == 0 + assert creq['context'] == crec['context'] + # TODO: check that GET had no answer + + def test_04_02(self, env: Env, server, client): + # run GET with a server requesting a cert, client has cert to offer + credentials = env.ca.get_first("clientsX") + cr = client.http_get(server, url=f'https://{env.example_domain}/', + credentials=credentials) + assert cr.returncode == 0 + cr.assert_verify_cert_handshake() + creqs = [r for r in cr.handshake if r.hsid == 13] # CertificateRequest + assert len(creqs) == 1 + creq = creqs[0].to_json() + certs = [r for r in cr.server.handshake if r.hsid == 11] # Certificate + assert len(certs) == 1 + crec = certs[0].to_json() + assert len(crec['certificate_list']) == 1 + assert creq['context'] == crec['context'] + # TODO: check that GET indeed gave a response diff --git a/examples/tests/test_05_ciphers.py b/examples/tests/test_05_ciphers.py new file mode 100644 index 0000000..27f326e --- /dev/null +++ b/examples/tests/test_05_ciphers.py @@ -0,0 +1,46 @@ +import sys + +import pytest + +from .ngtcp2test import ExampleClient +from .ngtcp2test import ExampleServer +from .ngtcp2test import Env + + +@pytest.mark.skipif(condition=len(Env.get_crypto_libs()) == 0, + reason="no crypto lib examples configured") +class TestCiphers: + + @pytest.fixture(scope='class', params=Env.get_crypto_libs()) + def server(self, env, request) -> ExampleServer: + s = ExampleServer(env=env, crypto_lib=request.param) + assert s.exists(), f'server not found: {s.path}' + assert s.start() + yield s + s.stop() + + @pytest.fixture(scope='function', + params=Env.get_crypto_libs(configurable_ciphers=True)) + def client(self, env, request) -> ExampleClient: + client = ExampleClient(env=env, crypto_lib=request.param) + assert client.exists() + yield client + + @pytest.mark.parametrize('cipher', [ + 'TLS_AES_128_GCM_SHA256', + 'TLS_AES_256_GCM_SHA384', + 'TLS_CHACHA20_POLY1305_SHA256', + 'TLS_AES_128_CCM_SHA256', + ]) + def test_05_01_get(self, env: Env, server, client, cipher): + if not client.uses_cipher_config: + pytest.skip(f'client {client.crypto_lib} ignores cipher config\n') + # run simple GET, no sessions, needs to give full handshake + if not client.supports_cipher(cipher): + pytest.skip(f'client {client.crypto_lib} does not support {cipher}\n') + if not server.supports_cipher(cipher): + pytest.skip(f'server {server.crypto_lib} does not support {cipher}\n') + cr = client.http_get(server, url=f'https://{env.example_domain}/', + ciphers=cipher) + assert cr.returncode == 0 + cr.assert_non_resume_handshake() diff --git a/examples/tls_client_context.h b/examples/tls_client_context.h new file mode 100644 index 0000000..31e2d47 --- /dev/null +++ b/examples/tls_client_context.h @@ -0,0 +1,52 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_CONTEXT_H +#define TLS_CLIENT_CONTEXT_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#if defined(ENABLE_EXAMPLE_OPENSSL) && defined(WITH_EXAMPLE_OPENSSL) +# include "tls_client_context_openssl.h" +#endif // ENABLE_EXAMPLE_OPENSSL && WITH_EXAMPLE_OPENSSL + +#if defined(ENABLE_EXAMPLE_GNUTLS) && defined(WITH_EXAMPLE_GNUTLS) +# include "tls_client_context_gnutls.h" +#endif // ENABLE_EXAMPLE_GNUTLS && WITH_EXAMPLE_GNUTLS + +#if defined(ENABLE_EXAMPLE_BORINGSSL) && defined(WITH_EXAMPLE_BORINGSSL) +# include "tls_client_context_boringssl.h" +#endif // ENABLE_EXAMPLE_BORINGSSL && WITH_EXAMPLE_BORINGSSL + +#if defined(ENABLE_EXAMPLE_PICOTLS) && defined(WITH_EXAMPLE_PICOTLS) +# include "tls_client_context_picotls.h" +#endif // ENABLE_EXAMPLE_PICOTLS && WITH_EXAMPLE_PICOTLS + +#if defined(ENABLE_EXAMPLE_WOLFSSL) && defined(WITH_EXAMPLE_WOLFSSL) +# include "tls_client_context_wolfssl.h" +#endif // ENABLE_EXAMPLE_WOLFSSL && WITH_EXAMPLE_WOLFSSL + +#endif // TLS_CLIENT_CONTEXT_H diff --git a/examples/tls_client_context_boringssl.cc b/examples/tls_client_context_boringssl.cc new file mode 100644 index 0000000..bfdc525 --- /dev/null +++ b/examples/tls_client_context_boringssl.cc @@ -0,0 +1,126 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_context_boringssl.h" + +#include <iostream> +#include <fstream> + +#include <ngtcp2/ngtcp2_crypto_boringssl.h> + +#include <openssl/err.h> + +#include "client_base.h" +#include "template.h" + +extern Config config; + +TLSClientContext::TLSClientContext() : ssl_ctx_{nullptr} {} + +TLSClientContext::~TLSClientContext() { + if (ssl_ctx_) { + SSL_CTX_free(ssl_ctx_); + } +} + +SSL_CTX *TLSClientContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int new_session_cb(SSL *ssl, SSL_SESSION *session) { + auto f = BIO_new_file(config.session_file, "w"); + if (f == nullptr) { + std::cerr << "Could not write TLS session in " << config.session_file + << std::endl; + return 0; + } + + if (!PEM_write_bio_SSL_SESSION(f, session)) { + std::cerr << "Unable to write TLS session to file" << std::endl; + } + + BIO_free(f); + + return 0; +} +} // namespace + +int TLSClientContext::init(const char *private_key_file, + const char *cert_file) { + ssl_ctx_ = SSL_CTX_new(TLS_client_method()); + if (!ssl_ctx_) { + std::cerr << "SSL_CTX_new: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + if (ngtcp2_crypto_boringssl_configure_client_context(ssl_ctx_) != 0) { + std::cerr << "ngtcp2_crypto_boringssl_configure_client_context failed" + << std::endl; + return -1; + } + + SSL_CTX_set_default_verify_paths(ssl_ctx_); + + if (SSL_CTX_set1_curves_list(ssl_ctx_, config.groups) != 1) { + std::cerr << "SSL_CTX_set1_curves_list failed" << std::endl; + return -1; + } + + if (private_key_file && cert_file) { + if (SSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != 1) { + std::cerr << "SSL_CTX_use_PrivateKey_file: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + if (SSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != 1) { + std::cerr << "SSL_CTX_use_certificate_chain_file: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + } + + if (config.session_file) { + SSL_CTX_set_session_cache_mode(ssl_ctx_, SSL_SESS_CACHE_CLIENT | + SSL_SESS_CACHE_NO_INTERNAL); + SSL_CTX_sess_set_new_cb(ssl_ctx_, new_session_cb); + } + + return 0; +} + +extern std::ofstream keylog_file; + +namespace { +void keylog_callback(const SSL *ssl, const char *line) { + keylog_file.write(line, strlen(line)); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace + +void TLSClientContext::enable_keylog() { + SSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +} diff --git a/examples/tls_client_context_boringssl.h b/examples/tls_client_context_boringssl.h new file mode 100644 index 0000000..22b581a --- /dev/null +++ b/examples/tls_client_context_boringssl.h @@ -0,0 +1,49 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_CONTEXT_BORINGSSL_H +#define TLS_CLIENT_CONTEXT_BORINGSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <openssl/ssl.h> + +class TLSClientContext { +public: + TLSClientContext(); + ~TLSClientContext(); + + int init(const char *private_key_file, const char *cert_file); + + SSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + SSL_CTX *ssl_ctx_; +}; + +#endif // TLS_CLIENT_CONTEXT_BORINGSSL_H diff --git a/examples/tls_client_context_gnutls.cc b/examples/tls_client_context_gnutls.cc new file mode 100644 index 0000000..1fa03a8 --- /dev/null +++ b/examples/tls_client_context_gnutls.cc @@ -0,0 +1,74 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_context_gnutls.h" + +#include <iostream> + +#include <ngtcp2/ngtcp2_crypto_gnutls.h> + +#include "client_base.h" +#include "template.h" + +// Based on https://github.com/ueno/ngtcp2-gnutls-examples + +extern Config config; + +TLSClientContext::TLSClientContext() : cred_{nullptr} {} + +TLSClientContext::~TLSClientContext() { + gnutls_certificate_free_credentials(cred_); +} + +gnutls_certificate_credentials_t TLSClientContext::get_native_handle() const { + return cred_; +} + +int TLSClientContext::init(const char *private_key_file, + const char *cert_file) { + + if (auto rv = gnutls_certificate_allocate_credentials(&cred_); rv != 0) { + std::cerr << "gnutls_certificate_allocate_credentials failed: " + << gnutls_strerror(rv) << std::endl; + return -1; + } + + if (auto rv = gnutls_certificate_set_x509_system_trust(cred_); rv < 0) { + std::cerr << "gnutls_certificate_set_x509_system_trust failed: " + << gnutls_strerror(rv) << std::endl; + return -1; + } + + if (private_key_file != nullptr && cert_file != nullptr) { + if (auto rv = gnutls_certificate_set_x509_key_file( + cred_, cert_file, private_key_file, GNUTLS_X509_FMT_PEM); + rv != 0) { + std::cerr << "gnutls_certificate_set_x509_key_file failed: " + << gnutls_strerror(rv) << std::endl; + return -1; + } + } + + return 0; +} diff --git a/examples/tls_client_context_gnutls.h b/examples/tls_client_context_gnutls.h new file mode 100644 index 0000000..f637a15 --- /dev/null +++ b/examples/tls_client_context_gnutls.h @@ -0,0 +1,50 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_CONTEXT_GNUTLS_H +#define TLS_CLIENT_CONTEXT_GNUTLS_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <gnutls/gnutls.h> + +class TLSClientContext { +public: + TLSClientContext(); + ~TLSClientContext(); + + int init(const char *private_key_file, const char *cert_file); + + gnutls_certificate_credentials_t get_native_handle() const; + + // Keylog is enabled per session. + void enable_keylog() {} + +private: + gnutls_certificate_credentials_t cred_; +}; + +#endif // TLS_CLIENT_CONTEXT_GNUTLS_H diff --git a/examples/tls_client_context_openssl.cc b/examples/tls_client_context_openssl.cc new file mode 100644 index 0000000..06c8af1 --- /dev/null +++ b/examples/tls_client_context_openssl.cc @@ -0,0 +1,137 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_context_openssl.h" + +#include <iostream> +#include <fstream> +#include <limits> + +#include <ngtcp2/ngtcp2_crypto_openssl.h> + +#include <openssl/err.h> + +#include "client_base.h" +#include "template.h" + +extern Config config; + +TLSClientContext::TLSClientContext() : ssl_ctx_{nullptr} {} + +TLSClientContext::~TLSClientContext() { + if (ssl_ctx_) { + SSL_CTX_free(ssl_ctx_); + } +} + +SSL_CTX *TLSClientContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int new_session_cb(SSL *ssl, SSL_SESSION *session) { + if (SSL_SESSION_get_max_early_data(session) != + std::numeric_limits<uint32_t>::max()) { + std::cerr << "max_early_data_size is not 0xffffffff" << std::endl; + } + auto f = BIO_new_file(config.session_file, "w"); + if (f == nullptr) { + std::cerr << "Could not write TLS session in " << config.session_file + << std::endl; + return 0; + } + + if (!PEM_write_bio_SSL_SESSION(f, session)) { + std::cerr << "Unable to write TLS session to file" << std::endl; + } + + BIO_free(f); + + return 0; +} +} // namespace + +int TLSClientContext::init(const char *private_key_file, + const char *cert_file) { + ssl_ctx_ = SSL_CTX_new(TLS_client_method()); + if (!ssl_ctx_) { + std::cerr << "SSL_CTX_new: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + if (ngtcp2_crypto_openssl_configure_client_context(ssl_ctx_) != 0) { + std::cerr << "ngtcp2_crypto_openssl_configure_client_context failed" + << std::endl; + return -1; + } + + SSL_CTX_set_default_verify_paths(ssl_ctx_); + + if (SSL_CTX_set_ciphersuites(ssl_ctx_, config.ciphers) != 1) { + std::cerr << "SSL_CTX_set_ciphersuites: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + if (SSL_CTX_set1_groups_list(ssl_ctx_, config.groups) != 1) { + std::cerr << "SSL_CTX_set1_groups_list failed" << std::endl; + return -1; + } + + if (private_key_file && cert_file) { + if (SSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != 1) { + std::cerr << "SSL_CTX_use_PrivateKey_file: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + if (SSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != 1) { + std::cerr << "SSL_CTX_use_certificate_chain_file: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + } + + if (config.session_file) { + SSL_CTX_set_session_cache_mode(ssl_ctx_, SSL_SESS_CACHE_CLIENT | + SSL_SESS_CACHE_NO_INTERNAL); + SSL_CTX_sess_set_new_cb(ssl_ctx_, new_session_cb); + } + + return 0; +} + +extern std::ofstream keylog_file; + +namespace { +void keylog_callback(const SSL *ssl, const char *line) { + keylog_file.write(line, strlen(line)); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace + +void TLSClientContext::enable_keylog() { + SSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +} diff --git a/examples/tls_client_context_openssl.h b/examples/tls_client_context_openssl.h new file mode 100644 index 0000000..a6d0114 --- /dev/null +++ b/examples/tls_client_context_openssl.h @@ -0,0 +1,49 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_CONTEXT_OPENSSL_H +#define TLS_CLIENT_CONTEXT_OPENSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <openssl/ssl.h> + +class TLSClientContext { +public: + TLSClientContext(); + ~TLSClientContext(); + + int init(const char *private_key_file, const char *cert_file); + + SSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + SSL_CTX *ssl_ctx_; +}; + +#endif // TLS_CLIENT_CONTEXT_OPENSSL_H diff --git a/examples/tls_client_context_picotls.cc b/examples/tls_client_context_picotls.cc new file mode 100644 index 0000000..363f6c7 --- /dev/null +++ b/examples/tls_client_context_picotls.cc @@ -0,0 +1,155 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_context_picotls.h" + +#include <iostream> + +#include <ngtcp2/ngtcp2_crypto_picotls.h> + +#include <openssl/bio.h> +#include <openssl/pem.h> + +#include "client_base.h" +#include "template.h" + +extern Config config; + +namespace { +int save_ticket_cb(ptls_save_ticket_t *self, ptls_t *ptls, ptls_iovec_t input) { + auto f = BIO_new_file(config.session_file, "w"); + if (f == nullptr) { + std::cerr << "Could not write TLS session in " << config.session_file + << std::endl; + return 0; + } + + if (!PEM_write_bio(f, "PICOTLS SESSION PARAMETERS", "", input.base, + input.len)) { + std::cerr << "Unable to write TLS session to file" << std::endl; + } + + BIO_free(f); + + return 0; +} + +ptls_save_ticket_t save_ticket = {save_ticket_cb}; +} // namespace + +namespace { +ptls_key_exchange_algorithm_t *key_exchanges[] = { + &ptls_openssl_x25519, + &ptls_openssl_secp256r1, + &ptls_openssl_secp384r1, + &ptls_openssl_secp521r1, + nullptr, +}; +} // namespace + +namespace { +ptls_cipher_suite_t *cipher_suites[] = { + &ptls_openssl_aes128gcmsha256, + &ptls_openssl_aes256gcmsha384, + &ptls_openssl_chacha20poly1305sha256, + nullptr, +}; +} // namespace + +TLSClientContext::TLSClientContext() + : ctx_{ + .random_bytes = ptls_openssl_random_bytes, + .get_time = &ptls_get_time, + .key_exchanges = key_exchanges, + .cipher_suites = cipher_suites, + .require_dhe_on_psk = 1, + } {} + +TLSClientContext::~TLSClientContext() { + if (sign_cert_.key) { + ptls_openssl_dispose_sign_certificate(&sign_cert_); + } + + for (size_t i = 0; i < ctx_.certificates.count; ++i) { + free(ctx_.certificates.list[i].base); + } + free(ctx_.certificates.list); +} + +ptls_context_t *TLSClientContext::get_native_handle() { return &ctx_; } + +int TLSClientContext::init(const char *private_key_file, + const char *cert_file) { + if (ngtcp2_crypto_picotls_configure_client_context(&ctx_) != 0) { + std::cerr << "ngtcp2_crypto_picotls_configure_client_context failed" + << std::endl; + return -1; + } + + if (config.session_file) { + ctx_.save_ticket = &save_ticket; + } + + if (private_key_file && cert_file) { + if (ptls_load_certificates(&ctx_, cert_file) != 0) { + std::cerr << "ptls_load_certificates failed" << std::endl; + return -1; + } + + if (load_private_key(private_key_file) != 0) { + return -1; + } + } + + return 0; +} + +int TLSClientContext::load_private_key(const char *private_key_file) { + auto fp = fopen(private_key_file, "rb"); + if (fp == nullptr) { + std::cerr << "Could not open private key file " << private_key_file << ": " + << strerror(errno) << std::endl; + return -1; + } + + auto fp_d = defer(fclose, fp); + + auto pkey = PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr); + if (pkey == nullptr) { + std::cerr << "Could not read private key file " << private_key_file + << std::endl; + return -1; + } + + auto pkey_d = defer(EVP_PKEY_free, pkey); + + if (ptls_openssl_init_sign_certificate(&sign_cert_, pkey) != 0) { + std::cerr << "ptls_openssl_init_sign_certificate failed" << std::endl; + return -1; + } + + ctx_.sign_certificate = &sign_cert_.super; + + return 0; +} diff --git a/examples/tls_client_context_picotls.h b/examples/tls_client_context_picotls.h new file mode 100644 index 0000000..aada78e --- /dev/null +++ b/examples/tls_client_context_picotls.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_CONTEXT_PICOTLS_H +#define TLS_CLIENT_CONTEXT_PICOTLS_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <picotls.h> +#include <picotls/openssl.h> + +class TLSClientContext { +public: + TLSClientContext(); + ~TLSClientContext(); + + int init(const char *private_key_file, const char *cert_file); + + ptls_context_t *get_native_handle(); + + // TODO Implement keylog. + void enable_keylog() {} + +private: + int load_private_key(const char *private_key_file); + + ptls_context_t ctx_; + ptls_openssl_sign_certificate_t sign_cert_; +}; + +#endif // TLS_CLIENT_CONTEXT_PICOTLS_H diff --git a/examples/tls_client_context_wolfssl.cc b/examples/tls_client_context_wolfssl.cc new file mode 100644 index 0000000..0141df5 --- /dev/null +++ b/examples/tls_client_context_wolfssl.cc @@ -0,0 +1,177 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_context_wolfssl.h" + +#include <iostream> +#include <fstream> +#include <limits> + +#include <ngtcp2/ngtcp2_crypto_wolfssl.h> + +#include <wolfssl/options.h> +#include <wolfssl/ssl.h> + +#include "client_base.h" +#include "template.h" + +extern Config config; + +TLSClientContext::TLSClientContext() : ssl_ctx_{nullptr} {} + +TLSClientContext::~TLSClientContext() { + if (ssl_ctx_) { + wolfSSL_CTX_free(ssl_ctx_); + } +} + +WOLFSSL_CTX *TLSClientContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int new_session_cb(WOLFSSL *ssl, WOLFSSL_SESSION *session) { + std::cerr << "new_session_cb called" << std::endl; +#ifdef HAVE_SESSION_TICKET + if (wolfSSL_SESSION_get_max_early_data(session) != + std::numeric_limits<uint32_t>::max()) { + std::cerr << "max_early_data_size is not 0xffffffff" << std::endl; + } + + unsigned char sbuffer[16 * 1024], *data; + unsigned int sz; + sz = wolfSSL_i2d_SSL_SESSION(session, nullptr); + if (sz <= 0) { + std::cerr << "Could not export TLS session in " << config.session_file + << std::endl; + return 0; + } + if (static_cast<size_t>(sz) > sizeof(sbuffer)) { + std::cerr << "Exported TLS session too large" << std::endl; + return 0; + } + data = sbuffer; + sz = wolfSSL_i2d_SSL_SESSION(session, &data); + + auto f = wolfSSL_BIO_new_file(config.session_file, "w"); + if (f == nullptr) { + std::cerr << "Could not write TLS session in " << config.session_file + << std::endl; + return 0; + } + + auto f_d = defer(wolfSSL_BIO_free, f); + + if (!wolfSSL_PEM_write_bio(f, "WOLFSSL SESSION PARAMETERS", "", sbuffer, + sz)) { + std::cerr << "Unable to write TLS session to file" << std::endl; + return 0; + } + std::cerr << "new_session_cb: wrote " << sz << " of session data" + << std::endl; +#else + std::cerr << "TLS session tickets not enabled in wolfSSL " << std::endl; +#endif + return 0; +} +} // namespace + +int TLSClientContext::init(const char *private_key_file, + const char *cert_file) { + ssl_ctx_ = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); + if (!ssl_ctx_) { + std::cerr << "wolfSSL_CTX_new: " + << wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + if (ngtcp2_crypto_wolfssl_configure_client_context(ssl_ctx_) != 0) { + std::cerr << "ngtcp2_crypto_wolfssl_configure_client_context failed" + << std::endl; + return -1; + } + + if (wolfSSL_CTX_set_default_verify_paths(ssl_ctx_) == + WOLFSSL_NOT_IMPLEMENTED) { + /* hmm, not verifying the server cert for now */ + wolfSSL_CTX_set_verify(ssl_ctx_, WOLFSSL_VERIFY_NONE, 0); + } + + if (wolfSSL_CTX_set_cipher_list(ssl_ctx_, config.ciphers) != + WOLFSSL_SUCCESS) { + std::cerr << "wolfSSL_CTX_set_cipher_list: " + << wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + if (wolfSSL_CTX_set1_curves_list( + ssl_ctx_, const_cast<char *>(config.groups)) != WOLFSSL_SUCCESS) { + std::cerr << "wolfSSL_CTX_set1_curves_list(" << config.groups << ") failed" + << std::endl; + return -1; + } + + if (private_key_file && cert_file) { + if (wolfSSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != WOLFSSL_SUCCESS) { + std::cerr << "wolfSSL_CTX_use_PrivateKey_file: " + << wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + if (wolfSSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != + WOLFSSL_SUCCESS) { + std::cerr << "wolfSSL_CTX_use_certificate_chain_file: " + << wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr) + << std::endl; + return -1; + } + } + + if (config.session_file) { + wolfSSL_CTX_UseSessionTicket(ssl_ctx_); + wolfSSL_CTX_sess_set_new_cb(ssl_ctx_, new_session_cb); + } + + return 0; +} + +extern std::ofstream keylog_file; + +#ifdef HAVE_SECRET_CALLBACK +namespace { +void keylog_callback(const WOLFSSL *ssl, const char *line) { + keylog_file.write(line, strlen(line)); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace +#endif + +void TLSClientContext::enable_keylog() { +#ifdef HAVE_SECRET_CALLBACK + wolfSSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +#endif +} diff --git a/examples/tls_client_context_wolfssl.h b/examples/tls_client_context_wolfssl.h new file mode 100644 index 0000000..f2ed14e --- /dev/null +++ b/examples/tls_client_context_wolfssl.h @@ -0,0 +1,51 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_CONTEXT_WOLFSSL_H +#define TLS_CLIENT_CONTEXT_WOLFSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <wolfssl/options.h> +#include <wolfssl/ssl.h> +#include <wolfssl/quic.h> + +class TLSClientContext { +public: + TLSClientContext(); + ~TLSClientContext(); + + int init(const char *private_key_file, const char *cert_file); + + WOLFSSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + WOLFSSL_CTX *ssl_ctx_; +}; + +#endif // TLS_CLIENT_CONTEXT_WOLFSSL_H diff --git a/examples/tls_client_session.h b/examples/tls_client_session.h new file mode 100644 index 0000000..e4fd0f2 --- /dev/null +++ b/examples/tls_client_session.h @@ -0,0 +1,52 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_SESSION_H +#define TLS_CLIENT_SESSION_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#if defined(ENABLE_EXAMPLE_OPENSSL) && defined(WITH_EXAMPLE_OPENSSL) +# include "tls_client_session_openssl.h" +#endif // ENABLE_EXAMPLE_OPENSSL && WITH_EXAMPLE_OPENSSL + +#if defined(ENABLE_EXAMPLE_GNUTLS) && defined(WITH_EXAMPLE_GNUTLS) +# include "tls_client_session_gnutls.h" +#endif // ENABLE_EXAMPLE_GNUTLS && WITH_EXAMPLE_GNUTLS + +#if defined(ENABLE_EXAMPLE_BORINGSSL) && defined(WITH_EXAMPLE_BORINGSSL) +# include "tls_client_session_boringssl.h" +#endif // ENABLE_EXAMPLE_BORINGSSL && WITH_EXAMPLE_BORINGSSL + +#if defined(ENABLE_EXAMPLE_PICOTLS) && defined(WITH_EXAMPLE_PICOTLS) +# include "tls_client_session_picotls.h" +#endif // ENABLE_EXAMPLE_PICOTLS && WITH_EXAMPLE_PICOTLS + +#if defined(ENABLE_EXAMPLE_WOLFSSL) && defined(WITH_EXAMPLE_WOLFSSL) +# include "tls_client_session_wolfssl.h" +#endif // ENABLE_EXAMPLE_WOLFSSL && WITH_EXAMPLE_WOLFSSL + +#endif // TLS_CLIENT_SESSION_H diff --git a/examples/tls_client_session_boringssl.cc b/examples/tls_client_session_boringssl.cc new file mode 100644 index 0000000..95b9834 --- /dev/null +++ b/examples/tls_client_session_boringssl.cc @@ -0,0 +1,110 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_session_boringssl.h" + +#include <cassert> +#include <iostream> + +#include "tls_client_context_boringssl.h" +#include "client_base.h" +#include "template.h" +#include "util.h" + +TLSClientSession::TLSClientSession() {} + +TLSClientSession::~TLSClientSession() {} + +extern Config config; + +int TLSClientSession::init(bool &early_data_enabled, + const TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, + uint32_t quic_version, AppProtocol app_proto) { + early_data_enabled = false; + + auto ssl_ctx = tls_ctx.get_native_handle(); + + ssl_ = SSL_new(ssl_ctx); + if (!ssl_) { + std::cerr << "SSL_new: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + SSL_set_app_data(ssl_, client->conn_ref()); + SSL_set_connect_state(ssl_); + + SSL_set_quic_use_legacy_codepoint(ssl_, + (quic_version & 0xff000000) == 0xff000000); + + switch (app_proto) { + case AppProtocol::H3: + SSL_set_alpn_protos(ssl_, H3_ALPN, str_size(H3_ALPN)); + break; + case AppProtocol::HQ: + SSL_set_alpn_protos(ssl_, HQ_ALPN, str_size(HQ_ALPN)); + break; + } + + if (!config.sni.empty()) { + SSL_set_tlsext_host_name(ssl_, config.sni.data()); + } else if (util::numeric_host(remote_addr)) { + // If remote host is numeric address, just send "localhost" as SNI + // for now. + SSL_set_tlsext_host_name(ssl_, "localhost"); + } else { + SSL_set_tlsext_host_name(ssl_, remote_addr); + } + + if (config.session_file) { + auto f = BIO_new_file(config.session_file, "r"); + if (f == nullptr) { + std::cerr << "Could not read TLS session file " << config.session_file + << std::endl; + } else { + auto session = PEM_read_bio_SSL_SESSION(f, nullptr, 0, nullptr); + BIO_free(f); + if (session == nullptr) { + std::cerr << "Could not read TLS session file " << config.session_file + << std::endl; + } else { + if (!SSL_set_session(ssl_, session)) { + std::cerr << "Could not set session" << std::endl; + } else if (!config.disable_early_data && + SSL_SESSION_early_data_capable(session)) { + early_data_enabled = true; + SSL_set_early_data_enabled(ssl_, 1); + } + SSL_SESSION_free(session); + } + } + } + + return 0; +} + +bool TLSClientSession::get_early_data_accepted() const { + return SSL_early_data_accepted(ssl_); +} diff --git a/examples/tls_client_session_boringssl.h b/examples/tls_client_session_boringssl.h new file mode 100644 index 0000000..27ce9ab --- /dev/null +++ b/examples/tls_client_session_boringssl.h @@ -0,0 +1,52 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_SESSION_BORINGSSL_H +#define TLS_CLIENT_SESSION_BORINGSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include "tls_session_base_openssl.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext; +class ClientBase; + +class TLSClientSession : public TLSSessionBase { +public: + TLSClientSession(); + ~TLSClientSession(); + + int init(bool &early_data_enabled, const TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, uint32_t quic_version, + AppProtocol app_proto); + + bool get_early_data_accepted() const; +}; + +#endif // TLS_CLIENT_SESSION_BORINGSSL_H diff --git a/examples/tls_client_session_gnutls.cc b/examples/tls_client_session_gnutls.cc new file mode 100644 index 0000000..c77394f --- /dev/null +++ b/examples/tls_client_session_gnutls.cc @@ -0,0 +1,190 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_session_gnutls.h" + +#include <iostream> +#include <fstream> +#include <array> + +#include <ngtcp2/ngtcp2_crypto_gnutls.h> + +#include <gnutls/crypto.h> + +#include "tls_client_context_gnutls.h" +#include "client_base.h" +#include "template.h" +#include "util.h" + +// Based on https://github.com/ueno/ngtcp2-gnutls-examples + +extern Config config; + +TLSClientSession::TLSClientSession() {} + +TLSClientSession::~TLSClientSession() {} + +namespace { +int hook_func(gnutls_session_t session, unsigned int htype, unsigned when, + unsigned int incoming, const gnutls_datum_t *msg) { + if (config.session_file && htype == GNUTLS_HANDSHAKE_NEW_SESSION_TICKET) { + gnutls_datum_t data; + if (auto rv = gnutls_session_get_data2(session, &data); rv != 0) { + std::cerr << "gnutls_session_get_data2 failed: " << gnutls_strerror(rv) + << std::endl; + return rv; + } + auto f = std::ofstream(config.session_file); + if (!f) { + return -1; + } + + gnutls_datum_t d; + if (auto rv = + gnutls_pem_base64_encode2("GNUTLS SESSION PARAMETERS", &data, &d); + rv < 0) { + std::cerr << "Could not encode session in " << config.session_file + << std::endl; + return -1; + } + + f.write(reinterpret_cast<const char *>(d.data), d.size); + if (!f) { + std::cerr << "Unable to write TLS session to file" << std::endl; + } + gnutls_free(d.data); + gnutls_free(data.data); + } + + return 0; +} +} // namespace + +int TLSClientSession::init(bool &early_data_enabled, + const TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, + uint32_t quic_version, AppProtocol app_proto) { + early_data_enabled = false; + + if (auto rv = + gnutls_init(&session_, GNUTLS_CLIENT | GNUTLS_ENABLE_EARLY_DATA | + GNUTLS_NO_END_OF_EARLY_DATA); + rv != 0) { + std::cerr << "gnutls_init failed: " << gnutls_strerror(rv) << std::endl; + return -1; + } + + std::string priority = "%DISABLE_TLS13_COMPAT_MODE:"; + priority += config.ciphers; + priority += ':'; + priority += config.groups; + + if (auto rv = gnutls_priority_set_direct(session_, priority.c_str(), nullptr); + rv != 0) { + std::cerr << "gnutls_priority_set_direct failed: " << gnutls_strerror(rv) + << std::endl; + return -1; + } + + gnutls_handshake_set_hook_function(session_, GNUTLS_HANDSHAKE_ANY, + GNUTLS_HOOK_POST, hook_func); + + if (ngtcp2_crypto_gnutls_configure_client_session(session_) != 0) { + std::cerr << "ngtcp2_crypto_gnutls_configure_client_session failed" + << std::endl; + return -1; + } + + if (config.session_file) { + auto f = std::ifstream(config.session_file); + if (f) { + f.seekg(0, std::ios::end); + auto pos = f.tellg(); + std::vector<char> content(pos); + f.seekg(0, std::ios::beg); + f.read(content.data(), pos); + + gnutls_datum_t s{ + .data = reinterpret_cast<unsigned char *>(content.data()), + .size = static_cast<unsigned int>(content.size()), + }; + + gnutls_datum_t d; + if (auto rv = + gnutls_pem_base64_decode2("GNUTLS SESSION PARAMETERS", &s, &d); + rv < 0) { + std::cerr << "Could not read session in " << config.session_file + << std::endl; + return -1; + } + + auto d_d = defer(gnutls_free, d.data); + + if (auto rv = gnutls_session_set_data(session_, d.data, d.size); + rv != 0) { + std::cerr << "gnutls_session_set_data failed: " << gnutls_strerror(rv) + << std::endl; + return -1; + } + + if (!config.disable_early_data) { + early_data_enabled = true; + } + } + } + + gnutls_session_set_ptr(session_, client->conn_ref()); + + if (auto rv = gnutls_credentials_set(session_, GNUTLS_CRD_CERTIFICATE, + tls_ctx.get_native_handle()); + rv != 0) { + std::cerr << "gnutls_credentials_set failed: " << gnutls_strerror(rv) + << std::endl; + return -1; + } + + // strip the first byte from H3_ALPN_V1 + gnutls_datum_t alpn{ + .data = const_cast<uint8_t *>(&H3_ALPN_V1[1]), + .size = H3_ALPN_V1[0], + }; + + gnutls_alpn_set_protocols(session_, &alpn, 1, GNUTLS_ALPN_MANDATORY); + + if (util::numeric_host(remote_addr)) { + // If remote host is numeric address, just send "localhost" as SNI + // for now. + gnutls_server_name_set(session_, GNUTLS_NAME_DNS, "localhost", + strlen("localhost")); + } else { + gnutls_server_name_set(session_, GNUTLS_NAME_DNS, remote_addr, + strlen(remote_addr)); + } + + return 0; +} + +bool TLSClientSession::get_early_data_accepted() const { + return gnutls_session_get_flags(session_) & GNUTLS_SFLAGS_EARLY_DATA; +} diff --git a/examples/tls_client_session_gnutls.h b/examples/tls_client_session_gnutls.h new file mode 100644 index 0000000..a76db49 --- /dev/null +++ b/examples/tls_client_session_gnutls.h @@ -0,0 +1,52 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_SESSION_GNUTLS_H +#define TLS_CLIENT_SESSION_GNUTLS_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include "tls_session_base_gnutls.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext; +class ClientBase; + +class TLSClientSession : public TLSSessionBase { +public: + TLSClientSession(); + ~TLSClientSession(); + + int init(bool &early_data_enabled, const TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, uint32_t quic_version, + AppProtocol app_proto); + + bool get_early_data_accepted() const; +}; + +#endif // TLS_CLIENT_SESSION_GNUTLS_H diff --git a/examples/tls_client_session_openssl.cc b/examples/tls_client_session_openssl.cc new file mode 100644 index 0000000..dd6bb5d --- /dev/null +++ b/examples/tls_client_session_openssl.cc @@ -0,0 +1,113 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_session_openssl.h" + +#include <cassert> +#include <iostream> + +#include <openssl/err.h> + +#include "tls_client_context_openssl.h" +#include "client_base.h" +#include "template.h" +#include "util.h" + +TLSClientSession::TLSClientSession() {} + +TLSClientSession::~TLSClientSession() {} + +extern Config config; + +int TLSClientSession::init(bool &early_data_enabled, + const TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, + uint32_t quic_version, AppProtocol app_proto) { + early_data_enabled = false; + + auto ssl_ctx = tls_ctx.get_native_handle(); + + ssl_ = SSL_new(ssl_ctx); + if (!ssl_) { + std::cerr << "SSL_new: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + SSL_set_app_data(ssl_, client->conn_ref()); + SSL_set_connect_state(ssl_); + + SSL_set_quic_use_legacy_codepoint(ssl_, + (quic_version & 0xff000000) == 0xff000000); + + switch (app_proto) { + case AppProtocol::H3: + SSL_set_alpn_protos(ssl_, H3_ALPN, str_size(H3_ALPN)); + break; + case AppProtocol::HQ: + SSL_set_alpn_protos(ssl_, HQ_ALPN, str_size(HQ_ALPN)); + break; + } + + if (!config.sni.empty()) { + SSL_set_tlsext_host_name(ssl_, config.sni.data()); + } else if (util::numeric_host(remote_addr)) { + // If remote host is numeric address, just send "localhost" as SNI + // for now. + SSL_set_tlsext_host_name(ssl_, "localhost"); + } else { + SSL_set_tlsext_host_name(ssl_, remote_addr); + } + + if (config.session_file) { + auto f = BIO_new_file(config.session_file, "r"); + if (f == nullptr) { + std::cerr << "Could not read TLS session file " << config.session_file + << std::endl; + } else { + auto session = PEM_read_bio_SSL_SESSION(f, nullptr, 0, nullptr); + BIO_free(f); + if (session == nullptr) { + std::cerr << "Could not read TLS session file " << config.session_file + << std::endl; + } else { + if (!SSL_set_session(ssl_, session)) { + std::cerr << "Could not set session" << std::endl; + } else if (!config.disable_early_data && + SSL_SESSION_get_max_early_data(session)) { + early_data_enabled = true; + SSL_set_quic_early_data_enabled(ssl_, 1); + } + SSL_SESSION_free(session); + } + } + } + + return 0; +} + +bool TLSClientSession::get_early_data_accepted() const { + // SSL_get_early_data_status works after handshake completes. + return SSL_get_early_data_status(ssl_) == SSL_EARLY_DATA_ACCEPTED; +} diff --git a/examples/tls_client_session_openssl.h b/examples/tls_client_session_openssl.h new file mode 100644 index 0000000..06ecb41 --- /dev/null +++ b/examples/tls_client_session_openssl.h @@ -0,0 +1,52 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_SESSION_OPENSSL_H +#define TLS_CLIENT_SESSION_OPENSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include "tls_session_base_openssl.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext; +class ClientBase; + +class TLSClientSession : public TLSSessionBase { +public: + TLSClientSession(); + ~TLSClientSession(); + + int init(bool &early_data_enabled, const TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, uint32_t quic_version, + AppProtocol app_proto); + + bool get_early_data_accepted() const; +}; + +#endif // TLS_CLIENT_SESSION_OPENSSL_H diff --git a/examples/tls_client_session_picotls.cc b/examples/tls_client_session_picotls.cc new file mode 100644 index 0000000..8f5bdfc --- /dev/null +++ b/examples/tls_client_session_picotls.cc @@ -0,0 +1,147 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_session_picotls.h" + +#include <iostream> +#include <memory> + +#include <ngtcp2/ngtcp2_crypto_picotls.h> + +#include <openssl/bio.h> +#include <openssl/pem.h> + +#include <picotls.h> + +#include "tls_client_context_picotls.h" +#include "client_base.h" +#include "template.h" +#include "util.h" + +using namespace std::literals; + +extern Config config; + +TLSClientSession::TLSClientSession() {} + +TLSClientSession::~TLSClientSession() { + auto &hsprops = cptls_.handshake_properties; + + delete[] hsprops.client.session_ticket.base; +} + +namespace { +auto negotiated_protocols = std::array<ptls_iovec_t, 1>{{ + { + .base = const_cast<uint8_t *>(&H3_ALPN_V1[1]), + .len = H3_ALPN_V1[0], + }, +}}; +} // namespace + +int TLSClientSession::init(bool &early_data_enabled, TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, + uint32_t quic_version, AppProtocol app_proto) { + cptls_.ptls = ptls_client_new(tls_ctx.get_native_handle()); + if (!cptls_.ptls) { + std::cerr << "ptls_client_new failed" << std::endl; + return -1; + } + + *ptls_get_data_ptr(cptls_.ptls) = client->conn_ref(); + + auto conn = client->conn(); + auto &hsprops = cptls_.handshake_properties; + + hsprops.additional_extensions = new ptls_raw_extension_t[2]{ + { + .type = UINT16_MAX, + }, + { + .type = UINT16_MAX, + }, + }; + + if (ngtcp2_crypto_picotls_configure_client_session(&cptls_, conn) != 0) { + std::cerr << "ngtcp2_crypto_picotls_configure_client_session failed" + << std::endl; + return -1; + } + + hsprops.client.negotiated_protocols.list = negotiated_protocols.data(); + hsprops.client.negotiated_protocols.count = negotiated_protocols.size(); + + if (util::numeric_host(remote_addr)) { + // If remote host is numeric address, just send "localhost" as SNI + // for now. + ptls_set_server_name(cptls_.ptls, "localhost", strlen("localhost")); + } else { + ptls_set_server_name(cptls_.ptls, remote_addr, strlen(remote_addr)); + } + + if (config.session_file) { + auto f = BIO_new_file(config.session_file, "r"); + if (f == nullptr) { + std::cerr << "Could not read TLS session file " << config.session_file + << std::endl; + } else { + auto f_d = defer(BIO_free, f); + + char *name, *header; + unsigned char *data; + long datalen; + + if (PEM_read_bio(f, &name, &header, &data, &datalen) != 1) { + std::cerr << "Could not read TLS session file " << config.session_file + << std::endl; + } else { + if ("PICOTLS SESSION PARAMETERS"sv != name) { + std::cerr << "TLS session file contains unexpected name: " << name + << std::endl; + } else { + hsprops.client.session_ticket.base = new uint8_t[datalen]; + hsprops.client.session_ticket.len = datalen; + memcpy(hsprops.client.session_ticket.base, data, datalen); + + if (!config.disable_early_data) { + // No easy way to check max_early_data from ticket. We + // need to run ptls_handle_message. + early_data_enabled = true; + } + } + + OPENSSL_free(name); + OPENSSL_free(header); + OPENSSL_free(data); + } + } + } + + return 0; +} + +bool TLSClientSession::get_early_data_accepted() const { + return cptls_.handshake_properties.client.early_data_acceptance == + PTLS_EARLY_DATA_ACCEPTED; +} diff --git a/examples/tls_client_session_picotls.h b/examples/tls_client_session_picotls.h new file mode 100644 index 0000000..75a376e --- /dev/null +++ b/examples/tls_client_session_picotls.h @@ -0,0 +1,52 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_SESSION_PICOTLS_H +#define TLS_CLIENT_SESSION_PICOTLS_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include "tls_session_base_picotls.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext; +class ClientBase; + +class TLSClientSession : public TLSSessionBase { +public: + TLSClientSession(); + ~TLSClientSession(); + + int init(bool &early_data_enabled, TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, uint32_t quic_version, + AppProtocol app_proto); + + bool get_early_data_accepted() const; +}; + +#endif // TLS_CLIENT_SESSION_PICOTLS_H diff --git a/examples/tls_client_session_wolfssl.cc b/examples/tls_client_session_wolfssl.cc new file mode 100644 index 0000000..87fb809 --- /dev/null +++ b/examples/tls_client_session_wolfssl.cc @@ -0,0 +1,160 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_client_session_wolfssl.h" + +#include <cassert> +#include <iostream> + +#include "tls_client_context_wolfssl.h" +#include "client_base.h" +#include "template.h" +#include "util.h" + +using namespace std::literals; + +TLSClientSession::TLSClientSession() {} + +TLSClientSession::~TLSClientSession() {} + +extern Config config; + +namespace { +int wolfssl_session_ticket_cb(WOLFSSL *ssl, const unsigned char *ticket, + int ticketSz, void *cb_ctx) { + std::cerr << "session ticket calback invoked" << std::endl; + return 0; +} +} // namespace + +int TLSClientSession::init(bool &early_data_enabled, + const TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, + uint32_t quic_version, AppProtocol app_proto) { + early_data_enabled = false; + + auto ssl_ctx = tls_ctx.get_native_handle(); + + ssl_ = wolfSSL_new(ssl_ctx); + if (!ssl_) { + std::cerr << "wolfSSL_new: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + wolfSSL_set_app_data(ssl_, client->conn_ref()); + wolfSSL_set_connect_state(ssl_); + + wolfSSL_set_quic_use_legacy_codepoint(ssl_, (quic_version & 0xff000000) == + 0xff000000); + + switch (app_proto) { + case AppProtocol::H3: + wolfSSL_set_alpn_protos(ssl_, H3_ALPN, str_size(H3_ALPN)); + break; + case AppProtocol::HQ: + wolfSSL_set_alpn_protos(ssl_, HQ_ALPN, str_size(HQ_ALPN)); + break; + } + + if (!config.sni.empty()) { + wolfSSL_UseSNI(ssl_, WOLFSSL_SNI_HOST_NAME, config.sni.data(), + config.sni.length()); + } else if (util::numeric_host(remote_addr)) { + // If remote host is numeric address, just send "localhost" as SNI + // for now. + wolfSSL_UseSNI(ssl_, WOLFSSL_SNI_HOST_NAME, "localhost", + sizeof("localhost") - 1); + } else { + wolfSSL_UseSNI(ssl_, WOLFSSL_SNI_HOST_NAME, remote_addr, + strlen(remote_addr)); + } + + if (config.session_file) { +#ifdef HAVE_SESSION_TICKET + auto f = wolfSSL_BIO_new_file(config.session_file, "r"); + if (f == nullptr) { + std::cerr << "Could not open TLS session file " << config.session_file + << std::endl; + } else { + char *name, *header; + unsigned char *data; + const unsigned char *pdata; + long datalen; + unsigned int ret; + WOLFSSL_SESSION *session; + + if (wolfSSL_PEM_read_bio(f, &name, &header, &data, &datalen) != 1) { + std::cerr << "Could not read TLS session file " << config.session_file + << std::endl; + } else { + if ("WOLFSSL SESSION PARAMETERS"sv != name) { + std::cerr << "TLS session file contains unexpected name: " << name + << std::endl; + } else { + pdata = data; + session = wolfSSL_d2i_SSL_SESSION(nullptr, &pdata, datalen); + if (session == nullptr) { + std::cerr << "Could not parse TLS session from file " + << config.session_file << std::endl; + } else { + ret = wolfSSL_set_session(ssl_, session); + if (ret != WOLFSSL_SUCCESS) { + std::cerr << "Could not install TLS session from file " + << config.session_file << std::endl; + } else { + if (!config.disable_early_data && + wolfSSL_SESSION_get_max_early_data(session)) { + early_data_enabled = true; + wolfSSL_set_quic_early_data_enabled(ssl_, 1); + } + } + wolfSSL_SESSION_free(session); + } + } + + wolfSSL_OPENSSL_free(name); + wolfSSL_OPENSSL_free(header); + wolfSSL_OPENSSL_free(data); + } + wolfSSL_BIO_free(f); + } + wolfSSL_UseSessionTicket(ssl_); + wolfSSL_set_SessionTicket_cb(ssl_, wolfssl_session_ticket_cb, nullptr); +#else + std::cerr << "TLS session im-/export not enabled in wolfSSL" << std::endl; +#endif + } + + return 0; +} + +bool TLSClientSession::get_early_data_accepted() const { + // wolfSSL_get_early_data_status works after handshake completes. +#ifdef WOLFSSL_EARLY_DATA + return wolfSSL_get_early_data_status(ssl_) == SSL_EARLY_DATA_ACCEPTED; +#else + return 0; +#endif +} diff --git a/examples/tls_client_session_wolfssl.h b/examples/tls_client_session_wolfssl.h new file mode 100644 index 0000000..1686e14 --- /dev/null +++ b/examples/tls_client_session_wolfssl.h @@ -0,0 +1,52 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_CLIENT_SESSION_WOLFSSL_H +#define TLS_CLIENT_SESSION_WOLFSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include "tls_session_base_wolfssl.h" +#include "shared.h" + +using namespace ngtcp2; + +class TLSClientContext; +class ClientBase; + +class TLSClientSession : public TLSSessionBase { +public: + TLSClientSession(); + ~TLSClientSession(); + + int init(bool &early_data_enabled, const TLSClientContext &tls_ctx, + const char *remote_addr, ClientBase *client, uint32_t quic_version, + AppProtocol app_proto); + + bool get_early_data_accepted() const; +}; + +#endif // TLS_CLIENT_SESSION_WOLFSSL_H diff --git a/examples/tls_server_context.h b/examples/tls_server_context.h new file mode 100644 index 0000000..1f4c574 --- /dev/null +++ b/examples/tls_server_context.h @@ -0,0 +1,52 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_CONTEXT_H +#define TLS_SERVER_CONTEXT_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#if defined(ENABLE_EXAMPLE_OPENSSL) && defined(WITH_EXAMPLE_OPENSSL) +# include "tls_server_context_openssl.h" +#endif // ENABLE_EXAMPLE_OPENSSL && WITH_EXAMPLE_OPENSSL + +#if defined(ENABLE_EXAMPLE_GNUTLS) && defined(WITH_EXAMPLE_GNUTLS) +# include "tls_server_context_gnutls.h" +#endif // ENABLE_EXAMPLE_GNUTLS && WITH_EXAMPLE_GNUTLS + +#if defined(ENABLE_EXAMPLE_BORINGSSL) && defined(WITH_EXAMPLE_BORINGSSL) +# include "tls_server_context_boringssl.h" +#endif // ENABLE_EXAMPLE_BORINGSSL && WITH_EXAMPLE_BORINGSSL + +#if defined(ENABLE_EXAMPLE_PICOTLS) && defined(WITH_EXAMPLE_PICOTLS) +# include "tls_server_context_picotls.h" +#endif // ENABLE_EXAMPLE_PICOTLS && WITH_EXAMPLE_PICOTLS + +#if defined(ENABLE_EXAMPLE_WOLFSSL) && defined(WITH_EXAMPLE_WOLFSSL) +# include "tls_server_context_wolfssl.h" +#endif // ENABLE_EXAMPLE_WOLFSSL && WITH_EXAMPLE_WOLFSSL + +#endif // TLS_SERVER_CONTEXT_H diff --git a/examples/tls_server_context_boringssl.cc b/examples/tls_server_context_boringssl.cc new file mode 100644 index 0000000..9b95583 --- /dev/null +++ b/examples/tls_server_context_boringssl.cc @@ -0,0 +1,257 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_context_boringssl.h" + +#include <iostream> +#include <fstream> + +#include <ngtcp2/ngtcp2_crypto_boringssl.h> + +#include <openssl/err.h> + +#include "server_base.h" +#include "template.h" + +extern Config config; + +TLSServerContext::TLSServerContext() : ssl_ctx_{nullptr} {} + +TLSServerContext::~TLSServerContext() { + if (ssl_ctx_) { + SSL_CTX_free(ssl_ctx_); + } +} + +SSL_CTX *TLSServerContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int alpn_select_proto_h3_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = static_cast<ngtcp2_crypto_conn_ref *>(SSL_get_app_data(ssl)); + auto h = static_cast<HandlerBase *>(conn_ref->user_data); + const uint8_t *alpn; + size_t alpnlen; + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version(h->conn()); + + switch (version) { + case QUIC_VER_DRAFT29: + alpn = H3_ALPN_DRAFT29; + alpnlen = str_size(H3_ALPN_DRAFT29); + break; + case QUIC_VER_DRAFT30: + alpn = H3_ALPN_DRAFT30; + alpnlen = str_size(H3_ALPN_DRAFT30); + break; + case QUIC_VER_DRAFT31: + alpn = H3_ALPN_DRAFT31; + alpnlen = str_size(H3_ALPN_DRAFT31); + break; + case QUIC_VER_DRAFT32: + alpn = H3_ALPN_DRAFT32; + alpnlen = str_size(H3_ALPN_DRAFT32); + break; + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2_DRAFT: + alpn = H3_ALPN_V1; + alpnlen = str_size(H3_ALPN_V1); + break; + default: + if (!config.quiet) { + std::cerr << "Unexpected quic protocol version: " << std::hex << "0x" + << version << std::dec << std::endl; + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto p = in, end = in + inlen; p + alpnlen <= end; p += *p + 1) { + if (std::equal(alpn, alpn + alpnlen, p)) { + *out = p + 1; + *outlen = *p; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::cerr << "Client did not present ALPN " << &alpn[1] << std::endl; + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int alpn_select_proto_hq_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = static_cast<ngtcp2_crypto_conn_ref *>(SSL_get_app_data(ssl)); + auto h = static_cast<HandlerBase *>(conn_ref->user_data); + const uint8_t *alpn; + size_t alpnlen; + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version(h->conn()); + + switch (version) { + case QUIC_VER_DRAFT29: + alpn = HQ_ALPN_DRAFT29; + alpnlen = str_size(HQ_ALPN_DRAFT29); + break; + case QUIC_VER_DRAFT30: + alpn = HQ_ALPN_DRAFT30; + alpnlen = str_size(HQ_ALPN_DRAFT30); + break; + case QUIC_VER_DRAFT31: + alpn = HQ_ALPN_DRAFT31; + alpnlen = str_size(HQ_ALPN_DRAFT31); + break; + case QUIC_VER_DRAFT32: + alpn = HQ_ALPN_DRAFT32; + alpnlen = str_size(HQ_ALPN_DRAFT32); + break; + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2_DRAFT: + alpn = HQ_ALPN_V1; + alpnlen = str_size(HQ_ALPN_V1); + break; + default: + if (!config.quiet) { + std::cerr << "Unexpected quic protocol version: " << std::hex << "0x" + << version << std::dec << std::endl; + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto p = in, end = in + inlen; p + alpnlen <= end; p += *p + 1) { + if (std::equal(alpn, alpn + alpnlen, p)) { + *out = p + 1; + *outlen = *p; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::cerr << "Client did not present ALPN " << &alpn[1] << std::endl; + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int verify_cb(int preverify_ok, X509_STORE_CTX *ctx) { + // We don't verify the client certificate. Just request it for the + // testing purpose. + return 1; +} +} // namespace + +int TLSServerContext::init(const char *private_key_file, const char *cert_file, + AppProtocol app_proto) { + constexpr static unsigned char sid_ctx[] = "ngtcp2 server"; + + ssl_ctx_ = SSL_CTX_new(TLS_server_method()); + if (!ssl_ctx_) { + std::cerr << "SSL_CTX_new: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + constexpr auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_SINGLE_ECDH_USE | + SSL_OP_CIPHER_SERVER_PREFERENCE; + + SSL_CTX_set_options(ssl_ctx_, ssl_opts); + + if (SSL_CTX_set1_curves_list(ssl_ctx_, config.groups) != 1) { + std::cerr << "SSL_CTX_set1_curves_list failed" << std::endl; + return -1; + } + + SSL_CTX_set_mode(ssl_ctx_, SSL_MODE_RELEASE_BUFFERS); + + if (ngtcp2_crypto_boringssl_configure_server_context(ssl_ctx_) != 0) { + std::cerr << "ngtcp2_crypto_boringssl_configure_server_context failed" + << std::endl; + return -1; + } + + switch (app_proto) { + case AppProtocol::H3: + SSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_h3_cb, nullptr); + break; + case AppProtocol::HQ: + SSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_hq_cb, nullptr); + break; + } + + SSL_CTX_set_default_verify_paths(ssl_ctx_); + + if (SSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != 1) { + std::cerr << "SSL_CTX_use_PrivateKey_file: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + if (SSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != 1) { + std::cerr << "SSL_CTX_use_certificate_chain_file: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + if (SSL_CTX_check_private_key(ssl_ctx_) != 1) { + std::cerr << "SSL_CTX_check_private_key: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + SSL_CTX_set_session_id_context(ssl_ctx_, sid_ctx, sizeof(sid_ctx) - 1); + + if (config.verify_client) { + SSL_CTX_set_verify(ssl_ctx_, + SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_cb); + } + + return 0; +} + +extern std::ofstream keylog_file; + +namespace { +void keylog_callback(const SSL *ssl, const char *line) { + keylog_file.write(line, strlen(line)); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace + +void TLSServerContext::enable_keylog() { + SSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +} diff --git a/examples/tls_server_context_boringssl.h b/examples/tls_server_context_boringssl.h new file mode 100644 index 0000000..d7d3dfb --- /dev/null +++ b/examples/tls_server_context_boringssl.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_CONTEXT_BORINGSSL_H +#define TLS_SERVER_CONTEXT_BORINGSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <openssl/ssl.h> + +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext { +public: + TLSServerContext(); + ~TLSServerContext(); + + int init(const char *private_key_file, const char *cert_file, + AppProtocol app_proto); + + SSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + SSL_CTX *ssl_ctx_; +}; + +#endif // TLS_SERVER_CONTEXT_BORINGSSL_H diff --git a/examples/tls_server_context_gnutls.cc b/examples/tls_server_context_gnutls.cc new file mode 100644 index 0000000..5b2be55 --- /dev/null +++ b/examples/tls_server_context_gnutls.cc @@ -0,0 +1,99 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_context_gnutls.h" + +#include <iostream> + +#include "server_base.h" +#include "template.h" + +// Based on https://github.com/ueno/ngtcp2-gnutls-examples + +extern Config config; + +namespace { +int anti_replay_db_add_func(void *dbf, time_t exp_time, + const gnutls_datum_t *key, + const gnutls_datum_t *data) { + return 0; +} +} // namespace + +TLSServerContext::TLSServerContext() : cred_{nullptr}, session_ticket_key_{} { + gnutls_anti_replay_init(&anti_replay_); + gnutls_anti_replay_set_add_function(anti_replay_, anti_replay_db_add_func); + gnutls_anti_replay_set_ptr(anti_replay_, nullptr); +} + +TLSServerContext::~TLSServerContext() { + gnutls_anti_replay_deinit(anti_replay_); + gnutls_free(session_ticket_key_.data); + gnutls_certificate_free_credentials(cred_); +} + +gnutls_certificate_credentials_t +TLSServerContext::get_certificate_credentials() const { + return cred_; +} + +const gnutls_datum_t *TLSServerContext::get_session_ticket_key() const { + return &session_ticket_key_; +} + +gnutls_anti_replay_t TLSServerContext::get_anti_replay() const { + return anti_replay_; +} + +int TLSServerContext::init(const char *private_key_file, const char *cert_file, + AppProtocol app_proto) { + if (auto rv = gnutls_certificate_allocate_credentials(&cred_); rv != 0) { + std::cerr << "gnutls_certificate_allocate_credentials failed: " + << gnutls_strerror(rv) << std::endl; + return -1; + } + + if (auto rv = gnutls_certificate_set_x509_system_trust(cred_); rv < 0) { + std::cerr << "gnutls_certificate_set_x509_system_trust failed: " + << gnutls_strerror(rv) << std::endl; + return -1; + } + + if (auto rv = gnutls_certificate_set_x509_key_file( + cred_, cert_file, private_key_file, GNUTLS_X509_FMT_PEM); + rv != 0) { + std::cerr << "gnutls_certificate_set_x509_key_file failed: " + << gnutls_strerror(rv) << std::endl; + return -1; + } + + if (auto rv = gnutls_session_ticket_key_generate(&session_ticket_key_); + rv != 0) { + std::cerr << "gnutls_session_ticket_key_generate failed: " + << gnutls_strerror(rv) << std::endl; + return -1; + } + + return 0; +} diff --git a/examples/tls_server_context_gnutls.h b/examples/tls_server_context_gnutls.h new file mode 100644 index 0000000..21ed109 --- /dev/null +++ b/examples/tls_server_context_gnutls.h @@ -0,0 +1,59 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_CONTEXT_GNUTLS_H +#define TLS_SERVER_CONTEXT_GNUTLS_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <gnutls/gnutls.h> + +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext { +public: + TLSServerContext(); + ~TLSServerContext(); + + int init(const char *private_key_file, const char *cert_file, + AppProtocol app_proto); + + gnutls_certificate_credentials_t get_certificate_credentials() const; + const gnutls_datum_t *get_session_ticket_key() const; + gnutls_anti_replay_t get_anti_replay() const; + + // Keylog is enabled per session. + void enable_keylog() {} + +private: + gnutls_certificate_credentials_t cred_; + gnutls_datum_t session_ticket_key_; + gnutls_anti_replay_t anti_replay_; +}; + +#endif // TLS_SERVER_CONTEXT_GNUTLS_H diff --git a/examples/tls_server_context_openssl.cc b/examples/tls_server_context_openssl.cc new file mode 100644 index 0000000..bdc35a3 --- /dev/null +++ b/examples/tls_server_context_openssl.cc @@ -0,0 +1,338 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_context_openssl.h" + +#include <cstring> +#include <iostream> +#include <fstream> +#include <limits> + +#include <ngtcp2/ngtcp2_crypto_openssl.h> + +#include <openssl/err.h> + +#include "server_base.h" +#include "template.h" + +extern Config config; + +TLSServerContext::TLSServerContext() : ssl_ctx_{nullptr} {} + +TLSServerContext::~TLSServerContext() { + if (ssl_ctx_) { + SSL_CTX_free(ssl_ctx_); + } +} + +SSL_CTX *TLSServerContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int alpn_select_proto_h3_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = static_cast<ngtcp2_crypto_conn_ref *>(SSL_get_app_data(ssl)); + auto h = static_cast<HandlerBase *>(conn_ref->user_data); + const uint8_t *alpn; + size_t alpnlen; + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version(h->conn()); + + switch (version) { + case QUIC_VER_DRAFT29: + alpn = H3_ALPN_DRAFT29; + alpnlen = str_size(H3_ALPN_DRAFT29); + break; + case QUIC_VER_DRAFT30: + alpn = H3_ALPN_DRAFT30; + alpnlen = str_size(H3_ALPN_DRAFT30); + break; + case QUIC_VER_DRAFT31: + alpn = H3_ALPN_DRAFT31; + alpnlen = str_size(H3_ALPN_DRAFT31); + break; + case QUIC_VER_DRAFT32: + alpn = H3_ALPN_DRAFT32; + alpnlen = str_size(H3_ALPN_DRAFT32); + break; + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2_DRAFT: + alpn = H3_ALPN_V1; + alpnlen = str_size(H3_ALPN_V1); + break; + default: + if (!config.quiet) { + std::cerr << "Unexpected quic protocol version: " << std::hex << "0x" + << version << std::dec << std::endl; + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto p = in, end = in + inlen; p + alpnlen <= end; p += *p + 1) { + if (std::equal(alpn, alpn + alpnlen, p)) { + *out = p + 1; + *outlen = *p; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::cerr << "Client did not present ALPN " << &alpn[1] << std::endl; + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int alpn_select_proto_hq_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = static_cast<ngtcp2_crypto_conn_ref *>(SSL_get_app_data(ssl)); + auto h = static_cast<HandlerBase *>(conn_ref->user_data); + const uint8_t *alpn; + size_t alpnlen; + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version(h->conn()); + + switch (version) { + case QUIC_VER_DRAFT29: + alpn = HQ_ALPN_DRAFT29; + alpnlen = str_size(HQ_ALPN_DRAFT29); + break; + case QUIC_VER_DRAFT30: + alpn = HQ_ALPN_DRAFT30; + alpnlen = str_size(HQ_ALPN_DRAFT30); + break; + case QUIC_VER_DRAFT31: + alpn = HQ_ALPN_DRAFT31; + alpnlen = str_size(HQ_ALPN_DRAFT31); + break; + case QUIC_VER_DRAFT32: + alpn = HQ_ALPN_DRAFT32; + alpnlen = str_size(HQ_ALPN_DRAFT32); + break; + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2_DRAFT: + alpn = HQ_ALPN_V1; + alpnlen = str_size(HQ_ALPN_V1); + break; + default: + if (!config.quiet) { + std::cerr << "Unexpected quic protocol version: " << std::hex << "0x" + << version << std::dec << std::endl; + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto p = in, end = in + inlen; p + alpnlen <= end; p += *p + 1) { + if (std::equal(alpn, alpn + alpnlen, p)) { + *out = p + 1; + *outlen = *p; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::cerr << "Client did not present ALPN " << &alpn[1] << std::endl; + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int verify_cb(int preverify_ok, X509_STORE_CTX *ctx) { + // We don't verify the client certificate. Just request it for the + // testing purpose. + return 1; +} +} // namespace + +namespace { +int gen_ticket_cb(SSL *ssl, void *arg) { + auto conn_ref = static_cast<ngtcp2_crypto_conn_ref *>(SSL_get_app_data(ssl)); + auto h = static_cast<HandlerBase *>(conn_ref->user_data); + auto ver = htonl(ngtcp2_conn_get_negotiated_version(h->conn())); + + if (!SSL_SESSION_set1_ticket_appdata(SSL_get0_session(ssl), &ver, + sizeof(ver))) { + return 0; + } + + return 1; +} +} // namespace + +namespace { +SSL_TICKET_RETURN decrypt_ticket_cb(SSL *ssl, SSL_SESSION *session, + const unsigned char *keyname, + size_t keynamelen, SSL_TICKET_STATUS status, + void *arg) { + switch (status) { + case SSL_TICKET_EMPTY: + case SSL_TICKET_NO_DECRYPT: + return SSL_TICKET_RETURN_IGNORE_RENEW; + } + + uint8_t *pver; + uint32_t ver; + size_t verlen; + + if (!SSL_SESSION_get0_ticket_appdata( + session, reinterpret_cast<void **>(&pver), &verlen) || + verlen != sizeof(ver)) { + switch (status) { + case SSL_TICKET_SUCCESS: + return SSL_TICKET_RETURN_IGNORE; + case SSL_TICKET_SUCCESS_RENEW: + default: + return SSL_TICKET_RETURN_IGNORE_RENEW; + } + } + + memcpy(&ver, pver, sizeof(ver)); + + auto conn_ref = static_cast<ngtcp2_crypto_conn_ref *>(SSL_get_app_data(ssl)); + auto h = static_cast<HandlerBase *>(conn_ref->user_data); + + if (ngtcp2_conn_get_client_chosen_version(h->conn()) != ntohl(ver)) { + switch (status) { + case SSL_TICKET_SUCCESS: + return SSL_TICKET_RETURN_IGNORE; + case SSL_TICKET_SUCCESS_RENEW: + default: + return SSL_TICKET_RETURN_IGNORE_RENEW; + } + } + + switch (status) { + case SSL_TICKET_SUCCESS: + return SSL_TICKET_RETURN_USE; + case SSL_TICKET_SUCCESS_RENEW: + default: + return SSL_TICKET_RETURN_USE_RENEW; + } +} +} // namespace + +int TLSServerContext::init(const char *private_key_file, const char *cert_file, + AppProtocol app_proto) { + constexpr static unsigned char sid_ctx[] = "ngtcp2 server"; + + ssl_ctx_ = SSL_CTX_new(TLS_server_method()); + if (!ssl_ctx_) { + std::cerr << "SSSL_CTX_new: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + if (ngtcp2_crypto_openssl_configure_server_context(ssl_ctx_) != 0) { + std::cerr << "ngtcp2_crypto_openssl_configure_server_context failed" + << std::endl; + return -1; + } + + SSL_CTX_set_max_early_data(ssl_ctx_, UINT32_MAX); + + constexpr auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_SINGLE_ECDH_USE | + SSL_OP_CIPHER_SERVER_PREFERENCE | + SSL_OP_NO_ANTI_REPLAY; + + SSL_CTX_set_options(ssl_ctx_, ssl_opts); + + if (SSL_CTX_set_ciphersuites(ssl_ctx_, config.ciphers) != 1) { + std::cerr << "SSL_CTX_set_ciphersuites: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + if (SSL_CTX_set1_groups_list(ssl_ctx_, config.groups) != 1) { + std::cerr << "SSL_CTX_set1_groups_list failed" << std::endl; + return -1; + } + + SSL_CTX_set_mode(ssl_ctx_, SSL_MODE_RELEASE_BUFFERS); + + switch (app_proto) { + case AppProtocol::H3: + SSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_h3_cb, nullptr); + break; + case AppProtocol::HQ: + SSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_hq_cb, nullptr); + break; + } + + SSL_CTX_set_default_verify_paths(ssl_ctx_); + + if (SSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != 1) { + std::cerr << "SSL_CTX_use_PrivateKey_file: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + if (SSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != 1) { + std::cerr << "SSL_CTX_use_certificate_chain_file: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + if (SSL_CTX_check_private_key(ssl_ctx_) != 1) { + std::cerr << "SSL_CTX_check_private_key: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + SSL_CTX_set_session_id_context(ssl_ctx_, sid_ctx, sizeof(sid_ctx) - 1); + + if (config.verify_client) { + SSL_CTX_set_verify(ssl_ctx_, + SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_cb); + } + + SSL_CTX_set_session_ticket_cb(ssl_ctx_, gen_ticket_cb, decrypt_ticket_cb, + nullptr); + + return 0; +} + +extern std::ofstream keylog_file; + +namespace { +void keylog_callback(const SSL *ssl, const char *line) { + keylog_file.write(line, strlen(line)); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace + +void TLSServerContext::enable_keylog() { + SSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +} diff --git a/examples/tls_server_context_openssl.h b/examples/tls_server_context_openssl.h new file mode 100644 index 0000000..94c7561 --- /dev/null +++ b/examples/tls_server_context_openssl.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_CONTEXT_OPENSSL_H +#define TLS_SERVER_CONTEXT_OPENSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <openssl/ssl.h> + +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext { +public: + TLSServerContext(); + ~TLSServerContext(); + + int init(const char *private_key_file, const char *cert_file, + AppProtocol app_proto); + + SSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + SSL_CTX *ssl_ctx_; +}; + +#endif // TLS_SERVER_CONTEXT_OPENSSL_H diff --git a/examples/tls_server_context_picotls.cc b/examples/tls_server_context_picotls.cc new file mode 100644 index 0000000..51d41b6 --- /dev/null +++ b/examples/tls_server_context_picotls.cc @@ -0,0 +1,318 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_context_picotls.h" + +#include <iostream> +#include <memory> + +#include <ngtcp2/ngtcp2_crypto_picotls.h> + +#include <openssl/pem.h> +#if OPENSSL_VERSION_NUMBER >= 0x30000000L +# include <openssl/core_names.h> +#endif // OPENSSL_VERSION_NUMBER >= 0x30000000L + +#include "server_base.h" +#include "template.h" + +extern Config config; + +namespace { +int on_client_hello_cb(ptls_on_client_hello_t *self, ptls_t *ptls, + ptls_on_client_hello_parameters_t *params) { + auto &negprotos = params->negotiated_protocols; + + for (size_t i = 0; i < negprotos.count; ++i) { + auto &proto = negprotos.list[i]; + if (H3_ALPN_V1[0] == proto.len && + memcmp(&H3_ALPN_V1[1], proto.base, proto.len) == 0) { + if (ptls_set_negotiated_protocol( + ptls, reinterpret_cast<char *>(proto.base), proto.len) != 0) { + return -1; + } + + return 0; + } + } + + return PTLS_ALERT_NO_APPLICATION_PROTOCOL; +} + +ptls_on_client_hello_t on_client_hello = {on_client_hello_cb}; +} // namespace + +namespace { +auto ticket_hmac = EVP_sha256(); + +template <size_t N> void random_bytes(std::array<uint8_t, N> &dest) { + ptls_openssl_random_bytes(dest.data(), dest.size()); +} + +const std::array<uint8_t, 16> &get_ticket_key_name() { + static std::array<uint8_t, 16> key_name; + random_bytes(key_name); + return key_name; +} + +const std::array<uint8_t, 32> &get_ticket_key() { + static std::array<uint8_t, 32> key; + random_bytes(key); + return key; +} + +const std::array<uint8_t, 32> &get_ticket_hmac_key() { + static std::array<uint8_t, 32> hmac_key; + random_bytes(hmac_key); + return hmac_key; +} +} // namespace + +namespace { +int ticket_key_cb(unsigned char *key_name, unsigned char *iv, + EVP_CIPHER_CTX *ctx, +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + EVP_MAC_CTX *hctx, +#else // OPENSSL_VERSION_NUMBER < 0x30000000L + HMAC_CTX *hctx, +#endif // OPENSSL_VERSION_NUMBER < 0x30000000L + int enc) { + static const auto &static_key_name = get_ticket_key_name(); + static const auto &static_key = get_ticket_key(); + static const auto &static_hmac_key = get_ticket_hmac_key(); + + if (enc) { + ptls_openssl_random_bytes(iv, EVP_MAX_IV_LENGTH); + + memcpy(key_name, static_key_name.data(), static_key_name.size()); + + EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, static_key.data(), iv); +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + std::array<OSSL_PARAM, 3> params{ + OSSL_PARAM_construct_octet_string( + OSSL_MAC_PARAM_KEY, const_cast<uint8_t *>(static_hmac_key.data()), + static_hmac_key.size()), + OSSL_PARAM_construct_utf8_string( + OSSL_MAC_PARAM_DIGEST, + const_cast<char *>(EVP_MD_get0_name(ticket_hmac)), 0), + OSSL_PARAM_construct_end(), + }; + if (!EVP_MAC_CTX_set_params(hctx, params.data())) { + /* TODO Which value should we return on error? */ + return 0; + } +#else // OPENSSL_VERSION_NUMBER < 0x30000000L + HMAC_Init_ex(hctx, static_hmac_key.data(), static_hmac_key.size(), + ticket_hmac, nullptr); +#endif // OPENSSL_VERSION_NUMBER < 0x30000000L + + return 1; + } + + if (memcmp(key_name, static_key_name.data(), static_key_name.size()) != 0) { + return 0; + } + + EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, static_key.data(), iv); +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + std::array<OSSL_PARAM, 3> params{ + OSSL_PARAM_construct_octet_string( + OSSL_MAC_PARAM_KEY, const_cast<uint8_t *>(static_hmac_key.data()), + static_hmac_key.size()), + OSSL_PARAM_construct_utf8_string( + OSSL_MAC_PARAM_DIGEST, + const_cast<char *>(EVP_MD_get0_name(ticket_hmac)), 0), + OSSL_PARAM_construct_end(), + }; + if (!EVP_MAC_CTX_set_params(hctx, params.data())) { + /* TODO Which value should we return on error? */ + return 0; + } +#else // OPENSSL_VERSION_NUMBER < 0x30000000L + HMAC_Init_ex(hctx, static_hmac_key.data(), static_hmac_key.size(), + ticket_hmac, nullptr); +#endif // OPENSSL_VERSION_NUMBER < 0x30000000L + + return 1; +} +} // namespace + +namespace { +int encrypt_ticket_cb(ptls_encrypt_ticket_t *encrypt_ticket, ptls_t *ptls, + int is_encrypt, ptls_buffer_t *dst, ptls_iovec_t src) { + int rv; + auto conn_ref = + static_cast<ngtcp2_crypto_conn_ref *>(*ptls_get_data_ptr(ptls)); + auto conn = conn_ref->get_conn(conn_ref); + uint32_t ver; + + if (is_encrypt) { + ver = htonl(ngtcp2_conn_get_negotiated_version(conn)); + // TODO Replace std::make_unique with + // std::make_unique_for_overwrite when it is available. + auto buf = std::make_unique<uint8_t[]>(src.len + sizeof(ver)); + auto p = std::copy_n(src.base, src.len, buf.get()); + p = std::copy_n(reinterpret_cast<uint8_t *>(&ver), sizeof(ver), p); + + src.base = buf.get(); + src.len = p - buf.get(); + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + rv = ptls_openssl_encrypt_ticket_evp(dst, src, ticket_key_cb); +#else // OPENSSL_VERSION_NUMBER < 0x30000000L + rv = ptls_openssl_encrypt_ticket(dst, src, ticket_key_cb); +#endif // OPENSSL_VERSION_NUMBER < 0x30000000L + if (rv != 0) { + return -1; + } + + return 0; + } + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + rv = ptls_openssl_decrypt_ticket_evp(dst, src, ticket_key_cb); +#else // OPENSSL_VERSION_NUMBER < 0x30000000L + rv = ptls_openssl_decrypt_ticket(dst, src, ticket_key_cb); +#endif // OPENSSL_VERSION_NUMBER < 0x30000000L + if (rv != 0) { + return -1; + } + + if (dst->off < sizeof(ver)) { + return -1; + } + + memcpy(&ver, dst->base + dst->off - sizeof(ver), sizeof(ver)); + + if (ngtcp2_conn_get_client_chosen_version(conn) != ntohl(ver)) { + return -1; + } + + dst->off -= sizeof(ver); + + return 0; +} + +ptls_encrypt_ticket_t encrypt_ticket = {encrypt_ticket_cb}; +} // namespace + +namespace { +ptls_key_exchange_algorithm_t *key_exchanges[] = { + &ptls_openssl_x25519, + &ptls_openssl_secp256r1, + &ptls_openssl_secp384r1, + &ptls_openssl_secp521r1, + nullptr, +}; +} // namespace + +namespace { +ptls_cipher_suite_t *cipher_suites[] = { + &ptls_openssl_aes128gcmsha256, + &ptls_openssl_aes256gcmsha384, + &ptls_openssl_chacha20poly1305sha256, + nullptr, +}; +} // namespace + +TLSServerContext::TLSServerContext() + : ctx_{ + .random_bytes = ptls_openssl_random_bytes, + .get_time = &ptls_get_time, + .key_exchanges = key_exchanges, + .cipher_suites = cipher_suites, + .on_client_hello = &on_client_hello, + .ticket_lifetime = 86400, + .require_dhe_on_psk = 1, + .server_cipher_preference = 1, + .encrypt_ticket = &encrypt_ticket, + }, + sign_cert_{} +{} + +TLSServerContext::~TLSServerContext() { + if (sign_cert_.key) { + ptls_openssl_dispose_sign_certificate(&sign_cert_); + } + + for (size_t i = 0; i < ctx_.certificates.count; ++i) { + free(ctx_.certificates.list[i].base); + } + free(ctx_.certificates.list); +} + +ptls_context_t *TLSServerContext::get_native_handle() { return &ctx_; } + +int TLSServerContext::init(const char *private_key_file, const char *cert_file, + AppProtocol app_proto) { + if (ngtcp2_crypto_picotls_configure_server_context(&ctx_) != 0) { + std::cerr << "ngtcp2_crypto_picotls_configure_server_context failed" + << std::endl; + return -1; + } + + if (ptls_load_certificates(&ctx_, cert_file) != 0) { + std::cerr << "ptls_load_certificates failed" << std::endl; + return -1; + } + + if (load_private_key(private_key_file) != 0) { + return -1; + } + + if (config.verify_client) { + ctx_.require_client_authentication = 1; + } + + return 0; +} + +int TLSServerContext::load_private_key(const char *private_key_file) { + auto fp = fopen(private_key_file, "rb"); + if (fp == nullptr) { + std::cerr << "Could not open private key file " << private_key_file << ": " + << strerror(errno) << std::endl; + return -1; + } + + auto fp_d = defer(fclose, fp); + + auto pkey = PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr); + if (pkey == nullptr) { + std::cerr << "Could not read private key file " << private_key_file + << std::endl; + return -1; + } + + auto pkey_d = defer(EVP_PKEY_free, pkey); + + if (ptls_openssl_init_sign_certificate(&sign_cert_, pkey) != 0) { + std::cerr << "ptls_openssl_init_sign_certificate failed" << std::endl; + return -1; + } + + ctx_.sign_certificate = &sign_cert_.super; + + return 0; +} diff --git a/examples/tls_server_context_picotls.h b/examples/tls_server_context_picotls.h new file mode 100644 index 0000000..c9dc489 --- /dev/null +++ b/examples/tls_server_context_picotls.h @@ -0,0 +1,59 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_CONTEXT_PICOTLS_H +#define TLS_SERVER_CONTEXT_PICOTLS_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <picotls.h> +#include <picotls/openssl.h> + +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext { +public: + TLSServerContext(); + ~TLSServerContext(); + + int init(const char *private_key_file, const char *cert_file, + AppProtocol app_proto); + + ptls_context_t *get_native_handle(); + + // TODO Implement keylog. + void enable_keylog() {} + +private: + int load_private_key(const char *private_key_file); + + ptls_context_t ctx_; + ptls_openssl_sign_certificate_t sign_cert_; +}; + +#endif // TLS_SERVER_CONTEXT_PICOTLS_H diff --git a/examples/tls_server_context_wolfssl.cc b/examples/tls_server_context_wolfssl.cc new file mode 100644 index 0000000..ed09b72 --- /dev/null +++ b/examples/tls_server_context_wolfssl.cc @@ -0,0 +1,284 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_context_wolfssl.h" + +#include <iostream> +#include <fstream> +#include <limits> + +#include <ngtcp2/ngtcp2_crypto_wolfssl.h> + +#include "server_base.h" +#include "template.h" + +extern Config config; + +TLSServerContext::TLSServerContext() : ssl_ctx_{nullptr} {} + +TLSServerContext::~TLSServerContext() { + if (ssl_ctx_) { + wolfSSL_CTX_free(ssl_ctx_); + } +} + +WOLFSSL_CTX *TLSServerContext::get_native_handle() const { return ssl_ctx_; } + +namespace { +int alpn_select_proto_h3_cb(WOLFSSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = + static_cast<ngtcp2_crypto_conn_ref *>(wolfSSL_get_app_data(ssl)); + auto h = static_cast<HandlerBase *>(conn_ref->user_data); + const uint8_t *alpn; + size_t alpnlen; + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version(h->conn()); + + switch (version) { + case QUIC_VER_DRAFT29: + alpn = H3_ALPN_DRAFT29; + alpnlen = str_size(H3_ALPN_DRAFT29); + break; + case QUIC_VER_DRAFT30: + alpn = H3_ALPN_DRAFT30; + alpnlen = str_size(H3_ALPN_DRAFT30); + break; + case QUIC_VER_DRAFT31: + alpn = H3_ALPN_DRAFT31; + alpnlen = str_size(H3_ALPN_DRAFT31); + break; + case QUIC_VER_DRAFT32: + alpn = H3_ALPN_DRAFT32; + alpnlen = str_size(H3_ALPN_DRAFT32); + break; + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2_DRAFT: + alpn = H3_ALPN_V1; + alpnlen = str_size(H3_ALPN_V1); + break; + default: + if (!config.quiet) { + std::cerr << "Unexpected quic protocol version: " << std::hex << "0x" + << version << std::dec << std::endl; + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto p = in, end = in + inlen; p + alpnlen <= end; p += *p + 1) { + if (std::equal(alpn, alpn + alpnlen, p)) { + *out = p + 1; + *outlen = *p; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::cerr << "Client did not present ALPN " << &alpn[1] << std::endl; + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int alpn_select_proto_hq_cb(WOLFSSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto conn_ref = + static_cast<ngtcp2_crypto_conn_ref *>(wolfSSL_get_app_data(ssl)); + auto h = static_cast<HandlerBase *>(conn_ref->user_data); + const uint8_t *alpn; + size_t alpnlen; + // This should be the negotiated version, but we have not set the + // negotiated version when this callback is called. + auto version = ngtcp2_conn_get_client_chosen_version(h->conn()); + + switch (version) { + case QUIC_VER_DRAFT29: + alpn = HQ_ALPN_DRAFT29; + alpnlen = str_size(HQ_ALPN_DRAFT29); + break; + case QUIC_VER_DRAFT30: + alpn = HQ_ALPN_DRAFT30; + alpnlen = str_size(HQ_ALPN_DRAFT30); + break; + case QUIC_VER_DRAFT31: + alpn = HQ_ALPN_DRAFT31; + alpnlen = str_size(HQ_ALPN_DRAFT31); + break; + case QUIC_VER_DRAFT32: + alpn = HQ_ALPN_DRAFT32; + alpnlen = str_size(HQ_ALPN_DRAFT32); + break; + case NGTCP2_PROTO_VER_V1: + case NGTCP2_PROTO_VER_V2_DRAFT: + alpn = HQ_ALPN_V1; + alpnlen = str_size(HQ_ALPN_V1); + break; + default: + if (!config.quiet) { + std::cerr << "Unexpected quic protocol version: " << std::hex << "0x" + << version << std::dec << std::endl; + } + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + for (auto p = in, end = in + inlen; p + alpnlen <= end; p += *p + 1) { + if (std::equal(alpn, alpn + alpnlen, p)) { + *out = p + 1; + *outlen = *p; + return SSL_TLSEXT_ERR_OK; + } + } + + if (!config.quiet) { + std::cerr << "Client did not present ALPN " << &alpn[1] << std::endl; + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace + +namespace { +int verify_cb(int preverify_ok, X509_STORE_CTX *ctx) { + // We don't verify the client certificate. Just request it for the + // testing purpose. + return 1; +} +} // namespace + +int TLSServerContext::init(const char *private_key_file, const char *cert_file, + AppProtocol app_proto) { + constexpr static unsigned char sid_ctx[] = "ngtcp2 server"; + +#if defined(DEBUG_WOLFSSL) + if (!config.quiet) { + /*wolfSSL_Debugging_ON();*/ + } +#endif + + ssl_ctx_ = wolfSSL_CTX_new(wolfTLSv1_3_server_method()); + if (!ssl_ctx_) { + std::cerr << "wolfSSL_CTX_new: " + << wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + if (ngtcp2_crypto_wolfssl_configure_server_context(ssl_ctx_) != 0) { + std::cerr << "ngtcp2_crypto_wolfssl_configure_server_context failed" + << std::endl; + return -1; + } + +#ifdef WOLFSSL_EARLY_DATA + wolfSSL_CTX_set_max_early_data(ssl_ctx_, UINT32_MAX); +#endif + + constexpr auto ssl_opts = + (WOLFSSL_OP_ALL & ~WOLFSSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + WOLFSSL_OP_SINGLE_ECDH_USE | WOLFSSL_OP_CIPHER_SERVER_PREFERENCE; + + wolfSSL_CTX_set_options(ssl_ctx_, ssl_opts); + + if (wolfSSL_CTX_set_cipher_list(ssl_ctx_, config.ciphers) != 1) { + std::cerr << "wolfSSL_CTX_set_cipher_list: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + if (wolfSSL_CTX_set1_curves_list(ssl_ctx_, + const_cast<char *>(config.groups)) != 1) { + std::cerr << "wolfSSL_CTX_set1_curves_list(" << config.groups << ") failed" + << std::endl; + return -1; + } + + wolfSSL_CTX_set_mode(ssl_ctx_, SSL_MODE_RELEASE_BUFFERS); + + switch (app_proto) { + case AppProtocol::H3: + wolfSSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_h3_cb, nullptr); + break; + case AppProtocol::HQ: + wolfSSL_CTX_set_alpn_select_cb(ssl_ctx_, alpn_select_proto_hq_cb, nullptr); + break; + } + + wolfSSL_CTX_set_default_verify_paths(ssl_ctx_); + + if (wolfSSL_CTX_use_PrivateKey_file(ssl_ctx_, private_key_file, + SSL_FILETYPE_PEM) != 1) { + std::cerr << "wolfSSL_CTX_use_PrivateKey_file: " + << wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + if (wolfSSL_CTX_use_certificate_chain_file(ssl_ctx_, cert_file) != 1) { + std::cerr << "wolfSSL_CTX_use_certificate_chain_file: " + << wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + if (wolfSSL_CTX_check_private_key(ssl_ctx_) != 1) { + std::cerr << "wolfSSL_CTX_check_private_key: " + << wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + wolfSSL_CTX_set_session_id_context(ssl_ctx_, sid_ctx, sizeof(sid_ctx) - 1); + + if (config.verify_client) { + wolfSSL_CTX_set_verify(ssl_ctx_, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_CLIENT_ONCE | + WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_cb); + } + + return 0; +} + +extern std::ofstream keylog_file; + +#ifdef HAVE_SECRET_CALLBACK +namespace { +void keylog_callback(const WOLFSSL *ssl, const char *line) { + keylog_file.write(line, strlen(line)); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace +#endif + +void TLSServerContext::enable_keylog() { +#ifdef HAVE_SECRET_CALLBACK + wolfSSL_CTX_set_keylog_callback(ssl_ctx_, keylog_callback); +#endif +} diff --git a/examples/tls_server_context_wolfssl.h b/examples/tls_server_context_wolfssl.h new file mode 100644 index 0000000..e0b3c38 --- /dev/null +++ b/examples/tls_server_context_wolfssl.h @@ -0,0 +1,55 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_CONTEXT_WOLFSSL_H +#define TLS_SERVER_CONTEXT_WOLFSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <wolfssl/options.h> +#include <wolfssl/ssl.h> + +#include "shared.h" + +using namespace ngtcp2; + +class TLSServerContext { +public: + TLSServerContext(); + ~TLSServerContext(); + + int init(const char *private_key_file, const char *cert_file, + AppProtocol app_proto); + + WOLFSSL_CTX *get_native_handle() const; + + void enable_keylog(); + +private: + WOLFSSL_CTX *ssl_ctx_; +}; + +#endif // TLS_SERVER_CONTEXT_WOLFSSL_H diff --git a/examples/tls_server_session.h b/examples/tls_server_session.h new file mode 100644 index 0000000..85b76e4 --- /dev/null +++ b/examples/tls_server_session.h @@ -0,0 +1,52 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_SESSION_H +#define TLS_SERVER_SESSION_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#if defined(ENABLE_EXAMPLE_OPENSSL) && defined(WITH_EXAMPLE_OPENSSL) +# include "tls_server_session_openssl.h" +#endif // ENABLE_EXAMPLE_OPENSSL && WITH_EXAMPLE_OPENSSL + +#if defined(ENABLE_EXAMPLE_GNUTLS) && defined(WITH_EXAMPLE_GNUTLS) +# include "tls_server_session_gnutls.h" +#endif // ENABLE_EXAMPLE_GNUTLS && WITH_EXAMPLE_GNUTLS + +#if defined(ENABLE_EXAMPLE_BORINGSSL) && defined(WITH_EXAMPLE_BORINGSSL) +# include "tls_server_session_boringssl.h" +#endif // ENABLE_EXAMPLE_BORINGSSL && WITH_EXAMPLE_BORINGSSL + +#if defined(ENABLE_EXAMPLE_PICOTLS) && defined(WITH_EXAMPLE_PICOTLS) +# include "tls_server_session_picotls.h" +#endif // ENABLE_EXAMPLE_PICOTLS && WITH_EXAMPLE_PICOTLS + +#if defined(ENABLE_EXAMPLE_WOLFSSL) && defined(WITH_EXAMPLE_WOLFSSL) +# include "tls_server_session_wolfssl.h" +#endif // ENABLE_EXAMPLE_WOLFSSL && WITH_EXAMPLE_WOLFSSL + +#endif // TLS_SERVER_SESSION_H diff --git a/examples/tls_server_session_boringssl.cc b/examples/tls_server_session_boringssl.cc new file mode 100644 index 0000000..133f4d0 --- /dev/null +++ b/examples/tls_server_session_boringssl.cc @@ -0,0 +1,84 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_session_boringssl.h" + +#include <cassert> +#include <iostream> + +#include <ngtcp2/ngtcp2.h> + +#include "tls_server_context_boringssl.h" +#include "server_base.h" + +extern Config config; + +TLSServerSession::TLSServerSession() {} + +TLSServerSession::~TLSServerSession() {} + +int TLSServerSession::init(const TLSServerContext &tls_ctx, + HandlerBase *handler) { + auto ssl_ctx = tls_ctx.get_native_handle(); + + ssl_ = SSL_new(ssl_ctx); + if (!ssl_) { + std::cerr << "SSL_new: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + SSL_set_app_data(ssl_, handler->conn_ref()); + SSL_set_accept_state(ssl_); + SSL_set_early_data_enabled(ssl_, 1); + SSL_set_quic_use_legacy_codepoint(ssl_, 0); + + std::array<uint8_t, 128> quic_early_data_ctx; + ngtcp2_transport_params params; + memset(¶ms, 0, sizeof(params)); + params.initial_max_streams_bidi = config.max_streams_bidi; + params.initial_max_streams_uni = config.max_streams_uni; + params.initial_max_stream_data_bidi_local = config.max_stream_data_bidi_local; + params.initial_max_stream_data_bidi_remote = + config.max_stream_data_bidi_remote; + params.initial_max_stream_data_uni = config.max_stream_data_uni; + params.initial_max_data = config.max_data; + + auto quic_early_data_ctxlen = ngtcp2_encode_transport_params( + quic_early_data_ctx.data(), quic_early_data_ctx.size(), + NGTCP2_TRANSPORT_PARAMS_TYPE_ENCRYPTED_EXTENSIONS, ¶ms); + if (quic_early_data_ctxlen < 0) { + std::cerr << "ngtcp2_encode_transport_params: " + << ngtcp2_strerror(quic_early_data_ctxlen) << std::endl; + return -1; + } + + if (SSL_set_quic_early_data_context(ssl_, quic_early_data_ctx.data(), + quic_early_data_ctxlen) != 1) { + std::cerr << "SSL_set_quic_early_data_context failed" << std::endl; + return -1; + } + + return 0; +} diff --git a/examples/tls_server_session_boringssl.h b/examples/tls_server_session_boringssl.h new file mode 100644 index 0000000..eebf18a --- /dev/null +++ b/examples/tls_server_session_boringssl.h @@ -0,0 +1,47 @@ +/* + * ngtcp2 + * + * Copyright (c) 2021 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_SESSION_BORINGSSL_H +#define TLS_SERVER_SESSION_BORINGSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include "tls_session_base_openssl.h" + +class TLSServerContext; +class HandlerBase; + +class TLSServerSession : public TLSSessionBase { +public: + TLSServerSession(); + ~TLSServerSession(); + + int init(const TLSServerContext &tls_ctx, HandlerBase *handler); + // ticket is sent automatically. + int send_session_ticket() { return 0; } +}; + +#endif // TLS_SERVER_SESSION_BORINGSSL_H diff --git a/examples/tls_server_session_gnutls.cc b/examples/tls_server_session_gnutls.cc new file mode 100644 index 0000000..ee776c4 --- /dev/null +++ b/examples/tls_server_session_gnutls.cc @@ -0,0 +1,155 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_session_gnutls.h" + +#include <cassert> +#include <iostream> +#include <fstream> +#include <array> + +#include <ngtcp2/ngtcp2_crypto_gnutls.h> + +#include "tls_server_context_gnutls.h" +#include "server_base.h" +#include "util.h" + +// Based on https://github.com/ueno/ngtcp2-gnutls-examples + +using namespace ngtcp2; + +extern Config config; + +TLSServerSession::TLSServerSession() {} + +TLSServerSession::~TLSServerSession() {} + +namespace { +int client_hello_cb(gnutls_session_t session, unsigned int htype, unsigned when, + unsigned int incoming, const gnutls_datum_t *msg) { + assert(htype == GNUTLS_HANDSHAKE_CLIENT_HELLO); + assert(when == GNUTLS_HOOK_POST); + assert(incoming == 1); + + // check if ALPN extension is present and properly selected h3 + gnutls_datum_t alpn; + if (auto rv = gnutls_alpn_get_selected_protocol(session, &alpn); rv != 0) { + return rv; + } + + // TODO Fix this to properly select ALPN based on app_proto. + + // strip the first byte from H3_ALPN_V1 + auto h3 = reinterpret_cast<const char *>(&H3_ALPN_V1[1]); + if (static_cast<size_t>(H3_ALPN_V1[0]) != alpn.size || + !std::equal(alpn.data, alpn.data + alpn.size, h3)) { + return -1; + } + + return 0; +} +} // namespace + +int TLSServerSession::init(const TLSServerContext &tls_ctx, + HandlerBase *handler) { + if (auto rv = + gnutls_init(&session_, GNUTLS_SERVER | GNUTLS_ENABLE_EARLY_DATA | + GNUTLS_NO_AUTO_SEND_TICKET | + GNUTLS_NO_END_OF_EARLY_DATA); + rv != 0) { + std::cerr << "gnutls_init failed: " << gnutls_strerror(rv) << std::endl; + return -1; + } + + std::string priority = "%DISABLE_TLS13_COMPAT_MODE:"; + priority += config.ciphers; + priority += ':'; + priority += config.groups; + + if (auto rv = gnutls_priority_set_direct(session_, priority.c_str(), nullptr); + rv != 0) { + std::cerr << "gnutls_priority_set_direct failed: " << gnutls_strerror(rv) + << std::endl; + return -1; + } + + auto rv = gnutls_session_ticket_enable_server( + session_, tls_ctx.get_session_ticket_key()); + if (rv != 0) { + std::cerr << "gnutls_session_ticket_enable_server failed: " + << gnutls_strerror(rv) << std::endl; + return -1; + } + + gnutls_handshake_set_hook_function(session_, GNUTLS_HANDSHAKE_CLIENT_HELLO, + GNUTLS_HOOK_POST, client_hello_cb); + + if (ngtcp2_crypto_gnutls_configure_server_session(session_) != 0) { + std::cerr << "ngtcp2_crypto_gnutls_configure_server_session failed" + << std::endl; + return -1; + } + + gnutls_anti_replay_enable(session_, tls_ctx.get_anti_replay()); + + gnutls_record_set_max_early_data_size(session_, 0xffffffffu); + + gnutls_session_set_ptr(session_, handler->conn_ref()); + + if (auto rv = gnutls_credentials_set(session_, GNUTLS_CRD_CERTIFICATE, + tls_ctx.get_certificate_credentials()); + rv != 0) { + std::cerr << "gnutls_credentials_set failed: " << gnutls_strerror(rv) + << std::endl; + return -1; + } + + // TODO Set all available ALPN based on app_proto. + + // strip the first byte from H3_ALPN_V1 + gnutls_datum_t alpn{ + .data = const_cast<uint8_t *>(&H3_ALPN_V1[1]), + .size = H3_ALPN_V1[0], + }; + gnutls_alpn_set_protocols(session_, &alpn, 1, + GNUTLS_ALPN_MANDATORY | + GNUTLS_ALPN_SERVER_PRECEDENCE); + + if (config.verify_client) { + gnutls_certificate_server_set_request(session_, GNUTLS_CERT_REQUIRE); + gnutls_certificate_send_x509_rdn_sequence(session_, 1); + } + + return 0; +} + +int TLSServerSession::send_session_ticket() { + if (auto rv = gnutls_session_ticket_send(session_, 1, 0); rv != 0) { + std::cerr << "gnutls_session_ticket_send failed: " << gnutls_strerror(rv) + << std::endl; + return -1; + } + + return 0; +} diff --git a/examples/tls_server_session_gnutls.h b/examples/tls_server_session_gnutls.h new file mode 100644 index 0000000..994c643 --- /dev/null +++ b/examples/tls_server_session_gnutls.h @@ -0,0 +1,46 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_SESSION_GNUTLS_H +#define TLS_SERVER_SESSION_GNUTLS_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include "tls_session_base_gnutls.h" + +class TLSServerContext; +class HandlerBase; + +class TLSServerSession : public TLSSessionBase { +public: + TLSServerSession(); + ~TLSServerSession(); + + int init(const TLSServerContext &tls_ctx, HandlerBase *handler); + int send_session_ticket(); +}; + +#endif // TLS_SERVER_SESSION_GNUTLS_H diff --git a/examples/tls_server_session_openssl.cc b/examples/tls_server_session_openssl.cc new file mode 100644 index 0000000..5e93e41 --- /dev/null +++ b/examples/tls_server_session_openssl.cc @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_session_openssl.h" + +#include <iostream> + +#include <openssl/err.h> + +#include "tls_server_context_openssl.h" +#include "server_base.h" + +TLSServerSession::TLSServerSession() {} + +TLSServerSession::~TLSServerSession() {} + +int TLSServerSession::init(const TLSServerContext &tls_ctx, + HandlerBase *handler) { + auto ssl_ctx = tls_ctx.get_native_handle(); + + ssl_ = SSL_new(ssl_ctx); + if (!ssl_) { + std::cerr << "SSL_new: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + SSL_set_app_data(ssl_, handler->conn_ref()); + SSL_set_accept_state(ssl_); + SSL_set_quic_early_data_enabled(ssl_, 1); + + return 0; +} diff --git a/examples/tls_server_session_openssl.h b/examples/tls_server_session_openssl.h new file mode 100644 index 0000000..ef84b39 --- /dev/null +++ b/examples/tls_server_session_openssl.h @@ -0,0 +1,47 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_SESSION_OPENSSL_H +#define TLS_SERVER_SESSION_OPENSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include "tls_session_base_openssl.h" + +class TLSServerContext; +class HandlerBase; + +class TLSServerSession : public TLSSessionBase { +public: + TLSServerSession(); + ~TLSServerSession(); + + int init(const TLSServerContext &tls_ctx, HandlerBase *handler); + // ticket is sent automatically. + int send_session_ticket() { return 0; } +}; + +#endif // TLS_SERVER_SESSION_OPENSSL_H diff --git a/examples/tls_server_session_picotls.cc b/examples/tls_server_session_picotls.cc new file mode 100644 index 0000000..f1124aa --- /dev/null +++ b/examples/tls_server_session_picotls.cc @@ -0,0 +1,70 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_session_picotls.h" + +#include <cassert> +#include <iostream> + +#include <ngtcp2/ngtcp2_crypto_picotls.h> + +#include "tls_server_context_picotls.h" +#include "server_base.h" +#include "util.h" + +using namespace ngtcp2; + +extern Config config; + +TLSServerSession::TLSServerSession() {} + +TLSServerSession::~TLSServerSession() {} + +int TLSServerSession::init(TLSServerContext &tls_ctx, HandlerBase *handler) { + cptls_.ptls = ptls_server_new(tls_ctx.get_native_handle()); + if (!cptls_.ptls) { + std::cerr << "ptls_server_new failed" << std::endl; + return -1; + } + + *ptls_get_data_ptr(cptls_.ptls) = handler->conn_ref(); + + cptls_.handshake_properties.additional_extensions = + new ptls_raw_extension_t[2]{ + { + .type = UINT16_MAX, + }, + { + .type = UINT16_MAX, + }, + }; + + if (ngtcp2_crypto_picotls_configure_server_session(&cptls_) != 0) { + std::cerr << "ngtcp2_crypto_picotls_configure_server_session failed" + << std::endl; + return -1; + } + + return 0; +} diff --git a/examples/tls_server_session_picotls.h b/examples/tls_server_session_picotls.h new file mode 100644 index 0000000..ea919a2 --- /dev/null +++ b/examples/tls_server_session_picotls.h @@ -0,0 +1,47 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_SESSION_PICOTLS_H +#define TLS_SERVER_SESSION_PICOTLS_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include "tls_session_base_picotls.h" + +class TLSServerContext; +class HandlerBase; + +class TLSServerSession : public TLSSessionBase { +public: + TLSServerSession(); + ~TLSServerSession(); + + int init(TLSServerContext &tls_ctx, HandlerBase *handler); + // ticket is sent automatically. + int send_session_ticket() { return 0; } +}; + +#endif // TLS_SERVER_SESSION_PICOTLS_H diff --git a/examples/tls_server_session_wolfssl.cc b/examples/tls_server_session_wolfssl.cc new file mode 100644 index 0000000..68497ad --- /dev/null +++ b/examples/tls_server_session_wolfssl.cc @@ -0,0 +1,55 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_server_session_wolfssl.h" + +#include <iostream> + +#include "tls_server_context_wolfssl.h" +#include "server_base.h" + +TLSServerSession::TLSServerSession() {} + +TLSServerSession::~TLSServerSession() {} + +int TLSServerSession::init(const TLSServerContext &tls_ctx, + HandlerBase *handler) { + auto ssl_ctx = tls_ctx.get_native_handle(); + + ssl_ = wolfSSL_new(ssl_ctx); + if (!ssl_) { + std::cerr << "wolfSSL_new: " + << wolfSSL_ERR_error_string(wolfSSL_ERR_get_error(), nullptr) + << std::endl; + return -1; + } + + wolfSSL_set_app_data(ssl_, handler->conn_ref()); + wolfSSL_set_accept_state(ssl_); +#ifdef WOLFSSL_EARLY_DATA + wolfSSL_set_quic_early_data_enabled(ssl_, 1); +#endif + + return 0; +} diff --git a/examples/tls_server_session_wolfssl.h b/examples/tls_server_session_wolfssl.h new file mode 100644 index 0000000..db32441 --- /dev/null +++ b/examples/tls_server_session_wolfssl.h @@ -0,0 +1,47 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SERVER_SESSION_WOLFSSL_H +#define TLS_SERVER_SESSION_WOLFSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include "tls_session_base_wolfssl.h" + +class TLSServerContext; +class HandlerBase; + +class TLSServerSession : public TLSSessionBase { +public: + TLSServerSession(); + ~TLSServerSession(); + + int init(const TLSServerContext &tls_ctx, HandlerBase *handler); + // ticket is sent automatically. + int send_session_ticket() { return 0; } +}; + +#endif // TLS_SERVER_SESSION_WOLFSSL_H diff --git a/examples/tls_session_base_gnutls.cc b/examples/tls_session_base_gnutls.cc new file mode 100644 index 0000000..51460e9 --- /dev/null +++ b/examples/tls_session_base_gnutls.cc @@ -0,0 +1,87 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_session_base_gnutls.h" + +#include <fstream> + +#include "util.h" + +// Based on https://github.com/ueno/ngtcp2-gnutls-examples + +using namespace ngtcp2; + +TLSSessionBase::TLSSessionBase() : session_{nullptr} {} + +TLSSessionBase::~TLSSessionBase() { gnutls_deinit(session_); } + +gnutls_session_t TLSSessionBase::get_native_handle() const { return session_; } + +std::string TLSSessionBase::get_cipher_name() const { + return gnutls_cipher_get_name(gnutls_cipher_get(session_)); +} + +std::string TLSSessionBase::get_selected_alpn() const { + gnutls_datum_t alpn; + + if (auto rv = gnutls_alpn_get_selected_protocol(session_, &alpn); rv == 0) { + return std::string{alpn.data, alpn.data + alpn.size}; + } + + return {}; +} + +extern std::ofstream keylog_file; + +namespace { +int keylog_callback(gnutls_session_t session, const char *label, + const gnutls_datum_t *secret) { + keylog_file.write(label, strlen(label)); + keylog_file.put(' '); + + gnutls_datum_t crandom; + gnutls_datum_t srandom; + + gnutls_session_get_random(session, &crandom, &srandom); + if (crandom.size != 32) { + return -1; + } + + auto crandom_hex = + util::format_hex(reinterpret_cast<unsigned char *>(crandom.data), 32); + keylog_file << crandom_hex << " "; + + auto secret_hex = util::format_hex( + reinterpret_cast<unsigned char *>(secret->data), secret->size); + keylog_file << secret_hex << " "; + + keylog_file.put('\n'); + keylog_file.flush(); + return 0; +} +} // namespace + +void TLSSessionBase::enable_keylog() { + gnutls_session_set_keylog_function(session_, keylog_callback); +} diff --git a/examples/tls_session_base_gnutls.h b/examples/tls_session_base_gnutls.h new file mode 100644 index 0000000..7a23418 --- /dev/null +++ b/examples/tls_session_base_gnutls.h @@ -0,0 +1,51 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SESSION_BASE_GNUTLS_H +#define TLS_SESSION_BASE_GNUTLS_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <string> + +#include <gnutls/gnutls.h> + +class TLSSessionBase { +public: + TLSSessionBase(); + ~TLSSessionBase(); + + gnutls_session_t get_native_handle() const; + + std::string get_cipher_name() const; + std::string get_selected_alpn() const; + void enable_keylog(); + +protected: + gnutls_session_t session_; +}; + +#endif // TLS_SESSION_BASE_GNUTLS_H diff --git a/examples/tls_session_base_openssl.cc b/examples/tls_session_base_openssl.cc new file mode 100644 index 0000000..2de47dc --- /dev/null +++ b/examples/tls_session_base_openssl.cc @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_session_base_openssl.h" + +#include <array> + +#include "util.h" + +using namespace ngtcp2; + +TLSSessionBase::TLSSessionBase() : ssl_{nullptr} {} + +TLSSessionBase::~TLSSessionBase() { + if (ssl_) { + SSL_free(ssl_); + } +} + +SSL *TLSSessionBase::get_native_handle() const { return ssl_; } + +std::string TLSSessionBase::get_cipher_name() const { + return SSL_get_cipher_name(ssl_); +} + +std::string TLSSessionBase::get_selected_alpn() const { + const unsigned char *alpn = nullptr; + unsigned int alpnlen; + + SSL_get0_alpn_selected(ssl_, &alpn, &alpnlen); + + return std::string{alpn, alpn + alpnlen}; +} diff --git a/examples/tls_session_base_openssl.h b/examples/tls_session_base_openssl.h new file mode 100644 index 0000000..ee63b39 --- /dev/null +++ b/examples/tls_session_base_openssl.h @@ -0,0 +1,52 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SESSION_BASE_OPENSSL_H +#define TLS_SESSION_BASE_OPENSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <string> + +#include <openssl/ssl.h> + +class TLSSessionBase { +public: + TLSSessionBase(); + ~TLSSessionBase(); + + SSL *get_native_handle() const; + + std::string get_cipher_name() const; + std::string get_selected_alpn() const; + // Keylog is enabled per SSL_CTX. + void enable_keylog() {} + +protected: + SSL *ssl_; +}; + +#endif // TLS_SESSION_BASE_OPENSSL_H diff --git a/examples/tls_session_base_picotls.cc b/examples/tls_session_base_picotls.cc new file mode 100644 index 0000000..b8413b4 --- /dev/null +++ b/examples/tls_session_base_picotls.cc @@ -0,0 +1,56 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_session_base_picotls.h" + +TLSSessionBase::TLSSessionBase() { ngtcp2_crypto_picotls_ctx_init(&cptls_); } + +TLSSessionBase::~TLSSessionBase() { + ngtcp2_crypto_picotls_deconfigure_session(&cptls_); + + delete[] cptls_.handshake_properties.additional_extensions; + + if (cptls_.ptls) { + ptls_free(cptls_.ptls); + } +} + +ngtcp2_crypto_picotls_ctx *TLSSessionBase::get_native_handle() { + return &cptls_; +} + +std::string TLSSessionBase::get_cipher_name() const { + auto cs = ptls_get_cipher(cptls_.ptls); + return cs->aead->name; +} + +std::string TLSSessionBase::get_selected_alpn() const { + auto alpn = ptls_get_negotiated_protocol(cptls_.ptls); + + if (!alpn) { + return {}; + } + + return alpn; +} diff --git a/examples/tls_session_base_picotls.h b/examples/tls_session_base_picotls.h new file mode 100644 index 0000000..e59ccbc --- /dev/null +++ b/examples/tls_session_base_picotls.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SESSION_BASE_PICOTLS_H +#define TLS_SESSION_BASE_PICOTLS_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <string> + +#include <ngtcp2/ngtcp2_crypto_picotls.h> + +#include <picotls.h> + +class TLSSessionBase { +public: + TLSSessionBase(); + ~TLSSessionBase(); + + ngtcp2_crypto_picotls_ctx *get_native_handle(); + + std::string get_cipher_name() const; + std::string get_selected_alpn() const; + // TODO make keylog work with picotls + void enable_keylog(){}; + +protected: + ngtcp2_crypto_picotls_ctx cptls_; +}; + +#endif // TLS_SESSION_BASE_PICOTLS_H diff --git a/examples/tls_session_base_wolfssl.cc b/examples/tls_session_base_wolfssl.cc new file mode 100644 index 0000000..4620182 --- /dev/null +++ b/examples/tls_session_base_wolfssl.cc @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls_session_base_wolfssl.h" + +#include <array> + +#include "util.h" + +using namespace ngtcp2; + +TLSSessionBase::TLSSessionBase() : ssl_{nullptr} {} + +TLSSessionBase::~TLSSessionBase() { + if (ssl_) { + wolfSSL_free(ssl_); + } +} + +WOLFSSL *TLSSessionBase::get_native_handle() const { return ssl_; } + +std::string TLSSessionBase::get_cipher_name() const { + return wolfSSL_get_cipher_name(ssl_); +} + +std::string TLSSessionBase::get_selected_alpn() const { + char *alpn = nullptr; + unsigned short alpnlen; + + wolfSSL_ALPN_GetProtocol(ssl_, &alpn, &alpnlen); + + return std::string{alpn, alpn + alpnlen}; +} diff --git a/examples/tls_session_base_wolfssl.h b/examples/tls_session_base_wolfssl.h new file mode 100644 index 0000000..c61eee8 --- /dev/null +++ b/examples/tls_session_base_wolfssl.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_SESSION_BASE_WOLFSSL_H +#define TLS_SESSION_BASE_WOLFSSL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <string> + +#include <wolfssl/options.h> +#include <wolfssl/ssl.h> +#include <wolfssl/quic.h> + +class TLSSessionBase { +public: + TLSSessionBase(); + ~TLSSessionBase(); + + WOLFSSL *get_native_handle() const; + + std::string get_cipher_name() const; + std::string get_selected_alpn() const; + // Keylog is enabled per SSL_CTX. + void enable_keylog() {} + +protected: + WOLFSSL *ssl_; +}; + +#endif // TLS_SESSION_BASE_WOLFSSL_H diff --git a/examples/util.cc b/examples/util.cc new file mode 100644 index 0000000..f8401d4 --- /dev/null +++ b/examples/util.cc @@ -0,0 +1,646 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * Copyright (c) 2012 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "util.h" + +#ifdef HAVE_ARPA_INET_H +# include <arpa/inet.h> +#endif // HAVE_ARPA_INET_H +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif +#include <sys/types.h> +#include <unistd.h> +#include <fcntl.h> +#include <netdb.h> + +#include <cassert> +#include <cstring> +#include <chrono> +#include <array> +#include <iostream> +#include <fstream> +#include <algorithm> +#include <limits> +#include <charconv> + +#include "template.h" + +using namespace std::literals; + +namespace ngtcp2 { + +namespace util { + +namespace { +constexpr char LOWER_XDIGITS[] = "0123456789abcdef"; +} // namespace + +std::string format_hex(uint8_t c) { + std::string s; + s.resize(2); + + s[0] = LOWER_XDIGITS[c >> 4]; + s[1] = LOWER_XDIGITS[c & 0xf]; + + return s; +} + +std::string format_hex(const uint8_t *s, size_t len) { + std::string res; + res.resize(len * 2); + + for (size_t i = 0; i < len; ++i) { + auto c = s[i]; + + res[i * 2] = LOWER_XDIGITS[c >> 4]; + res[i * 2 + 1] = LOWER_XDIGITS[c & 0x0f]; + } + return res; +} + +std::string format_hex(const std::string_view &s) { + return format_hex(reinterpret_cast<const uint8_t *>(s.data()), s.size()); +} + +std::string decode_hex(const std::string_view &s) { + assert(s.size() % 2 == 0); + std::string res(s.size() / 2, '0'); + auto p = std::begin(res); + for (auto it = std::begin(s); it != std::end(s); it += 2) { + *p++ = (hex_to_uint(*it) << 4) | hex_to_uint(*(it + 1)); + } + return res; +} + +namespace { +// format_fraction2 formats |n| as fraction part of integer. |n| is +// considered as fraction, and its precision is 3 digits. The last +// digit is ignored. The precision of the resulting fraction is 2 +// digits. +std::string format_fraction2(uint32_t n) { + n /= 10; + + if (n < 10) { + return {'.', '0', static_cast<char>('0' + n)}; + } + return {'.', static_cast<char>('0' + n / 10), + static_cast<char>('0' + (n % 10))}; +} +} // namespace + +namespace { +// round2even rounds the last digit of |n| so that the n / 10 becomes +// even. +uint64_t round2even(uint64_t n) { + if (n % 10 == 5) { + if ((n / 10) & 1) { + n += 10; + } + } else { + n += 5; + } + return n; +} +} // namespace + +std::string format_durationf(uint64_t ns) { + static constexpr const std::string_view units[] = {"us"sv, "ms"sv, "s"sv}; + if (ns < 1000) { + return format_uint(ns) + "ns"; + } + auto unit = 0; + if (ns < 1000000) { + // do nothing + } else if (ns < 1000000000) { + ns /= 1000; + unit = 1; + } else { + ns /= 1000000; + unit = 2; + } + + ns = round2even(ns); + + if (ns / 1000 >= 1000 && unit < 2) { + ns /= 1000; + ++unit; + } + + auto res = format_uint(ns / 1000); + res += format_fraction2(ns % 1000); + res += units[unit]; + + return res; +} + +std::mt19937 make_mt19937() { + std::random_device rd; + return std::mt19937(rd()); +} + +ngtcp2_tstamp timestamp(struct ev_loop *loop) { + return std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); +} + +bool numeric_host(const char *hostname) { + return numeric_host(hostname, AF_INET) || numeric_host(hostname, AF_INET6); +} + +bool numeric_host(const char *hostname, int family) { + int rv; + std::array<uint8_t, sizeof(struct in6_addr)> dst; + + rv = inet_pton(family, hostname, dst.data()); + + return rv == 1; +} + +namespace { +void hexdump8(FILE *out, const uint8_t *first, const uint8_t *last) { + auto stop = std::min(first + 8, last); + for (auto k = first; k != stop; ++k) { + fprintf(out, "%02x ", *k); + } + // each byte needs 3 spaces (2 hex value and space) + for (; stop != first + 8; ++stop) { + fputs(" ", out); + } + // we have extra space after 8 bytes + fputc(' ', out); +} +} // namespace + +void hexdump(FILE *out, const uint8_t *src, size_t len) { + if (len == 0) { + return; + } + size_t buflen = 0; + auto repeated = false; + std::array<uint8_t, 16> buf{}; + auto end = src + len; + auto i = src; + for (;;) { + auto nextlen = + std::min(static_cast<size_t>(16), static_cast<size_t>(end - i)); + if (nextlen == buflen && + std::equal(std::begin(buf), std::begin(buf) + buflen, i)) { + // as long as adjacent 16 bytes block are the same, we just + // print single '*'. + if (!repeated) { + repeated = true; + fputs("*\n", out); + } + i += nextlen; + continue; + } + repeated = false; + fprintf(out, "%08lx", static_cast<unsigned long>(i - src)); + if (i == end) { + fputc('\n', out); + break; + } + fputs(" ", out); + hexdump8(out, i, end); + hexdump8(out, i + 8, std::max(i + 8, end)); + fputc('|', out); + auto stop = std::min(i + 16, end); + buflen = stop - i; + auto p = buf.data(); + for (; i != stop; ++i) { + *p++ = *i; + if (0x20 <= *i && *i <= 0x7e) { + fputc(*i, out); + } else { + fputc('.', out); + } + } + fputs("|\n", out); + } +} + +std::string make_cid_key(const ngtcp2_cid *cid) { + return std::string(cid->data, cid->data + cid->datalen); +} + +std::string make_cid_key(const uint8_t *cid, size_t cidlen) { + return std::string(cid, cid + cidlen); +} + +std::string straddr(const sockaddr *sa, socklen_t salen) { + std::array<char, NI_MAXHOST> host; + std::array<char, NI_MAXSERV> port; + + auto rv = getnameinfo(sa, salen, host.data(), host.size(), port.data(), + port.size(), NI_NUMERICHOST | NI_NUMERICSERV); + if (rv != 0) { + std::cerr << "getnameinfo: " << gai_strerror(rv) << std::endl; + return ""; + } + std::string res = "["; + res.append(host.data(), strlen(host.data())); + res += "]:"; + res.append(port.data(), strlen(port.data())); + return res; +} + +std::string_view strccalgo(ngtcp2_cc_algo cc_algo) { + switch (cc_algo) { + case NGTCP2_CC_ALGO_RENO: + return "reno"sv; + case NGTCP2_CC_ALGO_CUBIC: + return "cubic"sv; + case NGTCP2_CC_ALGO_BBR: + return "bbr"sv; + case NGTCP2_CC_ALGO_BBR2: + return "bbr2"sv; + default: + assert(0); + abort(); + } +} + +namespace { +constexpr bool rws(char c) { return c == '\t' || c == ' '; } +} // namespace + +std::optional<std::unordered_map<std::string, std::string>> +read_mime_types(const std::string_view &filename) { + std::ifstream f(filename.data()); + if (!f) { + return {}; + } + + std::unordered_map<std::string, std::string> dest; + + std::string line; + while (std::getline(f, line)) { + if (line.empty() || line[0] == '#') { + continue; + } + + auto p = std::find_if(std::begin(line), std::end(line), rws); + if (p == std::begin(line) || p == std::end(line)) { + continue; + } + + auto media_type = std::string{std::begin(line), p}; + for (;;) { + auto ext = std::find_if_not(p, std::end(line), rws); + if (ext == std::end(line)) { + break; + } + + p = std::find_if(ext, std::end(line), rws); + dest.emplace(std::string{ext, p}, media_type); + } + } + + return dest; +} + +std::string format_duration(ngtcp2_duration n) { + if (n >= 3600 * NGTCP2_SECONDS && (n % (3600 * NGTCP2_SECONDS)) == 0) { + return format_uint(n / (3600 * NGTCP2_SECONDS)) + 'h'; + } + if (n >= 60 * NGTCP2_SECONDS && (n % (60 * NGTCP2_SECONDS)) == 0) { + return format_uint(n / (60 * NGTCP2_SECONDS)) + 'm'; + } + if (n >= NGTCP2_SECONDS && (n % NGTCP2_SECONDS) == 0) { + return format_uint(n / NGTCP2_SECONDS) + 's'; + } + if (n >= NGTCP2_MILLISECONDS && (n % NGTCP2_MILLISECONDS) == 0) { + return format_uint(n / NGTCP2_MILLISECONDS) + "ms"; + } + if (n >= NGTCP2_MICROSECONDS && (n % NGTCP2_MICROSECONDS) == 0) { + return format_uint(n / NGTCP2_MICROSECONDS) + "us"; + } + return format_uint(n) + "ns"; +} + +namespace { +std::optional<std::pair<uint64_t, size_t>> +parse_uint_internal(const std::string_view &s) { + uint64_t res = 0; + + if (s.empty()) { + return {}; + } + + for (size_t i = 0; i < s.size(); ++i) { + auto c = s[i]; + if (c < '0' || '9' < c) { + return {{res, i}}; + } + + auto d = c - '0'; + if (res > (std::numeric_limits<uint64_t>::max() - d) / 10) { + return {}; + } + + res *= 10; + res += d; + } + + return {{res, s.size()}}; +} +} // namespace + +std::optional<uint64_t> parse_uint(const std::string_view &s) { + auto o = parse_uint_internal(s); + if (!o) { + return {}; + } + auto [res, idx] = *o; + if (idx != s.size()) { + return {}; + } + return res; +} + +std::optional<uint64_t> parse_uint_iec(const std::string_view &s) { + auto o = parse_uint_internal(s); + if (!o) { + return {}; + } + auto [res, idx] = *o; + if (idx == s.size()) { + return res; + } + if (idx + 1 != s.size()) { + return {}; + } + + uint64_t m; + switch (s[idx]) { + case 'G': + case 'g': + m = 1 << 30; + break; + case 'M': + case 'm': + m = 1 << 20; + break; + case 'K': + case 'k': + m = 1 << 10; + break; + default: + return {}; + } + + if (res > std::numeric_limits<uint64_t>::max() / m) { + return {}; + } + + return res * m; +} + +std::optional<uint64_t> parse_duration(const std::string_view &s) { + auto o = parse_uint_internal(s); + if (!o) { + return {}; + } + auto [res, idx] = *o; + if (idx == s.size()) { + return res * NGTCP2_SECONDS; + } + + uint64_t m; + if (idx + 1 == s.size()) { + switch (s[idx]) { + case 'H': + case 'h': + m = 3600 * NGTCP2_SECONDS; + break; + case 'M': + case 'm': + m = 60 * NGTCP2_SECONDS; + break; + case 'S': + case 's': + m = NGTCP2_SECONDS; + break; + default: + return {}; + } + } else if (idx + 2 == s.size() && (s[idx + 1] == 's' || s[idx + 1] == 'S')) { + switch (s[idx]) { + case 'M': + case 'm': + m = NGTCP2_MILLISECONDS; + break; + case 'U': + case 'u': + m = NGTCP2_MICROSECONDS; + break; + case 'N': + case 'n': + return res; + default: + return {}; + } + } else { + return {}; + } + + if (res > std::numeric_limits<uint64_t>::max() / m) { + return {}; + } + + return res * m; +} + +namespace { +template <typename InputIt> InputIt eat_file(InputIt first, InputIt last) { + if (first == last) { + *first++ = '/'; + return first; + } + + if (*(last - 1) == '/') { + return last; + } + + auto p = last; + for (; p != first && *(p - 1) != '/'; --p) + ; + if (p == first) { + // this should not happened in normal case, where we expect path + // starts with '/' + *first++ = '/'; + return first; + } + + return p; +} +} // namespace + +namespace { +template <typename InputIt> InputIt eat_dir(InputIt first, InputIt last) { + auto p = eat_file(first, last); + + --p; + + assert(*p == '/'); + + return eat_file(first, p); +} +} // namespace + +std::string normalize_path(const std::string_view &path) { + assert(path.size() <= 1024); + assert(path.size() > 0); + assert(path[0] == '/'); + + std::array<char, 1024> res; + auto p = res.data(); + + auto first = std::begin(path); + auto last = std::end(path); + + *p++ = '/'; + ++first; + for (; first != last && *first == '/'; ++first) + ; + + for (; first != last;) { + if (*first == '.') { + if (first + 1 == last) { + break; + } + if (*(first + 1) == '/') { + first += 2; + continue; + } + if (*(first + 1) == '.') { + if (first + 2 == last) { + p = eat_dir(res.data(), p); + break; + } + if (*(first + 2) == '/') { + p = eat_dir(res.data(), p); + first += 3; + continue; + } + } + } + if (*(p - 1) != '/') { + p = eat_file(res.data(), p); + } + auto slash = std::find(first, last, '/'); + if (slash == last) { + p = std::copy(first, last, p); + break; + } + p = std::copy(first, slash + 1, p); + first = slash + 1; + for (; first != last && *first == '/'; ++first) + ; + } + return std::string{res.data(), p}; +} + +int make_socket_nonblocking(int fd) { + int rv; + int flags; + + while ((flags = fcntl(fd, F_GETFL, 0)) == -1 && errno == EINTR) + ; + if (flags == -1) { + return -1; + } + + while ((rv = fcntl(fd, F_SETFL, flags | O_NONBLOCK)) == -1 && errno == EINTR) + ; + + return rv; +} + +int create_nonblock_socket(int domain, int type, int protocol) { +#ifdef SOCK_NONBLOCK + auto fd = socket(domain, type | SOCK_NONBLOCK, protocol); + if (fd == -1) { + return -1; + } +#else // !SOCK_NONBLOCK + auto fd = socket(domain, type, protocol); + if (fd == -1) { + return -1; + } + + make_socket_nonblocking(fd); +#endif // !SOCK_NONBLOCK + + return fd; +} + +std::vector<std::string_view> split_str(const std::string_view &s, char delim) { + size_t len = 1; + auto last = std::end(s); + std::string_view::const_iterator d; + for (auto first = std::begin(s); (d = std::find(first, last, delim)) != last; + ++len, first = d + 1) + ; + + auto list = std::vector<std::string_view>(len); + + len = 0; + for (auto first = std::begin(s);; ++len) { + auto stop = std::find(first, last, delim); + // xcode clang does not understand std::string_view{first, stop}. + list[len] = std::string_view{first, static_cast<size_t>(stop - first)}; + if (stop == last) { + break; + } + first = stop + 1; + } + return list; +} + +std::optional<uint32_t> parse_version(const std::string_view &s) { + auto k = s; + if (!util::istarts_with(k, "0x"sv)) { + return {}; + } + k = k.substr(2); + uint32_t v; + auto rv = std::from_chars(k.data(), k.data() + k.size(), v, 16); + if (rv.ptr != k.data() + k.size() || rv.ec != std::errc{}) { + return {}; + } + + return v; +} + +} // namespace util + +std::ostream &operator<<(std::ostream &os, const ngtcp2_cid &cid) { + return os << "0x" << util::format_hex(cid.data, cid.datalen); +} + +} // namespace ngtcp2 diff --git a/examples/util.h b/examples/util.h new file mode 100644 index 0000000..c83449d --- /dev/null +++ b/examples/util.h @@ -0,0 +1,361 @@ +/* + * ngtcp2 + * + * Copyright (c) 2017 ngtcp2 contributors + * Copyright (c) 2012 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef UTIL_H +#define UTIL_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <sys/socket.h> + +#include <optional> +#include <string> +#include <random> +#include <unordered_map> +#include <string_view> + +#include <ngtcp2/ngtcp2.h> +#include <nghttp3/nghttp3.h> + +#include <ev.h> + +namespace ngtcp2 { + +namespace util { + +inline nghttp3_nv make_nv(const std::string_view &name, + const std::string_view &value, uint8_t flags) { + return nghttp3_nv{ + reinterpret_cast<uint8_t *>(const_cast<char *>(std::data(name))), + reinterpret_cast<uint8_t *>(const_cast<char *>(std::data(value))), + name.size(), + value.size(), + flags, + }; +} + +inline nghttp3_nv make_nv_cc(const std::string_view &name, + const std::string_view &value) { + return make_nv(name, value, NGHTTP3_NV_FLAG_NONE); +} + +inline nghttp3_nv make_nv_nc(const std::string_view &name, + const std::string_view &value) { + return make_nv(name, value, NGHTTP3_NV_FLAG_NO_COPY_NAME); +} + +inline nghttp3_nv make_nv_nn(const std::string_view &name, + const std::string_view &value) { + return make_nv(name, value, + NGHTTP3_NV_FLAG_NO_COPY_NAME | NGHTTP3_NV_FLAG_NO_COPY_VALUE); +} + +std::string format_hex(uint8_t c); + +std::string format_hex(const uint8_t *s, size_t len); + +std::string format_hex(const std::string_view &s); + +template <size_t N> std::string format_hex(const uint8_t (&s)[N]) { + return format_hex(s, N); +} + +std::string decode_hex(const std::string_view &s); + +// format_durationf formats |ns| in human readable manner. |ns| must +// be nanoseconds resolution. This function uses the largest unit so +// that the integral part is strictly more than zero, and the +// precision is at most 2 digits. For example, 1234 is formatted as +// "1.23us". The largest unit is seconds. +std::string format_durationf(uint64_t ns); + +std::mt19937 make_mt19937(); + +ngtcp2_tstamp timestamp(struct ev_loop *loop); + +bool numeric_host(const char *hostname); + +bool numeric_host(const char *hostname, int family); + +// Dumps |src| of length |len| in the format similar to `hexdump -C`. +void hexdump(FILE *out, const uint8_t *src, size_t len); + +inline char lowcase(char c) { + constexpr static unsigned char tbl[] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', + 'z', 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, + 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, + 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, + 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, + 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, + 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, + 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, + 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, + 255, + }; + return tbl[static_cast<unsigned char>(c)]; +} + +struct CaseCmp { + bool operator()(char lhs, char rhs) const { + return lowcase(lhs) == lowcase(rhs); + } +}; + +template <typename InputIterator1, typename InputIterator2> +bool istarts_with(InputIterator1 first1, InputIterator1 last1, + InputIterator2 first2, InputIterator2 last2) { + if (last1 - first1 < last2 - first2) { + return false; + } + return std::equal(first2, last2, first1, CaseCmp()); +} + +template <typename S, typename T> bool istarts_with(const S &a, const T &b) { + return istarts_with(a.begin(), a.end(), b.begin(), b.end()); +} + +// make_cid_key returns the key for |cid|. +std::string make_cid_key(const ngtcp2_cid *cid); +std::string make_cid_key(const uint8_t *cid, size_t cidlen); + +// straddr stringifies |sa| of length |salen| in a format "[IP]:PORT". +std::string straddr(const sockaddr *sa, socklen_t salen); + +// strccalgo stringifies |cc_algo|. +std::string_view strccalgo(ngtcp2_cc_algo cc_algo); + +template <typename T, size_t N> +bool streq_l(const T (&a)[N], const nghttp3_vec &b) { + return N - 1 == b.len && memcmp(a, b.base, N - 1) == 0; +} + +namespace { +constexpr char B64_CHARS[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', +}; +} // namespace + +template <typename InputIt> std::string b64encode(InputIt first, InputIt last) { + std::string res; + size_t len = last - first; + if (len == 0) { + return res; + } + size_t r = len % 3; + res.resize((len + 2) / 3 * 4); + auto j = last - r; + auto p = std::begin(res); + while (first != j) { + uint32_t n = static_cast<uint8_t>(*first++) << 16; + n += static_cast<uint8_t>(*first++) << 8; + n += static_cast<uint8_t>(*first++); + *p++ = B64_CHARS[n >> 18]; + *p++ = B64_CHARS[(n >> 12) & 0x3fu]; + *p++ = B64_CHARS[(n >> 6) & 0x3fu]; + *p++ = B64_CHARS[n & 0x3fu]; + } + + if (r == 2) { + uint32_t n = static_cast<uint8_t>(*first++) << 16; + n += static_cast<uint8_t>(*first++) << 8; + *p++ = B64_CHARS[n >> 18]; + *p++ = B64_CHARS[(n >> 12) & 0x3fu]; + *p++ = B64_CHARS[(n >> 6) & 0x3fu]; + *p++ = '='; + } else if (r == 1) { + uint32_t n = static_cast<uint8_t>(*first++) << 16; + *p++ = B64_CHARS[n >> 18]; + *p++ = B64_CHARS[(n >> 12) & 0x3fu]; + *p++ = '='; + *p++ = '='; + } + return res; +} + +// read_mime_types reads "MIME media types and the extensions" file +// denoted by |filename| and returns the mapping of extension to MIME +// media type. +std::optional<std::unordered_map<std::string, std::string>> +read_mime_types(const std::string_view &filename); + +// format_uint converts |n| into string. +template <typename T> std::string format_uint(T n) { + std::string res; + if (n == 0) { + res = "0"; + return res; + } + size_t nlen = 0; + for (auto t = n; t; t /= 10, ++nlen) + ; + res.resize(nlen); + for (; n; n /= 10) { + res[--nlen] = (n % 10) + '0'; + } + return res; +} + +// format_uint_iec converts |n| into string with the IEC unit (either +// "G", "M", or "K"). It chooses the largest unit which does not drop +// precision. +template <typename T> std::string format_uint_iec(T n) { + if (n >= (1 << 30) && (n & ((1 << 30) - 1)) == 0) { + return format_uint(n / (1 << 30)) + 'G'; + } + if (n >= (1 << 20) && (n & ((1 << 20) - 1)) == 0) { + return format_uint(n / (1 << 20)) + 'M'; + } + if (n >= (1 << 10) && (n & ((1 << 10) - 1)) == 0) { + return format_uint(n / (1 << 10)) + 'K'; + } + return format_uint(n); +} + +// format_duration converts |n| into string with the unit in either +// "h" (hours), "m" (minutes), "s" (seconds), "ms" (milliseconds), +// "us" (microseconds) or "ns" (nanoseconds). It chooses the largest +// unit which does not drop precision. |n| is in nanosecond +// resolution. +std::string format_duration(ngtcp2_duration n); + +// parse_uint parses |s| as 64-bit unsigned integer. If it cannot +// parse |s|, the return value does not contain a value. +std::optional<uint64_t> parse_uint(const std::string_view &s); + +// parse_uint_iec parses |s| as 64-bit unsigned integer. It accepts +// IEC unit letter (either "G", "M", or "K") in |s|. If it cannot +// parse |s|, the return value does not contain a value. +std::optional<uint64_t> parse_uint_iec(const std::string_view &s); + +// parse_duration parses |s| as 64-bit unsigned integer. It accepts a +// unit (either "h", "m", "s", "ms", "us", or "ns") in |s|. If no +// unit is present, the unit "s" is assumed. If it cannot parse |s|, +// the return value does not contain a value. +std::optional<uint64_t> parse_duration(const std::string_view &s); + +// generate_secure_random generates a cryptographically secure pseudo +// random data of |datalen| bytes and stores to the buffer pointed by +// |data|. +int generate_secure_random(uint8_t *data, size_t datalen); + +// generate_secret generates secret and writes it to the buffer +// pointed by |secret| of length |secretlen|. Currently, |secretlen| +// must be 32. +int generate_secret(uint8_t *secret, size_t secretlen); + +// normalize_path removes ".." by consuming a previous path component. +// It also removes ".". It assumes that |path| starts with "/". If +// it cannot consume a previous path component, it just removes "..". +std::string normalize_path(const std::string_view &path); + +constexpr bool is_digit(const char c) { return '0' <= c && c <= '9'; } + +constexpr bool is_hex_digit(const char c) { + return is_digit(c) || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f'); +} + +// Returns integer corresponding to hex notation |c|. If +// is_hex_digit(c) is false, it returns 256. +constexpr uint32_t hex_to_uint(char c) { + if (c <= '9') { + return c - '0'; + } + if (c <= 'Z') { + return c - 'A' + 10; + } + if (c <= 'z') { + return c - 'a' + 10; + } + return 256; +} + +template <typename InputIt> +std::string percent_decode(InputIt first, InputIt last) { + std::string result; + result.resize(last - first); + auto p = std::begin(result); + for (; first != last; ++first) { + if (*first != '%') { + *p++ = *first; + continue; + } + + if (first + 1 != last && first + 2 != last && is_hex_digit(*(first + 1)) && + is_hex_digit(*(first + 2))) { + *p++ = (hex_to_uint(*(first + 1)) << 4) + hex_to_uint(*(first + 2)); + first += 2; + continue; + } + + *p++ = *first; + } + result.resize(p - std::begin(result)); + return result; +} + +int make_socket_nonblocking(int fd); + +int create_nonblock_socket(int domain, int type, int protocol); + +std::optional<std::string> read_token(const std::string_view &filename); +int write_token(const std::string_view &filename, const uint8_t *token, + size_t tokenlen); + +const char *crypto_default_ciphers(); + +const char *crypto_default_groups(); + +// split_str parses delimited strings in |s| and returns substrings +// delimited by |delim|. The any white spaces around substring are +// treated as a part of substring. +std::vector<std::string_view> split_str(const std::string_view &s, + char delim = ','); + +// parse_version parses |s| to get 4 byte QUIC version. |s| must be a +// hex string and must start with "0x" (.e.g, 0x00000001). +std::optional<uint32_t> parse_version(const std::string_view &s); + +} // namespace util + +std::ostream &operator<<(std::ostream &os, const ngtcp2_cid &cid); + +} // namespace ngtcp2 + +#endif // UTIL_H diff --git a/examples/util_gnutls.cc b/examples/util_gnutls.cc new file mode 100644 index 0000000..a33ca6e --- /dev/null +++ b/examples/util_gnutls.cc @@ -0,0 +1,136 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "util.h" + +#include <cassert> +#include <iostream> +#include <fstream> +#include <array> + +#include <ngtcp2/ngtcp2_crypto.h> + +#include <gnutls/crypto.h> + +#include "template.h" + +// Based on https://github.com/ueno/ngtcp2-gnutls-examples + +namespace ngtcp2 { + +namespace util { + +int generate_secure_random(uint8_t *data, size_t datalen) { + if (gnutls_rnd(GNUTLS_RND_RANDOM, data, datalen) != 0) { + return -1; + } + + return 0; +} + +int generate_secret(uint8_t *secret, size_t secretlen) { + std::array<uint8_t, 16> rand; + std::array<uint8_t, 32> md; + + assert(md.size() == secretlen); + + if (generate_secure_random(rand.data(), rand.size()) != 0) { + return -1; + } + + if (gnutls_hash_fast(GNUTLS_DIG_SHA256, rand.data(), rand.size(), + md.data()) != 0) { + return -1; + } + + std::copy_n(std::begin(md), secretlen, secret); + return 0; +} + +std::optional<std::string> read_token(const std::string_view &filename) { + auto f = std::ifstream(filename.data()); + if (!f) { + std::cerr << "Could not read token file " << filename << std::endl; + return {}; + } + + auto pos = f.tellg(); + std::vector<char> content(pos); + f.seekg(0, std::ios::beg); + f.read(content.data(), pos); + + gnutls_datum_t s; + s.data = reinterpret_cast<unsigned char *>(content.data()); + s.size = content.size(); + + gnutls_datum_t d; + if (auto rv = gnutls_pem_base64_decode2("QUIC TOKEN", &s, &d); rv < 0) { + std::cerr << "Could not read token in " << filename << std::endl; + return {}; + } + + auto res = std::string{d.data, d.data + d.size}; + + gnutls_free(d.data); + + return res; +} + +int write_token(const std::string_view &filename, const uint8_t *token, + size_t tokenlen) { + auto f = std::ofstream(filename.data()); + if (!f) { + std::cerr << "Could not write token in " << filename << std::endl; + return -1; + } + + gnutls_datum_t s; + s.data = const_cast<uint8_t *>(token); + s.size = tokenlen; + + gnutls_datum_t d; + if (auto rv = gnutls_pem_base64_encode2("QUIC TOKEN", &s, &d); rv < 0) { + std::cerr << "Could not encode token in " << filename << std::endl; + return -1; + } + + f.write(reinterpret_cast<const char *>(d.data), d.size); + gnutls_free(d.data); + + return 0; +} + +const char *crypto_default_ciphers() { + return "NORMAL:-VERS-ALL:+VERS-TLS1.3:-CIPHER-ALL:+AES-128-GCM:+AES-256-GCM:" + "+CHACHA20-POLY1305:+AES-128-CCM"; +} + +const char *crypto_default_groups() { + return "-GROUP-ALL:+GROUP-X25519:+GROUP-SECP256R1:+GROUP-SECP384R1:" + "+GROUP-SECP521R1"; +} + +} // namespace util + +} // namespace ngtcp2 diff --git a/examples/util_openssl.cc b/examples/util_openssl.cc new file mode 100644 index 0000000..116505b --- /dev/null +++ b/examples/util_openssl.cc @@ -0,0 +1,131 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "util.h" + +#include <cassert> +#include <iostream> +#include <array> + +#include <ngtcp2/ngtcp2_crypto.h> + +#include <openssl/bio.h> +#include <openssl/ssl.h> +#include <openssl/evp.h> +#include <openssl/rand.h> + +#include "template.h" + +namespace ngtcp2 { + +namespace util { + +int generate_secure_random(uint8_t *data, size_t datalen) { + if (RAND_bytes(data, static_cast<int>(datalen)) != 1) { + return -1; + } + + return 0; +} + +int generate_secret(uint8_t *secret, size_t secretlen) { + std::array<uint8_t, 16> rand; + std::array<uint8_t, 32> md; + + assert(md.size() == secretlen); + + if (generate_secure_random(rand.data(), rand.size()) != 0) { + return -1; + } + + auto ctx = EVP_MD_CTX_new(); + if (ctx == nullptr) { + return -1; + } + + auto ctx_deleter = defer(EVP_MD_CTX_free, ctx); + + unsigned int mdlen = md.size(); + if (!EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) || + !EVP_DigestUpdate(ctx, rand.data(), rand.size()) || + !EVP_DigestFinal_ex(ctx, md.data(), &mdlen)) { + return -1; + } + + std::copy_n(std::begin(md), secretlen, secret); + return 0; +} + +std::optional<std::string> read_token(const std::string_view &filename) { + auto f = BIO_new_file(filename.data(), "r"); + if (f == nullptr) { + std::cerr << "Could not open token file " << filename << std::endl; + return {}; + } + + auto f_d = defer(BIO_free, f); + + char *name, *header; + unsigned char *data; + long datalen; + std::string token; + if (PEM_read_bio(f, &name, &header, &data, &datalen) != 1) { + std::cerr << "Could not read token file " << filename << std::endl; + return {}; + } + + OPENSSL_free(name); + OPENSSL_free(header); + + auto res = std::string{data, data + datalen}; + + OPENSSL_free(data); + + return res; +} + +int write_token(const std::string_view &filename, const uint8_t *token, + size_t tokenlen) { + auto f = BIO_new_file(filename.data(), "w"); + if (f == nullptr) { + std::cerr << "Could not write token in " << filename << std::endl; + return -1; + } + + PEM_write_bio(f, "QUIC TOKEN", "", token, tokenlen); + BIO_free(f); + + return 0; +} + +const char *crypto_default_ciphers() { + return "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_" + "SHA256:TLS_AES_128_CCM_SHA256"; +} + +const char *crypto_default_groups() { return "X25519:P-256:P-384:P-521"; } + +} // namespace util + +} // namespace ngtcp2 diff --git a/examples/util_test.cc b/examples/util_test.cc new file mode 100644 index 0000000..d59aab2 --- /dev/null +++ b/examples/util_test.cc @@ -0,0 +1,237 @@ +/* + * ngtcp2 + * + * Copyright (c) 2018 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "util_test.h" + +#include <limits> + +#include <CUnit/CUnit.h> + +#include "util.h" + +namespace ngtcp2 { + +void test_util_format_durationf() { + CU_ASSERT("0ns" == util::format_durationf(0)); + CU_ASSERT("999ns" == util::format_durationf(999)); + CU_ASSERT("1.00us" == util::format_durationf(1000)); + CU_ASSERT("1.00us" == util::format_durationf(1004)); + CU_ASSERT("1.00us" == util::format_durationf(1005)); + CU_ASSERT("1.02us" == util::format_durationf(1015)); + CU_ASSERT("2.00us" == util::format_durationf(1999)); + CU_ASSERT("1.00ms" == util::format_durationf(999999)); + CU_ASSERT("3.50ms" == util::format_durationf(3500111)); + CU_ASSERT("9999.99s" == util::format_durationf(9999990000000llu)); +} + +void test_util_format_uint() { + CU_ASSERT("0" == util::format_uint(0)); + CU_ASSERT("18446744073709551615" == + util::format_uint(18446744073709551615ull)); +} + +void test_util_format_uint_iec() { + CU_ASSERT("0" == util::format_uint_iec(0)); + CU_ASSERT("1023" == util::format_uint_iec((1 << 10) - 1)); + CU_ASSERT("1K" == util::format_uint_iec(1 << 10)); + CU_ASSERT("1M" == util::format_uint_iec(1 << 20)); + CU_ASSERT("1G" == util::format_uint_iec(1 << 30)); + CU_ASSERT("18446744073709551615" == + util::format_uint_iec(std::numeric_limits<uint64_t>::max())); + CU_ASSERT("1025K" == util::format_uint_iec((1 << 20) + (1 << 10))); +} + +void test_util_format_duration() { + CU_ASSERT("0ns" == util::format_duration(0)); + CU_ASSERT("999ns" == util::format_duration(999)); + CU_ASSERT("1us" == util::format_duration(1000)); + CU_ASSERT("1ms" == util::format_duration(1000000)); + CU_ASSERT("1s" == util::format_duration(1000000000)); + CU_ASSERT("1m" == util::format_duration(60000000000ull)); + CU_ASSERT("1h" == util::format_duration(3600000000000ull)); + CU_ASSERT("18446744073709551615ns" == + util::format_duration(std::numeric_limits<uint64_t>::max())); + CU_ASSERT("61s" == util::format_duration(61000000000ull)); +} + +void test_util_parse_uint() { + { + auto res = util::parse_uint("0"); + CU_ASSERT(res.has_value()); + CU_ASSERT(0 == *res); + } + { + auto res = util::parse_uint("1"); + CU_ASSERT(res.has_value()); + CU_ASSERT(1 == *res); + } + { + auto res = util::parse_uint("18446744073709551615"); + CU_ASSERT(res.has_value()); + CU_ASSERT(18446744073709551615ull == *res); + } + { + auto res = util::parse_uint("18446744073709551616"); + CU_ASSERT(!res.has_value()); + } + { + auto res = util::parse_uint("a"); + CU_ASSERT(!res.has_value()); + } + { + auto res = util::parse_uint("1a"); + CU_ASSERT(!res.has_value()); + } +} + +void test_util_parse_uint_iec() { + { + auto res = util::parse_uint_iec("0"); + CU_ASSERT(res.has_value()); + CU_ASSERT(0 == *res); + } + { + auto res = util::parse_uint_iec("1023"); + CU_ASSERT(res.has_value()); + CU_ASSERT(1023 == *res); + } + { + auto res = util::parse_uint_iec("1K"); + CU_ASSERT(res.has_value()); + CU_ASSERT(1 << 10 == *res); + } + { + auto res = util::parse_uint_iec("1M"); + CU_ASSERT(res.has_value()); + CU_ASSERT(1 << 20 == *res); + } + { + auto res = util::parse_uint_iec("1G"); + CU_ASSERT(res.has_value()); + CU_ASSERT(1 << 30 == *res); + } + { + auto res = util::parse_uint_iec("11G"); + CU_ASSERT(res.has_value()); + CU_ASSERT((1ull << 30) * 11 == *res); + } + { + auto res = util::parse_uint_iec("18446744073709551616"); + CU_ASSERT(!res.has_value()); + } + { + auto res = util::parse_uint_iec("1x"); + CU_ASSERT(!res.has_value()); + } + { + auto res = util::parse_uint_iec("1Gx"); + CU_ASSERT(!res.has_value()); + } +} + +void test_util_parse_duration() { + { + auto res = util::parse_duration("0"); + CU_ASSERT(res.has_value()); + CU_ASSERT(0 == *res); + } + { + auto res = util::parse_duration("1"); + CU_ASSERT(res.has_value()); + CU_ASSERT(NGTCP2_SECONDS == *res); + } + { + auto res = util::parse_duration("0ns"); + CU_ASSERT(res.has_value()); + CU_ASSERT(0 == *res); + } + { + auto res = util::parse_duration("1ns"); + CU_ASSERT(res.has_value()); + CU_ASSERT(1 == *res); + } + { + auto res = util::parse_duration("1us"); + CU_ASSERT(res.has_value()); + CU_ASSERT(NGTCP2_MICROSECONDS == *res); + } + { + auto res = util::parse_duration("1ms"); + CU_ASSERT(res.has_value()); + CU_ASSERT(NGTCP2_MILLISECONDS == *res); + } + { + auto res = util::parse_duration("1s"); + CU_ASSERT(res.has_value()); + CU_ASSERT(NGTCP2_SECONDS == *res); + } + { + auto res = util::parse_duration("1m"); + CU_ASSERT(res.has_value()); + CU_ASSERT(60 * NGTCP2_SECONDS == *res); + } + { + auto res = util::parse_duration("1h"); + CU_ASSERT(res.has_value()); + CU_ASSERT(3600 * NGTCP2_SECONDS == *res); + } + { + auto res = util::parse_duration("2h"); + CU_ASSERT(res.has_value()); + CU_ASSERT(2 * 3600 * NGTCP2_SECONDS == *res); + } + { + auto res = util::parse_duration("18446744073709551616"); + CU_ASSERT(!res.has_value()); + } + { + auto res = util::parse_duration("1x"); + CU_ASSERT(!res.has_value()); + } + { + auto res = util::parse_duration("1mx"); + CU_ASSERT(!res.has_value()); + } + { + auto res = util::parse_duration("1mxy"); + CU_ASSERT(!res.has_value()); + } +} + +void test_util_normalize_path() { + CU_ASSERT("/" == util::normalize_path("/")); + CU_ASSERT("/" == util::normalize_path("//")); + CU_ASSERT("/foo" == util::normalize_path("/foo")); + CU_ASSERT("/foo/bar/" == util::normalize_path("/foo/bar/")); + CU_ASSERT("/foo/bar/" == util::normalize_path("/foo/abc/../bar/")); + CU_ASSERT("/foo/bar/" == util::normalize_path("/../foo/abc/../bar/")); + CU_ASSERT("/foo/bar/" == + util::normalize_path("/./foo/././abc///.././bar/./")); + CU_ASSERT("/foo/" == util::normalize_path("/foo/.")); + CU_ASSERT("/foo/bar" == util::normalize_path("/foo/./bar")); + CU_ASSERT("/bar" == util::normalize_path("/foo/./../bar")); + CU_ASSERT("/bar" == util::normalize_path("/../../bar")); +} + +} // namespace ngtcp2 diff --git a/examples/util_test.h b/examples/util_test.h new file mode 100644 index 0000000..376d3a9 --- /dev/null +++ b/examples/util_test.h @@ -0,0 +1,45 @@ +/* + * ngtcp2 + * + * Copyright (c) 2018 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef UTIL_TEST_H +#define UTIL_TEST_H + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif // HAVE_CONFIG_H + +namespace ngtcp2 { + +void test_util_format_durationf(); +void test_util_format_uint(); +void test_util_format_uint_iec(); +void test_util_format_duration(); +void test_util_parse_uint(); +void test_util_parse_uint_iec(); +void test_util_parse_duration(); +void test_util_normalize_path(); + +} // namespace ngtcp2 + +#endif // UTIL_TEST_H diff --git a/examples/util_wolfssl.cc b/examples/util_wolfssl.cc new file mode 100644 index 0000000..80eb096 --- /dev/null +++ b/examples/util_wolfssl.cc @@ -0,0 +1,130 @@ +/* + * ngtcp2 + * + * Copyright (c) 2020 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "util.h" + +#include <cassert> +#include <iostream> +#include <array> + +#include <ngtcp2/ngtcp2_crypto.h> + +#include <wolfssl/options.h> +#include <wolfssl/ssl.h> +#include <wolfssl/openssl/pem.h> + +#include "template.h" + +namespace ngtcp2 { + +namespace util { + +int generate_secure_random(uint8_t *data, size_t datalen) { + if (wolfSSL_RAND_bytes(data, static_cast<int>(datalen)) != 1) { + return -1; + } + + return 0; +} + +int generate_secret(uint8_t *secret, size_t secretlen) { + std::array<uint8_t, 16> rand; + std::array<uint8_t, 32> md; + + assert(md.size() == secretlen); + + if (generate_secure_random(rand.data(), rand.size()) != 0) { + return -1; + } + + auto ctx = wolfSSL_EVP_MD_CTX_new(); + if (ctx == nullptr) { + return -1; + } + + unsigned int mdlen = md.size(); + if (!wolfSSL_EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) || + !wolfSSL_EVP_DigestUpdate(ctx, rand.data(), rand.size()) || + !wolfSSL_EVP_DigestFinal_ex(ctx, md.data(), &mdlen)) { + wolfSSL_EVP_MD_CTX_free(ctx); + return -1; + } + + std::copy_n(std::begin(md), secretlen, secret); + wolfSSL_EVP_MD_CTX_free(ctx); + return 0; +} + +std::optional<std::string> read_token(const std::string_view &filename) { + auto f = wolfSSL_BIO_new_file(filename.data(), "r"); + if (f == nullptr) { + std::cerr << "Could not open token file " << filename << std::endl; + return {}; + } + + char *name, *header; + unsigned char *data; + long datalen; + std::string token; + if (wolfSSL_PEM_read_bio(f, &name, &header, &data, &datalen) != 1) { + std::cerr << "Could not read token file " << filename << std::endl; + wolfSSL_BIO_free(f); + return {}; + } + wolfSSL_BIO_free(f); + + wolfSSL_OPENSSL_free(name); + wolfSSL_OPENSSL_free(header); + + auto res = std::string{data, data + datalen}; + + wolfSSL_OPENSSL_free(data); + + return res; +} + +int write_token(const std::string_view &filename, const uint8_t *token, + size_t tokenlen) { + auto f = wolfSSL_BIO_new_file(filename.data(), "w"); + if (f == nullptr) { + std::cerr << "Could not write token in " << filename << std::endl; + return -1; + } + + wolfSSL_PEM_write_bio(f, "QUIC TOKEN", "", token, tokenlen); + wolfSSL_BIO_free(f); + + return 0; +} + +const char *crypto_default_ciphers() { + return "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_" + "SHA256:TLS_AES_128_CCM_SHA256"; +} + +const char *crypto_default_groups() { return "X25519:P-256:P-384:P-521"; } + +} // namespace util + +} // namespace ngtcp2 |