diff --git a/frameworks/proxygen-coro/ArenaCoroServer.cpp b/frameworks/proxygen-coro/ArenaCoroServer.cpp new file mode 100644 index 000000000..ed3b65cf2 --- /dev/null +++ b/frameworks/proxygen-coro/ArenaCoroServer.cpp @@ -0,0 +1,974 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DEFINE_int32(http_port, 8080, "HTTP/1.1 and WebSocket port"); +DEFINE_int32(tls_port, 8081, "HTTP/1.1 over TLS port"); +DEFINE_int32(h2c_port, 8082, "HTTP/2 cleartext port"); +DEFINE_int32(h2_port, 8443, "HTTP/2 over TLS port"); +DEFINE_int32(h3_port, 8443, "HTTP/3 over QUIC port"); +DEFINE_string(ip, "::", "Address on which to listen"); +DEFINE_string(cert, "/certs/server.crt", "TLS certificate path"); +DEFINE_string(key, "/certs/server.key", "TLS private-key path"); +DEFINE_int32(threads, 0, "I/O threads; 0 uses the available CPU count"); + +namespace { + +using proxygen::HTTPMessage; +using proxygen::HTTPMethod; +using proxygen::coro::HTTPBodyEvent; +using proxygen::coro::HTTPError; +using proxygen::coro::HTTPErrorCode; +using proxygen::coro::HTTPFixedSource; +using proxygen::coro::HTTPHandler; +using proxygen::coro::HTTPHeaderEvent; +using proxygen::coro::HTTPServer; +using proxygen::coro::HTTPSessionContextPtr; +using proxygen::coro::HTTPSource; +using proxygen::coro::HTTPSourceHolder; +using proxygen::coro::TimedBaton; + +constexpr size_t kMaxBaselineBody = 1024; +constexpr uint64_t kMaxWebSocketMessage = 16ULL * 1024 * 1024; +constexpr std::string_view kJsonPrefix = "/json/"; +constexpr std::string_view kStaticPrefix = "/static/"; +constexpr std::string_view kStaticRoot = "/data/static/"; + +bool parseInteger(std::string_view input, int64_t &value) { + while (!input.empty() && + std::isspace(static_cast(input.front()))) { + input.remove_prefix(1); + } + while (!input.empty() && + std::isspace(static_cast(input.back()))) { + input.remove_suffix(1); + } + if (input.empty()) { + return false; + } + const auto result = + std::from_chars(input.data(), input.data() + input.size(), value); + return result.ec == std::errc() && result.ptr == input.data() + input.size(); +} + +bool checkedAdd(int64_t lhs, int64_t rhs, int64_t &result) { +#if defined(__GNUC__) || defined(__clang__) + return !__builtin_add_overflow(lhs, rhs, &result); +#else + if ((rhs > 0 && lhs > std::numeric_limits::max() - rhs) || + (rhs < 0 && lhs < std::numeric_limits::min() - rhs)) { + return false; + } + result = lhs + rhs; + return true; +#endif +} + +bool checkedMultiply(int64_t lhs, int64_t rhs, int64_t &result) { +#if defined(__GNUC__) || defined(__clang__) + return !__builtin_mul_overflow(lhs, rhs, &result); +#else + if (lhs > 0) { + if ((rhs > 0 && lhs > std::numeric_limits::max() / rhs) || + (rhs < 0 && rhs < std::numeric_limits::min() / lhs)) { + return false; + } + } else if (lhs < 0) { + if ((rhs > 0 && lhs < std::numeric_limits::min() / rhs) || + (rhs < 0 && rhs < std::numeric_limits::max() / lhs)) { + return false; + } + } + result = lhs * rhs; + return true; +#endif +} + +std::string contentType(std::string_view name) { + const auto endsWith = [name](std::string_view suffix) { + return name.size() >= suffix.size() && + name.substr(name.size() - suffix.size()) == suffix; + }; + if (endsWith(".css")) { + return "text/css"; + } + if (endsWith(".js")) { + return "application/javascript"; + } + if (endsWith(".html")) { + return "text/html"; + } + if (endsWith(".json")) { + return "application/json"; + } + if (endsWith(".svg")) { + return "image/svg+xml"; + } + if (endsWith(".webp")) { + return "image/webp"; + } + if (endsWith(".woff2")) { + return "font/woff2"; + } + return "application/octet-stream"; +} + +std::shared_ptr loadDataset() { + std::ifstream input("/data/dataset.json", std::ios::binary); + if (!input) { + throw std::runtime_error("cannot open /data/dataset.json"); + } + std::string contents((std::istreambuf_iterator(input)), + std::istreambuf_iterator()); + auto dataset = folly::parseJson(contents); + if (!dataset.isArray() || dataset.size() < 50) { + throw std::runtime_error("/data/dataset.json must contain 50 items"); + } + return std::make_shared(std::move(dataset)); +} + +bool validWebSocketKey(std::string_view key) noexcept { + if (key.size() != 24) { + return false; + } + std::array decoded{}; + const auto result = folly::base64Decode(key, decoded.data()); + return result.is_success && result.o == decoded.data() + 16; +} + +bool validUtf8(const uint8_t *data, size_t size) noexcept { + const auto continuation = [](uint8_t byte) { + return byte >= 0x80 && byte <= 0xbf; + }; + + size_t index = 0; + while (index < size) { + const uint8_t first = data[index]; + if (first <= 0x7f) { + ++index; + continue; + } + if (first >= 0xc2 && first <= 0xdf) { + if (index + 1 >= size || !continuation(data[index + 1])) { + return false; + } + index += 2; + continue; + } + if (first == 0xe0) { + if (index + 2 >= size || data[index + 1] < 0xa0 || + data[index + 1] > 0xbf || !continuation(data[index + 2])) { + return false; + } + index += 3; + continue; + } + if ((first >= 0xe1 && first <= 0xec) || (first >= 0xee && first <= 0xef)) { + if (index + 2 >= size || !continuation(data[index + 1]) || + !continuation(data[index + 2])) { + return false; + } + index += 3; + continue; + } + if (first == 0xed) { + if (index + 2 >= size || data[index + 1] < 0x80 || + data[index + 1] > 0x9f || !continuation(data[index + 2])) { + return false; + } + index += 3; + continue; + } + if (first == 0xf0) { + if (index + 3 >= size || data[index + 1] < 0x90 || + data[index + 1] > 0xbf || !continuation(data[index + 2]) || + !continuation(data[index + 3])) { + return false; + } + index += 4; + continue; + } + if (first >= 0xf1 && first <= 0xf3) { + if (index + 3 >= size || !continuation(data[index + 1]) || + !continuation(data[index + 2]) || !continuation(data[index + 3])) { + return false; + } + index += 4; + continue; + } + if (first == 0xf4) { + if (index + 3 >= size || data[index + 1] < 0x80 || + data[index + 1] > 0x8f || !continuation(data[index + 2]) || + !continuation(data[index + 3])) { + return false; + } + index += 4; + continue; + } + return false; + } + return true; +} + +bool validUtf8(const std::vector &data) noexcept { + return validUtf8(data.data(), data.size()); +} + +bool validWebSocketCloseCode(uint16_t code) noexcept { + const bool definedProtocolCode = code >= 1000 && code <= 1014 && + code != 1004 && code != 1005 && code != 1006; + const bool applicationCode = code >= 3000 && code <= 4999; + return definedProtocolCode || applicationCode; +} + +HTTPFixedSource *makeResponse(uint16_t status, + std::string_view contentTypeValue, + std::string body) { + auto *response = HTTPFixedSource::makeFixedResponse(status, std::move(body)); + response->msg_->getHeaders().set(proxygen::HTTP_HEADER_CONTENT_TYPE, + contentTypeValue); + return response; +} + +HTTPFixedSource *makeTextResponse(uint16_t status, std::string body) { + return makeResponse(status, "text/plain", std::move(body)); +} + +folly::coro::Task +readBodyEventNoSuspend(HTTPSourceHolder &source, + uint32_t max = std::numeric_limits::max()) { + while (true) { + auto event = co_await co_awaitTry(source.readBodyEvent(max)); + if (event.hasException()) { + co_yield folly::coro::co_error(std::move(event.exception())); + } + if (event->eventType == HTTPBodyEvent::SUSPEND) { + const auto status = co_await std::move(event->event.resume); + if (status == TimedBaton::Status::cancelled) { + co_yield folly::coro::co_error( + HTTPError(HTTPErrorCode::CORO_CANCELLED, "request read cancelled")); + } + continue; + } + co_return std::move(*event); + } +} + +struct RequestBody { + bool readOk{true}; + bool captureOk{true}; + size_t size{0}; + std::string captured; +}; + +folly::coro::Task readRequestBody(HTTPSourceHolder &source, + bool eom, size_t captureLimit) { + RequestBody result; + while (!eom) { + auto eventTry = co_await co_awaitTry(readBodyEventNoSuspend(source)); + if (eventTry.hasException()) { + result.readOk = false; + co_return result; + } + auto event = std::move(*eventTry); + eom = event.eom; + if (event.eventType == HTTPBodyEvent::PADDING) { + continue; + } + if (event.eventType != HTTPBodyEvent::BODY) { + result.readOk = false; + co_return result; + } + + const size_t bytes = event.event.body.chainLength(); + if (bytes > std::numeric_limits::max() - result.size) { + result.readOk = false; + co_return result; + } + result.size += bytes; + if (captureLimit == 0 || !result.captureOk) { + continue; + } + if (result.captured.size() > captureLimit || + bytes > captureLimit - result.captured.size()) { + result.captureOk = false; + continue; + } + auto body = event.event.body.move(); + if (body) { + const auto range = body->coalesce(); + result.captured.append(reinterpret_cast(range.data()), + range.size()); + } + } + co_return result; +} + +class WebSocketSource final : public HTTPSource { +public: + explicit WebSocketSource(HTTPSourceHolder request) + : request_(std::move(request)) { + setHeapAllocated(); + } + + folly::coro::Task readHeaderEvent() override { + auto response = std::make_unique(); + response->setHTTPVersion(1, 1); + response->setStatusCode(200); + response->setStatusMessage("OK"); + response->setEgressWebsocketUpgrade(); + HTTPHeaderEvent event(std::move(response), false); + auto guard = folly::makeGuard(lifetime(event)); + co_return event; + } + + folly::coro::Task + readBodyEvent(uint32_t max = std::numeric_limits::max()) override { + while (pending_.empty()) { + auto inputTry = co_await co_awaitTry(readBodyEventNoSuspend(request_)); + if (inputTry.hasException()) { + auto error = proxygen::coro::getHTTPError(inputTry); + auto guard = folly::makeGuard([this] { + if (heapAllocated_) { + delete this; + } + }); + co_yield folly::coro::co_error(std::move(error)); + } + + auto input = std::move(*inputTry); + if (input.eventType == HTTPBodyEvent::BODY) { + auto body = input.event.body.move(); + if (body) { + const auto range = body->coalesce(); + input_.insert(input_.end(), range.begin(), range.end()); + processFrames(); + } + } + if (input.eom && !finished_) { + finished_ = true; + if (pending_.empty()) { + pending_.push_back(PendingOutput{{}, 0, true}); + } else { + pending_.back().eom = true; + } + } + } + + auto &front = pending_.front(); + const size_t remaining = front.bytes.size() - front.offset; + const size_t limit = std::max(1, max); + const size_t amount = std::min(remaining, limit); + std::unique_ptr body; + if (amount > 0) { + body = + folly::IOBuf::copyBuffer(front.bytes.data() + front.offset, amount); + front.offset += amount; + } + const bool outputEom = front.eom && front.offset == front.bytes.size(); + if (front.offset == front.bytes.size()) { + pending_.pop_front(); + } + HTTPBodyEvent event(std::move(body), outputEom); + auto guard = folly::makeGuard(lifetime(event)); + co_return event; + } + + void stopReading(folly::Optional error = + folly::none) noexcept override { + if (request_) { + request_.stopReading(error); + } + if (heapAllocated_) { + delete this; + } + } + +private: + struct PendingOutput { + std::vector bytes; + size_t offset{0}; + bool eom{false}; + }; + + void queueEom() { + if (!finished_) { + finished_ = true; + pending_.push_back(PendingOutput{{}, 0, true}); + } + } + + void queueFrame(uint8_t opcode, const uint8_t *payload, size_t payloadLength, + bool eom = false) { + std::vector frame; + frame.reserve(payloadLength + 10); + frame.push_back(static_cast(0x80U | opcode)); + if (payloadLength <= 125) { + frame.push_back(static_cast(payloadLength)); + } else if (payloadLength <= std::numeric_limits::max()) { + frame.push_back(126); + frame.push_back(static_cast((payloadLength >> 8) & 0xff)); + frame.push_back(static_cast(payloadLength & 0xff)); + } else { + frame.push_back(127); + const auto length = static_cast(payloadLength); + for (int shift = 56; shift >= 0; shift -= 8) { + frame.push_back(static_cast((length >> shift) & 0xff)); + } + } + if (payloadLength > 0) { + frame.insert(frame.end(), payload, payload + payloadLength); + } + pending_.push_back(PendingOutput{std::move(frame), 0, eom}); + } + + void queueFrame(uint8_t opcode, const std::vector &payload, + bool eom = false) { + queueFrame(opcode, payload.data(), payload.size(), eom); + } + + void closeWith(uint16_t status) { + if (closeSent_ || finished_) { + return; + } + const std::array payload = { + static_cast((status >> 8) & 0xff), + static_cast(status & 0xff)}; + queueFrame(0x08, payload.data(), payload.size()); + closeSent_ = true; + } + + void protocolError() { closeWith(1002); } + + void invalidPayload() { closeWith(1007); } + + void handleFrame(bool fin, uint8_t opcode, std::vector payload) { + if (closeSent_ && opcode != 0x08) { + return; + } + + if ((opcode & 0x08U) != 0) { + if (!fin || payload.size() > 125) { + protocolError(); + return; + } + if (opcode == 0x08) { + if (payload.size() == 1) { + protocolError(); + return; + } + if (payload.size() >= 2) { + const uint16_t status = + (static_cast(payload[0]) << 8) | payload[1]; + if (!validWebSocketCloseCode(status)) { + protocolError(); + return; + } + if (!validUtf8(payload.data() + 2, payload.size() - 2)) { + invalidPayload(); + return; + } + } + if (closeSent_) { + queueEom(); + return; + } + finished_ = true; + queueFrame(0x08, payload, true); + } else if (opcode == 0x09) { + queueFrame(0x0a, payload); + } else if (opcode != 0x0a) { + protocolError(); + } + return; + } + + if (opcode == 0x00) { + if (fragmentOpcode_ == 0 || + payload.size() > kMaxWebSocketMessage - fragmentPayload_.size()) { + protocolError(); + return; + } + fragmentPayload_.insert(fragmentPayload_.end(), payload.begin(), + payload.end()); + if (fin) { + if (fragmentOpcode_ == 0x01 && !validUtf8(fragmentPayload_)) { + invalidPayload(); + return; + } + queueFrame(fragmentOpcode_, fragmentPayload_); + fragmentOpcode_ = 0; + fragmentPayload_.clear(); + } + return; + } + + if ((opcode != 0x01 && opcode != 0x02) || fragmentOpcode_ != 0) { + protocolError(); + return; + } + if (fin) { + if (opcode == 0x01 && !validUtf8(payload)) { + invalidPayload(); + return; + } + queueFrame(opcode, payload); + return; + } + fragmentOpcode_ = opcode; + fragmentPayload_ = std::move(payload); + } + + void processFrames() { + size_t cursor = 0; + while (!finished_) { + if (input_.size() - cursor < 2) { + break; + } + const uint8_t first = input_[cursor]; + const uint8_t second = input_[cursor + 1]; + const bool fin = (first & 0x80U) != 0; + const uint8_t opcode = first & 0x0fU; + const uint8_t encodedLength = second & 0x7fU; + if ((first & 0x70U) != 0 || (second & 0x80U) == 0 || + ((opcode & 0x08U) != 0 && encodedLength > 125)) { + protocolError(); + cursor = input_.size(); + break; + } + + uint64_t payloadLength = encodedLength; + size_t headerLength = 2; + if (payloadLength == 126) { + if (input_.size() - cursor < 4) { + break; + } + payloadLength = (static_cast(input_[cursor + 2]) << 8) | + input_[cursor + 3]; + if (payloadLength < 126) { + protocolError(); + cursor = input_.size(); + break; + } + headerLength = 4; + } else if (payloadLength == 127) { + if (input_.size() - cursor < 10) { + break; + } + if ((input_[cursor + 2] & 0x80U) != 0) { + protocolError(); + cursor = input_.size(); + break; + } + payloadLength = 0; + for (size_t index = 0; index < 8; ++index) { + payloadLength = (payloadLength << 8) | input_[cursor + 2 + index]; + } + if (payloadLength <= std::numeric_limits::max()) { + protocolError(); + cursor = input_.size(); + break; + } + headerLength = 10; + } + if (payloadLength > kMaxWebSocketMessage || + payloadLength > + std::numeric_limits::max() - headerLength - 4) { + protocolError(); + cursor = input_.size(); + break; + } + + const size_t frameLength = + headerLength + 4 + static_cast(payloadLength); + if (input_.size() - cursor < frameLength) { + break; + } + const size_t maskOffset = cursor + headerLength; + const size_t payloadOffset = maskOffset + 4; + std::vector payload(static_cast(payloadLength)); + for (size_t index = 0; index < payload.size(); ++index) { + payload[index] = + input_[payloadOffset + index] ^ input_[maskOffset + (index % 4)]; + } + cursor += frameLength; + handleFrame(fin, opcode, std::move(payload)); + if (closeSent_ || finished_) { + break; + } + } + + if (cursor > 0) { + input_.erase(input_.begin(), input_.begin() + cursor); + } + if (closeSent_ || finished_) { + input_.clear(); + } + } + + HTTPSourceHolder request_; + std::deque pending_; + std::vector input_; + std::vector fragmentPayload_; + uint8_t fragmentOpcode_{0}; + bool closeSent_{false}; + bool finished_{false}; +}; + +class ArenaCoroHandler final : public HTTPHandler { +public: + explicit ArenaCoroHandler(std::shared_ptr dataset) + : dataset_(std::move(dataset)) {} + + folly::coro::Task + handleRequest(folly::EventBase * /*eventBase*/, + HTTPSessionContextPtr /*session*/, + HTTPSourceHolder requestSource) override { + auto headerTry = co_await co_awaitTry(requestSource.readHeaderEvent()); + if (headerTry.hasException()) { + co_return makeTextResponse(400, "bad request"); + } + auto header = std::move(*headerTry); + auto request = std::move(header.headers); + const bool requestEom = header.eom; + const auto method = request->getMethod().value_or(HTTPMethod::GET); + const std::string path = request->getPath(); + + if (path == "/ws") { + const auto &headers = request->getHeaders(); + const auto &key = headers.getSingleOrEmpty("Sec-WebSocket-Key"); + const auto &version = headers.getSingleOrEmpty("Sec-WebSocket-Version"); + if (method != HTTPMethod::GET || !request->isIngressWebsocketUpgrade() || + !validWebSocketKey(std::string_view(key.data(), key.size())) || + version != "13") { + auto *response = makeTextResponse(426, "WebSocket upgrade required"); + response->msg_->getHeaders().set("Sec-WebSocket-Version", "13"); + co_return response; + } + co_return new WebSocketSource(std::move(requestSource)); + } + + if (path == "/baseline11" || path == "/baseline2") { + const bool allowPost = path == "/baseline11"; + if (method != HTTPMethod::GET && + (!allowPost || method != HTTPMethod::POST)) { + co_return makeTextResponse(405, "method not allowed"); + } + int64_t a = 0; + int64_t b = 0; + if (!parseInteger(request->getQueryParam("a"), a) || + !parseInteger(request->getQueryParam("b"), b)) { + co_return makeTextResponse(400, "invalid integer"); + } + int64_t sum = 0; + if (!checkedAdd(a, b, sum)) { + co_return makeTextResponse(400, "integer overflow"); + } + if (method == HTTPMethod::POST) { + auto body = co_await readRequestBody(requestSource, requestEom, + kMaxBaselineBody); + int64_t bodyValue = 0; + if (!body.readOk || !body.captureOk || + !parseInteger(body.captured, bodyValue) || + !checkedAdd(sum, bodyValue, sum)) { + co_return makeTextResponse(400, "invalid integer"); + } + } + co_return makeTextResponse(200, std::to_string(sum)); + } + + if (path == "/pipeline") { + if (method != HTTPMethod::GET) { + co_return makeTextResponse(405, "method not allowed"); + } + co_return makeTextResponse(200, "ok"); + } + + if (path.starts_with(kJsonPrefix)) { + if (method != HTTPMethod::GET) { + co_return makeTextResponse(405, "method not allowed"); + } + const std::string_view countText(path.data() + kJsonPrefix.size(), + path.size() - kJsonPrefix.size()); + int64_t count = 0; + int64_t multiplier = 1; + const auto multiplierText = request->getQueryParam("m"); + if (!parseInteger(countText, count) || count < 1 || count > 50 || + (!multiplierText.empty() && + !parseInteger(multiplierText, multiplier))) { + co_return makeTextResponse(400, "invalid JSON parameters"); + } + + try { + folly::dynamic items = folly::dynamic::array; + for (int64_t index = 0; index < count; ++index) { + folly::dynamic item = (*dataset_)[static_cast(index)]; + int64_t subtotal = 0; + int64_t total = 0; + if (!checkedMultiply(item["price"].asInt(), item["quantity"].asInt(), + subtotal) || + !checkedMultiply(subtotal, multiplier, total)) { + co_return makeTextResponse(400, "integer overflow"); + } + item["total"] = total; + items.push_back(std::move(item)); + } + folly::dynamic response = folly::dynamic::object; + response["items"] = std::move(items); + response["count"] = count; + co_return makeResponse(200, "application/json", + folly::toJson(response)); + } catch (const std::exception &) { + co_return makeTextResponse(500, "JSON serialization failed"); + } + } + + if (path == "/upload") { + if (method != HTTPMethod::POST) { + co_return makeTextResponse(405, "method not allowed"); + } + auto body = co_await readRequestBody(requestSource, requestEom, 0); + if (!body.readOk) { + co_return makeTextResponse(400, "upload failed"); + } + co_return makeTextResponse(200, std::to_string(body.size)); + } + + if (path.starts_with(kStaticPrefix)) { + if (method != HTTPMethod::GET) { + co_return makeTextResponse(405, "method not allowed"); + } + const std::string name(path.data() + kStaticPrefix.size(), + path.size() - kStaticPrefix.size()); + if (name.empty() || name.find('/') != std::string::npos || + name.find('\\') != std::string::npos || + name.find("..") != std::string::npos) { + co_return makeTextResponse(404, "not found"); + } + std::ifstream input(std::string(kStaticRoot) + name, std::ios::binary); + if (!input) { + co_return makeTextResponse(404, "not found"); + } + std::string body((std::istreambuf_iterator(input)), + std::istreambuf_iterator()); + if (!input.good() && !input.eof()) { + co_return makeTextResponse(500, "read error"); + } + co_return makeResponse(200, contentType(name), std::move(body)); + } + + co_return makeTextResponse(404, "not found"); + } + +private: + std::shared_ptr dataset_; +}; + +std::shared_ptr> compressibleTypes() { + return std::make_shared>(std::set{ + "application/javascript", "application/json", "image/svg+xml", "text/css", + "text/html", "text/plain"}); +} + +constexpr uint32_t kH2StreamWindow = 1U << 20; +constexpr size_t kH2ConnectionWindow = 10U << 20; +constexpr uint32_t kMaxConcurrentStreams = 1024; + +HTTPServer::SessionConfig makeSessionConfig() { + HTTPServer::SessionConfig session; + session.settings = { + {proxygen::SettingsId::MAX_HEADER_LIST_SIZE, 32 * 1024}, + {proxygen::SettingsId::HEADER_TABLE_SIZE, 4096}, + {proxygen::SettingsId::MAX_FRAME_SIZE, 16384}, + {proxygen::SettingsId::MAX_CONCURRENT_STREAMS, kMaxConcurrentStreams}, + {proxygen::SettingsId::INITIAL_WINDOW_SIZE, kH2StreamWindow}}; + session.streamFlowControl = kH2StreamWindow; + session.connFlowControl = kH2ConnectionWindow; + session.streamReadTimeout = std::chrono::seconds(60); + session.connIdleTimeout = std::chrono::seconds(60); + return session; +} + +void addCompressionFilter(HTTPServer::Config &config) { + proxygen::CompressionFilterUtils::FactoryOptions options; + options.compressibleContentTypes = compressibleTypes(); + config.filterFactories.push_back( + std::make_shared( + std::move(options))); +} + +wangle::SSLContextConfig tlsConfig(std::list protocols) { + auto config = HTTPServer::getDefaultTLSConfig(); + config.setCertificate(FLAGS_cert, FLAGS_key, ""); + config.setNextProtocols(protocols); + return config; +} + +std::shared_ptr +acceptorConfig(const HTTPServer::Config &serverConfig, + std::string plaintextProtocol, + std::list tlsProtocols = {}) { + auto config = std::make_shared(); + *static_cast(config.get()) = + serverConfig.socketConfig; + config->sslContextConfigs.clear(); + if (!tlsProtocols.empty()) { + config->sslContextConfigs.push_back(tlsConfig(std::move(tlsProtocols))); + } + config->egressSettings = serverConfig.sessionConfig.settings; + config->transactionIdleTimeout = serverConfig.sessionConfig.streamReadTimeout; + config->initialReceiveWindow = kH2StreamWindow; + config->receiveStreamWindowSize = kH2StreamWindow; + config->receiveSessionWindowSize = kH2ConnectionWindow; + config->maxConcurrentIncomingStreams = kMaxConcurrentStreams; + config->plaintextProtocol = std::move(plaintextProtocol); + config->forceHTTP1_0_to_1_1 = true; + config->connectionIdleTimeout = serverConfig.sessionConfig.connIdleTimeout; + config->readBufNewAllocSize = serverConfig.sessionConfig.readBufNewAllocSize; + return config; +} + +HTTPServer::SocketAcceptorConfigFactoryFn tcpSocketFactory() { + return [](folly::EventBase &eventBase, + const HTTPServer::Config &serverConfig) { + std::vector listeners; + const auto addListener = [&](uint16_t port, std::string protocol, + std::list tlsProtocols = {}) { + folly::AsyncServerSocket::UniquePtr socket( + new folly::AsyncServerSocket(&eventBase)); + socket->bind(folly::SocketAddress(FLAGS_ip, port, true)); + socket->listen(serverConfig.socketConfig.acceptBacklog); + listeners.emplace_back(std::move(socket), + acceptorConfig(serverConfig, std::move(protocol), + std::move(tlsProtocols))); + }; + + addListener(static_cast(FLAGS_http_port), "http/1.1"); + addListener(static_cast(FLAGS_tls_port), "http/1.1", + {"http/1.1"}); + addListener(static_cast(FLAGS_h2c_port), "h2c"); + addListener(static_cast(FLAGS_h2_port), "", {"h2"}); + return listeners; + }; +} + +HTTPServer::Config tcpConfig(size_t threads) { + HTTPServer::Config config; + config.numIOThreads = threads; + config.shutdownOnSignals = {SIGINT, SIGTERM}; + config.sessionConfig = makeSessionConfig(); + addCompressionFilter(config); + return config; +} + +HTTPServer::Config quicConfig(size_t threads) { + HTTPServer::Config config; + config.socketConfig.bindAddress = + folly::SocketAddress(FLAGS_ip, FLAGS_h3_port, true); + config.socketConfig.sslContextConfigs.push_back(tlsConfig({"h3"})); + config.numIOThreads = threads; + config.shutdownOnSignals = {}; + config.sessionConfig = makeSessionConfig(); + config.quicConfig = HTTPServer::QuicConfig(); + config.quicConfig->supportedAlpns = {"h3"}; + auto &transport = config.quicConfig->transportSettings; + transport.maxNumPTOs = 1000; + transport.maxCwndInMss = quic::kLargeMaxCwndInMss; + transport.batchingMode = quic::QuicBatchingMode::BATCHING_MODE_GSO; + transport.maxBatchSize = 48; + transport.dataPathType = quic::DataPathType::ContinuousMemory; + transport.writeConnectionDataPacketsLimit = 48; + addCompressionFilter(config); + return config; +} + +} // namespace + +int main(int argc, char *argv[]) { + const folly::Init init(&argc, &argv, true); + + try { + auto dataset = loadDataset(); + const size_t threads = + FLAGS_threads <= 0 ? static_cast(folly::available_concurrency()) + : static_cast(FLAGS_threads); + auto handler = std::make_shared(std::move(dataset)); + + HTTPServer tcpServer(tcpConfig(threads), handler, tcpSocketFactory()); + HTTPServer h3Server(quicConfig(threads), std::move(handler)); + + std::promise h3Ready; + auto h3ReadyFuture = h3Ready.get_future(); + std::thread h3Thread([&] { + try { + h3Server.start([&] { h3Ready.set_value(); }); + } catch (...) { + try { + h3Ready.set_exception(std::current_exception()); + } catch (const std::future_error &) { + } + } + }); + + try { + h3ReadyFuture.get(); + } catch (...) { + h3Thread.join(); + throw; + } + + try { + tcpServer.start(); + } catch (...) { + h3Server.forceStop(); + h3Thread.join(); + throw; + } + + h3Server.forceStop(); + h3Thread.join(); + } catch (const std::exception &error) { + std::cerr << "failed to start Proxygen coroutine HttpArena server: " + << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/frameworks/proxygen-coro/CMakeLists.txt b/frameworks/proxygen-coro/CMakeLists.txt new file mode 100644 index 000000000..33a700e02 --- /dev/null +++ b/frameworks/proxygen-coro/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.20) + +project(httparena-proxygen-coro LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Proxygen's exported Fizz package calls find_dependency(Sodium). The official +# builder image keeps that upstream find module with the Proxygen source tree. +list(APPEND CMAKE_MODULE_PATH "/proxygen/build/fbcode_builder/CMake") + +find_package(c-ares CONFIG REQUIRED) +add_library(cares ALIAS c-ares::cares) +find_package(proxygen CONFIG REQUIRED) + +add_executable(proxygen-arena-coro ArenaCoroServer.cpp) +target_compile_options(proxygen-arena-coro PRIVATE -Wall -Wextra -Wpedantic) +target_link_libraries( + proxygen-arena-coro + PRIVATE + proxygen::proxygen + proxygen::proxygen_coro + proxygen::proxygen_coro_server + proxygen::proxygen_http_coro_filters_compression_filter_factory + Folly::folly_init_init + Folly::folly_portability_gflags +) diff --git a/frameworks/proxygen-coro/Dockerfile b/frameworks/proxygen-coro/Dockerfile new file mode 100644 index 000000000..703ee4ae1 --- /dev/null +++ b/frameworks/proxygen-coro/Dockerfile @@ -0,0 +1,32 @@ +FROM ghcr.io/facebook/proxygen/base:latest AS build + +WORKDIR /arena +COPY CMakeLists.txt ArenaCoroServer.cpp ./ +RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ + && cmake --build build --parallel "$(nproc)" \ + && strip build/proxygen-arena-coro + +RUN set -eux; \ + ldd build/proxygen-arena-coro \ + | awk '/=> \// { print $3 } /^\// { print $1 }' | sort -u > /tmp/runtime-libs.txt; \ + tar -chf /tmp/runtime-libs.tar --files-from=/tmp/runtime-libs.txt + +FROM ubuntu:24.04@sha256:019e8eb29a85e74d64925745884f2ec79aa27e3feab36353d24656f4d6b89467 + +ENV LD_LIBRARY_PATH=/opt/proxygen/lib + +COPY --from=build /tmp/runtime-libs.tar /tmp/runtime-libs.tar +RUN tar -xf /tmp/runtime-libs.tar -C / \ + && rm /tmp/runtime-libs.tar + +COPY --from=build /arena/build/proxygen-arena-coro /usr/local/bin/proxygen-arena-coro +COPY entrypoint.sh /usr/local/bin/proxygen-coro-entrypoint +RUN chmod +x /usr/local/bin/proxygen-coro-entrypoint \ + && groupadd --system --gid 10001 httparena \ + && useradd --system --uid 10001 --gid httparena --no-create-home \ + --home-dir /nonexistent --shell /usr/sbin/nologin httparena + +EXPOSE 8080/tcp 8081/tcp 8082/tcp 8443/tcp 8443/udp + +USER httparena +ENTRYPOINT ["/usr/local/bin/proxygen-coro-entrypoint"] diff --git a/frameworks/proxygen-coro/README.md b/frameworks/proxygen-coro/README.md new file mode 100644 index 000000000..7ad4114f9 --- /dev/null +++ b/frameworks/proxygen-coro/README.md @@ -0,0 +1,52 @@ +# proxygen-coro + +This engine exercises Proxygen's native coroutine server stack rather than the +callback `RequestHandler` / `HTTPTransactionHandler` APIs used by the regular +`proxygen` entry. + +`ArenaCoroServer.cpp` implements `proxygen::coro::HTTPHandler`, consumes +requests through `HTTPSourceHolder`, and returns `HTTPFixedSource` responses. +Uploads are counted while asynchronously draining BODY events. WebSockets use +a long-lived custom `HTTPSource`: its response calls +`setEgressWebsocketUpgrade()`, then parses and emits RFC 6455 frames over the +raw upgraded BODY event stream. Response compression is provided by the coro +`ServerCompressionFilterFactory`. + +## Listener layout + +One process owns all five listeners: + +- `8080/tcp`: HTTP/1.1 and WebSockets +- `8081/tcp`: HTTP/1.1 over TLS, ALPN `http/1.1` +- `8082/tcp`: prior-knowledge h2c +- `8443/tcp`: HTTP/2 over TLS, ALPN `h2` +- `8443/udp`: HTTP/3 over QUIC, ALPN `h3` + +The four TCP listeners are acceptors on one coro `HTTPServer`, so they share a +single affinity-aware I/O pool. Proxygen's coro API selects either TCP or QUIC +per server, so HTTP/3 uses a second in-process pool; whichever transport is not +being benchmarked remains idle. Override the available-CPU default with +`PROXYGEN_CORO_THREADS`. + +HTTP/2 advertises 1024 concurrent streams with a 1 MiB stream window and a +10 MiB connection window. HTTP/3 mirrors Proxygen's coroutine benchmark +settings: GSO batches of 48 packets, continuous-memory writes, a large +congestion window, and a 48-packet connection write limit. + +## Upstream image and build + +The self-contained Docker build tracks the official +`ghcr.io/facebook/proxygen/base:latest` builder image. The runtime stage copies +only the compiled binary and its dynamically linked libraries into a pinned +Ubuntu 24.04 image, then runs as the non-root `httparena` user (UID/GID 10001). + +From the repository root: + +```bash +./scripts/validate.sh proxygen-coro +./scripts/run.sh proxygen-coro +``` + +The implementation follows the upstream coroutine echo server and the +`H12DownstreamSessionTest.WebSocketUpgrade` test, which documents upgraded +raw bytes flowing through coroutine BODY events. diff --git a/frameworks/proxygen-coro/entrypoint.sh b/frameworks/proxygen-coro/entrypoint.sh new file mode 100755 index 000000000..c96668cc6 --- /dev/null +++ b/frameworks/proxygen-coro/entrypoint.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +exec /usr/local/bin/proxygen-arena-coro \ + --ip=:: \ + --http_port=8080 \ + --tls_port=8081 \ + --h2c_port=8082 \ + --h2_port=8443 \ + --h3_port=8443 \ + --cert=/certs/server.crt \ + --key=/certs/server.key \ + --threads="${PROXYGEN_CORO_THREADS:-0}" diff --git a/frameworks/proxygen-coro/meta.json b/frameworks/proxygen-coro/meta.json new file mode 100644 index 000000000..6473059a8 --- /dev/null +++ b/frameworks/proxygen-coro/meta.json @@ -0,0 +1,30 @@ +{ + "display_name": "proxygen-coro", + "language": "C++", + "type": "engine", + "engine": "proxygen", + "description": "Meta's Proxygen native coroutine HTTPServer and HTTPSource APIs across HTTP/1.1, HTTP/1.1 TLS, h2c, HTTP/2 TLS, HTTP/3 QUIC, and RFC 6455 WebSockets.", + "repo": "https://github.com/facebook/proxygen", + "enabled": true, + "tests": [ + "baseline", + "json", + "json-comp", + "json-tls", + "upload", + "static", + "static-tls", + "pipelined", + "limited-conn", + "baseline-h2", + "baseline-h2c", + "json-h2c", + "static-h2", + "baseline-h3", + "static-h3", + "echo-ws", + "echo-ws-pipeline", + "echo-ws-limited" + ], + "maintainers": [] +} diff --git a/frameworks/proxygen/ArenaCommon.h b/frameworks/proxygen/ArenaCommon.h new file mode 100644 index 000000000..3e4f9b32b --- /dev/null +++ b/frameworks/proxygen/ArenaCommon.h @@ -0,0 +1,214 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace httparena { + +inline constexpr uint64_t kMaxWebSocketMessage = 16ULL * 1024 * 1024; +inline constexpr std::string_view kJsonPrefix = "/json/"; +inline constexpr std::string_view kStaticPrefix = "/static/"; +inline constexpr std::string_view kStaticRoot = "/data/static/"; + +inline bool parseInteger(std::string_view input, int64_t &value) { + while (!input.empty() && + std::isspace(static_cast(input.front()))) { + input.remove_prefix(1); + } + while (!input.empty() && + std::isspace(static_cast(input.back()))) { + input.remove_suffix(1); + } + if (input.empty()) { + return false; + } + const auto result = + std::from_chars(input.data(), input.data() + input.size(), value); + return result.ec == std::errc() && result.ptr == input.data() + input.size(); +} + +inline bool checkedAdd(int64_t lhs, int64_t rhs, int64_t &result) { +#if defined(__GNUC__) || defined(__clang__) + return !__builtin_add_overflow(lhs, rhs, &result); +#else + if ((rhs > 0 && lhs > std::numeric_limits::max() - rhs) || + (rhs < 0 && lhs < std::numeric_limits::min() - rhs)) { + return false; + } + result = lhs + rhs; + return true; +#endif +} + +inline bool checkedMultiply(int64_t lhs, int64_t rhs, int64_t &result) { +#if defined(__GNUC__) || defined(__clang__) + return !__builtin_mul_overflow(lhs, rhs, &result); +#else + if (lhs > 0) { + if ((rhs > 0 && lhs > std::numeric_limits::max() / rhs) || + (rhs < 0 && rhs < std::numeric_limits::min() / lhs)) { + return false; + } + } else if (lhs < 0) { + if ((rhs > 0 && lhs < std::numeric_limits::min() / rhs) || + (rhs < 0 && rhs < std::numeric_limits::max() / lhs)) { + return false; + } + } + result = lhs * rhs; + return true; +#endif +} + +inline std::string contentType(std::string_view name) { + const auto endsWith = [name](std::string_view suffix) { + return name.size() >= suffix.size() && + name.substr(name.size() - suffix.size()) == suffix; + }; + if (endsWith(".css")) { + return "text/css"; + } + if (endsWith(".js")) { + return "application/javascript"; + } + if (endsWith(".html")) { + return "text/html"; + } + if (endsWith(".json")) { + return "application/json"; + } + if (endsWith(".svg")) { + return "image/svg+xml"; + } + if (endsWith(".webp")) { + return "image/webp"; + } + if (endsWith(".woff2")) { + return "font/woff2"; + } + return "application/octet-stream"; +} + +inline std::shared_ptr loadDataset() { + std::ifstream input("/data/dataset.json", std::ios::binary); + if (!input) { + throw std::runtime_error("cannot open /data/dataset.json"); + } + std::string contents((std::istreambuf_iterator(input)), + std::istreambuf_iterator()); + auto dataset = folly::parseJson(contents); + if (!dataset.isArray() || dataset.size() < 50) { + throw std::runtime_error("/data/dataset.json must contain 50 items"); + } + return std::make_shared(std::move(dataset)); +} + +inline bool validWebSocketKey(std::string_view key) noexcept { + if (key.size() != 24) { + return false; + } + std::array decoded{}; + const auto result = folly::base64Decode(key, decoded.data()); + return result.is_success && result.o == decoded.data() + 16; +} + +inline bool validUtf8(const uint8_t *data, size_t size) noexcept { + const auto continuation = [](uint8_t byte) { + return byte >= 0x80 && byte <= 0xbf; + }; + + size_t index = 0; + while (index < size) { + const uint8_t first = data[index]; + if (first <= 0x7f) { + ++index; + continue; + } + if (first >= 0xc2 && first <= 0xdf) { + if (index + 1 >= size || !continuation(data[index + 1])) { + return false; + } + index += 2; + continue; + } + if (first == 0xe0) { + if (index + 2 >= size || data[index + 1] < 0xa0 || + data[index + 1] > 0xbf || !continuation(data[index + 2])) { + return false; + } + index += 3; + continue; + } + if ((first >= 0xe1 && first <= 0xec) || (first >= 0xee && first <= 0xef)) { + if (index + 2 >= size || !continuation(data[index + 1]) || + !continuation(data[index + 2])) { + return false; + } + index += 3; + continue; + } + if (first == 0xed) { + if (index + 2 >= size || data[index + 1] < 0x80 || + data[index + 1] > 0x9f || !continuation(data[index + 2])) { + return false; + } + index += 3; + continue; + } + if (first == 0xf0) { + if (index + 3 >= size || data[index + 1] < 0x90 || + data[index + 1] > 0xbf || !continuation(data[index + 2]) || + !continuation(data[index + 3])) { + return false; + } + index += 4; + continue; + } + if (first >= 0xf1 && first <= 0xf3) { + if (index + 3 >= size || !continuation(data[index + 1]) || + !continuation(data[index + 2]) || !continuation(data[index + 3])) { + return false; + } + index += 4; + continue; + } + if (first == 0xf4) { + if (index + 3 >= size || data[index + 1] < 0x80 || + data[index + 1] > 0x8f || !continuation(data[index + 2]) || + !continuation(data[index + 3])) { + return false; + } + index += 4; + continue; + } + return false; + } + return true; +} + +inline bool validUtf8(const std::vector &data) noexcept { + return validUtf8(data.data(), data.size()); +} + +inline bool validWebSocketCloseCode(uint16_t code) noexcept { + const bool definedProtocolCode = code >= 1000 && code <= 1014 && + code != 1004 && code != 1005 && code != 1006; + const bool applicationCode = code >= 3000 && code <= 4999; + return definedProtocolCode || applicationCode; +} + +} // namespace httparena diff --git a/frameworks/proxygen/ArenaHQServer.cpp b/frameworks/proxygen/ArenaHQServer.cpp new file mode 100644 index 000000000..1bccb09b6 --- /dev/null +++ b/frameworks/proxygen/ArenaHQServer.cpp @@ -0,0 +1,72 @@ +#include "ArenaHQServer.h" + +#include + +#include +#include +#include + +namespace { + +quic::samples::HQServerParams makeHQParams(size_t ioThreads) { + quic::samples::HQServerParams params; + params.serverThreads = ioThreads; + params.transportSettings.maxNumPTOs = 1000; + params.transportSettings.maxCwndInMss = quic::kLargeMaxCwndInMss; + params.transportSettings.batchingMode = + quic::QuicBatchingMode::BATCHING_MODE_GSO; + params.transportSettings.maxBatchSize = 48; + params.transportSettings.dataPathType = quic::DataPathType::ContinuousMemory; + params.transportSettings.writeConnectionDataPacketsLimit = 48; + return params; +} + +} // namespace + +namespace httparena { + +class ArenaHQServer::Impl final { +public: + Impl(const std::string &certificatePath, const std::string &privateKeyPath, + size_t ioThreads, HandlerProvider handlerProvider) + : server_(makeHQParams(ioThreads), std::move(handlerProvider), nullptr, + quic::samples::createFizzServerContext( + quic::samples::kDefaultSupportedAlpns, + fizz::server::ClientAuthMode::None, certificatePath, + privateKeyPath)) {} + + ~Impl() { stop(); } + + void start(const folly::SocketAddress &address) { + started_ = true; + server_.start(address); + server_.getAddress(); + } + + void stop() { + if (started_) { + server_.stop(); + started_ = false; + } + } + +private: + quic::samples::HQServer server_; + bool started_{false}; +}; + +ArenaHQServer::ArenaHQServer(const std::string &certificatePath, + const std::string &privateKeyPath, + size_t ioThreads, HandlerProvider handlerProvider) + : impl_(std::make_unique(certificatePath, privateKeyPath, ioThreads, + std::move(handlerProvider))) {} + +ArenaHQServer::~ArenaHQServer() = default; + +void ArenaHQServer::start(const folly::SocketAddress &address) { + impl_->start(address); +} + +void ArenaHQServer::stop() { impl_->stop(); } + +} // namespace httparena diff --git a/frameworks/proxygen/ArenaHQServer.h b/frameworks/proxygen/ArenaHQServer.h new file mode 100644 index 000000000..34c985b36 --- /dev/null +++ b/frameworks/proxygen/ArenaHQServer.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace proxygen { +class HTTPMessage; +class HTTPTransactionHandler; +} // namespace proxygen + +namespace httparena { + +class ArenaHQServer final { +public: + using HandlerProvider = std::function; + + ArenaHQServer(const std::string &certificatePath, + const std::string &privateKeyPath, size_t ioThreads, + HandlerProvider handlerProvider); + ~ArenaHQServer(); + + ArenaHQServer(const ArenaHQServer &) = delete; + ArenaHQServer &operator=(const ArenaHQServer &) = delete; + + void start(const folly::SocketAddress &address); + void stop(); + +private: + class Impl; + std::unique_ptr impl_; +}; + +} // namespace httparena diff --git a/frameworks/proxygen/ArenaHttpServer.cpp b/frameworks/proxygen/ArenaHttpServer.cpp new file mode 100644 index 000000000..09d504575 --- /dev/null +++ b/frameworks/proxygen/ArenaHttpServer.cpp @@ -0,0 +1,694 @@ +#include "ArenaCommon.h" +#include "ArenaHQServer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using folly::SocketAddress; +using proxygen::HTTPMessage; +using proxygen::HTTPMethod; +using proxygen::HTTPServer; +using proxygen::ProxygenError; +using proxygen::RequestHandler; +using proxygen::RequestHandlerChain; +using proxygen::RequestHandlerFactory; +using proxygen::ResponseBuilder; +using proxygen::UpgradeProtocol; + +DEFINE_int32(http_port, 8080, "HTTP/1.1 and WebSocket port"); +DEFINE_int32(tls_port, 8081, "HTTP/1.1 over TLS port"); +DEFINE_int32(h2c_port, 8082, "HTTP/2 cleartext port"); +DEFINE_int32(h2_port, 8443, "HTTP/2 over TLS port"); +DEFINE_int32(h3_port, 8443, "HTTP/3 over QUIC port"); +DEFINE_string(ip, "::", "Address on which to listen"); +DEFINE_string(cert, "/certs/server.crt", "TLS certificate path"); +DEFINE_string(key, "/certs/server.key", "TLS private-key path"); +DEFINE_int32(threads, 0, "I/O threads; 0 uses the available CPU count"); + +namespace { + +constexpr size_t kMaxRequestBody = 1024; +using httparena::checkedAdd; +using httparena::checkedMultiply; +using httparena::contentType; +using httparena::kJsonPrefix; +using httparena::kMaxWebSocketMessage; +using httparena::kStaticPrefix; +using httparena::kStaticRoot; +using httparena::loadDataset; +using httparena::parseInteger; +using httparena::validUtf8; +using httparena::validWebSocketCloseCode; +using httparena::validWebSocketKey; + +class ArenaHandler final : public RequestHandler { +public: + explicit ArenaHandler(std::shared_ptr dataset) + : dataset_(std::move(dataset)) {} + + void onRequest(std::unique_ptr request) noexcept override { + const auto path = request->getPath(); + const auto method = request->getMethod(); + + if (path == "/ws") { + route_ = Route::WebSocket; + const auto &headers = request->getHeaders(); + const auto &key = headers.getSingleOrEmpty("Sec-WebSocket-Key"); + const auto &version = headers.getSingleOrEmpty("Sec-WebSocket-Version"); + if (!method || *method != HTTPMethod::GET || + !request->isIngressWebsocketUpgrade() || + !validWebSocketKey(std::string_view(key.data(), key.size())) || + version != "13") { + ResponseBuilder(downstream_) + .status(426, "Upgrade Required") + .header("Content-Type", "text/plain") + .header("Sec-WebSocket-Version", "13") + .body("WebSocket upgrade required") + .sendWithEOM(); + responseFinished_ = true; + return; + } + + ResponseBuilder(downstream_) + .status(101, "Switching Protocols") + .setEgressWebsocketHeaders() + .send(); + websocketAccepted_ = true; + return; + } + + method_ = method.value_or(HTTPMethod::GET); + if (path == "/baseline11") { + route_ = Route::Baseline; + queryValid_ = parseInteger(request->getQueryParam("a"), a_) && + parseInteger(request->getQueryParam("b"), b_); + return; + } + if (path == "/baseline2") { + route_ = Route::BaselineH2; + queryValid_ = parseInteger(request->getQueryParam("a"), a_) && + parseInteger(request->getQueryParam("b"), b_); + return; + } + if (path.starts_with(kJsonPrefix)) { + route_ = Route::Json; + const std::string_view countText(path.data() + kJsonPrefix.size(), + path.size() - kJsonPrefix.size()); + int64_t count = 0; + const auto multiplierText = request->getQueryParam("m"); + const bool multiplierValid = + multiplierText.empty() ? (multiplier_ = 1, true) + : parseInteger(multiplierText, multiplier_); + jsonValid_ = parseInteger(countText, count) && count >= 1 && + count <= 50 && multiplierValid; + if (jsonValid_) { + jsonCount_ = static_cast(count); + } + return; + } + if (path == "/upload") { + route_ = Route::Upload; + return; + } + if (path.starts_with(kStaticPrefix)) { + route_ = Route::Static; + staticName_.assign(path.data() + kStaticPrefix.size(), + path.size() - kStaticPrefix.size()); + return; + } + if (path == "/pipeline") { + route_ = Route::Pipeline; + return; + } + route_ = Route::NotFound; + } + + void onBody(std::unique_ptr body) noexcept override { + if (!body || responseFinished_) { + return; + } + if (route_ == Route::Upload) { + const size_t bytes = body->computeChainDataLength(); + if (bytes > std::numeric_limits::max() - uploadBytes_) { + uploadValid_ = false; + } else { + uploadBytes_ += bytes; + } + return; + } + if (route_ == Route::WebSocket && websocketActive_) { + auto bytes = body->coalesce(); + websocketBytes_.insert(websocketBytes_.end(), bytes.begin(), bytes.end()); + processWebSocketFrames(); + return; + } + if (route_ != Route::Baseline) { + return; + } + auto bytes = body->coalesce(); + if (requestBody_.size() + bytes.size() > kMaxRequestBody) { + bodyValid_ = false; + return; + } + requestBody_.append(reinterpret_cast(bytes.data()), + bytes.size()); + } + + void onUpgrade(UpgradeProtocol /*protocol*/) noexcept override { + if (route_ != Route::WebSocket || !websocketAccepted_) { + downstream_->sendAbort(); + return; + } + websocketActive_ = true; + } + + void onEOM() noexcept override { + if (responseFinished_) { + return; + } + switch (route_) { + case Route::WebSocket: + responseFinished_ = true; + downstream_->sendEOM(); + return; + case Route::NotFound: + sendText(404, "Not Found", "not found"); + return; + case Route::Pipeline: + if (method_ != HTTPMethod::GET) { + sendText(405, "Method Not Allowed", "method not allowed"); + } else { + sendText(200, "OK", "ok"); + } + return; + case Route::Baseline: + handleBaseline(true); + return; + case Route::BaselineH2: + handleBaseline(false); + return; + case Route::Json: + handleJson(); + return; + case Route::Upload: + if (method_ != HTTPMethod::POST) { + sendText(405, "Method Not Allowed", "method not allowed"); + } else if (!uploadValid_) { + sendText(400, "Bad Request", "upload too large"); + } else { + sendText(200, "OK", std::to_string(uploadBytes_)); + } + return; + case Route::Static: + handleStatic(); + return; + } + } + + void requestComplete() noexcept override { delete this; } + + void onError(ProxygenError /*error*/) noexcept override { delete this; } + +private: + enum class Route { + NotFound, + Baseline, + BaselineH2, + Json, + Upload, + Static, + Pipeline, + WebSocket + }; + + void handleBaseline(bool allowPost) { + if (!queryValid_ || !bodyValid_) { + sendText(400, "Bad Request", "invalid integer"); + return; + } + if (method_ != HTTPMethod::GET && + (!allowPost || method_ != HTTPMethod::POST)) { + sendText(405, "Method Not Allowed", "method not allowed"); + return; + } + + int64_t sum = 0; + if (!checkedAdd(a_, b_, sum)) { + sendText(400, "Bad Request", "integer overflow"); + return; + } + if (method_ == HTTPMethod::POST) { + int64_t bodyValue = 0; + if (!parseInteger(requestBody_, bodyValue) || + !checkedAdd(sum, bodyValue, sum)) { + sendText(400, "Bad Request", "invalid integer"); + return; + } + } + sendText(200, "OK", std::to_string(sum)); + } + + void handleJson() { + if (method_ != HTTPMethod::GET) { + sendText(405, "Method Not Allowed", "method not allowed"); + return; + } + if (!jsonValid_ || jsonCount_ > dataset_->size()) { + sendText(400, "Bad Request", "invalid JSON parameters"); + return; + } + + try { + folly::dynamic items = folly::dynamic::array; + for (size_t index = 0; index < jsonCount_; ++index) { + folly::dynamic item = (*dataset_)[index]; + int64_t subtotal = 0; + int64_t total = 0; + if (!checkedMultiply(item["price"].asInt(), item["quantity"].asInt(), + subtotal) || + !checkedMultiply(subtotal, multiplier_, total)) { + sendText(400, "Bad Request", "integer overflow"); + return; + } + item["total"] = total; + items.push_back(std::move(item)); + } + folly::dynamic response = folly::dynamic::object; + response["items"] = std::move(items); + response["count"] = static_cast(jsonCount_); + sendResponse(200, "OK", "application/json", folly::toJson(response)); + } catch (const std::exception &) { + sendText(500, "Internal Server Error", "JSON serialization failed"); + } + } + + void handleStatic() { + if (method_ != HTTPMethod::GET) { + sendText(405, "Method Not Allowed", "method not allowed"); + return; + } + if (staticName_.empty() || staticName_.find('/') != std::string::npos || + staticName_.find('\\') != std::string::npos || + staticName_.find("..") != std::string::npos) { + sendText(404, "Not Found", "not found"); + return; + } + + std::ifstream input(std::string(kStaticRoot) + staticName_, + std::ios::binary); + if (!input) { + sendText(404, "Not Found", "not found"); + return; + } + std::string body((std::istreambuf_iterator(input)), + std::istreambuf_iterator()); + if (!input.good() && !input.eof()) { + sendText(500, "Internal Server Error", "read error"); + return; + } + sendResponse(200, "OK", contentType(staticName_), std::move(body)); + } + + void sendResponse(uint16_t status, const std::string &reason, + const std::string &type, std::string body) { + responseFinished_ = true; + ResponseBuilder(downstream_) + .status(status, reason) + .header("Content-Type", type) + .body(std::move(body)) + .sendWithEOM(); + } + + void sendText(uint16_t status, const std::string &reason, + const std::string &body) { + sendResponse(status, reason, "text/plain", body); + } + + void sendWebSocketFrame(uint8_t opcode, const uint8_t *payload, + size_t payloadLength) { + std::vector frame; + frame.reserve(payloadLength + 10); + frame.push_back(static_cast(0x80U | opcode)); + if (payloadLength <= 125) { + frame.push_back(static_cast(payloadLength)); + } else if (payloadLength <= std::numeric_limits::max()) { + frame.push_back(126); + frame.push_back(static_cast((payloadLength >> 8) & 0xff)); + frame.push_back(static_cast(payloadLength & 0xff)); + } else { + frame.push_back(127); + const auto length = static_cast(payloadLength); + for (int shift = 56; shift >= 0; shift -= 8) { + frame.push_back(static_cast((length >> shift) & 0xff)); + } + } + if (payloadLength > 0) { + frame.insert(frame.end(), payload, payload + payloadLength); + } + downstream_->sendBody(folly::IOBuf::copyBuffer(frame.data(), frame.size())); + } + + void sendWebSocketFrame(uint8_t opcode, const std::vector &payload) { + sendWebSocketFrame(opcode, payload.data(), payload.size()); + } + + void closeWebSocket(uint16_t status) { + if (responseFinished_ || closeSent_) { + return; + } + const std::array payload = { + static_cast((status >> 8) & 0xff), + static_cast(status & 0xff)}; + sendWebSocketFrame(0x8, payload.data(), payload.size()); + closeSent_ = true; + } + + void webSocketProtocolError() { closeWebSocket(1002); } + + void webSocketInvalidPayload() { closeWebSocket(1007); } + + void handleWebSocketFrame(bool fin, uint8_t opcode, + std::vector payload) { + if (closeSent_ && opcode != 0x08) { + return; + } + if ((opcode & 0x08U) != 0) { + if (!fin || payload.size() > 125) { + webSocketProtocolError(); + return; + } + if (opcode == 0x08) { + if (payload.size() == 1) { + webSocketProtocolError(); + return; + } + if (payload.size() >= 2) { + const uint16_t status = + (static_cast(payload[0]) << 8) | payload[1]; + if (!validWebSocketCloseCode(status)) { + webSocketProtocolError(); + return; + } + if (!validUtf8(payload.data() + 2, payload.size() - 2)) { + webSocketInvalidPayload(); + return; + } + } + if (closeSent_) { + responseFinished_ = true; + downstream_->sendEOM(); + return; + } + sendWebSocketFrame(0x08, payload); + responseFinished_ = true; + downstream_->sendEOM(); + } else if (opcode == 0x09) { + sendWebSocketFrame(0x0a, payload); + } else if (opcode != 0x0a) { + webSocketProtocolError(); + } + return; + } + + if (opcode == 0x00) { + if (fragmentOpcode_ == 0) { + webSocketProtocolError(); + return; + } + if (fragmentPayload_.size() + payload.size() > kMaxWebSocketMessage) { + webSocketProtocolError(); + return; + } + fragmentPayload_.insert(fragmentPayload_.end(), payload.begin(), + payload.end()); + if (fin) { + if (fragmentOpcode_ == 0x01 && !validUtf8(fragmentPayload_)) { + webSocketInvalidPayload(); + return; + } + sendWebSocketFrame(fragmentOpcode_, fragmentPayload_); + fragmentOpcode_ = 0; + fragmentPayload_.clear(); + } + return; + } + + if (opcode != 0x01 && opcode != 0x02) { + webSocketProtocolError(); + return; + } + if (fragmentOpcode_ != 0) { + webSocketProtocolError(); + return; + } + if (fin) { + if (opcode == 0x01 && !validUtf8(payload)) { + webSocketInvalidPayload(); + return; + } + sendWebSocketFrame(opcode, payload); + return; + } + fragmentOpcode_ = opcode; + fragmentPayload_ = std::move(payload); + } + + void processWebSocketFrames() { + size_t cursor = 0; + while (!responseFinished_) { + if (websocketBytes_.size() - cursor < 2) { + break; + } + const uint8_t first = websocketBytes_[cursor]; + const uint8_t second = websocketBytes_[cursor + 1]; + const bool fin = (first & 0x80U) != 0; + const uint8_t opcode = first & 0x0fU; + const uint8_t encodedPayloadLength = second & 0x7fU; + if ((first & 0x70U) != 0 || (second & 0x80U) == 0) { + webSocketProtocolError(); + break; + } + // RFC 6455 control frames cannot use either extended-length encoding, + // even when that encoding ultimately describes 125 bytes or fewer. + if ((opcode & 0x08U) != 0 && encodedPayloadLength > 125) { + webSocketProtocolError(); + break; + } + + uint64_t payloadLength = encodedPayloadLength; + size_t headerLength = 2; + if (payloadLength == 126) { + if (websocketBytes_.size() - cursor < 4) { + break; + } + payloadLength = + (static_cast(websocketBytes_[cursor + 2]) << 8) | + websocketBytes_[cursor + 3]; + if (payloadLength < 126) { + webSocketProtocolError(); + break; + } + headerLength = 4; + } else if (payloadLength == 127) { + if (websocketBytes_.size() - cursor < 10) { + break; + } + if ((websocketBytes_[cursor + 2] & 0x80U) != 0) { + webSocketProtocolError(); + break; + } + payloadLength = 0; + for (size_t index = 0; index < 8; ++index) { + payloadLength = + (payloadLength << 8) | websocketBytes_[cursor + 2 + index]; + } + if (payloadLength <= std::numeric_limits::max()) { + webSocketProtocolError(); + break; + } + headerLength = 10; + } + if (payloadLength > kMaxWebSocketMessage) { + webSocketProtocolError(); + break; + } + + constexpr size_t kMaskLength = 4; + if (payloadLength > + std::numeric_limits::max() - headerLength - kMaskLength) { + webSocketProtocolError(); + break; + } + const size_t frameLength = + headerLength + kMaskLength + static_cast(payloadLength); + if (websocketBytes_.size() - cursor < frameLength) { + break; + } + + const size_t maskOffset = cursor + headerLength; + const size_t payloadOffset = maskOffset + kMaskLength; + std::vector payload(static_cast(payloadLength)); + for (size_t index = 0; index < payload.size(); ++index) { + payload[index] = websocketBytes_[payloadOffset + index] ^ + websocketBytes_[maskOffset + (index % kMaskLength)]; + } + cursor += frameLength; + handleWebSocketFrame(fin, opcode, std::move(payload)); + } + + if (cursor > 0) { + websocketBytes_.erase(websocketBytes_.begin(), + websocketBytes_.begin() + cursor); + } + if (responseFinished_) { + websocketBytes_.clear(); + } + } + + Route route_{Route::NotFound}; + std::shared_ptr dataset_; + HTTPMethod method_{HTTPMethod::GET}; + int64_t a_{0}; + int64_t b_{0}; + int64_t multiplier_{1}; + size_t jsonCount_{0}; + size_t uploadBytes_{0}; + bool queryValid_{false}; + bool jsonValid_{false}; + bool uploadValid_{true}; + bool bodyValid_{true}; + bool websocketAccepted_{false}; + bool websocketActive_{false}; + bool closeSent_{false}; + bool responseFinished_{false}; + uint8_t fragmentOpcode_{0}; + std::string requestBody_; + std::string staticName_; + std::vector websocketBytes_; + std::vector fragmentPayload_; +}; + +class ArenaHandlerFactory final : public RequestHandlerFactory { +public: + explicit ArenaHandlerFactory(std::shared_ptr dataset) + : dataset_(std::move(dataset)) {} + + void onServerStart(folly::EventBase * /*eventBase*/) noexcept override {} + + void onServerStop() noexcept override {} + + RequestHandler *onRequest(RequestHandler *, HTTPMessage *) noexcept override { + return new ArenaHandler(dataset_); + } + +private: + std::shared_ptr dataset_; +}; + +wangle::SSLContextConfig h1TlsConfig() { + wangle::SSLContextConfig config; + config.isDefault = true; + config.clientVerification = + folly::SSLContext::VerifyClientCertificate::DO_NOT_REQUEST; + config.setCertificate(FLAGS_cert, FLAGS_key, ""); + config.setNextProtocols(std::list{"http/1.1"}); + return config; +} + +wangle::SSLContextConfig h2TlsConfig() { + wangle::SSLContextConfig config; + config.isDefault = true; + config.clientVerification = + folly::SSLContext::VerifyClientCertificate::DO_NOT_REQUEST; + config.setCertificate(FLAGS_cert, FLAGS_key, ""); + config.setNextProtocols(std::list{"h2"}); + return config; +} + +std::vector listenerConfigs() { + std::vector listeners; + listeners.emplace_back(SocketAddress(FLAGS_ip, FLAGS_http_port, true), + HTTPServer::Protocol::HTTP); + listeners.emplace_back(SocketAddress(FLAGS_ip, FLAGS_h2c_port, true), + HTTPServer::Protocol::HTTP2); + + HTTPServer::IPConfig tlsListener( + SocketAddress(FLAGS_ip, FLAGS_tls_port, true), + HTTPServer::Protocol::HTTP); + tlsListener.sslConfigs.push_back(h1TlsConfig()); + listeners.push_back(std::move(tlsListener)); + + HTTPServer::IPConfig h2Listener(SocketAddress(FLAGS_ip, FLAGS_h2_port, true), + HTTPServer::Protocol::HTTP2); + h2Listener.sslConfigs.push_back(h2TlsConfig()); + listeners.push_back(std::move(h2Listener)); + return listeners; +} + +} // namespace + +int main(int argc, char *argv[]) { + const folly::Init init(&argc, &argv, true); + + if (FLAGS_threads <= 0) { + FLAGS_threads = static_cast(folly::available_concurrency()); + } + CHECK_GT(FLAGS_threads, 0); + + try { + auto dataset = loadDataset(); + + proxygen::HTTPServerOptions options; + options.threads = static_cast(FLAGS_threads); + options.idleTimeout = std::chrono::milliseconds(60000); + options.shutdownOn = {SIGINT, SIGTERM}; + options.supportsConnect = true; + options.enableContentCompression = true; + options.initialReceiveWindow = 1U << 20; + options.receiveStreamWindowSize = 1U << 20; + options.receiveSessionWindowSize = 10U << 20; + options.maxConcurrentIncomingStreams = 1024; + options.handlerFactories = + RequestHandlerChain().addThen(dataset).build(); + + httparena::ArenaHQServer h3Server( + FLAGS_cert, FLAGS_key, static_cast(FLAGS_threads), + [dataset](HTTPMessage *) -> proxygen::HTTPTransactionHandler * { + return new proxygen::RequestHandlerAdaptor(new ArenaHandler(dataset)); + }); + HTTPServer server(std::move(options)); + server.bind(listenerConfigs()); + h3Server.start(SocketAddress(FLAGS_ip, FLAGS_h3_port, true)); + server.start(); + h3Server.stop(); + } catch (const std::exception &error) { + std::cerr << "failed to start Proxygen HttpArena server: " << error.what() + << '\n'; + return 1; + } + return 0; +} diff --git a/frameworks/proxygen/CMakeLists.txt b/frameworks/proxygen/CMakeLists.txt new file mode 100644 index 000000000..35df1fa42 --- /dev/null +++ b/frameworks/proxygen/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.20) + +project(httparena-proxygen LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Proxygen's exported Fizz package calls find_dependency(Sodium). The official +# builder image keeps that upstream find module with the Proxygen source tree. +list(APPEND CMAKE_MODULE_PATH "/proxygen/build/fbcode_builder/CMake") + +# Proxygen's export refers to the historical un-namespaced c-ares target. +# Resolve the installed package and provide the alias expected by that export. +find_package(c-ares CONFIG REQUIRED) +add_library(cares ALIAS c-ares::cares) + +find_package(proxygen CONFIG REQUIRED) + +add_executable(proxygen-arena ArenaHttpServer.cpp ArenaHQServer.cpp) +target_compile_options(proxygen-arena PRIVATE -Wall -Wextra -Wpedantic) +target_link_libraries( + proxygen-arena + PRIVATE + proxygen::proxygen + proxygen::proxygenhttpserver + proxygen::proxygen_hq_samples + proxygen::proxygen_hq_server + proxygen::proxygen_transport_persistent_quic_psk_cache + proxygen::proxygen_httpserver + Folly::folly_init_init + Folly::folly_portability_gflags +) diff --git a/frameworks/proxygen/Dockerfile b/frameworks/proxygen/Dockerfile new file mode 100644 index 000000000..ac4a0b30f --- /dev/null +++ b/frameworks/proxygen/Dockerfile @@ -0,0 +1,35 @@ +FROM ghcr.io/facebook/proxygen/base:latest AS build + +WORKDIR /arena +COPY CMakeLists.txt ArenaCommon.h ArenaHQServer.h ArenaHttpServer.cpp ArenaHQServer.cpp ./ +RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ + && cmake --build build --parallel "$(nproc)" \ + && strip build/proxygen-arena + +# Follow Proxygen's quic-interop image pattern: preserve the resolved shared +# library paths, then copy only those libraries and the server binary to +# a same-distro runtime image. +RUN set -eux; \ + ldd build/proxygen-arena \ + | awk '/=> \// { print $3 } /^\// { print $1 }' | sort -u > /tmp/runtime-libs.txt; \ + tar -chf /tmp/runtime-libs.tar --files-from=/tmp/runtime-libs.txt + +FROM ubuntu:24.04@sha256:019e8eb29a85e74d64925745884f2ec79aa27e3feab36353d24656f4d6b89467 + +ENV LD_LIBRARY_PATH=/opt/proxygen/lib + +COPY --from=build /tmp/runtime-libs.tar /tmp/runtime-libs.tar +RUN tar -xf /tmp/runtime-libs.tar -C / \ + && rm /tmp/runtime-libs.tar + +COPY --from=build /arena/build/proxygen-arena /usr/local/bin/proxygen-arena +COPY entrypoint.sh /usr/local/bin/proxygen-entrypoint +RUN chmod +x /usr/local/bin/proxygen-entrypoint \ + && groupadd --system --gid 10001 httparena \ + && useradd --system --uid 10001 --gid httparena --no-create-home \ + --home-dir /nonexistent --shell /usr/sbin/nologin httparena + +EXPOSE 8080/tcp 8081/tcp 8082/tcp 8443/tcp 8443/udp + +USER httparena +ENTRYPOINT ["/usr/local/bin/proxygen-entrypoint"] diff --git a/frameworks/proxygen/README.md b/frameworks/proxygen/README.md new file mode 100644 index 000000000..8efe071b5 --- /dev/null +++ b/frameworks/proxygen/README.md @@ -0,0 +1,72 @@ +# Proxygen + +This engine entry uses [Meta's Proxygen](https://github.com/facebook/proxygen) +for every advertised protocol: + +- Proxygen `HTTPServer` listens on TCP port 8080 for HTTP/1.1 and RFC 6455 + WebSocket upgrades, on TCP port 8081 for HTTP/1.1 over TLS (ALPN + `http/1.1` only), on TCP port 8082 for prior-knowledge h2c, and with + TLS/ALPN `h2` on TCP port 8443. +- Proxygen's mvfst-backed `HQServer` listens with ALPN `h3` on UDP port 8443. + +Both server APIs run in one process. TCP and QUIC each retain an +affinity-aware I/O pool so whichever transport is being benchmarked can use +the full CPU set while the other pool sleeps. The HTTP/2 and HTTP/3 listeners +use Proxygen's benchmark-oriented flow-control, stream-concurrency, GSO +batching, and write-path settings. + +| Listener | Endpoints | Subscribed profiles | +| --- | --- | --- | +| HTTP/1.1 `:8080` | `/baseline11`, `/pipeline`, `/json/{count}`, `/upload`, `/static/*`, `/ws` | `baseline`, `pipelined`, `limited-conn`, `json`, `json-comp`, `upload`, `static`, `echo-ws`, `echo-ws-pipeline`, `echo-ws-limited` | +| HTTP/1.1 TLS `:8081` | `/json/{count}`, `/static/*` | `json-tls`, `static-tls` | +| h2c `:8082` | `/baseline2`, `/json/{count}` | `baseline-h2c`, `json-h2c` | +| HTTP/2 TLS `:8443` | `/baseline2`, `/static/*` | `baseline-h2`, `static-h2` | +| HTTP/3 QUIC `:8443` | `/baseline2`, `/static/*` | `baseline-h3`, `static-h3` | + +The JSON routes load the immutable dataset once, build each requested slice +and derived `total` fields per request, and serialize the live object with +Folly. Proxygen's standard content-compression path provides conditional gzip +for clients that advertise it. Uploads count bytes delivered through the body +callbacks rather than trusting `Content-Length`; static files are read from +disk for each request. + +The WebSocket handler uses Proxygen's upgrade handshake (including its +per-connection `Sec-WebSocket-Accept` calculation) and implements incremental +RFC 6455 frame parsing. Client frames are unmasked before text or binary data +is echoed; fragmented messages, multiple frames per read, ping/pong, and close +frames are handled explicitly. This entry only claims WebSocket support over +HTTP/1.1, which is the protocol HttpArena's WebSocket profiles exercise. + +## Upstream image and Docker build + +The builder tracks the official `ghcr.io/facebook/proxygen/base:latest` image. +The final stage contains only the Arena binary and its resolved shared +libraries; its matching Ubuntu 24.04 runtime is pinned by digest. The final +image runs the servers as the unprivileged `httparena` user (UID/GID 10001). + +The build and launch arrangement follows Proxygen's upstream container and +coroutine benchmark patterns: + +- `Dockerfile`: build in a full Proxygen environment, discover runtime + libraries with `ldd`, and copy them into a same-distribution runtime image. +- `HTTPCoroBenchmark.cpp`: configure QUIC with GSO batching, continuous-memory + writes, a large congestion window, and a 48-packet write batch. + +HttpArena mounts `/certs/server.crt`, `/certs/server.key`, +`/data/dataset.json`, and `/data/static` at runtime. No benchmark data is baked +into the image. + +## Local validation + +From the repository root on a host where the standard ports are free: + +```bash +./scripts/validate.sh proxygen +``` + +The full benchmark driver is required for the complete 18-profile metadata set +(the lite driver currently rejects profiles it does not know about): + +```bash +LOADGEN_DOCKER=true SKIP_TUNE=true ./scripts/benchmark.sh proxygen --save +``` diff --git a/frameworks/proxygen/entrypoint.sh b/frameworks/proxygen/entrypoint.sh new file mode 100644 index 000000000..14a667c06 --- /dev/null +++ b/frameworks/proxygen/entrypoint.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +exec /usr/local/bin/proxygen-arena \ + --ip=:: \ + --http_port=8080 \ + --tls_port=8081 \ + --h2c_port=8082 \ + --h2_port=8443 \ + --h3_port=8443 \ + --cert=/certs/server.crt \ + --key=/certs/server.key \ + --threads="${PROXYGEN_THREADS:-0}" diff --git a/frameworks/proxygen/meta.json b/frameworks/proxygen/meta.json new file mode 100644 index 000000000..f4b718417 --- /dev/null +++ b/frameworks/proxygen/meta.json @@ -0,0 +1,30 @@ +{ + "display_name": "proxygen", + "language": "C++", + "type": "engine", + "engine": "proxygen", + "description": "Meta's Proxygen HTTP engine: HTTPServer for HTTP/1.1, HTTP/1.1 TLS, h2c, HTTP/2 TLS, and RFC 6455 WebSockets, plus the mvfst-backed HQ server for HTTP/3 over QUIC.", + "repo": "https://github.com/facebook/proxygen", + "enabled": true, + "tests": [ + "baseline", + "json", + "json-comp", + "json-tls", + "upload", + "static", + "static-tls", + "pipelined", + "limited-conn", + "baseline-h2", + "baseline-h2c", + "json-h2c", + "static-h2", + "baseline-h3", + "static-h3", + "echo-ws", + "echo-ws-pipeline", + "echo-ws-limited" + ], + "maintainers": [] +} diff --git a/site/data/frameworks.json b/site/data/frameworks.json index 63c0a4558..3065e5358 100644 --- a/site/data/frameworks.json +++ b/site/data/frameworks.json @@ -649,6 +649,20 @@ "type": "engine", "engine": "picoev" }, + "proxygen": { + "dir": "proxygen", + "description": "Meta's Proxygen HTTP engine: HTTPServer for HTTP/1.1, HTTP/1.1 TLS, h2c, HTTP/2 TLS, and RFC 6455 WebSockets, plus the mvfst-backed HQ server for HTTP/3 over QUIC.", + "repo": "https://github.com/facebook/proxygen", + "type": "engine", + "engine": "proxygen" + }, + "proxygen-coro": { + "dir": "proxygen-coro", + "description": "Meta's Proxygen native coroutine HTTPServer and HTTPSource APIs across HTTP/1.1, HTTP/1.1 TLS, h2c, HTTP/2 TLS, HTTP/3 QUIC, and RFC 6455 WebSockets.", + "repo": "https://github.com/facebook/proxygen", + "type": "engine", + "engine": "proxygen" + }, "pyronova": { "dir": "pyronova", "description": "Pyronova \u2014 Python web framework with a Rust core (hyper + tokio + rustls + mimalloc) and PEP 684 sub-interpreter workers for true multi-core parallelism. Opt-in features: gzip/brotli compression, rustls TLS with h2/h1 ALPN, streaming body ingest, async Postgres via sqlx::PgPool. Handlers are standard Python functions routed via decorators.", @@ -1171,4 +1185,4 @@ "type": "engine", "engine": "zix" } -} \ No newline at end of file +}