From 48a99fd71b11321b85c385245b5f2d36fed22df1 Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Thu, 10 Sep 2026 23:10:57 +0200 Subject: Fun with (raw) deflate --- server/src/http_server.cpp | 3 +- server/src/http_server.cppm | 13 ++-- server/src/srv.cpp | 141 ++++++++++++++++++++++++++++++++++++++++---- todo.org | 3 +- web/index.js | 15 ++++- 5 files changed, 154 insertions(+), 21 deletions(-) diff --git a/server/src/http_server.cpp b/server/src/http_server.cpp index c3a9ed7..ff96091 100644 --- a/server/src/http_server.cpp +++ b/server/src/http_server.cpp @@ -79,8 +79,7 @@ auto global_options_handler(base_ctx const& ctx, readable_request r) { // TODO: switch to "small (4KB) discarded" body type, similar to what Go // does? Same goes for default_options_handler? Not sure. - if (auto res = co_await read_request(ctx, std::move(r)); - !res) + if (auto res = co_await read_empty_request(ctx, std::move(r)); !res) co_return std::move(res.error()); auto req = r.p->release(); auto rsp = make_rsp( diff --git a/server/src/http_server.cppm b/server/src/http_server.cppm index 2bf3c63..e70110e 100644 --- a/server/src/http_server.cppm +++ b/server/src/http_server.cppm @@ -266,7 +266,7 @@ auto make_preflight_rsp(preflight_response res, keep_alive ka) // anyway, but all context types should derive from base_ctx). template auto read_request(base_ctx const& ctx, readable_request&& r) - -> net::awaitable, presponse>> + -> net::awaitable> { std::ignore = ctx; auto p = bhttp::request_parser{std::move(*r.p)}; @@ -274,10 +274,8 @@ auto read_request(base_ctx const& ctx, readable_request&& r) co_return std::move(p.release()); } -template <> -auto read_request(base_ctx const& ctx, readable_request&& r) - -> net:: - awaitable, presponse>> +auto read_empty_request(base_ctx const& ctx, readable_request&& r) -> net:: + awaitable, presponse>> { auto [ec, _] = co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); @@ -313,7 +311,7 @@ auto default_options_handler( Ctx const& ctx, readable_request r, std::vector const&) -> net::awaitable { - auto mreq = co_await read_request(ctx, std::move(r)); + auto mreq = co_await read_empty_request(ctx, std::move(r)); if (!mreq) co_return std::move(mreq.error()); @@ -324,7 +322,8 @@ auto default_options_handler( preflight_response{ // TODO: should access-control-allow-methods contain OPTIONS? .allow_methods = ctx.route_methods, - .allow_headers = {bhttp::field::content_type}, + .allow_headers = + {bhttp::field::content_type, bhttp::field::content_encoding}, }, keep_alive{mreq->keep_alive()}); } diff --git a/server/src/srv.cpp b/server/src/srv.cpp index adfef18..a901f8e 100644 --- a/server/src/srv.cpp +++ b/server/src/srv.cpp @@ -6,6 +6,7 @@ module; #include #include #include +#include #include #include #include @@ -156,10 +157,8 @@ struct readable_gpx_body auto total = 0uz; try { - for (auto it = net::buffer_sequence_begin(b); - it != net::buffer_sequence_end(b); it++) + for (auto cur_in_buf : beast::buffers_range(b)) { - auto cur_in_buf = net::const_buffer{*it}; while (cur_in_buf.size() > 0) { auto to_read = @@ -217,6 +216,120 @@ struct readable_gpx_body static_assert(bhttp::concepts::body); static_assert(bhttp::concepts::body_reader); +template +struct raw_deflate_body +{ + using value_type = Inner::value_type; + + class reader + { + static constexpr auto const chunk_size = 16384; + + Inner::reader r_; + bool stream_ended_ = false; + beast::zlib::z_params zs_ = {}; + beast::zlib::inflate_stream inflate_strm_ = {}; + std::unique_ptr> in_buf_ = nullptr; + std::unique_ptr> out_buf_ = nullptr; + + auto inflate_chunk(beast::error_code& ec) -> void + { + assert(zs_.avail_in > 0); + zs_.next_in = in_buf_->data(); + + do + { + zs_.avail_out = out_buf_->size(); + zs_.next_out = out_buf_->data(); + + inflate_strm_.write(zs_, beast::zlib::Flush::none, ec); + assert(ec != beast::zlib::error::stream_error); + if (ec == beast::zlib::error::need_buffers) + ec = {}; + if (ec == beast::zlib::error::end_of_stream) + { + ec = {}; + stream_ended_ = true; + } + if (ec) + break; + + auto have = chunk_size - zs_.avail_out; + auto cbuf = + net::buffer(static_cast(out_buf_->data()), have); + + auto inner_ec = beast::error_code{}; + do + cbuf += r_.put(cbuf, inner_ec); + while (cbuf.size() > 0 && !inner_ec); + if (inner_ec) + { + ec = inner_ec; + break; + } + } + while (zs_.avail_out == 0); + + // In the last case (stream_ended_), the user has sent too much data + assert(zs_.avail_in == 0 || ec || stream_ended_); + } + + public: + template + explicit reader(bhttp::header& hdr, value_type& v) + : r_{hdr, v} + { + } + + auto + init(boost::optional /* n */, beast::error_code& ec) noexcept + -> void + { + in_buf_.reset(new std::array{}); + out_buf_.reset(new std::array{}); + ec = {}; + } + + auto + put(beast::concepts::const_buffer_sequence auto b, + beast::error_code& ec) noexcept -> std::size_t + { + auto in_buf = net::buffer(in_buf_->data(), in_buf_->size()); + in_buf += zs_.avail_in; + + auto consumed_from_b = net::buffer_copy(in_buf, b); + zs_.avail_in += consumed_from_b; + + if (zs_.avail_in == in_buf_->size()) + inflate_chunk(ec); + else + ec = {}; + + return consumed_from_b; + } + + auto finish(beast::error_code& ec) noexcept + { + ec = {}; + if (zs_.avail_in > 0) + inflate_chunk(ec); + if (!ec && !stream_ended_) + ec = beast::zlib::error::stream_error; + if (stream_ended_ && zs_.avail_in > 0) + // client sent data past deflate stream end + ec = beast::zlib::error::stream_error; + + in_buf_.reset(); + out_buf_.reset(); + inflate_strm_.reset(); + if (!ec) + { + r_.finish(ec); + } + } + }; +}; + auto handler::handle_process_gpx(l0_ctx ctx, http::readable_request r) -> net::awaitable { @@ -228,23 +341,31 @@ auto handler::handle_process_gpx(l0_ctx ctx, http::readable_request r) { l.debug("Reading GPX request body"); auto const before_read_gpx = chrono::steady_clock::now(); - auto req = - co_await http::read_request(ctx, std::move(r)); + auto parse_res = + r.p->get()[bhttp::field::content_encoding] == "x-deflate-raw" + ? std::move( + (co_await http::read_request>( + ctx, std::move(r))) + .body()) + : std::move((co_await http::read_request( + ctx, std::move(r))) + .body()); l.debug( "Read GPX request body in {}; init took {}, actual parsing {}, " "finishing {}", chrono::duration{ chrono::steady_clock::now() - before_read_gpx }, - chrono::duration{req->body().init_parse_duration()}, - chrono::duration{req->body().put_parse_duration()}, + chrono::duration{parse_res.init_parse_duration()}, + chrono::duration{parse_res.put_parse_duration()}, chrono::duration{ - req->body().finish_parse_duration() + parse_res.finish_parse_duration() }); - gpx_file = std::move(req->body().unwrap()); + gpx_file = std::move(parse_res.unwrap()); } catch (std::exception& ex) { + l_.warn("Failed to parse GPX file: {}", ex.what()); // TODO: more detailed problem reporting auto tpl = problem::tpl{ .status = bhttp::status::bad_request, @@ -280,7 +401,7 @@ auto handler::handle_process_gpx(l0_ctx ctx, http::readable_request r) auto handler::handle_sysinfo(l0_ctx ctx, http::readable_request r) -> net::awaitable { - auto req = co_await http::read_request(ctx, std::move(r)); + auto req = co_await http::read_empty_request(ctx, std::move(r)); auto info = inner_.sysinfo(); auto rsp = http::make_rsp( diff --git a/todo.org b/todo.org index 91c02e3..1ad2bd7 100644 --- a/todo.org +++ b/todo.org @@ -1,2 +1,3 @@ -- [ ] Accept-Language negotation does not work as expected - [ ] Expect: 100-continue support in HTTP server? + +- [ ] UTM zome segmentation maybe not working? diff --git a/web/index.js b/web/index.js index c6fb3f7..e8accf0 100644 --- a/web/index.js +++ b/web/index.js @@ -102,12 +102,25 @@ async function getSysinfo() { } async function load(gpxFile) { + let compressedParts = []; + const compressedReader = gpxFile.stream().pipeThrough(new CompressionStream("deflate-raw")).getReader(); + while (true) { + const { done, value } = await compressedReader.read(); + if (done) { + break; + } else { + compressedParts.push(value); + } + } + const rsp = await fetch(apiBaseUrl + "/gpx", { method: "POST", headers: { "Accept-Language": "nl-NL", + "Content-Encoding": "x-deflate-raw", + "Content-Type": "application/gpx+xml", }, - body: gpxFile, + body: new Blob(compressedParts), }); if (!rsp.ok) { await showProblem(rsp); -- cgit v1.3