├── .gitmodules ├── AUTHORS ├── BUILDING.md ├── CMakeLists.txt ├── Doxyfile ├── LICENSE ├── README.md ├── TODO.md ├── TUTORIAL.md ├── cmake └── GenerateHeaderTest.cmake ├── dependency └── CMakeLists.txt ├── examples ├── CMakeLists.txt ├── echo │ ├── CMakeLists.txt │ ├── client.cc │ └── server.cc └── h3cli.cc ├── include └── nexus │ ├── error_code.hpp │ ├── global │ ├── context.hpp │ └── error.hpp │ ├── global_init.hpp │ ├── h3.hpp │ ├── h3 │ ├── client.hpp │ ├── error.hpp │ ├── fields.hpp │ ├── server.hpp │ └── stream.hpp │ ├── nexus.hpp │ ├── quic.hpp │ ├── quic │ ├── client.hpp │ ├── connection.hpp │ ├── connection_id.hpp │ ├── detail │ │ ├── connection_impl.hpp │ │ ├── connection_state.hpp │ │ ├── engine_impl.hpp │ │ ├── handler_ptr.hpp │ │ ├── operation.hpp │ │ ├── service.hpp │ │ ├── socket_impl.hpp │ │ ├── stream_impl.hpp │ │ ├── stream_open_handler.hpp │ │ └── stream_state.hpp │ ├── error.hpp │ ├── server.hpp │ ├── settings.hpp │ ├── socket.hpp │ ├── stream.hpp │ └── stream_id.hpp │ ├── ssl.hpp │ └── udp.hpp ├── src ├── CMakeLists.txt ├── client.cc ├── connection.cc ├── connection_state.cc ├── engine.cc ├── error.cc ├── global.cc ├── recv_header_set.hpp ├── server.cc ├── settings.cc ├── socket.cc ├── stream.cc └── stream_state.cc └── test ├── CMakeLists.txt ├── certificate.cc ├── certificate.hpp ├── h3 ├── CMakeLists.txt ├── test_connection_go_away.cc ├── test_fields.cc └── test_stream_shutdown.cc ├── headers └── CMakeLists.txt └── quic ├── CMakeLists.txt ├── test_client_work.cc ├── test_connection_id.cc ├── test_errors.cc ├── test_handshake.cc ├── test_lifetime.cc ├── test_server_accept.cc └── test_server_initiated_stream.cc /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dependency/boringssl"] 2 | path = dependency/boringssl 3 | url = https://boringssl.googlesource.com/boringssl 4 | [submodule "dependency/lsquic"] 5 | path = dependency/lsquic 6 | url = https://github.com/litespeedtech/lsquic 7 | [submodule "docs"] 8 | path = doc/html 9 | url = git@github.com:cbodley/nexus.git 10 | branch = githubpage 11 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Casey Bodley 2 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Building Nexus 2 | 3 | ## Dependencies 4 | 5 | * LiteSpeed QUIC (lsquic) 6 | * BoringSSL 7 | * Boost 8 | * zlib 9 | * googletest for tests 10 | 11 | BoringSSL and lsquic are included as git submodules, which must be initialized before building: 12 | 13 | ~/nexus $ git submodule update --init --recursive 14 | 15 | The boost, gtest and zlib dependencies must be installed manually. For example, on Fedora: 16 | 17 | ~/nexus $ sudo dnf install boost-devel gtest-devel zlib-devel 18 | 19 | ## Building 20 | 21 | Nexus uses the CMake build system. Start by creating a build directory: 22 | 23 | ~/nexus $ mkdir build && cd build 24 | 25 | Then invoke `cmake` to generate the build scripts: 26 | 27 | ~/nexus/build $ cmake .. 28 | 29 | Then build the library and its dependencies: 30 | 31 | ~/nexus/build $ cmake --build . 32 | 33 | You can run unit tests with `ctest`: 34 | 35 | ~/nexus/build $ ctest 36 | 37 | You can install nexus and its dependencies with: 38 | 39 | ~/nexus/build $ cmake --build . --target install 40 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.5.1) 2 | project(nexus) 3 | 4 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/") 5 | 6 | set(CMAKE_CXX_STANDARD 17) 7 | set(CMAKE_CXX_EXTENSIONS OFF) 8 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 9 | 10 | add_subdirectory(dependency) 11 | 12 | add_library(address-sanitizer INTERFACE) 13 | target_compile_options(address-sanitizer INTERFACE "-fsanitize=address,undefined") 14 | target_link_libraries(address-sanitizer INTERFACE "-fsanitize=address,undefined") 15 | 16 | add_library(thread-sanitizer INTERFACE) 17 | target_compile_options(thread-sanitizer INTERFACE "-fsanitize=thread") 18 | target_link_libraries(thread-sanitizer INTERFACE "-fsanitize=thread") 19 | 20 | add_library(nexus-headers INTERFACE) 21 | target_include_directories(nexus-headers INTERFACE include) 22 | target_link_libraries(nexus-headers INTERFACE asio) 23 | install(DIRECTORY include/nexus DESTINATION include) 24 | 25 | add_subdirectory(examples) 26 | add_subdirectory(src) 27 | 28 | enable_testing() 29 | add_subdirectory(test) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nexus HTTP/3 2 | 3 | Nexus is a C++ library for the QUIC and HTTP/3 protocols. 4 | 5 | For documentation, see https://cbodley.github.io/nexus/. 6 | 7 | For build instructions, see [BUILDING](BUILDING.md). 8 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## QUIC 4 | 5 | * stream prioritization 6 | - lsquic_stream_set_priority/lsquic_stream_set_priority for quic 7 | - lsquic_stream_get_http_prio/lsquic_stream_set_http_prio for h3 8 | * session resumption 9 | * push promises 10 | * connection migration 11 | 12 | ## connection 13 | 14 | * expose lsquic_conn_n_avail_streams()? 15 | * expose lsquic_conn_n_pending_streams/lsquic_conn_cancel_pending_streams? 16 | * option to pre-allocate stream_impls based on negotiated limits? 17 | * otherwise save closed streams and recycle them 18 | 19 | ## UDP 20 | 21 | * send packets with IP_PKTINFO 22 | * use sendmmsg()/recvmmsg() to reduce the number of system calls 23 | 24 | ## h3 25 | 26 | * allocator support for fields 27 | 28 | ## Async 29 | 30 | * maybe remove all synchronous interfaces and locking? 31 | * template everything on Executor type? difficult because it requires a lot more of the implementation in header files, and lsquic dependency is hidden from headers 32 | 33 | ## Boost vs. Standalone Asio 34 | 35 | * add #define to choose (along with std:: vs. boost::system::error_code) 36 | * then go finish std::net so we can use that instead 37 | 38 | ## test coverage 39 | 40 | * stream and connection is_open() 41 | * connection errors from stream interfaces 42 | * connection_impl lifetime vs handlers 43 | * socket_impl lifetime vs handlers 44 | 45 | ## possibly-interesting stuff not supported by lsquic 46 | 47 | * sending application errors via RESET_STREAM or CONNECTION_CLOSE 48 | * receiving application errors from RESET_STREAM 49 | * generic unidirectional streams 50 | * getting a callback after shutdown(1) once all data is acked, so no async_shutdown(1). this only works for close via es_delay_onclose 51 | -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | Nexus provides two sets of interfaces: one in `namespace nexus::quic` for generic QUIC clients and servers, and another in `namespace nexus::h3` for HTTP/3 clients and servers. 4 | 5 | ## Global Initialization 6 | 7 | The library must be initialized before use, and this is accomplished by creating a `nexus::global::context` with one of its factory functions: 8 | 9 | { 10 | auto global = nexus::global::init_client(); 11 | ... 12 | } // global cleanup on destruction 13 | 14 | ## TLS 15 | 16 | All QUIC connections are secure by default, so the use of TLS is not optional. TLS configuration is left to the application, which is expected to provide an initialized `boost::asio::ssl::context` to the client and server interfaces. 17 | 18 | ### TLS v1.3 19 | 20 | QUIC requires TLS version 1.3, so the ssl context should be initialized with a TLS 1.3 method, and its min/max protocol version should be set: 21 | 22 | auto ssl = boost::asio::ssl::context{boost::asio::ssl::context::tlsv13}; 23 | SSL_CTX_set_min_proto_version(ssl.native_handle(), TLS1_3_VERSION); 24 | SSL_CTX_set_max_proto_version(ssl.native_handle(), TLS1_3_VERSION); 25 | 26 | ### Certificate Verification 27 | 28 | All QUIC clients ["MUST authenticate the identity of the server"](https://www.rfc-editor.org/rfc/rfc9001.html#name-peer-authentication), and the application is responsible for providing an SSL context that does this: 29 | 30 | ssl.set_verify_mode(boost::asio::ssl::verify_peer); 31 | 32 | ## QUIC 33 | 34 | ### Application-Layer Protocol Negotiation 35 | 36 | QUIC endpoints use a TLS extension called [Application-Layer Protocol Negotiation](https://en.wikipedia.org/wiki/ALPN) (ALPN) to negotiate the application protocol. The HTTP/3 client and server automatically negotiate the "h3" protocol, but the generic QUIC client and server are protocol-agnostic so must negotiate this manually. 37 | 38 | The client provides its desired protocol names in a call to `SSL_CTX_set_alpn_protos()`. 39 | 40 | The server implements this negotiation using `SSL_CTX_set_alpn_select_cb()` and `SSL_select_next_proto()`, either selecting the first supported protocol in the client's list, or rejecting the client's handshake. 41 | 42 | The negotiated protocol can be queried on either side with `SSL_get0_alpn_selected()`. 43 | 44 | ### Settings 45 | 46 | As part of the connection handshake, the client and server exchange their QUIC [Transport Parameters](https://www.rfc-editor.org/rfc/rfc9000.html#transport-parameter-definitions). Some of the transport parameters we send, such as flow control limits and timeouts, can be configured with `class nexus::quic::settings`. 47 | 48 | Nexus also receives the peer's transport parameters, and will automatically respect the limits they impose. For example, once we've opened the maximum number of outgoing streams on the connection, requests to initiate a new outgoing stream will block until another stream closes. And once we reach a stream or connection flow control limit, requests to write more data will block until the peer reads some and adjusts the window. 49 | 50 | The client and server constructors take an optional `nexus::quic::settings` argument and, once constructed, these settings cannot be changed. 51 | 52 | ### Client 53 | 54 | The generic QUIC client (`class nexus::quic::client`) takes control of a bound `boost::asio::ip::udp::socket` and `boost::asio::ssl::context` to initiate secure connections (`class nexus::quic::connection`) to QUIC servers. 55 | 56 | auto client = nexus::quic::client{ex, bind_endpoint, ssl}; 57 | 58 | Initiating a client connection is instantaneous, and does not wait for the connection handshake to complete. This allows the application to start opening streams and staging data in the meantime. If the handshake does fail, the relevant error code will be delivered to any pending stream operations. 59 | 60 | A client initiates a connection either by calling `nexus::quic::client::connect()`: 61 | 62 | auto conn = nexus::quic::connection{client}; 63 | client.connect(conn, endpoint, hostname); 64 | 65 | Or by providing the remote endpoint and hostname arguments to the `nexus::quic::connection` constructor: 66 | 67 | auto conn = nexus::quic::connection{client, endpoint, hostname}; 68 | 69 | ### Server 70 | 71 | The generic QUIC server (`class nexus::quic::server`) listens on one or more UDP sockets (using `class nexus::quic::acceptor`) to accept secure connections from QUIC clients. 72 | 73 | auto server = nexus::quic::server{ex}; 74 | auto acceptor = nexus::quic::acceptor{server, bind_endpoint, ssl}; 75 | acceptor.listen(16); 76 | 77 | auto conn = nexus::quic::connection{acceptor}; 78 | acceptor.accept(conn); 79 | 80 | Unlike `nexus::quic::client::connect()` which returns immediately without waiting for the connection handshake, `nexus::quic::acceptor::accept()` only completes once the handshake is successful. 81 | 82 | ### Connection 83 | 84 | Once a generic QUIC connection (`class nexus::quic::connection`) has been connected or accepted, it can be used both to initiate outgoing streams with `nexus::quic::connection::connect()`: 85 | 86 | auto stream = nexus::quic::stream{conn}; 87 | conn.connect(stream); 88 | 89 | And to accept() incoming streams: 90 | 91 | auto stream = nexus::quic::stream{conn}; 92 | conn.accept(stream); 93 | 94 | When a connection closes, all related streams are closed and any pending operations are canceled with a connection error. 95 | 96 | ### Stream 97 | 98 | Once a generic QUIC stream (`class nexus::quic::stream`) has been connected or accepted, it provides bidirectional, reliable ordered delivery of data. 99 | 100 | For reads and writes, `nexus::quic::stream` models the asio concepts `AsyncReadStream`, `AsyncWriteStream`, `SyncReadStream` and `SyncWriteStream`, so can be used with the same read and write algorithms as `boost::asio::ip::tcp::socket`: 101 | 102 | char data[16]; // read 16 bytes from the stream 103 | auto bytes = boost::asio::read(stream, boost::asio::buffer(data)); 104 | 105 | The stream can be closed in one or both directions with `nexus::quic::stream::shutdown()` and `nexus::quic::stream::close()`. 106 | 107 | Writes may be buffered by the stream due to a preference to send full packets, similar to Nagle's algorithm for TCP. Buffered stream data can be flushed, either by calling `nexus::quic::stream::flush()` manually, shutting down the stream for write, or closing the stream. 108 | 109 | Graceful shutdown of a QUIC stream differs from normal TCP sockets, which can be closed immediately even if there is unsent or unacked data, and the kernel will continue to (re)transmit data in the background until everything is acked. Because this transmission in QUIC depends on an open connection, the application must not close the associated `nexus::quic::connection` until the stream shutdown completes. To support this graceful shutdown, the behavior of `nexus::quic::stream::close()` matches that of the socket option `SO_LINGER`, where the close does not complete until all stream data has been successfully acked. `nexus::quic::stream::async_close()` is provided for asynchronous graceful shutdown. 110 | 111 | A separate function `nexus::quic::stream::reset()` is provided for immediate shutdown, and the `nexus::quic::stream::~stream()` destructor calls `nexus::quic::stream::reset()` instead of `nexus::quic::stream::close()`. 112 | 113 | ## Synchronous and Asynchronous 114 | 115 | For each potentially-blocking operation, both a synchronous or asynchronous version of the function are provided. The asynchronous function names all begin with `async_`, and attempt to meet all "Requirements on asynchronous operations" specified by asio. 116 | 117 | However, QUIC requires that we regularly poll its sockets for incoming packets, respond with acknowledgements and other control messages, and resend unacknowledged packets. The Nexus client and server classes issue asynchronous operations for this background work, and expect their associated execution context to process those operations in a timely manner - for example, by calling `boost::asio::io_context::run()`. 118 | 119 | This is unlike the asio I/O objects you may be familiar with, which can be used exclusively in the synchronous blocking mode without requiring another thread to process asynchronous work. 120 | 121 | ## Exceptions 122 | 123 | The library should be useable with or without exceptions, so both a throwing and non-throwing version of each function is provided. Their signatures are the same, except the non-throwing version takes a mutable reference to error_code as an additional parameter. 124 | 125 | ## Thread Safety 126 | 127 | Global init/shutdown is not thread-safe with respect to other classes. 128 | 129 | All engine operations can be considered thread-safe. The lsquic engine instance is not re-entrant, so an engine-wide mutex is used to serialize access to the engine and its related state. This mutex is held over all calls into the lsquic engine, including its calls to the callback functions we provide. No blocking system calls are made under this mutex, as lsquic does no i/o of its own. 130 | -------------------------------------------------------------------------------- /cmake/GenerateHeaderTest.cmake: -------------------------------------------------------------------------------- 1 | file(WRITE ${HEADER_TEST_FILENAME} "#include <${HEADER_TEST_INCLUDE_PATH}>\n") 2 | -------------------------------------------------------------------------------- /dependency/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(ZLIB REQUIRED) 2 | 3 | add_subdirectory(boringssl) 4 | 5 | set(BORINGSSL_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/boringssl/include) 6 | set(BORINGSSL_LIB_ssl ssl) 7 | set(BORINGSSL_LIB_crypto crypto) 8 | set(BORINGSSL_LIB_decrepit decrepit) 9 | 10 | add_subdirectory(lsquic) 11 | 12 | cmake_policy(SET CMP0079 NEW) 13 | target_include_directories(lsquic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/lsquic/include ${BORINGSSL_INCLUDE}) 14 | target_link_libraries(lsquic PUBLIC ssl crypto decrepit ZLIB::ZLIB) 15 | 16 | #find_package(Boost REQUIRED COMPONENTS system) 17 | add_library(asio INTERFACE) 18 | target_compile_definitions(asio INTERFACE -DASIO_NO_TS_EXECUTORS) 19 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(echo) 2 | 3 | add_executable(h3cli h3cli.cc) 4 | target_link_libraries(h3cli nexus address-sanitizer) 5 | -------------------------------------------------------------------------------- /examples/echo/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(nexus_echo_client client.cc) 2 | target_link_libraries(nexus_echo_client nexus address-sanitizer) 3 | 4 | add_executable(nexus_echo_server server.cc) 5 | target_link_libraries(nexus_echo_server nexus address-sanitizer) 6 | -------------------------------------------------------------------------------- /examples/echo/client.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // echo client takes one or more input files, writes each file in parallel 12 | // to a different stream and reads back their echos for display to stdout. 13 | // because the streams are multiplexed, the output from multiple files will be 14 | // mixed together; however, running against a server with max-streams=1 will 15 | // display their output in sequential order 16 | 17 | namespace { 18 | 19 | struct configuration { 20 | const char* hostname; 21 | const char* portstr; 22 | char** files_begin; 23 | char** files_end; 24 | }; 25 | 26 | configuration parse_args(int argc, char** argv) 27 | { 28 | if (argc < 4) { 29 | std::cerr << "Usage: " << argv[0] << " [filenames...]\n"; 30 | ::exit(EXIT_FAILURE); 31 | } 32 | configuration config; 33 | config.hostname = argv[1]; 34 | config.portstr = argv[2]; 35 | config.files_begin = std::next(argv, 3); 36 | config.files_end = std::next(argv, argc); 37 | return config; 38 | } 39 | 40 | using boost::asio::ip::udp; 41 | using nexus::error_code; 42 | using nexus::system_error; 43 | 44 | using buffer_type = std::array; 45 | 46 | template 47 | using ref_counter = boost::intrusive_ref_counter; 48 | 49 | struct echo_connection : ref_counter { 50 | nexus::quic::client& client; 51 | nexus::quic::connection conn; 52 | 53 | echo_connection(nexus::quic::client& client, 54 | const udp::endpoint& endpoint, 55 | const char* hostname) 56 | : client(client), conn(client, endpoint, hostname) 57 | {} 58 | ~echo_connection() { 59 | client.close(); 60 | } 61 | }; 62 | 63 | using connection_ptr = boost::intrusive_ptr; 64 | 65 | struct echo_stream : ref_counter { 66 | connection_ptr conn; 67 | nexus::quic::stream stream; 68 | std::ifstream input; 69 | std::ostream& output; 70 | buffer_type readbuf; 71 | buffer_type writebuf; 72 | echo_stream(connection_ptr conn, const char* filename, std::ostream& output) 73 | : conn(std::move(conn)), stream(this->conn->conn), 74 | input(filename), output(output) 75 | {} 76 | }; 77 | using stream_ptr = boost::intrusive_ptr; 78 | 79 | void write_file(stream_ptr stream) 80 | { 81 | // read from input 82 | auto& data = stream->writebuf; 83 | stream->input.read(data.data(), data.size()); 84 | const auto bytes = stream->input.gcount(); 85 | // write to stream 86 | auto& s = stream->stream; 87 | boost::asio::async_write(s, boost::asio::buffer(data.data(), bytes), 88 | [stream=std::move(stream)] (error_code ec, size_t bytes) { 89 | if (ec) { 90 | std::cerr << "async_write failed with " << ec.message() << '\n'; 91 | } else if (!stream->input) { // no more input, done writing 92 | stream->stream.shutdown(1); 93 | } else { 94 | write_file(std::move(stream)); 95 | } 96 | }); 97 | } 98 | 99 | void read_file(stream_ptr stream) 100 | { 101 | // read back the echo 102 | auto& data = stream->readbuf; 103 | auto& s = stream->stream; 104 | s.async_read_some(boost::asio::buffer(data), 105 | [stream=std::move(stream)] (error_code ec, size_t bytes) { 106 | if (ec) { 107 | if (ec != nexus::quic::stream_error::eof) { 108 | std::cerr << "async_read_some returned " << ec.message() << '\n'; 109 | } 110 | return; 111 | } 112 | // write the output bytes then start reading more 113 | auto& data = stream->readbuf; 114 | stream->output.write(data.data(), bytes); 115 | read_file(std::move(stream)); 116 | }); 117 | } 118 | 119 | } // anonymous namespace 120 | 121 | int main(int argc, char** argv) 122 | { 123 | const auto cfg = parse_args(argc, argv); 124 | 125 | auto context = boost::asio::io_context{}; 126 | auto ex = context.get_executor(); 127 | const auto endpoint = [&] { 128 | auto resolver = udp::resolver{ex}; 129 | return resolver.resolve(cfg.hostname, cfg.portstr)->endpoint(); 130 | }(); 131 | 132 | auto ssl = boost::asio::ssl::context{boost::asio::ssl::context::tlsv13}; 133 | ::SSL_CTX_set_min_proto_version(ssl.native_handle(), TLS1_3_VERSION); 134 | ::SSL_CTX_set_max_proto_version(ssl.native_handle(), TLS1_3_VERSION); 135 | const unsigned char alpn[] = {4,'e','c','h','o'}; 136 | ::SSL_CTX_set_alpn_protos(ssl.native_handle(), alpn, sizeof(alpn)); 137 | 138 | auto global = nexus::global::init_client(); 139 | auto client = nexus::quic::client{ex, udp::endpoint{endpoint.protocol(), 0}, ssl}; 140 | 141 | auto conn = connection_ptr{new echo_connection(client, endpoint, cfg.hostname)}; 142 | 143 | // connect a stream for each input file 144 | for (auto f = cfg.files_begin; f != cfg.files_end; ++f) { 145 | auto s = stream_ptr{new echo_stream(conn, *f, std::cout)}; 146 | auto& stream = s->stream; 147 | conn->conn.async_connect(stream, [s=std::move(s)] (error_code ec) { 148 | if (ec) { 149 | std::cerr << "async_connect failed with " << ec.message() << '\n'; 150 | return; 151 | } 152 | write_file(s); 153 | read_file(std::move(s)); 154 | }); 155 | } 156 | conn.reset(); // let the connection close once all streams are done 157 | 158 | context.run(); 159 | return 0; 160 | } 161 | -------------------------------------------------------------------------------- /examples/echo/server.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | // echo server that accepts connections and their streams, writing back 15 | // anything it reads on each stream 16 | 17 | namespace { 18 | 19 | struct configuration { 20 | const char* hostname; 21 | const char* portstr; 22 | std::string cert; 23 | std::string key; 24 | std::optional max_streams; 25 | }; 26 | 27 | configuration parse_args(int argc, char** argv) 28 | { 29 | if (argc < 5) { 30 | std::cerr << "Usage: " << argv[0] << " [max-streams]\n"; 31 | ::exit(EXIT_FAILURE); 32 | } 33 | configuration config; 34 | config.hostname = argv[1]; 35 | config.portstr = argv[2]; 36 | config.cert = argv[3]; 37 | config.key = argv[4]; 38 | if (argc > 5) { // parse max-streams 39 | const auto begin = argv[5]; 40 | const auto end = begin + strlen(begin); 41 | uint32_t value; 42 | const auto result = std::from_chars(begin, end, value); 43 | if (auto ec = make_error_code(result.ec); ec) { 44 | std::cerr << "failed to parse max-streams \"" << argv[5] 45 | << "\": " << ec.message() << '\n'; 46 | ::exit(EXIT_FAILURE); 47 | } 48 | config.max_streams = value; 49 | } 50 | return config; 51 | } 52 | 53 | using boost::asio::ip::udp; 54 | using nexus::error_code; 55 | using nexus::system_error; 56 | 57 | int alpn_select_cb(SSL* ssl, const unsigned char** out, unsigned char* outlen, 58 | const unsigned char* in, unsigned int inlen, void* arg) 59 | { 60 | const unsigned char alpn[] = {4,'e','c','h','o'}; 61 | int r = ::SSL_select_next_proto(const_cast(out), outlen, 62 | const_cast(in), inlen, 63 | alpn, sizeof(alpn)); 64 | if (r == OPENSSL_NPN_NEGOTIATED) { 65 | return SSL_TLSEXT_ERR_OK; 66 | } else { 67 | return SSL_TLSEXT_ERR_ALERT_FATAL; 68 | } 69 | } 70 | 71 | template 72 | using ref_counter = boost::intrusive_ref_counter; 73 | 74 | struct echo_connection : ref_counter { 75 | nexus::quic::connection conn; 76 | 77 | explicit echo_connection(nexus::quic::acceptor& acceptor) 78 | : conn(acceptor) {} 79 | ~echo_connection() { 80 | std::cerr << "connection closed\n"; 81 | } 82 | }; 83 | using connection_ptr = boost::intrusive_ptr; 84 | 85 | struct echo_stream { 86 | connection_ptr conn; 87 | nexus::quic::stream stream; 88 | std::array buffer; 89 | 90 | explicit echo_stream(connection_ptr conn) 91 | : conn(std::move(conn)), stream(this->conn->conn) {} 92 | }; 93 | 94 | void on_stream_write(std::unique_ptr s, 95 | error_code ec, size_t bytes); 96 | 97 | void on_stream_read(std::unique_ptr s, 98 | error_code ec, size_t bytes) 99 | { 100 | auto& stream = s->stream; 101 | if (ec == nexus::quic::stream_error::eof) { 102 | // done reading and all writes were submitted, wait for the acks and shut 103 | // down gracefully 104 | stream.async_close([s=std::move(s)] (error_code ec) { 105 | if (ec) { 106 | std::cerr << "stream close failed with " << ec.message() << '\n'; 107 | } else { 108 | std::cerr << "stream closed\n"; 109 | } 110 | }); 111 | return; 112 | } 113 | if (ec) { 114 | std::cerr << "read failed with " << ec.message() << '\n'; 115 | return; 116 | } 117 | // echo the buffer back to the client 118 | auto& data = s->buffer; 119 | boost::asio::async_write(stream, boost::asio::buffer(data.data(), bytes), 120 | [s=std::move(s)] (error_code ec, size_t bytes) mutable { 121 | on_stream_write(std::move(s), ec, bytes); 122 | }); 123 | } 124 | 125 | void on_stream_write(std::unique_ptr s, 126 | error_code ec, size_t bytes) 127 | { 128 | if (ec) { 129 | std::cerr << "write failed with " << ec.message() << '\n'; 130 | return; 131 | } 132 | // read the next buffer from the client 133 | auto& stream = s->stream; 134 | auto& data = s->buffer; 135 | stream.async_read_some(boost::asio::buffer(data), 136 | [s=std::move(s)] (error_code ec, size_t bytes) mutable { 137 | on_stream_read(std::move(s), ec, bytes); 138 | }); 139 | } 140 | 141 | void accept_streams(connection_ptr c) 142 | { 143 | auto s = std::make_unique(c); 144 | auto& stream = s->stream; 145 | auto& conn = c->conn; 146 | conn.async_accept(stream, 147 | [c=std::move(c), s=std::move(s)] (error_code ec) mutable { 148 | if (ec) { 149 | std::cerr << "stream accept failed with " << ec.message() << '\n'; 150 | return; 151 | } 152 | // start next accept 153 | accept_streams(std::move(c)); 154 | // start reading from stream 155 | std::cerr << "new stream\n"; 156 | auto& stream = s->stream; 157 | auto& data = s->buffer; 158 | stream.async_read_some(boost::asio::buffer(data), 159 | [s=std::move(s)] (error_code ec, size_t bytes) mutable { 160 | on_stream_read(std::move(s), ec, bytes); 161 | }); 162 | }); 163 | } 164 | 165 | void accept_connections(nexus::quic::server& server, 166 | nexus::quic::acceptor& acceptor) 167 | { 168 | auto conn = connection_ptr{new echo_connection(acceptor)}; 169 | auto& c = conn->conn; 170 | acceptor.async_accept(c, 171 | [&server, &acceptor, conn=std::move(conn)] (error_code ec) { 172 | if (ec) { 173 | std::cerr << "accept failed with " << ec.message() 174 | << ", shutting down\n"; 175 | server.close(); 176 | return; 177 | } 178 | // start next accept 179 | accept_connections(server, acceptor); 180 | std::cerr << "new connection\n"; 181 | // start accepting streams on the connection 182 | accept_streams(std::move(conn)); 183 | }); 184 | } 185 | 186 | } // anonymous namespace 187 | 188 | int main(int argc, char** argv) 189 | { 190 | const auto cfg = parse_args(argc, argv); 191 | 192 | auto context = boost::asio::io_context{}; 193 | boost::asio::any_io_executor ex = context.get_executor(); 194 | const auto endpoint = [&] { 195 | auto resolver = udp::resolver{ex}; 196 | return resolver.resolve(cfg.hostname, cfg.portstr)->endpoint(); 197 | }(); 198 | 199 | auto ssl = boost::asio::ssl::context{boost::asio::ssl::context::tlsv13}; 200 | ::SSL_CTX_set_min_proto_version(ssl.native_handle(), TLS1_3_VERSION); 201 | ::SSL_CTX_set_max_proto_version(ssl.native_handle(), TLS1_3_VERSION); 202 | ::SSL_CTX_set_alpn_select_cb(ssl.native_handle(), alpn_select_cb, nullptr); 203 | 204 | ssl.use_certificate_chain_file(cfg.cert); 205 | ssl.use_private_key_file(cfg.key, boost::asio::ssl::context::file_format::pem); 206 | 207 | auto global = nexus::global::init_server(); 208 | auto settings = nexus::quic::default_server_settings(); 209 | if (cfg.max_streams) { 210 | settings.max_streams_per_connection = *cfg.max_streams; 211 | } 212 | auto server = nexus::quic::server{ex, settings}; 213 | auto acceptor = nexus::quic::acceptor{server, endpoint, ssl}; 214 | acceptor.listen(16); 215 | 216 | accept_connections(server, acceptor); 217 | context.run(); 218 | return 0; 219 | } 220 | -------------------------------------------------------------------------------- /examples/h3cli.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | using boost::asio::ip::udp; 10 | using nexus::error_code; 11 | using body_buffer = std::array; 12 | 13 | static void read_print_stream(nexus::h3::stream& stream, 14 | nexus::h3::client_connection& conn, 15 | body_buffer& buffer) 16 | { 17 | stream.async_read_some(boost::asio::buffer(buffer), 18 | [&] (error_code ec, size_t bytes) { 19 | if (ec) { 20 | if (ec != nexus::quic::stream_error::eof) { 21 | std::cerr << "async_read_some failed: " << ec.message() << std::endl; 22 | } 23 | conn.close(); 24 | return; 25 | } 26 | std::cout.write(buffer.data(), bytes); 27 | read_print_stream(stream, conn, buffer); 28 | }); 29 | } 30 | 31 | int main(int argc, char** argv) { 32 | // parse argv for 33 | if (argc < 4) { 34 | std::cerr << "Usage: " << argv[0] << " " << std::endl; 35 | return EXIT_FAILURE; 36 | } 37 | const char* hostname = argv[1]; 38 | const std::string_view portstr = argv[2]; 39 | const std::string_view path = argv[3]; 40 | 41 | auto ioc = boost::asio::io_context{}; 42 | auto ex = ioc.get_executor(); 43 | 44 | auto ssl = boost::asio::ssl::context{boost::asio::ssl::context::tlsv13}; 45 | ::SSL_CTX_set_min_proto_version(ssl.native_handle(), TLS1_3_VERSION); 46 | ::SSL_CTX_set_max_proto_version(ssl.native_handle(), TLS1_3_VERSION); 47 | 48 | // resolve hostname 49 | const auto remote_endpoint = [&] { 50 | auto resolver = udp::resolver{ex}; 51 | return resolver.resolve(hostname, portstr)->endpoint(); 52 | }(); 53 | 54 | auto global = nexus::global::init_client(); 55 | auto client = nexus::h3::client{ex, udp::endpoint{}, ssl}; 56 | auto conn = nexus::h3::client_connection{client}; 57 | client.connect(conn, remote_endpoint, hostname); 58 | auto stream = nexus::h3::stream{conn}; 59 | 60 | auto request = nexus::h3::fields{}; 61 | request.insert(":method", "GET"); 62 | request.insert(":scheme", "https"); 63 | request.insert(":path", path); 64 | request.insert(":authority", hostname); 65 | request.insert("user-agent", "h3cli/lsquic"); 66 | auto response = nexus::h3::fields{}; 67 | auto buffer = body_buffer{}; 68 | 69 | conn.async_connect(stream, [&] (error_code ec) { 70 | if (ec) { 71 | std::cerr << "async_connect failed: " << ec.message() << std::endl; 72 | client.close(); 73 | return; 74 | } 75 | stream.async_write_headers(request, [&] (error_code ec) { 76 | if (ec) { 77 | std::cerr << "async_write_headers failed: " << ec.message() << std::endl; 78 | client.close(); 79 | return; 80 | } 81 | stream.shutdown(1); 82 | stream.async_read_headers(response, [&] (error_code ec) { 83 | if (ec) { 84 | std::cerr << "async_read_headers failed: " << ec.message() << std::endl; 85 | client.close(); 86 | return; 87 | } 88 | for (const auto& f : response) { 89 | std::cout << f.c_str() << "\r\n"; 90 | } 91 | std::cout << "\r\n"; 92 | read_print_stream(stream, conn, buffer); 93 | }); 94 | }); 95 | }); 96 | 97 | ioc.run(); 98 | return 0; 99 | } 100 | -------------------------------------------------------------------------------- /include/nexus/error_code.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #define SYSTEM_ERROR_NAMESPACE boost::system 7 | 8 | namespace nexus { 9 | 10 | namespace errc = boost::system::errc; 11 | using boost::system::error_code; 12 | using boost::system::error_condition; 13 | using boost::system::error_category; 14 | using boost::system::system_category; 15 | using boost::system::system_error; 16 | 17 | } // namespace nexus 18 | -------------------------------------------------------------------------------- /include/nexus/global/context.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace nexus::global { 8 | 9 | class context; 10 | 11 | namespace detail { 12 | 13 | context init(int flags, error_code& ec); 14 | 15 | } // namespace detail 16 | 17 | /// a context object representing the global initialization of the nexus 18 | /// QUIC/HTTP library, and its consequent global cleanup on destruction 19 | class context { 20 | friend context detail::init(int flags, error_code& ec); 21 | using cleanup_fn = void (*)(); 22 | cleanup_fn cleanup; 23 | context(cleanup_fn cleanup) noexcept : cleanup(cleanup) {} 24 | public: 25 | /// construct an uninitialized context 26 | context() noexcept : cleanup(nullptr) {} 27 | /// move construct, claiming ownership of another context's cleanup 28 | context(context&& o) noexcept : cleanup(std::exchange(o.cleanup, nullptr)) {} 29 | /// move assign, swapping ownership with another context 30 | context& operator=(context&& o) noexcept { 31 | std::swap(cleanup, o.cleanup); 32 | return *this; 33 | } 34 | /// perform global shutdown if initialized 35 | ~context() { 36 | if (cleanup) { 37 | shutdown(); 38 | } 39 | } 40 | 41 | /// return true if the context represents successful initialization 42 | operator bool() const { return cleanup; } 43 | 44 | /// enable log output to stderr, where the log level is one of: 45 | /// emerg, alert, crit, error, warn, notice, info, debug 46 | void log_to_stderr(const char* level); 47 | 48 | /// perform global shutdown of an initialized context 49 | void shutdown() { 50 | cleanup(); 51 | } 52 | }; 53 | 54 | } // namespace nexus::global 55 | -------------------------------------------------------------------------------- /include/nexus/global/error.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace nexus::global { 6 | 7 | /// global error category 8 | const error_category& global_category(); 9 | 10 | /// global error codes 11 | enum class error { 12 | init_failed = 1, //< global initialization failed 13 | }; 14 | 15 | inline error_code make_error_code(error e) 16 | { 17 | return {static_cast(e), global_category()}; 18 | } 19 | 20 | inline error_condition make_error_condition(error e) 21 | { 22 | return {static_cast(e), global_category()}; 23 | } 24 | 25 | } // namespace nexus::global 26 | -------------------------------------------------------------------------------- /include/nexus/global_init.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | /// global initialization 6 | namespace nexus::global { 7 | 8 | /// initialize the library for clients only 9 | /// \relatesalso context 10 | context init_client(error_code& ec); 11 | /// initialize the library for clients only 12 | /// \relatesalso context 13 | context init_client(); 14 | 15 | /// initialize the library for server only 16 | /// \relatesalso context 17 | context init_server(error_code& ec); 18 | /// initialize the library for server only 19 | /// \relatesalso context 20 | context init_server(); 21 | 22 | /// initialize the library for client and server use 23 | /// \relatesalso context 24 | context init_client_server(error_code& ec); 25 | /// initialize the library for client and server use 26 | /// \relatesalso context 27 | context init_client_server(); 28 | 29 | } // namespace nexus::global 30 | -------------------------------------------------------------------------------- /include/nexus/h3.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /// HTTP/3 library 4 | namespace nexus::h3 {} 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | -------------------------------------------------------------------------------- /include/nexus/h3/client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace nexus::h3 { 7 | 8 | class client_connection; 9 | class stream; 10 | 11 | /// an HTTP/3 client that owns a UDP socket and uses it to service client 12 | /// connections 13 | class client { 14 | friend class client_connection; 15 | quic::detail::engine_impl engine; 16 | quic::detail::socket_impl socket; 17 | public: 18 | /// the polymorphic executor type, boost::asio::any_io_executor 19 | using executor_type = quic::detail::engine_impl::executor_type; 20 | 21 | /// construct the client, taking ownership of a bound UDP socket 22 | client(udp::socket&& socket, ssl::context& ctx); 23 | 24 | /// construct the client, taking ownership of a bound UDP socket 25 | client(udp::socket&& socket, ssl::context& ctx, 26 | const quic::settings& s); 27 | 28 | /// construct the client and bind a UDP socket to the given endpoint 29 | client(const executor_type& ex, const udp::endpoint& endpoint, 30 | ssl::context& ctx); 31 | 32 | /// construct the client and bind a UDP socket to the given endpoint 33 | client(const executor_type& ex, const udp::endpoint& endpoint, 34 | ssl::context& ctx, const quic::settings& s); 35 | 36 | /// return the associated io executor 37 | executor_type get_executor() const; 38 | 39 | /// return the socket's locally-bound address/port 40 | udp::endpoint local_endpoint() const; 41 | 42 | /// open a connection to the given remote endpoint and hostname. this 43 | /// initiates the TLS handshake, but returns immediately without waiting 44 | /// for the handshake to complete 45 | void connect(client_connection& conn, 46 | const udp::endpoint& endpoint, 47 | const char* hostname); 48 | 49 | /// close the socket, along with any related connections 50 | void close(error_code& ec); 51 | /// \overload 52 | void close(); 53 | }; 54 | 55 | /// an HTTP/3 connection that can initiate outgoing streams and accept 56 | /// server-pushed streams 57 | class client_connection { 58 | friend class client; 59 | friend class stream; 60 | quic::detail::connection_impl impl; 61 | public: 62 | /// the polymorphic executor type, boost::asio::any_io_executor 63 | using executor_type = quic::detail::connection_impl::executor_type; 64 | 65 | /// construct a client-side connection for use with connect() 66 | explicit client_connection(client& c) : impl(c.socket) {} 67 | 68 | /// open a connection to the given remote endpoint and hostname. this 69 | /// initiates the TLS handshake, but returns immediately without waiting 70 | /// for the handshake to complete 71 | client_connection(client& c, const udp::endpoint& endpoint, 72 | const char* hostname) : impl(c.socket) { 73 | c.connect(*this, endpoint, hostname); 74 | } 75 | 76 | /// return the associated io executor 77 | executor_type get_executor() const; 78 | 79 | /// determine whether the connection is open 80 | bool is_open() const; 81 | 82 | /// return the connection id if open 83 | quic::connection_id id(error_code& ec) const; 84 | /// \overload 85 | quic::connection_id id() const; 86 | 87 | /// return the remote's address/port if open 88 | udp::endpoint remote_endpoint(error_code& ec) const; 89 | /// \overload 90 | udp::endpoint remote_endpoint() const; 91 | 92 | /// open an outgoing stream 93 | template // void(error_code) 94 | decltype(auto) async_connect(stream& s, CompletionToken&& token) { 95 | return impl.async_connect(s, std::forward(token)); 96 | } 97 | /// \overload 98 | void connect(stream& s, error_code& ec); 99 | /// \overload 100 | void connect(stream& s); 101 | 102 | /// send a GOAWAY frame and stop initiating or accepting new streams 103 | void go_away(error_code& ec); 104 | /// \overload 105 | void go_away(); 106 | 107 | /// close the connection, along with any related streams 108 | void close(error_code& ec); 109 | /// \overload 110 | void close(); 111 | }; 112 | 113 | } // namespace nexus::h3 114 | -------------------------------------------------------------------------------- /include/nexus/h3/error.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace nexus::h3 { 6 | 7 | /// http3-specific transport error codes 8 | enum class error { 9 | no_error = 0x100, 10 | general_protocol_error = 0x101, 11 | internal_error = 0x102, 12 | stream_creation_error = 0x103, 13 | closed_critical_stream = 0x104, 14 | frame_unexpected = 0x105, 15 | frame_error = 0x106, 16 | excessive_load = 0x107, 17 | id_error = 0x108, 18 | settings_error = 0x109, 19 | missing_settings = 0x10a, 20 | request_rejected = 0x10b, 21 | request_cancelled = 0x10c, 22 | request_incomplete = 0x10d, 23 | message_error = 0x10e, 24 | connect_error = 0x10f, 25 | version_fallback = 0x110, 26 | qpack_decompression_failed = 0x200, 27 | qpack_encoder_stream_error = 0x201, 28 | qpack_decoder_stream_error = 0x202, 29 | }; 30 | 31 | /// h3 error category 32 | const error_category& h3_category(); 33 | 34 | inline error_code make_error_code(error e) 35 | { 36 | return {static_cast(e), h3_category()}; 37 | } 38 | 39 | inline error_condition make_error_condition(error e) 40 | { 41 | return {static_cast(e), h3_category()}; 42 | } 43 | 44 | } // namespace nexus::quic 45 | 46 | namespace SYSTEM_ERROR_NAMESPACE { 47 | 48 | /// enables implicit conversion to std::error_code 49 | template <> 50 | struct is_error_code_enum : public std::true_type {}; 51 | 52 | } // namespace SYSTEM_ERROR_NAMESPACE 53 | -------------------------------------------------------------------------------- /include/nexus/h3/fields.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | namespace nexus::h3 { 11 | 12 | /// an immutable key/value pair to represent a single header 13 | class field : public boost::intrusive::list_base_hook<>, 14 | public boost::intrusive::set_base_hook<> 15 | { 16 | friend class fields; 17 | using size_type = uint16_t; 18 | size_type name_size; 19 | size_type value_size; 20 | uint8_t never_index_; 21 | char buffer[3]; // accounts for delimiter and null terminator 22 | 23 | static constexpr auto delim = std::string_view{": "}; 24 | 25 | // private constructor, use the create() factory function instead 26 | field(std::string_view name, std::string_view value, uint8_t never_index) 27 | : name_size(name.size()), value_size(value.size()), 28 | never_index_(never_index) { 29 | auto pos = std::copy(name.begin(), name.end(), buffer); 30 | pos = std::copy(delim.begin(), delim.end(), pos); 31 | pos = std::copy(value.begin(), value.end(), pos); 32 | *pos = '\0'; 33 | } 34 | public: 35 | field(const field&) = delete; 36 | field& operator=(const field&) = delete; 37 | 38 | /// return a view of the field name 39 | std::string_view name() const { 40 | return {buffer, name_size}; 41 | } 42 | /// return a view of the field value 43 | std::string_view value() const { 44 | return {buffer + name_size + delim.size(), value_size}; 45 | } 46 | 47 | /// enable or disable the caching of this field for header compression 48 | void never_index(bool value) { never_index_ = value; } 49 | /// return whether or not this field can be cached for header compression 50 | bool never_index() const { return never_index_; } 51 | 52 | /// return a null-terminated string of the form ": " 53 | const char* c_str() const { return buffer; } 54 | /// return a null-terminated string of the form ": " 55 | const char* data() const { return buffer; } 56 | /// return the string length of c_str() 57 | size_type size() const { return name_size + delim.size() + value_size; } 58 | 59 | private: 60 | struct deleter { 61 | void operator()(field* f) { 62 | const size_t size = sizeof(field) + f->name_size + f->value_size; 63 | using Alloc = std::allocator; 64 | using Traits = std::allocator_traits; 65 | auto alloc = Alloc{}; 66 | Traits::destroy(alloc, f); 67 | Traits::deallocate(alloc, reinterpret_cast(f), size); 68 | } 69 | }; 70 | using ptr = std::unique_ptr; 71 | 72 | // allocate just enough memory to hold the given name and value 73 | static ptr create(std::string_view name, std::string_view value, 74 | bool never_index) 75 | { 76 | const size_t size = sizeof(field) + name.size() + value.size(); 77 | using Alloc = std::allocator; 78 | using Traits = std::allocator_traits; 79 | auto alloc = Alloc{}; 80 | auto p = Traits::allocate(alloc, size); 81 | try { 82 | return ptr{new (p) field(name, value, never_index)}; 83 | } catch (const std::exception&) { 84 | Traits::deallocate(alloc, p, size); 85 | throw; 86 | } 87 | } 88 | }; 89 | 90 | namespace detail { 91 | 92 | // case-insensitive field comparison for sorting in multiset 93 | struct field_compare { 94 | bool operator()(char lhs, char rhs) const { 95 | return std::tolower(lhs) < std::tolower(rhs); 96 | } 97 | bool operator()(std::string_view lhs, std::string_view rhs) const { 98 | return std::lexicographical_compare(lhs.begin(), lhs.end(), 99 | rhs.begin(), rhs.end(), *this); 100 | } 101 | bool operator()(const field& lhs, const field& rhs) const { 102 | return (*this)(lhs.name(), rhs.name()); 103 | } 104 | bool operator()(std::string_view lhs, const field& rhs) const { 105 | return (*this)(lhs, rhs.name()); 106 | } 107 | bool operator()(const field& lhs, std::string_view rhs) const { 108 | return (*this)(lhs.name(), rhs); 109 | } 110 | }; 111 | 112 | struct field_key { 113 | using type = std::string_view; 114 | type operator()(const field& f) { return f.name(); } 115 | }; 116 | 117 | using field_multiset = boost::intrusive::multiset, 119 | boost::intrusive::key_of_value>; 120 | 121 | using field_list = boost::intrusive::list, 123 | boost::intrusive::cache_last, 124 | boost::intrusive::size_type>; 125 | 126 | } // namespace detail 127 | 128 | /// an ordered list of headers for an http request or response. all field name 129 | /// comparisons are case-insensitive 130 | class fields { 131 | using list_type = detail::field_list; 132 | list_type list; 133 | // maintain an index of the names for efficient searching 134 | using multiset_type = detail::field_multiset; 135 | multiset_type set; 136 | public: 137 | /// construct an empty list of fields 138 | fields() = default; 139 | /// move-construct the fields, leaving o empty 140 | fields(fields&& o) = default; 141 | /// move-assign the fields, leaving o empty 142 | fields& operator=(fields&& o) { 143 | clear(); 144 | list = std::move(o.list); 145 | set = std::move(o.set); 146 | return *this; 147 | } 148 | ~fields() { clear(); } 149 | 150 | using size_type = list_type::size_type; 151 | /// return the total number of fields in the list 152 | size_type size() const { return list.size(); } 153 | 154 | bool empty() const { return list.empty(); } 155 | 156 | using value_type = field; 157 | using iterator = list_type::iterator; 158 | using const_iterator = list_type::const_iterator; 159 | 160 | iterator begin() { return list.begin(); } 161 | const_iterator begin() const { return list.begin(); } 162 | const_iterator cbegin() const { return list.cbegin(); } 163 | 164 | iterator end() { return list.end(); } 165 | const_iterator end() const { return list.end(); } 166 | const_iterator cend() const { return list.cend(); } 167 | 168 | /// return the number of fields that match the given name 169 | size_type count(std::string_view name) const { 170 | return set.count(name); 171 | } 172 | 173 | /// return an iterator to the first field that matches the given name 174 | iterator find(std::string_view name) { 175 | if (auto i = set.find(name); i != set.end()) { 176 | return list.iterator_to(*i); 177 | } 178 | return list.end(); 179 | } 180 | 181 | /// return an iterator to the first field that matches the given name 182 | const_iterator find(std::string_view name) const { 183 | if (auto i = set.find(name); i != set.end()) { 184 | return list.iterator_to(*i); 185 | } 186 | return list.end(); 187 | } 188 | 189 | /// return an iterator pair corresponding to the range of fields that match 190 | /// the given name (the first match and one-past the last match) 191 | auto equal_range(std::string_view name) 192 | -> std::pair 193 | { 194 | auto lower = set.lower_bound(name); 195 | if (lower == set.end()) { 196 | return {list.end(), list.end()}; 197 | } 198 | auto upper = set.upper_bound(name); 199 | auto list_upper = std::next(list.iterator_to(*std::prev(upper))); 200 | return {list.iterator_to(*lower), list_upper}; 201 | } 202 | 203 | /// return an iterator pair corresponding to the range of fields that match 204 | /// the given name (the first match and one-past the last match) 205 | auto equal_range(std::string_view name) const 206 | -> std::pair 207 | { 208 | auto lower = set.lower_bound(name); 209 | if (lower == set.end()) { 210 | return {list.end(), list.end()}; 211 | } 212 | auto upper = set.upper_bound(name); 213 | auto list_upper = std::next(list.iterator_to(*std::prev(upper))); 214 | return {list.iterator_to(*lower), list_upper}; 215 | } 216 | 217 | /// insert the given field after the last field that matches its name, or at 218 | /// the end of the list 219 | iterator insert(std::string_view name, std::string_view value, 220 | bool never_index = false) 221 | { 222 | auto ptr = field::create(name, value, never_index); 223 | 224 | auto lower = set.lower_bound(name); 225 | if (lower == set.end()) { 226 | set.insert(set.end(), *ptr); 227 | return list.insert(list.end(), *ptr.release()); 228 | } 229 | auto upper = set.upper_bound(name); 230 | auto list_upper = std::next(list.iterator_to(*std::prev(upper))); 231 | set.insert(upper, *ptr); 232 | return list.insert(list_upper, *ptr.release()); 233 | } 234 | 235 | /// insert the given field at the end of the list, erasing any existing fields 236 | /// with a matching name 237 | iterator assign(std::string_view name, std::string_view value, 238 | bool never_index = false) 239 | { 240 | auto ptr = field::create(name, value, never_index); 241 | 242 | auto lower = set.lower_bound(name); 243 | if (lower == set.end()) { 244 | set.insert(set.end(), *ptr); 245 | return list.insert(list.end(), *ptr.release()); 246 | } 247 | auto upper = set.upper_bound(name); 248 | auto list_lower = list.iterator_to(*lower); 249 | auto list_upper = std::next(list.iterator_to(*std::prev(upper))); 250 | set.erase(lower, upper); 251 | list.erase_and_dispose(list_lower, list_upper, field::deleter{}); 252 | 253 | set.insert(set.end(), *ptr); 254 | return list.insert(list.end(), *ptr.release()); 255 | } 256 | 257 | /// erase the field at the given position 258 | iterator erase(iterator p) { 259 | set.erase(set.iterator_to(*p)); 260 | return list.erase_and_dispose(p, field::deleter{}); 261 | } 262 | 263 | /// erase all fields in the range [begin,end) 264 | iterator erase(iterator begin, iterator end) { 265 | set.erase(set.iterator_to(*begin), 266 | std::next(set.iterator_to(*std::prev(end)))); 267 | return list.erase_and_dispose(begin, end, field::deleter{}); 268 | } 269 | 270 | /// erase all fields 271 | void clear() { 272 | set.clear(); 273 | list.clear_and_dispose(field::deleter{}); 274 | } 275 | }; 276 | 277 | } // namespace nexus::h3 278 | -------------------------------------------------------------------------------- /include/nexus/h3/server.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace nexus::h3 { 8 | 9 | class acceptor; 10 | class server_connection; 11 | class stream; 12 | 13 | /// an HTTP/3 server capable of managing one or more UDP sockets via 14 | /// class acceptor 15 | class server { 16 | friend class acceptor; 17 | quic::detail::engine_impl engine; 18 | public: 19 | /// the polymorphic executor type, boost::asio::any_io_executor 20 | using executor_type = quic::detail::engine_impl::executor_type; 21 | 22 | /// construct the server with its associated executor 23 | explicit server(const executor_type& ex); 24 | 25 | /// construct the server with its associated executor and transport settings 26 | server(const executor_type& ex, const quic::settings& s); 27 | 28 | /// return the associated io executor 29 | executor_type get_executor() const; 30 | 31 | /// stop accepting new connections and streams entirely, and mark existing 32 | /// connections as 'going away'. each associated acceptor is responsible for 33 | /// closing its own socket 34 | void close(); 35 | }; 36 | 37 | /// an HTTP/3 acceptor that owns a UDP socket and uses it to accept and 38 | /// service incoming connections 39 | class acceptor { 40 | friend class server_connection; 41 | quic::detail::socket_impl impl; 42 | public: 43 | /// the polymorphic executor type, boost::asio::any_io_executor 44 | using executor_type = quic::detail::socket_impl::executor_type; 45 | 46 | /// construct the acceptor, taking ownership of a bound UDP socket 47 | acceptor(server& s, udp::socket&& socket, ssl::context& ctx); 48 | 49 | /// construct the acceptor and bind a UDP socket to the given endpoint 50 | acceptor(server& s, const udp::endpoint& endpoint, ssl::context& ctx); 51 | 52 | /// return the associated io executor 53 | executor_type get_executor() const; 54 | 55 | /// return the socket's locally-bound address/port 56 | udp::endpoint local_endpoint() const; 57 | 58 | /// start receiving packets on the socket. incoming connections can be 59 | /// accepted with accept()/async_accept(). if the queue of unaccepted 60 | /// connections reaches 'backlog' in size, new connections are rejected 61 | void listen(int backlog); 62 | 63 | /// accept an incoming connection whose TLS handshake has completed 64 | /// successfully 65 | template // void(error_code) 66 | decltype(auto) async_accept(server_connection& conn, 67 | CompletionToken&& token) { 68 | return impl.async_accept(conn, std::forward(token)); 69 | } 70 | 71 | /// accept an incoming connection whose TLS handshake has completed 72 | /// successfully 73 | void accept(server_connection& conn, error_code& ec); 74 | /// \overload 75 | void accept(server_connection& conn); 76 | 77 | /// close the socket, along with any related connections 78 | void close(); 79 | }; 80 | 81 | /// an HTTP/3 connection that can accept incoming streams and push associated 82 | /// outgoing streams 83 | class server_connection { 84 | friend class acceptor; 85 | friend class stream; 86 | friend class quic::detail::socket_impl; 87 | quic::detail::connection_impl impl; 88 | public: 89 | /// the polymorphic executor type, boost::asio::any_io_executor 90 | using executor_type = quic::detail::connection_impl::executor_type; 91 | 92 | /// construct a server-side connection for use with accept() 93 | explicit server_connection(acceptor& a) : impl(a.impl) {} 94 | 95 | /// return the associated io executor 96 | executor_type get_executor() const; 97 | 98 | /// determine whether the connection is open 99 | bool is_open() const; 100 | 101 | /// return the connection id if open 102 | quic::connection_id id(error_code& ec) const; 103 | /// \overload 104 | quic::connection_id id() const; 105 | 106 | /// return the remote's address/port if open 107 | udp::endpoint remote_endpoint(error_code& ec) const; 108 | /// \overload 109 | udp::endpoint remote_endpoint() const; 110 | 111 | /// accept an incoming stream 112 | template // void(error_code) 113 | decltype(auto) async_accept(stream& s, CompletionToken&& token) { 114 | return impl.async_accept(s, std::forward(token)); 115 | } 116 | /// \overload 117 | void accept(stream& s, error_code& ec); 118 | /// \overload 119 | void accept(stream& s); 120 | 121 | // TODO: push stream 122 | 123 | /// send a GOAWAY frame and stop initiating or accepting new streams 124 | void go_away(error_code& ec); 125 | /// \overload 126 | void go_away(); 127 | 128 | /// close the connection, along with any related streams 129 | void close(error_code& ec); 130 | /// \overload 131 | void close(); 132 | }; 133 | 134 | } // namespace nexus::h3 135 | -------------------------------------------------------------------------------- /include/nexus/h3/stream.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace nexus::h3 { 7 | 8 | class client_connection; 9 | class server_connection; 10 | 11 | /// a bidirectional HTTP/3 stream that can send and receive HTTP headers, and 12 | /// meets the type requirements of asio's AsyncRead/WriteStream and 13 | /// SyncRead/WriteStream for transferring the HTTP message body 14 | class stream : public quic::stream { 15 | friend class client_connection; 16 | friend class server_connection; 17 | using quic::stream::stream; 18 | public: 19 | /// construct a stream associated with the given client connection 20 | explicit stream(client_connection& conn); 21 | 22 | /// construct a stream associated with the given server connection 23 | explicit stream(server_connection& conn); 24 | 25 | /// read headers from the stream 26 | template // void(error_code) 27 | decltype(auto) async_read_headers(fields& f, CompletionToken&& token) { 28 | return impl.async_read_headers(f, std::forward(token)); 29 | } 30 | 31 | /// read headers from the stream 32 | void read_headers(fields& f, error_code& ec); 33 | /// \overload 34 | void read_headers(fields& f); 35 | 36 | /// write headers to the stream 37 | template // void(error_code) 38 | decltype(auto) async_write_headers(const fields& f, CompletionToken&& token) { 39 | return impl.async_write_headers(f, std::forward(token)); 40 | } 41 | 42 | /// write headers to the stream 43 | void write_headers(const fields& f, error_code& ec); 44 | /// \overload 45 | void write_headers(const fields& f); 46 | }; 47 | 48 | } // namespace nexus::h3 49 | -------------------------------------------------------------------------------- /include/nexus/nexus.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | /// networking 8 | namespace nexus {} 9 | -------------------------------------------------------------------------------- /include/nexus/quic.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /// Generic QUIC library 4 | namespace nexus::quic {} 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | -------------------------------------------------------------------------------- /include/nexus/quic/client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace nexus::quic { 9 | 10 | class connection; 11 | class stream; 12 | 13 | /// a generic QUIC client that owns a UDP socket and uses it to service client 14 | /// connections 15 | class client { 16 | friend class connection; 17 | detail::engine_impl engine; 18 | detail::socket_impl socket; 19 | public: 20 | /// the polymorphic executor type, boost::asio::any_io_executor 21 | using executor_type = detail::engine_impl::executor_type; 22 | 23 | /// construct the client, taking ownership of a bound UDP socket 24 | client(udp::socket&& socket, ssl::context& ctx); // TODO: noexcept 25 | 26 | /// construct the client, taking ownership of a bound UDP socket 27 | client(udp::socket&& socket, ssl::context& ctx, const settings& s); // TODO: noexcept 28 | 29 | /// construct the client and bind a UDP socket to the given endpoint 30 | client(const executor_type& ex, const udp::endpoint& endpoint, 31 | ssl::context& ctx); 32 | 33 | /// construct the client and bind a UDP socket to the given endpoint 34 | client(const executor_type& ex, const udp::endpoint& endpoint, 35 | ssl::context& ctx, const settings& s); 36 | 37 | /// return the associated io executor 38 | executor_type get_executor() const; 39 | 40 | /// return the socket's locally-bound address/port 41 | udp::endpoint local_endpoint() const; 42 | 43 | /// open a connection to the given remote endpoint and hostname. this 44 | /// initiates the TLS handshake, but returns immediately without waiting 45 | /// for the handshake to complete 46 | void connect(connection& conn, 47 | const udp::endpoint& endpoint, 48 | const char* hostname); 49 | 50 | /// close the socket, along with any related connections 51 | void close(error_code& ec); 52 | /// \overload 53 | void close(); 54 | }; 55 | 56 | } // namespace nexus::quic 57 | -------------------------------------------------------------------------------- /include/nexus/quic/connection.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace nexus::quic { 7 | 8 | class acceptor; 9 | class client; 10 | class stream; 11 | 12 | /// a generic QUIC connection that can initiate outgoing streams and accept 13 | /// incoming streams 14 | class connection { 15 | friend class acceptor; 16 | friend class client; 17 | friend class stream; 18 | friend class detail::socket_impl; 19 | detail::connection_impl impl; 20 | public: 21 | /// the polymorphic executor type, boost::asio::any_io_executor 22 | using executor_type = detail::connection_impl::executor_type; 23 | 24 | /// construct a server-side connection for use with accept() 25 | explicit connection(acceptor& a); 26 | 27 | /// construct a client-side connection for use with connect() 28 | explicit connection(client& c); 29 | 30 | /// open a connection to the given remote endpoint and hostname. this 31 | /// initiates the TLS handshake, but returns immediately without waiting 32 | /// for the handshake to complete 33 | connection(client& c, const udp::endpoint& endpoint, const char* hostname); 34 | 35 | /// return the associated io executor 36 | executor_type get_executor() const; 37 | 38 | /// determine whether the connection is open 39 | bool is_open() const; 40 | 41 | /// return the connection id if open 42 | connection_id id(error_code& ec) const; 43 | /// \overload 44 | connection_id id() const; 45 | 46 | /// return the remote's address/port if open 47 | udp::endpoint remote_endpoint(error_code& ec) const; 48 | /// \overload 49 | udp::endpoint remote_endpoint() const; 50 | 51 | /// open an outgoing stream 52 | template // void(error_code, stream) 53 | decltype(auto) async_connect(stream& s, CompletionToken&& token) { 54 | return impl.async_connect(s, std::forward(token)); 55 | } 56 | /// \overload 57 | void connect(stream& s, error_code& ec); 58 | /// \overload 59 | void connect(stream& s); 60 | 61 | /// accept an incoming stream 62 | template // void(error_code, stream) 63 | decltype(auto) async_accept(stream& s, CompletionToken&& token) { 64 | return impl.async_accept(s, std::forward(token)); 65 | } 66 | /// \overload 67 | void accept(stream& s, error_code& ec); 68 | /// \overload 69 | void accept(stream& s); 70 | 71 | /// stop initiating or accepting new streams 72 | void go_away(error_code& ec); 73 | /// \overload 74 | void go_away(); 75 | 76 | /// close the connection, along with any related streams 77 | void close(error_code& ec); 78 | /// \overload 79 | void close(); 80 | }; 81 | 82 | } // namespace nexus::quic 83 | -------------------------------------------------------------------------------- /include/nexus/quic/connection_id.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace nexus::quic { 10 | 11 | /// an opaque connection id string 12 | class connection_id { 13 | public: 14 | using value_type = unsigned char; 15 | using size_type = std::uint_fast8_t; 16 | using difference_type = std::ptrdiff_t; 17 | using reference = value_type&; 18 | using const_reference = const value_type&; 19 | using pointer = value_type*; 20 | using const_pointer = const value_type*; 21 | using iterator = pointer; 22 | using const_iterator = const_pointer; 23 | using reverse_iterator = std::reverse_iterator; 24 | using const_reverse_iterator = std::reverse_iterator; 25 | 26 | /// default-construct an empty connection id 27 | constexpr connection_id() noexcept 28 | : data_{}, size_(0) 29 | {} 30 | /// construct with a copy of the given array 31 | template 32 | constexpr connection_id(const value_type (&data)[Size]) noexcept 33 | : data_{}, size_(Size) 34 | { 35 | static_assert(Size <= max_size_); 36 | for (size_type i = 0; i < Size; i++) { 37 | data_[i] = data[i]; 38 | } 39 | } 40 | /// construct with a copy of the given array 41 | template 42 | constexpr connection_id(const std::array& data) noexcept 43 | : data_{}, size_(Size) 44 | { 45 | static_assert(Size <= max_size_); 46 | for (size_type i = 0; i < Size; i++) { 47 | data_[i] = data[i]; 48 | } 49 | } 50 | /// construct with a copy of the given string. throws std::length_error if 51 | /// size exceeds max_size() 52 | constexpr connection_id(const_pointer data, size_type size) 53 | : data_{}, size_(size) 54 | { 55 | if (size > max_size_) { 56 | throw std::length_error("maximum connection id length (20) exceeded"); 57 | } 58 | for (size_type i = 0; i < size; i++) { 59 | data_[i] = data[i]; 60 | } 61 | } 62 | 63 | /// construct with a copy of the given connection id 64 | constexpr connection_id(const connection_id&) noexcept = default; 65 | /// overwrite with a copy of the given connection id 66 | constexpr connection_id& operator=(const connection_id&) noexcept = default; 67 | 68 | /// return true if empty 69 | constexpr bool empty() const noexcept { return size_ == 0; } 70 | /// return the size 71 | constexpr size_type size() const { return size_; } 72 | /// return the maximum size 73 | constexpr static size_type max_size() { return max_size_; } 74 | 75 | /// resize the connection id 76 | constexpr void resize(size_type size) 77 | { 78 | if (size > max_size_) { 79 | throw std::length_error("maximum connection id length (20) exceeded"); 80 | } 81 | size_ = size; 82 | } 83 | 84 | /// return a reference to the element at the given position. throws 85 | /// std::out_of_range for invalid positions 86 | constexpr reference at(size_type p) 87 | { 88 | if (p >= size()) { 89 | throw std::out_of_range("array index out of range"); 90 | } 91 | return data_[p]; 92 | } 93 | /// \overload 94 | constexpr const_reference at(size_type p) const 95 | { 96 | if (p >= size()) { 97 | throw std::out_of_range("array index out of range"); 98 | } 99 | return data_[p]; 100 | } 101 | 102 | /// return a reference to the element at the given position without 103 | /// bounds checking 104 | constexpr reference operator[](size_type p) { return data_[p]; } 105 | /// \overload 106 | constexpr const_reference operator[](size_type p) const { return data_[p]; } 107 | 108 | /// return a reference to the first element 109 | constexpr reference front() { return data_.front(); } 110 | /// \overload 111 | constexpr const_reference front() const { return data_.front(); } 112 | 113 | /// return a reference to the last element 114 | constexpr reference back() { return *std::prev(end()); } 115 | /// \overload 116 | constexpr const_reference back() const { return *std::prev(end()); } 117 | 118 | /// return a pointer to the underlying bytes 119 | constexpr pointer data() { return data_.data(); } 120 | /// \overload 121 | constexpr const_pointer data() const { return data_.data(); } 122 | 123 | /// return an iterator to the beginning 124 | constexpr iterator begin() { return data_.begin(); } 125 | /// \overload 126 | constexpr const_iterator begin() const { return data_.begin(); } 127 | /// \overload 128 | constexpr const_iterator cbegin() const { return data_.cbegin(); } 129 | 130 | /// return an iterator past the end 131 | constexpr iterator end() { return std::next(begin(), size_); } 132 | /// \overload 133 | constexpr const_iterator end() const { return std::next(begin(), size_); } 134 | /// \overload 135 | constexpr const_iterator cend() const { return std::next(begin(), size_); } 136 | 137 | /// return a reverse iterator to the beginning 138 | constexpr reverse_iterator rbegin() { 139 | return std::next(data_.rbegin(), max_size_ - size_); 140 | } 141 | /// \overload 142 | constexpr const_reverse_iterator rbegin() const { 143 | return std::next(data_.rbegin(), max_size_ - size_); 144 | } 145 | /// \overload 146 | constexpr const_reverse_iterator crbegin() const { 147 | return std::next(data_.rbegin(), max_size_ - size_); 148 | } 149 | 150 | /// return a reverse iterator to the endning 151 | constexpr reverse_iterator rend() { return data_.rend(); } 152 | /// \overload 153 | constexpr const_reverse_iterator rend() const { return data_.rend(); } 154 | /// \overload 155 | constexpr const_reverse_iterator crend() const { return data_.crend(); } 156 | 157 | private: 158 | static constexpr size_type max_size_ = 20; 159 | using array_type = std::array; 160 | array_type data_; 161 | size_type size_; 162 | }; 163 | 164 | /// equality comparison for connection_ids 165 | /// \relates connection_id 166 | inline bool operator==(const connection_id& l, const connection_id& r) noexcept 167 | { 168 | return l.size() == r.size() && std::equal(l.begin(), l.end(), r.begin()); 169 | } 170 | /// inequality comparison for connection_ids 171 | /// \relates connection_id 172 | inline bool operator!=(const connection_id& l, const connection_id& r) noexcept 173 | { 174 | return l.size() != r.size() || !std::equal(l.begin(), l.end(), r.begin()); 175 | } 176 | /// less-than comparison for connection_ids 177 | /// \relates connection_id 178 | inline bool operator<(const connection_id& l, const connection_id& r) noexcept 179 | { 180 | return std::lexicographical_compare(l.begin(), l.end(), r.begin(), r.end()); 181 | } 182 | /// greater-than comparison for connection_ids 183 | /// \relates connection_id 184 | inline bool operator>(const connection_id& l, const connection_id& r) noexcept 185 | { 186 | return std::lexicographical_compare(r.begin(), r.end(), l.begin(), l.end()); 187 | } 188 | /// less-than-or-equal comparison for connection_ids 189 | /// \relates connection_id 190 | inline bool operator<=(const connection_id& l, const connection_id& r) noexcept 191 | { 192 | return !(l > r); 193 | } 194 | /// greater-than-or-equal comparison for connection_ids 195 | /// \relates connection_id 196 | inline bool operator>=(const connection_id& l, const connection_id& r) noexcept 197 | { 198 | return !(l < r); 199 | } 200 | 201 | /// swap two connection_ids 202 | /// \relates connection_id 203 | inline void swap(connection_id& lhs, connection_id& rhs) noexcept 204 | { 205 | auto tmp = lhs; 206 | lhs = std::move(rhs); 207 | rhs = std::move(tmp); 208 | } 209 | 210 | } // namespace nexus::quic 211 | -------------------------------------------------------------------------------- /include/nexus/quic/detail/connection_impl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | struct lsquic_conn; 10 | struct lsquic_stream; 11 | 12 | namespace nexus::quic::detail { 13 | 14 | struct accept_operation; 15 | struct socket_impl; 16 | 17 | struct connection_impl : public connection_context, 18 | public boost::intrusive::list_base_hook<>, 19 | public service_list_base_hook { 20 | service& svc; 21 | socket_impl& socket; 22 | connection_state::variant state; 23 | 24 | explicit connection_impl(socket_impl& socket); 25 | ~connection_impl(); 26 | 27 | void service_shutdown(); 28 | 29 | using executor_type = boost::asio::any_io_executor; 30 | executor_type get_executor() const; 31 | 32 | connection_id id(error_code& ec) const; 33 | udp::endpoint remote_endpoint(error_code& ec) const; 34 | 35 | void connect(stream_connect_operation& op); 36 | stream_impl* on_connect(lsquic_stream* stream); 37 | 38 | template 39 | decltype(auto) async_connect(Stream& stream, CompletionToken&& token) { 40 | auto& s = stream.impl; 41 | return boost::asio::async_initiate( 42 | [this, &s] (auto h) { 43 | using Handler = std::decay_t; 44 | using op_type = stream_connect_async; 45 | auto p = handler_allocate(h, std::move(h), get_executor(), s); 46 | auto op = handler_ptr{p, &p->handler}; 47 | connect(*op); 48 | op.release(); // release ownership 49 | }, token); 50 | } 51 | 52 | void accept(stream_accept_operation& op); 53 | stream_impl* on_accept(lsquic_stream* stream); 54 | 55 | template 56 | decltype(auto) async_accept(Stream& stream, CompletionToken&& token) { 57 | auto& s = stream.impl; 58 | return boost::asio::async_initiate( 59 | [this, &s] (auto h) { 60 | using Handler = std::decay_t; 61 | using op_type = stream_accept_async; 62 | auto p = handler_allocate(h, std::move(h), get_executor(), s); 63 | auto op = handler_ptr{p, &p->handler}; 64 | accept(*op); 65 | op.release(); // release ownership 66 | }, token); 67 | } 68 | 69 | bool is_open() const; 70 | 71 | void go_away(error_code& ec); 72 | void close(error_code& ec); 73 | 74 | void on_close(); 75 | void on_handshake(int status); 76 | void on_remote_goaway(); 77 | void on_remote_close(int app_error, uint64_t code); 78 | 79 | void on_incoming_stream_closed(stream_impl& s); 80 | void on_accepting_stream_closed(stream_impl& s); 81 | void on_connecting_stream_closed(stream_impl& s); 82 | void on_open_stream_closing(stream_impl& s); 83 | void on_open_stream_closed(stream_impl& s); 84 | void on_closing_stream_closed(stream_impl& s); 85 | }; 86 | 87 | } // namespace nexus::quic::detail 88 | -------------------------------------------------------------------------------- /include/nexus/quic/detail/connection_state.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | struct lsquic_conn; 11 | 12 | namespace nexus::quic::detail { 13 | 14 | struct accept_operation; 15 | struct stream_accept_operation; 16 | struct stream_connect_operation; 17 | 18 | using stream_list = boost::intrusive::list; 19 | 20 | inline void list_erase(stream_impl& s, stream_list& from) 21 | { 22 | from.erase(from.iterator_to(s)); 23 | } 24 | 25 | inline void list_transfer(stream_impl& s, stream_list& from, stream_list& to) 26 | { 27 | from.erase(from.iterator_to(s)); 28 | to.push_back(s); 29 | } 30 | 31 | struct connection_context { 32 | bool incoming; 33 | explicit connection_context(bool incoming) noexcept : incoming(incoming) {} 34 | }; 35 | 36 | struct incoming_connection : connection_context { 37 | lsquic_conn* handle; 38 | boost::circular_buffer incoming_streams; // TODO: allocator 39 | 40 | incoming_connection(lsquic_conn* handle, uint32_t max_streams) 41 | : connection_context(true), 42 | handle(handle), 43 | incoming_streams(max_streams) {} 44 | }; 45 | 46 | /// state machine for quic connections 47 | namespace connection_state { 48 | 49 | /// the application has requested to accept() a connection, but no incoming 50 | /// connection has been received yet to satisfy the request 51 | struct accepting { 52 | accept_operation* op = nullptr; 53 | }; 54 | 55 | /// the connection is open and ready to initiate and accept streams 56 | struct open { 57 | lsquic_conn& handle; 58 | boost::circular_buffer incoming_streams; 59 | stream_list connecting_streams; 60 | stream_list accepting_streams; 61 | stream_list open_streams; 62 | stream_list closing_streams; 63 | // handshake errors are stored here until they can be delivered on close 64 | error_code ec; 65 | 66 | explicit open(lsquic_conn& handle) noexcept : handle(handle) {} 67 | }; 68 | 69 | /// the connection is processing open streams but not initiating or accepting 70 | struct going_away { 71 | lsquic_conn& handle; 72 | stream_list open_streams; 73 | stream_list closing_streams; 74 | // handshake errors are stored here until they can be delivered on close 75 | error_code ec; 76 | 77 | explicit going_away(lsquic_conn& handle) noexcept : handle(handle) {} 78 | }; 79 | 80 | /// the connection has closed with a connection error which hasn't yet been 81 | /// delivered to the application 82 | struct error { 83 | error_code ec; 84 | }; 85 | 86 | /// the connection is closed 87 | struct closed { 88 | }; 89 | 90 | using variant = std::variant; 91 | 92 | /// connection state transitions (only those relevent to close) 93 | enum class transition { 94 | none, 95 | accepting_to_closed, 96 | open_to_going_away, 97 | open_to_closed, 98 | open_to_error, 99 | going_away_to_closed, 100 | going_away_to_error, 101 | error_to_closed, 102 | }; 103 | 104 | // connection accessors 105 | bool is_open(const variant& state); 106 | connection_id id(const variant& state, error_code& ec); 107 | udp::endpoint remote_endpoint(const variant& state, error_code& ec); 108 | 109 | // connection events 110 | void on_connect(variant& state, lsquic_conn* handle); 111 | void on_handshake(variant& state, int status); 112 | void accept(variant& state, accept_operation& op); 113 | void accept_incoming(variant& state, incoming_connection&& incoming); 114 | void on_accept(variant& state, lsquic_conn* handle); 115 | 116 | bool stream_connect(variant& state, stream_connect_operation& op); 117 | stream_impl* on_stream_connect(variant& state, lsquic_stream* handle, 118 | bool is_http); 119 | 120 | void stream_accept(variant& state, stream_accept_operation& op, bool is_http); 121 | stream_impl* on_stream_accept(variant& state, lsquic_stream* handle, 122 | bool is_http); 123 | 124 | transition goaway(variant& state, error_code& ec); 125 | transition on_remote_goaway(variant& state); 126 | transition reset(variant& state, error_code ec); 127 | transition close(variant& state, error_code& ec); 128 | transition on_close(variant& state); 129 | transition on_remote_close(variant& state, error_code ec); 130 | void destroy(variant& state); 131 | 132 | } // namespace connection_state 133 | 134 | } // namespace nexus::quic::detail 135 | -------------------------------------------------------------------------------- /include/nexus/quic/detail/engine_impl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | 10 | struct lsquic_engine; 11 | struct lsquic_conn; 12 | struct lsquic_stream; 13 | struct lsquic_out_spec; 14 | 15 | namespace nexus::quic::detail { 16 | 17 | struct connection_impl; 18 | struct stream_impl; 19 | struct socket_impl; 20 | 21 | struct engine_deleter { void operator()(lsquic_engine* e) const; }; 22 | using lsquic_engine_ptr = std::unique_ptr; 23 | 24 | struct engine_impl { 25 | mutable std::mutex mutex; 26 | boost::asio::any_io_executor ex; 27 | boost::asio::steady_timer timer; 28 | lsquic_engine_ptr handle; 29 | // pointer to client socket or null if server 30 | socket_impl* client; 31 | uint32_t max_streams_per_connection; 32 | bool is_http; 33 | 34 | void process(std::unique_lock& lock); 35 | void reschedule(std::unique_lock& lock); 36 | void on_timer(); 37 | 38 | engine_impl(const boost::asio::any_io_executor& ex, socket_impl* client, 39 | const settings* s, unsigned flags); 40 | ~engine_impl(); 41 | 42 | using executor_type = boost::asio::any_io_executor; 43 | executor_type get_executor() const { return ex; } 44 | 45 | void close(); 46 | 47 | int send_packets(const lsquic_out_spec *specs, unsigned n_specs); 48 | 49 | stream_impl* on_new_stream(connection_impl& c, lsquic_stream* stream); 50 | }; 51 | 52 | } // namespace nexus::quic::detail 53 | -------------------------------------------------------------------------------- /include/nexus/quic/detail/handler_ptr.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace nexus::quic::detail { 7 | 8 | /// allocate a T using the allocator associated with the given handler, 9 | /// forwarding additional arguments to T's constructor 10 | // 11 | /// handler_allocate() returns a raw pointer instead of handler_ptr, because it 12 | /// doesn't necessarily know what handler to use for its deleter. the Handler 13 | /// itself is commonly stored in T and moved into T's constructor, so only the 14 | /// caller knows where the handler's ends up 15 | /// 16 | /// example usage: 17 | /// using T = handler_wrapper; 18 | /// auto t = handler_allocate(handler, std::move(handler)); 19 | /// auto p = handler_ptr{t, &t->handler}; // take ownership 20 | /// 21 | template 22 | T* handler_allocate(Handler& handler, Args&& ...args) 23 | { 24 | using Alloc = boost::asio::associated_allocator_t; 25 | using Traits = std::allocator_traits; 26 | using Rebind = typename Traits::template rebind_alloc; 27 | using RebindTraits = std::allocator_traits; 28 | auto alloc = Rebind{boost::asio::get_associated_allocator(handler)}; 29 | auto p = RebindTraits::allocate(alloc, 1); 30 | try { 31 | RebindTraits::construct(alloc, p, std::forward(args)...); 32 | return p; 33 | } catch (const std::exception&) { 34 | RebindTraits::deallocate(alloc, p, 1); 35 | throw; 36 | } 37 | } 38 | 39 | /// unique_ptr deleter that uses the Handler's associated allocator 40 | /// 41 | /// handler-allocated memory must be released before invoking the handler. if 42 | /// the handler itself is stored in this memory, it must be moved out before 43 | /// release. whenever the handler is moved, the deleter's handler pointer must 44 | /// be updated to its new memory location 45 | /// 46 | /// example usage: 47 | /// auto p = handler_ptr{t, &t->handler}; 48 | /// auto handler2 = std::move(p->handler); 49 | /// p.get_deleter().handler = &handler2; 50 | /// p.reset(); // delete t using handler2's allocator 51 | /// 52 | template 53 | struct handler_ptr_deleter { 54 | using Alloc = boost::asio::associated_allocator_t; 55 | using Traits = std::allocator_traits; 56 | 57 | /// public handler pointer, must be updated whenever the handler moves 58 | Handler* handler; 59 | handler_ptr_deleter(Handler* handler) noexcept : handler(handler) {} 60 | 61 | template 62 | void operator()(T* p) { 63 | using Rebind = typename Traits::template rebind_alloc; 64 | using RebindTraits = std::allocator_traits; 65 | auto alloc = Rebind{boost::asio::get_associated_allocator(*handler)}; 66 | RebindTraits::destroy(alloc, p); 67 | RebindTraits::deallocate(alloc, p, 1); 68 | } 69 | }; 70 | 71 | /// unique_ptr alias for handler-allocated memory 72 | template 73 | using handler_ptr = std::unique_ptr>; 74 | 75 | } // namespace nexus::quic::detail 76 | -------------------------------------------------------------------------------- /include/nexus/quic/detail/service.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace nexus::quic::detail { 8 | 9 | struct service_tag {}; 10 | using service_list_base_hook = boost::intrusive::list_base_hook< 11 | boost::intrusive::tag>; 12 | 13 | /// service for two-phase execution_context shutdown, which breaks ownership 14 | /// cycles between completion handlers and their io objects. tracks objects 15 | /// which may have outstanding completion handlers, and calls their member 16 | /// function service_shutdown() when the execution_context is shutting down. 17 | /// this member function should destroy any memory associated with its 18 | /// outstanding completion handlers 19 | /// 20 | /// requirements for IoObject: 21 | /// * inherits publicly from service_list_base_hook 22 | /// * has public member function service_shutdown() 23 | template 24 | class service : public boost::asio::execution_context::service { 25 | using base_hook = boost::intrusive::base_hook; 26 | boost::intrusive::list entries; 27 | std::mutex mutex; 28 | 29 | /// called by the execution_context on shutdown 30 | void shutdown() override { 31 | while (!entries.empty()) { 32 | auto& entry = entries.front(); 33 | entries.pop_front(); 34 | entry.service_shutdown(); 35 | } 36 | } 37 | public: 38 | using key_type = service; 39 | static inline boost::asio::execution_context::id id; 40 | 41 | explicit service(boost::asio::execution_context& ctx) 42 | : boost::asio::execution_context::service(ctx) {} 43 | 44 | /// register an io object for notification of service_shutdown() 45 | void add(IoObject& entry) { 46 | auto lock = std::scoped_lock{mutex}; 47 | entries.push_back(entry); 48 | } 49 | /// unregister an object 50 | void remove(IoObject& entry) { 51 | auto lock = std::scoped_lock{mutex}; 52 | if (entries.empty()) { 53 | // already shut down 54 | } else { 55 | entries.erase(entries.iterator_to(entry)); 56 | } 57 | } 58 | }; 59 | 60 | } // namespace nexus::quic::detail 61 | -------------------------------------------------------------------------------- /include/nexus/quic/detail/socket_impl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | struct lsquic_conn; 9 | struct lsquic_out_spec; 10 | 11 | namespace nexus::quic::detail { 12 | 13 | struct engine_impl; 14 | struct connection_impl; 15 | 16 | union sockaddr_union { 17 | sockaddr_storage storage; 18 | sockaddr addr; 19 | sockaddr_in addr4; 20 | sockaddr_in6 addr6; 21 | }; 22 | 23 | using connection_list = boost::intrusive::list; 24 | 25 | inline void list_erase(connection_impl& s, connection_list& from) 26 | { 27 | from.erase(from.iterator_to(s)); 28 | } 29 | 30 | inline void list_transfer(connection_impl& s, connection_list& from, 31 | connection_list& to) 32 | { 33 | from.erase(from.iterator_to(s)); 34 | to.push_back(s); 35 | } 36 | 37 | struct socket_impl : boost::intrusive::list_base_hook<> { 38 | engine_impl& engine; 39 | udp::socket socket; 40 | ssl::context& ssl; 41 | udp::endpoint local_addr; // socket's bound address 42 | boost::circular_buffer incoming_connections; 43 | connection_list accepting_connections; 44 | connection_list open_connections; 45 | bool receiving = false; 46 | 47 | socket_impl(engine_impl& engine, udp::socket&& socket, 48 | ssl::context& ssl); 49 | socket_impl(engine_impl& engine, const udp::endpoint& endpoint, 50 | bool is_server, ssl::context& ssl); 51 | ~socket_impl() { 52 | close(); 53 | } 54 | 55 | using executor_type = boost::asio::any_io_executor; 56 | executor_type get_executor() const; 57 | 58 | udp::endpoint local_endpoint() const { return local_addr; } 59 | 60 | void listen(int backlog); 61 | 62 | void connect(connection_impl& c, 63 | const udp::endpoint& endpoint, 64 | const char* hostname); 65 | void on_connect(connection_impl& c, lsquic_conn* conn); 66 | 67 | void accept(connection_impl& c, accept_operation& op); 68 | connection_context* on_accept(lsquic_conn* conn); 69 | 70 | template 71 | decltype(auto) async_accept(Connection& conn, 72 | CompletionToken&& token) { 73 | auto& c = conn.impl; 74 | return boost::asio::async_initiate( 75 | [this, &c] (auto h) { 76 | using Handler = std::decay_t; 77 | using op_type = accept_async; 78 | auto p = handler_allocate(h, std::move(h), get_executor()); 79 | auto op = handler_ptr{p, &p->handler}; 80 | accept(c, *op); 81 | op.release(); // release ownership 82 | }, token); 83 | } 84 | 85 | void close(); 86 | 87 | void abort_connections(error_code ec); 88 | 89 | void start_recv(); 90 | void on_readable(); 91 | void on_writeable(); 92 | 93 | const lsquic_out_spec* send_packets(const lsquic_out_spec* begin, 94 | const lsquic_out_spec* end, 95 | error_code& ec); 96 | 97 | size_t recv_packet(iovec iov, udp::endpoint& peer, sockaddr_union& self, 98 | int& ecn, error_code& ec); 99 | }; 100 | 101 | } // namespace nexus::quic::detail 102 | -------------------------------------------------------------------------------- /include/nexus/quic/detail/stream_impl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | struct lsquic_stream; 13 | 14 | namespace nexus { 15 | namespace quic::detail { 16 | 17 | struct connection_impl; 18 | struct engine_impl; 19 | 20 | struct stream_impl : public boost::intrusive::list_base_hook<>, 21 | public service_list_base_hook { 22 | using executor_type = boost::asio::any_io_executor; 23 | engine_impl& engine; 24 | service& svc; 25 | connection_impl& conn; 26 | stream_state::variant state; 27 | 28 | template 29 | static void init_op(const BufferSequence& buffers, 30 | stream_data_operation& op) { 31 | const auto end = boost::asio::buffer_sequence_end(buffers); 32 | for (auto i = boost::asio::buffer_sequence_begin(buffers); 33 | i != end && op.num_iovs < op.max_iovs; 34 | ++i, ++op.num_iovs) { 35 | op.iovs[op.num_iovs].iov_base = const_cast(i->data()); 36 | op.iovs[op.num_iovs].iov_len = i->size(); 37 | } 38 | } 39 | 40 | explicit stream_impl(connection_impl& conn); 41 | ~stream_impl(); 42 | 43 | void service_shutdown(); 44 | 45 | executor_type get_executor() const; 46 | 47 | bool is_open() const; 48 | stream_id id(error_code& ec) const; 49 | 50 | void read_headers(stream_header_read_operation& op); 51 | 52 | template 53 | decltype(auto) async_read_headers(h3::fields& fields, 54 | CompletionToken&& token) { 55 | return boost::asio::async_initiate( 56 | [this, &fields] (auto h) { 57 | using Handler = std::decay_t; 58 | using op_type = stream_header_read_async; 59 | auto p = handler_allocate(h, std::move(h), 60 | get_executor(), fields); 61 | auto op = handler_ptr{p, &p->handler}; 62 | read_headers(*op); 63 | op.release(); // release ownership 64 | }, token); 65 | } 66 | 67 | void read_some(stream_data_operation& op); 68 | void on_read(); 69 | 70 | template 71 | decltype(auto) async_read_some(const MutableBufferSequence& buffers, 72 | CompletionToken&& token) { 73 | return boost::asio::async_initiate( 74 | [this, &buffers] (auto h) { 75 | using Handler = std::decay_t; 76 | using op_type = stream_data_async; 77 | auto p = handler_allocate(h, std::move(h), get_executor()); 78 | auto op = handler_ptr{p, &p->handler}; 79 | init_op(buffers, *op); 80 | read_some(*op); 81 | op.release(); // release ownership 82 | }, token); 83 | } 84 | 85 | template 86 | std::enable_if_t::value, size_t> 88 | read_some(const MutableBufferSequence& buffers, error_code& ec) { 89 | stream_data_sync op; 90 | init_op(buffers, op); 91 | read_some(op); 92 | op.wait(); 93 | ec = std::get<0>(*op.result); 94 | return std::get<1>(*op.result); 95 | } 96 | 97 | void write_headers(stream_header_write_operation& op); 98 | 99 | template 100 | decltype(auto) async_write_headers(const h3::fields& fields, 101 | CompletionToken&& token) { 102 | return boost::asio::async_initiate( 103 | [this, &fields] (auto h) { 104 | using Handler = std::decay_t; 105 | using op_type = stream_header_write_async; 106 | auto p = handler_allocate(h, std::move(h), 107 | get_executor(), fields); 108 | auto op = handler_ptr{p, &p->handler}; 109 | write_headers(*op); 110 | op.release(); // release ownership 111 | }, token); 112 | } 113 | 114 | void write_some(stream_data_operation& op); 115 | void on_write(); 116 | 117 | template 118 | decltype(auto) async_write_some(const ConstBufferSequence& buffers, 119 | CompletionToken&& token) { 120 | return boost::asio::async_initiate( 121 | [this, &buffers] (auto h) { 122 | using Handler = std::decay_t; 123 | using op_type = stream_data_async; 124 | auto p = handler_allocate(h, std::move(h), get_executor()); 125 | auto op = handler_ptr{p, &p->handler}; 126 | init_op(buffers, *op); 127 | write_some(*op); 128 | op.release(); // release ownership 129 | }, token); 130 | } 131 | 132 | template 133 | std::enable_if_t::value, size_t> 135 | write_some(const ConstBufferSequence& buffers, error_code& ec) { 136 | stream_data_sync op; 137 | init_op(buffers, op); 138 | write_some(op); 139 | op.wait(); 140 | ec = std::get<0>(*op.result); 141 | return std::get<1>(*op.result); 142 | } 143 | 144 | void flush(error_code& ec); 145 | void shutdown(int how, error_code& ec); 146 | 147 | void close(stream_close_operation& op); 148 | void on_close(); 149 | 150 | template 151 | decltype(auto) async_close(CompletionToken&& token) { 152 | return boost::asio::async_initiate( 153 | [this] (auto h) { 154 | using Handler = std::decay_t; 155 | using op_type = stream_close_async; 156 | auto p = handler_allocate(h, std::move(h), get_executor()); 157 | auto op = handler_ptr{p, &p->handler}; 158 | close(*op); 159 | op.release(); // release ownership 160 | }, token); 161 | } 162 | 163 | void reset(); 164 | }; 165 | 166 | } // namespace quic::detail 167 | } // namespace nexus 168 | -------------------------------------------------------------------------------- /include/nexus/quic/detail/stream_open_handler.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace nexus::quic::detail { 9 | 10 | struct stream_impl; 11 | 12 | /// generic Stream factory calls private unique_ptr constructors, 13 | /// so must be friended by the Stream type 14 | template 15 | struct stream_factory { 16 | static Stream create(std::unique_ptr s) { return s; } 17 | }; 18 | 19 | /// the generic stream handler returns a unique_ptr. wrap that 20 | /// completion with one that returns a Stream constructed with that state 21 | template 22 | struct stream_open_handler { 23 | Handler handler; 24 | explicit stream_open_handler(Handler&& handler) 25 | : handler(std::move(handler)) 26 | {} 27 | void operator()(error_code ec, std::unique_ptr s) & 28 | { 29 | handler(ec, stream_factory::create(std::move(s))); 30 | } 31 | void operator()(error_code ec, std::unique_ptr s) && 32 | { 33 | std::move(handler)(ec, stream_factory::create(std::move(s))); 34 | } 35 | 36 | using allocator_type = boost::asio::associated_allocator_t; 37 | allocator_type get_allocator() const noexcept { 38 | return boost::asio::get_associated_allocator(handler); 39 | } 40 | }; 41 | 42 | } // namespace nexus::quic::detail 43 | 44 | namespace boost::asio { 45 | 46 | // specialize associated_executor<> for stream_open_handler 47 | template 48 | struct associated_executor, Executor> { 49 | using type = associated_executor_t; 50 | 51 | static type get(const nexus::quic::detail::stream_open_handler& handler, 52 | const Executor& ex = Executor()) noexcept { 53 | return get_associated_executor(handler.handler, ex); 54 | } 55 | }; 56 | 57 | } // namespace boost::asio 58 | -------------------------------------------------------------------------------- /include/nexus/quic/detail/stream_state.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | struct lsquic_stream; 8 | 9 | namespace nexus::quic::detail { 10 | 11 | struct connection_impl; 12 | struct stream_impl; 13 | 14 | struct stream_header_read_operation; 15 | struct stream_header_write_operation; 16 | struct stream_data_operation; 17 | struct stream_accept_operation; 18 | struct stream_connect_operation; 19 | struct stream_close_operation; 20 | 21 | /// state machine for the sending side of a quic stream. h3 streams start at the 22 | /// expecting_header state, and non-h3 streams start at expecting_body 23 | namespace sending_stream_state { 24 | 25 | using header_operation = stream_header_write_operation; 26 | using data_operation = stream_data_operation; 27 | 28 | struct expecting_header {}; 29 | struct header { 30 | header_operation* op = nullptr; 31 | }; 32 | struct expecting_body {}; 33 | struct body { 34 | data_operation* op = nullptr; 35 | }; 36 | struct shutdown {}; 37 | 38 | using variant = std::variant; 41 | 42 | // sending stream events 43 | void write_header(variant& state, lsquic_stream* handle, header_operation& op); 44 | void write_body(variant& state, lsquic_stream* handle, data_operation& op); 45 | void on_write_header(variant& state, lsquic_stream* handle); 46 | void on_write_body(variant& state, lsquic_stream* handle); 47 | void on_write(variant& state, lsquic_stream* handle); 48 | int cancel(variant& state, error_code ec); 49 | void destroy(variant& state); 50 | 51 | } // namespace sending_stream_state 52 | 53 | /// state machine for the receiving side of a quic stream. h3 streams start at 54 | /// the expecting_header state, and non-h3 streams start at expecting_body 55 | namespace receiving_stream_state { 56 | 57 | using header_operation = stream_header_read_operation; 58 | using data_operation = stream_data_operation; 59 | 60 | struct expecting_header {}; 61 | struct header { 62 | header_operation* op = nullptr; 63 | }; 64 | struct expecting_body {}; 65 | struct body { 66 | data_operation* op = nullptr; 67 | }; 68 | struct shutdown {}; 69 | 70 | using variant = std::variant; 73 | 74 | // receiving stream events 75 | void read_header(variant& state, lsquic_stream* handle, header_operation* op); 76 | void read_body(variant& state, lsquic_stream* handle, data_operation* op); 77 | void on_read_header(variant& state, error_code ec); 78 | void on_read_body(variant& state, error_code ec); 79 | void on_read(variant& state, lsquic_stream* handle); 80 | int cancel(variant& state, error_code ec); 81 | void destroy(variant& state); 82 | 83 | } // namespace receiving_stream_state 84 | 85 | /// state machine for quic streams 86 | namespace stream_state { 87 | 88 | /// the application has requested to accept() a stream, but no incoming stream 89 | /// has been received yet to satisfy the request 90 | struct accepting { 91 | stream_accept_operation* op = nullptr; 92 | }; 93 | 94 | /// the application has requested to connect() a new outgoing stream, but the 95 | /// library has not yet opened one 96 | struct connecting { 97 | stream_connect_operation* op = nullptr; 98 | }; 99 | 100 | /// the stream is open 101 | struct open { 102 | lsquic_stream& handle; 103 | 104 | receiving_stream_state::variant in; 105 | sending_stream_state::variant out; 106 | 107 | struct quic_tag {}; 108 | open(lsquic_stream& handle, quic_tag) noexcept 109 | : handle(handle), 110 | in(receiving_stream_state::expecting_body{}), 111 | out(sending_stream_state::expecting_body{}) 112 | {} 113 | 114 | struct h3_tag {}; 115 | open(lsquic_stream& handle, h3_tag) noexcept 116 | : handle(handle), 117 | in(receiving_stream_state::expecting_header{}), 118 | out(sending_stream_state::expecting_header{}) 119 | {} 120 | }; 121 | 122 | /// the application has requested graceful shutdown with close(), and the 123 | /// library is waiting for all sent bytes to be acknowledged 124 | struct closing { 125 | stream_close_operation* op = nullptr; 126 | }; 127 | 128 | /// closed with a connection error that has yet to be delivered to the stream 129 | struct error { 130 | error_code ec; 131 | }; 132 | 133 | /// the stream is closed 134 | struct closed { 135 | }; 136 | 137 | using variant = std::variant; 139 | 140 | /// stream state transitions (only those relevent to close) 141 | enum class transition { 142 | none, 143 | accepting_to_closed, 144 | connecting_to_closed, 145 | open_to_closing, 146 | open_to_closed, 147 | open_to_error, 148 | closing_to_closed, 149 | error_to_closed, 150 | }; 151 | 152 | // stream accessors 153 | bool is_open(const variant& state); 154 | stream_id id(const variant& state, error_code& ec); 155 | 156 | // stream events 157 | void connect(variant& state, stream_connect_operation& op); 158 | void on_connect(variant& state, lsquic_stream* handle, bool is_http); 159 | 160 | void accept(variant& state, stream_accept_operation& op); 161 | void on_accept(variant& state, lsquic_stream* handle, bool is_http); 162 | 163 | bool read(variant& state, stream_data_operation& op); 164 | bool read_headers(variant& state, stream_header_read_operation& op); 165 | void on_read(variant& state); 166 | 167 | bool write(variant& state, stream_data_operation& op); 168 | bool write_headers(variant& state, stream_header_write_operation& op); 169 | void on_write(variant& state); 170 | 171 | void flush(variant& state, error_code& ec); 172 | void shutdown(variant& state, int how, error_code& ec); 173 | int cancel(variant& state, error_code ec); 174 | 175 | transition close(variant& state, stream_close_operation& op); 176 | transition on_close(variant& state); 177 | transition on_error(variant& state, error_code ec); 178 | transition reset(variant& state); 179 | 180 | void destroy(variant& state); 181 | 182 | } // namespace stream_state 183 | 184 | } // namespace nexus::quic::detail 185 | -------------------------------------------------------------------------------- /include/nexus/quic/error.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace nexus::quic { 6 | 7 | /// quic connection errors 8 | enum class connection_error { 9 | /// this end of the connection was closed 10 | aborted = 1, 11 | /// the connection's tls handshake failed 12 | handshake_failed, 13 | /// the connection or handshake timed out 14 | timed_out, 15 | /// connection reset by peer 16 | reset, 17 | /// sent GOAWAY to peer 18 | going_away, 19 | /// peer sent GOAWAY 20 | peer_going_away, 21 | }; 22 | 23 | /// connection error category 24 | const error_category& connection_category(); 25 | 26 | inline error_code make_error_code(connection_error e) 27 | { 28 | return {static_cast(e), connection_category()}; 29 | } 30 | inline error_condition make_error_condition(connection_error e) 31 | { 32 | return {static_cast(e), connection_category()}; 33 | } 34 | 35 | 36 | /// quic stream errors 37 | enum class stream_error { 38 | /// no more bytes can be read because the peer closed the stream for writing 39 | eof = 1, 40 | /// stream cannot process more than one read or more than one write at a time 41 | busy, 42 | /// this end of the stream was closed 43 | aborted, 44 | /// stream reset by peer 45 | reset, 46 | }; 47 | 48 | /// stream error category 49 | const error_category& stream_category(); 50 | 51 | inline error_code make_error_code(stream_error e) 52 | { 53 | return {static_cast(e), stream_category()}; 54 | } 55 | inline error_condition make_error_condition(stream_error e) 56 | { 57 | return {static_cast(e), stream_category()}; 58 | } 59 | 60 | 61 | /// quic transport error codes sent in CONNECTION_CLOSE frames 62 | enum class transport_error { 63 | no_error = 0x0, 64 | internal_error = 0x1, 65 | connection_refused = 0x2, 66 | flow_control_error = 0x3, 67 | stream_limit_error = 0x4, 68 | stream_state_error = 0x5, 69 | final_size_error = 0x6, 70 | frame_encoding_error = 0x7, 71 | transport_parameter_error = 0x8, 72 | connection_id_limit_error = 0x9, 73 | protocol_violation = 0xa, 74 | invalid_token = 0xb, 75 | application_error = 0xc, 76 | crypto_buffer_exceeded = 0xd, 77 | key_update_error = 0xe, 78 | aead_limit_reached = 0xf, 79 | no_viable_path = 0x10, 80 | }; 81 | 82 | /// transport error category 83 | const error_category& transport_category(); 84 | 85 | inline error_code make_error_code(transport_error e) 86 | { 87 | return {static_cast(e), transport_category()}; 88 | } 89 | inline error_condition make_error_condition(transport_error e) 90 | { 91 | return {static_cast(e), transport_category()}; 92 | } 93 | 94 | 95 | /// tls alerts 96 | enum class tls_alert : uint8_t { 97 | close_notify = 0, 98 | unexpected_message = 10, 99 | bad_record_mac = 20, 100 | record_overflow = 22, 101 | handshake_failure = 40, 102 | bad_certificate = 42, 103 | unsupported_certificate = 43, 104 | certificate_revoked = 44, 105 | certificate_expired = 45, 106 | certificate_unknown = 46, 107 | illegal_parameter = 47, 108 | unknown_ca = 48, 109 | access_denied = 49, 110 | decode_error = 50, 111 | decrypt_error = 51, 112 | protocol_version = 70, 113 | insufficient_security = 71, 114 | internal_error = 80, 115 | inappropriate_fallback = 86, 116 | user_canceled = 90, 117 | missing_extension = 109, 118 | unsupported_extension = 110, 119 | unrecognized_name = 112, 120 | bad_certificate_status_response = 113, 121 | unknown_psk_identity = 115, 122 | certificate_required = 116, 123 | no_application_protocol = 120, 124 | }; 125 | 126 | /// tls error category 127 | const error_category& tls_category(); 128 | 129 | inline error_code make_error_code(tls_alert e) 130 | { 131 | return {static_cast(e), tls_category()}; 132 | } 133 | inline error_condition make_error_condition(tls_alert e) 134 | { 135 | return {static_cast(e), tls_category()}; 136 | } 137 | 138 | 139 | /// application-level error category 140 | const error_category& application_category(); 141 | 142 | } // namespace nexus::quic 143 | 144 | namespace SYSTEM_ERROR_NAMESPACE { 145 | 146 | /// enables implicit conversion to std::error_code 147 | template <> 148 | struct is_error_code_enum : public std::true_type {}; 149 | template <> 150 | struct is_error_code_enum : public std::true_type {}; 151 | template <> 152 | struct is_error_code_enum : public std::true_type {}; 153 | template <> 154 | struct is_error_code_enum : public std::true_type {}; 155 | 156 | } // namespace SYSTEM_ERROR_NAMESPACE 157 | -------------------------------------------------------------------------------- /include/nexus/quic/server.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace nexus::quic { 9 | 10 | class acceptor; 11 | class connection; 12 | 13 | /// a generic QUIC server capable of managing one or more UDP sockets via 14 | /// class acceptor 15 | class server { 16 | friend class acceptor; 17 | detail::engine_impl engine; 18 | public: 19 | /// the polymorphic executor type, boost::asio::any_io_executor 20 | using executor_type = detail::engine_impl::executor_type; 21 | 22 | /// construct the server with its associated executor 23 | explicit server(const executor_type& ex); 24 | 25 | /// construct the server with its associated executor and transport settings 26 | server(const executor_type& ex, const settings& s); 27 | 28 | /// return the associated io executor 29 | executor_type get_executor() const; 30 | 31 | /// stop accepting new connections and streams entirely, and mark existing 32 | /// connections as 'going away'. each associated acceptor is responsible for 33 | /// closing its own socket 34 | void close(); 35 | }; 36 | 37 | /// a generic QUIC acceptor that owns a UDP socket and uses it to accept and 38 | /// service incoming connections 39 | class acceptor { 40 | friend class connection; 41 | detail::socket_impl impl; 42 | public: 43 | /// the polymorphic executor type, boost::asio::any_io_executor 44 | using executor_type = detail::socket_impl::executor_type; 45 | 46 | /// construct the acceptor, taking ownership of a bound UDP socket 47 | acceptor(server& s, udp::socket&& socket, ssl::context& ctx); 48 | 49 | /// construct the acceptor and bind a UDP socket to the given endpoint 50 | acceptor(server& s, const udp::endpoint& endpoint, ssl::context& ctx); 51 | 52 | /// return the associated io executor 53 | executor_type get_executor() const; 54 | 55 | /// return the socket's locally-bound address/port 56 | udp::endpoint local_endpoint() const; 57 | 58 | /// start receiving packets on the socket. incoming connections can be 59 | /// accepted with accept()/async_accept(). if the queue of unaccepted 60 | /// connections reaches 'backlog' in size, new connections are rejected 61 | void listen(int backlog); 62 | 63 | /// accept an incoming connection whose TLS handshake has completed 64 | /// successfully 65 | template // void(error_code) 66 | decltype(auto) async_accept(connection& conn, CompletionToken&& token) { 67 | return impl.async_accept(conn, std::forward(token)); 68 | } 69 | 70 | /// accept an incoming connection whose TLS handshake has completed 71 | /// successfully 72 | void accept(connection& conn, error_code& ec); 73 | /// \overload 74 | void accept(connection& conn); 75 | 76 | /// close the socket, along with any related connections 77 | void close(); 78 | }; 79 | 80 | } // namespace nexus::quic 81 | -------------------------------------------------------------------------------- /include/nexus/quic/settings.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | struct lsquic_engine_settings; 7 | 8 | namespace nexus::quic { 9 | 10 | /// exception thrown by client/server constructors on unchecked settings 11 | struct bad_setting : std::runtime_error { 12 | using runtime_error::runtime_error; 13 | }; 14 | 15 | /// quic transport settings used to initialize a client or server 16 | struct settings { 17 | /// handshake timeout, resulting in connection_error::timed_out 18 | std::chrono::seconds handshake_timeout; 19 | 20 | /// connection idle timeout, resulting in connection_error::timed_out 21 | std::chrono::seconds idle_timeout; 22 | 23 | /// number of concurrent streams a peer is allowed to open per connection 24 | uint32_t max_streams_per_connection; 25 | 26 | /// amount of unread bytes a peer is allowed to send per connection 27 | uint32_t connection_flow_control_window; 28 | 29 | /// amount of unread bytes a peer is allowed to send on streams they initiate 30 | uint32_t incoming_stream_flow_control_window; 31 | 32 | /// amount of unread bytes a peer is allowed to send on streams we initiate 33 | uint32_t outgoing_stream_flow_control_window; 34 | }; 35 | 36 | /// return default client settings 37 | /// \relatesalso settings 38 | settings default_client_settings(); 39 | /// return default server settings 40 | /// \relatesalso settings 41 | settings default_server_settings(); 42 | 43 | /// check the validity of the client settings 44 | /// \relatesalso settings 45 | bool check_client_settings(const settings& s, std::string* message); 46 | /// check the validity of the server settings 47 | /// \relatesalso settings 48 | bool check_server_settings(const settings& s, std::string* message); 49 | 50 | 51 | namespace detail { 52 | 53 | void read_settings(settings& out, const lsquic_engine_settings& in); 54 | void write_settings(const settings& in, lsquic_engine_settings& out); 55 | 56 | } // namespace detail 57 | 58 | } // namespace nexus::quic 59 | -------------------------------------------------------------------------------- /include/nexus/quic/socket.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace nexus::quic { 7 | 8 | // enable the socket options necessary for a quic client or server 9 | void prepare_socket(udp::socket& sock, bool is_server, error_code& ec); 10 | 11 | } // namespace nexus::quic 12 | -------------------------------------------------------------------------------- /include/nexus/quic/stream.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace nexus::quic { 8 | 9 | namespace detail { 10 | 11 | struct connection_impl; 12 | 13 | template struct stream_factory; 14 | 15 | } // namespace detail 16 | 17 | class connection; 18 | 19 | /// a generic bidirectional QUIC stream that meets the type requirements of 20 | /// asio's AsyncRead/WriteStream and SyncRead/WriteStream 21 | class stream { 22 | protected: 23 | friend class connection; 24 | friend class detail::connection_impl; 25 | detail::stream_impl impl; 26 | explicit stream(detail::connection_impl& impl); 27 | public: 28 | /// construct a stream associated with the given connection 29 | explicit stream(connection& conn); 30 | 31 | /// reset the stream on destruction 32 | ~stream(); 33 | 34 | stream(const stream&) = delete; 35 | stream& operator=(const stream&) = delete; 36 | stream(stream&&) = delete; 37 | stream& operator=(stream&&) = delete; 38 | 39 | /// the polymorphic executor type, boost::asio::any_io_executor 40 | using executor_type = detail::stream_impl::executor_type; 41 | 42 | /// return the associated io executor 43 | executor_type get_executor() const; 44 | 45 | /// determine whether the stream is open 46 | bool is_open() const; 47 | 48 | /// return the stream identifier if open. for streams initiated locally, 49 | /// an identifier may not be assigned until the first STREAM frame is sent 50 | stream_id id(error_code& ec) const; 51 | /// \overload 52 | stream_id id() const; 53 | 54 | /// read some bytes into the given buffer sequence 55 | template // void(error_code, size_t) 57 | decltype(auto) async_read_some(const MutableBufferSequence& buffers, 58 | CompletionToken&& token) { 59 | return impl.async_read_some(buffers, std::forward(token)); 60 | } 61 | 62 | /// read some bytes into the given buffer sequence 63 | template 64 | size_t read_some(const MutableBufferSequence& buffers, error_code& ec) { 65 | return impl.read_some(buffers, ec); 66 | } 67 | /// \overload 68 | template 69 | size_t read_some(const MutableBufferSequence& buffers) { 70 | error_code ec; 71 | const size_t bytes = impl.read_some(buffers, ec); 72 | if (ec) { 73 | throw system_error(ec); 74 | } 75 | return bytes; 76 | } 77 | 78 | /// write some bytes from the given buffer sequence. written bytes may be 79 | /// buffered until they fill an outgoing packet 80 | template // void(error_code, size_t) 82 | decltype(auto) async_write_some(const ConstBufferSequence& buffers, 83 | CompletionToken&& token) { 84 | return impl.async_write_some(buffers, std::forward(token)); 85 | } 86 | 87 | /// write some bytes from the given buffer sequence. written bytes may be 88 | /// buffered until they fill an outgoing packet 89 | template 90 | size_t write_some(const ConstBufferSequence& buffers, error_code& ec) { 91 | return impl.write_some(buffers, ec); 92 | } 93 | /// \overload 94 | template 95 | size_t write_some(const ConstBufferSequence& buffers) { 96 | error_code ec; 97 | const size_t bytes = impl.write_some(buffers, ec); 98 | if (ec) { 99 | throw system_error(ec); 100 | } 101 | return bytes; 102 | } 103 | 104 | /// flush any bytes that were buffered by write_some()/async_write_some() but 105 | /// not yet delivered 106 | void flush(error_code& ec); 107 | /// \overload 108 | void flush(); 109 | 110 | /// shut down a stream for reads (0), writes (1), or both (2). shutting down 111 | /// the read side will cancel any pending read operations. shutting down the 112 | /// write side will flush any buffered data, and cancel any pending write 113 | /// operations 114 | void shutdown(int how, error_code& ec); 115 | /// \overload 116 | void shutdown(int how); 117 | 118 | /// close the stream gracefully, blocking until all written data is 119 | /// acknowledged by the peer. the associated connection must remain open until 120 | /// this graceful shutdown completes 121 | template // void(error_code) 122 | decltype(auto) async_close(CompletionToken&& token) { 123 | return impl.async_close(std::forward(token)); 124 | } 125 | /// \overload 126 | void close(error_code& ec); 127 | /// \overload 128 | void close(); 129 | 130 | /// reset the stream immediately in both directions, canceling any pending 131 | /// operations and discarding any unacked data 132 | void reset(); 133 | }; 134 | 135 | } // namespace nexus::quic 136 | -------------------------------------------------------------------------------- /include/nexus/quic/stream_id.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace nexus::quic { 6 | 7 | /// stream identifier that is unique to a connection 8 | using stream_id = uint64_t; 9 | 10 | 11 | /// return true if the stream id was initiated by the client 12 | inline bool client_initiated(stream_id id) 13 | { 14 | return (id & 0x1) == 0; 15 | } 16 | 17 | /// return true if the stream id was initiated by the server 18 | inline bool server_initiated(stream_id id) 19 | { 20 | return (id & 0x1); 21 | } 22 | 23 | } // namespace nexus::quic 24 | -------------------------------------------------------------------------------- /include/nexus/ssl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace nexus { 6 | 7 | namespace ssl = boost::asio::ssl; 8 | 9 | } // namespace nexus 10 | -------------------------------------------------------------------------------- /include/nexus/udp.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace nexus { 8 | 9 | using boost::asio::ip::udp; 10 | 11 | namespace detail { 12 | 13 | template 14 | class socket_option { 15 | int value; 16 | public: 17 | constexpr socket_option(bool b) : value(b ? 1 : 0) {} 18 | 19 | constexpr operator bool() const { return value; } 20 | 21 | template 22 | constexpr int level(const Protocol& proto) const { 23 | return proto.family() == PF_INET6 ? IPPROTO_IPV6 : IPPROTO_IP; 24 | } 25 | template 26 | constexpr int name(const Protocol& proto) const { 27 | return proto.family() == PF_INET6 ? Name6 : Name4; 28 | } 29 | template 30 | constexpr size_t size(const Protocol&) const { 31 | return sizeof(value); 32 | } 33 | template 34 | constexpr void* data(const Protocol&) { 35 | return &value; 36 | } 37 | template 38 | constexpr const void* data(const Protocol&) const { 39 | return &value; 40 | } 41 | template 42 | constexpr void resize(Protocol&, std::size_t) {} 43 | }; 44 | 45 | template 46 | error_code set_option(Socket& sock, Option&& option) { 47 | error_code ec; 48 | sock.set_option(option, ec); 49 | return ec; 50 | } 51 | 52 | template 53 | error_code set_options(Socket& sock, Options&& ...options) { 54 | error_code ec; 55 | // fold expression calls set_option() on each until one returns an error 56 | ((ec = set_option(sock, options)) || ...); 57 | return ec; 58 | } 59 | 60 | } // namespace detail 61 | 62 | using receive_ecn = detail::socket_option; 63 | 64 | #ifdef IP_RECVORIGDSTADDR 65 | using receive_dstaddr = detail::socket_option; 66 | #else 67 | using receive_dstaddr = detail::socket_option; 68 | #endif 69 | 70 | } // namespace nexus 71 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(nexus-srcs 2 | client.cc 3 | connection.cc 4 | connection_state.cc 5 | engine.cc 6 | error.cc 7 | global.cc 8 | server.cc 9 | settings.cc 10 | socket.cc 11 | stream.cc 12 | stream_state.cc) 13 | 14 | add_library(nexus ${nexus-srcs}) 15 | target_link_libraries(nexus PUBLIC nexus-headers lsquic) 16 | install(TARGETS nexus LIBRARY DESTINATION lib) 17 | -------------------------------------------------------------------------------- /src/client.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace nexus { 7 | namespace quic { 8 | 9 | client::client(const executor_type& ex, const udp::endpoint& endpoint, 10 | ssl::context& ctx) 11 | : engine(ex, &socket, nullptr, 0), 12 | socket(engine, endpoint, false, ctx) 13 | { 14 | } 15 | 16 | client::client(const executor_type& ex, const udp::endpoint& endpoint, 17 | ssl::context& ctx, const settings& s) 18 | : engine(ex, &socket, &s, 0), 19 | socket(engine, endpoint, false, ctx) 20 | { 21 | } 22 | 23 | client::client(udp::socket&& socket, ssl::context& ctx) 24 | : engine(socket.get_executor(), &this->socket, nullptr, 0), 25 | socket(engine, std::move(socket), ctx) 26 | { 27 | } 28 | 29 | client::client(udp::socket&& socket, ssl::context& ctx, const settings& s) 30 | : engine(socket.get_executor(), &this->socket, &s, 0), 31 | socket(engine, std::move(socket), ctx) 32 | { 33 | } 34 | 35 | client::executor_type client::get_executor() const 36 | { 37 | return engine.get_executor(); 38 | } 39 | 40 | udp::endpoint client::local_endpoint() const 41 | { 42 | return socket.local_endpoint(); 43 | } 44 | 45 | void client::connect(connection& conn, 46 | const udp::endpoint& endpoint, 47 | const char* hostname) 48 | { 49 | socket.connect(conn.impl, endpoint, hostname); 50 | } 51 | 52 | void client::close() 53 | { 54 | engine.close(); 55 | socket.close(); 56 | } 57 | 58 | } // namespace quic 59 | 60 | namespace h3 { 61 | 62 | client::client(const executor_type& ex, const udp::endpoint& endpoint, 63 | ssl::context& ctx) 64 | : engine(ex, &socket, nullptr, LSENG_HTTP), 65 | socket(engine, endpoint, false, ctx) 66 | { 67 | } 68 | 69 | client::client(const executor_type& ex, const udp::endpoint& endpoint, 70 | ssl::context& ctx, const quic::settings& s) 71 | : engine(ex, &socket, &s, LSENG_HTTP), 72 | socket(engine, endpoint, false, ctx) 73 | { 74 | } 75 | 76 | client::client(udp::socket&& socket, ssl::context& ctx) 77 | : engine(socket.get_executor(), &this->socket, nullptr, LSENG_HTTP), 78 | socket(engine, std::move(socket), ctx) 79 | { 80 | } 81 | 82 | client::client(udp::socket&& socket, ssl::context& ctx, 83 | const quic::settings& s) 84 | : engine(socket.get_executor(), &this->socket, &s, LSENG_HTTP), 85 | socket(engine, std::move(socket), ctx) 86 | { 87 | } 88 | 89 | client::executor_type client::get_executor() const 90 | { 91 | return engine.get_executor(); 92 | } 93 | 94 | udp::endpoint client::local_endpoint() const 95 | { 96 | return socket.local_endpoint(); 97 | } 98 | 99 | void client::connect(client_connection& conn, 100 | const udp::endpoint& endpoint, 101 | const char* hostname) 102 | { 103 | socket.connect(conn.impl, endpoint, hostname); 104 | } 105 | 106 | void client::close() 107 | { 108 | engine.close(); 109 | socket.close(); 110 | } 111 | 112 | bool client_connection::is_open() const 113 | { 114 | return impl.is_open(); 115 | } 116 | 117 | quic::connection_id client_connection::id(error_code& ec) const 118 | { 119 | return impl.id(ec); 120 | } 121 | 122 | quic::connection_id client_connection::id() const 123 | { 124 | error_code ec; 125 | auto i = impl.id(ec); 126 | if (ec) { 127 | throw system_error(ec); 128 | } 129 | return i; 130 | } 131 | 132 | udp::endpoint client_connection::remote_endpoint(error_code& ec) const 133 | { 134 | return impl.remote_endpoint(ec); 135 | } 136 | 137 | udp::endpoint client_connection::remote_endpoint() const 138 | { 139 | error_code ec; 140 | auto e = impl.remote_endpoint(ec); 141 | if (ec) { 142 | throw system_error(ec); 143 | } 144 | return e; 145 | } 146 | 147 | void client_connection::connect(stream& s, error_code& ec) 148 | { 149 | auto op = quic::detail::stream_connect_sync{s.impl}; 150 | impl.connect(op); 151 | op.wait(); 152 | ec = std::get<0>(*op.result); 153 | } 154 | 155 | void client_connection::connect(stream& s) 156 | { 157 | error_code ec; 158 | connect(s, ec); 159 | if (ec) { 160 | throw system_error(ec); 161 | } 162 | } 163 | 164 | void client_connection::go_away(error_code& ec) 165 | { 166 | impl.go_away(ec); 167 | } 168 | 169 | void client_connection::go_away() 170 | { 171 | error_code ec; 172 | impl.go_away(ec); 173 | if (ec) { 174 | throw system_error(ec); 175 | } 176 | } 177 | 178 | void client_connection::close(error_code& ec) 179 | { 180 | impl.close(ec); 181 | } 182 | 183 | void client_connection::close() 184 | { 185 | error_code ec; 186 | close(ec); 187 | if (ec) { 188 | throw system_error(ec); 189 | } 190 | } 191 | 192 | } // namespace h3 193 | } // namespace nexus 194 | -------------------------------------------------------------------------------- /src/connection.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace nexus::quic { 8 | 9 | connection::connection(acceptor& a) : impl(a.impl) {} 10 | connection::connection(client& c) : impl(c.socket) {} 11 | 12 | connection::connection(client& c, const udp::endpoint& endpoint, 13 | const char* hostname) 14 | : impl(c.socket) 15 | { 16 | c.connect(*this, endpoint, hostname); 17 | } 18 | 19 | connection::executor_type connection::get_executor() const 20 | { 21 | return impl.get_executor(); 22 | } 23 | 24 | bool connection::is_open() const 25 | { 26 | return impl.is_open(); 27 | } 28 | 29 | connection_id connection::id(error_code& ec) const 30 | { 31 | return impl.id(ec); 32 | } 33 | 34 | connection_id connection::id() const 35 | { 36 | error_code ec; 37 | auto i = impl.id(ec); 38 | if (ec) { 39 | throw system_error(ec); 40 | } 41 | return i; 42 | } 43 | 44 | udp::endpoint connection::remote_endpoint(error_code& ec) const 45 | { 46 | return impl.remote_endpoint(ec); 47 | } 48 | 49 | udp::endpoint connection::remote_endpoint() const 50 | { 51 | error_code ec; 52 | auto e = impl.remote_endpoint(ec); 53 | if (ec) { 54 | throw system_error(ec); 55 | } 56 | return e; 57 | } 58 | 59 | void connection::connect(stream& s, error_code& ec) 60 | { 61 | auto op = detail::stream_connect_sync{s.impl}; 62 | impl.connect(op); 63 | op.wait(); 64 | ec = std::get<0>(*op.result); 65 | } 66 | 67 | void connection::connect(stream& s) 68 | { 69 | error_code ec; 70 | connect(s, ec); 71 | if (ec) { 72 | throw system_error(ec); 73 | } 74 | } 75 | 76 | void connection::accept(stream& s, error_code& ec) 77 | { 78 | auto op = detail::stream_accept_sync{s.impl}; 79 | impl.accept(op); 80 | op.wait(); 81 | ec = std::get<0>(*op.result); 82 | } 83 | 84 | void connection::accept(stream& s) 85 | { 86 | error_code ec; 87 | accept(s, ec); 88 | if (ec) { 89 | throw system_error(ec); 90 | } 91 | } 92 | 93 | void connection::go_away(error_code& ec) 94 | { 95 | impl.go_away(ec); 96 | } 97 | 98 | void connection::go_away() 99 | { 100 | error_code ec; 101 | impl.go_away(ec); 102 | if (ec) { 103 | throw system_error(ec); 104 | } 105 | } 106 | 107 | void connection::close(error_code& ec) 108 | { 109 | impl.close(ec); 110 | } 111 | 112 | void connection::close() 113 | { 114 | error_code ec; 115 | close(ec); 116 | if (ec) { 117 | throw system_error(ec); 118 | } 119 | } 120 | 121 | namespace detail { 122 | 123 | connection_impl::connection_impl(socket_impl& socket) 124 | : connection_context(false), 125 | svc(boost::asio::use_service>( 126 | boost::asio::query(socket.get_executor(), 127 | boost::asio::execution::context))), 128 | socket(socket), state(connection_state::closed{}) 129 | { 130 | // register for service_shutdown() notifications 131 | svc.add(*this); 132 | } 133 | 134 | connection_impl::~connection_impl() 135 | { 136 | error_code ec_ignored; 137 | close(ec_ignored); 138 | svc.remove(*this); 139 | } 140 | 141 | void connection_impl::service_shutdown() 142 | { 143 | // destroy any pending operations 144 | connection_state::destroy(state); 145 | } 146 | 147 | connection_impl::executor_type connection_impl::get_executor() const 148 | { 149 | return socket.get_executor(); 150 | } 151 | 152 | bool connection_impl::is_open() const 153 | { 154 | auto lock = std::unique_lock{socket.engine.mutex}; 155 | return connection_state::is_open(state); 156 | } 157 | 158 | connection_id connection_impl::id(error_code& ec) const 159 | { 160 | auto lock = std::unique_lock{socket.engine.mutex}; 161 | return connection_state::id(state, ec); 162 | } 163 | 164 | udp::endpoint connection_impl::remote_endpoint(error_code& ec) const 165 | { 166 | auto lock = std::unique_lock{socket.engine.mutex}; 167 | return connection_state::remote_endpoint(state, ec); 168 | } 169 | 170 | void connection_impl::connect(stream_connect_operation& op) 171 | { 172 | auto lock = std::unique_lock{socket.engine.mutex}; 173 | if (connection_state::stream_connect(state, op)) { 174 | socket.engine.process(lock); 175 | } 176 | } 177 | 178 | stream_impl* connection_impl::on_connect(lsquic_stream_t* stream) 179 | { 180 | return connection_state::on_stream_connect(state, stream, socket.engine.is_http); 181 | } 182 | 183 | void connection_impl::accept(stream_accept_operation& op) 184 | { 185 | auto lock = std::unique_lock{socket.engine.mutex}; 186 | connection_state::stream_accept(state, op, socket.engine.is_http); 187 | } 188 | 189 | stream_impl* connection_impl::on_accept(lsquic_stream* stream) 190 | { 191 | return connection_state::on_stream_accept(state, stream, socket.engine.is_http); 192 | } 193 | 194 | void connection_impl::go_away(error_code& ec) 195 | { 196 | auto lock = std::unique_lock{socket.engine.mutex}; 197 | const auto t = connection_state::goaway(state, ec); 198 | if (t == connection_state::transition::open_to_going_away) { 199 | socket.engine.process(lock); 200 | } 201 | } 202 | 203 | void connection_impl::close(error_code& ec) 204 | { 205 | auto lock = std::unique_lock{socket.engine.mutex}; 206 | const auto t = connection_state::close(state, ec); 207 | switch (t) { 208 | case connection_state::transition::accepting_to_closed: 209 | list_erase(*this, socket.accepting_connections); 210 | break; 211 | case connection_state::transition::open_to_closed: 212 | case connection_state::transition::going_away_to_closed: 213 | list_erase(*this, socket.open_connections); 214 | socket.engine.process(lock); 215 | break; 216 | default: 217 | break; 218 | } 219 | } 220 | 221 | void connection_impl::on_close() 222 | { 223 | const auto t = connection_state::on_close(state); 224 | switch (t) { 225 | case connection_state::transition::open_to_error: 226 | case connection_state::transition::open_to_closed: 227 | case connection_state::transition::going_away_to_error: 228 | case connection_state::transition::going_away_to_closed: 229 | list_erase(*this, socket.open_connections); 230 | break; 231 | default: 232 | break; 233 | } 234 | } 235 | 236 | void connection_impl::on_handshake(int status) 237 | { 238 | connection_state::on_handshake(state, status); 239 | } 240 | 241 | void connection_impl::on_remote_goaway() 242 | { 243 | connection_state::on_remote_goaway(state); 244 | } 245 | 246 | void connection_impl::on_remote_close(int app_error, uint64_t code) 247 | { 248 | error_code ec; 249 | if (app_error == -1) { 250 | ec = make_error_code(connection_error::reset); 251 | } else if (app_error) { 252 | ec.assign(code, application_category()); 253 | } else if ((code & 0xffff'ffff'ffff'ff00) == 0x0100) { 254 | // CRYPTO_ERROR 0x0100-0x01ff 255 | ec.assign(code & 0xff, tls_category()); 256 | } else { 257 | ec.assign(code, transport_category()); 258 | } 259 | 260 | const auto t = connection_state::on_remote_close(state, ec); 261 | switch (t) { 262 | case connection_state::transition::open_to_error: 263 | case connection_state::transition::open_to_closed: 264 | case connection_state::transition::going_away_to_error: 265 | case connection_state::transition::going_away_to_closed: 266 | list_erase(*this, socket.open_connections); 267 | break; 268 | default: 269 | break; 270 | } 271 | } 272 | 273 | void connection_impl::on_accepting_stream_closed(stream_impl& s) 274 | { 275 | if (std::holds_alternative(state)) { 276 | auto& o = *std::get_if(&state); 277 | list_erase(s, o.accepting_streams); 278 | } 279 | } 280 | 281 | void connection_impl::on_connecting_stream_closed(stream_impl& s) 282 | { 283 | if (std::holds_alternative(state)) { 284 | auto& o = *std::get_if(&state); 285 | list_erase(s, o.connecting_streams); 286 | } 287 | } 288 | 289 | void connection_impl::on_open_stream_closing(stream_impl& s) 290 | { 291 | if (std::holds_alternative(state)) { 292 | auto& o = *std::get_if(&state); 293 | list_transfer(s, o.open_streams, o.closing_streams); 294 | } 295 | } 296 | 297 | void connection_impl::on_open_stream_closed(stream_impl& s) 298 | { 299 | if (std::holds_alternative(state)) { 300 | auto& o = *std::get_if(&state); 301 | list_erase(s, o.open_streams); 302 | } else if (std::holds_alternative(state)) { 303 | auto& g = *std::get_if(&state); 304 | list_erase(s, g.open_streams); 305 | } 306 | } 307 | 308 | void connection_impl::on_closing_stream_closed(stream_impl& s) 309 | { 310 | if (std::holds_alternative(state)) { 311 | auto& o = *std::get_if(&state); 312 | list_erase(s, o.closing_streams); 313 | } else if (std::holds_alternative(state)) { 314 | auto& g = *std::get_if(&state); 315 | list_erase(s, g.closing_streams); 316 | } 317 | } 318 | 319 | } // namespace detail 320 | 321 | } // namespace nexus::quic 322 | -------------------------------------------------------------------------------- /src/engine.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "recv_header_set.hpp" 11 | 12 | namespace nexus::quic::detail { 13 | 14 | void engine_deleter::operator()(lsquic_engine* e) const { 15 | ::lsquic_engine_destroy(e); 16 | } 17 | 18 | engine_impl::~engine_impl() 19 | { 20 | close(); 21 | } 22 | 23 | stream_impl* engine_impl::on_new_stream(connection_impl& c, 24 | lsquic_stream_t* stream) 25 | { 26 | // XXX: any way to decide between connect/accept without stream id? 27 | const auto id = ::lsquic_stream_id(stream); 28 | const int server = !client; 29 | if ((id & 1) == server) { // self-initiated 30 | return c.on_connect(stream); 31 | } else { // peer-initiated 32 | return c.on_accept(stream); 33 | } 34 | } 35 | 36 | void engine_impl::close() 37 | { 38 | auto lock = std::unique_lock{mutex}; 39 | ::lsquic_engine_cooldown(handle.get()); 40 | process(lock); 41 | } 42 | 43 | void engine_impl::process(std::unique_lock& lock) 44 | { 45 | ::lsquic_engine_process_conns(handle.get()); 46 | reschedule(lock); 47 | } 48 | 49 | void engine_impl::reschedule(std::unique_lock& lock) 50 | { 51 | int micros = 0; 52 | if (!::lsquic_engine_earliest_adv_tick(handle.get(), µs)) { 53 | // no connections to process. servers should keep listening for packets, 54 | // but clients can stop reading 55 | if (client && client->receiving) { 56 | client->receiving = false; 57 | client->socket.cancel(); 58 | } 59 | timer.cancel(); 60 | return; 61 | } 62 | if (micros <= 0) { 63 | process(lock); 64 | return; 65 | } 66 | const auto dur = std::chrono::microseconds{micros}; 67 | timer.expires_after(dur); 68 | timer.async_wait([this] (error_code ec) { 69 | if (!ec) { 70 | on_timer(); 71 | } 72 | }); 73 | } 74 | 75 | void engine_impl::on_timer() 76 | { 77 | auto lock = std::unique_lock{mutex}; 78 | process(lock); 79 | } 80 | 81 | int engine_impl::send_packets(const lsquic_out_spec* specs, unsigned n_specs) 82 | { 83 | auto p = specs; 84 | const auto end = std::next(p, n_specs); 85 | while (p < end) { 86 | socket_impl& socket = *static_cast(p->peer_ctx); 87 | error_code ec; 88 | p = socket.send_packets(p, end, ec); 89 | if (ec) { 90 | break; 91 | } 92 | } 93 | return std::distance(specs, p); 94 | } 95 | 96 | 97 | // stream api 98 | static lsquic_conn_ctx_t* on_new_conn(void* ectx, lsquic_conn_t* conn) 99 | { 100 | auto estate = static_cast(ectx); 101 | auto cctx = ::lsquic_conn_get_ctx(conn); 102 | 103 | // outgoing connections will have a context set by lsquic_engine_connect() 104 | if (cctx) { 105 | auto c = reinterpret_cast(cctx); 106 | c->socket.on_connect(*c, conn); 107 | return cctx; 108 | } 109 | // for incoming connections, determine which socket is associated with the 110 | // connection's local address 111 | const sockaddr* local = nullptr; 112 | const sockaddr* peer = nullptr; 113 | int r = ::lsquic_conn_get_sockaddr(conn, &local, &peer); 114 | if (r != 0) { 115 | return nullptr; 116 | } 117 | // get the peer_ctx from our call to lsquic_engine_packet_in() 118 | auto peer_ctx = ::lsquic_conn_get_peer_ctx(conn, local); 119 | assert(peer_ctx); 120 | auto& socket = *static_cast(peer_ctx); 121 | return reinterpret_cast(socket.on_accept(conn)); 122 | } 123 | 124 | static lsquic_stream_ctx_t* on_new_stream(void* ectx, lsquic_stream_t* stream) 125 | { 126 | auto estate = static_cast(ectx); 127 | if (stream == nullptr) { 128 | return nullptr; // connection went away? 129 | } 130 | auto conn = ::lsquic_stream_conn(stream); 131 | auto ctx = reinterpret_cast(::lsquic_conn_get_ctx(conn)); 132 | assert(ctx); 133 | if (ctx->incoming) { 134 | auto c = static_cast(ctx); 135 | assert(!c->incoming_streams.full()); // lsquic shouldn't allow this 136 | c->incoming_streams.push_back(stream); 137 | return nullptr; 138 | } else { 139 | auto c = static_cast(ctx); 140 | auto s = estate->on_new_stream(*c, stream); 141 | return reinterpret_cast(s); 142 | } 143 | } 144 | 145 | static void on_read(lsquic_stream_t* stream, lsquic_stream_ctx_t* sctx) 146 | { 147 | auto s = reinterpret_cast(sctx); 148 | s->on_read(); 149 | } 150 | 151 | static void on_write(lsquic_stream_t* stream, lsquic_stream_ctx_t* sctx) 152 | { 153 | auto s = reinterpret_cast(sctx); 154 | s->on_write(); 155 | } 156 | 157 | static void on_close(lsquic_stream_t* stream, lsquic_stream_ctx_t* sctx) 158 | { 159 | auto s = reinterpret_cast(sctx); 160 | if (s) { 161 | s->on_close(); 162 | } 163 | } 164 | 165 | static void on_conn_closed(lsquic_conn_t* conn) 166 | { 167 | auto cctx = ::lsquic_conn_get_ctx(conn); 168 | if (!cctx) { 169 | return; 170 | } 171 | auto c = reinterpret_cast(cctx); 172 | c->on_close(); 173 | } 174 | 175 | static void on_hsk_done(lsquic_conn_t* conn, lsquic_hsk_status s) 176 | { 177 | auto cctx = ::lsquic_conn_get_ctx(conn); 178 | if (!cctx) { 179 | return; 180 | } 181 | auto c = reinterpret_cast(cctx); 182 | c->on_handshake(s); 183 | } 184 | 185 | void on_goaway_received(lsquic_conn_t* conn) 186 | { 187 | auto cctx = ::lsquic_conn_get_ctx(conn); 188 | if (!cctx) { 189 | return; 190 | } 191 | auto c = reinterpret_cast(cctx); 192 | c->on_remote_goaway(); 193 | } 194 | 195 | void on_conncloseframe_received(lsquic_conn_t* conn, 196 | int app_error, uint64_t code, 197 | const char* reason, int reason_len) 198 | { 199 | auto ctx = reinterpret_cast(::lsquic_conn_get_ctx(conn)); 200 | if (!ctx) { 201 | return; 202 | } 203 | assert(!ctx->incoming); 204 | auto c = reinterpret_cast(ctx); 205 | c->on_remote_close(app_error, code); 206 | } 207 | 208 | static constexpr lsquic_stream_if make_stream_api() 209 | { 210 | lsquic_stream_if api = {}; 211 | api.on_new_conn = on_new_conn; 212 | api.on_conn_closed = on_conn_closed; 213 | api.on_new_stream = on_new_stream; 214 | api.on_read = on_read; 215 | api.on_write = on_write; 216 | api.on_close = on_close; 217 | api.on_hsk_done = on_hsk_done; 218 | api.on_goaway_received = on_goaway_received; 219 | api.on_conncloseframe_received = on_conncloseframe_received; 220 | return api; 221 | } 222 | 223 | 224 | // header set api 225 | static void* header_set_create(void* ctx, lsquic_stream_t* stream, 226 | int is_push_promise) 227 | { 228 | // TODO: store this in stream_impl to avoid allocation? 229 | return new recv_header_set(is_push_promise); 230 | } 231 | 232 | static lsxpack_header* header_set_prepare(void* hset, lsxpack_header* hdr, 233 | size_t space) 234 | { 235 | auto headers = reinterpret_cast(hset); 236 | auto& header = headers->header; 237 | auto& buf = headers->buffer; 238 | buf.resize(space); 239 | if (hdr) { // existing header, just update the pointer and capacity 240 | header.buf = buf.data(); 241 | header.val_len = space; 242 | } else { // initialize the entire header 243 | lsxpack_header_prepare_decode(&header, buf.data(), 0, space); 244 | } 245 | return &header; 246 | } 247 | 248 | static int header_set_process(void* hset, lsxpack_header* hdr) 249 | { 250 | if (hdr) { 251 | auto headers = reinterpret_cast(hset); 252 | auto name = std::string_view{hdr->buf + hdr->name_offset, hdr->name_len}; 253 | auto value = std::string_view{hdr->buf + hdr->val_offset, hdr->val_len}; 254 | const bool never_index = hdr->flags & LSXPACK_NEVER_INDEX; 255 | auto f = headers->fields.insert(name, value, never_index); 256 | } 257 | return 0; 258 | } 259 | 260 | static void header_set_discard(void* hset) 261 | { 262 | delete reinterpret_cast(hset); 263 | } 264 | 265 | static constexpr lsquic_hset_if make_header_api() 266 | { 267 | lsquic_hset_if api = {}; 268 | api.hsi_create_header_set = header_set_create; 269 | api.hsi_prepare_decode = header_set_prepare; 270 | api.hsi_process_header = header_set_process; 271 | api.hsi_discard_header_set = header_set_discard; 272 | return api; 273 | } 274 | 275 | static int api_send_packets(void* ectx, const lsquic_out_spec *specs, 276 | unsigned n_specs) 277 | { 278 | auto estate = static_cast(ectx); 279 | return estate->send_packets(specs, n_specs); 280 | } 281 | 282 | ssl_ctx_st* api_peer_ssl_ctx(void* peer_ctx, const sockaddr* local) 283 | { 284 | auto& socket = *static_cast(peer_ctx); 285 | return socket.ssl.native_handle(); 286 | } 287 | 288 | engine_impl::engine_impl(const boost::asio::any_io_executor& ex, 289 | socket_impl* client, const settings* s, 290 | unsigned flags) 291 | : ex(ex), timer(ex), client(client), is_http(flags & LSENG_HTTP) 292 | { 293 | lsquic_engine_api api = {}; 294 | api.ea_packets_out = api_send_packets; 295 | api.ea_packets_out_ctx = this; 296 | static const lsquic_stream_if stream_api = make_stream_api(); 297 | api.ea_stream_if = &stream_api; 298 | api.ea_stream_if_ctx = this; 299 | api.ea_get_ssl_ctx = api_peer_ssl_ctx; 300 | if (flags & LSENG_HTTP) { 301 | static const lsquic_hset_if header_api = make_header_api(); 302 | api.ea_hsi_if = &header_api; 303 | api.ea_hsi_ctx = this; 304 | } 305 | 306 | // apply and validate the settings 307 | lsquic_engine_settings es; 308 | ::lsquic_engine_init_settings(&es, flags); 309 | if (s) { 310 | write_settings(*s, es); 311 | } 312 | es.es_versions = (1 << LSQVER_I001); // RFC version only 313 | char errbuf[256]; 314 | int r = ::lsquic_engine_check_settings(&es, flags, errbuf, sizeof(errbuf)); 315 | if (r == -1) { 316 | throw bad_setting(errbuf); 317 | } 318 | es.es_delay_onclose = 1; 319 | api.ea_settings = &es; 320 | 321 | max_streams_per_connection = es.es_init_max_streams_bidi; 322 | 323 | handle.reset(::lsquic_engine_new(flags, &api)); 324 | } 325 | 326 | } // namespace nexus::quic::detail 327 | -------------------------------------------------------------------------------- /src/error.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace nexus { 6 | namespace quic { 7 | 8 | const error_category& connection_category() 9 | { 10 | struct category : public error_category { 11 | const char* name() const noexcept override { 12 | return "nexus::quic::connection"; 13 | } 14 | 15 | std::string message(int ev) const override { 16 | switch (static_cast(ev)) { 17 | case connection_error::aborted: 18 | return "connection aborted"; 19 | case connection_error::handshake_failed: 20 | return "connection handshake failed"; 21 | case connection_error::timed_out: 22 | return "connection timed out"; 23 | case connection_error::reset: 24 | return "connection reset by peer"; 25 | case connection_error::going_away: 26 | return "connection is going away"; 27 | case connection_error::peer_going_away: 28 | return "peer is going away"; 29 | default: 30 | return "unknown"; 31 | } 32 | } 33 | 34 | error_condition default_error_condition(int code) const noexcept override { 35 | switch (static_cast(code)) { 36 | case connection_error::aborted: 37 | return errc::connection_aborted; 38 | 39 | case connection_error::timed_out: 40 | return errc::timed_out; 41 | 42 | case connection_error::reset: 43 | return errc::connection_reset; 44 | 45 | default: 46 | return {code, category()}; 47 | } 48 | } 49 | }; 50 | static category instance; 51 | return instance; 52 | } 53 | 54 | const error_category& stream_category() 55 | { 56 | struct category : public error_category { 57 | const char* name() const noexcept override { 58 | return "nexus::quic::stream"; 59 | } 60 | 61 | std::string message(int ev) const override { 62 | switch (static_cast(ev)) { 63 | case stream_error::eof: 64 | return "end of stream"; 65 | case stream_error::busy: 66 | return "stream busy"; 67 | case stream_error::aborted: 68 | return "stream aborted"; 69 | case stream_error::reset: 70 | return "stream reset by peer"; 71 | default: 72 | return "unknown"; 73 | } 74 | } 75 | 76 | error_condition default_error_condition(int code) const noexcept override { 77 | switch (static_cast(code)) { 78 | case stream_error::busy: 79 | return errc::device_or_resource_busy; 80 | 81 | case stream_error::aborted: 82 | return errc::connection_aborted; 83 | 84 | case stream_error::reset: 85 | return errc::connection_reset; 86 | 87 | default: 88 | return {code, category()}; 89 | } 90 | } 91 | }; 92 | static category instance; 93 | return instance; 94 | } 95 | 96 | const error_category& application_category() 97 | { 98 | struct category : public error_category { 99 | const char* name() const noexcept override { 100 | return "nexus::quic::application"; 101 | } 102 | std::string message(int ev) const override { 103 | return "unknown"; 104 | } 105 | }; 106 | static category instance; 107 | return instance; 108 | } 109 | 110 | const error_category& transport_category() 111 | { 112 | struct category : public error_category { 113 | const char* name() const noexcept override { 114 | return "nexus::quic::transport"; 115 | } 116 | 117 | std::string message(int ev) const override { 118 | switch (static_cast(ev)) { 119 | case transport_error::no_error: 120 | return "no error"; 121 | case transport_error::internal_error: 122 | return "internal error"; 123 | case transport_error::connection_refused: 124 | return "connection refused"; 125 | case transport_error::flow_control_error: 126 | return "flow control error"; 127 | case transport_error::stream_limit_error: 128 | return "stream limit error"; 129 | case transport_error::stream_state_error: 130 | return "stream state error"; 131 | case transport_error::final_size_error: 132 | return "final size error"; 133 | case transport_error::frame_encoding_error: 134 | return "frame encoding error"; 135 | case transport_error::transport_parameter_error: 136 | return "transport parameter error"; 137 | case transport_error::connection_id_limit_error: 138 | return "connection id limit error"; 139 | case transport_error::protocol_violation: 140 | return "protocol violation"; 141 | case transport_error::invalid_token: 142 | return "invalid token"; 143 | case transport_error::application_error: 144 | return "application error"; 145 | case transport_error::crypto_buffer_exceeded: 146 | return "crypto buffer exceeded"; 147 | case transport_error::key_update_error: 148 | return "key update error"; 149 | case transport_error::aead_limit_reached: 150 | return "aead limit reached"; 151 | case transport_error::no_viable_path: 152 | return "no viable path"; 153 | default: 154 | return "unknown"; 155 | } 156 | } 157 | }; 158 | static category instance; 159 | return instance; 160 | } 161 | 162 | const error_category& tls_category() 163 | { 164 | struct category : public error_category { 165 | const char* name() const noexcept override { 166 | return "nexus::quic::tls"; 167 | } 168 | std::string message(int ev) const override { 169 | return ::SSL_alert_desc_string_long(ev); 170 | } 171 | }; 172 | static category instance; 173 | return instance; 174 | } 175 | 176 | } // namespace quic 177 | 178 | namespace h3 { 179 | 180 | const error_category& quic_category() 181 | { 182 | struct category : public error_category { 183 | const char* name() const noexcept override { 184 | return "nexus::quic"; 185 | } 186 | 187 | std::string message(int ev) const override { 188 | switch (static_cast(ev)) { 189 | case error::no_error: 190 | return "no error"; 191 | case error::general_protocol_error: 192 | return "general protocol error"; 193 | case error::internal_error: 194 | return "internal error"; 195 | case error::stream_creation_error: 196 | return "stream creation error"; 197 | case error::closed_critical_stream: 198 | return "closed critical stream"; 199 | case error::frame_unexpected: 200 | return "frame unexpected"; 201 | case error::frame_error: 202 | return "frame error"; 203 | case error::excessive_load: 204 | return "excessive load"; 205 | case error::id_error: 206 | return "id error"; 207 | case error::settings_error: 208 | return "settings error"; 209 | case error::missing_settings: 210 | return "missing settings"; 211 | case error::request_rejected: 212 | return "request rejected"; 213 | case error::request_cancelled: 214 | return "request cancelled"; 215 | case error::request_incomplete: 216 | return "request incomplete"; 217 | case error::message_error: 218 | return "message error"; 219 | case error::connect_error: 220 | return "connect error"; 221 | case error::version_fallback: 222 | return "version fallback"; 223 | case error::qpack_decompression_failed: 224 | return "qpack decompression failed"; 225 | case error::qpack_encoder_stream_error: 226 | return "qpack encoder stream error"; 227 | case error::qpack_decoder_stream_error: 228 | return "qpack decoder stream error"; 229 | default: 230 | return "unknown"; 231 | } 232 | } 233 | }; 234 | static category instance; 235 | return instance; 236 | } 237 | 238 | } // namespace h3 239 | } // namespace nexus 240 | -------------------------------------------------------------------------------- /src/global.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | namespace nexus::global { 7 | 8 | const error_category& global_category() 9 | { 10 | struct category : public error_category { 11 | const char* name() const noexcept override { 12 | return "nexus::global"; 13 | } 14 | std::string message(int ev) const override { 15 | switch (static_cast(ev)) { 16 | case global::error::init_failed: 17 | return "global initialization failed"; 18 | default: 19 | return "unknown"; 20 | } 21 | } 22 | }; 23 | static category instance; 24 | return instance; 25 | } 26 | 27 | namespace detail { 28 | 29 | int log(void* ctx, const char* buf, size_t len) 30 | { 31 | return ::fwrite(buf, 1, len, stderr); 32 | } 33 | 34 | context init(int flags, error_code& ec) 35 | { 36 | if (int r = ::lsquic_global_init(flags); r != 0) { 37 | ec = make_error_code(error::init_failed); 38 | return {}; // failure 39 | } 40 | return ::lsquic_global_cleanup; // success 41 | } 42 | 43 | } // namespace detail 44 | 45 | void context::log_to_stderr(const char* level) 46 | { 47 | static lsquic_logger_if logger{detail::log}; 48 | ::lsquic_logger_init(&logger, nullptr, LLTS_HHMMSSMS); 49 | ::lsquic_set_log_level(level); 50 | } 51 | 52 | 53 | context init_client(error_code& ec) 54 | { 55 | return detail::init(LSQUIC_GLOBAL_CLIENT, ec); 56 | } 57 | context init_client() 58 | { 59 | error_code ec; 60 | auto ctx = init_client(ec); 61 | if (ec) { 62 | throw system_error(ec); 63 | } 64 | return ctx; 65 | } 66 | 67 | context init_server(error_code& ec) 68 | { 69 | return detail::init(LSQUIC_GLOBAL_SERVER, ec); 70 | } 71 | context init_server() 72 | { 73 | error_code ec; 74 | auto ctx = init_server(ec); 75 | if (ec) { 76 | throw system_error(ec); 77 | } 78 | return ctx; 79 | } 80 | 81 | context init_client_server(error_code& ec) 82 | { 83 | return detail::init(LSQUIC_GLOBAL_CLIENT | LSQUIC_GLOBAL_SERVER, ec); 84 | } 85 | context init_client_server() 86 | { 87 | error_code ec; 88 | auto ctx = init_client_server(ec); 89 | if (ec) { 90 | throw system_error(ec); 91 | } 92 | return ctx; 93 | } 94 | 95 | } // namespace nexus::global 96 | -------------------------------------------------------------------------------- /src/recv_header_set.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace nexus::quic::detail { 8 | 9 | struct recv_header_set { 10 | h3::fields fields; 11 | int is_push_promise; 12 | lsxpack_header header; 13 | std::vector buffer; 14 | 15 | recv_header_set(int is_push_promise) : is_push_promise(is_push_promise) {} 16 | }; 17 | 18 | } // namespace nexus::quic::detail 19 | -------------------------------------------------------------------------------- /src/server.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace nexus { 8 | namespace quic { 9 | 10 | server::server(const executor_type& ex) 11 | : engine(ex, nullptr, nullptr, LSENG_SERVER) 12 | {} 13 | 14 | server::server(const executor_type& ex, const settings& s) 15 | : engine(ex, nullptr, &s, LSENG_SERVER) 16 | {} 17 | 18 | server::executor_type server::get_executor() const 19 | { 20 | return engine.get_executor(); 21 | } 22 | 23 | void server::close() 24 | { 25 | engine.close(); 26 | } 27 | 28 | acceptor::acceptor(server& s, udp::socket&& socket, ssl::context& ctx) 29 | : impl(s.engine, std::move(socket), ctx) 30 | {} 31 | 32 | acceptor::acceptor(server& s, const udp::endpoint& endpoint, 33 | ssl::context& ctx) 34 | : impl(s.engine, endpoint, true, ctx) 35 | {} 36 | 37 | acceptor::executor_type acceptor::get_executor() const 38 | { 39 | return impl.get_executor(); 40 | } 41 | 42 | udp::endpoint acceptor::local_endpoint() const 43 | { 44 | return impl.local_endpoint(); 45 | } 46 | 47 | void acceptor::listen(int backlog) 48 | { 49 | return impl.listen(backlog); 50 | } 51 | 52 | void acceptor::accept(connection& conn, error_code& ec) 53 | { 54 | detail::accept_sync op; 55 | impl.accept(conn.impl, op); 56 | op.wait(); 57 | ec = std::get<0>(*op.result); 58 | } 59 | 60 | void acceptor::accept(connection& conn) 61 | { 62 | error_code ec; 63 | accept(conn, ec); 64 | if (ec) { 65 | throw system_error(ec); 66 | } 67 | } 68 | 69 | void acceptor::close() 70 | { 71 | impl.close(); 72 | } 73 | 74 | } // namespace quic 75 | 76 | namespace h3 { 77 | 78 | server::server(const executor_type& ex) 79 | : engine(ex, nullptr, nullptr, LSENG_SERVER | LSENG_HTTP) 80 | {} 81 | 82 | server::server(const executor_type& ex, const quic::settings& s) 83 | : engine(ex, nullptr, &s, LSENG_SERVER | LSENG_HTTP) 84 | {} 85 | 86 | server::executor_type server::get_executor() const 87 | { 88 | return engine.get_executor(); 89 | } 90 | 91 | acceptor::acceptor(server& s, udp::socket&& socket, ssl::context& ctx) 92 | : impl(s.engine, std::move(socket), ctx) 93 | {} 94 | 95 | acceptor::acceptor(server& s, const udp::endpoint& endpoint, 96 | ssl::context& ctx) 97 | : impl(s.engine, endpoint, true, ctx) 98 | {} 99 | 100 | acceptor::executor_type acceptor::get_executor() const 101 | { 102 | return impl.get_executor(); 103 | } 104 | 105 | udp::endpoint acceptor::local_endpoint() const 106 | { 107 | return impl.local_endpoint(); 108 | } 109 | 110 | void acceptor::listen(int backlog) 111 | { 112 | return impl.listen(backlog); 113 | } 114 | 115 | void acceptor::accept(server_connection& conn, error_code& ec) 116 | { 117 | quic::detail::accept_sync op; 118 | impl.accept(conn.impl, op); 119 | op.wait(); 120 | ec = std::get<0>(*op.result); 121 | } 122 | 123 | void acceptor::accept(server_connection& conn) 124 | { 125 | error_code ec; 126 | accept(conn, ec); 127 | if (ec) { 128 | throw system_error(ec); 129 | } 130 | } 131 | 132 | void acceptor::close() 133 | { 134 | impl.close(); 135 | } 136 | 137 | bool server_connection::is_open() const 138 | { 139 | return impl.is_open(); 140 | } 141 | 142 | quic::connection_id server_connection::id(error_code& ec) const 143 | { 144 | return impl.id(ec); 145 | } 146 | 147 | quic::connection_id server_connection::id() const 148 | { 149 | error_code ec; 150 | auto i = impl.id(ec); 151 | if (ec) { 152 | throw system_error(ec); 153 | } 154 | return i; 155 | } 156 | 157 | udp::endpoint server_connection::remote_endpoint(error_code& ec) const 158 | { 159 | return impl.remote_endpoint(ec); 160 | } 161 | 162 | udp::endpoint server_connection::remote_endpoint() const 163 | { 164 | error_code ec; 165 | auto e = impl.remote_endpoint(ec); 166 | if (ec) { 167 | throw system_error(ec); 168 | } 169 | return e; 170 | } 171 | 172 | void server_connection::accept(stream& s, error_code& ec) 173 | { 174 | auto op = quic::detail::stream_accept_sync{s.impl}; 175 | impl.accept(op); 176 | op.wait(); 177 | ec = std::get<0>(*op.result); 178 | } 179 | 180 | void server_connection::accept(stream& s) 181 | { 182 | error_code ec; 183 | accept(s, ec); 184 | if (ec) { 185 | throw system_error(ec); 186 | } 187 | } 188 | 189 | void server_connection::go_away(error_code& ec) 190 | { 191 | impl.go_away(ec); 192 | } 193 | 194 | void server_connection::go_away() 195 | { 196 | error_code ec; 197 | impl.go_away(ec); 198 | if (ec) { 199 | throw system_error(ec); 200 | } 201 | } 202 | 203 | void server_connection::close(error_code& ec) 204 | { 205 | impl.close(ec); 206 | } 207 | 208 | void server_connection::close() 209 | { 210 | error_code ec; 211 | close(ec); 212 | if (ec) { 213 | throw system_error(ec); 214 | } 215 | } 216 | 217 | } // namespace h3 218 | } // namespace nexus 219 | -------------------------------------------------------------------------------- /src/settings.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace nexus::quic { 5 | 6 | namespace detail { 7 | 8 | void read_settings(settings& out, const lsquic_engine_settings& in) 9 | { 10 | const auto us = std::chrono::microseconds(in.es_handshake_to); 11 | out.handshake_timeout = std::chrono::duration_cast(us); 12 | out.idle_timeout = std::chrono::seconds(in.es_idle_timeout); 13 | out.max_streams_per_connection = 14 | in.es_init_max_streams_bidi; 15 | out.connection_flow_control_window = 16 | in.es_init_max_data; 17 | out.incoming_stream_flow_control_window = 18 | in.es_init_max_stream_data_bidi_remote; 19 | out.outgoing_stream_flow_control_window = 20 | in.es_init_max_stream_data_bidi_local; 21 | } 22 | 23 | void write_settings(const settings& in, lsquic_engine_settings& out) 24 | { 25 | out.es_handshake_to = std::chrono::duration_cast( 26 | in.handshake_timeout).count(); 27 | out.es_idle_timeout = in.idle_timeout.count(); 28 | out.es_init_max_streams_bidi = 29 | in.max_streams_per_connection; 30 | out.es_init_max_data = 31 | in.connection_flow_control_window; 32 | out.es_init_max_stream_data_bidi_remote = 33 | in.incoming_stream_flow_control_window; 34 | out.es_init_max_stream_data_bidi_local = 35 | in.outgoing_stream_flow_control_window; 36 | } 37 | 38 | bool check_settings(const lsquic_engine_settings& es, int flags, 39 | std::string* message) 40 | { 41 | char errbuf[256]; 42 | int r = ::lsquic_engine_check_settings(&es, flags, errbuf, sizeof(errbuf)); 43 | if (r == 0) { 44 | return true; 45 | } 46 | if (message) { 47 | message->assign(errbuf); 48 | } 49 | return false; 50 | } 51 | 52 | } // namespace detail 53 | 54 | settings default_client_settings() 55 | { 56 | lsquic_engine_settings es; 57 | ::lsquic_engine_init_settings(&es, 0); 58 | settings s; 59 | detail::read_settings(s, es); 60 | return s; 61 | } 62 | 63 | settings default_server_settings() 64 | { 65 | lsquic_engine_settings es; 66 | ::lsquic_engine_init_settings(&es, LSENG_SERVER); 67 | settings s; 68 | detail::read_settings(s, es); 69 | return s; 70 | } 71 | 72 | bool check_client_settings(const settings& s, std::string* message) 73 | { 74 | constexpr int flags = 0; 75 | lsquic_engine_settings es; 76 | ::lsquic_engine_init_settings(&es, flags); 77 | detail::write_settings(s, es); 78 | return detail::check_settings(es, flags, message); 79 | } 80 | 81 | bool check_server_settings(const settings& s, std::string* message) 82 | { 83 | constexpr int flags = LSENG_SERVER; 84 | lsquic_engine_settings es; 85 | ::lsquic_engine_init_settings(&es, flags); 86 | detail::write_settings(s, es); 87 | return detail::check_settings(es, flags, message); 88 | } 89 | 90 | } // namespace nexus::quic 91 | -------------------------------------------------------------------------------- /src/stream.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | namespace nexus { 11 | namespace quic { 12 | namespace detail { 13 | 14 | stream_impl::stream_impl(connection_impl& conn) 15 | : engine(conn.socket.engine), 16 | svc(boost::asio::use_service>( 17 | boost::asio::query(engine.get_executor(), 18 | boost::asio::execution::context))), 19 | conn(conn), 20 | state(stream_state::closed{}) 21 | { 22 | // register for service_shutdown() notifications 23 | svc.add(*this); 24 | } 25 | 26 | stream_impl::~stream_impl() 27 | { 28 | svc.remove(*this); 29 | } 30 | 31 | void stream_impl::service_shutdown() 32 | { 33 | // destroy any pending operations 34 | stream_state::destroy(state); 35 | } 36 | 37 | stream_impl::executor_type stream_impl::get_executor() const 38 | { 39 | return engine.get_executor(); 40 | } 41 | 42 | bool stream_impl::is_open() const 43 | { 44 | auto lock = std::unique_lock{engine.mutex}; 45 | return stream_state::is_open(state); 46 | } 47 | 48 | stream_id stream_impl::id(error_code& ec) const 49 | { 50 | auto lock = std::unique_lock{engine.mutex}; 51 | return stream_state::id(state, ec); 52 | } 53 | 54 | void stream_impl::read_headers(stream_header_read_operation& op) 55 | { 56 | auto lock = std::unique_lock{engine.mutex}; 57 | if (stream_state::read_headers(state, op)) { 58 | engine.process(lock); 59 | } 60 | } 61 | 62 | void stream_impl::read_some(stream_data_operation& op) 63 | { 64 | auto lock = std::unique_lock{engine.mutex}; 65 | if (stream_state::read(state, op)) { 66 | engine.process(lock); 67 | } 68 | } 69 | 70 | void stream_impl::on_read() 71 | { 72 | stream_state::on_read(state); 73 | } 74 | 75 | void stream_impl::write_some(stream_data_operation& op) 76 | { 77 | auto lock = std::unique_lock{engine.mutex}; 78 | if (stream_state::write(state, op)) { 79 | engine.process(lock); 80 | } 81 | } 82 | 83 | void stream_impl::write_headers(stream_header_write_operation& op) 84 | { 85 | auto lock = std::unique_lock{engine.mutex}; 86 | if (stream_state::write_headers(state, op)) { 87 | engine.process(lock); 88 | } 89 | } 90 | 91 | void stream_impl::on_write() 92 | { 93 | stream_state::on_write(state); 94 | } 95 | 96 | void stream_impl::flush(error_code& ec) 97 | { 98 | auto lock = std::unique_lock{engine.mutex}; 99 | stream_state::flush(state, ec); 100 | if (!ec) { 101 | engine.process(lock); 102 | } 103 | } 104 | 105 | void stream_impl::shutdown(int how, error_code& ec) 106 | { 107 | auto lock = std::unique_lock{engine.mutex}; 108 | stream_state::shutdown(state, how, ec); 109 | if (!ec) { 110 | engine.process(lock); 111 | } 112 | } 113 | 114 | void stream_impl::close(stream_close_operation& op) 115 | { 116 | auto lock = std::unique_lock{engine.mutex}; 117 | const auto t = stream_state::close(state, op); 118 | if (t == stream_state::transition::open_to_closing) { 119 | conn.on_open_stream_closing(*this); 120 | engine.process(lock); 121 | } 122 | } 123 | 124 | void stream_impl::on_close() 125 | { 126 | const auto t = stream_state::on_close(state); 127 | switch (t) { 128 | case stream_state::transition::closing_to_closed: 129 | conn.on_closing_stream_closed(*this); 130 | break; 131 | case stream_state::transition::open_to_closed: 132 | case stream_state::transition::open_to_error: 133 | conn.on_open_stream_closed(*this); 134 | break; 135 | default: 136 | break; 137 | } 138 | } 139 | 140 | void stream_impl::reset() 141 | { 142 | auto lock = std::unique_lock{engine.mutex}; 143 | const auto t = stream_state::reset(state); 144 | switch (t) { 145 | case stream_state::transition::accepting_to_closed: 146 | conn.on_accepting_stream_closed(*this); 147 | break; 148 | case stream_state::transition::connecting_to_closed: 149 | conn.on_connecting_stream_closed(*this); 150 | break; 151 | case stream_state::transition::closing_to_closed: 152 | conn.on_closing_stream_closed(*this); 153 | break; 154 | case stream_state::transition::open_to_closed: 155 | conn.on_open_stream_closed(*this); 156 | break; 157 | default: 158 | return; // nothing changed, return without calling process() 159 | } 160 | engine.process(lock); 161 | } 162 | 163 | } // namespace detail 164 | 165 | stream::stream(connection& conn) : stream(conn.impl) {} 166 | stream::stream(detail::connection_impl& conn) : impl(conn) {} 167 | 168 | stream::~stream() 169 | { 170 | impl.reset(); 171 | } 172 | 173 | stream::executor_type stream::get_executor() const 174 | { 175 | return impl.get_executor(); 176 | } 177 | 178 | bool stream::is_open() const 179 | { 180 | return impl.is_open(); 181 | } 182 | 183 | stream_id stream::id(error_code& ec) const 184 | { 185 | return impl.id(ec); 186 | } 187 | 188 | stream_id stream::id() const 189 | { 190 | error_code ec; 191 | auto sid = id(ec); 192 | if (ec) { 193 | throw system_error(ec); 194 | } 195 | return sid; 196 | } 197 | 198 | void stream::flush(error_code& ec) 199 | { 200 | impl.flush(ec); 201 | } 202 | 203 | void stream::flush() 204 | { 205 | error_code ec; 206 | flush(ec); 207 | if (ec) { 208 | throw system_error(ec); 209 | } 210 | } 211 | 212 | void stream::shutdown(int how, error_code& ec) 213 | { 214 | impl.shutdown(how, ec); 215 | } 216 | 217 | void stream::shutdown(int how) 218 | { 219 | error_code ec; 220 | shutdown(how, ec); 221 | if (ec) { 222 | throw system_error(ec); 223 | } 224 | } 225 | 226 | void stream::close(error_code& ec) 227 | { 228 | detail::stream_close_sync op; 229 | impl.close(op); 230 | op.wait(); 231 | ec = std::get<0>(*op.result); 232 | } 233 | 234 | void stream::close() 235 | { 236 | error_code ec; 237 | close(ec); 238 | if (ec) { 239 | throw system_error(ec); 240 | } 241 | } 242 | 243 | void stream::reset() 244 | { 245 | impl.reset(); 246 | } 247 | 248 | } // namespace quic 249 | 250 | namespace h3 { 251 | 252 | stream::stream(client_connection& conn) : quic::stream(conn.impl) {} 253 | stream::stream(server_connection& conn) : quic::stream(conn.impl) {} 254 | 255 | void stream::read_headers(fields& f, error_code& ec) 256 | { 257 | auto op = quic::detail::stream_header_read_sync{f}; 258 | impl.read_headers(op); 259 | op.wait(); 260 | ec = std::get<0>(*op.result); 261 | } 262 | void stream::read_headers(fields& f) 263 | { 264 | error_code ec; 265 | read_headers(f, ec); 266 | if (ec) { 267 | throw system_error(ec); 268 | } 269 | } 270 | 271 | void stream::write_headers(const fields& f, error_code& ec) 272 | { 273 | auto op = quic::detail::stream_header_write_sync{f}; 274 | impl.write_headers(op); 275 | op.wait(); 276 | ec = std::get<0>(*op.result); 277 | } 278 | void stream::write_headers(const fields& f) 279 | { 280 | error_code ec; 281 | write_headers(f, ec); 282 | if (ec) { 283 | throw system_error(ec); 284 | } 285 | } 286 | 287 | } // namespace h3 288 | } // namespace nexus 289 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(GTest REQUIRED) 2 | 3 | add_library(test_base certificate.cc) 4 | target_include_directories(test_base PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 5 | target_link_libraries(test_base PUBLIC nexus GTest::gtest GTest::gtest_main) 6 | 7 | function(add_unit_test target) 8 | add_executable(${target} ${ARGN}) 9 | target_link_libraries(${target} address-sanitizer) 10 | add_test(NAME ${target} COMMAND $) 11 | endfunction() 12 | 13 | add_subdirectory(headers) 14 | 15 | add_subdirectory(h3) 16 | add_subdirectory(quic) 17 | -------------------------------------------------------------------------------- /test/certificate.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "certificate.hpp" 6 | 7 | void intrusive_ptr_add_ref(EVP_PKEY* pkey) 8 | { 9 | ::EVP_PKEY_up_ref(pkey); 10 | } 11 | void intrusive_ptr_release(EVP_PKEY* pkey) 12 | { 13 | ::EVP_PKEY_free(pkey); 14 | } 15 | 16 | namespace nexus::test { 17 | 18 | using evp_pkey_ptr = boost::intrusive_ptr; 19 | 20 | struct x509_deleter { void operator()(x509_st* p) { ::X509_free(p); } }; 21 | using x509_ptr = std::unique_ptr; 22 | 23 | struct evp_pkey_ctx_deleter { 24 | void operator()(EVP_PKEY_CTX* p) { ::EVP_PKEY_CTX_free(p); }; 25 | }; 26 | using evp_pkey_ctx_ptr = std::unique_ptr; 27 | 28 | 29 | evp_pkey_ptr generate_rsa_key(int bits, error_code& ec) 30 | { 31 | auto ctx = evp_pkey_ctx_ptr{::EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)}; 32 | if (!ctx) { 33 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 34 | return nullptr; 35 | } 36 | if (::EVP_PKEY_keygen_init(ctx.get()) != 1) { 37 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 38 | return nullptr; 39 | } 40 | if (::EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), bits) != 1) { 41 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 42 | return nullptr; 43 | } 44 | EVP_PKEY *pkey = nullptr; 45 | if (::EVP_PKEY_keygen(ctx.get(), &pkey) != 1) { 46 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 47 | return nullptr; 48 | } 49 | constexpr bool add_ref = false; // EVP_PKEY_generate() starts with 1 ref 50 | return {pkey, add_ref}; 51 | } 52 | 53 | int add_name_entry(X509_NAME* name, const char* field, std::string_view value) 54 | { 55 | auto bytes = reinterpret_cast(value.data()); 56 | return ::X509_NAME_add_entry_by_txt(name, field, MBSTRING_ASC, 57 | bytes, value.size(), -1, 0); 58 | } 59 | 60 | x509_ptr self_sign_certificate(evp_pkey_ptr key, 61 | std::string_view country, 62 | std::string_view organization, 63 | std::string_view common_name, 64 | std::chrono::seconds duration, 65 | error_code& ec) 66 | { 67 | auto cert = x509_ptr{::X509_new()}; 68 | if (!cert) { 69 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 70 | return nullptr; 71 | } 72 | if (::X509_set_version(cert.get(), 2) != 1) { 73 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 74 | return nullptr; 75 | } 76 | if (::ASN1_INTEGER_set(::X509_get_serialNumber(cert.get()), 1) != 1) { 77 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 78 | return nullptr; 79 | } 80 | if (::X509_set_pubkey(cert.get(), key.get()) != 1) { 81 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 82 | return nullptr; 83 | } 84 | if (!::X509_gmtime_adj(::X509_get_notBefore(cert.get()), 0)) { 85 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 86 | return nullptr; 87 | } 88 | if (!::X509_gmtime_adj(::X509_get_notAfter(cert.get()), duration.count())) { 89 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 90 | return nullptr; 91 | } 92 | auto name = ::X509_get_subject_name(cert.get()); 93 | if (add_name_entry(name, "C", country) != 1) { 94 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 95 | return nullptr; 96 | } 97 | if (add_name_entry(name, "O", organization) != 1) { 98 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 99 | return nullptr; 100 | } 101 | if (add_name_entry(name, "CN", common_name) != 1) { 102 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 103 | return nullptr; 104 | } 105 | if (::X509_set_issuer_name(cert.get(), name) != 1) { 106 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 107 | return nullptr; 108 | } 109 | if (::X509_sign(cert.get(), key.get(), ::EVP_sha256()) == 0) { 110 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 111 | return nullptr; 112 | } 113 | return cert; 114 | } 115 | 116 | void self_sign_certificate(ssl::context& ctx, 117 | std::string_view country, 118 | std::string_view organization, 119 | std::string_view common_name, 120 | std::chrono::seconds duration, 121 | error_code& ec) 122 | { 123 | auto key = test::generate_rsa_key(2048, ec); 124 | if (ec) { 125 | return; 126 | } 127 | auto cert = test::self_sign_certificate(key, country, organization, 128 | common_name, duration, ec); 129 | if (ec) { 130 | return; 131 | } 132 | if (::SSL_CTX_use_certificate(ctx.native_handle(), cert.get()) != 1) { 133 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 134 | return; 135 | } 136 | if (::SSL_CTX_use_PrivateKey(ctx.native_handle(), key.get()) != 1) { 137 | ec.assign(ERR_get_error(), boost::asio::error::get_ssl_category()); 138 | return; 139 | } 140 | } 141 | 142 | void self_sign_certificate(ssl::context& ctx, 143 | std::string_view country, 144 | std::string_view organization, 145 | std::string_view common_name, 146 | std::chrono::seconds duration) 147 | { 148 | error_code ec; 149 | self_sign_certificate(ctx, country, organization, common_name, duration, ec); 150 | if (ec) { 151 | throw system_error(ec); 152 | } 153 | } 154 | 155 | int alpn_select_cb(SSL* ssl, const unsigned char** out, unsigned char* outlen, 156 | const unsigned char* in, unsigned int inlen, void* arg) 157 | { 158 | auto alpn = static_cast(arg); 159 | int r = ::SSL_select_next_proto(const_cast(out), outlen, 160 | const_cast(in), inlen, 161 | reinterpret_cast(alpn), 162 | strlen(alpn)); 163 | if (r == OPENSSL_NPN_NEGOTIATED) { 164 | return SSL_TLSEXT_ERR_OK; 165 | } else { 166 | return SSL_TLSEXT_ERR_ALERT_FATAL; 167 | } 168 | } 169 | 170 | ssl::context init_client_context(const char* alpn) 171 | { 172 | auto ctx = ssl::context{ssl::context::tlsv13}; 173 | ::SSL_CTX_set_min_proto_version(ctx.native_handle(), TLS1_3_VERSION); 174 | ::SSL_CTX_set_max_proto_version(ctx.native_handle(), TLS1_3_VERSION); 175 | ::SSL_CTX_set_alpn_protos(ctx.native_handle(), 176 | reinterpret_cast(alpn), 177 | strlen(alpn)); 178 | return ctx; 179 | } 180 | 181 | ssl::context init_server_context(const char* alpn) 182 | { 183 | auto ctx = ssl::context{ssl::context::tlsv13}; 184 | ::SSL_CTX_set_min_proto_version(ctx.native_handle(), TLS1_3_VERSION); 185 | ::SSL_CTX_set_max_proto_version(ctx.native_handle(), TLS1_3_VERSION); 186 | ::SSL_CTX_set_alpn_select_cb(ctx.native_handle(), alpn_select_cb, 187 | const_cast(alpn)); 188 | self_sign_certificate(ctx, "US", "Nexus", "host", std::chrono::hours(24)); 189 | return ctx; 190 | } 191 | 192 | } // namespace nexus::test 193 | -------------------------------------------------------------------------------- /test/certificate.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace nexus::test { 9 | 10 | void self_sign_certificate(ssl::context& ctx, 11 | std::string_view country, 12 | std::string_view organization, 13 | std::string_view common_name, 14 | std::chrono::seconds duration, 15 | error_code& ec); 16 | 17 | void self_sign_certificate(ssl::context& ctx, 18 | std::string_view country, 19 | std::string_view organization, 20 | std::string_view common_name, 21 | std::chrono::seconds duration); 22 | 23 | ssl::context init_client_context(const char* alpn); 24 | ssl::context init_server_context(const char* alpn); 25 | 26 | } // namespace nexus::test 27 | -------------------------------------------------------------------------------- /test/h3/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_unit_test(test_h3_fields test_fields.cc) 2 | target_link_libraries(test_h3_fields test_base nexus) 3 | 4 | add_unit_test(test_h3_connection_go_away test_connection_go_away.cc) 5 | target_link_libraries(test_h3_connection_go_away test_base nexus) 6 | 7 | add_unit_test(test_h3_stream_shutdown test_stream_shutdown.cc) 8 | target_link_libraries(test_h3_stream_shutdown test_base nexus) 9 | -------------------------------------------------------------------------------- /test/h3/test_connection_go_away.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "certificate.hpp" 8 | 9 | namespace nexus { 10 | 11 | namespace { 12 | 13 | static constexpr const char* alpn = "\02h3"; 14 | 15 | const error_code ok; 16 | 17 | auto capture(std::optional& ec) { 18 | return [&] (error_code e, size_t = 0) { ec = e; }; 19 | } 20 | 21 | } // anonymous namespace 22 | 23 | TEST(Client, go_away_closed) 24 | { 25 | boost::asio::io_context context; 26 | auto global = global::init_client(); 27 | auto sslc = test::init_client_context(alpn); 28 | auto client = h3::client{context.get_executor(), udp::endpoint{}, sslc}; 29 | auto cconn = h3::client_connection{client}; 30 | 31 | // go_away() before the connection is open 32 | error_code goaway_ec; 33 | cconn.go_away(goaway_ec); 34 | EXPECT_EQ(errc::not_connected, goaway_ec); 35 | } 36 | 37 | TEST(Client, go_away_before_connect) 38 | { 39 | boost::asio::io_context context; 40 | auto global = global::init_client(); 41 | auto sslc = test::init_client_context(alpn); 42 | auto client = h3::client{context.get_executor(), udp::endpoint{}, sslc}; 43 | boost::asio::ip::address localhost = boost::asio::ip::make_address("127.0.0.1"); 44 | auto cconn = h3::client_connection{client, udp::endpoint{udp::v4(), 1}, "host"}; 45 | 46 | context.poll(); 47 | EXPECT_TRUE(cconn.is_open()); 48 | 49 | error_code goaway_ec; 50 | cconn.go_away(goaway_ec); 51 | EXPECT_EQ(ok, goaway_ec); 52 | 53 | auto cstream = h3::stream{cconn}; 54 | std::optional connect_ec; 55 | cconn.async_connect(cstream, capture(connect_ec)); 56 | 57 | context.poll(); 58 | ASSERT_TRUE(connect_ec); 59 | EXPECT_EQ(quic::connection_error::going_away, *connect_ec); 60 | } 61 | 62 | class Connection : public ::testing::Test { 63 | protected: 64 | static constexpr const char* alpn = "\02h3"; 65 | 66 | static quic::settings server_settings() 67 | { 68 | auto settings = quic::default_server_settings(); 69 | settings.max_streams_per_connection = 2; 70 | return settings; 71 | } 72 | 73 | boost::asio::io_context context; 74 | global::context global = global::init_client_server(); 75 | 76 | ssl::context ssl = test::init_server_context(alpn); 77 | ssl::context sslc = test::init_client_context(alpn); 78 | 79 | h3::server server{context.get_executor(), server_settings()}; 80 | boost::asio::ip::address localhost = boost::asio::ip::make_address("127.0.0.1"); 81 | h3::acceptor acceptor{server, udp::endpoint{localhost, 0}, ssl}; 82 | h3::server_connection sconn{acceptor}; 83 | h3::stream sstream{sconn}; 84 | 85 | h3::client client{context.get_executor(), udp::endpoint{}, sslc}; 86 | h3::client_connection cconn{client, acceptor.local_endpoint(), "host"}; 87 | h3::stream cstream{cconn}; 88 | 89 | void SetUp() override 90 | { 91 | global.log_to_stderr("debug"); 92 | acceptor.listen(16); 93 | 94 | std::optional accept_ec; 95 | acceptor.async_accept(sconn, capture(accept_ec)); 96 | 97 | std::optional connect_ec; 98 | cconn.async_connect(cstream, capture(connect_ec)); 99 | 100 | context.poll(); 101 | ASSERT_TRUE(accept_ec); 102 | EXPECT_EQ(ok, *accept_ec); 103 | ASSERT_TRUE(connect_ec); 104 | EXPECT_EQ(ok, *connect_ec); 105 | 106 | std::optional sstream_accept_ec; 107 | sconn.async_accept(sstream, capture(sstream_accept_ec)); 108 | 109 | // the server won't see this stream until we write a packet for it 110 | auto request = h3::fields{}; 111 | request.insert("salad", "potato"); 112 | std::optional write_headers_ec; 113 | cstream.async_write_headers(request, capture(write_headers_ec)); 114 | 115 | context.poll(); 116 | ASSERT_TRUE(write_headers_ec); 117 | EXPECT_EQ(ok, *write_headers_ec); 118 | cstream.flush(); // force the connection to send the HEADERS 119 | 120 | context.poll(); 121 | ASSERT_TRUE(sstream_accept_ec); 122 | EXPECT_EQ(ok, *sstream_accept_ec); 123 | } 124 | }; 125 | 126 | TEST_F(Connection, go_away_during_connect) 127 | { 128 | auto cstream2 = h3::stream{cconn}; 129 | std::optional connect2_ec; 130 | cconn.async_connect(cstream2, capture(connect2_ec)); 131 | 132 | context.poll(); 133 | ASSERT_TRUE(cconn.is_open()); 134 | ASSERT_TRUE(connect2_ec); 135 | EXPECT_EQ(ok, *connect2_ec); 136 | 137 | auto cstream3 = h3::stream{cconn}; 138 | std::optional connect3_ec; 139 | cconn.async_connect(cstream3, capture(connect3_ec)); 140 | 141 | context.poll(); 142 | ASSERT_TRUE(cconn.is_open()); 143 | ASSERT_FALSE(connect3_ec); 144 | 145 | error_code goaway_ec; 146 | cconn.go_away(goaway_ec); 147 | EXPECT_EQ(ok, goaway_ec); 148 | 149 | context.poll(); 150 | ASSERT_TRUE(connect3_ec); 151 | EXPECT_EQ(quic::connection_error::going_away, *connect3_ec); 152 | } 153 | 154 | TEST_F(Connection, remote_go_away_before_connect) 155 | { 156 | error_code goaway_ec; 157 | sconn.go_away(goaway_ec); 158 | EXPECT_EQ(ok, goaway_ec); 159 | 160 | context.poll(); 161 | 162 | auto cstream2 = h3::stream{cconn}; 163 | std::optional connect2_ec; 164 | cconn.async_connect(cstream2, capture(connect2_ec)); 165 | 166 | context.poll(); 167 | ASSERT_TRUE(connect2_ec); 168 | EXPECT_EQ(quic::connection_error::going_away, *connect2_ec); 169 | } 170 | 171 | TEST_F(Connection, remote_go_away_during_connect) 172 | { 173 | auto cstream2 = h3::stream{cconn}; 174 | std::optional connect2_ec; 175 | cconn.async_connect(cstream2, capture(connect2_ec)); 176 | 177 | context.poll(); 178 | ASSERT_TRUE(cconn.is_open()); 179 | ASSERT_TRUE(connect2_ec); 180 | EXPECT_EQ(ok, *connect2_ec); 181 | 182 | auto cstream3 = h3::stream{cconn}; 183 | std::optional connect3_ec; 184 | cconn.async_connect(cstream3, capture(connect3_ec)); 185 | 186 | context.poll(); 187 | ASSERT_TRUE(cconn.is_open()); 188 | ASSERT_FALSE(connect3_ec); 189 | 190 | error_code goaway_ec; 191 | sconn.go_away(goaway_ec); 192 | EXPECT_EQ(ok, goaway_ec); 193 | 194 | context.poll(); 195 | ASSERT_TRUE(connect3_ec); 196 | EXPECT_EQ(quic::connection_error::peer_going_away, *connect3_ec); 197 | } 198 | 199 | TEST_F(Connection, go_away_before_accept) 200 | { 201 | error_code goaway_ec; 202 | sconn.go_away(goaway_ec); 203 | EXPECT_EQ(ok, goaway_ec); 204 | 205 | auto sstream2 = h3::stream{sconn}; 206 | std::optional accept_ec; 207 | sconn.async_accept(sstream2, capture(accept_ec)); 208 | 209 | context.poll(); 210 | ASSERT_TRUE(accept_ec); 211 | EXPECT_EQ(quic::connection_error::going_away, *accept_ec); 212 | } 213 | 214 | TEST_F(Connection, go_away_during_accept) 215 | { 216 | auto sstream2 = h3::stream{sconn}; 217 | std::optional accept_ec; 218 | sconn.async_accept(sstream2, capture(accept_ec)); 219 | 220 | context.poll(); 221 | ASSERT_FALSE(accept_ec); 222 | 223 | error_code goaway_ec; 224 | sconn.go_away(goaway_ec); 225 | EXPECT_EQ(ok, goaway_ec); 226 | 227 | context.poll(); 228 | ASSERT_TRUE(accept_ec); 229 | EXPECT_EQ(quic::connection_error::going_away, *accept_ec); 230 | } 231 | 232 | #if 0 // XXX: can't get client connection to send GOAWAY without seeing a PUSH_PROMISE from the server? 233 | TEST_F(Connection, remote_go_away_before_accept) 234 | { 235 | error_code goaway_ec; 236 | cconn.go_away(goaway_ec); 237 | EXPECT_EQ(ok, goaway_ec); 238 | 239 | context.poll(); 240 | 241 | auto sstream2 = h3::stream{sconn}; 242 | std::optional accept_ec; 243 | sconn.async_accept(sstream2, capture(accept_ec)); 244 | 245 | context.poll(); 246 | ASSERT_TRUE(accept_ec); 247 | EXPECT_EQ(quic::connection_error::going_away, *accept_ec); 248 | } 249 | 250 | TEST_F(Connection, remote_go_away_during_accept) 251 | { 252 | auto sstream2 = h3::stream{sconn}; 253 | std::optional accept_ec; 254 | sconn.async_accept(sstream2, capture(accept_ec)); 255 | 256 | context.poll(); 257 | ASSERT_FALSE(accept_ec); 258 | 259 | error_code goaway_ec; 260 | cconn.go_away(goaway_ec); 261 | EXPECT_EQ(ok, goaway_ec); 262 | 263 | context.poll(); 264 | ASSERT_TRUE(accept_ec); 265 | EXPECT_EQ(quic::connection_error::going_away, *accept_ec); 266 | } 267 | #endif 268 | TEST_F(Connection, go_away_before_write) 269 | { 270 | error_code goaway_ec; 271 | cconn.go_away(goaway_ec); 272 | EXPECT_EQ(ok, goaway_ec); 273 | 274 | auto data = std::array{}; 275 | std::optional write_ec; 276 | cstream.async_write_some(boost::asio::buffer(data), capture(write_ec)); 277 | 278 | context.poll(); 279 | ASSERT_TRUE(write_ec); 280 | EXPECT_EQ(ok, *write_ec); 281 | } 282 | 283 | } // namespace nexus 284 | -------------------------------------------------------------------------------- /test/h3/test_fields.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace nexus::h3 { 5 | 6 | TEST(fields, empty) 7 | { 8 | fields f; 9 | EXPECT_EQ(0, f.size()); 10 | const auto begin = f.begin(); 11 | const auto end = f.end(); 12 | EXPECT_EQ(end, begin); 13 | const auto [lower, upper] = f.equal_range("shape"); 14 | EXPECT_EQ(end, lower); 15 | EXPECT_EQ(end, upper); 16 | } 17 | 18 | TEST(fields, insert) 19 | { 20 | fields f; 21 | f.insert("shape", "square"); 22 | f.insert("color", "blue"); 23 | 24 | EXPECT_EQ(2, f.size()); 25 | const auto first = f.begin(); 26 | ASSERT_NE(first, f.end()); 27 | EXPECT_STREQ("shape: square", first->c_str()); 28 | const auto second = std::next(first); 29 | ASSERT_NE(second, f.end()); 30 | EXPECT_STREQ("color: blue", second->c_str()); 31 | const auto third = std::next(second); 32 | EXPECT_EQ(third, f.end()); 33 | } 34 | 35 | TEST(fields, insert_same_name) 36 | { 37 | fields f; 38 | f.insert("shape", "square"); 39 | f.insert("shape", "circle"); 40 | 41 | const auto first = f.begin(); 42 | ASSERT_NE(first, f.end()); 43 | EXPECT_STREQ("shape: square", first->c_str()); 44 | const auto second = std::next(first); 45 | ASSERT_NE(second, f.end()); 46 | EXPECT_STREQ("shape: circle", second->c_str()); 47 | const auto third = std::next(second); 48 | EXPECT_EQ(third, f.end()); 49 | } 50 | 51 | TEST(fields, insert_same_name_out_of_order) 52 | { 53 | fields f; 54 | f.insert("shape", "square"); 55 | f.insert("color", "blue"); 56 | f.insert("shape", "circle", true); 57 | 58 | EXPECT_EQ(3, f.size()); 59 | const auto first = f.begin(); 60 | ASSERT_NE(first, f.end()); 61 | EXPECT_STREQ("shape: square", first->c_str()); 62 | EXPECT_FALSE(first->never_index()); 63 | const auto second = std::next(first); 64 | ASSERT_NE(second, f.end()); 65 | EXPECT_STREQ("shape: circle", second->c_str()); 66 | EXPECT_TRUE(second->never_index()); 67 | const auto third = std::next(second); 68 | ASSERT_NE(third, f.end()); 69 | EXPECT_STREQ("color: blue", third->c_str()); 70 | EXPECT_FALSE(third->never_index()); 71 | const auto fourth = std::next(third); 72 | EXPECT_EQ(fourth, f.end()); 73 | 74 | const auto [lower, upper] = f.equal_range("shape"); 75 | EXPECT_EQ(first, lower); 76 | EXPECT_EQ(third, upper); 77 | } 78 | 79 | TEST(fields, assign) 80 | { 81 | fields f; 82 | f.insert("shape", "square", true); 83 | f.insert("color", "blue"); 84 | f.insert("shape", "circle"); 85 | f.assign("shape", "line"); 86 | 87 | EXPECT_EQ(2, f.size()); 88 | const auto first = f.begin(); 89 | ASSERT_NE(first, f.end()); 90 | EXPECT_STREQ("color: blue", first->c_str()); 91 | const auto second = std::next(first); 92 | ASSERT_NE(second, f.end()); 93 | EXPECT_STREQ("shape: line", second->c_str()); 94 | EXPECT_FALSE(second->never_index()); 95 | const auto third = std::next(second); 96 | EXPECT_EQ(third, f.end()); 97 | 98 | const auto [lower, upper] = f.equal_range("shape"); 99 | EXPECT_EQ(second, lower); 100 | EXPECT_EQ(third, upper); 101 | } 102 | 103 | } // namespace nexus::h3 104 | -------------------------------------------------------------------------------- /test/headers/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # add an executable for each header just to test that it's self-contained 2 | set(NEXUS_HEADER_DIR ${CMAKE_SOURCE_DIR}/include) 3 | file(GLOB_RECURSE HEADER_FILES CONFIGURE_DEPENDS ${NEXUS_HEADER_DIR}/*.hpp) 4 | #list(FILTER HEADER_FILES EXCLUDE REGEX detail) 5 | 6 | function(add_header_test target include_path) 7 | set(HEADER_TEST_FILENAME ${target}.cc) 8 | add_custom_command(OUTPUT ${HEADER_TEST_FILENAME} 9 | COMMAND ${CMAKE_COMMAND} 10 | -DHEADER_TEST_FILENAME=${HEADER_TEST_FILENAME} 11 | -DHEADER_TEST_INCLUDE_PATH=${include_path} 12 | -P ${CMAKE_MODULE_PATH}/GenerateHeaderTest.cmake) 13 | add_executable(${target} ${HEADER_TEST_FILENAME}) 14 | target_link_libraries(${target} PRIVATE nexus gtest_main) 15 | # do not run this as a unit test 16 | endfunction() 17 | 18 | foreach(HEADER ${HEADER_FILES}) 19 | string(REPLACE "${NEXUS_HEADER_DIR}/" "" RELATIVE_HEADER ${HEADER}) 20 | string(REGEX REPLACE "[/\.]" "_" TARGET_NAME "include_${RELATIVE_HEADER}") 21 | add_header_test(${TARGET_NAME} ${RELATIVE_HEADER}) 22 | endforeach() 23 | -------------------------------------------------------------------------------- /test/quic/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_unit_test(test_quic_client_work test_client_work.cc) 2 | target_link_libraries(test_quic_client_work test_base nexus) 3 | 4 | add_unit_test(test_quic_connection_id test_connection_id.cc) 5 | target_link_libraries(test_quic_connection_id test_base nexus) 6 | 7 | add_unit_test(test_quic_errors test_errors.cc) 8 | target_link_libraries(test_quic_errors test_base nexus) 9 | 10 | add_unit_test(test_quic_handshake test_handshake.cc) 11 | target_link_libraries(test_quic_handshake test_base nexus) 12 | 13 | add_unit_test(test_quic_lifetime test_lifetime.cc) 14 | target_link_libraries(test_quic_lifetime test_base nexus) 15 | 16 | add_unit_test(test_quic_server_accept test_server_accept.cc) 17 | target_link_libraries(test_quic_server_accept test_base nexus) 18 | 19 | add_unit_test(test_quic_server_initiated_stream test_server_initiated_stream.cc) 20 | target_link_libraries(test_quic_server_initiated_stream test_base nexus) 21 | -------------------------------------------------------------------------------- /test/quic/test_client_work.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "certificate.hpp" 9 | 10 | namespace nexus { 11 | 12 | // constuct a client and server on different io_contexts 13 | class Client : public testing::Test { 14 | protected: 15 | static constexpr const char* alpn = "\04quic"; 16 | global::context global = global::init_client_server(); 17 | ssl::context ssl = test::init_server_context(alpn); 18 | ssl::context sslc = test::init_client_context(alpn); 19 | boost::asio::io_context scontext; 20 | quic::server server = quic::server{scontext.get_executor()}; 21 | boost::asio::ip::address localhost = boost::asio::ip::make_address("127.0.0.1"); 22 | quic::acceptor acceptor{server, udp::endpoint{localhost, 0}, ssl}; 23 | boost::asio::io_context ccontext; 24 | quic::client client{ccontext.get_executor(), udp::endpoint{}, sslc}; 25 | }; 26 | 27 | TEST_F(Client, connection_work) 28 | { 29 | auto conn = quic::connection{client, acceptor.local_endpoint(), "host"}; 30 | 31 | ccontext.poll(); 32 | ASSERT_FALSE(ccontext.stopped()); // connection maintains work 33 | 34 | conn.close(); 35 | 36 | ccontext.poll(); 37 | ASSERT_TRUE(ccontext.stopped()); // close stops work 38 | } 39 | 40 | TEST_F(Client, two_connection_work) 41 | { 42 | auto conn1 = quic::connection{client, acceptor.local_endpoint(), "host"}; 43 | 44 | ccontext.poll(); 45 | ASSERT_FALSE(ccontext.stopped()); 46 | 47 | auto conn2 = quic::connection{client, acceptor.local_endpoint(), "host"}; 48 | 49 | ccontext.poll(); 50 | ASSERT_FALSE(ccontext.stopped()); 51 | 52 | conn1.close(); 53 | 54 | ccontext.poll(); 55 | ASSERT_FALSE(ccontext.stopped()); 56 | 57 | conn2.close(); 58 | 59 | ccontext.poll(); 60 | ASSERT_TRUE(ccontext.stopped()); 61 | } 62 | 63 | } // namespace nexus 64 | -------------------------------------------------------------------------------- /test/quic/test_connection_id.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace nexus::quic { 5 | 6 | using value_type = connection_id::value_type; 7 | 8 | template 9 | using array_type = std::array; 10 | 11 | // return a resized copy of the given array 12 | template 13 | constexpr auto resize(const std::array& data) 14 | -> std::array 15 | { 16 | auto result = array_type{}; 17 | constexpr auto count = std::min(NewSize, Size); 18 | for (size_t i = 0; i < count; i++) { 19 | result[i] = data[i]; 20 | } 21 | return result; 22 | } 23 | 24 | constexpr array_type<6> short_id{ 25 | 'a','b','c','d','e','f' 26 | }; 27 | constexpr array_type<26> long_id{ 28 | 'a','b','c','d','e','f','g','h','i','j','k','l','m', 29 | 'n','o','p','q','r','s','t','u','v','w','x','y','z' 30 | }; 31 | 32 | constexpr auto long_id19 = resize<19>(long_id); 33 | constexpr auto long_id20 = resize<20>(long_id); 34 | 35 | // array comparisons 36 | template 37 | inline bool operator==(const value_type (&l)[Size], const connection_id& r) { 38 | return Size == r.size() && std::equal(std::begin(l), std::end(l), r.begin()); 39 | } 40 | template 41 | inline bool operator==(const connection_id& l, const value_type (&r)[Size]) { 42 | return l.size() == Size && std::equal(l.begin(), l.end(), std::begin(r)); 43 | } 44 | template 45 | inline bool operator!=(const value_type (&l)[Size], const connection_id& r) { 46 | return Size != r.size() || !std::equal(std::begin(l), std::end(l), r.begin()); 47 | } 48 | template 49 | inline bool operator!=(const connection_id& l, const value_type (&r)[Size]) { 50 | return l.size() != Size || !std::equal(l.begin(), l.end(), std::begin(r)); 51 | } 52 | 53 | // std::array comparisons 54 | template 55 | inline bool operator==(const array_type& l, const connection_id& r) { 56 | return l.size() == r.size() && std::equal(l.begin(), l.end(), r.begin()); 57 | } 58 | template 59 | inline bool operator==(const connection_id& l, const array_type& r) { 60 | return l.size() == r.size() && std::equal(l.begin(), l.end(), r.begin()); 61 | } 62 | template 63 | inline bool operator!=(const array_type& l, const connection_id& r) { 64 | return l.size() != r.size() || !std::equal(l.begin(), l.end(), r.begin()); 65 | } 66 | template 67 | inline bool operator!=(const connection_id& l, const array_type& r) { 68 | return l.size() != r.size() || !std::equal(l.begin(), l.end(), r.begin()); 69 | } 70 | 71 | TEST(connection_id, construct) 72 | { 73 | constexpr connection_id id1; 74 | EXPECT_TRUE(id1.empty()); 75 | EXPECT_EQ(0, id1.size()); 76 | EXPECT_EQ(id1.begin(), id1.end()); 77 | EXPECT_EQ(id1.rbegin(), id1.rend()); 78 | 79 | constexpr unsigned char arr[] = {'a','b','c','d'}; 80 | constexpr auto id2 = connection_id{arr}; 81 | EXPECT_FALSE(id2.empty()); 82 | EXPECT_EQ(sizeof(arr), id2.size()); 83 | EXPECT_EQ(arr, id2); 84 | 85 | constexpr auto id3 = connection_id{resize<16>(long_id)}; 86 | EXPECT_FALSE(id3.empty()); 87 | EXPECT_EQ(16, id3.size()); 88 | EXPECT_EQ(resize<16>(long_id), id3); 89 | 90 | constexpr auto id4 = connection_id{long_id.data(), 12}; 91 | EXPECT_FALSE(id4.empty()); 92 | EXPECT_EQ(12, id4.size()); 93 | EXPECT_EQ(resize<12>(long_id), id4); 94 | 95 | constexpr auto id5 = connection_id{{'a','b','c','d'}}; 96 | EXPECT_FALSE(id5.empty()); 97 | EXPECT_EQ(4, id5.size()); 98 | EXPECT_EQ(arr, id5); 99 | } 100 | 101 | TEST(connection_id, resize) 102 | { 103 | connection_id id1; 104 | EXPECT_EQ(0, std::distance(id1.begin(), id1.end())); 105 | EXPECT_EQ(0, std::distance(id1.rbegin(), id1.rend())); 106 | id1.resize(12); 107 | EXPECT_EQ(12, id1.size()); 108 | EXPECT_EQ(12, std::distance(id1.begin(), id1.end())); 109 | EXPECT_EQ(12, std::distance(id1.rbegin(), id1.rend())); 110 | 111 | auto id2 = connection_id{long_id20}; 112 | EXPECT_EQ(long_id20, id2); 113 | EXPECT_NE(long_id19, id2); 114 | id2.resize(19); 115 | EXPECT_EQ(19, std::distance(id2.begin(), id2.end())); 116 | EXPECT_EQ(19, std::distance(id2.rbegin(), id2.rend())); 117 | EXPECT_EQ(long_id19, id2); 118 | EXPECT_NE(long_id20, id2); 119 | id2.resize(0); 120 | EXPECT_TRUE(id2.empty()); 121 | EXPECT_EQ(0, std::distance(id2.begin(), id2.end())); 122 | EXPECT_EQ(0, std::distance(id2.rbegin(), id2.rend())); 123 | } 124 | 125 | constexpr array_type<1> a{'a'}; 126 | constexpr array_type<2> aa{'a','a'}; 127 | constexpr array_type<1> b{'b'}; 128 | 129 | TEST(connection_id, eq) 130 | { 131 | EXPECT_EQ(connection_id(), connection_id()); 132 | EXPECT_EQ(connection_id(a), connection_id(a)); 133 | EXPECT_EQ(connection_id(long_id20), connection_id(long_id20)); 134 | } 135 | 136 | TEST(connection_id, ne) 137 | { 138 | EXPECT_NE(connection_id(), connection_id(a)); 139 | EXPECT_NE(connection_id(a), connection_id()); 140 | 141 | EXPECT_NE(connection_id(a), connection_id(aa)); 142 | EXPECT_NE(connection_id(aa), connection_id(a)); 143 | 144 | EXPECT_NE(connection_id(a), connection_id(b)); 145 | EXPECT_NE(connection_id(b), connection_id(a)); 146 | 147 | EXPECT_NE(connection_id(long_id19), connection_id(long_id20)); 148 | EXPECT_NE(connection_id(long_id20), connection_id(long_id19)); 149 | } 150 | 151 | TEST(connection_id, lt) 152 | { 153 | EXPECT_LT(connection_id(), connection_id(a)); 154 | EXPECT_LT(connection_id(a), connection_id(aa)); 155 | EXPECT_LT(connection_id(a), connection_id(b)); 156 | EXPECT_LT(connection_id(long_id19), connection_id(long_id20)); 157 | } 158 | 159 | TEST(connection_id, lte) 160 | { 161 | EXPECT_LE(connection_id(), connection_id()); 162 | EXPECT_LE(connection_id(), connection_id(a)); 163 | EXPECT_LE(connection_id(a), connection_id(a)); 164 | EXPECT_LE(connection_id(a), connection_id(aa)); 165 | EXPECT_LE(connection_id(a), connection_id(b)); 166 | EXPECT_LE(connection_id(long_id19), connection_id(long_id20)); 167 | EXPECT_LE(connection_id(long_id20), connection_id(long_id20)); 168 | } 169 | 170 | TEST(connection_id, gt) 171 | { 172 | EXPECT_GT(connection_id(a), connection_id()); 173 | EXPECT_GT(connection_id(aa), connection_id(a)); 174 | EXPECT_GT(connection_id(b), connection_id(a)); 175 | EXPECT_GT(connection_id(long_id20), connection_id(long_id19)); 176 | } 177 | 178 | TEST(connection_id, gte) 179 | { 180 | EXPECT_GE(connection_id(), connection_id()); 181 | EXPECT_GE(connection_id(a), connection_id()); 182 | EXPECT_GE(connection_id(a), connection_id(a)); 183 | EXPECT_GE(connection_id(aa), connection_id(a)); 184 | EXPECT_GE(connection_id(b), connection_id(a)); 185 | EXPECT_GE(connection_id(long_id20), connection_id(long_id19)); 186 | EXPECT_GE(connection_id(long_id20), connection_id(long_id20)); 187 | } 188 | 189 | TEST(connection_id, copy) 190 | { 191 | constexpr auto id1 = connection_id{long_id20}; 192 | connection_id id2 = id1; 193 | EXPECT_EQ(long_id20, id1); 194 | EXPECT_EQ(long_id20, id2); 195 | } 196 | 197 | TEST(connection_id, copy_assign) 198 | { 199 | constexpr auto id1 = connection_id{long_id20}; 200 | connection_id id2; 201 | id2 = id1; 202 | EXPECT_EQ(long_id20, id1); 203 | EXPECT_EQ(long_id20, id2); 204 | } 205 | 206 | TEST(connection_id, move) 207 | { 208 | auto id1 = connection_id{long_id20}; 209 | auto id2 = connection_id{std::move(id1)}; 210 | EXPECT_EQ(long_id20, id2); 211 | } 212 | 213 | TEST(connection_id, move_assign) 214 | { 215 | auto id1 = connection_id{long_id20}; 216 | connection_id id2; 217 | id2 = std::move(id1); 218 | EXPECT_EQ(long_id20, id2); 219 | } 220 | 221 | TEST(connection_id, length_error) 222 | { 223 | // test construction 224 | EXPECT_NO_THROW(connection_id(long_id.data(), 20)); 225 | EXPECT_THROW(connection_id(long_id.data(), 21), std::length_error); 226 | 227 | // test resize 228 | connection_id id; 229 | id.resize(20); 230 | EXPECT_THROW(id.resize(21), std::length_error); 231 | EXPECT_EQ(20, id.size()); 232 | } 233 | 234 | TEST(connection_id, out_of_range) 235 | { 236 | connection_id id; 237 | EXPECT_EQ(0, id.size()); 238 | EXPECT_NO_THROW(id[0]); 239 | EXPECT_THROW(id.at(0), std::out_of_range); 240 | 241 | id.resize(20); 242 | EXPECT_EQ(20, id.size()); 243 | EXPECT_NO_THROW(id.at(19)); 244 | EXPECT_NO_THROW(id[20]); 245 | EXPECT_THROW(id.at(20), std::out_of_range); 246 | 247 | id.resize(10); 248 | EXPECT_EQ(10, id.size()); 249 | EXPECT_NO_THROW(id.at(9)); 250 | EXPECT_NO_THROW(id[10]); 251 | EXPECT_THROW(id.at(10), std::out_of_range); 252 | } 253 | 254 | } // namespace nexus::quic 255 | -------------------------------------------------------------------------------- /test/quic/test_errors.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace nexus::quic { 5 | 6 | TEST(errors, connection) 7 | { 8 | // connection errors are distinguishable from stream errors 9 | EXPECT_NE(make_error_condition(stream_error::reset), 10 | make_error_code(connection_error::reset)); 11 | EXPECT_NE(make_error_condition(stream_error::aborted), 12 | make_error_code(connection_error::aborted)); 13 | // connection errors can be compared to generic error conditions 14 | EXPECT_EQ(make_error_condition(errc::connection_reset), 15 | make_error_code(connection_error::reset)); 16 | EXPECT_EQ(make_error_condition(errc::connection_aborted), 17 | make_error_code(connection_error::aborted)); 18 | EXPECT_EQ(make_error_condition(errc::timed_out), 19 | make_error_code(connection_error::timed_out)); 20 | } 21 | 22 | TEST(errors, stream) 23 | { 24 | // stream errors are distinguishable from connection errors 25 | EXPECT_NE(make_error_condition(connection_error::reset), 26 | make_error_code(stream_error::reset)); 27 | EXPECT_NE(make_error_condition(connection_error::aborted), 28 | make_error_code(stream_error::aborted)); 29 | // stream errors can be compared to generic error conditions 30 | EXPECT_EQ(make_error_condition(errc::connection_reset), 31 | make_error_code(stream_error::reset)); 32 | EXPECT_EQ(make_error_condition(errc::connection_aborted), 33 | make_error_code(stream_error::aborted)); 34 | EXPECT_EQ(make_error_condition(errc::device_or_resource_busy), 35 | make_error_code(stream_error::busy)); 36 | } 37 | 38 | } // namespace nexus::quic 39 | -------------------------------------------------------------------------------- /test/quic/test_lifetime.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "certificate.hpp" 10 | 11 | namespace nexus { 12 | 13 | namespace { 14 | 15 | const error_code ok; 16 | 17 | auto capture(std::optional& ec) { 18 | return [&] (error_code e, size_t = 0) { ec = e; }; 19 | } 20 | 21 | } // anonymous namespace 22 | 23 | // establish a connection between client and server 24 | class Lifetime : public testing::Test { 25 | protected: 26 | static constexpr const char* alpn = "\04quic"; 27 | boost::asio::io_context context; 28 | global::context global = global::init_client_server(); 29 | ssl::context ssl = test::init_server_context(alpn); 30 | ssl::context sslc = test::init_client_context(alpn); 31 | quic::server server = quic::server{context.get_executor()}; 32 | boost::asio::ip::address localhost = boost::asio::ip::make_address("127.0.0.1"); 33 | quic::acceptor acceptor{server, udp::endpoint{localhost, 0}, ssl}; 34 | quic::client client{context.get_executor(), udp::endpoint{}, sslc}; 35 | 36 | void SetUp() override { 37 | //global.log_to_stderr("debug"); 38 | acceptor.listen(16); 39 | } 40 | }; 41 | 42 | TEST_F(Lifetime, connection_in_accept_handler) 43 | { 44 | // allocate a server connection and move it into the accept handler 45 | auto sconn = std::make_unique(acceptor); 46 | auto &ref = *sconn; 47 | std::optional accept_ec; 48 | acceptor.async_accept(ref, [&accept_ec, c=std::move(sconn)] (error_code ec) { 49 | accept_ec = ec; 50 | }); 51 | ASSERT_FALSE(accept_ec); 52 | } 53 | 54 | TEST_F(Lifetime, stream_in_read_handler) 55 | { 56 | quic::connection cconn{client, acceptor.local_endpoint(), "host"}; 57 | 58 | std::optional connect_ec; 59 | auto stream = std::make_unique(cconn); 60 | cconn.async_connect(*stream, capture(connect_ec)); 61 | 62 | context.poll(); 63 | ASSERT_FALSE(context.stopped()); 64 | ASSERT_TRUE(connect_ec); 65 | EXPECT_EQ(ok, *connect_ec); 66 | 67 | auto &ref = *stream; 68 | auto data = std::array{}; 69 | std::optional read_ec; 70 | ref.async_read_some(boost::asio::buffer(data), 71 | [&read_ec, s=std::move(stream)] (error_code ec, size_t) { 72 | read_ec = ec; 73 | }); 74 | ASSERT_FALSE(read_ec); 75 | } 76 | 77 | } // namespace nexus 78 | -------------------------------------------------------------------------------- /test/quic/test_server_accept.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "certificate.hpp" 12 | 13 | namespace nexus { 14 | 15 | namespace { 16 | 17 | const error_code ok; 18 | 19 | auto capture(std::optional& out) { 20 | return [&] (error_code ec) { out = ec; }; 21 | } 22 | 23 | } // anonymous namespace 24 | 25 | TEST(server, accept_wait) // accept() before a connection is received 26 | { 27 | auto context = boost::asio::io_context{}; 28 | auto ex = context.get_executor(); 29 | auto global = global::init_client_server(); 30 | 31 | auto ssl = test::init_server_context("\04test"); 32 | auto sslc = test::init_client_context("\04test"); 33 | 34 | auto server = quic::server{ex}; 35 | const auto localhost = boost::asio::ip::make_address("127.0.0.1"); 36 | auto acceptor = quic::acceptor{server, udp::endpoint{localhost, 0}, ssl}; 37 | const auto endpoint = acceptor.local_endpoint(); 38 | acceptor.listen(16); 39 | 40 | std::optional accept_ec; 41 | auto sconn = quic::connection{acceptor}; 42 | acceptor.async_accept(sconn, capture(accept_ec)); 43 | 44 | context.poll(); 45 | ASSERT_FALSE(context.stopped()); 46 | EXPECT_FALSE(accept_ec); 47 | 48 | auto client = quic::client{ex, udp::endpoint{}, sslc}; 49 | auto cconn = quic::connection{client, endpoint, "host"}; 50 | 51 | std::optional stream_connect_ec; 52 | auto cstream = quic::stream{cconn}; 53 | cconn.async_connect(cstream, capture(stream_connect_ec)); 54 | 55 | context.poll(); 56 | ASSERT_FALSE(context.stopped()); 57 | ASSERT_TRUE(accept_ec); 58 | EXPECT_EQ(ok, *accept_ec); 59 | } 60 | 61 | TEST(server, accept_ready) // accept() after a connection is received 62 | { 63 | auto context = boost::asio::io_context{}; 64 | auto ex = context.get_executor(); 65 | auto global = global::init_client_server(); 66 | 67 | auto ssl = test::init_server_context("\04test"); 68 | auto sslc = test::init_client_context("\04test"); 69 | 70 | auto server = quic::server{ex}; 71 | const auto localhost = boost::asio::ip::make_address("127.0.0.1"); 72 | auto acceptor = quic::acceptor{server, udp::endpoint{localhost, 0}, ssl}; 73 | const auto endpoint = acceptor.local_endpoint(); 74 | acceptor.listen(16); 75 | 76 | auto client = quic::client{ex, udp::endpoint{}, sslc}; 77 | auto cconn = quic::connection{client, endpoint, "host"}; 78 | 79 | std::optional stream_connect_ec; 80 | auto cstream = quic::stream{cconn}; 81 | cconn.async_connect(cstream, capture(stream_connect_ec)); 82 | 83 | context.poll(); 84 | ASSERT_FALSE(context.stopped()); 85 | EXPECT_TRUE(stream_connect_ec); 86 | 87 | std::optional accept_ec; 88 | auto sconn = quic::connection{acceptor}; 89 | acceptor.async_accept(sconn, capture(accept_ec)); 90 | 91 | context.poll(); 92 | ASSERT_FALSE(context.stopped()); 93 | ASSERT_TRUE(accept_ec); 94 | EXPECT_EQ(ok, *accept_ec); 95 | } 96 | 97 | } // namespace nexus 98 | -------------------------------------------------------------------------------- /test/quic/test_server_initiated_stream.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "certificate.hpp" 12 | 13 | namespace nexus { 14 | 15 | namespace { 16 | 17 | const error_code ok; 18 | 19 | auto capture(std::optional& out) { 20 | return [&] (error_code ec, size_t bytes = 0) { out = ec; }; 21 | } 22 | 23 | } // anonymous namespace 24 | 25 | TEST(server, connect_stream) 26 | { 27 | auto context = boost::asio::io_context{}; 28 | auto ex = context.get_executor(); 29 | auto global = global::init_client_server(); 30 | 31 | const char* alpn = "\04test"; 32 | auto ssl = test::init_server_context(alpn); 33 | auto sslc = test::init_client_context(alpn); 34 | 35 | auto server = quic::server{ex}; 36 | const auto localhost = boost::asio::ip::make_address("127.0.0.1"); 37 | auto acceptor = quic::acceptor{server, udp::endpoint{localhost, 0}, ssl}; 38 | const auto endpoint = acceptor.local_endpoint(); 39 | acceptor.listen(16); 40 | 41 | std::optional accept_ec; 42 | auto sconn = quic::connection{acceptor}; 43 | acceptor.async_accept(sconn, capture(accept_ec)); 44 | 45 | auto client = quic::client{ex, udp::endpoint{}, sslc}; 46 | auto cconn = quic::connection{client, endpoint, "host"}; 47 | 48 | context.poll(); 49 | ASSERT_FALSE(context.stopped()); 50 | ASSERT_TRUE(accept_ec); 51 | EXPECT_EQ(ok, *accept_ec); 52 | 53 | std::optional sstream_connect_ec; 54 | auto sstream = quic::stream{sconn}; 55 | sconn.async_connect(sstream, capture(sstream_connect_ec)); 56 | 57 | std::optional cistream_accept_ec; 58 | auto cistream = quic::stream{cconn}; 59 | cconn.async_accept(cistream, capture(cistream_accept_ec)); 60 | 61 | context.poll(); 62 | ASSERT_FALSE(context.stopped()); 63 | ASSERT_TRUE(sstream_connect_ec); 64 | EXPECT_EQ(ok, *sstream_connect_ec); 65 | { 66 | const auto data = std::string_view{"1234"}; 67 | std::optional sstream_write_ec; 68 | sstream.async_write_some(boost::asio::buffer(data), capture(sstream_write_ec)); 69 | sstream.shutdown(1); 70 | context.poll(); 71 | ASSERT_FALSE(context.stopped()); 72 | ASSERT_TRUE(sstream_write_ec); 73 | EXPECT_EQ(ok, *sstream_write_ec); 74 | } 75 | ASSERT_TRUE(cistream_accept_ec); 76 | EXPECT_EQ(ok, *cistream_accept_ec); 77 | { 78 | auto data = std::array{}; 79 | std::optional cistream_read_ec; 80 | cistream.async_read_some(boost::asio::buffer(data), capture(cistream_read_ec)); 81 | context.poll(); 82 | ASSERT_FALSE(context.stopped()); 83 | ASSERT_TRUE(cistream_read_ec); 84 | EXPECT_EQ(ok, *cistream_read_ec); 85 | EXPECT_STREQ(data.data(), "1234"); 86 | } 87 | } 88 | 89 | } // namespace nexus 90 | --------------------------------------------------------------------------------