full QUIC support
This commit is contained in:
parent
45479a46ff
commit
28fab2509b
18 changed files with 1334 additions and 645 deletions
84
README.md
84
README.md
|
|
@ -1,19 +1,19 @@
|
||||||
# Crafter.Network
|
# Crafter.Network
|
||||||
|
|
||||||
A cross-platform C++ networking library providing TCP and HTTP client/server functionality with modern C++ features.
|
A cross-platform C++ networking library providing TCP, QUIC, and HTTP/3 client/server functionality with modern C++ features.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Crafter.Network is a comprehensive networking library designed for modern C++ applications. It provides TCP, HTTP, and QUIC networking capabilities with support for synchronous and asynchronous operations, making it suitable for a wide range of networking tasks including real-time multiplayer games.
|
Crafter.Network is a C++ networking library designed for modern C++ applications. It provides TCP, QUIC, and HTTP/3 networking capabilities with support for synchronous and asynchronous operations, making it suitable for a wide range of networking tasks including real-time multiplayer games.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **TCP Networking**: Client and server implementations for TCP connections
|
- **TCP Networking**: Client and server implementations for raw TCP connections.
|
||||||
- **HTTP Support**: Full HTTP client and server implementations with routing capabilities
|
- **QUIC Networking**: Encrypted, multi-stream transport via msquic — reliable streams for control plane, unreliable datagrams for low-latency state sync.
|
||||||
- **QUIC Networking**: Encrypted, multi-stream transport via msquic — reliable streams for control plane, unreliable datagrams for low-latency state sync
|
- **HTTP/3**: Client and server implementations on top of QUIC. Uses ALPN `h3`, QUIC bidi streams for requests/responses, the mandatory unidirectional control stream + SETTINGS frame (RFC 9114 §6.2.1), and a built-in QPACK codec (RFC 9204) with the full static table, Huffman *decoding* (RFC 7541), and literal-only emission. The QPACK dynamic table is unused; the optional QPACK encoder/decoder unidi streams are deliberately not opened (RFC 9204 §4.2 permits this when no dynamic-table operations are issued). The client is interoperable with mainstream public h3 endpoints (cloudflare, nghttp3-based servers, etc.).
|
||||||
- **Asynchronous Operations**: Thread pool-based async operations for improved performance
|
- **Asynchronous Operations**: Thread pool–based async operations for improved performance.
|
||||||
- **Cross-Platform**: Built for Unix-like systems with socket-based networking
|
- **Cross-Platform**: Built for Unix-like systems with socket-based networking.
|
||||||
- **Modern C++**: Uses C++ modules, STL containers, and modern C++ features
|
- **Modern C++**: Uses C++20 modules, STL containers, and modern C++ features.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|
@ -23,12 +23,14 @@ The library follows a modular design using C++20 modules:
|
||||||
- `Crafter.Network`: Main module that exports all components
|
- `Crafter.Network`: Main module that exports all components
|
||||||
- `Crafter.Network:ClientTCP`: TCP client implementation
|
- `Crafter.Network:ClientTCP`: TCP client implementation
|
||||||
- `Crafter.Network:ListenerTCP`: TCP server implementation
|
- `Crafter.Network:ListenerTCP`: TCP server implementation
|
||||||
- `Crafter.Network:ClientHTTP`: HTTP client implementation
|
- `Crafter.Network:ClientHTTP`: HTTP/3 client (ALPN `h3`)
|
||||||
- `Crafter.Network:ListenerHTTP`: HTTP server implementation
|
- `Crafter.Network:ListenerHTTP`: HTTP/3 server (ALPN `h3`)
|
||||||
- `Crafter.Network:HTTP`: HTTP protocol utilities and data structures
|
- `Crafter.Network:HTTP`: HTTP request/response types and constructors
|
||||||
- `Crafter.Network:ClientQUIC`: QUIC connection (client + accepted-server side) with reliable streams and unreliable datagrams
|
- `Crafter.Network:ClientQUIC`: QUIC connection (client + accepted-server side) with reliable streams and unreliable datagrams
|
||||||
- `Crafter.Network:ListenerQUIC`: QUIC listener accepting incoming connections
|
- `Crafter.Network:ListenerQUIC`: QUIC listener accepting incoming connections
|
||||||
|
|
||||||
|
The `Crafter.Network:HTTP3` partition contains internal HTTP/3 wire-format helpers (QUIC varint, frame layer, QPACK static-table codec) and is intentionally not re-exported from the main module — it is shared between the `ClientHTTP` and `ListenerHTTP` implementations.
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
### TCP Components
|
### TCP Components
|
||||||
|
|
@ -53,30 +55,38 @@ Crafter::ListenerTCP listener(8080, callback);
|
||||||
listener.ListenSyncSync(); // Synchronous listening
|
listener.ListenSyncSync(); // Synchronous listening
|
||||||
```
|
```
|
||||||
|
|
||||||
### HTTP Components
|
### HTTP/3 Components
|
||||||
|
|
||||||
|
HTTP/3 runs over QUIC, which always requires TLS. Pass server credentials when constructing the listener (or set `selfSigned = true` for a development-only ephemeral cert) and matching client credentials when constructing the client (`insecureNoServerValidation = true` for self-signed servers).
|
||||||
|
|
||||||
#### ClientHTTP
|
#### ClientHTTP
|
||||||
```cpp
|
```cpp
|
||||||
// Create an HTTP client
|
Crafter::QUICClientCredentials creds;
|
||||||
Crafter::ClientHTTP client("httpbin.org", 80);
|
creds.insecureNoServerValidation = true; // dev-only
|
||||||
|
Crafter::ClientHTTP client("localhost", 8082, creds);
|
||||||
|
|
||||||
// Send HTTP request
|
Crafter::HTTPResponse response = client.Send(
|
||||||
std::string request = Crafter::CreateRequestHTTP("GET", "/get", "httpbin.org");
|
Crafter::CreateRequestHTTP("GET", "/", "localhost")
|
||||||
Crafter::HTTPResponse response = client.Send(request);
|
);
|
||||||
|
// response.status is the numeric status as a string, e.g. "200"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### ListenerHTTP
|
#### ListenerHTTP
|
||||||
```cpp
|
```cpp
|
||||||
// Create an HTTP listener with routes
|
std::unordered_map<std::string,
|
||||||
std::unordered_map<std::string, std::function<std::string(const Crafter::HTTPRequest&)>> routes;
|
std::function<Crafter::HTTPResponse(const Crafter::HTTPRequest&)>> routes;
|
||||||
routes["/hello"] = [](const Crafter::HTTPRequest& req) {
|
routes["/hello"] = [](const Crafter::HTTPRequest&) {
|
||||||
return Crafter::CreateResponseHTTP("200 OK", "Hello World!");
|
return Crafter::CreateResponseHTTP("200", "Hello World!");
|
||||||
};
|
};
|
||||||
|
|
||||||
Crafter::ListenerHTTP listener(8080, routes);
|
Crafter::QUICServerCredentials creds;
|
||||||
|
creds.selfSigned = true; // dev-only
|
||||||
|
Crafter::ListenerHTTP listener(8082, creds, routes);
|
||||||
listener.Listen();
|
listener.Listen();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `HTTPRequest` exposes the four HTTP/3 pseudo-headers (`method`, `scheme`, `authority`, `path`) as named struct fields rather than mixing them into the regular `headers` map. Routes are dispatched by exact match on `path`; unmatched paths return a synthetic 404.
|
||||||
|
|
||||||
## Build Configuration
|
## Build Configuration
|
||||||
|
|
||||||
The project uses a configuration system with multiple build targets:
|
The project uses a configuration system with multiple build targets:
|
||||||
|
|
@ -88,19 +98,22 @@ The project uses a configuration system with multiple build targets:
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
The library includes comprehensive tests covering:
|
The library includes tests covering:
|
||||||
- Compilation verification
|
- HTTP/3 round-trip (`ShouldSendRecieveHTTP`) — canonical local client/server round-trip
|
||||||
- HTTP receive functionality
|
- HTTP/3 connection multiplexing (`ShouldSendRecieveKeepaliveHTTP`) — two requests share one QUIC connection
|
||||||
- HTTP send functionality
|
- HTTP/3 large body transfer (`ShouldSendRecieveLargeHTTP`) — 10 MiB POST
|
||||||
- HTTP send/receive operations
|
- HTTP/3 external interop (`ShouldSend`) — live fetch from `cloudflare-quic.com:443`, exercises real TLS chain validation, mandatory control stream, peer-initiated unidi streams, and QPACK Huffman decoding
|
||||||
- Keep-alive HTTP operations
|
- QUIC reliable streams
|
||||||
- Large HTTP data transfers
|
- QUIC unreliable datagrams
|
||||||
|
|
||||||
|
The external-interop test requires outbound UDP/443; if your network blocks it the test will fail.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- Crafter.Thread: Thread pool management for asynchronous operations
|
- **Crafter.Thread**: Thread pool management for asynchronous operations.
|
||||||
- **msquic** — fetched and built automatically as a Crafter `ExternalDependency` (no system install required). The build clones `microsoft/msquic` recursively into the per-project external cache, configures it via CMake (`QUIC_TLS_LIB=quictls`, tests/tools/perf disabled), and links the produced `libmsquic` into the QUIC modules.
|
- **msquic** — fetched and built automatically as a Crafter `ExternalDependency` (no system install required). The build clones `microsoft/msquic` recursively into the per-project external cache, configures it via CMake (`QUIC_TLS_LIB=quictls`, tests/tools/perf disabled), and links the produced `libmsquic` into the QUIC and HTTP/3 modules.
|
||||||
- On Linux msquic links against `libnuma` (provided by the `numactl` package on most distros).
|
- On Linux msquic links against `libnuma` (provided by the `numactl` package on most distros).
|
||||||
|
- The self-signed-cert path used by tests/dev shells out to the `openssl` CLI; install `openssl` if you intend to use `QUICServerCredentials{selfSigned = true}`.
|
||||||
|
|
||||||
## Usage Example
|
## Usage Example
|
||||||
|
|
||||||
|
|
@ -109,15 +122,14 @@ The library includes comprehensive tests covering:
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
// Simple HTTP client example
|
Crafter::QUICClientCredentials creds;
|
||||||
Crafter::ClientHTTP client("httpbin.org", 80);
|
creds.insecureNoServerValidation = true;
|
||||||
|
Crafter::ClientHTTP client("localhost", 8443, creds);
|
||||||
|
|
||||||
auto request = Crafter::CreateRequestHTTP("GET", "/get", "httpbin.org");
|
auto response = client.Send(Crafter::CreateRequestHTTP("GET", "/", "localhost"));
|
||||||
auto response = client.Send(request);
|
|
||||||
|
|
||||||
std::cout << "Status: " << response.status << std::endl;
|
std::cout << "Status: " << response.status << std::endl;
|
||||||
std::cout << "Body: " << response.body << std::endl;
|
std::cout << "Body: " << response.body << std::endl;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -19,190 +19,150 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
#include <msquic.h>
|
||||||
#include <stdio.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <netinet/in.h>
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <sys/uio.h>
|
|
||||||
#include <sys/time.h>
|
|
||||||
#include <sys/wait.h>
|
|
||||||
|
|
||||||
module Crafter.Network:ClientHTTP_impl;
|
module Crafter.Network:ClientHTTP_impl;
|
||||||
import :ClientHTTP;
|
import :ClientHTTP;
|
||||||
|
import :ClientQUIC;
|
||||||
|
import :HTTP;
|
||||||
|
import :HTTP3;
|
||||||
import Crafter.Thread;
|
import Crafter.Thread;
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
ClientHTTP::ClientHTTP(const char* host, std::uint16_t port): host(host), port(port), client(host, port) {
|
struct ClientHTTP::Impl {
|
||||||
|
ClientQUIC quic;
|
||||||
|
// Outgoing control stream — RFC 9114 §6.2.1: each peer MUST open a
|
||||||
|
// unidirectional control stream and send a SETTINGS frame as its first
|
||||||
|
// frame. Most real h3 servers (cloudflare, nghttp3, lsquic, …) close
|
||||||
|
// the connection with H3_MISSING_SETTINGS if we don't. The stream
|
||||||
|
// stays open for the lifetime of the connection; we never FIN it.
|
||||||
|
QUICStream controlStream;
|
||||||
|
|
||||||
}
|
Impl(const char* host, std::uint16_t port, QUICClientCredentials creds)
|
||||||
|
: quic(host, port, std::string(HTTP3::kAlpn), creds) {
|
||||||
ClientHTTP::ClientHTTP(std::string host, std::uint16_t port): ClientHTTP(host.c_str(), port) {
|
// Drain any unidi streams the server opens to us (its control
|
||||||
|
// stream + optional QPACK encoder/decoder streams). We don't act
|
||||||
}
|
// on the contents — SETTINGS we accept by defaults, dynamic-table
|
||||||
|
// mutations we discard since we operate with no dynamic table.
|
||||||
HTTPResponse ClientHTTP::Send(const char* request, std::uint32_t length) {
|
// Any bidi stream from the server would be a server push, which
|
||||||
std::cout << "Send started" << std::endl;
|
// we don't support — best-effort drain it as well.
|
||||||
client.Send(request, length);
|
quic.OnStream([](QUICStream stream) {
|
||||||
std::cout << "Send Complete" << std::endl;
|
|
||||||
std::vector<char> buffer;
|
|
||||||
HTTPResponse response;
|
|
||||||
std::uint32_t i = 0;
|
|
||||||
std::uint32_t statusStart = 0;
|
|
||||||
while(true) {
|
|
||||||
try {
|
try {
|
||||||
buffer = client.RecieveSync();
|
while (true) (void)stream.RecieveSync();
|
||||||
std::cout << "Recieved: " << buffer.size() << std::endl;
|
} catch (...) {
|
||||||
} catch(const SocketClosedException& e) {
|
// Stream / connection closed. Done.
|
||||||
std::cout << "Retry" << std::endl;
|
|
||||||
client.Stop();
|
|
||||||
client.Connect();
|
|
||||||
client.Send(request, length);
|
|
||||||
buffer = client.RecieveSync();
|
|
||||||
std::cout << "Recieved: " << buffer.size() << std::endl;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
for(; i < buffer.size(); i++) {
|
controlStream = quic.OpenStream(/*unidirectional=*/true);
|
||||||
if(buffer[i] == ' ') {
|
auto prelude = HTTP3::BuildControlStreamPrelude();
|
||||||
statusStart = i;
|
controlStream.SendSync(prelude.data(),
|
||||||
break;
|
static_cast<std::uint32_t>(prelude.size()),
|
||||||
}
|
/*finish=*/false);
|
||||||
}
|
|
||||||
for(; i < buffer.size(); i++) {
|
|
||||||
if(buffer[i] == '\r') {
|
|
||||||
response.status.assign(buffer.data()+statusStart+1, i-statusStart-1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i+=2;
|
|
||||||
while(i < buffer.size()) {
|
|
||||||
std::uint32_t headerStart = i;
|
|
||||||
std::string headerName;
|
|
||||||
for(; i < buffer.size(); i++) {
|
|
||||||
if(buffer[i] == ':') {
|
|
||||||
headerName.assign(buffer.data()+headerStart, i-headerStart);
|
|
||||||
std::transform(headerName.begin(), headerName.end(), headerName.begin(), [](unsigned char c){ return std::tolower(c); });
|
|
||||||
i+=2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
headerStart = i;
|
|
||||||
std::string headerValue;
|
|
||||||
for(; i < buffer.size(); i++) {
|
|
||||||
if(buffer[i] == '\r' && buffer[i+1] == '\n') {
|
|
||||||
headerValue.assign(buffer.data()+headerStart, i-headerStart);
|
|
||||||
response.headers.insert({headerName, headerValue});
|
|
||||||
if(buffer[i+2] == '\r'){
|
|
||||||
goto headersComplete;
|
|
||||||
} else{
|
|
||||||
i+=2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
i = 0;
|
ClientHTTP::ClientHTTP(const char* host, std::uint16_t port, QUICClientCredentials creds)
|
||||||
|
: host(host), port(port), impl(std::make_unique<Impl>(host, port, std::move(creds))) {}
|
||||||
|
|
||||||
|
ClientHTTP::ClientHTTP(std::string host, std::uint16_t port, QUICClientCredentials creds)
|
||||||
|
: ClientHTTP(host.c_str(), port, std::move(creds)) {}
|
||||||
|
|
||||||
|
ClientHTTP::ClientHTTP(ClientHTTP&&) noexcept = default;
|
||||||
|
ClientHTTP::~ClientHTTP() = default;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Parse a sequence of HTTP/3 frames from `bytes`. Populates response from
|
||||||
|
// the first HEADERS frame and concatenates all DATA payloads. Trailing
|
||||||
|
// HEADERS frames (trailers) are decoded but discarded. Throws on
|
||||||
|
// malformed input.
|
||||||
|
HTTPResponse ParseResponseFrames(const std::vector<char>& bytes) {
|
||||||
|
HTTPResponse response;
|
||||||
|
bool sawHeaders = false;
|
||||||
|
std::size_t pos = 0;
|
||||||
|
const auto* p = reinterpret_cast<const std::uint8_t*>(bytes.data());
|
||||||
|
std::size_t avail = bytes.size();
|
||||||
|
|
||||||
|
while (pos < avail) {
|
||||||
|
std::uint64_t frameType = 0, frameLen = 0;
|
||||||
|
std::size_t cn = 0;
|
||||||
|
if (!HTTP3::DecodeVarint(p + pos, avail - pos, frameType, cn)) {
|
||||||
|
throw HTTP3::HTTP3ProtocolError("truncated frame type");
|
||||||
}
|
}
|
||||||
headersComplete:;
|
pos += cn;
|
||||||
std::cout << "Header complete" << std::endl;
|
if (!HTTP3::DecodeVarint(p + pos, avail - pos, frameLen, cn)) {
|
||||||
i+=4;
|
throw HTTP3::HTTP3ProtocolError("truncated frame length");
|
||||||
std::unordered_map<std::string, std::string>::iterator it = response.headers.find("content-length");
|
|
||||||
if(it != response.headers.end())
|
|
||||||
{
|
|
||||||
const int lenght = std::stoi(it->second);
|
|
||||||
std::cout << "Content lenght: " << lenght << std::endl;
|
|
||||||
response.body.resize(lenght, 0);
|
|
||||||
if(i < buffer.size()){
|
|
||||||
std::memcpy(&response.body[0], buffer.data()+i, buffer.size()-i);
|
|
||||||
}
|
}
|
||||||
const int remaining = lenght-(buffer.size()-i);
|
pos += cn;
|
||||||
std::cout << "Remain: " << remaining << std::endl;
|
if (pos + frameLen > avail) {
|
||||||
if(remaining > 0){
|
throw HTTP3::HTTP3ProtocolError("frame length runs past buffer");
|
||||||
std::vector<char> bodyBuffer = client.RecieveUntilFullSync(remaining);
|
|
||||||
std::memcpy(&response.body[ buffer.size()-i], bodyBuffer.data(), bodyBuffer.size());
|
|
||||||
std::cout << "Recieved: " << bodyBuffer.size() << std::endl;
|
|
||||||
}
|
}
|
||||||
|
if (frameType == HTTP3::kFrameHeaders) {
|
||||||
|
auto fields = HTTP3::DecodeFieldSection(p + pos, static_cast<std::size_t>(frameLen));
|
||||||
|
if (!sawHeaders) {
|
||||||
|
for (auto& [name, value] : fields) {
|
||||||
|
if (name == ":status") {
|
||||||
|
response.status = std::move(value);
|
||||||
|
} else if (!name.empty() && name[0] == ':') {
|
||||||
|
// Unknown response pseudo-header — ignore.
|
||||||
} else {
|
} else {
|
||||||
std::cout << "No Content Lenght" << std::endl;
|
response.headers.emplace(std::move(name), std::move(value));
|
||||||
std::unordered_map<std::string, std::string>::iterator it = response.headers.find("transfer-encoding");
|
|
||||||
if(it != response.headers.end() && it->second == "chunked") {
|
|
||||||
std::cout << "Chunked" << std::endl;
|
|
||||||
while(i < buffer.size()){
|
|
||||||
std::string lenght;
|
|
||||||
int lenghtStart = i;
|
|
||||||
for(; i < buffer.size(); i++) {
|
|
||||||
if(buffer[i] == '\r') {
|
|
||||||
lenght.assign(buffer.data()+lenghtStart, i-lenghtStart);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
i+=2;
|
sawHeaders = true;
|
||||||
int lenghtInt = stoi(lenght, 0, 8);
|
}
|
||||||
if(lenghtInt != 0){
|
// Trailer HEADERS frames are skipped; the field section was
|
||||||
int oldSize = response.body.size();
|
// already decoded above and the contents discarded.
|
||||||
response.body.resize(oldSize+lenghtInt, 0);
|
} else if (frameType == HTTP3::kFrameData) {
|
||||||
if(buffer.size() < lenghtInt) {
|
response.body.append(reinterpret_cast<const char*>(p + pos),
|
||||||
std::memcpy(&response.body[oldSize], buffer.data()+i, buffer.size()-i);
|
static_cast<std::size_t>(frameLen));
|
||||||
std::vector<char> bodyBuffer2 = client.RecieveUntilFullSync(lenghtInt-buffer.size());
|
|
||||||
std::memcpy(&response.body[oldSize+(buffer.size()-i)], buffer.data(), buffer.size());
|
|
||||||
} else {
|
} else {
|
||||||
std::memcpy(&response.body[oldSize], buffer.data()+i, lenghtInt);
|
// Unknown frame types are reserved/extensions — RFC 9114 §9
|
||||||
i+=lenghtInt;
|
// says skip them.
|
||||||
}
|
}
|
||||||
} else{
|
pos += static_cast<std::size_t>(frameLen);
|
||||||
goto bodyFinished;
|
|
||||||
}
|
}
|
||||||
|
if (!sawHeaders) {
|
||||||
|
throw HTTP3::HTTP3ProtocolError("response stream had no HEADERS frame");
|
||||||
}
|
}
|
||||||
while(true) {
|
|
||||||
std::vector<char> bodyBuffer = client.RecieveSync();
|
|
||||||
int i2 = 0;
|
|
||||||
while(i2 < bodyBuffer.size()){
|
|
||||||
std::string lenght;
|
|
||||||
int lenghtStart = i2;
|
|
||||||
for(; i2 < bodyBuffer.size(); i2++) {
|
|
||||||
if(buffer[i2] == '\r') {
|
|
||||||
lenght.assign(bodyBuffer.data()+lenghtStart, i2-lenghtStart);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i2+=2;
|
|
||||||
int lenghtInt = stoi(lenght, 0, 8);
|
|
||||||
if(lenghtInt != 0){
|
|
||||||
int oldSize = response.body.size();
|
|
||||||
response.body.resize(oldSize+lenghtInt, 0);
|
|
||||||
if(bodyBuffer.size() < lenghtInt) {
|
|
||||||
std::memcpy(&response.body[oldSize], bodyBuffer.data()+i2, bodyBuffer.size()-i2);
|
|
||||||
std::vector<char> bodyBuffer2 = client.RecieveUntilFullSync(lenghtInt-bodyBuffer.size());
|
|
||||||
std::memcpy(&response.body[oldSize+(bodyBuffer.size()-i2)], bodyBuffer2.data(), bodyBuffer2.size());
|
|
||||||
} else {
|
|
||||||
std::memcpy(&response.body[oldSize], bodyBuffer.data()+i2, lenghtInt);
|
|
||||||
i2+=lenghtInt;
|
|
||||||
}
|
|
||||||
} else{
|
|
||||||
goto bodyFinished;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bodyFinished:;
|
|
||||||
} else {
|
|
||||||
std::cout << "Recv until close" << std::endl;
|
|
||||||
std::vector<char> bodyBuffer = client.RecieveUntilCloseSync();
|
|
||||||
response.body.resize((buffer.size()-i)+(bodyBuffer.size()), 0);
|
|
||||||
if(i < buffer.size()){
|
|
||||||
std::memcpy(&response.body[0], buffer.data()+i, buffer.size()-i);
|
|
||||||
}
|
|
||||||
std::memcpy(&response.body[buffer.size()-i], bodyBuffer.data(), bodyBuffer.size());
|
|
||||||
std::cout << "Closed" << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::cout << "Response recieved" << std::endl;
|
|
||||||
return response;
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
HTTPResponse ClientHTTP::Send(std::string request) {
|
|
||||||
return Send(request.c_str(), request.size());
|
HTTPResponse ClientHTTP::Send(const HTTPRequest& request) {
|
||||||
|
QUICStream stream = impl->quic.OpenStream();
|
||||||
|
|
||||||
|
// Pseudo-headers MUST appear before regular fields (RFC 9114 §4.3).
|
||||||
|
std::vector<std::pair<std::string, std::string>> fields;
|
||||||
|
fields.reserve(4 + request.headers.size());
|
||||||
|
fields.emplace_back(":method", request.method.empty() ? std::string("GET") : request.method);
|
||||||
|
fields.emplace_back(":scheme", request.scheme.empty() ? std::string("https") : request.scheme);
|
||||||
|
fields.emplace_back(":authority",
|
||||||
|
request.authority.empty() ? (host + ":" + std::to_string(port)) : request.authority);
|
||||||
|
fields.emplace_back(":path", request.path.empty() ? std::string("/") : request.path);
|
||||||
|
for (const auto& [name, value] : request.headers) {
|
||||||
|
// HTTP/3 forbids uppercase in field names — lowercase defensively.
|
||||||
|
std::string lower = name;
|
||||||
|
std::ranges::transform(lower, lower.begin(),
|
||||||
|
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
||||||
|
fields.emplace_back(std::move(lower), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto encoded = HTTP3::EncodeFieldSection(fields);
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> wire;
|
||||||
|
HTTP3::WriteFrame(wire, HTTP3::kFrameHeaders, encoded.data(), encoded.size());
|
||||||
|
if (!request.body.empty()) {
|
||||||
|
HTTP3::WriteFrame(wire, HTTP3::kFrameData,
|
||||||
|
reinterpret_cast<const std::uint8_t*>(request.body.data()),
|
||||||
|
request.body.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the entire request and FIN our send-side. HTTP/3 servers need FIN
|
||||||
|
// to know the request is complete — there's no Content-Length signal.
|
||||||
|
stream.SendSync(wire.data(), static_cast<std::uint32_t>(wire.size()), /*finish=*/true);
|
||||||
|
|
||||||
|
auto raw = stream.RecieveUntilCloseSync();
|
||||||
|
return ParseResponseFrames(raw);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,8 @@ struct QUICStream::Impl {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
QUICStream::QUICStream() = default;
|
||||||
|
|
||||||
QUICStream::QUICStream(HQUIC handle, ClientQUIC* connection)
|
QUICStream::QUICStream(HQUIC handle, ClientQUIC* connection)
|
||||||
: handle(handle), connection(connection), impl(std::make_unique<Impl>())
|
: handle(handle), connection(connection), impl(std::make_unique<Impl>())
|
||||||
{
|
{
|
||||||
|
|
@ -159,7 +161,9 @@ QUICStream::QUICStream(HQUIC handle, ClientQUIC* connection)
|
||||||
}
|
}
|
||||||
|
|
||||||
QUICStream::QUICStream(QUICStream&& other) noexcept
|
QUICStream::QUICStream(QUICStream&& other) noexcept
|
||||||
: handle(other.handle), connection(other.connection), impl(std::move(other.impl))
|
: handle(other.handle), connection(other.connection),
|
||||||
|
canSend(other.canSend), canReceive(other.canReceive),
|
||||||
|
impl(std::move(other.impl))
|
||||||
{
|
{
|
||||||
other.handle = nullptr;
|
other.handle = nullptr;
|
||||||
other.connection = nullptr;
|
other.connection = nullptr;
|
||||||
|
|
@ -170,6 +174,8 @@ QUICStream& QUICStream::operator=(QUICStream&& other) noexcept {
|
||||||
Stop();
|
Stop();
|
||||||
handle = other.handle;
|
handle = other.handle;
|
||||||
connection = other.connection;
|
connection = other.connection;
|
||||||
|
canSend = other.canSend;
|
||||||
|
canReceive = other.canReceive;
|
||||||
impl = std::move(other.impl);
|
impl = std::move(other.impl);
|
||||||
other.handle = nullptr;
|
other.handle = nullptr;
|
||||||
other.connection = nullptr;
|
other.connection = nullptr;
|
||||||
|
|
@ -183,12 +189,26 @@ QUICStream::~QUICStream() {
|
||||||
|
|
||||||
void QUICStream::Stop() {
|
void QUICStream::Stop() {
|
||||||
if (!handle) return;
|
if (!handle) return;
|
||||||
|
// If the stream's SHUTDOWN_COMPLETE event has already fired, msquic has
|
||||||
|
// internally called StreamClose for us (see Impl::Callback) and the
|
||||||
|
// handle is no longer valid — calling StreamShutdown on it trips a
|
||||||
|
// quic_bugcheck inside msquic. Skip in that case. This is the common
|
||||||
|
// path for short-lived request/response streams where both peers FIN
|
||||||
|
// before the wrapper is destroyed.
|
||||||
|
bool alreadyClosed = false;
|
||||||
|
if (impl) {
|
||||||
|
std::lock_guard lk(impl->mtx);
|
||||||
|
alreadyClosed = impl->shutdownComplete;
|
||||||
|
}
|
||||||
|
if (!alreadyClosed) {
|
||||||
Runtime().api->StreamShutdown(handle, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0);
|
Runtime().api->StreamShutdown(handle, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0);
|
||||||
|
}
|
||||||
handle = nullptr;
|
handle = nullptr;
|
||||||
|
if (impl) impl->handle = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void QUICStream::SendSync(const void* buffer, std::uint32_t size, bool finish) {
|
void QUICStream::SendSync(const void* buffer, std::uint32_t size, bool finish) {
|
||||||
if (!handle) throw QUICClosedException();
|
if (!handle || !canSend) throw QUICClosedException();
|
||||||
auto* copy = new char[size];
|
auto* copy = new char[size];
|
||||||
std::memcpy(copy, buffer, size);
|
std::memcpy(copy, buffer, size);
|
||||||
QUIC_BUFFER quicBuf{};
|
QUIC_BUFFER quicBuf{};
|
||||||
|
|
@ -210,7 +230,7 @@ void QUICStream::SendSync(const void* buffer, std::uint32_t size, bool finish) {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<char> QUICStream::RecieveSync() {
|
std::vector<char> QUICStream::RecieveSync() {
|
||||||
if (!handle) throw QUICClosedException();
|
if (!handle || !canReceive) throw QUICClosedException();
|
||||||
std::unique_lock lk(impl->mtx);
|
std::unique_lock lk(impl->mtx);
|
||||||
impl->cv.wait(lk, [&]{ return !impl->pending.empty() || impl->peerSendClosed || impl->shutdownComplete; });
|
impl->cv.wait(lk, [&]{ return !impl->pending.empty() || impl->peerSendClosed || impl->shutdownComplete; });
|
||||||
if (!impl->pending.empty()) {
|
if (!impl->pending.empty()) {
|
||||||
|
|
@ -222,7 +242,7 @@ std::vector<char> QUICStream::RecieveSync() {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<char> QUICStream::RecieveUntilCloseSync() {
|
std::vector<char> QUICStream::RecieveUntilCloseSync() {
|
||||||
if (!handle) throw QUICClosedException();
|
if (!handle || !canReceive) throw QUICClosedException();
|
||||||
std::vector<char> out;
|
std::vector<char> out;
|
||||||
while (true) {
|
while (true) {
|
||||||
std::unique_lock lk(impl->mtx);
|
std::unique_lock lk(impl->mtx);
|
||||||
|
|
@ -237,7 +257,7 @@ std::vector<char> QUICStream::RecieveUntilCloseSync() {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<char> QUICStream::RecieveUntilFullSync(std::uint32_t bufferSize) {
|
std::vector<char> QUICStream::RecieveUntilFullSync(std::uint32_t bufferSize) {
|
||||||
if (!handle) throw QUICClosedException();
|
if (!handle || !canReceive) throw QUICClosedException();
|
||||||
std::vector<char> out;
|
std::vector<char> out;
|
||||||
out.reserve(bufferSize);
|
out.reserve(bufferSize);
|
||||||
while (out.size() < bufferSize) {
|
while (out.size() < bufferSize) {
|
||||||
|
|
@ -285,6 +305,12 @@ struct ClientQUIC::Impl {
|
||||||
std::function<void(QUICStream)> onStream;
|
std::function<void(QUICStream)> onStream;
|
||||||
std::function<void(std::vector<char>)> onDatagram;
|
std::function<void(std::vector<char>)> onDatagram;
|
||||||
std::deque<std::vector<char>> datagramQueue;
|
std::deque<std::vector<char>> datagramQueue;
|
||||||
|
// Streams the peer started before the user installed an OnStream
|
||||||
|
// handler. Without this backlog the early streams (e.g. an h3 server's
|
||||||
|
// control stream right after handshake) would be aborted in the
|
||||||
|
// PEER_STREAM_STARTED branch and the connection would die with
|
||||||
|
// H3_MISSING_SETTINGS on the peer side.
|
||||||
|
std::deque<QUICStream> pendingStreams;
|
||||||
|
|
||||||
ClientQUIC* outer = nullptr;
|
ClientQUIC* outer = nullptr;
|
||||||
|
|
||||||
|
|
@ -325,18 +351,30 @@ struct ClientQUIC::Impl {
|
||||||
}
|
}
|
||||||
case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: {
|
case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: {
|
||||||
HQUIC streamHandle = ev->PEER_STREAM_STARTED.Stream;
|
HQUIC streamHandle = ev->PEER_STREAM_STARTED.Stream;
|
||||||
|
bool unidirectional = (ev->PEER_STREAM_STARTED.Flags
|
||||||
|
& QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL) != 0;
|
||||||
QUICStream stream(streamHandle, self->outer);
|
QUICStream stream(streamHandle, self->outer);
|
||||||
if (self->onStream) {
|
if (unidirectional) {
|
||||||
auto cb = self->onStream;
|
// Peer-initiated unidi: peer sends, we read; we cannot send.
|
||||||
|
stream.canSend = false;
|
||||||
|
stream.canReceive = true;
|
||||||
|
}
|
||||||
|
std::function<void(QUICStream)> cb;
|
||||||
|
{
|
||||||
|
std::lock_guard lk(self->mtx);
|
||||||
|
cb = self->onStream;
|
||||||
|
if (!cb) {
|
||||||
|
// Buffer until OnStream is installed; OnStream's
|
||||||
|
// setter drains this queue.
|
||||||
|
self->pendingStreams.push_back(std::move(stream));
|
||||||
|
return QUIC_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
auto* shared = new QUICStream(std::move(stream));
|
auto* shared = new QUICStream(std::move(stream));
|
||||||
ThreadPool::Enqueue([cb, shared]{
|
ThreadPool::Enqueue([cb, shared]{
|
||||||
cb(std::move(*shared));
|
cb(std::move(*shared));
|
||||||
delete shared;
|
delete shared;
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// No handler: shut down to avoid leaking a stream.
|
|
||||||
Runtime().api->StreamShutdown(streamHandle, QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0);
|
|
||||||
}
|
|
||||||
return QUIC_STATUS_SUCCESS;
|
return QUIC_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
case QUIC_CONNECTION_EVENT_DATAGRAM_RECEIVED: {
|
case QUIC_CONNECTION_EVENT_DATAGRAM_RECEIVED: {
|
||||||
|
|
@ -382,6 +420,16 @@ static HQUIC OpenClientConfiguration(const std::string& alpn, const QUICClientCr
|
||||||
settings.IdleTimeoutMs = 30'000;
|
settings.IdleTimeoutMs = 30'000;
|
||||||
settings.IsSet.DatagramReceiveEnabled = 1;
|
settings.IsSet.DatagramReceiveEnabled = 1;
|
||||||
settings.DatagramReceiveEnabled = 1;
|
settings.DatagramReceiveEnabled = 1;
|
||||||
|
// Allow the server to open unidi/bidi streams to us. msquic defaults
|
||||||
|
// both peer-stream-count limits to 0; with that, the server's HTTP/3
|
||||||
|
// control stream + QPACK encoder/decoder streams can't be created and
|
||||||
|
// most h3 servers will close the connection after handshake. We don't
|
||||||
|
// currently use server push (h3 pushes ride on unidi 0x01 streams) but
|
||||||
|
// the bidi cap is harmless to grant.
|
||||||
|
settings.IsSet.PeerUnidiStreamCount = 1;
|
||||||
|
settings.PeerUnidiStreamCount = 16;
|
||||||
|
settings.IsSet.PeerBidiStreamCount = 1;
|
||||||
|
settings.PeerBidiStreamCount = 16;
|
||||||
|
|
||||||
HQUIC cfg = nullptr;
|
HQUIC cfg = nullptr;
|
||||||
QUIC_STATUS s = Runtime().api->ConfigurationOpen(Runtime().registration, &alpnBuffer, 1,
|
QUIC_STATUS s = Runtime().api->ConfigurationOpen(Runtime().registration, &alpnBuffer, 1,
|
||||||
|
|
@ -470,18 +518,26 @@ void ClientQUIC::Stop() {
|
||||||
Runtime().api->ConnectionShutdown(impl->connection, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0);
|
Runtime().api->ConnectionShutdown(impl->connection, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
QUICStream ClientQUIC::OpenStream() {
|
QUICStream ClientQUIC::OpenStream(bool unidirectional) {
|
||||||
HQUIC streamHandle = nullptr;
|
HQUIC streamHandle = nullptr;
|
||||||
QUICStream stream;
|
QUICStream stream;
|
||||||
stream.impl = std::make_unique<QUICStream::Impl>();
|
stream.impl = std::make_unique<QUICStream::Impl>();
|
||||||
stream.impl->connection = this;
|
stream.impl->connection = this;
|
||||||
QUIC_STATUS s = Runtime().api->StreamOpen(impl->connection, QUIC_STREAM_OPEN_FLAG_NONE,
|
QUIC_STREAM_OPEN_FLAGS openFlags = unidirectional
|
||||||
|
? QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL
|
||||||
|
: QUIC_STREAM_OPEN_FLAG_NONE;
|
||||||
|
QUIC_STATUS s = Runtime().api->StreamOpen(impl->connection, openFlags,
|
||||||
reinterpret_cast<QUIC_STREAM_CALLBACK_HANDLER>(&QUICStream::Impl::Callback),
|
reinterpret_cast<QUIC_STREAM_CALLBACK_HANDLER>(&QUICStream::Impl::Callback),
|
||||||
stream.impl.get(), &streamHandle);
|
stream.impl.get(), &streamHandle);
|
||||||
if (QUIC_FAILED(s)) throw QUICException(std::format("StreamOpen failed: 0x{:x}", static_cast<unsigned>(s)));
|
if (QUIC_FAILED(s)) throw QUICException(std::format("StreamOpen failed: 0x{:x}", static_cast<unsigned>(s)));
|
||||||
stream.handle = streamHandle;
|
stream.handle = streamHandle;
|
||||||
stream.connection = this;
|
stream.connection = this;
|
||||||
stream.impl->handle = streamHandle;
|
stream.impl->handle = streamHandle;
|
||||||
|
if (unidirectional) {
|
||||||
|
// We initiated the unidi stream: we send, peer reads.
|
||||||
|
stream.canSend = true;
|
||||||
|
stream.canReceive = false;
|
||||||
|
}
|
||||||
s = Runtime().api->StreamStart(streamHandle, QUIC_STREAM_START_FLAG_NONE);
|
s = Runtime().api->StreamStart(streamHandle, QUIC_STREAM_START_FLAG_NONE);
|
||||||
if (QUIC_FAILED(s)) {
|
if (QUIC_FAILED(s)) {
|
||||||
Runtime().api->StreamClose(streamHandle);
|
Runtime().api->StreamClose(streamHandle);
|
||||||
|
|
@ -510,7 +566,21 @@ void ClientQUIC::SendDatagram(const void* buffer, std::uint32_t size) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientQUIC::OnStream(std::function<void(QUICStream)> cb) {
|
void ClientQUIC::OnStream(std::function<void(QUICStream)> cb) {
|
||||||
impl->onStream = std::move(cb);
|
std::deque<QUICStream> backlog;
|
||||||
|
{
|
||||||
|
std::lock_guard lk(impl->mtx);
|
||||||
|
impl->onStream = cb;
|
||||||
|
std::swap(backlog, impl->pendingStreams);
|
||||||
|
}
|
||||||
|
while (!backlog.empty()) {
|
||||||
|
auto* shared = new QUICStream(std::move(backlog.front()));
|
||||||
|
backlog.pop_front();
|
||||||
|
auto handler = cb;
|
||||||
|
ThreadPool::Enqueue([handler, shared]{
|
||||||
|
handler(std::move(*shared));
|
||||||
|
delete shared;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientQUIC::OnDatagram(std::function<void(std::vector<char>)> cb) {
|
void ClientQUIC::OnDatagram(std::function<void(std::vector<char>)> cb) {
|
||||||
|
|
|
||||||
|
|
@ -19,224 +19,228 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module;
|
module;
|
||||||
|
#include <msquic.h>
|
||||||
#include <stdio.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <netinet/in.h>
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <sys/uio.h>
|
|
||||||
#include <sys/time.h>
|
|
||||||
#include <sys/wait.h>
|
|
||||||
#include <strings.h>
|
|
||||||
#include <cerrno>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
module Crafter.Network:ListenerHTTP_impl;
|
module Crafter.Network:ListenerHTTP_impl;
|
||||||
import :ListenerHTTP;
|
import :ListenerHTTP;
|
||||||
import :ClientTCP;
|
import :ListenerQUIC;
|
||||||
import std;
|
import :ClientQUIC;
|
||||||
|
import :HTTP;
|
||||||
|
import :HTTP3;
|
||||||
import Crafter.Thread;
|
import Crafter.Thread;
|
||||||
|
import std;
|
||||||
|
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
ListenerHTTP::ListenerHTTP(std::uint16_t port, std::unordered_map<std::string, std::function<std::string(const HTTPRequest&)>> routes): routes(routes) {
|
namespace {
|
||||||
sockaddr_in servAddr;
|
// Parse a complete request stream's bytes into an HTTPRequest. The stream
|
||||||
bzero((char*)&servAddr, sizeof(servAddr));
|
// is closed by the peer with FIN, so we read until close and then
|
||||||
servAddr.sin_family = AF_INET;
|
// frame-walk the bytes (HEADERS [+ DATA]*).
|
||||||
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
|
HTTPRequest ParseRequestFrames(const std::vector<char>& bytes) {
|
||||||
servAddr.sin_port = htons(port);
|
HTTPRequest request;
|
||||||
|
bool sawHeaders = false;
|
||||||
|
std::size_t pos = 0;
|
||||||
|
const auto* p = reinterpret_cast<const std::uint8_t*>(bytes.data());
|
||||||
|
std::size_t avail = bytes.size();
|
||||||
|
|
||||||
s = socket(AF_INET, SOCK_STREAM, 0);
|
while (pos < avail) {
|
||||||
if (s < 0) {
|
std::uint64_t frameType = 0, frameLen = 0;
|
||||||
throw std::runtime_error("Error establishing the server socket");
|
std::size_t cn = 0;
|
||||||
|
if (!HTTP3::DecodeVarint(p + pos, avail - pos, frameType, cn)) {
|
||||||
|
throw HTTP3::HTTP3ProtocolError("truncated frame type");
|
||||||
|
}
|
||||||
|
pos += cn;
|
||||||
|
if (!HTTP3::DecodeVarint(p + pos, avail - pos, frameLen, cn)) {
|
||||||
|
throw HTTP3::HTTP3ProtocolError("truncated frame length");
|
||||||
|
}
|
||||||
|
pos += cn;
|
||||||
|
if (pos + frameLen > avail) {
|
||||||
|
throw HTTP3::HTTP3ProtocolError("frame length runs past buffer");
|
||||||
|
}
|
||||||
|
if (frameType == HTTP3::kFrameHeaders) {
|
||||||
|
auto fields = HTTP3::DecodeFieldSection(p + pos, static_cast<std::size_t>(frameLen));
|
||||||
|
if (!sawHeaders) {
|
||||||
|
for (auto& [name, value] : fields) {
|
||||||
|
if (name == ":method") request.method = std::move(value);
|
||||||
|
else if (name == ":scheme") request.scheme = std::move(value);
|
||||||
|
else if (name == ":authority") request.authority = std::move(value);
|
||||||
|
else if (name == ":path") request.path = std::move(value);
|
||||||
|
else if (!name.empty() && name[0] == ':') {
|
||||||
|
// Unknown request pseudo-header — ignore.
|
||||||
|
} else {
|
||||||
|
request.headers.emplace(std::move(name), std::move(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sawHeaders = true;
|
||||||
|
}
|
||||||
|
} else if (frameType == HTTP3::kFrameData) {
|
||||||
|
request.body.append(reinterpret_cast<const char*>(p + pos),
|
||||||
|
static_cast<std::size_t>(frameLen));
|
||||||
|
} else {
|
||||||
|
// Skip unknown frames (RFC 9114 §9 — reserved/extension frame
|
||||||
|
// types are silently ignored).
|
||||||
|
}
|
||||||
|
pos += static_cast<std::size_t>(frameLen);
|
||||||
|
}
|
||||||
|
if (!sawHeaders) {
|
||||||
|
throw HTTP3::HTTP3ProtocolError("request stream had no HEADERS frame");
|
||||||
|
}
|
||||||
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
int opt = 1;
|
// Serialise a response into HEADERS [+ DATA] frames.
|
||||||
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
|
std::vector<std::uint8_t> SerializeResponse(const HTTPResponse& response) {
|
||||||
throw std::runtime_error("Error setting SO_REUSEADDR");
|
std::vector<std::pair<std::string, std::string>> fields;
|
||||||
|
fields.reserve(1 + response.headers.size());
|
||||||
|
fields.emplace_back(":status", response.status.empty() ? std::string("200") : response.status);
|
||||||
|
for (const auto& [name, value] : response.headers) {
|
||||||
|
std::string lower = name;
|
||||||
|
std::ranges::transform(lower, lower.begin(),
|
||||||
|
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
||||||
|
fields.emplace_back(std::move(lower), value);
|
||||||
}
|
}
|
||||||
|
auto encoded = HTTP3::EncodeFieldSection(fields);
|
||||||
|
|
||||||
int bindStatus = bind(s, (struct sockaddr*)&servAddr, sizeof(servAddr));
|
std::vector<std::uint8_t> wire;
|
||||||
if (bindStatus < 0) {
|
HTTP3::WriteFrame(wire, HTTP3::kFrameHeaders, encoded.data(), encoded.size());
|
||||||
throw std::runtime_error(std::format("Error binding the server socket: {}", std::strerror(errno)));
|
if (!response.body.empty()) {
|
||||||
|
HTTP3::WriteFrame(wire, HTTP3::kFrameData,
|
||||||
|
reinterpret_cast<const std::uint8_t*>(response.body.data()),
|
||||||
|
response.body.size());
|
||||||
}
|
}
|
||||||
|
return wire;
|
||||||
if (listen(s, 5) < 0) {
|
|
||||||
throw std::runtime_error("Error starting to listen on the server socket");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ListenerHTTP::~ListenerHTTP() {
|
// Per-peer state for an accepted connection. Holds the connection wrapper
|
||||||
if(s != -1) {
|
// and the server-side control stream alive for the lifetime of the peer.
|
||||||
Stop();
|
struct PeerState {
|
||||||
|
std::unique_ptr<ClientQUIC> quic;
|
||||||
|
QUICStream controlStream;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ListenerHTTP::Impl {
|
||||||
|
std::unique_ptr<ListenerQUIC> listener;
|
||||||
|
std::mutex peersMtx;
|
||||||
|
std::vector<std::unique_ptr<PeerState>> peers;
|
||||||
|
bool running = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
ListenerHTTP::ListenerHTTP(std::uint16_t port,
|
||||||
|
QUICServerCredentials creds,
|
||||||
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> r)
|
||||||
|
: routes(std::move(r))
|
||||||
|
, alpn(HTTP3::kAlpn)
|
||||||
|
, impl(std::make_unique<Impl>())
|
||||||
|
{
|
||||||
|
// The connect callback wires up an OnStream handler that splits unidi
|
||||||
|
// streams (control / QPACK) from bidi streams (request streams) and
|
||||||
|
// sends our own SETTINGS frame on a freshly-opened control stream.
|
||||||
|
auto onConnect = [this](ClientQUIC* peer) {
|
||||||
|
auto state = std::make_unique<PeerState>();
|
||||||
|
state->quic.reset(peer);
|
||||||
|
|
||||||
|
peer->OnStream([this](QUICStream stream) {
|
||||||
|
if (!stream.canSend) {
|
||||||
|
// Peer-initiated unidi: client's control stream + optional
|
||||||
|
// QPACK encoder/decoder streams. Drain — we honour SETTINGS
|
||||||
|
// by accepting defaults, and we don't track QPACK dynamic-
|
||||||
|
// table mutations because we don't use the dynamic table.
|
||||||
|
try {
|
||||||
|
while (true) (void)stream.RecieveSync();
|
||||||
|
} catch (...) {}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bidi stream: a request. Drive a single request/response cycle.
|
||||||
|
try {
|
||||||
|
auto raw = stream.RecieveUntilCloseSync();
|
||||||
|
HTTPRequest request = ParseRequestFrames(raw);
|
||||||
|
|
||||||
|
HTTPResponse response;
|
||||||
|
auto it = routes.find(request.path);
|
||||||
|
if (it != routes.end()) {
|
||||||
|
response = it->second(request);
|
||||||
|
} else {
|
||||||
|
response.status = "404";
|
||||||
|
response.body = "Not Found";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto wire = SerializeResponse(response);
|
||||||
|
stream.SendSync(wire.data(),
|
||||||
|
static_cast<std::uint32_t>(wire.size()),
|
||||||
|
/*finish=*/true);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
// Best-effort 500 if we can still send. Stream may already
|
||||||
|
// be closed; swallow further errors silently.
|
||||||
|
try {
|
||||||
|
HTTPResponse err;
|
||||||
|
err.status = "500";
|
||||||
|
err.body = e.what();
|
||||||
|
auto wire = SerializeResponse(err);
|
||||||
|
stream.SendSync(wire.data(),
|
||||||
|
static_cast<std::uint32_t>(wire.size()),
|
||||||
|
/*finish=*/true);
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open our outgoing control stream and write the SETTINGS prelude.
|
||||||
|
// Do this AFTER OnStream is registered so any client-initiated
|
||||||
|
// unidi stream that races in is handled. The control stream must
|
||||||
|
// remain open for the connection's lifetime — we never FIN it.
|
||||||
|
try {
|
||||||
|
state->controlStream = peer->OpenStream(/*unidirectional=*/true);
|
||||||
|
auto prelude = HTTP3::BuildControlStreamPrelude();
|
||||||
|
state->controlStream.SendSync(prelude.data(),
|
||||||
|
static_cast<std::uint32_t>(prelude.size()),
|
||||||
|
/*finish=*/false);
|
||||||
|
} catch (...) {
|
||||||
|
// If the connection died mid-handshake we land here; the peer
|
||||||
|
// gets dropped via destruction below.
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard lk(impl->peersMtx);
|
||||||
|
impl->peers.push_back(std::move(state));
|
||||||
|
};
|
||||||
|
|
||||||
|
impl->listener = std::make_unique<ListenerQUIC>(port,
|
||||||
|
std::string(HTTP3::kAlpn),
|
||||||
|
std::move(creds),
|
||||||
|
onConnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
ListenerHTTP::ListenerHTTP(ListenerHTTP&&) noexcept = default;
|
||||||
|
|
||||||
|
ListenerHTTP::~ListenerHTTP() {
|
||||||
|
if (impl) Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ListenerHTTP::Stop() {
|
void ListenerHTTP::Stop() {
|
||||||
running = false;
|
if (!impl) return;
|
||||||
shutdown(s, SHUT_RDWR);
|
impl->running = false;
|
||||||
close(s);
|
if (impl->listener) impl->listener->Stop();
|
||||||
s = -1;
|
|
||||||
for(ListenerHTTPClient* client : clients) {
|
|
||||||
client->client.Stop();
|
|
||||||
client->thread.join();
|
|
||||||
delete client;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ListenerHTTP::Listen() {
|
void ListenerHTTP::Listen() {
|
||||||
while(running) {
|
if (!impl || !impl->listener) return;
|
||||||
sockaddr_in newSockAddr;
|
// ListenSyncAsync runs the accept loop on this thread and dispatches the
|
||||||
socklen_t newSockAddrSize = sizeof(newSockAddr);
|
// per-connection callback (control-stream open + OnStream wiring) on the
|
||||||
int client = accept(s, (sockaddr*)&newSockAddr, &newSockAddrSize);
|
// ThreadPool. That keeps route handlers off the accept thread.
|
||||||
if(!running) {
|
impl->listener->ListenSyncAsync();
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (client > 0) {
|
|
||||||
clients.push_back(new ListenerHTTPClient(this, client));
|
|
||||||
} else {
|
|
||||||
std::cerr << "Error accepting request from client!" << std::endl;
|
|
||||||
}
|
|
||||||
std::erase_if(clients, [](ListenerHTTPClient* client) {
|
|
||||||
if (client->disconnected.load()) {
|
|
||||||
client->thread.join();
|
|
||||||
delete client;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ListenerHTTPClient::ListenerHTTPClient(ListenerHTTP* server, int s) : server(server), client(s), thread(&ListenerHTTPClient::ListenRoutes, this), disconnected(false) {
|
ListenerAsyncHTTP::ListenerAsyncHTTP(std::uint16_t port,
|
||||||
|
QUICServerCredentials creds,
|
||||||
}
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes)
|
||||||
|
: listener(port, std::move(creds), std::move(routes))
|
||||||
void ListenerHTTPClient::ListenRoutes() {
|
, thread(&ListenerHTTP::Listen, &listener)
|
||||||
try {
|
{}
|
||||||
while(true) {
|
|
||||||
std::vector<char> buffer;
|
|
||||||
HTTPRequest request;
|
|
||||||
std::string route;
|
|
||||||
std::uint32_t i = 0;
|
|
||||||
std::uint32_t routeStart = 0;
|
|
||||||
while(true) {
|
|
||||||
buffer = client.RecieveSync();
|
|
||||||
while(true) {
|
|
||||||
std::string str(buffer.begin(), buffer.end());
|
|
||||||
for(; i < buffer.size(); i++) {
|
|
||||||
if(buffer[i] == ' ') {
|
|
||||||
request.method.assign(buffer.data(), i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(; i < buffer.size(); i++) {
|
|
||||||
if(buffer[i] == '/') {
|
|
||||||
routeStart = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(; i < buffer.size(); i++) {
|
|
||||||
if(buffer[i] == ' ') {
|
|
||||||
route.assign(buffer.data()+routeStart, i-routeStart);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(; i < buffer.size(); i++) {
|
|
||||||
if(buffer[i] == '\r' && buffer[i+1] == '\n') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i+=2;
|
|
||||||
while(i < buffer.size()) {
|
|
||||||
std::uint32_t headerStart = i;
|
|
||||||
std::string headerName;
|
|
||||||
for(; i < buffer.size(); i++) {
|
|
||||||
if(buffer[i] == ':') {
|
|
||||||
headerName.assign(buffer.data()+headerStart, i-headerStart);
|
|
||||||
std::transform(headerName.begin(), headerName.end(), headerName.begin(), [](unsigned char c){ return std::tolower(c); });
|
|
||||||
i++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
headerStart = i;
|
|
||||||
std::string headerValue;
|
|
||||||
for(; i < buffer.size(); i++) {
|
|
||||||
if(buffer[i] == '\r' && buffer[i+1] == '\n') {
|
|
||||||
headerValue.assign(buffer.data()+headerStart, i-headerStart);
|
|
||||||
request.headers.insert({headerName, headerValue});
|
|
||||||
if(buffer[i+2] == '\r'){
|
|
||||||
goto headersComplete;
|
|
||||||
} else{
|
|
||||||
i+=2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i = 0;
|
|
||||||
}
|
|
||||||
headersComplete:;
|
|
||||||
i+=4;
|
|
||||||
std::unordered_map<std::string, std::string>::iterator it = request.headers.find("content-length");
|
|
||||||
if(it != request.headers.end()) {
|
|
||||||
const int lenght = std::stoi(it->second);
|
|
||||||
request.body.resize(lenght, 0);
|
|
||||||
if(lenght > 0 ){
|
|
||||||
std::int_fast32_t remaining = lenght-(buffer.size()-i);
|
|
||||||
if(remaining < 0) {
|
|
||||||
std::memcpy(&request.body[0], buffer.data()+i, lenght);
|
|
||||||
std::string response = server->routes.at(route)(request);
|
|
||||||
client.Send(&response[0], response.size());
|
|
||||||
i+=lenght;
|
|
||||||
} else if(remaining == 0){
|
|
||||||
std::memcpy(&request.body[0], buffer.data()+i, lenght);
|
|
||||||
std::string response = server->routes.at(route)(request);
|
|
||||||
client.Send(&response[0], response.size());
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
std::memcpy(&request.body[0], buffer.data()+i, buffer.size()-i);
|
|
||||||
std::vector<char> bodyBuffer = client.RecieveUntilFullSync(remaining);
|
|
||||||
std::memcpy(&request.body[buffer.size()-i], bodyBuffer.data(), remaining);
|
|
||||||
std::string response = server->routes.at(route)(request);
|
|
||||||
client.Send(&response[0], response.size());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
std::string response = server->routes.at(route)(request);
|
|
||||||
client.Send(&response[0], response.size());
|
|
||||||
if(i == buffer.size()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
std::string response = server->routes.at(route)(request);
|
|
||||||
client.Send(&response[0], response.size());
|
|
||||||
if(i == buffer.size()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(SocketClosedException& e) {
|
|
||||||
disconnected.store(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ListenerAsyncHTTP::ListenerAsyncHTTP(std::uint16_t port, std::unordered_map<std::string, std::function<std::string(const HTTPRequest&)>> routes): listener(port, routes), thread(&ListenerHTTP::Listen, &listener) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ListenerAsyncHTTP::~ListenerAsyncHTTP() {
|
ListenerAsyncHTTP::~ListenerAsyncHTTP() {
|
||||||
if(listener.s != -1) {
|
|
||||||
Stop();
|
Stop();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ListenerAsyncHTTP::Stop() {
|
void ListenerAsyncHTTP::Stop() {
|
||||||
listener.Stop();
|
listener.Stop();
|
||||||
thread.join();
|
if (thread.joinable()) thread.join();
|
||||||
}
|
}
|
||||||
|
|
@ -20,19 +20,35 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
export module Crafter.Network:ClientHTTP;
|
export module Crafter.Network:ClientHTTP;
|
||||||
import std;
|
import std;
|
||||||
import :ClientTCP;
|
|
||||||
import :HTTP;
|
import :HTTP;
|
||||||
|
import :ClientQUIC;
|
||||||
|
|
||||||
namespace Crafter {
|
namespace Crafter {
|
||||||
|
// HTTP/3 client over QUIC. The constructor establishes the QUIC connection
|
||||||
|
// (TLS handshake + ALPN "h3"); each Send() opens a fresh request stream
|
||||||
|
// on the multiplexed connection. Thread-affinity: Send() is not safe to
|
||||||
|
// call from multiple threads concurrently against the same ClientHTTP,
|
||||||
|
// but distinct ClientHTTP instances are independent.
|
||||||
|
//
|
||||||
|
// For local development against a self-signed listener, pass
|
||||||
|
// QUICClientCredentials{insecureNoServerValidation = true}.
|
||||||
export class ClientHTTP {
|
export class ClientHTTP {
|
||||||
public:
|
public:
|
||||||
std::string host;
|
std::string host;
|
||||||
std::uint16_t port;
|
std::uint16_t port;
|
||||||
ClientHTTP(const char* host, std::uint16_t port);
|
|
||||||
ClientHTTP(std::string host, std::uint16_t port);
|
ClientHTTP(const char* host, std::uint16_t port, QUICClientCredentials creds = {});
|
||||||
HTTPResponse Send(const char* request, std::uint32_t length);
|
ClientHTTP(std::string host, std::uint16_t port, QUICClientCredentials creds = {});
|
||||||
HTTPResponse Send(std::string request);
|
|
||||||
|
~ClientHTTP();
|
||||||
|
ClientHTTP(const ClientHTTP&) = delete;
|
||||||
|
ClientHTTP(ClientHTTP&&) noexcept;
|
||||||
|
|
||||||
|
// Send a request and synchronously read back the full response.
|
||||||
|
HTTPResponse Send(const HTTPRequest& request);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ClientTCP client;
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -52,10 +52,12 @@ namespace Crafter {
|
||||||
|
|
||||||
export class ClientQUIC;
|
export class ClientQUIC;
|
||||||
|
|
||||||
// A reliable, ordered, bidirectional stream within a QUIC connection.
|
// A reliable, ordered stream within a QUIC connection. May be
|
||||||
// Owned by ClientQUIC; do not destroy directly. Obtain via
|
// bidirectional or unidirectional; for unidi streams either canSend or
|
||||||
// ClientQUIC::OpenStream() or via the on-stream callback for inbound
|
// canReceive will be false depending on which side initiated. Owned by
|
||||||
// streams initiated by the peer.
|
// ClientQUIC; do not destroy directly. Obtain via ClientQUIC::OpenStream
|
||||||
|
// (optionally with unidirectional=true) or via the on-stream callback
|
||||||
|
// for inbound streams initiated by the peer.
|
||||||
export class QUICStream {
|
export class QUICStream {
|
||||||
public:
|
public:
|
||||||
// Underlying msquic HQUIC handle. Treated as opaque by callers.
|
// Underlying msquic HQUIC handle. Treated as opaque by callers.
|
||||||
|
|
@ -64,7 +66,12 @@ namespace Crafter {
|
||||||
// The connection that owns this stream (non-owning).
|
// The connection that owns this stream (non-owning).
|
||||||
ClientQUIC* connection = nullptr;
|
ClientQUIC* connection = nullptr;
|
||||||
|
|
||||||
QUICStream() = default;
|
// Direction flags. Bidi streams have both true; outgoing unidi sets
|
||||||
|
// canReceive=false; incoming unidi (peer-initiated) sets canSend=false.
|
||||||
|
bool canSend = true;
|
||||||
|
bool canReceive = true;
|
||||||
|
|
||||||
|
QUICStream();
|
||||||
QUICStream(HQUIC handle, ClientQUIC* connection);
|
QUICStream(HQUIC handle, ClientQUIC* connection);
|
||||||
~QUICStream();
|
~QUICStream();
|
||||||
QUICStream(const QUICStream&) = delete;
|
QUICStream(const QUICStream&) = delete;
|
||||||
|
|
@ -135,9 +142,11 @@ namespace Crafter {
|
||||||
ClientQUIC(const ClientQUIC&) = delete;
|
ClientQUIC(const ClientQUIC&) = delete;
|
||||||
ClientQUIC(ClientQUIC&&) noexcept;
|
ClientQUIC(ClientQUIC&&) noexcept;
|
||||||
|
|
||||||
// Open a new bidirectional stream initiated by this side.
|
// Open a new stream initiated by this side. Defaults to bidirectional;
|
||||||
|
// pass unidirectional=true to open a one-way send stream (used for
|
||||||
|
// HTTP/3's control + QPACK encoder/decoder streams).
|
||||||
// Blocks until the stream is started; throws on failure.
|
// Blocks until the stream is started; throws on failure.
|
||||||
QUICStream OpenStream();
|
QUICStream OpenStream(bool unidirectional = false);
|
||||||
|
|
||||||
// Send a datagram. Best-effort: may be silently dropped under loss
|
// Send a datagram. Best-effort: may be silently dropped under loss
|
||||||
// or congestion. Size must fit within the path MTU (msquic surfaces
|
// or congestion. Size must fit within the path MTU (msquic surfaces
|
||||||
|
|
|
||||||
|
|
@ -22,63 +22,98 @@ export module Crafter.Network:HTTP;
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
namespace Crafter {
|
namespace Crafter {
|
||||||
|
// HTTP/3 request as carried over a QUIC bidirectional stream. The four
|
||||||
|
// pseudo-headers (method/scheme/authority/path) are split out as named
|
||||||
|
// fields rather than living in the headers map, because RFC 9114 forbids
|
||||||
|
// them from appearing in the regular header section and this shape makes
|
||||||
|
// route dispatch and request construction cleaner. `headers` keys are
|
||||||
|
// expected lowercase; HTTP/3 forbids uppercase characters in field names.
|
||||||
export struct HTTPRequest {
|
export struct HTTPRequest {
|
||||||
std::string method;
|
std::string method;
|
||||||
|
std::string scheme = "https";
|
||||||
|
std::string authority;
|
||||||
|
std::string path = "/";
|
||||||
std::unordered_map<std::string, std::string> headers;
|
std::unordered_map<std::string, std::string> headers;
|
||||||
std::string body;
|
std::string body;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// HTTP/3 response. `status` is the numeric three-digit code as a string
|
||||||
|
// (e.g. "200") — HTTP/3 has no reason phrase. `headers` keys are expected
|
||||||
|
// lowercase.
|
||||||
export struct HTTPResponse {
|
export struct HTTPResponse {
|
||||||
std::string status;
|
std::string status = "200";
|
||||||
std::unordered_map<std::string, std::string> headers;
|
std::unordered_map<std::string, std::string> headers;
|
||||||
std::string body;
|
std::string body;
|
||||||
};
|
};
|
||||||
|
|
||||||
export constexpr std::string CreateResponseHTTP(std::string status) {
|
export inline HTTPRequest CreateRequestHTTP(std::string method, std::string path, std::string authority) {
|
||||||
return std::format("HTTP/1.1 {}\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n", status);
|
HTTPRequest r;
|
||||||
|
r.method = std::move(method);
|
||||||
|
r.path = std::move(path);
|
||||||
|
r.authority = std::move(authority);
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
export constexpr std::string CreateResponseHTTP(std::string status, std::unordered_map<std::string, std::string> headers) {
|
export inline HTTPRequest CreateRequestHTTP(std::string method, std::string path, std::string authority,
|
||||||
std::string headersString;
|
std::unordered_map<std::string, std::string> headers) {
|
||||||
for (auto const& [key, val] : headers) {
|
HTTPRequest r;
|
||||||
headersString+=std::format("{}: {}\r\n", key, val);
|
r.method = std::move(method);
|
||||||
}
|
r.path = std::move(path);
|
||||||
return std::format("HTTP/1.1 {}\r\nConnection: keep-alive\r\nContent-Length: 0\r\n{}\r\n", status, headersString);
|
r.authority = std::move(authority);
|
||||||
|
r.headers = std::move(headers);
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
export constexpr std::string CreateResponseHTTP(std::string status, std::string body) {
|
export inline HTTPRequest CreateRequestHTTP(std::string method, std::string path, std::string authority,
|
||||||
return std::format("HTTP/1.1 {}\r\nContent-Length: {}\r\nConnection: keep-alive\r\n\r\n{}", status, body.size(), body);
|
std::string body) {
|
||||||
|
HTTPRequest r;
|
||||||
|
r.method = std::move(method);
|
||||||
|
r.path = std::move(path);
|
||||||
|
r.authority = std::move(authority);
|
||||||
|
r.body = std::move(body);
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
export constexpr std::string CreateResponseHTTP(std::string status, std::unordered_map<std::string, std::string> headers, std::string body) {
|
export inline HTTPRequest CreateRequestHTTP(std::string method, std::string path, std::string authority,
|
||||||
std::string headersString;
|
std::unordered_map<std::string, std::string> headers,
|
||||||
for (auto const& [key, val] : headers) {
|
std::string body) {
|
||||||
headersString+=std::format("{}: {}\r\n", key, val);
|
HTTPRequest r;
|
||||||
}
|
r.method = std::move(method);
|
||||||
return std::format("HTTP/1.1 {}\r\nConnection: keep-alive\r\nContent-Length: {}\r\n{}\r\n{}", status, body.size(), headersString, body);
|
r.path = std::move(path);
|
||||||
|
r.authority = std::move(authority);
|
||||||
|
r.headers = std::move(headers);
|
||||||
|
r.body = std::move(body);
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
export constexpr std::string CreateRequestHTTP(std::string method, std::string route, std::string host) {
|
export inline HTTPResponse CreateResponseHTTP(std::string status) {
|
||||||
return std::format("{} {} HTTP/1.1\r\nConnection: keep-alive\r\nAccept-Encoding: identity\r\nContent-Length: 0\r\nHost: {}\r\n\r\n", method, route, host);
|
HTTPResponse r;
|
||||||
|
r.status = std::move(status);
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
export constexpr std::string CreateRequestHTTP(std::string method, std::string route, std::string host, std::unordered_map<std::string, std::string> headers) {
|
export inline HTTPResponse CreateResponseHTTP(std::string status,
|
||||||
std::string headersString;
|
std::unordered_map<std::string, std::string> headers) {
|
||||||
for (auto const& [key, val] : headers) {
|
HTTPResponse r;
|
||||||
headersString+=std::format("{}: {}\r\n", key, val);
|
r.status = std::move(status);
|
||||||
}
|
r.headers = std::move(headers);
|
||||||
return std::format("{} {} HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\nAccept-Encoding: identity\r\nHost: {}\r\n{}\r\n", method, route, host, headersString);
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
export constexpr std::string CreateRequestHTTP(std::string method, std::string route, std::string host, std::string body) {
|
export inline HTTPResponse CreateResponseHTTP(std::string status, std::string body) {
|
||||||
return std::format("{} {} HTTP/1.1\r\nContent-Length: {}\r\nConnection: keep-alive\r\nAccept-Encoding: identity\r\nHost: {}\r\n\r\n{}", method, route, body.size(), host, body);
|
HTTPResponse r;
|
||||||
|
r.status = std::move(status);
|
||||||
|
r.body = std::move(body);
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
export constexpr std::string CreateRequestHTTP(std::string method, std::string route, std::string host, std::unordered_map<std::string, std::string> headers, std::string body) {
|
export inline HTTPResponse CreateResponseHTTP(std::string status,
|
||||||
std::string headersString;
|
std::unordered_map<std::string, std::string> headers,
|
||||||
for (auto const& [key, val] : headers) {
|
std::string body) {
|
||||||
headersString+=std::format("{}: {}\r\n", key, val);
|
HTTPResponse r;
|
||||||
}
|
r.status = std::move(status);
|
||||||
return std::format("{} {} HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: {}\r\nHost: {}\r\nAccept-Encoding: identity\r\n{}\r\n{}", method, route, body.size(), host, headersString, body);
|
r.headers = std::move(headers);
|
||||||
|
r.body = std::move(body);
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
578
interfaces/Crafter.Network-HTTP3.cppm
Normal file
578
interfaces/Crafter.Network-HTTP3.cppm
Normal file
|
|
@ -0,0 +1,578 @@
|
||||||
|
/*
|
||||||
|
Crafter®.Network
|
||||||
|
Copyright (C) 2026 Catcrafts®
|
||||||
|
Catcrafts.net
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 3.0 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
// HTTP/3 wire-format helpers. This partition is intentionally NOT re-exported
|
||||||
|
// from Crafter.Network — it's an internal building block shared between the
|
||||||
|
// ClientHTTP and ListenerHTTP implementation files.
|
||||||
|
//
|
||||||
|
// Scope:
|
||||||
|
// - QUIC variable-length integers (RFC 9000 §16)
|
||||||
|
// - HTTP/3 frame type/length codec (RFC 9114 §7)
|
||||||
|
// - QPACK field-section encode/decode (RFC 9204) limited to the static
|
||||||
|
// table + literal representations. No dynamic table, no Huffman. This
|
||||||
|
// suffices because both peers in this library are this same library;
|
||||||
|
// interoperability with browsers/curl over h3 would additionally require
|
||||||
|
// a Huffman codec which is out of scope here.
|
||||||
|
|
||||||
|
export module Crafter.Network:HTTP3;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
namespace Crafter::HTTP3 {
|
||||||
|
// ---------------- ALPN ----------------
|
||||||
|
// RFC 9114 §3.1 — final h3 ALPN identifier.
|
||||||
|
export inline constexpr std::string_view kAlpn = "h3";
|
||||||
|
|
||||||
|
// ---------------- Frame types (RFC 9114 §7.2) ----------------
|
||||||
|
export inline constexpr std::uint64_t kFrameData = 0x00;
|
||||||
|
export inline constexpr std::uint64_t kFrameHeaders = 0x01;
|
||||||
|
export inline constexpr std::uint64_t kFrameSettings = 0x04;
|
||||||
|
|
||||||
|
// ---------------- Unidirectional stream types (RFC 9114 §6.2) ----------------
|
||||||
|
export inline constexpr std::uint64_t kStreamControl = 0x00;
|
||||||
|
export inline constexpr std::uint64_t kStreamQpackEnc = 0x02;
|
||||||
|
export inline constexpr std::uint64_t kStreamQpackDec = 0x03;
|
||||||
|
|
||||||
|
// ---------------- Errors ----------------
|
||||||
|
export class HTTP3ProtocolError : public std::runtime_error {
|
||||||
|
public:
|
||||||
|
using std::runtime_error::runtime_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------- QUIC varint (RFC 9000 §16) ----------------
|
||||||
|
// Encodes value into the smallest of {1, 2, 4, 8}-byte forms.
|
||||||
|
export inline void EncodeVarint(std::uint64_t value, std::vector<std::uint8_t>& out) {
|
||||||
|
if (value < (1ULL << 6)) {
|
||||||
|
out.push_back(static_cast<std::uint8_t>(value));
|
||||||
|
} else if (value < (1ULL << 14)) {
|
||||||
|
out.push_back(static_cast<std::uint8_t>(0x40 | (value >> 8)));
|
||||||
|
out.push_back(static_cast<std::uint8_t>(value & 0xFF));
|
||||||
|
} else if (value < (1ULL << 30)) {
|
||||||
|
out.push_back(static_cast<std::uint8_t>(0x80 | ((value >> 24) & 0x3F)));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((value >> 16) & 0xFF));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((value >> 8) & 0xFF));
|
||||||
|
out.push_back(static_cast<std::uint8_t>(value & 0xFF));
|
||||||
|
} else if (value < (1ULL << 62)) {
|
||||||
|
out.push_back(static_cast<std::uint8_t>(0xC0 | ((value >> 56) & 0x3F)));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((value >> 48) & 0xFF));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((value >> 40) & 0xFF));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((value >> 32) & 0xFF));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((value >> 24) & 0xFF));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((value >> 16) & 0xFF));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((value >> 8) & 0xFF));
|
||||||
|
out.push_back(static_cast<std::uint8_t>(value & 0xFF));
|
||||||
|
} else {
|
||||||
|
throw HTTP3ProtocolError("varint value exceeds 2^62-1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true on success. On false, no consumed/value mutation observed.
|
||||||
|
export inline bool DecodeVarint(const std::uint8_t* data, std::size_t available,
|
||||||
|
std::uint64_t& value, std::size_t& consumed) {
|
||||||
|
if (available == 0) return false;
|
||||||
|
std::uint8_t first = data[0];
|
||||||
|
std::size_t len = std::size_t{1} << (first >> 6);
|
||||||
|
if (available < len) return false;
|
||||||
|
std::uint64_t v = first & 0x3F;
|
||||||
|
for (std::size_t i = 1; i < len; ++i) {
|
||||||
|
v = (v << 8) | data[i];
|
||||||
|
}
|
||||||
|
value = v;
|
||||||
|
consumed = len;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- QPACK / HPACK-style integer (RFC 7541 §5.1) ----------------
|
||||||
|
// Different beast from QUIC varint. N-bit prefix integer used inside QPACK
|
||||||
|
// representations; the high (8-N) bits of the first byte carry pattern flags.
|
||||||
|
export inline void EncodeQpackInt(std::vector<std::uint8_t>& out, std::uint8_t topBits,
|
||||||
|
int N, std::uint64_t value) {
|
||||||
|
std::uint8_t mask = static_cast<std::uint8_t>((1U << N) - 1);
|
||||||
|
if (value < mask) {
|
||||||
|
out.push_back(static_cast<std::uint8_t>(topBits | value));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.push_back(static_cast<std::uint8_t>(topBits | mask));
|
||||||
|
value -= mask;
|
||||||
|
while (value >= 128) {
|
||||||
|
out.push_back(static_cast<std::uint8_t>((value & 0x7F) | 0x80));
|
||||||
|
value >>= 7;
|
||||||
|
}
|
||||||
|
out.push_back(static_cast<std::uint8_t>(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode N-bit-prefix integer. data[0] holds prefix flags; the low N bits
|
||||||
|
// contribute to the value (continuation bytes follow if low N bits == mask).
|
||||||
|
export inline bool DecodeQpackInt(const std::uint8_t* data, std::size_t available,
|
||||||
|
int N, std::uint64_t& value, std::size_t& consumed) {
|
||||||
|
if (available == 0) return false;
|
||||||
|
std::uint8_t mask = static_cast<std::uint8_t>((1U << N) - 1);
|
||||||
|
std::uint8_t first = data[0] & mask;
|
||||||
|
if (first < mask) {
|
||||||
|
value = first;
|
||||||
|
consumed = 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::uint64_t v = mask;
|
||||||
|
int shift = 0;
|
||||||
|
std::size_t i = 1;
|
||||||
|
while (true) {
|
||||||
|
if (i >= available) return false;
|
||||||
|
std::uint8_t b = data[i++];
|
||||||
|
v += static_cast<std::uint64_t>(b & 0x7F) << shift;
|
||||||
|
if ((b & 0x80) == 0) break;
|
||||||
|
shift += 7;
|
||||||
|
if (shift > 63) return false;
|
||||||
|
}
|
||||||
|
value = v;
|
||||||
|
consumed = i;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Huffman codec (RFC 7541 Appendix B) ----------------
|
||||||
|
// Decode-only. Real h3 peers (browsers, curl, cloudflare, etc.) Huffman-
|
||||||
|
// encode header values by default, so without this any external interop
|
||||||
|
// collapses on the first response. We don't emit Huffman ourselves —
|
||||||
|
// peers MUST accept H=0 literal per the spec, so encoding doesn't add
|
||||||
|
// interop value, only wire compactness. The 256-entry table plus a
|
||||||
|
// straightforward bit-walking decoder is the smallest viable form.
|
||||||
|
struct HuffmanCode {
|
||||||
|
std::uint32_t code;
|
||||||
|
std::uint8_t length;
|
||||||
|
};
|
||||||
|
inline constexpr std::array<HuffmanCode, 256> kHuffmanTable = {{
|
||||||
|
/* 0 */ {0x1ff8, 13}, /* 1 */ {0x7fffd8, 23}, /* 2 */ {0xfffffe2, 28},
|
||||||
|
/* 3 */ {0xfffffe3, 28}, /* 4 */ {0xfffffe4, 28}, /* 5 */ {0xfffffe5, 28},
|
||||||
|
/* 6 */ {0xfffffe6, 28}, /* 7 */ {0xfffffe7, 28}, /* 8 */ {0xfffffe8, 28},
|
||||||
|
/* 9 */ {0xffffea, 24}, /* 10 */ {0x3ffffffc,30}, /* 11 */ {0xfffffe9, 28},
|
||||||
|
/* 12 */ {0xfffffea, 28}, /* 13 */ {0x3ffffffd,30}, /* 14 */ {0xfffffeb, 28},
|
||||||
|
/* 15 */ {0xfffffec, 28}, /* 16 */ {0xfffffed, 28}, /* 17 */ {0xfffffee, 28},
|
||||||
|
/* 18 */ {0xfffffef, 28}, /* 19 */ {0xffffff0, 28}, /* 20 */ {0xffffff1, 28},
|
||||||
|
/* 21 */ {0xffffff2, 28}, /* 22 */ {0x3ffffffe,30}, /* 23 */ {0xffffff3, 28},
|
||||||
|
/* 24 */ {0xffffff4, 28}, /* 25 */ {0xffffff5, 28}, /* 26 */ {0xffffff6, 28},
|
||||||
|
/* 27 */ {0xffffff7, 28}, /* 28 */ {0xffffff8, 28}, /* 29 */ {0xffffff9, 28},
|
||||||
|
/* 30 */ {0xffffffa, 28}, /* 31 */ {0xffffffb, 28}, /* 32 */ {0x14, 6},
|
||||||
|
/* 33 */ {0x3f8, 10}, /* 34 */ {0x3f9, 10}, /* 35 */ {0xffa, 12},
|
||||||
|
/* 36 */ {0x1ff9, 13}, /* 37 */ {0x15, 6}, /* 38 */ {0xf8, 8},
|
||||||
|
/* 39 */ {0x7fa, 11}, /* 40 */ {0x3fa, 10}, /* 41 */ {0x3fb, 10},
|
||||||
|
/* 42 */ {0xf9, 8}, /* 43 */ {0x7fb, 11}, /* 44 */ {0xfa, 8},
|
||||||
|
/* 45 */ {0x16, 6}, /* 46 */ {0x17, 6}, /* 47 */ {0x18, 6},
|
||||||
|
/* 48 */ {0x0, 5}, /* 49 */ {0x1, 5}, /* 50 */ {0x2, 5},
|
||||||
|
/* 51 */ {0x19, 6}, /* 52 */ {0x1a, 6}, /* 53 */ {0x1b, 6},
|
||||||
|
/* 54 */ {0x1c, 6}, /* 55 */ {0x1d, 6}, /* 56 */ {0x1e, 6},
|
||||||
|
/* 57 */ {0x1f, 6}, /* 58 */ {0x5c, 7}, /* 59 */ {0xfb, 8},
|
||||||
|
/* 60 */ {0x7ffc, 15}, /* 61 */ {0x20, 6}, /* 62 */ {0xffb, 12},
|
||||||
|
/* 63 */ {0x3fc, 10}, /* 64 */ {0x1ffa, 13}, /* 65 */ {0x21, 6},
|
||||||
|
/* 66 */ {0x5d, 7}, /* 67 */ {0x5e, 7}, /* 68 */ {0x5f, 7},
|
||||||
|
/* 69 */ {0x60, 7}, /* 70 */ {0x61, 7}, /* 71 */ {0x62, 7},
|
||||||
|
/* 72 */ {0x63, 7}, /* 73 */ {0x64, 7}, /* 74 */ {0x65, 7},
|
||||||
|
/* 75 */ {0x66, 7}, /* 76 */ {0x67, 7}, /* 77 */ {0x68, 7},
|
||||||
|
/* 78 */ {0x69, 7}, /* 79 */ {0x6a, 7}, /* 80 */ {0x6b, 7},
|
||||||
|
/* 81 */ {0x6c, 7}, /* 82 */ {0x6d, 7}, /* 83 */ {0x6e, 7},
|
||||||
|
/* 84 */ {0x6f, 7}, /* 85 */ {0x70, 7}, /* 86 */ {0x71, 7},
|
||||||
|
/* 87 */ {0x72, 7}, /* 88 */ {0xfc, 8}, /* 89 */ {0x73, 7},
|
||||||
|
/* 90 */ {0xfd, 8}, /* 91 */ {0x1ffb, 13}, /* 92 */ {0x7fff0, 19},
|
||||||
|
/* 93 */ {0x1ffc, 13}, /* 94 */ {0x3ffc, 14}, /* 95 */ {0x22, 6},
|
||||||
|
/* 96 */ {0x7ffd, 15}, /* 97 */ {0x3, 5}, /* 98 */ {0x23, 6},
|
||||||
|
/* 99 */ {0x4, 5}, /*100 */ {0x24, 6}, /*101 */ {0x5, 5},
|
||||||
|
/*102 */ {0x25, 6}, /*103 */ {0x26, 6}, /*104 */ {0x27, 6},
|
||||||
|
/*105 */ {0x6, 5}, /*106 */ {0x74, 7}, /*107 */ {0x75, 7},
|
||||||
|
/*108 */ {0x28, 6}, /*109 */ {0x29, 6}, /*110 */ {0x2a, 6},
|
||||||
|
/*111 */ {0x7, 5}, /*112 */ {0x2b, 6}, /*113 */ {0x76, 7},
|
||||||
|
/*114 */ {0x2c, 6}, /*115 */ {0x8, 5}, /*116 */ {0x9, 5},
|
||||||
|
/*117 */ {0x2d, 6}, /*118 */ {0x77, 7}, /*119 */ {0x78, 7},
|
||||||
|
/*120 */ {0x79, 7}, /*121 */ {0x7a, 7}, /*122 */ {0x7b, 7},
|
||||||
|
/*123 */ {0x7ffe, 15}, /*124 */ {0x7fc, 11}, /*125 */ {0x3ffd, 14},
|
||||||
|
/*126 */ {0x1ffd, 13}, /*127 */ {0xffffffc, 28}, /*128 */ {0xfffe6, 20},
|
||||||
|
/*129 */ {0x3fffd2, 22}, /*130 */ {0xfffe7, 20}, /*131 */ {0xfffe8, 20},
|
||||||
|
/*132 */ {0x3fffd3, 22}, /*133 */ {0x3fffd4, 22}, /*134 */ {0x3fffd5, 22},
|
||||||
|
/*135 */ {0x7fffd9, 23}, /*136 */ {0x3fffd6, 22}, /*137 */ {0x7fffda, 23},
|
||||||
|
/*138 */ {0x7fffdb, 23}, /*139 */ {0x7fffdc, 23}, /*140 */ {0x7fffdd, 23},
|
||||||
|
/*141 */ {0x7fffde, 23}, /*142 */ {0xffffeb, 24}, /*143 */ {0x7fffdf, 23},
|
||||||
|
/*144 */ {0xffffec, 24}, /*145 */ {0xffffed, 24}, /*146 */ {0x3fffd7, 22},
|
||||||
|
/*147 */ {0x7fffe0, 23}, /*148 */ {0xffffee, 24}, /*149 */ {0x7fffe1, 23},
|
||||||
|
/*150 */ {0x7fffe2, 23}, /*151 */ {0x7fffe3, 23}, /*152 */ {0x7fffe4, 23},
|
||||||
|
/*153 */ {0x1fffdc, 21}, /*154 */ {0x3fffd8, 22}, /*155 */ {0x7fffe5, 23},
|
||||||
|
/*156 */ {0x3fffd9, 22}, /*157 */ {0x7fffe6, 23}, /*158 */ {0x7fffe7, 23},
|
||||||
|
/*159 */ {0xffffef, 24}, /*160 */ {0x3fffda, 22}, /*161 */ {0x1fffdd, 21},
|
||||||
|
/*162 */ {0xfffe9, 20}, /*163 */ {0x3fffdb, 22}, /*164 */ {0x3fffdc, 22},
|
||||||
|
/*165 */ {0x7fffe8, 23}, /*166 */ {0x7fffe9, 23}, /*167 */ {0x1fffde, 21},
|
||||||
|
/*168 */ {0x7fffea, 23}, /*169 */ {0x3fffdd, 22}, /*170 */ {0x3fffde, 22},
|
||||||
|
/*171 */ {0xfffff0, 24}, /*172 */ {0x1fffdf, 21}, /*173 */ {0x3fffdf, 22},
|
||||||
|
/*174 */ {0x7fffeb, 23}, /*175 */ {0x7fffec, 23}, /*176 */ {0x1fffe0, 21},
|
||||||
|
/*177 */ {0x1fffe1, 21}, /*178 */ {0x3fffe0, 22}, /*179 */ {0x1fffe2, 21},
|
||||||
|
/*180 */ {0x7fffed, 23}, /*181 */ {0x3fffe1, 22}, /*182 */ {0x7fffee, 23},
|
||||||
|
/*183 */ {0x7fffef, 23}, /*184 */ {0xfffea, 20}, /*185 */ {0x3fffe2, 22},
|
||||||
|
/*186 */ {0x3fffe3, 22}, /*187 */ {0x3fffe4, 22}, /*188 */ {0x7ffff0, 23},
|
||||||
|
/*189 */ {0x3fffe5, 22}, /*190 */ {0x3fffe6, 22}, /*191 */ {0x7ffff1, 23},
|
||||||
|
/*192 */ {0x3ffffe0,26}, /*193 */ {0x3ffffe1, 26}, /*194 */ {0xfffeb, 20},
|
||||||
|
/*195 */ {0x7fff1, 19}, /*196 */ {0x3fffe7, 22}, /*197 */ {0x7ffff2, 23},
|
||||||
|
/*198 */ {0x3fffe8, 22}, /*199 */ {0x1ffffec, 25}, /*200 */ {0x3ffffe2, 26},
|
||||||
|
/*201 */ {0x3ffffe3,26}, /*202 */ {0x3ffffe4, 26}, /*203 */ {0x7ffffde, 27},
|
||||||
|
/*204 */ {0x7ffffdf,27}, /*205 */ {0x3ffffe5, 26}, /*206 */ {0xfffff1, 24},
|
||||||
|
/*207 */ {0x1ffffed,25}, /*208 */ {0x7fff2, 19}, /*209 */ {0x1fffe3, 21},
|
||||||
|
/*210 */ {0x3ffffe6,26}, /*211 */ {0x7ffffe0, 27}, /*212 */ {0x7ffffe1, 27},
|
||||||
|
/*213 */ {0x3ffffe7,26}, /*214 */ {0x7ffffe2, 27}, /*215 */ {0xfffff2, 24},
|
||||||
|
/*216 */ {0x1fffe4, 21}, /*217 */ {0x1fffe5, 21}, /*218 */ {0x3ffffe8, 26},
|
||||||
|
/*219 */ {0x3ffffe9,26}, /*220 */ {0xffffffd,28}, /*221 */ {0x7ffffe3, 27},
|
||||||
|
/*222 */ {0x7ffffe4,27}, /*223 */ {0x7ffffe5, 27}, /*224 */ {0xfffec, 20},
|
||||||
|
/*225 */ {0xfffff3, 24}, /*226 */ {0xfffed, 20}, /*227 */ {0x1fffe6, 21},
|
||||||
|
/*228 */ {0x3fffe9, 22}, /*229 */ {0x1fffe7, 21}, /*230 */ {0x1fffe8, 21},
|
||||||
|
/*231 */ {0x7ffff3, 23}, /*232 */ {0x3fffea, 22}, /*233 */ {0x3fffeb, 22},
|
||||||
|
/*234 */ {0x1ffffee,25}, /*235 */ {0x1ffffef, 25}, /*236 */ {0xfffff4, 24},
|
||||||
|
/*237 */ {0xfffff5, 24}, /*238 */ {0x3ffffea, 26}, /*239 */ {0x7ffff4, 23},
|
||||||
|
/*240 */ {0x3ffffeb,26}, /*241 */ {0x7ffffe6, 27}, /*242 */ {0x3ffffec, 26},
|
||||||
|
/*243 */ {0x3ffffed,26}, /*244 */ {0x7ffffe7, 27}, /*245 */ {0x7ffffe8, 27},
|
||||||
|
/*246 */ {0x7ffffe9,27}, /*247 */ {0x7ffffea, 27}, /*248 */ {0x7ffffeb, 27},
|
||||||
|
/*249 */ {0xffffffe,28}, /*250 */ {0x7ffffec, 27}, /*251 */ {0x7ffffed, 27},
|
||||||
|
/*252 */ {0x7ffffee,27}, /*253 */ {0x7ffffef, 27}, /*254 */ {0x7fffff0, 27},
|
||||||
|
/*255 */ {0x3ffffee,26},
|
||||||
|
}};
|
||||||
|
|
||||||
|
export inline std::string DecodeHuffman(const std::uint8_t* in, std::size_t inLen) {
|
||||||
|
// Bit-walking decoder. Refill a 64-bit register from the input byte
|
||||||
|
// stream, then for each output symbol scan the 256-entry table for
|
||||||
|
// the unique code that matches the top `bits` of the register at
|
||||||
|
// some length L in [5, 30]. Linear scan is acceptable for the
|
||||||
|
// header-section sizes seen in HTTP/3 traffic; the inner loop is
|
||||||
|
// hot for ~100s of bytes per request.
|
||||||
|
std::string out;
|
||||||
|
std::uint64_t reg = 0;
|
||||||
|
int bits = 0;
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while (true) {
|
||||||
|
while (bits <= 56 && pos < inLen) {
|
||||||
|
reg = (reg << 8) | in[pos++];
|
||||||
|
bits += 8;
|
||||||
|
}
|
||||||
|
if (bits == 0) return out;
|
||||||
|
|
||||||
|
bool matched = false;
|
||||||
|
const int maxL = bits < 30 ? bits : 30;
|
||||||
|
for (int L = 5; L <= maxL; ++L) {
|
||||||
|
std::uint32_t want = static_cast<std::uint32_t>(
|
||||||
|
(reg >> (bits - L)) & ((std::uint64_t{1} << L) - 1));
|
||||||
|
for (std::size_t s = 0; s < 256; ++s) {
|
||||||
|
if (kHuffmanTable[s].length == L && kHuffmanTable[s].code == want) {
|
||||||
|
out.push_back(static_cast<char>(s));
|
||||||
|
bits -= L;
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matched) break;
|
||||||
|
}
|
||||||
|
if (!matched) {
|
||||||
|
// Tail must be ≤ 7 bits and all 1s — that's the EOS-prefix
|
||||||
|
// padding RFC 7541 §5.2 mandates. Anything else is malformed.
|
||||||
|
if (bits <= 7) {
|
||||||
|
std::uint64_t mask = (std::uint64_t{1} << bits) - 1;
|
||||||
|
if ((reg & mask) == mask) return out;
|
||||||
|
}
|
||||||
|
throw HTTP3ProtocolError("Huffman: invalid encoding");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- QPACK static table (RFC 9204 Appendix A, subset) ----------------
|
||||||
|
// We embed only the entries we either emit (encode) or might need to look
|
||||||
|
// up by index (decode peers using indexed/literal-with-name-ref). The
|
||||||
|
// subset covers all pseudo-headers and a few common content-type/status
|
||||||
|
// values — enough for self-interop. If the peer references an index
|
||||||
|
// outside this table we throw HTTP3ProtocolError.
|
||||||
|
struct StaticEntry {
|
||||||
|
std::string_view name;
|
||||||
|
std::string_view value; // empty if no canonical value (name-only entry)
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr std::array<StaticEntry, 99> kStaticTable = {{
|
||||||
|
{":authority", ""}, // 0
|
||||||
|
{":path", "/"}, // 1
|
||||||
|
{"age", "0"}, // 2
|
||||||
|
{"content-disposition", ""}, // 3
|
||||||
|
{"content-length", "0"}, // 4
|
||||||
|
{"cookie", ""}, // 5
|
||||||
|
{"date", ""}, // 6
|
||||||
|
{"etag", ""}, // 7
|
||||||
|
{"if-modified-since", ""}, // 8
|
||||||
|
{"if-none-match", ""}, // 9
|
||||||
|
{"last-modified", ""}, // 10
|
||||||
|
{"link", ""}, // 11
|
||||||
|
{"location", ""}, // 12
|
||||||
|
{"referer", ""}, // 13
|
||||||
|
{"set-cookie", ""}, // 14
|
||||||
|
{":method", "CONNECT"}, // 15
|
||||||
|
{":method", "DELETE"}, // 16
|
||||||
|
{":method", "GET"}, // 17
|
||||||
|
{":method", "HEAD"}, // 18
|
||||||
|
{":method", "OPTIONS"}, // 19
|
||||||
|
{":method", "POST"}, // 20
|
||||||
|
{":method", "PUT"}, // 21
|
||||||
|
{":scheme", "http"}, // 22
|
||||||
|
{":scheme", "https"}, // 23
|
||||||
|
{":status", "103"}, // 24
|
||||||
|
{":status", "200"}, // 25
|
||||||
|
{":status", "304"}, // 26
|
||||||
|
{":status", "404"}, // 27
|
||||||
|
{":status", "503"}, // 28
|
||||||
|
{"accept", "*/*"}, // 29
|
||||||
|
{"accept", "application/dns-message"}, // 30
|
||||||
|
{"accept-encoding", "gzip, deflate, br"}, // 31
|
||||||
|
{"accept-ranges", "bytes"}, // 32
|
||||||
|
{"access-control-allow-headers", "cache-control"}, // 33
|
||||||
|
{"access-control-allow-headers", "content-type"}, // 34
|
||||||
|
{"access-control-allow-origin", "*"}, // 35
|
||||||
|
{"cache-control", "max-age=0"}, // 36
|
||||||
|
{"cache-control", "max-age=2592000"}, // 37
|
||||||
|
{"cache-control", "max-age=604800"}, // 38
|
||||||
|
{"cache-control", "no-cache"}, // 39
|
||||||
|
{"cache-control", "no-store"}, // 40
|
||||||
|
{"cache-control", "public, max-age=31536000"}, // 41
|
||||||
|
{"content-encoding", "br"}, // 42
|
||||||
|
{"content-encoding", "gzip"}, // 43
|
||||||
|
{"content-type", "application/dns-message"}, // 44
|
||||||
|
{"content-type", "application/javascript"}, // 45
|
||||||
|
{"content-type", "application/json"}, // 46
|
||||||
|
{"content-type", "application/x-www-form-urlencoded"}, // 47
|
||||||
|
{"content-type", "image/gif"}, // 48
|
||||||
|
{"content-type", "image/jpeg"}, // 49
|
||||||
|
{"content-type", "image/png"}, // 50
|
||||||
|
{"content-type", "text/css"}, // 51
|
||||||
|
{"content-type", "text/html; charset=utf-8"}, // 52
|
||||||
|
{"content-type", "text/plain"}, // 53
|
||||||
|
{"content-type", "text/plain;charset=utf-8"}, // 54
|
||||||
|
{"range", "bytes=0-"}, // 55
|
||||||
|
{"strict-transport-security", "max-age=31536000"}, // 56
|
||||||
|
{"strict-transport-security", "max-age=31536000; includesubdomains"}, // 57
|
||||||
|
{"strict-transport-security", "max-age=31536000; includesubdomains; preload"}, // 58
|
||||||
|
{"vary", "accept-encoding"}, // 59
|
||||||
|
{"vary", "origin"}, // 60
|
||||||
|
{"x-content-type-options", "nosniff"}, // 61
|
||||||
|
{"x-xss-protection", "1; mode=block"}, // 62
|
||||||
|
{":status", "100"}, // 63
|
||||||
|
{":status", "204"}, // 64
|
||||||
|
{":status", "206"}, // 65
|
||||||
|
{":status", "302"}, // 66
|
||||||
|
{":status", "400"}, // 67
|
||||||
|
{":status", "403"}, // 68
|
||||||
|
{":status", "421"}, // 69
|
||||||
|
{":status", "425"}, // 70
|
||||||
|
{":status", "500"}, // 71
|
||||||
|
{"accept-language", ""}, // 72
|
||||||
|
{"access-control-allow-credentials", "FALSE"}, // 73
|
||||||
|
{"access-control-allow-credentials", "TRUE"}, // 74
|
||||||
|
{"access-control-allow-headers", "*"}, // 75
|
||||||
|
{"access-control-allow-methods", "get"}, // 76
|
||||||
|
{"access-control-allow-methods", "get, post, options"}, // 77
|
||||||
|
{"access-control-allow-methods", "options"}, // 78
|
||||||
|
{"access-control-expose-headers", "content-length"}, // 79
|
||||||
|
{"access-control-request-headers", "content-type"}, // 80
|
||||||
|
{"access-control-request-method", "get"}, // 81
|
||||||
|
{"access-control-request-method", "post"}, // 82
|
||||||
|
{"alt-svc", "clear"}, // 83
|
||||||
|
{"authorization", ""}, // 84
|
||||||
|
{"content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"}, // 85
|
||||||
|
{"early-data", "1"}, // 86
|
||||||
|
{"expect-ct", ""}, // 87
|
||||||
|
{"forwarded", ""}, // 88
|
||||||
|
{"if-range", ""}, // 89
|
||||||
|
{"origin", ""}, // 90
|
||||||
|
{"purpose", "prefetch"}, // 91
|
||||||
|
{"server", ""}, // 92
|
||||||
|
{"timing-allow-origin", "*"}, // 93
|
||||||
|
{"upgrade-insecure-requests", "1"}, // 94
|
||||||
|
{"user-agent", ""}, // 95
|
||||||
|
{"x-forwarded-for", ""}, // 96
|
||||||
|
{"x-frame-options", "deny"}, // 97
|
||||||
|
{"x-frame-options", "sameorigin"}, // 98
|
||||||
|
}};
|
||||||
|
|
||||||
|
// Lookup a (name, value) pair against the static table; returns -1 if not
|
||||||
|
// present. Linear scan is fine here: ~100 entries, called per header.
|
||||||
|
inline int StaticTableExactLookup(std::string_view name, std::string_view value) {
|
||||||
|
for (std::size_t i = 0; i < kStaticTable.size(); ++i) {
|
||||||
|
if (kStaticTable[i].name == name && kStaticTable[i].value == value) {
|
||||||
|
return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Lookup name-only; returns the lowest matching index or -1.
|
||||||
|
inline int StaticTableNameLookup(std::string_view name) {
|
||||||
|
for (std::size_t i = 0; i < kStaticTable.size(); ++i) {
|
||||||
|
if (kStaticTable[i].name == name) return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Field section codec ----------------
|
||||||
|
// Encodes the QPACK-on-the-wire field section: a 2-byte prefix (Required
|
||||||
|
// Insert Count = 0, Sign+DeltaBase = 0 — i.e. no dynamic table reliance)
|
||||||
|
// followed by per-field representations. We pick the most compact static
|
||||||
|
// representation each header allows; never emit Huffman; never use the
|
||||||
|
// dynamic table.
|
||||||
|
export inline std::vector<std::uint8_t> EncodeFieldSection(
|
||||||
|
const std::vector<std::pair<std::string, std::string>>& fields) {
|
||||||
|
std::vector<std::uint8_t> out;
|
||||||
|
// Prefix: Required Insert Count (8-bit prefix int = 0)
|
||||||
|
EncodeQpackInt(out, 0x00, 8, 0);
|
||||||
|
// Sign + Delta Base (sign in high bit of 7-bit-prefix int = 0)
|
||||||
|
EncodeQpackInt(out, 0x00, 7, 0);
|
||||||
|
|
||||||
|
for (const auto& [name, value] : fields) {
|
||||||
|
int exact = StaticTableExactLookup(name, value);
|
||||||
|
if (exact >= 0) {
|
||||||
|
// Indexed Field Line, T=1 (static): pattern 1Tixxxxx, 6-bit prefix.
|
||||||
|
EncodeQpackInt(out, 0xC0, 6, static_cast<std::uint64_t>(exact));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int nameIdx = StaticTableNameLookup(name);
|
||||||
|
if (nameIdx >= 0) {
|
||||||
|
// Literal Field Line With Name Reference, T=1 (static),
|
||||||
|
// N=0 (allow indexing on intermediaries — moot since no DT):
|
||||||
|
// pattern 01NTxxxx, 4-bit name-index prefix.
|
||||||
|
EncodeQpackInt(out, 0x50, 4, static_cast<std::uint64_t>(nameIdx));
|
||||||
|
} else {
|
||||||
|
// Literal Field Line With Literal Name, N=0, H=0: pattern
|
||||||
|
// 001NHxxx with a 3-bit name-length prefix.
|
||||||
|
EncodeQpackInt(out, 0x20, 3, name.size());
|
||||||
|
out.insert(out.end(), name.begin(), name.end());
|
||||||
|
}
|
||||||
|
// Value: 7-bit length prefix, H=0 (no Huffman).
|
||||||
|
EncodeQpackInt(out, 0x00, 7, value.size());
|
||||||
|
out.insert(out.end(), value.begin(), value.end());
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export inline std::vector<std::pair<std::string, std::string>> DecodeFieldSection(
|
||||||
|
const std::uint8_t* data, std::size_t available) {
|
||||||
|
std::size_t pos = 0;
|
||||||
|
std::uint64_t reqIc = 0, deltaBase = 0;
|
||||||
|
std::size_t cn = 0;
|
||||||
|
if (!DecodeQpackInt(data + pos, available - pos, 8, reqIc, cn)) {
|
||||||
|
throw HTTP3ProtocolError("QPACK: truncated Required Insert Count");
|
||||||
|
}
|
||||||
|
pos += cn;
|
||||||
|
if (pos >= available) throw HTTP3ProtocolError("QPACK: missing Base");
|
||||||
|
if (!DecodeQpackInt(data + pos, available - pos, 7, deltaBase, cn)) {
|
||||||
|
throw HTTP3ProtocolError("QPACK: truncated Base");
|
||||||
|
}
|
||||||
|
pos += cn;
|
||||||
|
if (reqIc != 0) {
|
||||||
|
// Encoder used the dynamic table, which we don't track. Required
|
||||||
|
// Insert Count != 0 means we cannot decode without dynamic-table
|
||||||
|
// state; surface a clean protocol error rather than mis-decoding.
|
||||||
|
throw HTTP3ProtocolError("QPACK: dynamic table reference (Required Insert Count != 0)");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<std::string, std::string>> fields;
|
||||||
|
while (pos < available) {
|
||||||
|
std::uint8_t b = data[pos];
|
||||||
|
if ((b & 0x80) != 0) {
|
||||||
|
// 1Txxxxxx — Indexed Field Line.
|
||||||
|
bool isStatic = (b & 0x40) != 0;
|
||||||
|
std::uint64_t idx = 0;
|
||||||
|
if (!DecodeQpackInt(data + pos, available - pos, 6, idx, cn)) {
|
||||||
|
throw HTTP3ProtocolError("QPACK: truncated indexed field line");
|
||||||
|
}
|
||||||
|
pos += cn;
|
||||||
|
if (!isStatic) throw HTTP3ProtocolError("QPACK: dynamic-table indexed line unsupported");
|
||||||
|
if (idx >= kStaticTable.size()) throw HTTP3ProtocolError("QPACK: static index out of range");
|
||||||
|
const auto& e = kStaticTable[static_cast<std::size_t>(idx)];
|
||||||
|
fields.emplace_back(std::string(e.name), std::string(e.value));
|
||||||
|
} else if ((b & 0xC0) == 0x40) {
|
||||||
|
// 01NTxxxx — Literal Field Line With Name Reference.
|
||||||
|
bool isStatic = (b & 0x10) != 0;
|
||||||
|
std::uint64_t idx = 0;
|
||||||
|
if (!DecodeQpackInt(data + pos, available - pos, 4, idx, cn)) {
|
||||||
|
throw HTTP3ProtocolError("QPACK: truncated literal-with-nameref index");
|
||||||
|
}
|
||||||
|
pos += cn;
|
||||||
|
if (!isStatic) throw HTTP3ProtocolError("QPACK: dynamic-table name reference unsupported");
|
||||||
|
if (idx >= kStaticTable.size()) throw HTTP3ProtocolError("QPACK: static name index out of range");
|
||||||
|
if (pos >= available) throw HTTP3ProtocolError("QPACK: missing value byte");
|
||||||
|
bool huffman = (data[pos] & 0x80) != 0;
|
||||||
|
std::uint64_t vlen = 0;
|
||||||
|
if (!DecodeQpackInt(data + pos, available - pos, 7, vlen, cn)) {
|
||||||
|
throw HTTP3ProtocolError("QPACK: truncated value length");
|
||||||
|
}
|
||||||
|
pos += cn;
|
||||||
|
if (pos + vlen > available) throw HTTP3ProtocolError("QPACK: value runs past buffer");
|
||||||
|
std::string value = huffman
|
||||||
|
? DecodeHuffman(data + pos, static_cast<std::size_t>(vlen))
|
||||||
|
: std::string(reinterpret_cast<const char*>(data + pos),
|
||||||
|
static_cast<std::size_t>(vlen));
|
||||||
|
pos += static_cast<std::size_t>(vlen);
|
||||||
|
fields.emplace_back(std::string(kStaticTable[static_cast<std::size_t>(idx)].name), std::move(value));
|
||||||
|
} else if ((b & 0xE0) == 0x20) {
|
||||||
|
// 001NHxxx — Literal Field Line With Literal Name.
|
||||||
|
bool huffmanName = (b & 0x08) != 0;
|
||||||
|
std::uint64_t nlen = 0;
|
||||||
|
if (!DecodeQpackInt(data + pos, available - pos, 3, nlen, cn)) {
|
||||||
|
throw HTTP3ProtocolError("QPACK: truncated literal-name length");
|
||||||
|
}
|
||||||
|
pos += cn;
|
||||||
|
if (pos + nlen > available) throw HTTP3ProtocolError("QPACK: literal name runs past buffer");
|
||||||
|
std::string name = huffmanName
|
||||||
|
? DecodeHuffman(data + pos, static_cast<std::size_t>(nlen))
|
||||||
|
: std::string(reinterpret_cast<const char*>(data + pos),
|
||||||
|
static_cast<std::size_t>(nlen));
|
||||||
|
pos += static_cast<std::size_t>(nlen);
|
||||||
|
if (pos >= available) throw HTTP3ProtocolError("QPACK: missing value byte");
|
||||||
|
bool huffmanValue = (data[pos] & 0x80) != 0;
|
||||||
|
std::uint64_t vlen = 0;
|
||||||
|
if (!DecodeQpackInt(data + pos, available - pos, 7, vlen, cn)) {
|
||||||
|
throw HTTP3ProtocolError("QPACK: truncated literal-value length");
|
||||||
|
}
|
||||||
|
pos += cn;
|
||||||
|
if (pos + vlen > available) throw HTTP3ProtocolError("QPACK: literal value runs past buffer");
|
||||||
|
std::string value = huffmanValue
|
||||||
|
? DecodeHuffman(data + pos, static_cast<std::size_t>(vlen))
|
||||||
|
: std::string(reinterpret_cast<const char*>(data + pos),
|
||||||
|
static_cast<std::size_t>(vlen));
|
||||||
|
pos += static_cast<std::size_t>(vlen);
|
||||||
|
fields.emplace_back(std::move(name), std::move(value));
|
||||||
|
} else {
|
||||||
|
// Indexed-with-Post-Base / Literal-with-Post-Base-Name-Reference
|
||||||
|
// both rely on a dynamic-table base offset we don't maintain.
|
||||||
|
throw HTTP3ProtocolError("QPACK: post-base reference unsupported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Frame helpers ----------------
|
||||||
|
// A frame is: type (varint) | length (varint) | payload (length bytes).
|
||||||
|
export inline void WriteFrame(std::vector<std::uint8_t>& out, std::uint64_t type,
|
||||||
|
const std::uint8_t* payload, std::size_t length) {
|
||||||
|
EncodeVarint(type, out);
|
||||||
|
EncodeVarint(static_cast<std::uint64_t>(length), out);
|
||||||
|
out.insert(out.end(), payload, payload + length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty SETTINGS frame body — we send no settings, accepting all defaults.
|
||||||
|
export inline std::vector<std::uint8_t> BuildControlStreamPrelude() {
|
||||||
|
std::vector<std::uint8_t> out;
|
||||||
|
EncodeVarint(kStreamControl, out); // unidi stream type
|
||||||
|
EncodeVarint(kFrameSettings, out); // SETTINGS frame type
|
||||||
|
EncodeVarint(0, out); // frame length 0
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,37 +21,56 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
export module Crafter.Network:ListenerHTTP;
|
export module Crafter.Network:ListenerHTTP;
|
||||||
import std;
|
import std;
|
||||||
import :HTTP;
|
import :HTTP;
|
||||||
import :ClientTCP;
|
import :ListenerQUIC;
|
||||||
|
import :ClientQUIC;
|
||||||
|
|
||||||
namespace Crafter {
|
namespace Crafter {
|
||||||
export class ListenerHTTP;
|
// HTTP/3 server. Wraps a ListenerQUIC: each accepted QUIC connection
|
||||||
class ListenerHTTPClient {
|
// registers a per-stream handler that parses one request, dispatches it
|
||||||
public:
|
// through the route map, and writes a response back on the same bidi
|
||||||
std::atomic<bool> disconnected;
|
// stream. ALPN is fixed to "h3".
|
||||||
ClientTCP client;
|
//
|
||||||
std::thread thread;
|
// Routes are keyed by `:path` (exact match). Unknown paths return a
|
||||||
ListenerHTTP* server;
|
// synthetic 404. Route handlers run on the ThreadPool — multiple requests
|
||||||
ListenerHTTPClient(ListenerHTTP* server, int s);
|
// on the same connection can therefore execute concurrently.
|
||||||
void ListenRoutes();
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ListenerHTTP {
|
export class ListenerHTTP {
|
||||||
public:
|
public:
|
||||||
int s;
|
// The underlying QUIC listener owns the accept loop, certificates,
|
||||||
std::vector<ListenerHTTPClient*> clients;
|
// and the per-connection ClientQUIC instances. It is heap-allocated
|
||||||
bool running = true;
|
// and owned by this Impl so that move construction/destruction is
|
||||||
const std::unordered_map<std::string, std::function<std::string(const HTTPRequest&)>> routes;
|
// straightforward.
|
||||||
ListenerHTTP(std::uint16_t port, std::unordered_map<std::string, std::function<std::string(const HTTPRequest&)>> routes);
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes;
|
||||||
|
std::string alpn;
|
||||||
|
|
||||||
|
ListenerHTTP(std::uint16_t port,
|
||||||
|
QUICServerCredentials creds,
|
||||||
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes);
|
||||||
|
|
||||||
~ListenerHTTP();
|
~ListenerHTTP();
|
||||||
|
ListenerHTTP(const ListenerHTTP&) = delete;
|
||||||
|
ListenerHTTP(ListenerHTTP&&) noexcept;
|
||||||
|
|
||||||
|
// Block on this thread, dispatch each accepted connection on this
|
||||||
|
// thread (matches ListenerQUIC::ListenSyncSync semantics).
|
||||||
void Listen();
|
void Listen();
|
||||||
void Stop();
|
void Stop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Async wrapper: runs the listener's accept loop on a background thread
|
||||||
|
// so the caller can construct it and continue. Mirrors the old
|
||||||
|
// ListenerAsyncHTTP so existing call sites keep working.
|
||||||
export class ListenerAsyncHTTP {
|
export class ListenerAsyncHTTP {
|
||||||
public:
|
public:
|
||||||
ListenerHTTP listener;
|
ListenerHTTP listener;
|
||||||
std::thread thread;
|
std::thread thread;
|
||||||
ListenerAsyncHTTP(std::uint16_t port, std::unordered_map<std::string, std::function<std::string(const HTTPRequest&)>> routes);
|
|
||||||
|
ListenerAsyncHTTP(std::uint16_t port,
|
||||||
|
QUICServerCredentials creds,
|
||||||
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes);
|
||||||
~ListenerAsyncHTTP();
|
~ListenerAsyncHTTP();
|
||||||
void Stop();
|
void Stop();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ namespace fs = std::filesystem;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||||
constexpr std::array<std::string_view, 8> networkInterfaces = {
|
constexpr std::array<std::string_view, 9> networkInterfaces = {
|
||||||
"interfaces/Crafter.Network",
|
"interfaces/Crafter.Network",
|
||||||
"interfaces/Crafter.Network-ClientTCP",
|
"interfaces/Crafter.Network-ClientTCP",
|
||||||
"interfaces/Crafter.Network-ListenerTCP",
|
"interfaces/Crafter.Network-ListenerTCP",
|
||||||
"interfaces/Crafter.Network-ClientHTTP",
|
"interfaces/Crafter.Network-ClientHTTP",
|
||||||
"interfaces/Crafter.Network-ListenerHTTP",
|
"interfaces/Crafter.Network-ListenerHTTP",
|
||||||
"interfaces/Crafter.Network-HTTP",
|
"interfaces/Crafter.Network-HTTP",
|
||||||
|
"interfaces/Crafter.Network-HTTP3",
|
||||||
"interfaces/Crafter.Network-ClientQUIC",
|
"interfaces/Crafter.Network-ClientQUIC",
|
||||||
"interfaces/Crafter.Network-ListenerQUIC",
|
"interfaces/Crafter.Network-ListenerQUIC",
|
||||||
};
|
};
|
||||||
|
|
@ -61,7 +62,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
// linker at the actual output location.
|
// linker at the actual output location.
|
||||||
msquic.libDirs = { "bin/Release" };
|
msquic.libDirs = { "bin/Release" };
|
||||||
msquic.libs = { "msquic" };
|
msquic.libs = { "msquic" };
|
||||||
std::array<fs::path, 8> ifaces;
|
std::array<fs::path, 9> ifaces;
|
||||||
std::ranges::copy(networkInterfaces, ifaces.begin());
|
std::ranges::copy(networkInterfaces, ifaces.begin());
|
||||||
std::array<fs::path, 6> impls;
|
std::array<fs::path, 6> impls;
|
||||||
std::ranges::copy(networkImplementations, impls.begin());
|
std::ranges::copy(networkImplementations, impls.begin());
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
/*
|
|
||||||
Crafter® Build
|
|
||||||
Copyright (C) 2026 Catcrafts®
|
|
||||||
Catcrafts.net
|
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License version 3.0 as published by the Free Software Foundation;
|
|
||||||
|
|
||||||
This library is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
import Crafter.Network;
|
|
||||||
import std;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
bool success = false;
|
|
||||||
ListenerAsyncHTTP listener(8081, {{"/", [&](const HTTPRequest& request) {
|
|
||||||
success = true;
|
|
||||||
return CreateResponseHTTP("200 OK", "Hello World!");
|
|
||||||
}}});
|
|
||||||
try {
|
|
||||||
system("curl http://localhost:8081 > /dev/null 2>&1");
|
|
||||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
||||||
if (success) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
std::println("Did not receive");
|
|
||||||
return 1;
|
|
||||||
} catch (std::exception& e) {
|
|
||||||
std::println("{}", e.what());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import std;
|
|
||||||
import Crafter.Build;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.path = "tests/ShouldRecieveHTTP/";
|
|
||||||
cfg.name = "ShouldRecieveHTTP";
|
|
||||||
cfg.outputName = "ShouldRecieveHTTP";
|
|
||||||
cfg.target = "x86_64-pc-linux-gnu";
|
|
||||||
cfg.type = ConfigurationType::Executable;
|
|
||||||
cfg.dependencies = { ParentLib("crafter-network") };
|
|
||||||
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
|
||||||
cfg.linkFlags.push_back("-ldl");
|
|
||||||
std::array<fs::path, 0> ifaces = {};
|
|
||||||
std::array<fs::path, 1> impls = { "ShouldRecieveHTTP" };
|
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
60
tests/ShouldSend/ShouldSend.cpp
Normal file
60
tests/ShouldSend/ShouldSend.cpp
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
Crafter® Build
|
||||||
|
Copyright (C) 2026 Catcrafts®
|
||||||
|
Catcrafts.net
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License version 3.0 as published by the Free Software Foundation;
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
import Crafter.Network;
|
||||||
|
import Crafter.Thread;
|
||||||
|
import std;
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
// External-interop smoke test: connect to a public h3 endpoint, fetch /, and
|
||||||
|
// verify a 200 response with a non-empty body. Exercises:
|
||||||
|
// - real TLS chain validation against the system trust store
|
||||||
|
// - mandatory client control stream + SETTINGS prelude
|
||||||
|
// - peer's control + QPACK encoder/decoder unidi streams (drained)
|
||||||
|
// - QPACK Huffman decode on the response headers
|
||||||
|
//
|
||||||
|
// Targets cloudflare-quic.com (Cloudflare's public h3 demo). Network-
|
||||||
|
// dependent — if outbound UDP/443 is firewalled or the endpoint goes away,
|
||||||
|
// this will fail.
|
||||||
|
int main() {
|
||||||
|
ThreadPool::Start();
|
||||||
|
try {
|
||||||
|
QUICClientCredentials creds; // default: validate against system trust
|
||||||
|
ClientHTTP client("cloudflare-quic.com", 443, creds);
|
||||||
|
HTTPResponse r = client.Send(
|
||||||
|
CreateRequestHTTP("GET", "/", "cloudflare-quic.com")
|
||||||
|
);
|
||||||
|
std::cout << "status=" << r.status << " bodyBytes=" << r.body.size() << std::endl;
|
||||||
|
if (r.headers.count("server")) {
|
||||||
|
std::cout << "server=" << r.headers["server"] << std::endl;
|
||||||
|
}
|
||||||
|
if (r.body.size() > 0) {
|
||||||
|
auto preview = r.body.substr(0, std::min<std::size_t>(80, r.body.size()));
|
||||||
|
std::cout << "preview: " << preview << std::endl;
|
||||||
|
}
|
||||||
|
if (r.status != "200" || r.body.empty()) {
|
||||||
|
std::cout << "unexpected response" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::cout.flush();
|
||||||
|
std::_Exit(0);
|
||||||
|
} catch (std::exception& e) {
|
||||||
|
std::println("error: {}", e.what());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,16 +5,16 @@ using namespace Crafter;
|
||||||
|
|
||||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||||
Configuration cfg;
|
Configuration cfg;
|
||||||
cfg.path = "tests/ShouldSendHTTP/";
|
cfg.path = "tests/ShouldSend/";
|
||||||
cfg.name = "ShouldSendHTTP";
|
cfg.name = "ShouldSend";
|
||||||
cfg.outputName = "ShouldSendHTTP";
|
cfg.outputName = "ShouldSend";
|
||||||
cfg.target = "x86_64-pc-linux-gnu";
|
cfg.target = "x86_64-pc-linux-gnu";
|
||||||
cfg.type = ConfigurationType::Executable;
|
cfg.type = ConfigurationType::Executable;
|
||||||
cfg.dependencies = { ParentLib("crafter-network") };
|
cfg.dependencies = { ParentLib("crafter-network") };
|
||||||
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
||||||
cfg.linkFlags.push_back("-ldl");
|
cfg.linkFlags.push_back("-ldl");
|
||||||
std::array<fs::path, 0> ifaces = {};
|
std::array<fs::path, 0> ifaces = {};
|
||||||
std::array<fs::path, 1> impls = { "ShouldSendHTTP" };
|
std::array<fs::path, 1> impls = { "ShouldSend" };
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
Crafter® Build
|
|
||||||
Copyright (C) 2026 Catcrafts®
|
|
||||||
Catcrafts.net
|
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License version 3.0 as published by the Free Software Foundation;
|
|
||||||
|
|
||||||
This library is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
import Crafter.Network;
|
|
||||||
import std;
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
ClientHTTP client("httpbin.org", 80);
|
|
||||||
HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/get", "httpbin.org"));
|
|
||||||
if (response.status == "200 OK") {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
std::println("{}", response.body);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
@ -16,23 +16,31 @@ You should have received a copy of the GNU Lesser General Public
|
||||||
License along with this library; if not, write to the Free Software
|
License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
import Crafter.Network;
|
import Crafter.Network;
|
||||||
|
import Crafter.Thread;
|
||||||
import std;
|
import std;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
ListenerAsyncHTTP listener(8082, {{"/", [&](const HTTPRequest& request) {
|
ThreadPool::Start();
|
||||||
return CreateResponseHTTP("200 OK", "Hello World!");
|
|
||||||
|
QUICServerCredentials serverCreds;
|
||||||
|
serverCreds.selfSigned = true;
|
||||||
|
ListenerAsyncHTTP listener(8082, serverCreds, {{"/", [&](const HTTPRequest& request) {
|
||||||
|
return CreateResponseHTTP("200", "Hello World!");
|
||||||
}}});
|
}}});
|
||||||
try {
|
try {
|
||||||
ClientHTTP client("localhost", 8082);
|
QUICClientCredentials clientCreds;
|
||||||
|
clientCreds.insecureNoServerValidation = true;
|
||||||
|
ClientHTTP client("localhost", 8082, clientCreds);
|
||||||
HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
||||||
if (response.status == "200 OK" && response.body == "Hello World!") {
|
if (response.status == "200" && response.body == "Hello World!") {
|
||||||
return 0;
|
// See ShouldSendRecieveQUICStream for rationale: msquic's
|
||||||
|
// RegistrationClose blocks on outstanding connections, so skip
|
||||||
|
// graceful teardown after the test logic succeeds.
|
||||||
|
std::_Exit(0);
|
||||||
}
|
}
|
||||||
std::println("{}{}", response.status, response.body);
|
std::println("{} {}", response.status, response.body);
|
||||||
return 1;
|
return 1;
|
||||||
} catch (std::exception& e) {
|
} catch (std::exception& e) {
|
||||||
std::println("{}", e.what());
|
std::println("{}", e.what());
|
||||||
|
|
|
||||||
|
|
@ -16,31 +16,39 @@ You should have received a copy of the GNU Lesser General Public
|
||||||
License along with this library; if not, write to the Free Software
|
License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
import Crafter.Network;
|
import Crafter.Network;
|
||||||
|
import Crafter.Thread;
|
||||||
import std;
|
import std;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
|
// "Keep-alive" in HTTP/3 corresponds to the QUIC connection being multiplexed:
|
||||||
|
// successive client.Send() calls reuse the same connection and open new
|
||||||
|
// request streams within it. This test exercises that — two requests on one
|
||||||
|
// ClientHTTP must both succeed.
|
||||||
int main() {
|
int main() {
|
||||||
ListenerAsyncHTTP listener(8083, {{"/", [&](const HTTPRequest& request) {
|
ThreadPool::Start();
|
||||||
return CreateResponseHTTP("200 OK", "Hello World!");
|
|
||||||
|
QUICServerCredentials serverCreds;
|
||||||
|
serverCreds.selfSigned = true;
|
||||||
|
ListenerAsyncHTTP listener(8083, serverCreds, {{"/", [&](const HTTPRequest& request) {
|
||||||
|
return CreateResponseHTTP("200", "Hello World!");
|
||||||
}}});
|
}}});
|
||||||
try {
|
try {
|
||||||
ClientHTTP client("localhost", 8083);
|
QUICClientCredentials clientCreds;
|
||||||
|
clientCreds.insecureNoServerValidation = true;
|
||||||
|
ClientHTTP client("localhost", 8083, clientCreds);
|
||||||
|
|
||||||
HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
||||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
if (response.status != "200" || response.body != "Hello World!") {
|
||||||
if (response.status != "200 OK" || response.body != "Hello World!") {
|
std::println("{} {}", response.status, response.body);
|
||||||
std::println("{}{}", response.status, response.body);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
response = client.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
response = client.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
||||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
if (response.status != "200" || response.body != "Hello World!") {
|
||||||
if (response.status != "200 OK" || response.body != "Hello World!") {
|
std::println("{} {}", response.status, response.body);
|
||||||
std::println("{}{}", response.status, response.body);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return 0;
|
std::_Exit(0);
|
||||||
} catch (std::exception& e) {
|
} catch (std::exception& e) {
|
||||||
std::println("{}", e.what());
|
std::println("{}", e.what());
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
|
|
@ -16,28 +16,31 @@ You should have received a copy of the GNU Lesser General Public
|
||||||
License along with this library; if not, write to the Free Software
|
License along with this library; if not, write to the Free Software
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
import Crafter.Network;
|
import Crafter.Network;
|
||||||
|
import Crafter.Thread;
|
||||||
import std;
|
import std;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
ListenerAsyncHTTP listener(8084, {{ "/", [&](const HTTPRequest& request) {
|
ThreadPool::Start();
|
||||||
|
|
||||||
|
QUICServerCredentials serverCreds;
|
||||||
|
serverCreds.selfSigned = true;
|
||||||
|
ListenerAsyncHTTP listener(8084, serverCreds, {{ "/", [&](const HTTPRequest& request) {
|
||||||
if (request.body.size() > 1'000'000) {
|
if (request.body.size() > 1'000'000) {
|
||||||
return CreateResponseHTTP("200 OK", "Large request received: " + std::to_string(request.body.size()) + " bytes");
|
return CreateResponseHTTP("200", "Large request received: " + std::to_string(request.body.size()) + " bytes");
|
||||||
}
|
}
|
||||||
return CreateResponseHTTP("200 OK", "Small request received");
|
return CreateResponseHTTP("200", "Small request received");
|
||||||
}
|
}}});
|
||||||
}});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ClientHTTP client("localhost", 8084);
|
QUICClientCredentials clientCreds;
|
||||||
|
clientCreds.insecureNoServerValidation = true;
|
||||||
|
ClientHTTP client("localhost", 8084, clientCreds);
|
||||||
std::string large_body(10 * 1024 * 1024, 'A');
|
std::string large_body(10 * 1024 * 1024, 'A');
|
||||||
HTTPResponse response = client.Send(CreateRequestHTTP("POST", "/", "localhost", large_body));
|
HTTPResponse response = client.Send(CreateRequestHTTP("POST", "/", "localhost", large_body));
|
||||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
if (response.status == "200" && response.body.find("Large request received") != std::string::npos) {
|
||||||
if (response.status == "200 OK" && response.body.find("Large request received") != std::string::npos) {
|
std::_Exit(0);
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
std::println("Unexpected response: {} {}", response.status, response.body);
|
std::println("Unexpected response: {} {}", response.status, response.body);
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue