Repository navigation
Expand file tree
/
Copy pathconnection_engine_websocket.cpp
More file actions
289 lines (285 loc) · 14.1 KB
/
Copy pathconnection_engine_websocket.cpp
File metadata and controls
289 lines (285 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
/*
This file is part of libhttpserver
Copyright (C) 2011-2026 Sebastiano Merlino
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 2.1 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 file
LICENSE in the distribution; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA
*/
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <httpserver/detail/connection_engine.hpp>
#include <httpserver/detail/http1_websocket_handshake.hpp>
namespace httpserver::detail {
namespace {
bool retire_completed_front(http1_response_outbox& outbox) {
// A zero-byte end can arrive after the final write consumed the head.
if (!outbox.front_complete() || outbox.front_failed()) return false;
outbox.consume_front(0);
return true;
}
bool websocket_terminal(const std::shared_ptr<websocket_driver>& driver) {
return driver && driver->snapshot().terminal;
}
} // namespace
websocket_upgrade_result http1_exchange_sink::on_upgrade(const ws_upgrade_options& options) {
websocket_upgrade_result out;
auto plan = negotiate_http1_websocket(head_, options,
engine_->config_.head.max_head_bytes, engine_->config_.head.max_fields);
out.status = plan.status; out.rejection_status = plan.rejection_status;
out.rejection_fields = std::move(plan.rejection_fields);
if (!out.status.ok()) return out;
if (slot_ == nullptr) {
out.status = {http::outcome_code::invalid_state, "upgrade slot unavailable"};
return out;
}
const auto& cap = engine_->config_.websocket_limits;
auto limits = options.limits;
// Default route values project down to server caps. Explicit values
// can tighten, never enlarge, any per-connection allocation budget.
limits.max_message_bytes = std::min(limits.max_message_bytes, cap.max_message_bytes);
limits.incoming_bytes = std::min(limits.incoming_bytes, cap.incoming_bytes);
limits.output_bytes = std::min(limits.output_bytes, cap.output_bytes);
limits.incoming_messages = std::min(limits.incoming_messages, cap.incoming_messages);
limits.outgoing_messages = std::min(limits.outgoing_messages, cap.outgoing_messages);
// Queue allocation and empty-message metadata use the same envelopes as
// multiplexed WebSockets. These reservations belong to the live upgrade.
const auto input_fixed = 16384 + 4096 + 256 * limits.incoming_messages;
const auto wire_headroom = 256 + plan.selected_subprotocol.size();
const auto output_fixed = 4096 + (8192 + 256) * (limits.outgoing_messages + 3);
auto free = [&](server::resource kind) {
return engine_->budget_.capacity(kind) - engine_->budget_.in_use(kind);
};
if (free(server::resource::body_buffer_bytes) <= input_fixed ||
free(server::resource::response_queue_bytes) <= output_fixed + wire_headroom) {
out.status = {http::outcome_code::limit_exceeded, "WebSocket queue storage exhausted"}; return out;
}
limits.incoming_bytes = std::min(limits.incoming_bytes, (free(server::resource::body_buffer_bytes) - input_fixed) / 5);
limits.output_bytes = std::min(limits.output_bytes, (free(server::resource::response_queue_bytes) - output_fixed - wire_headroom) / 4);
if (limits.output_bytes <= 10) {
out.status = {http::outcome_code::limit_exceeded, "WebSocket output storage exhausted"}; return out;
}
limits.max_message_bytes = std::min({limits.max_message_bytes, limits.incoming_bytes,
limits.output_bytes - 10, free(server::resource::ws_message_bytes)});
server::reservation input, output, message;
if (!limits.max_message_bytes ||
!engine_->budget_.reserve(server::resource::body_buffer_bytes, 5 * limits.incoming_bytes + input_fixed, input).ok() ||
!engine_->budget_.reserve(server::resource::response_queue_bytes, 4 * limits.output_bytes + output_fixed, output).ok() ||
!engine_->budget_.reserve(server::resource::ws_message_bytes, limits.max_message_bytes, message).ok()) {
out.status = {http::outcome_code::limit_exceeded, "WebSocket shared storage admission refused"}; return out;
}
out.status = limits.validate();
if (!out.status.ok()) return out;
auto driver = std::make_shared<websocket_driver>(limits);
engine_->observe_websocket_driver(driver);
auto session = driver->take_session();
std::shared_ptr<op_state> pending_read;
{
std::lock_guard lock(engine_->mu_);
if (engine_->shutdown_ || engine_->quiescing_ || engine_->eof_ || engine_->phase_ != connection_engine::stream_phase::http) {
out.status = {http::outcome_code::connection_closed, "upgrade connection unavailable"};
return out;
}
std::string tail;
tail.reserve(engine_->early_bytes_.size() + engine_->pending_tail_.size());
tail.append(engine_->early_bytes_).append(engine_->pending_tail_);
out.status = slot_->commit_upgrade(plan.accept, plan.selected_subprotocol);
if (!out.status.ok()) return out;
// All throwing allocations precede commitment. Only the reader
// feeds this tail, ahead of any read already in flight.
engine_->pending_tail_ = std::move(tail);
engine_->early_bytes_.clear();
engine_->websocket_ = driver;
engine_->websocket_input_charge_ = std::move(input);
engine_->websocket_output_charge_ = std::move(output);
engine_->websocket_message_charge_ = std::move(message);
engine_->release_tail_charge_locked();
engine_->phase_ = connection_engine::stream_phase::upgrade_pending_flush;
engine_->upgrade_anchor_ = std::chrono::steady_clock::now();
engine_->gate_ = connection_engine::body_gate::none;
upgraded_ = true;
pending_read = engine_->pending_read_;
}
out.session.emplace(std::move(session)); out.selected_subprotocol = std::move(plan.selected_subprotocol);
if (pending_read) static_cast<void>(engine_->backend_.request_cancel(*pending_read));
engine_->wake_loops_ordered(); engine_->rearm_watchdog();
return out;
}
void connection_engine::feed_websocket_tail() {
std::shared_ptr<websocket_driver> driver;
std::span<const std::byte> bytes;
{
std::lock_guard lock(mu_);
driver = websocket_;
if (!driver || (pending_tail_.empty() && !websocket_codec_blocked_)) return;
// This sole reader owns the buffer until feed returns. Protocol
// notifications run outside mu_ and cannot mutate the wire buffer.
bytes = {reinterpret_cast<const std::byte*>(pending_tail_.data()) + websocket_tail_offset_,
pending_tail_.size() - websocket_tail_offset_};
}
auto fed = driver->feed(bytes);
{
std::lock_guard lock(mu_);
websocket_tail_offset_ += fed.consumed;
websocket_codec_blocked_ = fed.blocked && fed.status.ok();
if (websocket_tail_offset_ == pending_tail_.size()) {
std::string().swap(pending_tail_);
release_tail_charge_locked();
websocket_tail_offset_ = 0;
}
}
}
connection_engine::io_posture connection_engine::reader_posture(wake_operation& wake) {
std::lock_guard lock(mu_);
if (shutdown_ || close_after_drain_) return io_posture::stop;
if (websocket_ && (!pending_tail_.empty() || websocket_codec_blocked_) && websocket_->snapshot().input_ready) return io_posture::retry;
// A body pull can release room between tail replay and this locked
// posture check. Retry before parking so an earlier wake is not lost.
if (body_tail_ready_locked())
return io_posture::retry;
if (reader_may_read_locked()) return io_posture::proceed;
wake.submit(backend_);
return io_posture::park;
}
bool connection_engine::submit_read(read_operation& read) {
std::lock_guard lock(mu_);
if (shutdown_ || close_after_drain_) return false;
if (websocket_ && (!pending_tail_.empty() || websocket_codec_blocked_)) return false;
pending_read_ = read.state();
read.submit(backend_);
return true;
}
bool connection_engine::read_completed(const io_result& result) {
std::lock_guard lock(mu_);
pending_read_.reset();
return result.code == http::outcome_code::cancelled && websocket_ != nullptr;
}
void connection_engine::handle_read_failure(const io_result& result) {
bool upgraded;
{ std::lock_guard lock(mu_); upgraded = websocket_ != nullptr; }
if (upgraded) {
fail_websocket({http::outcome_code::connection_closed, "WebSocket transport read failed or EOF"});
shutdown();
return;
}
if (result.code == http::outcome_code::connection_closed) {
bool routing;
{ std::lock_guard lock(mu_); eof_ = true; routing = current_ != nullptr; }
if (routing) disconnect_current(http::outcome_code::connection_closed, "http1 connection engine: peer hangup mid-exchange");
} else {
disconnect_current(http::outcome_code::connection_closed, "http1 connection engine: transport read failed");
request_close();
}
wake_loops();
}
std::shared_ptr<websocket_driver> connection_engine::websocket_output_driver() {
std::lock_guard lock(mu_);
if (!websocket_ || !outbox_.empty()) return {};
if (phase_ == stream_phase::upgrade_pending_flush) {
phase_ = stream_phase::websocket;
upgrade_anchor_.reset();
// Output queued while 101 was flushing gets its write interval now.
websocket_write_anchor_.reset();
if (websocket_->snapshot().output_pending) websocket_write_anchor_ = std::chrono::steady_clock::now();
}
return websocket_;
}
std::size_t connection_engine::copy_transport_output(
std::span<std::byte> buffer, std::shared_ptr<websocket_driver>& driver) {
const auto count = outbox_.copy_front(buffer);
if (count > 0) return count;
driver = websocket_output_driver();
return driver ? driver->copy_output(buffer) : 0;
}
connection_engine::io_posture connection_engine::writer_posture(
const std::shared_ptr<websocket_driver>& driver, wake_operation& wake) {
std::lock_guard lock(mu_);
// Retire that slot, then recheck output and any eligible protocol transfer.
if (retire_completed_front(outbox_)) return io_posture::retry;
const bool ws_done = websocket_terminal(driver);
const bool ordinary_done = !websocket_ && route_done_;
if (outbox_drained() && (shutdown_ || ws_done || ordinary_done)) {
phase_ = stream_phase::terminal;
backend_.release_connection(id_);
return io_posture::stop;
}
// The observer mutates session state before acquiring this same
// registration mutex. Rechecking closes the notify-before-park race.
std::byte probe;
if (outbox_.copy_front(std::span(&probe, 1)) > 0) return io_posture::retry;
if (driver && driver->snapshot().output_pending) return io_posture::retry;
wake.submit(backend_);
return io_posture::park;
}
bool connection_engine::watchdog_terminal_locked() const {
return shutdown_ || phase_ == stream_phase::terminal || (close_after_drain_ && !websocket_);
}
bool connection_engine::transport_output_pending_locked() const {
if (outbox_.queued_bytes() > 0) return true;
return websocket_ && websocket_->snapshot().output_pending;
}
bool connection_engine::arm_watchdog(timer_operation& timer, const watchdog_plan& plan) {
std::lock_guard lock(mu_);
const auto current = plan_watchdog_locked();
if (current.exit) return false;
if (current.defer != plan.defer || current.deadline != plan.deadline) return false;
pending_timer_ = timer.state();
timer.submit(backend_);
return true;
}
void connection_engine::add_protocol_deadlines_locked(
std::span<std::chrono::steady_clock::time_point> candidates, std::size_t& count) const {
if (phase_ == stream_phase::upgrade_pending_flush && upgrade_anchor_) {
candidates[count++] = *upgrade_anchor_ + config_.timeouts.handshake;
}
if (websocket_) {
const auto progress = websocket_->snapshot();
if (!progress.terminal && progress.closing_since)
candidates[count++] = *progress.closing_since + config_.timeouts.ws_close;
}
if (server_drain_deadline_) candidates[count++] = *server_drain_deadline_;
}
void connection_engine::observe_websocket_driver(const std::shared_ptr<websocket_driver>& driver) {
const std::weak_ptr<connection_engine> weak = shared_from_this();
driver->observe_progress([weak] { if (auto engine = weak.lock()) engine->websocket_progressed(); });
}
void connection_engine::update_websocket_write_anchor_locked(const websocket_progress& progress) {
if (!progress.output_pending) {
websocket_write_anchor_.reset();
} else if (progress.output_pending_since && (!websocket_write_anchor_ ||
*websocket_write_anchor_ < *progress.output_pending_since)) {
websocket_write_anchor_ = progress.output_pending_since;
}
}
void connection_engine::websocket_progressed() {
bool terminal = false;
{
std::lock_guard lock(mu_);
const auto progress = websocket_ ? websocket_->snapshot() : websocket_progress{};
terminal = progress.terminal;
if (phase_ == stream_phase::websocket) {
update_websocket_write_anchor_locked(progress);
}
if (terminal) close_after_drain_ = true;
wake_loops();
}
if (terminal) disconnect_current(http::outcome_code::connection_closed, "WebSocket session terminal");
rearm_watchdog();
}
void connection_engine::fail_websocket(http::outcome reason) {
std::shared_ptr<websocket_driver> driver;
{ std::lock_guard lock(mu_); driver = websocket_; }
if (driver) driver->transport_failed(std::move(reason));
}
} // namespace httpserver::detail