summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRutger Broekhoff2026-09-10 23:10:57 +0200
committerRutger Broekhoff2026-09-10 23:11:03 +0200
commit48a99fd71b11321b85c385245b5f2d36fed22df1 (patch)
treeeef558a1f8b5d69f9a1431d03f3999be66757fdc
parenta4408b5ac4ec10421bf9a7d5bf277946ec9f786e (diff)
downloadroutemon-48a99fd71b11321b85c385245b5f2d36fed22df1.tar.gz
routemon-48a99fd71b11321b85c385245b5f2d36fed22df1.zip
Fun with (raw) deflate
-rw-r--r--server/src/http_server.cpp3
-rw-r--r--server/src/http_server.cppm13
-rw-r--r--server/src/srv.cpp141
-rw-r--r--todo.org3
-rw-r--r--web/index.js15
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)
79{ 79{
80 // TODO: switch to "small (4KB) discarded" body type, similar to what Go 80 // TODO: switch to "small (4KB) discarded" body type, similar to what Go
81 // does? Same goes for default_options_handler? Not sure. 81 // does? Same goes for default_options_handler? Not sure.
82 if (auto res = co_await read_request<bhttp::empty_body>(ctx, std::move(r)); 82 if (auto res = co_await read_empty_request(ctx, std::move(r)); !res)
83 !res)
84 co_return std::move(res.error()); 83 co_return std::move(res.error());
85 auto req = r.p->release(); 84 auto req = r.p->release();
86 auto rsp = make_rsp<bhttp::empty_body>( 85 auto rsp = make_rsp<bhttp::empty_body>(
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)
266// anyway, but all context types should derive from base_ctx). 266// anyway, but all context types should derive from base_ctx).
267template <bhttp::concepts::body_reader Body> 267template <bhttp::concepts::body_reader Body>
268auto read_request(base_ctx const& ctx, readable_request&& r) 268auto read_request(base_ctx const& ctx, readable_request&& r)
269 -> net::awaitable<std::expected<bhttp::request<Body>, presponse>> 269 -> net::awaitable<bhttp::request<Body>>
270{ 270{
271 std::ignore = ctx; 271 std::ignore = ctx;
272 auto p = bhttp::request_parser<Body>{std::move(*r.p)}; 272 auto p = bhttp::request_parser<Body>{std::move(*r.p)};
@@ -274,10 +274,8 @@ auto read_request(base_ctx const& ctx, readable_request&& r)
274 co_return std::move(p.release()); 274 co_return std::move(p.release());
275} 275}
276 276
277template <> 277auto read_empty_request(base_ctx const& ctx, readable_request&& r) -> net::
278auto read_request<bhttp::empty_body>(base_ctx const& ctx, readable_request&& r) 278 awaitable<std::expected<bhttp::request<bhttp::empty_body>, presponse>>
279 -> net::
280 awaitable<std::expected<bhttp::request<bhttp::empty_body>, presponse>>
281{ 279{
282 auto [ec, _] = 280 auto [ec, _] =
283 co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); 281 co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple);
@@ -313,7 +311,7 @@ auto default_options_handler(
313 Ctx const& ctx, readable_request r, std::vector<std::string> const&) 311 Ctx const& ctx, readable_request r, std::vector<std::string> const&)
314 -> net::awaitable<presponse> 312 -> net::awaitable<presponse>
315{ 313{
316 auto mreq = co_await read_request<bhttp::empty_body>(ctx, std::move(r)); 314 auto mreq = co_await read_empty_request(ctx, std::move(r));
317 if (!mreq) 315 if (!mreq)
318 co_return std::move(mreq.error()); 316 co_return std::move(mreq.error());
319 317
@@ -324,7 +322,8 @@ auto default_options_handler(
324 preflight_response{ 322 preflight_response{
325 // TODO: should access-control-allow-methods contain OPTIONS? 323 // TODO: should access-control-allow-methods contain OPTIONS?
326 .allow_methods = ctx.route_methods, 324 .allow_methods = ctx.route_methods,
327 .allow_headers = {bhttp::field::content_type}, 325 .allow_headers =
326 {bhttp::field::content_type, bhttp::field::content_encoding},
328 }, 327 },
329 keep_alive{mreq->keep_alive()}); 328 keep_alive{mreq->keep_alive()});
330 } 329 }
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;
6#include <boost/asio/ip/tcp.hpp> 6#include <boost/asio/ip/tcp.hpp>
7#include <boost/beast/core.hpp> 7#include <boost/beast/core.hpp>
8#include <boost/beast/http.hpp> 8#include <boost/beast/http.hpp>
9#include <boost/beast/zlib.hpp>
9#include <boost/config.hpp> 10#include <boost/config.hpp>
10#include <boost/json.hpp> 11#include <boost/json.hpp>
11#include <boost/locale/generator.hpp> 12#include <boost/locale/generator.hpp>
@@ -156,10 +157,8 @@ struct readable_gpx_body
156 auto total = 0uz; 157 auto total = 0uz;
157 try 158 try
158 { 159 {
159 for (auto it = net::buffer_sequence_begin(b); 160 for (auto cur_in_buf : beast::buffers_range(b))
160 it != net::buffer_sequence_end(b); it++)
161 { 161 {
162 auto cur_in_buf = net::const_buffer{*it};
163 while (cur_in_buf.size() > 0) 162 while (cur_in_buf.size() > 0)
164 { 163 {
165 auto to_read = 164 auto to_read =
@@ -217,6 +216,120 @@ struct readable_gpx_body
217static_assert(bhttp::concepts::body<readable_gpx_body>); 216static_assert(bhttp::concepts::body<readable_gpx_body>);
218static_assert(bhttp::concepts::body_reader<readable_gpx_body>); 217static_assert(bhttp::concepts::body_reader<readable_gpx_body>);
219 218
219template <bhttp::concepts::body_reader Inner>
220struct raw_deflate_body
221{
222 using value_type = Inner::value_type;
223
224 class reader
225 {
226 static constexpr auto const chunk_size = 16384;
227
228 Inner::reader r_;
229 bool stream_ended_ = false;
230 beast::zlib::z_params zs_ = {};
231 beast::zlib::inflate_stream inflate_strm_ = {};
232 std::unique_ptr<std::array<char, chunk_size>> in_buf_ = nullptr;
233 std::unique_ptr<std::array<char, chunk_size>> out_buf_ = nullptr;
234
235 auto inflate_chunk(beast::error_code& ec) -> void
236 {
237 assert(zs_.avail_in > 0);
238 zs_.next_in = in_buf_->data();
239
240 do
241 {
242 zs_.avail_out = out_buf_->size();
243 zs_.next_out = out_buf_->data();
244
245 inflate_strm_.write(zs_, beast::zlib::Flush::none, ec);
246 assert(ec != beast::zlib::error::stream_error);
247 if (ec == beast::zlib::error::need_buffers)
248 ec = {};
249 if (ec == beast::zlib::error::end_of_stream)
250 {
251 ec = {};
252 stream_ended_ = true;
253 }
254 if (ec)
255 break;
256
257 auto have = chunk_size - zs_.avail_out;
258 auto cbuf =
259 net::buffer(static_cast<void const*>(out_buf_->data()), have);
260
261 auto inner_ec = beast::error_code{};
262 do
263 cbuf += r_.put(cbuf, inner_ec);
264 while (cbuf.size() > 0 && !inner_ec);
265 if (inner_ec)
266 {
267 ec = inner_ec;
268 break;
269 }
270 }
271 while (zs_.avail_out == 0);
272
273 // In the last case (stream_ended_), the user has sent too much data
274 assert(zs_.avail_in == 0 || ec || stream_ended_);
275 }
276
277 public:
278 template <bool isRequest, bhttp::concepts::fields Fields>
279 explicit reader(bhttp::header<isRequest, Fields>& hdr, value_type& v)
280 : r_{hdr, v}
281 {
282 }
283
284 auto
285 init(boost::optional<std::uint64_t> /* n */, beast::error_code& ec) noexcept
286 -> void
287 {
288 in_buf_.reset(new std::array<char, chunk_size>{});
289 out_buf_.reset(new std::array<char, chunk_size>{});
290 ec = {};
291 }
292
293 auto
294 put(beast::concepts::const_buffer_sequence auto b,
295 beast::error_code& ec) noexcept -> std::size_t
296 {
297 auto in_buf = net::buffer(in_buf_->data(), in_buf_->size());
298 in_buf += zs_.avail_in;
299
300 auto consumed_from_b = net::buffer_copy(in_buf, b);
301 zs_.avail_in += consumed_from_b;
302
303 if (zs_.avail_in == in_buf_->size())
304 inflate_chunk(ec);
305 else
306 ec = {};
307
308 return consumed_from_b;
309 }
310
311 auto finish(beast::error_code& ec) noexcept
312 {
313 ec = {};
314 if (zs_.avail_in > 0)
315 inflate_chunk(ec);
316 if (!ec && !stream_ended_)
317 ec = beast::zlib::error::stream_error;
318 if (stream_ended_ && zs_.avail_in > 0)
319 // client sent data past deflate stream end
320 ec = beast::zlib::error::stream_error;
321
322 in_buf_.reset();
323 out_buf_.reset();
324 inflate_strm_.reset();
325 if (!ec)
326 {
327 r_.finish(ec);
328 }
329 }
330 };
331};
332
220auto handler::handle_process_gpx(l0_ctx ctx, http::readable_request r) 333auto handler::handle_process_gpx(l0_ctx ctx, http::readable_request r)
221 -> net::awaitable<http::presponse> 334 -> net::awaitable<http::presponse>
222{ 335{
@@ -228,23 +341,31 @@ auto handler::handle_process_gpx(l0_ctx ctx, http::readable_request r)
228 { 341 {
229 l.debug("Reading GPX request body"); 342 l.debug("Reading GPX request body");
230 auto const before_read_gpx = chrono::steady_clock::now(); 343 auto const before_read_gpx = chrono::steady_clock::now();
231 auto req = 344 auto parse_res =
232 co_await http::read_request<readable_gpx_body>(ctx, std::move(r)); 345 r.p->get()[bhttp::field::content_encoding] == "x-deflate-raw"
346 ? std::move(
347 (co_await http::read_request<raw_deflate_body<readable_gpx_body>>(
348 ctx, std::move(r)))
349 .body())
350 : std::move((co_await http::read_request<readable_gpx_body>(
351 ctx, std::move(r)))
352 .body());
233 l.debug( 353 l.debug(
234 "Read GPX request body in {}; init took {}, actual parsing {}, " 354 "Read GPX request body in {}; init took {}, actual parsing {}, "
235 "finishing {}", 355 "finishing {}",
236 chrono::duration<double, std::milli>{ 356 chrono::duration<double, std::milli>{
237 chrono::steady_clock::now() - before_read_gpx 357 chrono::steady_clock::now() - before_read_gpx
238 }, 358 },
239 chrono::duration<double, std::milli>{req->body().init_parse_duration()}, 359 chrono::duration<double, std::milli>{parse_res.init_parse_duration()},
240 chrono::duration<double, std::milli>{req->body().put_parse_duration()}, 360 chrono::duration<double, std::milli>{parse_res.put_parse_duration()},
241 chrono::duration<double, std::milli>{ 361 chrono::duration<double, std::milli>{
242 req->body().finish_parse_duration() 362 parse_res.finish_parse_duration()
243 }); 363 });
244 gpx_file = std::move(req->body().unwrap()); 364 gpx_file = std::move(parse_res.unwrap());
245 } 365 }
246 catch (std::exception& ex) 366 catch (std::exception& ex)
247 { 367 {
368 l_.warn("Failed to parse GPX file: {}", ex.what());
248 // TODO: more detailed problem reporting 369 // TODO: more detailed problem reporting
249 auto tpl = problem::tpl{ 370 auto tpl = problem::tpl{
250 .status = bhttp::status::bad_request, 371 .status = bhttp::status::bad_request,
@@ -280,7 +401,7 @@ auto handler::handle_process_gpx(l0_ctx ctx, http::readable_request r)
280auto handler::handle_sysinfo(l0_ctx ctx, http::readable_request r) 401auto handler::handle_sysinfo(l0_ctx ctx, http::readable_request r)
281 -> net::awaitable<http::presponse> 402 -> net::awaitable<http::presponse>
282{ 403{
283 auto req = co_await http::read_request<bhttp::empty_body>(ctx, std::move(r)); 404 auto req = co_await http::read_empty_request(ctx, std::move(r));
284 auto info = inner_.sysinfo(); 405 auto info = inner_.sysinfo();
285 406
286 auto rsp = http::make_rsp<bhttp::string_body>( 407 auto rsp = http::make_rsp<bhttp::string_body>(
diff --git a/todo.org b/todo.org
index 91c02e3..1ad2bd7 100644
--- a/todo.org
+++ b/todo.org
@@ -1,2 +1,3 @@
1- [ ] Accept-Language negotation does not work as expected
2- [ ] Expect: 100-continue support in HTTP server? 1- [ ] Expect: 100-continue support in HTTP server?
2
3- [ ] 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() {
102} 102}
103 103
104async function load(gpxFile) { 104async function load(gpxFile) {
105 let compressedParts = [];
106 const compressedReader = gpxFile.stream().pipeThrough(new CompressionStream("deflate-raw")).getReader();
107 while (true) {
108 const { done, value } = await compressedReader.read();
109 if (done) {
110 break;
111 } else {
112 compressedParts.push(value);
113 }
114 }
115
105 const rsp = await fetch(apiBaseUrl + "/gpx", { 116 const rsp = await fetch(apiBaseUrl + "/gpx", {
106 method: "POST", 117 method: "POST",
107 headers: { 118 headers: {
108 "Accept-Language": "nl-NL", 119 "Accept-Language": "nl-NL",
120 "Content-Encoding": "x-deflate-raw",
121 "Content-Type": "application/gpx+xml",
109 }, 122 },
110 body: gpxFile, 123 body: new Blob(compressedParts),
111 }); 124 });
112 if (!rsp.ok) { 125 if (!rsp.ok) {
113 await showProblem(rsp); 126 await showProblem(rsp);