Compare commits

...

6 Commits

Author SHA1 Message Date
yhirose 79d83feb18 Fix WebSocketClient dropping query string from URL during handshake (#2468)
The constructor stored only uc.path in path_, discarding uc.query, so the
WebSocket upgrade handshake sent the Request-URI without the query string.
Append the query to path_ so query parameters (e.g. auth tokens) are sent.
2026-06-07 17:08:11 -04:00
yhirose fe56a07da5 Wait for in-progress CI runs before releasing
The release check treated runs with an empty conclusion as failures.
Now it inspects each run's status and aborts with an error if any CI
check is still running, so releases wait until CI completes.
2026-06-06 13:38:36 -04:00
Kim, Hyuk 907257f51d add set_hostname_addr_map to WebSocketClient (#2463)
* add set_hostname_addr_map to WebSocketClient

* add WebSocketTest unit test cases
* SpecifyServerIPAddress_AnotherHostname
* SpecifyServerIPAddress_RealHostname

* Change wrong_ip from 0.0.0.0 to 192.0.2.1

Use 192.0.2.1 (RFC 5737 documentation address) to ensure it acts
as a non-routable address and does not alias to loopback.

* Fix style check

* set short timeout in WebSocketTest.SpecifyServerIPAddress_RealHostname

cannot reach wrong_ip
2026-06-05 16:34:20 -04:00
yhirose 0c2f535b74 Fix #2467 2026-06-04 21:20:37 -04:00
Florian Fischer c7ba963a17 Ignore ranges for unknown-length streams (#2465) 2026-06-04 20:15:21 -04:00
yhirose 4465e81b9f Fix #2464 2026-06-03 22:24:52 -04:00
3 changed files with 214 additions and 5 deletions
+38 -2
View File
@@ -1643,6 +1643,8 @@ public:
using Expect100ContinueHandler =
std::function<int(const Request &, Response &)>;
using StartHandler = std::function<void()>;
using WebSocketHandler =
std::function<void(const Request &, ws::WebSocket &)>;
using SubProtocolSelector =
@@ -1694,6 +1696,9 @@ public:
Server &set_pre_request_handler(HandlerWithResponse handler);
Server &set_expect_100_continue_handler(Expect100ContinueHandler handler);
Server &set_start_handler(StartHandler handler);
Server &set_logger(Logger logger);
Server &set_pre_compression_logger(Logger logger);
Server &set_error_logger(ErrorLogger error_logger);
@@ -1883,6 +1888,7 @@ private:
Handler post_routing_handler_;
HandlerWithResponse pre_request_handler_;
Expect100ContinueHandler expect_100_continue_handler_;
StartHandler start_handler_;
mutable std::mutex logger_mutex_;
Logger logger_;
@@ -3842,6 +3848,7 @@ public:
void set_socket_options(SocketOptions socket_options);
void set_connection_timeout(time_t sec, time_t usec = 0);
void set_interface(const std::string &intf);
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
#ifdef CPPHTTPLIB_SSL_ENABLED
void set_ca_cert_path(const std::string &path);
@@ -3876,6 +3883,9 @@ private:
time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND;
std::string interface_;
// Hostname-IP map
std::map<std::string, std::string> addr_map_;
#ifdef CPPHTTPLIB_SSL_ENABLED
bool is_ssl_ = false;
tls::ctx_t tls_ctx_ = nullptr;
@@ -4629,7 +4639,7 @@ inline std::string sha1(const std::string &input) {
// Pre-processing: adding padding bits
std::string msg = input;
uint64_t original_bit_len = static_cast<uint64_t>(msg.size()) * 8;
msg.push_back(static_cast<char>(0x80));
msg.push_back(static_cast<char>(0x80u));
while (msg.size() % 64 != 56) {
msg.push_back(0);
}
@@ -8443,6 +8453,14 @@ inline void coalesce_ranges(Ranges &ranges, size_t content_length) {
inline bool range_error(Request &req, Response &res) {
if (!req.ranges.empty() && 200 <= res.status && res.status < 300) {
if (res.body.empty() && res.content_provider_ && res.content_length_ == 0) {
req.ranges.clear();
if (res.status == StatusCode::PartialContent_206) {
res.status = StatusCode::OK_200;
}
return false;
}
ssize_t content_len = static_cast<ssize_t>(
res.content_length_ ? res.content_length_ : res.body.size());
@@ -11092,6 +11110,11 @@ Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) {
return *this;
}
inline Server &Server::set_start_handler(StartHandler handler) {
start_handler_ = std::move(handler);
return *this;
}
inline Server &Server::set_address_family(int family) {
address_family_ = family;
return *this;
@@ -11787,6 +11810,8 @@ inline bool Server::listen_internal() {
is_running_ = true;
auto se = detail::scope_exit([&]() { is_running_ = false; });
if (start_handler_) { start_handler_(); }
{
std::unique_ptr<TaskQueue> task_queue(new_task_queue());
@@ -20241,6 +20266,7 @@ inline WebSocketClient::WebSocketClient(
if (!uc.port.empty() && !detail::parse_port(uc.port, port_)) { return; }
path_ = std::move(uc.path);
if (!uc.query.empty()) { path_ += uc.query; }
#ifdef CPPHTTPLIB_SSL_ENABLED
is_ssl_ = is_ssl;
@@ -20305,9 +20331,14 @@ inline bool WebSocketClient::connect() {
if (!is_valid_) { return false; }
shutdown_and_close();
// Check is custom IP specified for host_
std::string ip;
auto it = addr_map_.find(host_);
if (it != addr_map_.end()) { ip = it->second; }
Error error;
sock_ = detail::create_client_socket(
host_, std::string(), port_, address_family_, tcp_nodelay_, ipv6_v6only_,
host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
socket_options_, connection_timeout_sec_, connection_timeout_usec_,
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
write_timeout_usec_, interface_, error);
@@ -20402,6 +20433,11 @@ inline void WebSocketClient::set_interface(const std::string &intf) {
interface_ = intf;
}
inline void WebSocketClient::set_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
addr_map_ = std::move(addr_map);
}
#ifdef CPPHTTPLIB_SSL_ENABLED
inline void WebSocketClient::set_ca_cert_path(const std::string &path) {
+18 -3
View File
@@ -62,7 +62,7 @@ HEAD_SHORT=$(git rev-parse --short HEAD)
echo " Latest commit: $HEAD_SHORT"
# Fetch all workflow runs for the HEAD commit
RUNS=$(gh run list --commit "$HEAD_SHA" --json name,conclusion,headSha)
RUNS=$(gh run list --commit "$HEAD_SHA" --json name,status,conclusion,headSha)
NUM_RUNS=$(echo "$RUNS" | jq 'length')
@@ -75,8 +75,17 @@ fi
echo " Found $NUM_RUNS workflow run(s):"
FAILED=0
RUNNING=0
ABIDIFF_PASSED=0
while IFS=$'\t' read -r name conclusion; do
while IFS=$'\t' read -r name status conclusion; do
# A run that hasn't completed yet has an empty conclusion; don't treat it
# as a failure — the release should wait until CI finishes.
if [ "$status" != "completed" ]; then
echo " [ .. ] $name (still running)"
RUNNING=1
continue
fi
if [[ "$name" == *abidiff* ]] || [[ "$name" == *abi* && "$name" != *stability* ]]; then
if [ "$conclusion" = "success" ]; then
echo " [ OK ] $name"
@@ -94,7 +103,13 @@ while IFS=$'\t' read -r name conclusion; do
echo " [FAIL] $name ($conclusion)"
FAILED=1
fi
done < <(echo "$RUNS" | jq -r '.[] | [.name, .conclusion] | @tsv')
done < <(echo "$RUNS" | jq -r '.[] | [.name, .status, .conclusion] | @tsv')
if [ "$RUNNING" -eq 1 ]; then
echo ""
echo "Error: Some CI checks are still running. Wait for them to complete before releasing."
exit 1
fi
if [ "$FAILED" -eq 1 ]; then
echo ""
+158
View File
@@ -810,6 +810,41 @@ TEST(ParseAcceptHeaderTest, ContentTypesPopulatedAndInvalidHeaderHandling) {
}
}
TEST(ServerStartHandlerTest, CalledOnceWhenReady) {
Server svr;
svr.Get("/", [](const Request & /*req*/, Response &res) {
res.set_content("ok", "text/plain");
});
std::atomic<int> start_count{0};
std::atomic<bool> running_when_called{false};
svr.set_start_handler([&]() {
running_when_called = svr.is_running();
start_count++;
});
auto port = svr.bind_to_any_port(HOST);
std::thread t([&]() { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
// A successful request proves the accept loop is running, which the start
// handler precedes; so by now the handler must have run exactly once.
Client cli(HOST, port);
cli.set_connection_timeout(std::chrono::seconds(5));
auto res = cli.Get("/");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(1, start_count.load());
EXPECT_TRUE(running_when_called.load());
}
TEST(DivideTest, DivideStringTests) {
auto divide = [](const std::string &str, char d) {
std::string lhs;
@@ -3718,6 +3753,23 @@ protected:
return true;
});
})
.Get("/streamed-without-length",
[&](const Request & /*req*/, Response &res) {
auto data = new std::string("abcdefg");
res.set_content_provider(
"text/plain",
[data](size_t offset, DataSink &sink) {
if (offset < data->size()) {
sink.os << data->substr(offset);
}
sink.done();
return true;
},
[data](bool success) {
EXPECT_TRUE(success);
delete data;
});
})
.Get("/streamed-with-range",
[&](const Request &req, Response &res) {
auto data = new std::string("abcdefg");
@@ -5197,6 +5249,16 @@ TEST_F(ServerTest, GetStreamed) {
EXPECT_EQ(std::string("aaabbb"), res->body);
}
TEST_F(ServerTest, GetStreamedWithoutLengthWithRange) {
auto res =
cli_.Get("/streamed-without-length", {make_range_header({{0, -1}})});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(false, res->has_header("Content-Length"));
EXPECT_EQ(false, res->has_header("Content-Range"));
EXPECT_EQ(std::string("abcdefg"), res->body);
}
TEST_F(ServerTest, GetStreamedWithRange1) {
auto res = cli_.Get("/streamed-with-range", {{make_range_header({{3, 5}})}});
ASSERT_TRUE(res);
@@ -17644,6 +17706,57 @@ TEST(WebSocketTest, ComplexPath) {
EXPECT_TRUE(ws2.is_valid());
}
TEST(WebSocketTest, SpecifyServerIPAddress_AnotherHostname) {
Server svr;
svr.WebSocket("/ws", [](const Request &, ws::WebSocket &ws) {
std::string msg;
while (ws.read(msg)) {}
});
auto port = svr.bind_to_any_port(HOST);
std::thread t([&]() { svr.listen_after_bind(); });
svr.wait_until_ready();
auto another_host = "example.com";
auto wrong_ip = "192.0.2.1";
ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws");
client.set_hostname_addr_map({{another_host, wrong_ip}});
ASSERT_TRUE(client.connect());
EXPECT_TRUE(client.is_open());
client.close();
svr.stop();
t.join();
}
TEST(WebSocketTest, SpecifyServerIPAddress_RealHostname) {
Server svr;
svr.WebSocket("/ws", [](const Request &, ws::WebSocket &ws) {
std::string msg;
while (ws.read(msg)) {}
});
auto port = svr.bind_to_any_port(HOST);
std::thread t([&]() { svr.listen_after_bind(); });
svr.wait_until_ready();
auto wrong_ip = "192.0.2.1";
ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws");
client.set_hostname_addr_map({{"localhost", wrong_ip}});
client.set_connection_timeout(1);
client.set_read_timeout(1);
client.set_write_timeout(1);
EXPECT_FALSE(client.connect());
EXPECT_FALSE(client.is_open());
svr.stop();
t.join();
}
class WebSocketIntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
@@ -18078,6 +18191,51 @@ TEST(WebSocketPreRoutingTest, RejectWithoutAuth) {
t.join();
}
TEST(WebSocketTest, QueryStringInHandshake) {
Server svr;
std::mutex mtx;
std::string received_target;
std::string received_token;
svr.WebSocket("/ws", [&](const Request &req, ws::WebSocket &ws) {
{
std::lock_guard<std::mutex> lock(mtx);
received_target = req.target;
if (req.has_param("token")) {
received_token = req.get_param_value("token");
}
}
std::string msg;
while (ws.read(msg)) {
ws.send(msg);
}
});
auto port = svr.bind_to_any_port("localhost");
std::thread t([&]() { svr.listen_after_bind(); });
svr.wait_until_ready();
ws::WebSocketClient client("ws://localhost:" + std::to_string(port) +
"/ws?token=ABC&session=123");
ASSERT_TRUE(client.connect());
// Round-trip ensures the handler has run and captured the request.
ASSERT_TRUE(client.send("hello"));
std::string msg;
ASSERT_TRUE(client.read(msg));
EXPECT_EQ("hello", msg);
client.close();
{
std::lock_guard<std::mutex> lock(mtx);
EXPECT_EQ("/ws?token=ABC&session=123", received_target);
EXPECT_EQ("ABC", received_token);
}
svr.stop();
t.join();
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
class WebSocketSSLIntegrationTest : public ::testing::Test {
protected: