summaryrefslogtreecommitdiffstats
path: root/server/src
diff options
context:
space:
mode:
authorRutger Broekhoff2026-08-28 18:03:05 +0200
committerRutger Broekhoff2026-08-28 18:03:05 +0200
commit973aec43ea54bbf95b64fbcb636403401d1ca60e (patch)
tree41b7911c420766a9b463245b9296f44c5bf35258 /server/src
downloadroutemon-973aec43ea54bbf95b64fbcb636403401d1ca60e.tar.gz
routemon-973aec43ea54bbf95b64fbcb636403401d1ca60e.zip
Import from e4b104792206ee7ea64bf39c6b7d2c0c230f9d14
Diffstat (limited to 'server/src')
-rw-r--r--server/src/api.cpp206
-rw-r--r--server/src/api.cppm79
-rw-r--r--server/src/config.cpp167
-rw-r--r--server/src/config.cppm39
-rw-r--r--server/src/database.cppm37
-rw-r--r--server/src/datex2.cppm296
-rw-r--r--server/src/geo.cppm40
-rw-r--r--server/src/gpx.cpp189
-rw-r--r--server/src/gpx.cppm43
-rw-r--r--server/src/http_client.cppm62
-rw-r--r--server/src/http_common.cppm139
-rw-r--r--server/src/http_server.cppm661
-rw-r--r--server/src/locale.cppm245
-rw-r--r--server/src/log.cppm148
-rw-r--r--server/src/main.cpp109
-rw-r--r--server/src/problem.cppm60
-rw-r--r--server/src/req_ctx.cppm51
-rw-r--r--server/src/routemon.cppm11
-rw-r--r--server/src/rwgps.cppm137
-rw-r--r--server/src/sqlite3.cppm257
-rw-r--r--server/src/srv.cppm221
-rw-r--r--server/src/time.cppm205
-rw-r--r--server/src/trace.cppm79
-rw-r--r--server/src/util.cppm254
-rw-r--r--server/src/xml.cpp61
-rw-r--r--server/src/xml.cppm632
26 files changed, 4428 insertions, 0 deletions
diff --git a/server/src/api.cpp b/server/src/api.cpp
new file mode 100644
index 0000000..b3c8e31
--- /dev/null
+++ b/server/src/api.cpp
@@ -0,0 +1,206 @@
1module;
2
3#include <boost/geometry.hpp>
4#include <boost/json.hpp>
5
6module routemon:api$impl;
7
8import std;
9import :api;
10import :datex2;
11import :geo;
12import :gpx;
13import :log;
14import :req_ctx;
15import :time;
16import :trace;
17
18namespace {
19
20 namespace chrono = std::chrono;
21 namespace json = boost::json;
22 namespace views = std::views;
23
24} // namespace <anonymous>
25
26namespace routemon::api {
27
28 auto json_value_from_point(geo::point const& p) -> json::value {
29 return json::array{bgeo::get<1>(p), bgeo::get<0>(p)};
30 }
31 auto json_value_from_linestring(geo::linestring const& ls) -> json::value {
32 json::array a;
33 for (auto const& p : ls)
34 a.push_back(json_value_from_point(p));
35 return a;
36 }
37 auto json_value_from_linestrings(std::vector<geo::linestring> const& lss) -> json::value {
38 json::array a;
39 for (auto const& ls : lss)
40 a.push_back(json_value_from_linestring(ls));
41 return a;
42 }
43
44 auto tag_invoke(json::value_from_tag, json::value& jv, relevant_road_closure const& clo) -> void {
45 jv = json::object{
46 {"relevant_lss", json_value_from_linestrings(clo.relevant_lss)},
47 };
48 }
49 auto tag_invoke(json::value_from_tag, json::value& jv, relevant_situation const& sit) -> void {
50 jv = json::object{
51 {"id", json::value_from(sit.id)},
52 {"location", sit.location ? json_value_from_point(*sit.location) : nullptr},
53 {"comments", json::value_from(sit.comments)},
54 {"relevant_road_closures", json::value_from(sit.relevant_road_closures)},
55 };
56 }
57 auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) -> void {
58 jv = json::object{
59 {"points", json_value_from_linestring(seg.points)},
60 };
61 }
62 auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) -> void {
63 jv = json::object{
64 {"segments", json::value_from(track.segments)},
65 };
66 }
67 auto tag_invoke(json::value_from_tag, json::value& jv, process_gpx_result const& res) -> void {
68 jv = json::object{
69 {"tracks", json::value_from(res.tracks)},
70 {"relevant_situations", json::value_from(res.relevant_situations)},
71 };
72 }
73 auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) -> void {
74 jv = json::object{
75 {"using_publication_of", std::format("{:%FT%TZ}", info.using_publication_of)},
76 };
77 }
78
79 handler::handler(log::logger const& l, datex2::situation_publication pub)
80 : l_{l.sub("handler")}, pub_{std::move(pub)}
81 {
82 l_.info("Building indices");
83 auto const before_build = chrono::steady_clock::now();
84 for (auto const& sit : pub_.situations) {
85 for (auto const& rc : sit->road_closures) {
86 for (auto const& ls : rc->relevant_line_strings) {
87 auto box = geo::box{};
88 bgeo::envelope(*ls, box);
89 lse_index_.insert(std::make_tuple(box, ls, rc));
90 }
91 for (auto p : rc->relevant_points) {
92 p_index_.insert(std::make_pair(p, rc));
93 }
94 }
95 }
96 auto const after_build = chrono::steady_clock::now();
97 auto const dur_build = chrono::duration_cast<chrono::milliseconds>(after_build - before_build);
98 l_.info("Indices built in {}", dur_build);
99 l_.info("LSE index size: {}", lse_index_.size());
100 l_.info("Point index size: {}", p_index_.size());
101 }
102
103 auto handler::process_gpx(gpx::file&& gpx_file) -> std::optional<process_gpx_result> {
104 auto const now = chrono::utc_clock::now();
105 auto const relevant = std::initializer_list<time::period>{time::period{now - chrono::days(7), now + chrono::days(7)}};
106 auto const check_periods = time::period_seq{relevant.begin(), relevant.end()};
107
108 auto splits_with_overlap_segments = std::vector<geo::linestring>{};
109 for (auto const& track : gpx_file.tracks)
110 for (auto const& seg : track.segments)
111 geo::split_linestring_with_overlap_segments(seg.waypoints, 5000 /* meters max total dist until a new split is forced */,
112 splits_with_overlap_segments);
113 auto const before_query = chrono::steady_clock::now();
114
115 l_.debug("Querying for relevant situations");
116 auto relevant_road_closures = std::unordered_set<std::shared_ptr<datex2::road_closure>>{};
117 auto ls_checked = 0uz;
118 auto p_checked = 0uz;
119 auto i = 0;
120 for (geo::linestring const& part : splits_with_overlap_segments) {
121 l_.debug("Checking part [{}/{}]", ++i, splits_with_overlap_segments.size());
122
123 auto part_box = geo::box{};
124 bgeo::envelope(part, part_box);
125
126 for (auto it = lse_index_.qbegin(bgeo::index::intersects(part_box)); it != lse_index_.qend(); it++) {
127 // Cannot use structured bindings here, as boost::geometry::get interferes with ADL.
128 // It is a candidate as the namespace boost::geometry is part of the associated namespace set,
129 // which happens because geo::linestring ≡ boost::geometry::model::linestring<geo::point> is part
130 // of the whole tuple type (lse_index_value) that is the value_type of the iterator.
131 std::shared_ptr<geo::linestring> const& ls = std::get<1>(*it);
132 std::shared_ptr<datex2::road_closure> const& rc = std::get<2>(*it);
133 if (rc->validity && rc->validity->intersect(check_periods).periods().empty())
134 continue;
135 if (bgeo::distance(*ls, part, geo::vincenty_strategy{}) < 5.0)
136 relevant_road_closures.emplace(rc);
137 ls_checked++;
138 }
139 for (auto it = p_index_.qbegin(bgeo::index::intersects(part_box)); it != p_index_.qend(); it++) {
140 // Cannot use structured bindings here for the same reason as above.
141 geo::point const& p = std::get<0>(*it);
142 std::shared_ptr<datex2::road_closure> const& rc = std::get<1>(*it);
143 if (rc->validity && rc->validity->intersect(check_periods).periods().empty())
144 continue;
145 if (bgeo::distance(p, part, geo::vincenty_strategy{}) < 5.0)
146 relevant_road_closures.emplace(rc);
147 p_checked++;
148 }
149 }
150
151 auto const after_query = chrono::steady_clock::now();
152 l_.debug("Done (checked {} line string(s) and {} point(s)) in {}",
153 ls_checked, p_checked, chrono::duration_cast<chrono::milliseconds>(after_query - before_query));
154
155 auto relevant_situations = std::unordered_set<std::shared_ptr<datex2::situation>>{};
156 for (auto const& rc : relevant_road_closures)
157 relevant_situations.emplace(rc->parent);
158
159 l_.debug("Identified {} relevant road closure(s), part of {} unique situation(s)",
160 relevant_road_closures.size(), relevant_situations.size());
161 for (auto const& sit : relevant_situations)
162 l_.debug("Relevant situation: {}", sit->id);
163
164 return process_gpx_result{
165 .tracks = gpx_file.tracks
166 | views::transform([](auto const& trk) -> track {
167 return {
168 .segments = trk.segments
169 | views::transform([](auto const& seg) -> track_segment {
170 return {.points = seg.waypoints};
171 })
172 | std::ranges::to<std::vector<track_segment>>(),
173 };
174 })
175 | std::ranges::to<std::vector<track>>(),
176 .relevant_situations = relevant_situations
177 | views::transform([&](std::shared_ptr<datex2::situation> sit) -> relevant_situation {
178 return {
179 .id = sit->id,
180 .location = sit->location,
181 .comments = sit->comments,
182 .relevant_road_closures = relevant_road_closures
183 | views::filter([&](std::shared_ptr<datex2::road_closure> const& rc) -> bool {
184 return std::shared_ptr{rc->parent} == sit;
185 })
186 | views::transform([](std::shared_ptr<datex2::road_closure> const& rc) -> relevant_road_closure {
187 return {
188 .relevant_lss = rc->relevant_line_strings
189 | views::transform([](auto const& lsp) -> geo::linestring {
190 return *lsp;
191 })
192 | std::ranges::to<std::vector<geo::linestring>>(),
193 };
194 })
195 | std::ranges::to<std::vector<relevant_road_closure>>(),
196 };
197 })
198 | std::ranges::to<std::vector<relevant_situation>>(),
199 };
200 }
201
202 auto handler::sysinfo() -> struct sysinfo {
203 return {.using_publication_of = pub_.publication_time};
204 }
205
206} // namespace routemon::api
diff --git a/server/src/api.cppm b/server/src/api.cppm
new file mode 100644
index 0000000..caeb44c
--- /dev/null
+++ b/server/src/api.cppm
@@ -0,0 +1,79 @@
1module;
2
3#include <boost/geometry.hpp>
4#include <boost/json.hpp>
5
6export module routemon:api;
7
8import std;
9import :datex2;
10import :geo;
11import :gpx;
12import :log;
13import :time;
14import :trace;
15
16namespace {
17
18 namespace chrono = std::chrono;
19 namespace json = boost::json;
20 namespace views = std::views;
21
22} // namespace <anonymous>
23
24export
25namespace routemon::api {
26
27 struct relevant_road_closure {
28 std::vector<geo::linestring> relevant_lss;
29 };
30 auto tag_invoke(json::value_from_tag, json::value& jv, relevant_road_closure const& clo) -> void;
31
32 struct relevant_situation {
33 std::string id;
34 std::optional<geo::point> location;
35 std::vector<std::string> comments;
36 std::vector<relevant_road_closure> relevant_road_closures;
37 };
38 auto tag_invoke(json::value_from_tag, json::value& jv, relevant_situation const& sit) -> void;
39
40 struct track_segment {
41 geo::linestring points;
42 };
43 auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) -> void;
44
45 struct track {
46 std::vector<track_segment> segments;
47 };
48 auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) -> void;
49
50 struct process_gpx_result {
51 std::vector<track> tracks;
52 std::vector<relevant_situation> relevant_situations;
53 };
54 auto tag_invoke(json::value_from_tag, json::value& jv, process_gpx_result const& res) -> void;
55
56 struct sysinfo {
57 time::timestamp using_publication_of;
58 };
59 auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) -> void;
60
61 class handler {
62 using lse_index_value = std::tuple<geo::box, std::shared_ptr<geo::linestring>, std::shared_ptr<datex2::road_closure>>;
63 using p_index_value = std::pair<geo::point, std::shared_ptr<datex2::road_closure>>;
64 using lse_index = bgeo::index::rtree<lse_index_value, bgeo::index::quadratic<16>>;
65 using p_index = bgeo::index::rtree<p_index_value, bgeo::index::quadratic<16>>;
66
67 log::logger l_;
68 datex2::situation_publication pub_;
69 lse_index lse_index_;
70 p_index p_index_;
71
72 public:
73 explicit handler(log::logger const& l, datex2::situation_publication pub);
74
75 auto process_gpx(gpx::file&& gpx_file) -> std::optional<process_gpx_result>;
76 auto sysinfo() -> sysinfo;
77 };
78
79} // namespace routemon::api
diff --git a/server/src/config.cpp b/server/src/config.cpp
new file mode 100644
index 0000000..3b17691
--- /dev/null
+++ b/server/src/config.cpp
@@ -0,0 +1,167 @@
1module;
2
3#include <boost/json.hpp>
4#include <boost/system/system_error.hpp>
5
6module routemon:config$impl;
7
8import std;
9import :config;
10import :log;
11import :util;
12
13namespace json = boost::json;
14
15namespace routemon::config {
16
17 class location {
18 std::optional<std::pair<std::string_view, util::not_null<location const*>>> next_;
19
20 auto append_to(std::string& s) const -> void {
21 if (next_) {
22 s += next_->first;
23 s += ".";
24 next_->second->append_to(s);
25 }
26 }
27
28 public:
29 location() = default;
30
31 explicit location(location const& next, std::string_view entry)
32 : next_{std::make_pair(entry, util::not_null{&next})}
33 {}
34
35 [[nodiscard]] auto to_string() const -> std::string {
36 if (!next_)
37 return "";
38 auto s = std::string{next_->first};
39 next_->second->append_to(s);
40 return s;
41 }
42
43 auto sub(std::string_view entry) const& -> location {
44 return location{*this, entry};
45 }
46 };
47
48 class object_reader {
49 location loc_;
50 json::object const& obj_;
51 std::unordered_set<std::string> visited_;
52
53 public:
54 object_reader(json::value const& jv, location loc)
55 try : loc_{std::move(loc)}, obj_{jv.as_object()}
56 {} catch (boost::system::system_error const&) {
57 throw std::runtime_error{std::format("expected an object at {}", loc.to_string())};
58 }
59
60 auto check_unused() const -> void {
61 for (auto const& kv : obj_) {
62 if (!visited_.contains(std::string{kv.key()})) {
63 throw std::runtime_error{std::format("unexpected key {}", loc_.sub(kv.key()).to_string())};
64 }
65 }
66 }
67
68 template<class T>
69 auto expect_at(std::string_view key) -> T {
70 visited_.emplace(key);
71 auto const loc = loc_.sub(key);
72 if (auto const jv = obj_.try_at(key)) {
73 try {
74 return json::value_to<T>(*jv, loc);
75 } catch (boost::system::system_error const& e) {
76 throw std::runtime_error{std::format("failed to read {}: {}", loc.to_string(), e.code().message())};
77 }
78 } else {
79 throw std::runtime_error{std::format("did not find expected key {}", loc.to_string())};
80 }
81 }
82 };
83
84 auto as_checked_object(json::value const& jv, location const& loc, std::invocable<object_reader&> auto const& f) -> decltype(f(std::declval<object_reader&>())) {
85 auto r = object_reader{jv, loc};
86 auto&& v = f(r);
87 r.check_unused();
88 return std::forward<decltype(f(r))>(v);
89 }
90
91 auto tag_invoke(json::value_to_tag<rwgps> const&, json::value const& jv, location const& loc) -> rwgps {
92 return as_checked_object(jv, loc, [](object_reader& r) -> rwgps {
93 return {
94 .api_key = r.expect_at<std::string>("api_key"),
95 .auth_token = r.expect_at<std::string>("auth_token"),
96 };
97 });
98 }
99
100 auto tag_invoke(json::value_to_tag<situations> const&, json::value const& jv, location const& loc) -> situations {
101 return as_checked_object(jv, loc, [](object_reader& r) -> situations {
102 return {
103 .datex2_filename = r.expect_at<std::string>("datex2_filename"),
104 };
105 });
106 }
107
108 auto tag_invoke(json::value_to_tag<database> const&, json::value const& jv, location const& loc) -> database {
109 return as_checked_object(jv, loc, [](object_reader& r) -> database {
110 return {
111 .sqlite3_filename = r.expect_at<std::string>("sqlite3_filename"),
112 };
113 });
114 }
115
116 auto tag_invoke(json::value_to_tag<http_server> const&, json::value const& jv, location const& loc) -> http_server {
117 return as_checked_object(jv, loc, [](object_reader& r) -> http_server {
118 return {
119 .lax_cors = r.expect_at<bool>("lax_cors"),
120 };
121 });
122 }
123
124 auto tag_invoke(json::value_to_tag<logger> const&, json::value const& jv, location const& loc) -> logger {
125 return as_checked_object(jv, loc, [obj_loc = loc](object_reader& r) -> logger {
126 auto const level_str = r.expect_at<std::string>("level");
127 auto level = log::level{};
128 if (level_str == "debug")
129 level = log::level::debug;
130 else if (level_str == "info")
131 level = log::level::info;
132 else if (level_str == "warn")
133 level = log::level::warn;
134 else if (level_str == "error")
135 level = log::level::error;
136 else
137 throw std::runtime_error{std::format("unable to parse log level {:?} at {}: expected one of {{debug, info, warn, error}}", level_str, obj_loc.sub("level").to_string())};
138 return logger{level};
139 });
140 }
141
142 auto json_value_to_app(json::value const& jv) -> app {
143 return as_checked_object(jv, location{}, [](object_reader& r) -> app {
144 return {
145 .rwgps = r.expect_at<rwgps>("rwgps"),
146 .situations = r.expect_at<situations>("situations"),
147 .database = r.expect_at<database>("database"),
148 .http_server = r.expect_at<http_server>("http_server"),
149 .logger = r.expect_at<logger>("logger"),
150 };
151 });
152 }
153
154 auto load_file(std::string const& filename) -> app {
155 auto f = std::ifstream{filename}; // TODO: ensure that we are opening in binary mode?
156 if (!f.is_open())
157 throw std::runtime_error{std::format("failed to open {}", filename)};
158 auto jv = json::value{};
159 try {
160 jv = json::parse(f);
161 } catch (boost::system::system_error const& e) {
162 throw std::runtime_error{std::format("failed to parse: {}", e.code().message())};
163 }
164 return json_value_to_app(jv);
165 }
166
167} // namespace routemon::config
diff --git a/server/src/config.cppm b/server/src/config.cppm
new file mode 100644
index 0000000..ef827a6
--- /dev/null
+++ b/server/src/config.cppm
@@ -0,0 +1,39 @@
1export module routemon:config;
2
3import std;
4import :log;
5
6namespace routemon::config {
7
8 export struct rwgps {
9 std::string api_key;
10 std::string auth_token;
11 };
12
13 export struct situations {
14 std::string datex2_filename;
15 };
16
17 export struct database {
18 std::string sqlite3_filename;
19 };
20
21 export struct http_server {
22 bool lax_cors;
23 };
24
25 export struct logger {
26 log::level level;
27 };
28
29 export struct app {
30 rwgps rwgps;
31 situations situations;
32 database database;
33 http_server http_server;
34 logger logger;
35 };
36
37 export auto load_file(std::string const& filename) -> app;
38
39} // namespace routemon::config
diff --git a/server/src/database.cppm b/server/src/database.cppm
new file mode 100644
index 0000000..0312dda
--- /dev/null
+++ b/server/src/database.cppm
@@ -0,0 +1,37 @@
1export module routemon:database;
2
3import std;
4import :sqlite3;
5
6namespace routemon::database {
7
8 static constexpr std::int64_t expected_database_version = 1;
9
10 export class connection {
11 sqlite3::connection dbc_;
12
13 explicit connection(sqlite3::connection dbc) : dbc_{std::move(dbc)} {}
14
15 friend auto open(std::string const& filename) -> std::shared_ptr<connection>;
16
17 public:
18 // Nothing here yet
19 };
20
21 export auto open(std::string const& filename) -> std::shared_ptr<connection> {
22 auto dbc = sqlite3::open(filename);
23 try {
24 auto version = std::optional<std::int64_t>{};
25 dbc.query("SELECT version FROM migration;").scan_single(version);
26 if (!version)
27 throw std::runtime_error{"failed to fetch database migration version"};
28 if (version != expected_database_version) {
29 throw std::runtime_error{std::format("database migration version ({}) does not match expected version ({}), consider running migrations", *version, expected_database_version)};
30 }
31 } catch (std::exception const& e) {
32 throw std::runtime_error{std::format("failed to query database version: {}", e.what())};
33 }
34 return std::shared_ptr<connection>{new connection{std::move(dbc)}};
35 }
36
37} // namespace routemon::database
diff --git a/server/src/datex2.cppm b/server/src/datex2.cppm
new file mode 100644
index 0000000..b507e6f
--- /dev/null
+++ b/server/src/datex2.cppm
@@ -0,0 +1,296 @@
1module;
2
3#include <boost/geometry/algorithms/is_empty.hpp>
4#include <boost/geometry/srs/transformation.hpp>
5#include <boost/geometry/srs/epsg.hpp>
6
7#include <pugixml.hpp>
8
9export module routemon:datex2;
10
11import std;
12import :geo;
13import :time;
14import :util;
15
16using namespace std::literals::string_view_literals;
17
18namespace routemon::datex2 {
19
20 export struct situation;
21
22 export struct road_closure {
23 std::weak_ptr<situation> parent;
24 std::optional<time::period_seq> validity;
25 std::vector<geo::point> relevant_points = {};
26 std::vector<std::shared_ptr<geo::linestring>> relevant_line_strings = {};
27 };
28
29 export struct situation {
30 std::string id;
31 std::optional<geo::point> location = std::nullopt; // as shown on the map, not used for querying
32 std::vector<std::string> comments = {};
33 std::vector<std::shared_ptr<road_closure>> road_closures = {};
34 };
35
36 export struct situation_publication {
37 time::timestamp publication_time;
38 std::vector<std::shared_ptr<situation>> situations;
39 };
40
41 auto parse_timestamp(char const* in) -> std::optional<time::timestamp> {
42 auto res = time::timestamp{};
43 auto is = std::istringstream{in};
44 is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res);
45 return is.fail() ? std::nullopt : std::make_optional(res);
46 }
47
48 export class loader {
49 // ETRS 89 (EPSG:4258) -> WGS 84 (EPSG:4326)
50 bgeo::srs::transformation<bgeo::srs::static_epsg<4258>, bgeo::srs::static_epsg<4326>> etrs89_to_wgs84_{};
51
52 std::multiset<std::string> warnings_;
53
54 auto add_location_from_xml(road_closure& rc, pugi::xml_node const& loc_xml) -> void {
55 auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()};
56 if (loc_xml_type == "loc:ItineraryByIndexedLocations") {
57 for (auto const loc_cont_xml : loc_xml.children("loc:locationContainedInItinerary")) {
58 add_location_from_xml(rc, loc_cont_xml.child("loc:location"));
59 }
60 } else if (loc_xml_type == "loc:LinearLocation" || loc_xml_type == "loc:SingleRoadLinearLocation") {
61 auto const& loc_gml_xml = loc_xml.child("loc:gmlLineString");
62 if (!loc_gml_xml)
63 return;
64
65 auto const srs_name = std::string_view{loc_gml_xml.attribute("srsName").value()};
66 if (srs_name != "WGS 84"sv) {
67 warnings_.insert(std::format("don't now how to handle the CRS {}", srs_name));
68 return;
69 }
70 auto const pos_list_str = std::string_view{loc_gml_xml.child_value("loc:posList")};
71 // lat1 long1 lat2 long2 ... lat(n-1) long(n-1) latn longn
72
73 auto ls = std::make_shared<geo::linestring>();
74
75 auto lat_set = false;
76 auto lat = 0.0;
77 for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv)) {
78 auto mlat_or_long = util::parse_double(std::string_view{lat_or_long_str});
79 if (!mlat_or_long) {
80 warnings_.insert(std::format("failed to parse coordinate {:?}", std::string_view{lat_or_long_str}));
81 return;
82 }
83
84 if (!lat_set) {
85 lat = *mlat_or_long;
86 lat_set = true;
87 } else {
88 bgeo::append(*ls, geo::point{*mlat_or_long, lat});
89 lat = 0;
90 lat_set = false;
91 }
92 }
93
94 if (bgeo::is_empty(*ls)) {
95 warnings_.emplace("empty line string in data set");
96 return;
97 }
98
99 rc.relevant_line_strings.push_back(ls);
100 } else if (loc_xml_type == "loc:PointLocation") {
101 auto const& coords_xml = loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates");
102 if (!coords_xml)
103 return;
104
105 auto mlat = util::parse_double(coords_xml.child_value("loc:latitude"));
106 auto mlon = util::parse_double(coords_xml.child_value("loc:longitude"));
107 if (!mlat || !mlon) {
108 warnings_.emplace("failed to parse PointLocation coordinates");
109 return;
110 }
111
112 // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat:
113 // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/
114 // maar heeft het UML-model van DATEX II v3 het over ETRS 89:
115 // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm
116
117 auto const coords_etrs89 = geo::point{*mlon, *mlat};
118 auto coords_wgs84 = geo::point{};
119 etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84);
120
121 rc.relevant_points.push_back(coords_wgs84);
122 } else {
123 warnings_.insert(std::format("don't know how to hande location of type {}, ignoring", loc_xml.attribute("xsi:type").value()));
124 return;
125 }
126 }
127
128 auto handle_road_or_carriageway_or_lane_management(pugi::xml_node const& record_xml, std::weak_ptr<situation> parent) -> std::optional<std::shared_ptr<road_closure>> {
129 auto const type = std::string_view{record_xml.child("sit:roadOrCarriagewayOrLaneManagementType").child_value()};
130 if (type != "carriagewayClosures" && type != "roadClosed")
131 // TODO: checken of er nog andere types fietsers de doorgang zouden kunnen blokkeren?
132 return std::nullopt;
133
134 auto const& restricted_vehicle_types_xml = record_xml.child("sit:forVehiclesWithCharacteristicsOf");
135 bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty();
136 for (auto const vehicle_type_xml : restricted_vehicle_types_xml.children("com:vehicleType")) {
137 auto vehicle_type = std::string_view{vehicle_type_xml.child_value()};
138 if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle" ||
139 vehicle_type == "unknown" || vehicle_type == "other") {
140 likely_restriction_for_bikes = true;
141 }
142 }
143 if (!likely_restriction_for_bikes)
144 return std::nullopt;
145
146 //---- Check if within the defined validity period
147
148 auto validity = std::optional<time::period_seq>{};
149 auto const& validity_xml = record_xml.child("sit:validity");
150 if (validity_xml && validity_xml.child_value("com:validityStatus") == "definedByValidityTimeSpec"sv) {
151 auto const& validity_spec_xml = validity_xml.child("com:validityTimeSpecification");
152
153 auto valid_periods = std::vector<time::period>{};
154 auto exception_periods = std::vector<time::period>{};
155
156 // TODO: com:overallEndTime may be missing (according to the DATEX II v3 data model)
157 auto const overall_start_time = parse_timestamp(validity_spec_xml.child_value("com:overallStartTime"));
158 auto const overall_end_time = parse_timestamp(validity_spec_xml.child_value("com:overallEndTime"));
159 if (overall_start_time && overall_end_time && *overall_start_time < *overall_end_time) {
160 valid_periods.emplace_back(*overall_start_time, *overall_end_time);
161
162 for (auto const valid_period_xml : validity_xml.children("com:validPeriod")) {
163 auto const start_of_period = parse_timestamp(valid_period_xml.child_value("com:startOfPeriod"));
164 auto const end_of_period = parse_timestamp(valid_period_xml.child_value("com:endOfPeriod"));
165 if (start_of_period && end_of_period && *start_of_period < *end_of_period) {
166 valid_periods.emplace_back(*start_of_period, *end_of_period);
167 }
168 }
169 for (auto const exception_period_xml : validity_xml.children("com:exceptionPeriod")) {
170 auto const start_of_period = parse_timestamp(exception_period_xml.child_value("com:startOfPeriod"));
171 auto const end_of_period = parse_timestamp(exception_period_xml.child_value("com:endOfPeriod"));
172 if (start_of_period && end_of_period && *start_of_period < *end_of_period) {
173 exception_periods.emplace_back(*start_of_period, *end_of_period);
174 }
175 }
176
177 validity = time::period_seq{valid_periods.begin(), valid_periods.end()}
178 .except(time::period_seq{exception_periods.begin(), exception_periods.end()});
179 } else {
180 warnings_.insert(std::format("invalid overall start / end time (start time: {}, end time: {})",
181 validity_spec_xml.child_value("com:overallStartTime"),
182 validity_spec_xml.child_value("com:overallEndTime")));
183 return std::nullopt;
184 }
185 }
186
187 //---- Try to extract the location info
188
189 auto rc = std::make_shared<road_closure>(std::move(parent), validity);
190 add_location_from_xml(*rc, record_xml.child("sit:locationReference"));
191 return rc;
192 }
193
194 public:
195 [[nodiscard]] auto load_situation_publication(std::string const& filename) -> situation_publication {
196 auto doc = pugi::xml_document{};
197 if (auto result = doc.load_file(filename.c_str()); !result) {
198 throw std::runtime_error{result.description()};
199 }
200 auto payload_xml = doc.child("mc:messageContainer").child("mc:payload");
201 auto mpublication_time = parse_timestamp(payload_xml.child_value("com:publicationTime"));
202 if (!mpublication_time)
203 throw std::runtime_error{"provided publication does not name publication time"};
204
205 auto situations = std::vector<std::shared_ptr<situation>>{};
206 for (auto const sit_xml : payload_xml.children("sit:situation")) {
207 auto id = std::string_view{sit_xml.attribute("id").value()};
208
209 auto const sit = std::make_shared<situation>(std::string{id});
210 situations.push_back(sit);
211
212 auto const& header_info_xml = sit_xml.child("sit:headerInformation");
213 if (header_info_xml.child_value("com:informationStatus") != "real"sv)
214 continue;
215
216 for (auto const record_xml : sit_xml.children("sit:situationRecord")) {
217 auto const record_type = std::string_view{record_xml.attribute("xsi:type").value()};
218 auto const primary_record_types = std::unordered_set<std::string_view>{
219 "sit:Roadworks",
220 /* { */ "sit:MaintenanceWorks",
221 /* | */ "sit:ConstructionWorks",
222 /* } */
223 "sit:Obstruction",
224 /* { */ "sit:EnvironmentalObstruction",
225 /* | */ "sit:GeneralObstruction",
226 /* | */ "sit:InfrastructureDamageObstruction",
227 /* } */
228 "sit:Activity",
229 /* { */ "sit:PublicEvent",
230 /* } */
231 };
232
233 if (record_type == "sit:RoadOrCarriagewayOrLaneManagement") {
234 if (auto rc = handle_road_or_carriageway_or_lane_management(record_xml, sit)) {
235 sit->road_closures.push_back(*rc);
236 }
237 } else if (primary_record_types.contains(record_type)) {
238 for (auto const comment_xml : record_xml.children("sit:generalPublicComment")) {
239// if (comment_xml.child_value("sit:commentType") == "internalNote"sv) {
240 auto candidate = std::optional<std::pair<std::string_view, std::string_view>>{}; // (text, language)
241 for (auto const comment_value_xml : comment_xml.child("sit:comment").child("com:values").children("com:value")) {
242 if (!candidate ||
243 comment_value_xml.attribute("lang").value() == "nl"sv ||
244 (candidate->second != "nl"sv && comment_value_xml.attribute("lang").value() == "nl"sv)) {
245 candidate = std::make_pair(comment_value_xml.child_value(), comment_value_xml.attribute("lang").value());
246 }
247 }
248 if (candidate) {
249 auto already_present = false;
250 for (auto const& comment : sit->comments)
251 already_present = already_present || comment == candidate->first;
252 if (!already_present) {
253 sit->comments.emplace_back(candidate->first);
254 }
255 }
256// }
257 }
258
259 if (auto const location_ref_xml = record_xml.child("sit:locationReference")) {
260 if (location_ref_xml.attribute("xsi:type").value() == "loc:PointLocation"sv) {
261 if (auto const coords_xml = location_ref_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates")) {
262 auto const mlat = util::parse_double(coords_xml.child_value("loc:latitude"));
263 auto const mlon = util::parse_double(coords_xml.child_value("loc:longitude"));
264 if (mlat && mlon) {
265 // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat:
266 // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/
267 // maar heeft het UML-model van DATEX II v3 het over ETRS 89:
268 // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm
269
270 auto const coords_etrs89 = geo::point{*mlon, *mlat};
271 auto coords_wgs84 = geo::point{};
272 etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84);
273
274 if (!sit->location) {
275 sit->location = coords_wgs84;
276 }
277 }
278 }
279 }
280 }
281 }
282 }
283 }
284
285 return {
286 .publication_time = *mpublication_time,
287 .situations = situations,
288 };
289 }
290
291 [[nodiscard]] auto warnings() const -> std::multiset<std::string> const& {
292 return warnings_;
293 }
294 };
295
296} // namespace routemon::datex2
diff --git a/server/src/geo.cppm b/server/src/geo.cppm
new file mode 100644
index 0000000..5cdbdd9
--- /dev/null
+++ b/server/src/geo.cppm
@@ -0,0 +1,40 @@
1module;
2
3#include <boost/geometry.hpp>
4
5export module routemon:geo;
6
7export namespace bgeo = boost::geometry;
8
9export namespace routemon::geo {
10
11 using point = bgeo::model::point<double, 2, bgeo::cs::spherical_equatorial<bgeo::degree>>;
12 using linestring = bgeo::model::linestring<point>;
13 using box = bgeo::model::box<point>;
14 using stype = bgeo::srs::spheroid<double>;
15 using vincenty_strategy = bgeo::strategy::distance::vincenty<stype>;
16
17 auto split_linestring_with_overlap_segments(linestring const& ls, double max_split_distance_m, std::vector<linestring>& append_to) -> void {
18 if (bgeo::is_empty(ls))
19 return;
20
21 auto current_ls = linestring{};
22 auto current_ls_length = 0.0;
23 auto previous = std::optional<point>{};
24 bgeo::for_each_point(ls, [&](point p) -> void {
25 bgeo::append(current_ls, p);
26 if (previous) {
27 auto d = bgeo::distance(*previous, p, vincenty_strategy());
28 current_ls_length += d;
29 if (current_ls_length > max_split_distance_m) {
30 append_to.push_back(std::move(current_ls));
31 current_ls = linestring{*previous, p};
32 current_ls_length = d;
33 }
34 }
35 previous = p;
36 });
37 append_to.emplace_back(std::move(current_ls));
38 }
39
40} // namespace routemon::geo
diff --git a/server/src/gpx.cpp b/server/src/gpx.cpp
new file mode 100644
index 0000000..62dccff
--- /dev/null
+++ b/server/src/gpx.cpp
@@ -0,0 +1,189 @@
1module;
2
3#include <boost/geometry/algorithms/append.hpp>
4
5module routemon:gpx$impl;
6
7import std;
8import :gpx;
9import :util;
10import :xml;
11
12using namespace std::literals::string_view_literals;
13
14namespace routemon::gpx {
15
16 namespace v10 {
17
18 constexpr auto xmlns = "http://www.topografix.com/GPX/1/0"sv;
19 auto qname(std::string_view local) -> xml::qname_view {
20 return {.ns_uri = xmlns, .local = local};
21 }
22
23 auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser<geo::point> {
24 auto parse_xml_double = [](std::string_view sv) -> std::optional<double> {
25 return util::parse_double(sv, std::chars_format::fixed);
26 };
27 auto mlat = std::optional<double>{};
28 auto mlon = std::optional<double>{};
29 for (auto const& [name, value] : attrs) {
30 if (name == qname("lat")) {
31 mlat = parse_xml_double(value);
32 } else if (name == qname("lon")) {
33 mlon = parse_xml_double(value);
34 }
35 }
36 if (!mlat || !mlon)
37 throw std::runtime_error{"expected valid latitude and longitude for waypoint"};
38 co_await xml::ignore_contents(e);
39 co_return geo::point{*mlon, *mlat};
40 }
41
42 auto parse_trkseg(xml::executor_ref e, xml::attribute_view) -> xml::parser<track_segment> {
43 auto s = track_segment{};
44 while (auto mwpt = co_await allow_element(e, qname("trkpt"), xml::hohalo<parse_wpt>()))
45 bgeo::append(s.waypoints, *mwpt);
46 co_return std::move(s);
47 }
48
49 auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser<track> {
50 auto t = track{};
51 t.name = co_await allow_element(e, qname("name"), xml::hohalo<xml::read_string_contents>());
52 co_await allow_element(e, qname("cmt"), xml::hohalo<xml::ignore_element_contents>());
53 t.desc = co_await allow_element(e, qname("desc"), xml::hohalo<xml::read_string_contents>());
54 co_await xml::ignore_contents(e, /* until */ qname("trkseg"));
55 while (auto mseg = co_await allow_element(e, qname("trkseg"), xml::hohalo<parse_trkseg>()))
56 t.segments.push_back(std::move(*mseg));
57 co_return std::move(t);
58 }
59
60 auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser<file> {
61 auto f = file{};
62 if (attrs.lookup(qname("version")) != "1.0"sv)
63 throw std::runtime_error{"expected GPX version to be 1.0"};
64 if (auto mcreator = attrs.lookup(qname("creator")))
65 f.creator = *mcreator;
66 else
67 throw std::runtime_error{"expected GPX file to have creator"};
68
69 f.meta.name = co_await allow_element(e, qname("name"), xml::hohalo<xml::read_string_contents>());
70 f.meta.desc = co_await allow_element(e, qname("desc"), xml::hohalo<xml::read_string_contents>());
71
72 co_await xml::ignore_contents(e, /* until */ qname("trk"));
73 while (auto mtrk = co_await allow_element(e, qname("trk"), xml::hohalo<parse_trk>()))
74 f.tracks.push_back(std::move(*mtrk));
75 co_await xml::ignore_contents(e);
76
77 co_return std::move(f);
78 }
79
80 } // namespace v10
81
82 namespace v11 {
83
84 constexpr auto xmlns = "http://www.topografix.com/GPX/1/1"sv;
85 auto qname(std::string_view local) -> xml::qname_view {
86 return {.ns_uri = xmlns, .local = local};
87 }
88
89 auto parse_metadata(xml::executor_ref e, xml::attribute_view) -> xml::parser<metadata> {
90 auto meta = metadata{};
91 meta.name = co_await allow_element(e, qname("name"), xml::hohalo<xml::read_string_contents>());
92 meta.desc = co_await allow_element(e, qname("desc"), xml::hohalo<xml::read_string_contents>());
93 co_await xml::ignore_contents(e);
94 co_return meta;
95 }
96
97 auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser<geo::point> {
98 auto parse_xml_double = [](std::string_view sv) -> std::optional<double> {
99 return util::parse_double(sv, std::chars_format::fixed);
100 };
101 auto mlat = std::optional<double>{};
102 auto mlon = std::optional<double>{};
103 for (auto const& [name, value] : attrs) {
104 if (name == qname("lat")) {
105 mlat = parse_xml_double(value);
106 } else if (name == qname("lon")) {
107 mlon = parse_xml_double(value);
108 }
109 }
110 if (!mlat || !mlon)
111 throw std::runtime_error{"expected valid latitude and longitude for waypoint"};
112 co_await xml::ignore_contents(e);
113 co_return geo::point{*mlon, *mlat};
114 }
115
116 auto parse_trkseg(xml::executor_ref e, xml::attribute_view) -> xml::parser<track_segment> {
117 auto s = track_segment{};
118 while (auto mwpt = co_await allow_element(e, qname("trkpt"), xml::hohalo<parse_wpt>()))
119 bgeo::append(s.waypoints, *mwpt);
120 co_await allow_element(e, qname("extensions"), xml::hohalo<xml::ignore_element_contents>());
121 co_return std::move(s);
122 }
123
124 auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser<track> {
125 auto t = track{};
126 t.name = co_await allow_element(e, qname("name"), xml::hohalo<xml::read_string_contents>());
127 co_await allow_element(e, qname("cmt"), xml::hohalo<xml::ignore_element_contents>());
128 t.desc = co_await allow_element(e, qname("desc"), xml::hohalo<xml::read_string_contents>());
129 co_await xml::ignore_contents(e, /* until */ qname("trkseg"));
130 while (auto mseg = co_await allow_element(e, qname("trkseg"), xml::hohalo<parse_trkseg>()))
131 t.segments.push_back(std::move(*mseg));
132 co_return std::move(t);
133 }
134
135 auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser<file> {
136 auto f = file{};
137 if (attrs.lookup(qname("version")) != "1.1"sv)
138 throw std::runtime_error{"expected GPX version to be 1.1"};
139 if (auto mcreator = attrs.lookup(qname("creator")))
140 f.creator = *mcreator;
141 else
142 throw std::runtime_error{"expected GPX file to have creator"};
143
144 if (auto mmeta = co_await allow_element(e, qname("metadata"), xml::hohalo<parse_metadata>()))
145 f.meta = *mmeta;
146 co_await xml::ignore_contents(e, /* until */ qname("trk"));
147 while (auto mtrk = co_await allow_element(e, qname("trk"), xml::hohalo<parse_trk>()))
148 f.tracks.push_back(std::move(*mtrk));
149 co_await allow_element(e, qname("extensions"), xml::hohalo<xml::ignore_element_contents>());
150
151 co_return std::move(f);
152 }
153
154 } // namespace v11
155
156 auto parse_file(xml::executor_ref e) -> xml::parser<file> {
157 auto decl = co_await expect_event<xml::xml_decl_event>(e);
158 if (decl.version != "1.0"sv)
159 throw std::runtime_error{std::format("unsupported XML version, got {}", std::string_view{decl.version})};
160 if (decl.encoding != "UTF-8"sv)
161 throw std::runtime_error{"unsupported encoding"};
162 if (auto mf = co_await allow_element(e, v10::qname("gpx"), xml::hohalo<v10::parse_gpx>()))
163 co_return std::move(*mf);
164 if (auto mf = co_await allow_element(e, v11::qname("gpx"), xml::hohalo<v11::parse_gpx>()))
165 co_return std::move(*mf);
166 throw std::runtime_error{"no supported GPX document found"};
167 }
168
169 reader::reader()
170 : p_{parse_file(util::not_null{&e_})}
171 { e_.set_continuation(p_.promise().base_handle()); }
172
173 auto reader::init() -> void {
174 e_.start();
175 }
176
177 auto reader::put(std::string_view buf) -> void {
178 e_.read(buf, false);
179 }
180
181 auto reader::finish() -> gpx::file {
182 e_.read(std::string_view{}, true);
183 e_.end();
184 // Promise is still alive since the last coroutine performs a
185 // symmetric transfer to std::noop_coroutine() in final_suspend().
186 return std::move(p_.promise().returned_value());
187 }
188
189} // namespace routemon::gpx
diff --git a/server/src/gpx.cppm b/server/src/gpx.cppm
new file mode 100644
index 0000000..20e6242
--- /dev/null
+++ b/server/src/gpx.cppm
@@ -0,0 +1,43 @@
1export module routemon:gpx;
2
3import std;
4import :geo;
5import :util;
6import :xml;
7
8namespace routemon::gpx {
9
10 struct metadata {
11 std::optional<std::string> name;
12 std::optional<std::string> desc;
13 };
14
15 struct track_segment {
16 geo::linestring waypoints;
17 };
18
19 struct track {
20 std::optional<std::string> name;
21 std::optional<std::string> desc;
22 std::vector<track_segment> segments;
23 };
24
25 struct file {
26 std::string creator;
27 metadata meta;
28 std::vector<track> tracks;
29 };
30
31 class reader {
32 xml::executor e_;
33 xml::parser<file> p_;
34
35 public:
36 explicit reader();
37
38 auto init() -> void;
39 auto put(std::string_view buf) -> void;
40 [[nodiscard]] auto finish() -> gpx::file;
41 };
42
43} // namespace routemon::gpx
diff --git a/server/src/http_client.cppm b/server/src/http_client.cppm
new file mode 100644
index 0000000..d5316d6
--- /dev/null
+++ b/server/src/http_client.cppm
@@ -0,0 +1,62 @@
1module;
2
3#include <boost/asio/connect.hpp>
4#include <boost/asio/ip/tcp.hpp>
5#include <boost/asio/ssl.hpp>
6#include <boost/beast/core.hpp>
7#include <boost/beast/http.hpp>
8
9export module routemon:http.client;
10
11export import :http.common;
12
13namespace net = boost::asio;
14namespace ssl = net::ssl;
15using tcp = net::ip::tcp;
16
17namespace routemon::http {
18
19 export class client {
20 net::io_context& ioc_;
21 ssl::context sslc_{ssl::context::tlsv12_client};
22 tcp::resolver resolver_;
23
24 public:
25 explicit client(net::io_context& ioc)
26 : ioc_{ioc}, resolver_{ioc}
27 {
28 sslc_.set_default_verify_paths();
29 sslc_.set_verify_mode(net::ssl::verify_peer | net::ssl::verify_fail_if_no_peer_cert);
30 }
31
32 template<class ReqBody>
33 auto do_request(bhttp::request<ReqBody>& req) -> bhttp::response<bhttp::dynamic_body> {
34 auto stream = ssl::stream<beast::tcp_stream>{ioc_, sslc_};
35
36 auto host = std::string{req.at(bhttp::field::host)};
37 if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) {
38 throw beast::system_error(static_cast<int>(::ERR_get_error()),
39 net::error::get_ssl_category());
40 }
41 stream.set_verify_callback(ssl::host_name_verification(host));
42 auto const results = resolver_.resolve(host, "443");
43 beast::get_lowest_layer(stream).connect(results);
44 stream.handshake(ssl::stream_base::client);
45
46 req.set(bhttp::field::user_agent, "routemon/1.0");
47 bhttp::write(stream, req);
48
49 auto buffer = beast::flat_buffer{};
50 auto res = bhttp::response<bhttp::dynamic_body>{};
51 bhttp::read(stream, buffer, res);
52
53 auto ec = beast::error_code{};
54 stream.shutdown(ec);
55 if (ec != net::ssl::error::stream_truncated)
56 throw beast::system_error{ec};
57
58 return res;
59 }
60 };
61
62} // namespace routemon::http
diff --git a/server/src/http_common.cppm b/server/src/http_common.cppm
new file mode 100644
index 0000000..3c38da7
--- /dev/null
+++ b/server/src/http_common.cppm
@@ -0,0 +1,139 @@
1module;
2
3#include <boost/beast/core.hpp>
4#include <boost/beast/http.hpp>
5
6export module routemon:http.common;
7
8namespace beast = boost::beast;
9export namespace bhttp = beast::http;
10
11namespace boost::beast {
12
13 namespace concepts {
14
15 template<class T>
16 concept buffers_generator = beast::is_buffers_generator<T>::value;
17
18 template<class T>
19 concept const_buffer_sequence = beast::is_const_buffer_sequence<T>::value;
20
21 } // namespace concepts
22
23 namespace http::concepts {
24
25 template<class T>
26 concept fields = is_fields<T>::value;
27
28 template<class T>
29 concept body = is_body<T>::value;
30
31 template<class T>
32 concept body_reader = is_body_reader<T>::value;
33
34 } // namespace http::concepts
35
36} // namespace boost::beast
37
38namespace routemon::http {
39
40 struct supported_verb {
41 enum supported_verb_t : std::uint8_t {
42 options,
43 delete_,
44 get,
45 head,
46 post,
47 put,
48 };
49
50 supported_verb_t value;
51
52 supported_verb(supported_verb_t value) : value{value} {}
53
54 static auto from(bhttp::verb v) -> std::optional<supported_verb> {
55 switch (v) {
56 case bhttp::verb::options: return supported_verb::options;
57 case bhttp::verb::delete_: return supported_verb::delete_;
58 case bhttp::verb::get: return supported_verb::get;
59 case bhttp::verb::head: return supported_verb::head;
60 case bhttp::verb::post: return supported_verb::post;
61 case bhttp::verb::put: return supported_verb::put;
62 default: return std::nullopt;
63 }
64 }
65
66 operator bhttp::verb() const {
67 switch (value) {
68 case supported_verb::options: return bhttp::verb::options;
69 case supported_verb::delete_: return bhttp::verb::delete_;
70 case supported_verb::get: return bhttp::verb::get;
71 case supported_verb::head: return bhttp::verb::head;
72 case supported_verb::post: return bhttp::verb::post;
73 case supported_verb::put: return bhttp::verb::put;
74 }
75 }
76 };
77
78 export struct verb_set {
79 bool delete_ : 1 = false;
80 bool get : 1 = false;
81 bool head : 1 = false;
82 bool post : 1 = false;
83 bool put : 1 = false;
84 bool options : 1 = false;
85
86 auto enable(supported_verb v) -> void {
87 switch (v.value) {
88 case supported_verb::delete_:
89 delete_ = true;
90 break;
91 case supported_verb::get:
92 get = true;
93 break;
94 case supported_verb::head:
95 head = true;
96 break;
97 case supported_verb::post:
98 post = true;
99 break;
100 case supported_verb::put:
101 put = true;
102 break;
103 case supported_verb::options:
104 options = true;
105 break;
106 default:;
107 }
108 }
109
110 auto operator==(verb_set const& rhs) const noexcept -> bool = default;
111
112 auto empty() const -> bool {
113 return *this == verb_set{};
114 }
115
116 verb_set(std::initializer_list<supported_verb> vs) {
117 for (auto const v : vs) enable(v);
118 }
119
120 auto to_string() const -> std::string {
121 std::ostringstream ss;
122 bool wrote = false;
123 auto write = [&](bhttp::verb v) {
124 if (wrote)
125 ss << ", ";
126 ss << v;
127 wrote = true;
128 };
129 if (delete_) write(bhttp::verb::delete_);
130 if (get) write(bhttp::verb::get);
131 if (head) write(bhttp::verb::head);
132 if (post) write(bhttp::verb::post);
133 if (put) write(bhttp::verb::put);
134 if (options) write(bhttp::verb::options);
135 return ss.str();
136 }
137 };
138
139} // namespace routemon::http
diff --git a/server/src/http_server.cppm b/server/src/http_server.cppm
new file mode 100644
index 0000000..0770d97
--- /dev/null
+++ b/server/src/http_server.cppm
@@ -0,0 +1,661 @@
1module;
2
3#include <boost/config.hpp>
4#include <boost/asio/as_tuple.hpp>
5#include <boost/asio/awaitable.hpp>
6#include <boost/asio/co_spawn.hpp>
7#include <boost/asio/ip/tcp.hpp>
8#include <boost/beast/core.hpp>
9#include <boost/beast/http.hpp>
10#include <boost/json/serialize.hpp>
11#include <boost/url.hpp>
12
13export module routemon:http.server;
14
15import std;
16import :config;
17import :trace;
18export import :http.common;
19import :problem;
20
21namespace net = boost::asio;
22using tcp = net::ip::tcp;
23
24namespace routemon::http {
25
26 struct readable_request {
27 util::not_null<bhttp::request_parser<bhttp::empty_body>*> p;
28 util::not_null<beast::tcp_stream*> strm;
29 util::not_null<beast::flat_buffer*> buf;
30 };
31
32 class presponse {
33 public:
34 using const_buffers_type = beast::span<net::const_buffer>;
35
36 private:
37 struct impl_base {
38 virtual ~impl_base() = default;
39 virtual auto header() -> bhttp::response_header<bhttp::fields>& = 0;
40 virtual auto header() const -> bhttp::response_header<bhttp::fields> const& = 0;
41 virtual auto is_done() const -> bool = 0;
42 virtual auto prepare(beast::error_code&) -> const_buffers_type = 0;
43 virtual auto consume(std::size_t n) -> void = 0;
44 virtual auto keep_alive() const -> bool = 0;
45 };
46 std::unique_ptr<impl_base> impl_;
47
48 template<bhttp::concepts::body Body>
49 class impl : public impl_base {
50 // Initializes in the response state.
51 // At the first call to prepare, we switch to the message generator state.
52 // After that point, header may not be called anymore (it will throw).
53 std::variant<bhttp::response<Body>, bhttp::message_generator> state_;
54
55 auto ensure_message_generator() -> bhttp::message_generator& {
56 if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) {
57 auto rsp = bhttp::response<Body>{std::move(*prsp)};
58 state_.template emplace<bhttp::message_generator>(std::move(rsp));
59 }
60 return std::get<bhttp::message_generator>(state_);
61 }
62
63 public:
64 explicit impl(bhttp::response<Body>&& rsp) : state_{std::move(rsp)} {}
65
66 auto header() -> bhttp::response_header<bhttp::fields>& override {
67 if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) {
68 return prsp->base();
69 } else {
70 // TODO: define custom exception type presponse::bad_header_access
71 throw std::logic_error{"header() may not be called after prepare()"};
72 }
73 }
74 auto header() const -> bhttp::response_header<bhttp::fields> const& override {
75 if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) {
76 return prsp->base();
77 } else {
78 throw std::logic_error{"header() may not be called after prepare()"};
79 }
80 }
81
82 auto is_done() const -> bool override {
83 if (auto pgen = std::get_if<bhttp::message_generator>(&state_)) {
84 return pgen->is_done();
85 } else /* still in the response state */ {
86 return false;
87 }
88 }
89
90 auto prepare(beast::error_code& ec) -> const_buffers_type override {
91 return ensure_message_generator().prepare(ec);
92 }
93
94 auto consume(std::size_t n) -> void override {
95 ensure_message_generator().consume(n);
96 }
97
98 auto keep_alive() const noexcept -> bool override {
99 return state_.visit(util::overloaded{
100 [](bhttp::response<Body> const& rsp) -> bool {
101 return rsp.keep_alive();
102 },
103 [](bhttp::message_generator const& gen) -> bool {
104 return gen.keep_alive();
105 },
106 });
107 }
108 };
109
110 public:
111 template<bhttp::concepts::body Body>
112 explicit presponse(bhttp::response<Body>&& rsp)
113 : impl_{new impl{std::move(rsp)}}
114 {}
115
116 auto header() -> bhttp::response_header<bhttp::fields>& {
117 return impl_->header();
118 }
119 auto header() const -> bhttp::response_header<bhttp::fields> const& {
120 return impl_->header();
121 }
122
123 auto is_done() const -> bool {
124 return impl_->is_done();
125 }
126
127 auto prepare(beast::error_code& ec) -> const_buffers_type {
128 return impl_->prepare(ec);
129 }
130
131 auto consume(std::size_t n) -> void {
132 return impl_->consume(n);
133 }
134
135 auto keep_alive() const noexcept -> bool {
136 return impl_->keep_alive();
137 }
138 };
139 static_assert(beast::concepts::buffers_generator<presponse>);
140
141 template<class Ctx>
142 using next_handler_t = std::function<auto(Ctx) -> net::awaitable<presponse>>;
143
144 template<class OuterCtx, class InnerCtx>
145 using middleware_t = std::function<auto(OuterCtx, bhttp::request_header<bhttp::fields>&, next_handler_t<InnerCtx>) -> net::awaitable<presponse>>;
146
147 template<class Ctx>
148 auto lax_cors_middleware(Ctx ctx, bhttp::request_header<bhttp::fields>& req_hdr, next_handler_t<Ctx> next) -> net::awaitable<presponse> {
149 std::ignore = req_hdr;
150 auto prersp = co_await next(ctx);
151 prersp.header().set(bhttp::field::access_control_allow_origin, "*");
152 co_return std::move(prersp);
153 }
154
155 template<class InnerCtx>
156 struct trace_id_ctx : InnerCtx {
157 trace::id trace_id = {};
158 };
159
160 template<class OuterCtx>
161 auto trace_id_middleware(OuterCtx ctx0, bhttp::request_header<bhttp::fields>& req_hdr, next_handler_t<trace_id_ctx<OuterCtx>> next) -> net::awaitable<presponse> {
162 std::ignore = req_hdr;
163 auto ctx = trace_id_ctx{std::move(ctx0)};
164 auto prersp = co_await next(std::move(ctx));
165 prersp.header().set("X-Routemon-Trace-Id", std::string_view{ctx.trace_id.as_string()});
166 prersp.header().insert(bhttp::field::access_control_expose_headers, "X-Routemon-Trace-Id");
167 co_return std::move(prersp);
168 }
169
170 struct base_ctx {
171 std::locale locale;
172 };
173
174 template<class Ctx>
175 using basic_route_handler_fn_t = std::function<auto(Ctx, readable_request, std::vector<std::string> const& matches) -> net::awaitable<presponse>>;
176
177 struct keep_alive {
178 bool value;
179
180 explicit keep_alive(bool value) : value{value} {}
181 };
182
183 template<bhttp::concepts::body Body>
184 auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response<Body> {
185 auto rsp = bhttp::response<Body>{}; // HTTP version gets set later
186 rsp.result(status);
187 rsp.keep_alive(ka.value);
188 return rsp;
189 }
190
191 auto problem_rsp(base_ctx const& ctx, problem::details const& problem, keep_alive ka) -> presponse {
192 auto rsp = make_rsp<bhttp::string_body>(problem.status, ka);
193 rsp.set(bhttp::field::content_type, "application/problem+json");
194 rsp.body() = json::serialize(json::value_from(problem, ctx.locale));
195 rsp.prepare_payload();
196 return presponse{std::move(rsp)};
197 }
198
199 struct preflight_response {
200 verb_set allow_methods;
201 std::vector<bhttp::field> allow_headers;
202 };
203 auto make_preflight_rsp(preflight_response res, keep_alive ka) -> bhttp::response<bhttp::empty_body> {
204 auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, ka);
205 auto allow_headers_str = res.allow_headers
206 | std::views::transform([](auto const& field) -> std::string_view { return bhttp::to_string(field); })
207 | std::views::join_with(std::string_view{", "})
208 | std::ranges::to<std::string>();
209 rsp.set(bhttp::field::access_control_allow_methods, res.allow_methods.to_string());
210 rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str);
211 rsp.prepare_payload();
212 return rsp;
213 }
214
215 // Using base_ctx instead of a template here since that saves you
216 // typing on invocation (and we do not care about the context type
217 // anyway, but all context types should derive from base_ctx).
218 template<bhttp::concepts::body_reader Body>
219 auto read_request(base_ctx const& ctx, readable_request&& r) -> net::awaitable<std::expected<bhttp::request<Body>, presponse>> {
220 std::ignore = ctx;
221 auto p = bhttp::request_parser<Body>{std::move(*r.p)};
222 co_await bhttp::async_read(*r.strm, *r.buf, p);
223 co_return std::move(p.release());
224 }
225
226 template<>
227 auto read_request<bhttp::empty_body>(base_ctx const& ctx, readable_request&& r) -> net::awaitable<std::expected<bhttp::request<bhttp::empty_body>, presponse>> {
228 auto [ec, _] = co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple);
229 if (ec == bhttp::error::unexpected_body) {
230 auto tpl = problem::tpl{
231 .status = bhttp::status::bad_request,
232 .title = translate("No body expected for this request"),
233 .type_uri = "https://routemon.fautchen.eu/problems/unexpected-body",
234 };
235 co_return std::unexpected{problem_rsp(ctx, tpl.instantiate(), keep_alive{false})};
236 } else if (ec) {
237 throw boost::system::system_error{ec};
238 }
239 co_return r.p->release();
240 }
241
242 template<class InnerCtx>
243 struct routed_ctx : InnerCtx {
244 verb_set route_methods;
245 };
246
247 template<class Ctx>
248 requires requires(Ctx ctx) {
249 // Ctx must be derived from an instantiation of routed_ctx
250 []<class InnerCtx>(routed_ctx<InnerCtx> const&) {}(ctx);
251 }
252 auto default_options_handler(Ctx const& ctx, readable_request r, std::vector<std::string> const&) -> net::awaitable<presponse> {
253 auto mreq = co_await read_request<bhttp::empty_body>(ctx, std::move(r));
254 if (!mreq)
255 co_return std::move(mreq.error());
256
257 if (mreq->find(bhttp::field::access_control_request_method) != mreq->end()) {
258 // CORS preflight request
259 co_return make_preflight_rsp(preflight_response{
260 // TODO: should access-control-allow-methods contain OPTIONS?
261 .allow_methods = ctx.route_methods,
262 .allow_headers = {bhttp::field::content_type},
263 }, keep_alive{mreq->keep_alive()});
264 } else {
265 // Normal OPTIONS request
266 auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, keep_alive{mreq->keep_alive()});
267 rsp.set(bhttp::field::allow, ctx.route_methods.to_string());
268 rsp.prepare_payload();
269 co_return std::move(rsp);
270 }
271 }
272
273 auto global_options_handler(base_ctx const& ctx, readable_request r) -> net::awaitable<presponse> {
274 // TODO: switch to "small (4KB) discarded" body type, similar to what Go does?
275 // Same goes for default_options_handler? Not sure.
276 if (auto res = co_await read_request<bhttp::empty_body>(ctx, std::move(r)); !res)
277 co_return std::move(res.error());
278 auto req = r.p->release();
279 auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, keep_alive{req.keep_alive()});
280 rsp.prepare_payload();
281 co_return std::move(rsp);
282 }
283
284 template<class Ctx>
285 auto id_middleware(Ctx ctx, bhttp::request_header<bhttp::fields>&, next_handler_t<Ctx> next) -> net::awaitable<presponse> {
286 co_return co_await next(std::move(ctx));
287 }
288
289 template<class A, class B, class C>
290 auto middleware_compose(middleware_t<A, B> ab, middleware_t<B, C> bc) -> middleware_t<A, C> {
291 return [ab = std::move(ab), bc = std::move(bc)](A a, bhttp::request_header<bhttp::fields>& header, next_handler_t<C> next) -> net::awaitable<presponse> {
292 co_return co_await ab(std::move(a), header, [&](B b) -> net::awaitable<presponse> {
293 co_return co_await bc(std::move(b), header, next);
294 });
295 };
296 }
297
298 template<class A, class B>
299 auto middleware_wrap_fn(middleware_t<A, B> ab, basic_route_handler_fn_t<B> fn) -> basic_route_handler_fn_t<A> {
300 return [ab = std::move(ab), fn = std::move(fn)](A a_ctx, readable_request r, std::vector<std::string> const& matches) -> net::awaitable<presponse> {
301 co_return co_await ab(std::move(a_ctx), r.p->get().base(), [&](B b_ctx) -> net::awaitable<presponse> {
302 co_return co_await fn(std::move(b_ctx), r, matches);
303 });
304 };
305 }
306
307 template<std::default_initializable V>
308 requires requires(V v) {
309 { static_cast<bool>(v) };
310 }
311 struct handler_map {
312 V options = {};
313 V delete_ = {};
314 V get = {};
315 V head = {};
316 V post = {};
317 V put = {};
318
319 template<class Self>
320 auto lookup(this Self&& self, supported_verb v) -> auto&& {
321 switch (v.value) {
322 case supported_verb::options: return std::forward<Self>(self).options;
323 case supported_verb::delete_: return std::forward<Self>(self).delete_;
324 case supported_verb::get: return std::forward<Self>(self).get;
325 case supported_verb::head: return std::forward<Self>(self).head;
326 case supported_verb::post: return std::forward<Self>(self).post;
327 case supported_verb::put: return std::forward<Self>(self).put;
328 }
329 }
330
331 auto verbs() const -> verb_set {
332 auto set = verb_set{};
333 if (static_cast<bool>(options))
334 set.enable(supported_verb::options);
335 if (static_cast<bool>(delete_))
336 set.enable(supported_verb::delete_);
337 if (static_cast<bool>(get))
338 set.enable(supported_verb::get);
339 if (static_cast<bool>(head))
340 set.enable(supported_verb::head);
341 if (static_cast<bool>(post))
342 set.enable(supported_verb::post);
343 if (static_cast<bool>(put))
344 set.enable(supported_verb::put);
345 return set;
346 }
347
348 auto empty() const -> bool {
349 return verbs().empty();
350 }
351
352 template<std::default_initializable U>
353 auto map(std::invocable<V const&> auto f) const -> handler_map<U>
354 requires std::assignable_from<U&, std::invoke_result_t<decltype(f), V const&>>
355 {
356 return {
357 .options = static_cast<bool>(options) ? f(options) : U{},
358 .delete_ = static_cast<bool>(delete_) ? f(delete_) : U{},
359 .get = static_cast<bool>(get) ? f(get) : U{},
360 .head = static_cast<bool>(head) ? f(head) : U{},
361 .post = static_cast<bool>(post) ? f(post) : U{},
362 .put = static_cast<bool>(put) ? f(put) : U{},
363 };
364 }
365 };
366
367 template<class Ctx>
368 struct route_tree {
369 using leaves = handler_map<basic_route_handler_fn_t<Ctx>>;
370 using named_subtrees = std::unordered_map<std::string, route_tree>;
371 using wildcard_subtree = std::indirect<route_tree>;
372
373 leaves here;
374 // TODO: consider making the first alternative a radix tree
375 // Note: the map is the first variant here; the variant will be
376 // default-constructed with the default-constructed first
377 // alternative. The empty map denotes a lack of subtrees.
378 std::variant<named_subtrees, wildcard_subtree> sub;
379 };
380
381 template<class OuterCtx, class InnerCtx>
382 auto middleware_wrap_tree(middleware_t<OuterCtx, InnerCtx> mw, route_tree<InnerCtx> const& tree) -> route_tree<OuterCtx> {
383 auto new_leaves = tree.here.template map<basic_route_handler_fn_t<OuterCtx>>(std::bind_front(middleware_wrap_fn<OuterCtx, InnerCtx>, mw));
384 auto new_sub = tree.sub.visit(util::overloaded{
385 [&mw](route_tree<InnerCtx>::named_subtrees const& subtrees) -> decltype(route_tree<OuterCtx>::sub) {
386 auto new_subtrees = typename route_tree<OuterCtx>::named_subtrees{};
387 for (auto [seg, subtree] : subtrees)
388 new_subtrees[seg] = middleware_wrap_tree(mw, subtree);
389 return new_subtrees;
390 },
391 [&mw](route_tree<InnerCtx>::wildcard_subtree const& subtree) -> decltype(route_tree<OuterCtx>::sub) {
392 return typename route_tree<OuterCtx>::wildcard_subtree{middleware_wrap_tree(mw, *subtree)};
393 },
394 });
395 return {.here = new_leaves, .sub = new_sub};
396 }
397
398 template<class T>
399 concept match_arg = std::constructible_from<T, std::string const&>;
400
401 template<class Ctx, match_arg... MatchArgs>
402 using route_handler_fn_t = std::function<auto(Ctx, readable_request, MatchArgs...) -> net::awaitable<presponse>>;
403
404 template<class Ctx, match_arg... MatchArgs>
405 auto degen_route_handler(route_handler_fn_t<Ctx, MatchArgs...> fn) -> basic_route_handler_fn_t<Ctx> {
406 return [fn = std::move(fn)](Ctx ctx, readable_request r, std::vector<std::string> const& matches) -> net::awaitable<presponse> {
407 if (sizeof...(MatchArgs) != matches.size())
408 throw std::runtime_error{"got unexpected amount of matches"};
409 auto it = matches.begin();
410 co_return co_await fn(std::move(ctx), r, MatchArgs{static_cast<std::string const&>(*it++)}...);
411 };
412 }
413
414 template<class Ctx, match_arg... MatchArgs>
415 struct ctree : route_tree<Ctx> {
416 template<class OuterCtx>
417 auto wrap(middleware_t<OuterCtx, Ctx> mw) const -> ctree<OuterCtx, MatchArgs...> {
418 return {middleware_wrap_tree(std::move(mw), *this)};
419 }
420 };
421
422 template<class Ctx, match_arg... MatchArgs>
423 struct dtree : handler_map<route_handler_fn_t<Ctx, MatchArgs...>> {
424 [[nodiscard]] auto to_leaves() const -> typename route_tree<Ctx>::leaves {
425 auto here = this->template map<basic_route_handler_fn_t<Ctx>>(degen_route_handler<Ctx, MatchArgs...>);
426 if (!here.verbs().empty() && !static_cast<bool>(this->options))
427 here.options = default_options_handler<Ctx>;
428 return here;
429 }
430
431 [[nodiscard]] auto named_subtrees(std::initializer_list<std::pair<std::string, ctree<Ctx, MatchArgs...>>> subtrees) const -> ctree<Ctx, MatchArgs...> {
432 auto sub = typename route_tree<Ctx>::named_subtrees{
433 std::from_range,
434 subtrees | std::views::transform([](auto const& p) {
435 return std::make_pair(p.first, static_cast<route_tree<Ctx>>(p.second));
436 })
437 };
438 return {route_tree<Ctx>{.here = to_leaves(), .sub = sub}};
439 }
440
441 template<match_arg MatchArg>
442 [[nodiscard]] auto wildcard_subtree(ctree<Ctx, MatchArgs..., MatchArg> subtree) -> ctree<Ctx, MatchArgs...> {
443 return {route_tree<Ctx>{.here = to_leaves(), .sub = typename route_tree<Ctx>::wildcard_subtree{static_cast<route_tree<Ctx>>(subtree)}}};
444 }
445
446 [[nodiscard]] auto no_subtrees() const -> ctree<Ctx, MatchArgs...> {
447 return {route_tree<Ctx>{.here = to_leaves(), .sub = {}}};
448 }
449 };
450
451 template<std::derived_from<base_ctx> PreRouteCtx>
452 class server {
453 log::logger l_;
454 locale::selector lsel_;
455 middleware_t<base_ctx, PreRouteCtx> global_middleware_;
456 route_tree<routed_ctx<PreRouteCtx>> routes_;
457
458 public:
459 explicit server(log::logger const& l, locale::selector&& lsel, middleware_t<base_ctx, PreRouteCtx> global_middleware, route_tree<routed_ctx<PreRouteCtx>> routes)
460 : l_{l.sub("http_server")}, lsel_{std::move(lsel)}, global_middleware_{std::move(global_middleware)}, routes_{std::move(routes)}
461 {}
462
463 struct match_result {
464 util::not_null<handler_map<basic_route_handler_fn_t<routed_ctx<PreRouteCtx>>> const*> route_handlers;
465 std::vector<std::string> wildcard_matches;
466
467 auto allowed_methods() const -> verb_set {
468 return route_handlers->verbs();
469 }
470 };
471
472 auto match(boost::urls::segments_view segments) const -> std::optional<match_result> {
473 auto const* tree = &routes_;
474 auto wildcard_matches = std::vector<std::string>{};
475 for (auto const& seg : segments) {
476 tree->sub.visit(util::overloaded{
477 [&](route_tree<routed_ctx<PreRouteCtx>>::named_subtrees const& subtrees) {
478 auto it = subtrees.find(seg);
479 tree = it == subtrees.end() ? nullptr : &it->second;
480 },
481 [&](route_tree<routed_ctx<PreRouteCtx>>::wildcard_subtree const& wildcard_subtree) {
482 wildcard_matches.push_back(seg);
483 tree = &*wildcard_subtree;
484 },
485 });
486 if (!tree) return std::nullopt;
487 }
488 if (tree->here.empty()) return std::nullopt;
489 return match_result{
490 .route_handlers = util::not_null{&tree->here},
491 .wildcard_matches = wildcard_matches,
492 };
493 }
494
495 auto route_request(PreRouteCtx ctx, readable_request r) const -> net::awaitable<presponse> {
496 auto req_base = r.p->get().base();
497
498 auto const bad_request_tpl = problem::tpl{
499 .status = bhttp::status::bad_request,
500 .title = translate("Bad request"),
501 .type_uri = "https://routemon.fautchen.eu/problems/bad-request",
502 };
503
504 if (req_base.target() == "*") {
505 // request-target is in asterisk-form (RFC 9112, § 3.2.4),
506 // so the request must be a server-wide OPTIONS request.
507
508 if (req_base.method() != bhttp::verb::options) {
509 auto tpl = problem::tpl{
510 .status = bhttp::status::method_not_allowed,
511 .title = translate("Method not allowed"),
512 .type_uri = "https://routemon.fautchen.eu/problems/method-not-allowed",
513 };
514 co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false});
515 }
516
517 co_return co_await global_options_handler(ctx, r);
518 } else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) {
519 // request-target is in origin-form (RFC 9112, § 3.2.1),
520 // so it must be a normal request (not a CONNECT or
521 // server-wide OPTIONS request).
522
523 auto req_url = boost::urls::url{*mreq_url0};
524 req_url.normalize();
525 if (!req_url.is_path_absolute()) {
526 auto problem = bad_request_tpl.instantiate().
527 set_detail(translate("Path of normalized (RFC 3986, § 6) "
528 "origin-form request-target (RFC "
529 "9112, § 3.2.1) should be "
530 "absolute"));
531 co_return problem_rsp(ctx, problem, keep_alive{false});
532 }
533
534 auto mres = match(req_url.segments());
535 if (!mres) {
536 auto tpl = problem::tpl{
537 .status = bhttp::status::not_found,
538 .title = translate("Not found"),
539 .type_uri = "https://routemon.fautchen.eu/problems/not-found",
540 };
541 co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false});
542 }
543
544 auto mverb = supported_verb::from(req_base.method());
545 if (!mverb) {
546 // Method not implemented.
547 auto tpl = problem::tpl{
548 .status = bhttp::status::not_implemented,
549 .title = translate("Method not implemented"),
550 .type_uri = "https://routemon.fautchen.eu/problems/method-not-implemented",
551 };
552 co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false});
553 }
554
555 if (auto mhdl = mres->route_handlers->lookup(*mverb)) {
556 auto new_ctx = routed_ctx<PreRouteCtx>{std::move(ctx), mres->allowed_methods()};
557 co_return co_await mhdl(std::move(new_ctx), r, mres->wildcard_matches);
558 } else {
559 // Path recognized, but method not allowed.
560 auto tpl = problem::tpl{
561 .status = bhttp::status::method_not_allowed,
562 .title = translate("Method not allowed"),
563 .type_uri = "https://routemon.fautchen.eu/problems/method-not-allowed",
564 };
565 auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false});
566 rsp.header().set(bhttp::field::allow, mres->allowed_methods().to_string());
567 co_return std::move(rsp);
568 }
569 } else {
570 // We do not accept any other request-target forms.
571
572 auto problem = bad_request_tpl.instantiate().
573 set_detail(translate("Invalid request-target, expected "
574 "asterisk-form or origin-form "
575 "(see RFC 9112, § 3.2)"));
576 co_return problem_rsp(ctx, problem, keep_alive{false});
577 }
578 }
579
580 auto handle_request(readable_request r) const -> net::awaitable<presponse> {
581 auto header = r.p->get().base();
582 auto locale = lsel_.select(header[bhttp::field::accept_language]);
583 auto ctx0 = base_ctx{.locale = locale};
584
585 co_return co_await global_middleware_(std::move(ctx0), header, [&](PreRouteCtx ctx) -> net::awaitable<presponse> {
586 co_return co_await route_request(std::move(ctx), std::move(r));
587 });
588 }
589
590 auto do_session(beast::tcp_stream strm) -> net::awaitable<void> {
591 auto buf = beast::flat_buffer{};
592
593 while (true) {
594 auto p0 = bhttp::request_parser<bhttp::empty_body>{};
595 p0.body_limit(boost::none);
596 auto [ec, _] = co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple);
597 if (ec == bhttp::error::end_of_stream) {
598 break;
599 } else if (ec) {
600 throw boost::system::system_error{ec};
601 }
602
603 auto http_version = p0.get().version();
604 auto&& rsp = co_await handle_request(readable_request{
605 .p = util::not_null{&p0},
606 .strm = util::not_null{&strm},
607 .buf = util::not_null{&buf},
608 });
609 rsp.header().version(http_version);
610 bool keep_alive = rsp.keep_alive();
611 co_await beast::async_write(strm, std::move(rsp));
612 if (!keep_alive) {
613 break;
614 }
615 }
616
617 strm.socket().shutdown(tcp::socket::shutdown_send);
618 }
619
620 auto do_listen(tcp::endpoint endpoint) -> net::awaitable<void> {
621 auto executor = co_await net::this_coro::executor;
622 auto acceptor = tcp::acceptor{executor, endpoint};
623
624 l_.with("endpoint", endpoint.address().to_string()).
625 with("port", std::to_string(endpoint.port())).
626 info("Serving");
627 while (true) {
628 net::co_spawn(executor,
629 do_session(beast::tcp_stream{co_await acceptor.async_accept()}),
630 [this](std::exception_ptr e) {
631 if (e) {
632 try {
633 std::rethrow_exception(e);
634 } catch (std::exception const& e) {
635 l_.error("Error in session: {}", e.what());
636 }
637 }
638 });
639 }
640 }
641
642 auto spawn(net::io_context& ioc) -> void {
643 auto const addr = net::ip::make_address("0.0.0.0");
644 auto const endpoint = tcp::endpoint{addr, 8284};
645
646 // TODO: make exception handling as nice as in srv.cpp
647 net::co_spawn(ioc,
648 do_listen(endpoint),
649 [this](std::exception_ptr e) {
650 if (e) {
651 try {
652 std::rethrow_exception(e);
653 } catch (std::exception const& e) {
654 l_.error("Error: {}", e.what());
655 }
656 }
657 });
658 }
659 };
660
661} // namespace routemon::http
diff --git a/server/src/locale.cppm b/server/src/locale.cppm
new file mode 100644
index 0000000..65ae2ea
--- /dev/null
+++ b/server/src/locale.cppm
@@ -0,0 +1,245 @@
1module;
2
3#include <boost/locale.hpp>
4#include <unicode/localematcher.h>
5
6export module routemon:locale;
7
8import std;
9import :util;
10
11export namespace blocale = boost::locale;
12
13export namespace routemon {
14 using lformat = blocale::format;
15 using blocale::translate;
16 using blocale::gettext;
17} // namespace routemon
18
19namespace routemon::locale {
20
21 struct locale_priority {
22 float weight;
23 std::size_t original_index;
24 };
25
26 auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool {
27 if (lhs.weight != rhs.weight)
28 return lhs.weight > rhs.weight;
29 return lhs.original_index < rhs.original_index;
30 }
31
32 struct icu_locale_hash {
33 std::size_t operator()(icu::Locale const& l) const noexcept {
34 static_assert(sizeof(std::int32_t) < sizeof(std::size_t));
35 std::int32_t hash = l.hashCode();
36 if (hash < 0) {
37 return static_cast<std::size_t>(std::numeric_limits<int>::max()) + static_cast<std::size_t>(-hash) + 1;
38 } else {
39 return static_cast<std::size_t>(hash);
40 }
41 }
42 };
43
44 using icu_locale_priority_map = std::unordered_map<icu::Locale, locale_priority, icu_locale_hash>;
45 using icu_priority_locale = std::pair<icu::Locale, locale_priority>;
46
47 auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) -> bool {
48 return lhs.second < rhs.second;
49 }
50
51 class icu_priority_locale_vec_iterator : public icu::Locale::Iterator {
52 std::size_t i_ = 0uz;
53 std::vector<icu_priority_locale> ls_;
54
55 public:
56 explicit icu_priority_locale_vec_iterator(std::vector<icu_priority_locale>&& ls)
57 : ls_(std::move(ls))
58 {}
59
60 auto hasNext() const -> UBool override {
61 return i_ < ls_.size();
62 }
63
64 auto next() -> icu::Locale const& override {
65 return ls_[i_++].first;
66 }
67
68 ~icu_priority_locale_vec_iterator() override = default;
69 };
70
71 export template<class T>
72 concept locale_input_range =
73 std::ranges::input_range<T> &&
74 std::same_as<std::locale const&, std::ranges::range_const_reference_t<T>>;
75
76 // Helps select a locale based on the Accept-Language header in an
77 // HTTP request.
78 export class selector {
79 std::locale default_;
80 icu::LocaleMatcher matcher_;
81 std::shared_ptr<blocale::generator const> lgen_;
82
83 auto make_matcher(locale_input_range auto supported_locales, std::locale default_locale) {
84 auto builder = icu::LocaleMatcher::Builder{};
85 for (auto const& supported_locale : supported_locales) {
86 auto const& supported_locale_info = std::use_facet<blocale::info>(supported_locale);
87 auto supported_icu_locale = icu::Locale{supported_locale_info.name().c_str()};
88 if (supported_icu_locale.isBogus())
89 throw std::runtime_error{"supported locale gives rise to bogus ICU locale"};
90 builder.addSupportedLocale(supported_icu_locale);
91 }
92 auto const& default_locale_info = std::use_facet<blocale::info>(default_locale);
93 auto default_icu_locale = icu::Locale{default_locale_info.name().c_str()};
94 if (default_icu_locale.isBogus())
95 throw std::runtime_error{"default locale gives rise to bogus ICU locale"};
96 builder.setDefaultLocale(&default_icu_locale);
97 auto ec = UErrorCode::U_ZERO_ERROR;
98 auto matcher = builder.build(ec);
99 if (U_FAILURE(ec))
100 throw std::runtime_error{"failed to build icu::LocaleMatcher"};
101 return matcher;
102 }
103
104 // Trimming optional whitespace as defined in RFC 9110, § 12.4.2.
105 static auto ltrim_ows(std::string_view s) -> std::string_view {
106 if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos)
107 s.remove_prefix(i);
108 return s;
109 }
110 static auto rtrim_ows(std::string_view s) -> std::string_view {
111 if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos)
112 return s.substr(0, i + 1);
113 return s;
114 }
115 static auto trim_ows(std::string_view s) -> std::string_view {
116 return rtrim_ows(ltrim_ows(s));
117 }
118
119 auto from_icu_locale(icu::Locale const& l) const -> std::locale {
120 auto posix_name = std::string{l.getLanguage()};
121 if (l.getScript() && std::strlen(l.getScript()) > 0) {
122 posix_name += "_";
123 posix_name += l.getScript();
124 }
125 if (l.getCountry() && std::strlen(l.getCountry()) > 0) {
126 posix_name += "_";
127 posix_name += l.getCountry();
128 }
129 posix_name += ".UTF-8";
130 auto added_at = false;
131 if (l.getVariant() && std::strlen(l.getVariant()) > 0) {
132 added_at = true;
133 posix_name += "@";
134 posix_name += l.getVariant();
135 }
136 auto ec = UErrorCode::U_ZERO_ERROR;
137 auto* keywords = l.createKeywords(ec);
138 if (U_FAILURE(ec))
139 throw std::runtime_error{"failed to create keywords"};
140 if (keywords) {
141 std::int32_t kw_len = 0;
142 char const* kw = nullptr;
143 while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw) {
144 auto value = l.getKeywordValue<std::string>(icu::StringPiece(kw, kw_len), ec);
145 if (!added_at) {
146 posix_name += "@";
147 added_at = true;
148 } else {
149 posix_name += ";";
150 }
151 posix_name += kw;
152 posix_name += "=";
153 posix_name += value;
154 }
155 if (U_FAILURE(ec))
156 throw std::runtime_error{"failed to iterate over keywords"};
157 delete keywords;
158 }
159 return lgen_->generate(posix_name);
160 }
161
162 public:
163 // Note: lgen must live at least as long as the selector constructed here!
164 // It is unfortunately not possible to copy/move a blocale::generator.
165 explicit selector(locale_input_range auto locales, std::locale default_, std::shared_ptr<blocale::generator const> lgen)
166 : default_{default_}, matcher_{make_matcher(locales, default_)}, lgen_{lgen}
167 {}
168
169 auto select(std::string_view accept_language) const -> std::locale {
170 using namespace std::literals::string_view_literals;
171 // NOTE: can also contain a *;q=0.1
172 // q should have at most 3 digits after period
173 auto dlpm = icu_locale_priority_map{};
174 for (auto const [i, lang_prio] : accept_language | std::views::split(","sv) | std::views::enumerate) {
175 auto [lang_range_ut, mweight_ut] = util::split_on(std::string_view{lang_prio}, ';');
176 auto lang_range_str = trim_ows(lang_range_ut);
177 auto mweight_str = mweight_ut.transform(trim_ows);
178 if (lang_range_str == "*")
179 break;
180
181 auto ec = UErrorCode::U_ZERO_ERROR;
182 auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec);
183 if (U_FAILURE(ec) || icu_locale.isBogus())
184 continue; // ignore this locale
185
186 auto weight = 1.0f;
187 if (mweight_str && mweight_str->starts_with("q=")) {
188 auto weight_str = mweight_str->substr(2, 4);
189 if (auto mweight = util::parse_float(weight_str, std::chars_format::fixed);
190 mweight && 0.0f < *mweight && *mweight < 1.0f) {
191 weight = *mweight;
192 }
193 }
194
195 if (weight > 0.0f) {
196 dlpm[icu_locale] = {
197 .weight = weight,
198 .original_index = static_cast<std::size_t>(i),
199 };
200 } else {
201 dlpm.erase(icu_locale);
202 }
203 }
204
205 auto desired_locales = std::vector<icu_priority_locale>{dlpm.begin(), dlpm.end()};
206 std::sort(desired_locales.begin(), desired_locales.end());
207 auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)};
208 auto ec = UErrorCode::U_ZERO_ERROR;
209 auto res = matcher_.getBestMatchResult(it, ec);
210 if (U_FAILURE(ec))
211 return default_;
212 auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't?
213 if (U_FAILURE(ec))
214 return from_icu_locale(*res.getSupportedLocale());
215 return from_icu_locale(resolved);
216 }
217 };
218
219 export auto to_bcp47_lang_tag(std::locale locale) -> std::optional<std::string> {
220 auto const& locale_info = std::use_facet<blocale::info>(locale);
221 auto ec = UErrorCode::U_ZERO_ERROR;
222 auto bcp47_lang_tag = icu::Locale{locale_info.name().c_str()}.toLanguageTag<std::string>(ec);
223 if (U_FAILURE(ec))
224 return std::nullopt;
225 return bcp47_lang_tag;
226 }
227
228#ifdef LOCALEDIR
229# define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s)
230# define LOCALEDIR_AUX_STR(s) #s
231 constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)};
232# undef LOCALEDIR_AUX_STR
233# undef LOCALEDIR_AUX_XSTR
234#else // ifdef LOCALEDIR
235 constexpr auto messages_path = std::string_view{"locale/dev"};
236#endif // ifdef LOCALEDIR
237
238 export auto make_generator() -> std::shared_ptr<blocale::generator const> {
239 auto lgen = std::make_shared<blocale::generator>();
240 lgen->add_messages_path(std::string{messages_path});
241 lgen->add_messages_domain("routemon");
242 return std::static_pointer_cast<blocale::generator const>(lgen);
243 }
244
245} // namespace routemon::locale
diff --git a/server/src/log.cppm b/server/src/log.cppm
new file mode 100644
index 0000000..d7e2bff
--- /dev/null
+++ b/server/src/log.cppm
@@ -0,0 +1,148 @@
1module;
2
3// Seems like ADL for std::quoted is broken with
4// import std;
5// Might be because the _Quoted_string object is defined in
6// std::__detail, which is not exported by the module.
7#include <iomanip>
8
9export module routemon:log;
10
11import std;
12
13namespace routemon::log {
14
15 export enum class level : std::uint8_t {
16 debug,
17 info,
18 warn,
19 error,
20 };
21
22 namespace {
23
24 auto operator<<(std::ostream& os, level lvl) -> std::ostream& {
25 switch (lvl) {
26 case level::debug: os << "dbg"; break;
27 case level::info: os << "inf"; break;
28 case level::warn: os << "wrn"; break;
29 case level::error: os << "err"; break;
30 }
31 return os;
32 }
33
34 } // namespace (unique)
35
36 export class sink {
37 std::atomic<enum level> lvl_;
38 std::ostream& os_ = std::cout;
39
40 struct tmp_message {
41 level lvl;
42 std::string_view component;
43 std::string_view txt;
44 std::map<std::string, std::string> const& attrs;
45 };
46
47 auto write(tmp_message msg) -> void {
48 auto sos = std::osyncstream{os_};
49 sos << "[" << msg.lvl;
50 if (!msg.component.empty())
51 sos << " " << msg.component;
52 sos << "] " << msg.txt;
53 for (auto const& [k, v] : msg.attrs) {
54 sos << " " << k << "=" << std::quoted(v);
55 }
56 sos << '\n';
57 }
58
59 explicit sink(level lvl)
60 : lvl_{lvl}
61 {}
62
63 friend auto make_sink(level lvl) -> std::shared_ptr<sink>;
64 friend class logger;
65
66 public:
67 [[nodiscard]] auto level() const -> enum level {
68 return lvl_;
69 }
70
71 auto set_level(enum level lvl) -> void {
72 lvl_ = lvl;
73 }
74 };
75
76 export auto make_sink(level lvl) -> std::shared_ptr<sink> {
77 return std::shared_ptr<sink>{new sink{lvl}};
78 }
79
80 export class logger {
81 std::shared_ptr<sink> sink_;
82 std::string component_;
83 std::map<std::string, std::string> attrs_;
84
85 template<log::level lvl>
86 auto log_at(std::string_view fmt, std::format_args args) -> logger& {
87 if (sink_->level() <= lvl)
88 sink_->write(sink::tmp_message{
89 .lvl = lvl,
90 .component = component_,
91 .txt = std::vformat(fmt, args),
92 .attrs = attrs_,
93 });
94 return *this;
95 }
96
97 public:
98 explicit logger(std::shared_ptr<sink> const& sink)
99 : sink_{sink}
100 {
101 if (!sink) {
102 throw std::invalid_argument{"logger sink may not be null"};
103 }
104 }
105
106 [[nodiscard]] auto sub(std::string_view component) const -> logger {
107 auto l = *this;
108 if (l.component_.empty()) {
109 l.component_ = component;
110 } else {
111 l.component_ += ".";
112 l.component_ += component;
113 }
114 return l;
115 }
116
117 [[nodiscard]] auto with(std::string const& k, std::string&& v) const -> logger {
118 auto l = *this;
119 l.attrs_[k] = std::move(v);
120 return l;
121 }
122
123 [[nodiscard]] auto with(std::string const& k, std::string_view v) const -> logger {
124 return with(k, std::string{v});
125 }
126
127 template<class... Args>
128 auto debug(std::format_string<Args...> fmt, Args&&... args) -> logger& {
129 return log_at<level::debug>(fmt.get(), std::make_format_args(args...));
130 }
131
132 template<class... Args>
133 auto info(std::format_string<Args...> fmt, Args&&... args) -> logger& {
134 return log_at<level::info>(fmt.get(), std::make_format_args(args...));
135 }
136
137 template<class... Args>
138 auto warn(std::format_string<Args...> fmt, Args&&... args) -> logger& {
139 return log_at<level::warn>(fmt.get(), std::make_format_args(args...));
140 }
141
142 template<class... Args>
143 auto error(std::format_string<Args...> fmt, Args&&... args) -> logger& {
144 return log_at<level::error>(fmt.get(), std::make_format_args(args...));
145 }
146 };
147
148} // namespace routemon::log
diff --git a/server/src/main.cpp b/server/src/main.cpp
new file mode 100644
index 0000000..a40b0b0
--- /dev/null
+++ b/server/src/main.cpp
@@ -0,0 +1,109 @@
1#include <malloc.h> // for malloc_trim(3)
2#include <boost/asio.hpp>
3
4import std;
5import routemon;
6
7namespace chrono = std::chrono;
8namespace net = boost::asio;
9
10enum class exit_status {
11 failure,
12 bad_usage,
13};
14
15auto real_main(std::span<char const*> args) -> exit_status {
16 auto sink = routemon::log::make_sink(routemon::log::level::info);
17 auto l = routemon::log::logger{sink};
18
19 if (args.size() != 2) {
20 l.error("Fatal: expected exactly one argument (the configuration file location), got {}", args.size() - 1);
21 return exit_status::bad_usage;
22 }
23 auto const* config_filename = args[1];
24
25 auto lgen = routemon::locale::make_generator();
26 auto default_locale = lgen->generate("en_US.UTF-8");
27 auto locales = {
28 default_locale,
29 lgen->generate("nl_NL.UTF-8"),
30 lgen->generate("de_DE.UTF-8"),
31 lgen->generate("en_GB.UTF-8"),
32 };
33 auto lsel = routemon::locale::selector{locales, default_locale, lgen};
34
35 auto ioc = net::io_context{1 /* concurrency hint */};
36
37 auto config = routemon::config::app{};
38 try {
39 config = routemon::config::load_file(config_filename);
40 } catch (std::exception const& e) {
41 l.with("filename", std::string_view{config_filename}).
42 error("Failed to load configuration: {}", e.what());
43 return exit_status::failure;
44 }
45 sink->set_level(config.logger.level);
46 // auto rwgps_client = routemon::rwgps::client{ioc, l, config.rwgps.api_key, config.rwgps.auth_token};
47 // for (auto route : rwgps_client.get_all_routes()) {
48 // l.info("Route {} (user {}): {} @ {}", route.id, route.user_id, route.name, route.url);
49 // }
50
51 auto dbc = std::shared_ptr<routemon::database::connection>{};
52 try {
53 dbc = routemon::database::open(config.database.sqlite3_filename);
54 } catch (std::exception const& e) {
55 l.with("filename", config.database.sqlite3_filename).
56 error("Failed to open database: {}", e.what());
57 return exit_status::failure;
58 }
59
60 l.with("filename", config.situations.datex2_filename).
61 info("Loading situations");
62 auto const before_load = chrono::steady_clock::now();
63 auto d2loader = routemon::datex2::loader{};
64 auto pub = routemon::datex2::situation_publication{};
65 try {
66 pub = d2loader.load_situation_publication(config.situations.datex2_filename);
67 } catch (std::exception const& e) {
68 l.error("Failed to load DATEX II situations publication: {}", e.what());
69 return exit_status::failure;
70 }
71 if (!d2loader.warnings().empty()) {
72 auto const& warns = d2loader.warnings();
73 l.warn("Encountered {} unique warnings while loading DATEX II situations publication", warns.size());
74 auto i = 0uz;
75 for (auto it = warns.begin(); it != warns.end(); it = warns.upper_bound(*it)) {
76 l.warn("Warning {} (appeared {}×): {}", ++i, warns.count(*it), *it);
77 }
78 }
79 // Processing the feed is by far the most memory-intensive operation
80 // during the run time of this application (at the moment), the
81 // resident set will likely never be this big again. So we ask libc
82 // to return as much memory as possible to the OS.
83 malloc_trim(0);
84 auto const after_load = chrono::steady_clock::now();
85 auto const dur_load = chrono::duration_cast<chrono::milliseconds>(after_load - before_load);
86 l.info("Loading situations finished in {}", dur_load);
87
88 auto handler = routemon::api::handler{l, std::move(pub)};
89 auto http_server = routemon::srv::server{l, std::move(lsel), std::move(handler)};
90 http_server.spawn(ioc);
91 ioc.run();
92
93 l.error("I/O context stopped");
94 return exit_status::failure;
95}
96
97auto main(int argc, char* argv[]) -> int {
98 if (argc < 0) {
99 std::cout << "Fatal: argument count below zero" << std::endl;
100 return EXIT_FAILURE;
101 }
102 auto res = real_main(std::span{const_cast<char const**>(argv), static_cast<std::size_t>(argc)});
103 switch (res) {
104 case exit_status::failure:
105 return EXIT_FAILURE;
106 case exit_status::bad_usage:
107 return 2;
108 }
109}
diff --git a/server/src/problem.cppm b/server/src/problem.cppm
new file mode 100644
index 0000000..8764962
--- /dev/null
+++ b/server/src/problem.cppm
@@ -0,0 +1,60 @@
1module;
2
3#include <boost/beast/http/message.hpp>
4#include <boost/json.hpp>
5#include <boost/locale/message.hpp>
6
7export module routemon:problem;
8
9import std;
10import :locale;
11
12namespace http = boost::beast::http;
13namespace json = boost::json;
14
15namespace routemon::problem {
16
17 export struct details {
18 http::status status;
19 blocale::message title;
20 std::string_view type_uri;
21 std::optional<blocale::message> detail = std::nullopt;
22 std::optional<std::string> instance = std::nullopt;
23
24 auto set_detail(blocale::message detail) -> details& {
25 this->detail = detail;
26 return *this;
27 }
28
29 auto set_instance(std::string&& instance) -> details& {
30 this->instance = instance;
31 return *this;
32 }
33 auto set_instance(std::string_view instance) -> details& {
34 this->instance = std::string{instance};
35 return *this;
36 }
37 };
38
39 export auto tag_invoke(json::value_from_tag, json::value& jv, details const& details, std::locale locale) -> void {
40 auto obj = json::object{
41 {"type", details.type_uri},
42 {"title", details.title.str(locale)},
43 {"status", static_cast<unsigned>(details.status)},
44 };
45 if (details.detail) obj["detail"] = details.detail->str(locale);
46 if (details.instance) obj["instance"] = *details.instance;
47 jv = obj;
48 }
49
50 export struct tpl {
51 http::status status;
52 blocale::message title;
53 std::string_view type_uri;
54
55 auto instantiate() const -> details {
56 return details{status, title, type_uri};
57 }
58 };
59
60}
diff --git a/server/src/req_ctx.cppm b/server/src/req_ctx.cppm
new file mode 100644
index 0000000..797c51d
--- /dev/null
+++ b/server/src/req_ctx.cppm
@@ -0,0 +1,51 @@
1module;
2
3#include <boost/beast/http/message.hpp>
4#include <boost/beast/http/verb.hpp>
5
6export module routemon:req_ctx;
7
8import :http.common;
9import :trace;
10
11namespace routemon {
12
13 export class req_ctx {
14 trace::id tid_;
15 std::locale locale_;
16 http::verb_set route_verbs_;
17 bool keep_alive_;
18 bhttp::request_header<bhttp::fields> const& req_header_;
19
20 public:
21 explicit req_ctx(trace::id tid, std::locale locale, http::verb_set route_verbs, bool keep_alive, bhttp::request_header<bhttp::fields> const& req_header)
22 : tid_{tid}, locale_{locale}, route_verbs_{route_verbs}, keep_alive_{keep_alive}, req_header_{req_header}
23 {}
24
25 template<typename ReqBody>
26 explicit req_ctx(trace::id tid, std::locale locale, http::verb_set route_verbs, bhttp::request<ReqBody> const& req)
27 : req_ctx{tid, locale, route_verbs, req.keep_alive(), req.base()}
28 {}
29
30 auto trace_id() const -> trace::id {
31 return tid_;
32 }
33
34 auto locale() const -> std::locale {
35 return locale_;
36 }
37
38 auto route_verbs() const -> http::verb_set {
39 return route_verbs_;
40 }
41
42 auto keep_alive() const -> bool {
43 return keep_alive_;
44 }
45
46 auto req_header() const -> bhttp::request_header<bhttp::fields> const& {
47 return req_header_;
48 }
49 };
50
51} // namespace routemon
diff --git a/server/src/routemon.cppm b/server/src/routemon.cppm
new file mode 100644
index 0000000..62db02a
--- /dev/null
+++ b/server/src/routemon.cppm
@@ -0,0 +1,11 @@
1export module routemon;
2export import :api;
3export import :config;
4export import :database;
5export import :datex2;
6export import :gpx;
7export import :locale;
8export import :log;
9export import :srv;
10export import :rwgps;
11export import :util;
diff --git a/server/src/rwgps.cppm b/server/src/rwgps.cppm
new file mode 100644
index 0000000..7ad6605
--- /dev/null
+++ b/server/src/rwgps.cppm
@@ -0,0 +1,137 @@
1module;
2
3#include <boost/beast/core.hpp>
4#include <boost/beast/http.hpp>
5#include <boost/json.hpp>
6
7export module routemon:rwgps;
8
9import std;
10import :http.client;
11import :log;
12
13namespace beast = boost::beast;
14namespace bhttp = beast::http;
15namespace net = boost::asio;
16namespace json = boost::json;
17
18namespace routemon::rwgps {
19
20 export struct route_summary {
21 std::int64_t id;
22 std::int64_t user_id;
23 std::string url;
24 std::string name;
25 std::string description;
26 };
27
28 struct pagination {
29 std::size_t record_count;
30 std::size_t page_count;
31 std::size_t page_size;
32 std::optional<std::string> next_page_url;
33 };
34
35 struct get_routes_meta {
36 pagination pagination;
37 };
38
39 struct get_routes_response {
40 std::vector<route_summary> routes;
41 get_routes_meta meta;
42 };
43
44 auto tag_invoke(json::value_to_tag<route_summary> const&, json::value const& jv) -> route_summary {
45 return {
46 .id = json::value_to<std::int64_t>(jv.at("id")),
47 .user_id = json::value_to<std::int64_t>(jv.at("user_id")),
48 .url = json::value_to<std::string>(jv.at("url")),
49 .name = json::value_to<std::string>(jv.at("name")),
50 .description = json::value_to<std::string>(jv.at("description")),
51 };
52 }
53
54 auto tag_invoke(json::value_to_tag<pagination> const&, json::value const& jv) -> pagination {
55 return {
56 .record_count = json::value_to<std::size_t>(jv.at("record_count")),
57 .page_count = json::value_to<std::size_t>(jv.at("page_count")),
58 .page_size = json::value_to<std::size_t>(jv.at("page_size")),
59 .next_page_url = json::value_to<std::optional<std::string>>(jv.at("next_page_url")),
60 };
61 }
62
63 auto tag_invoke(json::value_to_tag<get_routes_meta> const&, json::value const& jv) -> get_routes_meta {
64 return {
65 .pagination = json::value_to<pagination>(jv.at("pagination")),
66 };
67 }
68
69 auto tag_invoke(json::value_to_tag<get_routes_response> const&, json::value const& jv) -> get_routes_response {
70 return {
71 .routes = json::value_to<std::vector<route_summary>>(jv.at("routes")),
72 .meta = json::value_to<get_routes_meta>(jv.at("meta")),
73 };
74 }
75
76 auto json_value_to_get_routes_response(json::value const& jv) -> get_routes_response {
77 return json::value_to<get_routes_response>(jv);
78 }
79
80 export class client {
81 log::logger l_;
82 http::client hc_;
83 std::string api_key_;
84 std::string auth_token_;
85
86 static constexpr std::string host = "ridewithgps.com";
87
88 // TODO: handle failure appropriately
89 auto get_routes_page(std::size_t page) -> get_routes_response {
90 auto req = bhttp::request<bhttp::string_body>{
91 bhttp::verb::get,
92 std::format("/api/v1/routes.json?page_size=200?page={}", page),
93 11, // HTTP 1.1
94 };
95 req.set(bhttp::field::host, host);
96 req.set("x-rwgps-api-key", api_key_);
97 req.set("x-rwgps-auth-token", auth_token_);
98
99 auto rsp = hc_.do_request(req);
100 auto p = json::stream_parser{};
101 for (auto const frag : rsp.body().cdata()) {
102 p.write(static_cast<char const*>(frag.data()), frag.size());
103 }
104 assert(p.done());
105 return json_value_to_get_routes_response(p.release());
106 }
107
108 public:
109 explicit client(net::io_context& ioc, log::logger const& l, std::string api_key, std::string auth_token)
110 : l_{l.sub("rwgps-client")}, hc_{ioc}, api_key_{std::move(api_key)}, auth_token_{std::move(auth_token)}
111 {}
112
113 auto get_all_routes() -> std::vector<route_summary> {
114 // TODO: make sure that there are no duplicates here.
115 // What does RWGPS sort on, by default?
116 // Consider using an associative container instead of a vector.
117 auto record_count = 0uz;
118 auto current_page = 0uz;
119 auto routes = std::vector<route_summary>{};
120
121 while (true) {
122 auto rsp = get_routes_page(current_page);
123 if (rsp.meta.pagination.next_page_url)
124 l_.debug("Next page URL: {}", *rsp.meta.pagination.next_page_url);
125 routes.append_range(rsp.routes);
126 if (rsp.meta.pagination.record_count > 0)
127 record_count = rsp.meta.pagination.record_count;
128 if (rsp.routes.empty() || routes.size() >= record_count) {
129 break;
130 }
131 }
132
133 return routes;
134 }
135 };
136
137} // namespace routemon::rwgps
diff --git a/server/src/sqlite3.cppm b/server/src/sqlite3.cppm
new file mode 100644
index 0000000..f226c6b
--- /dev/null
+++ b/server/src/sqlite3.cppm
@@ -0,0 +1,257 @@
1module;
2
3#include <sqlite3.h>
4
5export module routemon:sqlite3;
6
7import std;
8import :util;
9
10namespace routemon::sqlite3 {
11
12 class mutex_guard {
13 explicit mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} {
14 ::sqlite3_mutex_enter(mut_);
15 }
16
17 friend auto do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) -> decltype(f(std::declval<mutex_guard const&>()));
18
19 public:
20 mutex_guard(mutex_guard const&) = delete;
21 ~mutex_guard() {
22 ::sqlite3_mutex_leave(mut_);
23 }
24
25 private:
26 ::sqlite3_mutex* mut_;
27 };
28
29 auto do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) -> decltype(f(std::declval<mutex_guard const&>())) {
30 return f(mutex_guard{mut});
31 }
32
33 auto do_guarded(::sqlite3* dbc, std::invocable<mutex_guard const&> auto f) -> decltype(f(std::declval<mutex_guard const&>())) {
34 return do_guarded(::sqlite3_db_mutex(dbc), f);
35 }
36
37 class error : public std::exception {
38 int code_;
39 std::string message_;
40
41 public:
42 explicit error(mutex_guard const&, int code, ::sqlite3* dbc)
43 : code_{code}, message_{::sqlite3_errmsg(dbc)}
44 {}
45
46 explicit error(int code)
47 : code_{code}, message_{::sqlite3_errstr(code)}
48 {}
49
50 [[nodiscard]] auto what() const noexcept -> char const* override {
51 return message_.c_str();
52 }
53
54 [[nodiscard]] auto code() const noexcept -> int {
55 return code_;
56 }
57 };
58
59 template<class T, template<class U> concept C>
60 concept optional_of = requires {
61 typename T::value_type;
62 requires std::same_as<T, std::optional<typename T::value_type>>;
63 requires C<typename T::value_type>;
64 };
65
66 template<class T>
67 concept scannable_prim =
68 std::same_as<T, std::string> ||
69 std::same_as<T, double> ||
70 std::same_as<T, std::int64_t>;
71
72 template<class T>
73 concept scannable = scannable_prim<T> || optional_of<T, scannable_prim>;
74
75 class statement {
76 ::sqlite3_stmt* stmt_;
77
78 public:
79 explicit statement(::sqlite3_stmt* stmt) : stmt_{stmt} {}
80 statement(statement const&) = delete;
81 statement(statement&& s) noexcept {
82 stmt_ = s.stmt_;
83 s.stmt_ = nullptr;
84 }
85 ~statement() {
86 ::sqlite3_finalize(stmt_);
87 }
88 auto get() -> ::sqlite3_stmt* {
89 return stmt_;
90 }
91 };
92
93 class row_reader {
94 statement stmt_;
95
96 explicit row_reader(statement stmt) : stmt_{std::move(stmt)} {}
97
98 friend class connection;
99
100 void scan(int col, std::string& s) {
101 if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT)
102 throw std::invalid_argument{"invalid type for scan"};
103 unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col);
104 auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col));
105 if (!size.has_value())
106 throw std::logic_error{"unexpected negative amount of bytes in column"};
107 s = std::string{reinterpret_cast<char const*>(chs), *size};
108 }
109
110 void scan(int col, double& v) {
111 if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT)
112 throw std::invalid_argument{"invalid type for scan"};
113 v = ::sqlite3_column_double(stmt_.get(), col);
114 }
115
116 void scan(int col, std::int64_t& v) {
117 if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER)
118 throw std::invalid_argument{"invalid type for scan"};
119 v = ::sqlite3_column_int64(stmt_.get(), col);
120 }
121
122 void scan(int col, optional_of<scannable> auto& v) {
123 if (::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL) {
124 v.reset();
125 } else {
126 typename std::remove_cvref_t<decltype(v)>::value_type tmp;
127 scan(col, tmp);
128 v = std::move(tmp);
129 }
130 }
131
132 public:
133 auto next() -> bool {
134 ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get());
135 return do_guarded(dbc, [&](auto const& guard) -> bool {
136 auto const s = ::sqlite3_step(stmt_.get());
137 if (s == SQLITE_ROW)
138 return true;
139 if (s == SQLITE_DONE)
140 return false;
141 throw error{guard, s, dbc};
142 });
143 }
144
145 auto scan(scannable auto&... args) -> void {
146 auto const ncols = util::size_from_int(::sqlite3_data_count(stmt_.get()));
147 if (!ncols.has_value())
148 throw std::logic_error{"got unexpected negative amount of columns"};
149 if (sizeof...(args) > *ncols)
150 throw std::invalid_argument{"more scanning arguments provided than columns in result set"};
151 auto col = 0; (..., scan(col++, args));
152 }
153
154 auto scan_single(scannable auto&... args) -> void {
155 if (!next())
156 throw std::logic_error{"no row in result set"};
157 scan(args...);
158 if (next()) {
159 throw std::logic_error{"more than one row in result set"};
160 }
161 }
162 };
163
164 class binder {
165 statement& stmt_;
166
167 explicit binder(statement& stmt) : stmt_{stmt} {}
168
169 friend class connection;
170
171 public:
172 auto text(std::string const& param_name, std::string_view str) -> void {
173 int const i = ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str());
174 if (i == 0)
175 throw std::invalid_argument{std::format("bind: no parameter with name {} found", param_name)};
176 auto str_size = util::int_from_size(str.size());
177 if (!str_size.has_value())
178 throw std::invalid_argument{"bind: provided text is too long"};
179 if (auto s = ::sqlite3_bind_text(stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); s != SQLITE_OK) {
180 throw error{s};
181 }
182 }
183
184 static auto noop(binder&) -> void {}
185 };
186
187 export class connection {
188 ::sqlite3* dbc_;
189 ::sqlite3_mutex* mut_;
190
191 explicit connection(::sqlite3* dbc) : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} {}
192
193 friend auto open(std::string const& filename) -> connection;
194
195 public:
196 connection(connection const&) = delete;
197 connection(connection&& c) noexcept {
198 dbc_ = c.dbc_;
199 mut_ = c.mut_;
200 c.dbc_ = nullptr;
201 c.mut_ = nullptr;
202 }
203
204 [[nodiscard]] auto query(std::string const& sql, std::function<void(binder&)> const& bf = binder::noop) -> row_reader {
205 ::sqlite3_stmt* pstmt = nullptr;
206 char const* sql_tail = nullptr;
207 auto sql_size = util::int_from_size(sql.size());
208 if (!sql_size.has_value() || *sql_size >= std::numeric_limits<int>::max() - 1)
209 throw std::invalid_argument{"provided input text too large"};
210 do_guarded(mut_, [&](auto const& guard) -> void {
211 if (auto s = ::sqlite3_prepare_v2(dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); s != SQLITE_OK) {
212 if (pstmt != nullptr) {
213 // Use contract_assert when having a compiler with contracts available
214 ::sqlite3_finalize(pstmt);
215 throw std::logic_error{"expected stmt to be null after failed preparation"};
216 }
217 throw error{guard, s, dbc_};
218 }
219 });
220 if (!pstmt)
221 throw std::invalid_argument{"provided input text contains no SQL"};
222 auto stmt = statement{pstmt};
223 if (sql_tail && std::strlen(sql_tail) > 0)
224 throw std::invalid_argument{"provided input text contains more than one SQL statement"};
225 auto b = binder{stmt}; bf(b);
226 return row_reader{std::move(stmt)};
227 }
228
229 auto exec(std::string const& sql, std::function<void(binder&)> const& bf = binder::noop) -> void {
230 auto reader = query(sql, bf);
231 while (reader.next());
232 }
233
234 ~connection() {
235 std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr));
236 }
237 };
238
239 export auto open(std::string const& filename) -> connection {
240 ::sqlite3* dbc = nullptr;
241 auto s = ::sqlite3_open_v2(filename.c_str(), &dbc,
242 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
243 SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_EXRESCODE,
244 nullptr);
245 if (s != SQLITE_OK) {
246 if (dbc) {
247 do_guarded(dbc, [&](auto const& guard) -> void {
248 throw error{guard, s, dbc};
249 });
250 } else {
251 throw error{s};
252 }
253 }
254 return connection{dbc};
255 }
256
257} // namespace routemon::sqlite3
diff --git a/server/src/srv.cppm b/server/src/srv.cppm
new file mode 100644
index 0000000..2ca48c6
--- /dev/null
+++ b/server/src/srv.cppm
@@ -0,0 +1,221 @@
1module;
2
3#include <boost/config.hpp>
4#include <boost/asio/ip/tcp.hpp>
5#include <boost/asio/as_tuple.hpp>
6#include <boost/asio/awaitable.hpp>
7#include <boost/asio/co_spawn.hpp>
8#include <boost/beast/core.hpp>
9#include <boost/beast/http.hpp>
10#include <boost/json.hpp>
11#include <boost/locale/generator.hpp>
12
13#include <expat.h>
14
15export module routemon:srv;
16
17import std;
18import :api;
19import :config;
20import :gpx;
21import :http.server;
22import :locale;
23import :log;
24import :problem;
25import :req_ctx;
26import :util;
27
28namespace beast = boost::beast;
29namespace json = boost::json;
30namespace net = boost::asio;
31using tcp = boost::asio::ip::tcp;
32
33namespace routemon::srv {
34
35 class gpx_parse_error_category_impl : public std::error_category {
36 public:
37 char const* name() const noexcept override { return "gpx_parse"; }
38 auto message(int condition) const noexcept -> std::string override {
39 std::ignore = condition;
40 return "failed to parse GPX file";
41 }
42 };
43 auto gpx_parse_error_category() noexcept -> gpx_parse_error_category_impl const& {
44 static auto const inst = gpx_parse_error_category_impl{};
45 return inst;
46 }
47 auto gpx_parse_error() noexcept -> std::error_code {
48 return std::error_code{1, gpx_parse_error_category()};
49 }
50
51 class gpx_parse_result {
52 std::variant<std::exception_ptr, gpx::file> res_;
53
54 public:
55 auto set_exception(std::exception_ptr ex) noexcept {
56 res_ = ex;
57 }
58 auto set_gpx_file(gpx::file&& f) noexcept {
59 res_ = std::move(f);
60 }
61
62 auto unwrap() -> gpx::file&& {
63 return std::visit(util::overloaded{
64 [](std::exception_ptr ex) -> gpx::file&& {
65 if (ex) std::rethrow_exception(ex);
66 else throw std::runtime_error{"no GPX file parse result available"};
67 },
68 [](gpx::file&& f) -> gpx::file&& { return std::move(f); },
69 }, std::move(res_));
70 }
71 };
72
73 struct readable_gpx_body {
74 using value_type = gpx_parse_result;
75
76 class reader {
77 gpx::reader r_;
78 util::not_null<value_type*> res_;
79
80 public:
81 template<bool isRequest, bhttp::concepts::fields Fields>
82 explicit reader(bhttp::header<isRequest, Fields>&, value_type& v)
83 : res_{&v}
84 {}
85
86 // The following methods (which are called by Beast) are marked
87 // noexcept, since Beast does not ensure that exceptions thrown
88 // here are appropriately directed to the caller of
89 // (async_)read(_some), so throwing here might cause the program
90 // to crash.
91
92 auto init(boost::optional<std::uint64_t> /* n */, beast::error_code& ec) noexcept -> void {
93 try {
94 r_.init();
95 ec = {};
96 } catch (std::exception& ex) {
97 res_->set_exception(std::current_exception());
98 ec = gpx_parse_error();
99 }
100 }
101
102 auto put(beast::concepts::const_buffer_sequence auto b, beast::error_code& ec) noexcept -> std::size_t {
103 auto total = 0uz;
104 try {
105 for (auto it = net::buffer_sequence_begin(b); it != net::buffer_sequence_end(b); it++) {
106 r_.put(std::string_view{static_cast<char const*>(it->data()), it->size()});
107 total += it->size();
108 }
109 ec = {};
110 } catch (std::exception& ex) {
111 res_->set_exception(std::current_exception());
112 ec = gpx_parse_error();
113 }
114 return total;
115 }
116
117 auto finish(beast::error_code& ec) noexcept {
118 try {
119 res_->set_gpx_file(r_.finish());
120 ec = {};
121 } catch (std::exception& ex) {
122 res_->set_exception(std::current_exception());
123 ec = gpx_parse_error();
124 }
125 }
126 };
127 };
128 static_assert(bhttp::concepts::body<readable_gpx_body>);
129 static_assert(bhttp::concepts::body_reader<readable_gpx_body>);
130
131 class handler {
132 api::handler inner_;
133
134 public:
135 using outer_ctx = http::trace_id_ctx<http::base_ctx>;
136 using l0_ctx = http::routed_ctx<outer_ctx>;
137
138 private:
139 auto handle_process_gpx(l0_ctx ctx, http::readable_request r) -> net::awaitable<http::presponse> {
140 auto gpx_file = gpx::file{};
141 try {
142 auto req = co_await http::read_request<readable_gpx_body>(ctx, std::move(r));
143 gpx_file = std::move(req->body().unwrap());
144 } catch (std::exception& ex) {
145 // TODO: more detailed problem reporting
146 auto tpl = problem::tpl{
147 .status = bhttp::status::bad_request,
148 .title = translate("Failed to parse GPX file"),
149 .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed",
150 };
151 co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{false});
152 }
153
154 // TODO: catch handler exceptions and return 500 when raised?
155 // (keep-alive depends on whether whole request was read)
156 auto mres = inner_.process_gpx(std::move(gpx_file));
157 if (!mres) {
158 auto tpl = problem::tpl{
159 .status = bhttp::status::internal_server_error,
160 .title = translate("Internal server error"),
161 .type_uri = "https://routemon.fautchen.eu/problems/internal-server-error",
162 };
163 co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{true});
164 }
165
166 auto rsp = http::make_rsp<bhttp::string_body>(bhttp::status::ok, http::keep_alive{true});
167 rsp.set(bhttp::field::content_type, "application/json");
168 rsp.body() = json::serialize(json::value_from(*mres));
169 rsp.prepare_payload();
170 co_return rsp;
171 }
172
173 auto handle_sysinfo(l0_ctx ctx, http::readable_request r) -> net::awaitable<http::presponse> {
174 auto req = co_await http::read_request<bhttp::empty_body>(ctx, std::move(r));
175 auto info = inner_.sysinfo();
176
177 auto rsp = http::make_rsp<bhttp::string_body>(bhttp::status::ok, http::keep_alive{true});
178 rsp.set(bhttp::field::content_type, "application/json");
179 rsp.body() = json::serialize(json::value_from(info));
180 rsp.prepare_payload();
181 co_return rsp;
182 }
183
184 public:
185 handler(api::handler&& inner) : inner_{std::move(inner)} {}
186
187 auto make_routes() -> http::route_tree<http::routed_ctx<outer_ctx>> {
188 auto handler = [this]<class MemFn>(MemFn member) {
189 return std::bind_front(member, this);
190 };
191
192 return http::dtree<http::routed_ctx<outer_ctx>>{}.named_subtrees({
193 {"gpx", http::dtree<l0_ctx>{{
194 .post = handler(&handler::handle_process_gpx),
195 }}.no_subtrees()},
196 {"sysinfo", http::dtree<l0_ctx>{{
197 .get = handler(&handler::handle_sysinfo),
198 }}.no_subtrees()},
199 });
200 }
201 };
202
203 export class server {
204 handler handler_;
205 http::server<handler::outer_ctx> srv_;
206
207 static auto make_global_middleware() -> http::middleware_t<http::base_ctx, handler::outer_ctx> {
208 return http::middleware_compose<http::base_ctx, http::trace_id_ctx<http::base_ctx>, http::trace_id_ctx<http::base_ctx>>(http::trace_id_middleware<http::base_ctx>, http::lax_cors_middleware<http::trace_id_ctx<http::base_ctx>>);
209 }
210
211 public:
212 server(log::logger const& l, locale::selector&& lsel, api::handler&& inner)
213 : handler_{std::move(inner)}, srv_{l, std::move(lsel), make_global_middleware(), handler_.make_routes()}
214 {}
215
216 auto spawn(net::io_context& ioc) -> void {
217 srv_.spawn(ioc);
218 }
219 };
220
221} // namespace routemon::srv
diff --git a/server/src/time.cppm b/server/src/time.cppm
new file mode 100644
index 0000000..767883c
--- /dev/null
+++ b/server/src/time.cppm
@@ -0,0 +1,205 @@
1export module routemon:time;
2
3import std;
4
5export namespace routemon::time {
6
7 using timestamp = std::chrono::time_point<std::chrono::utc_clock>;
8
9 class period {
10 // Assuming [start, end). Unfortunately the DATEX II model is not
11 // clear about this.
12 timestamp start_;
13 timestamp end_;
14
15 public:
16 explicit period(timestamp start, timestamp end)
17 : start_{start}, end_{end}
18 {
19 if (start >= end) {
20 throw std::invalid_argument("period: start should be before end");
21 }
22 }
23
24 [[nodiscard]] auto intersect(period other) const -> std::optional<period> {
25 auto const new_start = start_ < other.start() ? other.start() : start_;
26 auto const new_end = other.end() < end_ ? other.end() : end_;
27 return new_start < new_end ? std::make_optional(period{new_start, new_end}) : std::nullopt;
28 }
29
30 [[nodiscard]] auto except(period other) const -> std::pair<std::optional<period>, std::optional<period>> {
31 auto const before_start = start_;
32 auto const before_end = other.start();
33 auto const after_start = end_;
34 auto const after_end = other.end();
35 std::optional<period> before, after;
36 if (before_start < before_end)
37 before = period{before_start, before_end};
38 if (after_start < after_end)
39 after = period{after_end, after_start};
40 return std::make_pair(before, after);
41 }
42
43 [[nodiscard]] auto start() const -> timestamp { return start_; }
44 [[nodiscard]] auto end() const -> timestamp { return end_; }
45 };
46
47 class period_seq {
48 std::vector<period> periods_;
49
50 // The way lt and ge are ordered makes a difference for how the sorting
51 // (insertion based on lower_bound) works. Do not carelessly reorder this.
52 enum lt_ge : std::uint8_t {
53 ge, // >=
54 lt, // <
55 };
56
57 // O(n log n)
58 template<std::input_iterator I, std::sentinel_for<I> S>
59 requires std::same_as<std::iter_value_t<I>, period>
60 static auto consolidate(I begin, S end) -> std::vector<period> {
61 auto periods = std::vector<period>{};
62 auto preds = std::vector<std::pair<timestamp, lt_ge>>{};
63
64 for (auto it = begin; it != end; it++) {
65 auto const& period = *it;
66
67 auto const a = std::make_pair(period.start(), ge);
68 auto const b = std::make_pair(period.end(), lt);
69 preds.insert(std::lower_bound(preds.begin(), preds.end(), a), a);
70 preds.insert(std::lower_bound(preds.begin(), preds.end(), b), b);
71 }
72
73 if (preds.empty())
74 return periods;
75
76 if (preds.size() < 2)
77 throw std::logic_error{"period_seq::consolidate: amount of predicates should be >= 2"};
78 if (preds.front().second != ge)
79 throw std::logic_error{"period_seq::consolidate: first element of preds should be a ge-element"};
80 if (preds.back().second != lt)
81 throw std::logic_error{"period_seq::consolidate: last element of preds should be an lt-element"};
82
83 auto period_start = preds[0].first;
84 for (std::size_t i = 1; i < preds.size(); i++) {
85 if (preds[i].second == lt && (i + 1 == preds.size() || preds[i + 1].second == ge)) {
86 auto const period_end = preds[i].first;
87 if (!periods.empty() && periods.back().start() == period_start)
88 periods.back() = period{periods.back().end(), period_end};
89 else
90 periods.emplace_back(period_start, period_end);
91 if (i + 1 != preds.size()) {
92 period_start = preds[i + 1].first;
93 i++;
94 }
95 }
96 }
97
98 return periods;
99 }
100
101 explicit period_seq(std::vector<period> periods)
102 : periods_{std::move(periods)}
103 {
104 for (auto i = 0uz; i < periods_.size(); i++) {
105 if (i + 1 < periods_.size()) {
106 if (periods_[i].end() >= periods_[i + 1].start()) {
107 throw std::logic_error{"period_seq: vector provided to private constructor not ordered properly"};
108 }
109 }
110 }
111 }
112
113 public:
114 template<std::input_iterator I, std::sentinel_for<I> S>
115 requires std::same_as<std::iter_value_t<I>, period>
116 explicit period_seq(I begin, S end)
117 : periods_{consolidate(begin, end)}
118 {}
119
120 explicit period_seq(period singleton)
121 : periods_{singleton}
122 {}
123
124 [[nodiscard]] auto intersect(period_seq const& other) const -> period_seq {
125 auto it1 = periods_.begin(); auto end1 = periods_.end();
126 auto it2 = other.periods_.begin(); auto end2 = other.periods_.end();
127
128 auto res = std::vector<period>{};
129 while (it1 != end1 && it2 != end2) {
130 auto overlap = it1->intersect(*it2);
131 if (overlap) {
132 res.push_back(*overlap);
133 if (it1->end() < it2->end()) {
134 it1++;
135 } else {
136 it2++;
137 }
138 } else {
139 if (it1->end() < it2->start()) {
140 it1++;
141 } else {
142 it2++;
143 }
144 }
145 }
146
147 return period_seq{res};
148 }
149
150 [[nodiscard]] auto except(period_seq const& other) const -> period_seq {
151 // This code was pretty tricky to write, I wouldn't be surprised if it has some bugs in it.
152
153 auto it1 = periods_.begin(); auto end1 = periods_.end();
154 auto it2 = other.periods_.begin(); auto end2 = other.periods_.end();
155
156 auto res = std::vector<period>{};
157 if (it1 == end1)
158 return period_seq{res};
159 if (it2 == end2)
160 return period_seq{periods_};
161 auto period1 = period{*it1++};
162
163 while (it1 != end1 && it2 != end2) {
164 if (period1.end() <= it2->start()) {
165 res.push_back(period1);
166 period1 = *it1++;
167 } else if (it2->end() <= period1.start()) {
168 it2++;
169 } else /* period1.begin() < it2->end() && it2->begin() < period1.end() */ {
170 auto const [mbefore, mafter] = period1.except(*it2);
171 if (mbefore)
172 res.push_back(*mbefore);
173 if (mafter) {
174 period1 = *mafter;
175 } else {
176 period1 = *it1++;
177 }
178 }
179 }
180
181 return period_seq{res};
182 }
183
184 [[nodiscard]] auto periods() const -> std::vector<period> const& {
185 return periods_;
186 }
187 };
188
189 auto operator<<(std::ostream& os, period const& p) -> std::ostream& {
190 return os << "[" << p.start() << ", " << p.end() << ")";
191 }
192
193 auto operator<<(std::ostream &os, period_seq const& ps) -> std::ostream& {
194 os << "{";
195 auto it = ps.periods().begin();
196 while (it != ps.periods().end()) {
197 os << " " << *it;
198 if (++it != ps.periods().end()) {
199 os << ",";
200 }
201 }
202 return os << " }";
203 }
204
205} // namespace routemon::time
diff --git a/server/src/trace.cppm b/server/src/trace.cppm
new file mode 100644
index 0000000..00ecd05
--- /dev/null
+++ b/server/src/trace.cppm
@@ -0,0 +1,79 @@
1module;
2
3// Might as well since we're using OpenSSL
4#include <openssl/err.h>
5#include <openssl/rand.h>
6
7export module routemon:trace;
8
9import std;
10import :util;
11
12namespace routemon::trace {
13
14 class uuid7 {
15 std::uint64_t high_ = 0;
16 std::uint64_t low_ = 0;
17
18 public:
19 uuid7() {
20 namespace chrono = std::chrono;
21 auto const unix_time_ms_signed = static_cast<std::int64_t>(chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count());
22 if (unix_time_ms_signed < 0)
23 throw std::runtime_error{"system time before UNIX epoch"};
24 auto const unix_time_ms = static_cast<std::uint64_t>(unix_time_ms_signed);
25 if (std::countl_zero(unix_time_ms) < 16)
26 throw std::runtime_error{"system time too great"};
27
28 auto rand = std::array<unsigned char, 10>{};
29 int s = RAND_bytes(rand.data(), static_cast<int>(rand.size()));
30 if (s != 1) {
31 unsigned long e = ERR_get_error();
32 throw std::runtime_error{std::format("failed to generate UUID(v7): {} ({}, code {})", ERR_reason_error_string(e), ERR_lib_error_string(e), e)};
33 }
34
35 auto version = std::uint64_t{0b0111};
36 auto variant = std::uint64_t{0b10};
37
38 high_ |= unix_time_ms << 16;
39 high_ |= version << 12;
40 high_ |= std::uint64_t{rand[0]} << 4;
41 high_ |= std::uint64_t{rand[1]};
42 low_ |= variant << 62;
43 low_ |= std::uint64_t{rand[2]} << 54;
44 low_ |= std::uint64_t{rand[3]} << 48;
45 low_ |= std::uint64_t{rand[4]} << 40;
46 low_ |= std::uint64_t{rand[5]} << 32;
47 low_ |= std::uint64_t{rand[6]} << 24;
48 low_ |= std::uint64_t{rand[7]} << 16;
49 low_ |= std::uint64_t{rand[8]} << 8;
50 low_ |= std::uint64_t{rand[9]};
51 }
52
53 auto format(std::array<char, 37>& target) -> void {
54 auto high_high = (high_ & 0xffff'ffff'0000'0000) >> 32;
55 auto high_low_high = (high_ & 0x0000'0000'ffff'0000) >> 16;
56 auto low_low_high = (high_ & 0x0000'0000'0000'ffff) >> 0;
57 auto high_low = (low_ & 0xffff'0000'0000'0000) >> 48;
58 auto low_low = (low_ & 0x0000'ffff'ffff'ffff) >> 0;
59
60 std::format_to(target.begin(), "{:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>12x}",
61 high_high, high_low_high, low_low_high, high_low, low_low);
62 target.back() = '\0';
63 }
64 };
65
66 export class id {
67 std::array<char, 37> chars_;
68
69 public:
70 id() {
71 uuid7{}.format(chars_);
72 }
73
74 auto as_string() const -> util::zstring_view {
75 return util::zstring_view{chars_.data(), chars_.size() - 1};
76 }
77 };
78
79} // namespace routemon::trace
diff --git a/server/src/util.cppm b/server/src/util.cppm
new file mode 100644
index 0000000..65f4d67
--- /dev/null
+++ b/server/src/util.cppm
@@ -0,0 +1,254 @@
1// Stuff that doesn't really have a place right now, but that is
2// broadly useful.
3export module routemon:util;
4
5import std;
6
7namespace routemon::util {
8
9 // For use with e.g. std::visit (on std::variant).
10 template<class... Ts>
11 struct overloaded : Ts... {
12 using Ts::operator()...;
13 };
14
15 export constexpr auto parse_double(std::string_view s, std::chars_format fmt = std::chars_format::general) noexcept -> std::optional<double> {
16 auto x = 0.0;
17 auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt);
18 if (ec == std::errc{}) {
19 return x;
20 } else {
21 return std::nullopt;
22 }
23 }
24
25 export constexpr auto parse_float(std::string_view s, std::chars_format fmt = std::chars_format::general) noexcept -> std::optional<float> {
26 auto x = 0.0;
27 auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt);
28 if (ec == std::errc{}) {
29 return x;
30 } else {
31 return std::nullopt;
32 }
33 }
34
35 export template<class T>
36 class aolist : public std::enable_shared_from_this<aolist<T>> {
37 T v_;
38 std::shared_ptr<aolist<T> const> next_;
39
40 explicit aolist(T v, std::shared_ptr<aolist<T> const> next)
41 : v_{v}, next_{next}
42 {}
43
44 public:
45 static auto nil() -> std::shared_ptr<aolist<T>> {
46 return nullptr;
47 }
48
49 static auto cons(T v, std::shared_ptr<aolist<T> const> l) -> std::shared_ptr<aolist<T>> {
50 return std::shared_ptr<aolist<T>>{new aolist<T>{v, l}};
51 }
52
53 auto next() const -> std::shared_ptr<aolist<T> const> {
54 return next_;
55 }
56
57 auto value() const noexcept -> T const& {
58 return v_;
59 }
60 };
61
62 export constexpr auto size_from_int(int x) -> std::optional<std::size_t> {
63 static_assert(sizeof(int) <= sizeof(std::size_t), "cannot cast int to smaller size_t type");
64 if (x < 0)
65 return std::nullopt;
66 return static_cast<std::size_t>(x);
67 }
68
69 export constexpr auto int_from_size(std::size_t x) -> std::optional<int> {
70 constexpr auto int_max = size_from_int(std::numeric_limits<int>::max());
71 static_assert(int_max.has_value());
72 if (x > *int_max)
73 return std::nullopt;
74 return static_cast<int>(x);
75 }
76
77 export class zstring_view {
78 char const* s_;
79 std::size_t length_;
80
81 public:
82 constexpr explicit zstring_view(char const* s, std::size_t length) :
83 s_{s}, length_{length}
84 {}
85
86 constexpr zstring_view(char const* s) :
87 zstring_view{s, std::char_traits<char>::length(s)}
88 {}
89
90 auto length() const -> std::size_t {
91 return length_;
92 }
93
94 auto c_str() const -> char const* {
95 return s_;
96 }
97
98 operator std::string_view() const {
99 return std::string_view{s_, length_};
100 }
101
102 operator char const*() const {
103 return s_;
104 }
105 };
106
107 // View for null-terminated strings for which we might not
108 // necessarily be interested in the length. String length is only
109 // calculated on demand, at most once on each thread (on more than
110 // one thread when racing). May be null.
111 //
112 // It is undefined behavior to assign to a lazy_zstring_view when
113 // it is in use by other threads.
114 export class lazy_zstring_view {
115 static constexpr auto unset_length = std::numeric_limits<std::size_t>::max();
116
117 char const* s_; // nullable
118 mutable std::atomic<std::size_t> length_;
119 static_assert(decltype(length_)::is_always_lock_free);
120
121 public:
122 constexpr explicit lazy_zstring_view(char const* s) :
123 s_{s}, length_{s ? unset_length : 0}
124 {}
125 ~lazy_zstring_view() = default;
126
127 lazy_zstring_view(lazy_zstring_view const& sv) noexcept
128 : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)}
129 {}
130 lazy_zstring_view(lazy_zstring_view&& sv) noexcept
131 : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)}
132 {}
133 auto operator=(lazy_zstring_view const& rhs) noexcept -> lazy_zstring_view& {
134 if (this != &rhs) {
135 s_ = rhs.s_;
136 length_.store(rhs.length_.load(std::memory_order_acquire), std::memory_order_release);
137 }
138 return *this;
139 }
140 auto operator=(lazy_zstring_view&& rhs) noexcept -> lazy_zstring_view& {
141 return *this = rhs; // use the copy assignment operator
142 }
143
144 auto length() const noexcept -> std::size_t {
145 if (auto v = length_.load(std::memory_order_acquire); v != unset_length)
146 return v;
147 auto l = std::char_traits<char>::length(s_);
148 length_.store(l, std::memory_order_release);
149 return l;
150 }
151
152 auto c_str() const noexcept -> char const* {
153 return s_;
154 }
155
156 operator std::string_view() const noexcept {
157 return std::string_view{s_, length()};
158 }
159
160 operator char const*() const noexcept {
161 return s_;
162 }
163
164 auto operator==(std::string_view sv) const noexcept -> bool {
165 if (auto v = length_.load(std::memory_order_acquire); v != unset_length)
166 if (sv.length() != v)
167 return false;
168 auto res = std::char_traits<char>::compare(s_, sv.data(), sv.length());
169 if (res != 0)
170 return false;
171 // Strings are equal for sv.length() characters.
172 if (s_[sv.length()] != '\0')
173 return false;
174 // Strings are actually equal, and we have just found out the
175 // length of this string, so we might as well set it.
176 length_.store(sv.length(), std::memory_order_release);
177 return true;
178 }
179 };
180
181 constexpr auto operator""_zsv(char const* s, std::size_t length) noexcept -> zstring_view {
182 return zstring_view{s, length};
183 }
184
185 auto operator==(zstring_view lhs, zstring_view rhs) -> bool {
186 return std::string_view{lhs} == std::string_view{rhs};
187 }
188
189 export constexpr auto split_on(std::string_view s, char c) -> std::pair<std::string_view, std::optional<std::string_view>> {
190 if (auto i = s.find(c); i != std::string_view::npos)
191 return std::make_pair(s.substr(0, i), s.substr(i + 1));
192 return std::make_pair(s, std::nullopt);
193 }
194
195 export template<class T>
196 class not_null;
197
198 export template<class T>
199 class not_null<T*> {
200 T* p_;
201
202 struct guaranteed_not_null_t {};
203 explicit not_null(T* p, guaranteed_not_null_t) noexcept : p_{p} {}
204
205 public:
206 explicit not_null(T* p)
207 : p_{p}
208 { if (!p_) throw std::runtime_error{"not_null constructed with null pointer"}; }
209 ~not_null() = default;
210
211 not_null(not_null const& other) = default;
212 not_null(not_null&& other) noexcept = default;
213 auto operator=(not_null const& rhs) noexcept -> not_null& = default;
214 auto operator=(not_null&& rhs) noexcept -> not_null& = default;
215
216 friend auto make_not_null(T* p) noexcept -> std::optional<not_null> {
217 if (p) return not_null(p, guaranteed_not_null_t{});
218 else return std::nullopt;
219 }
220
221 [[nodiscard]] auto get() const noexcept -> T* {
222 return p_;
223 }
224
225 auto operator*() const noexcept -> std::add_lvalue_reference_t<T> {
226 return *p_;
227 }
228
229 auto operator->() const noexcept -> T* {
230 return p_;
231 }
232 };
233 export template<class T> explicit not_null(T*) -> not_null<T*>;
234
235 export template<>
236 class not_null<lazy_zstring_view> {
237 lazy_zstring_view s_;
238
239 public:
240 explicit not_null(lazy_zstring_view s)
241 : s_{std::move(s)}
242 { if (!s_) throw std::runtime_error{"not_null constructed with null pointer"}; }
243
244 [[nodiscard]] auto get() const noexcept -> lazy_zstring_view {
245 return s_;
246 }
247
248 operator lazy_zstring_view() const noexcept { return s_; }
249 operator std::string_view() const noexcept { return s_; }
250 operator char const*() const noexcept { return s_; }
251 };
252 export explicit not_null(lazy_zstring_view s) -> not_null<lazy_zstring_view>;
253
254} // namespace routemon::util
diff --git a/server/src/xml.cpp b/server/src/xml.cpp
new file mode 100644
index 0000000..cad42d1
--- /dev/null
+++ b/server/src/xml.cpp
@@ -0,0 +1,61 @@
1module;
2
3#include <cassert>
4#include <expat.h>
5
6module routemon:xml$impl;
7
8import :xml;
9
10namespace routemon::xml {
11
12 auto qname_view::operator==(qname_view const& rhs) const -> bool {
13 return ns_uri == rhs.ns_uri && local == rhs.local;
14 }
15
16 executor::executor() :
17 p_{XML_ParserCreateNS("UTF-8", detail::qname_sep)}
18 {
19 XML_SetUserData(p_, this);
20 XML_SetElementHandler(p_, handle_start_element, handle_end_element);
21 XML_SetCharacterDataHandler(p_, handle_character_data);
22 XML_SetProcessingInstructionHandler(p_, handle_processing_instructions);
23 XML_SetExternalEntityRefHandler(p_, handle_external_entity_ref);
24 XML_SetNamespaceDeclHandler(p_, handle_start_namespace_decl, handle_end_namespace_decl);
25 XML_SetXmlDeclHandler(p_, handle_xml_decl);
26 }
27
28 executor::~executor() {
29 XML_ParserFree(p_);
30 }
31
32 auto executor::start() -> void {
33 continuation_.resume();
34 if (ex_) std::rethrow_exception(ex_);
35 }
36
37 auto executor::read(std::string_view xml, bool is_final) -> void {
38 if (ex_)
39 throw std::runtime_error{"refusing to restart parser that was thrown in"};
40 // TODO: narrow_cast
41 if (auto s = XML_Parse(p_, xml.data(), static_cast<int>(xml.size()), is_final); s != XML_STATUS_OK) {
42 auto errc = XML_GetErrorCode(p_);
43 if (errc == XML_ERROR_ABORTED) {
44 assert(ex_);
45 std::rethrow_exception(ex_);
46 } else {
47 throw std::runtime_error{std::format("failed to parse XML: {}", XML_ErrorString(errc))};
48 }
49 }
50 }
51
52 auto executor::end() -> void {
53 if (ex_)
54 throw std::runtime_error{"refusing to restart parser that was thrown in"};
55 ev_ = eof_event{};
56 advance_ = false;
57 while (continuation_) continuation_.resume();
58 if (ex_) std::rethrow_exception(ex_);
59 }
60
61} // namespace routemon::xml
diff --git a/server/src/xml.cppm b/server/src/xml.cppm
new file mode 100644
index 0000000..957f149
--- /dev/null
+++ b/server/src/xml.cppm
@@ -0,0 +1,632 @@
1module;
2
3#include <cassert>
4#include <expat.h>
5
6export module routemon:xml;
7
8import std;
9import :util;
10
11// XML parsing module.
12//
13// Makes heavy use of C++20 coroutines. Provides combinators to build
14// XML (data format) parsers with. To keep overhead low (and allow for
15// HALO/CoroElide), many non-polymorphic definitions are marked inline
16// (which is not the default in module units). This also has the added
17// benefit of allowing HALO across TU boundaries, which is practically
18// necessary to reduce unnecessary allocations when building parsers
19// using the provided combinators.
20
21// Ensure that Expat is speaking UTF-8
22static_assert(std::is_same_v<XML_Char, char>);
23
24namespace routemon::xml {
25
26 struct qname_view {
27 std::string_view ns_uri;
28 std::string_view local;
29
30 auto operator==(qname_view const& rhs) const -> bool;
31 };
32
33 namespace detail {
34
35 constexpr auto qname_sep = '\xFF';
36 auto split_name(char const* name) noexcept -> qname_view {
37 auto [l, r] = util::split_on(name, qname_sep);
38 if (r) return { .ns_uri = l, .local = *r };
39 else return { .ns_uri = std::string_view{}, .local = l };
40 }
41
42 } // namespace detail
43
44 class attribute_view_iterator {
45 std::string_view default_ns_uri_;
46 char const* const* attrs_;
47
48 auto advance() -> void {
49 attrs_ += 2;
50 }
51
52 public:
53 using difference_type = std::ptrdiff_t;
54 using value_type = std::pair<qname_view, char const*>;
55
56 struct sentinel {
57 friend constexpr auto operator==(attribute_view_iterator const& it, sentinel) noexcept -> bool {
58 return !*it.attrs_;
59 }
60 };
61
62 inline explicit attribute_view_iterator(std::string_view default_ns_uri, char const* const* attrs)
63 : default_ns_uri_{default_ns_uri}, attrs_{attrs}
64 {}
65
66 inline auto operator*() const -> std::pair<qname_view, char const*> {
67 if (!*attrs_)
68 throw std::runtime_error{"end of attribute list"};
69 auto qname = detail::split_name(attrs_[0]);
70 if (qname.ns_uri.empty())
71 qname.ns_uri = default_ns_uri_;
72 return std::make_pair(qname, attrs_[1]);
73 }
74
75 // Pre-increment
76 inline auto operator++() -> attribute_view_iterator& {
77 advance();
78 return *this;
79 }
80
81 // Post-increment
82 inline auto operator++(int) -> attribute_view_iterator {
83 auto pre = *this;
84 advance();
85 return pre;
86 }
87 };
88 static_assert(std::input_iterator<attribute_view_iterator>);
89
90 class attribute_view : std::ranges::view_base {
91 std::string_view default_ns_uri_;
92 char const* const* attrs_;
93
94 public:
95 inline explicit attribute_view(std::string_view default_ns_uri, char const** attrs)
96 : default_ns_uri_{default_ns_uri}, attrs_{const_cast<char const* const*>(attrs)}
97 {}
98
99 [[nodiscard]] inline auto begin() const -> attribute_view_iterator {
100 return attribute_view_iterator{default_ns_uri_, attrs_};
101 }
102
103 [[nodiscard]] inline auto end() const -> attribute_view_iterator::sentinel {
104 return {};
105 }
106
107 inline auto lookup(qname_view want) -> std::optional<util::not_null<util::lazy_zstring_view>> {
108 for (auto const& [name, v] : *this) {
109 if (name == want) {
110 return util::not_null{util::lazy_zstring_view{v}};
111 }
112 }
113 return std::nullopt;
114 }
115 };
116 static_assert(std::ranges::input_range<attribute_view>);
117
118 template<class T> class promise;
119
120 template<class T>
121 struct [[clang::coro_await_elidable, clang::coro_return_type]] parser {
122 using promise_type = promise<T>;
123 using result_type = promise_type::result_type;
124 using handle_type = std::coroutine_handle<promise_type>;
125
126 private:
127 handle_type h_;
128
129 public:
130 explicit parser(handle_type h)
131 : h_{h}
132 { assert(h); }
133
134 parser(const parser&) = delete;
135 parser(parser&& c) noexcept
136 : h_{std::exchange(c.h_, nullptr)}
137 {}
138 auto operator=(const parser&) -> parser& = delete;
139 auto operator=(parser&&) -> parser& = delete;
140
141 [[nodiscard]] auto promise() const -> promise_type& {
142 return h_.promise();
143 }
144
145 ~parser() {
146 if (h_) h_.destroy();
147 }
148 };
149
150 struct start_element_event {
151 qname_view name;
152 attribute_view attrs;
153 };
154 struct end_element_event {
155 qname_view name;
156 };
157 struct character_data_event {
158 std::string_view data;
159 };
160 struct processing_instructions_event {
161 util::lazy_zstring_view target;
162 util::lazy_zstring_view data;
163 };
164 struct xml_decl_event {
165 util::lazy_zstring_view version;
166 util::lazy_zstring_view encoding;
167 std::optional<bool> standalone;
168 };
169 struct eof_event {};
170 using event = std::variant<start_element_event,
171 end_element_event,
172 character_data_event,
173 processing_instructions_event,
174 xml_decl_event,
175 eof_event>;
176 template<class T>
177 concept event_type = requires(event ev) { std::get<T>(ev); };
178
179 class executor;
180 using executor_ref = util::not_null<executor*>;
181
182 struct current_event_t {
183 executor_ref executor;
184 };
185 auto current_event(executor_ref executor) -> current_event_t {
186 return current_event_t{executor};
187 }
188
189 class promise_base {
190 executor_ref executor_;
191 std::coroutine_handle<promise_base> continuation_ = nullptr;
192
193 public:
194 // Not having this constructor marked inline messes with coroutine
195 // HALO. (Hours 'wasted': many)
196 inline explicit promise_base(executor_ref executor)
197 : executor_{executor}
198 {}
199
200 [[nodiscard]] inline auto executor() const -> executor& {
201 return *executor_;
202 }
203
204 inline auto base_handle() -> std::coroutine_handle<promise_base> {
205 return std::coroutine_handle<promise_base>::from_promise(*this);
206 }
207
208 [[nodiscard]] inline auto continuation() const -> std::coroutine_handle<promise_base> {
209 return continuation_;
210 }
211 inline auto set_continuation(std::coroutine_handle<promise_base> c) -> void {
212 continuation_ = c;
213 }
214 };
215
216 struct position {
217 std::size_t line;
218 std::size_t col;
219 };
220
221 class executor {
222 XML_Parser p_;
223 std::exception_ptr ex_ = nullptr;
224 std::coroutine_handle<promise_base> continuation_ = nullptr;
225 std::vector<std::optional<std::string>> default_namespace_;
226 std::unordered_map<std::string_view, std::vector<std::string>> namespaces_;
227 std::optional<event> ev_;
228 bool advance_ = true;
229
230 inline auto try_handle_event(event ev) noexcept -> void {
231 assert(!ex_);
232
233 try {
234 ev_ = std::move(ev);
235 } catch (...) {
236 ex_ = std::current_exception();
237 return;
238 }
239 advance_ = false;
240 if (!continuation_) {
241 // Parser returned (all subparsers are done) and has set the continuation to nullptr.
242 if (auto s = XML_StopParser(p_, /* resumable */ false); s != XML_STATUS_OK) {
243 ex_ = std::make_exception_ptr(std::runtime_error{"unexpected error when stopping XML parser"});
244 return;
245 }
246 ex_ = std::make_exception_ptr(std::runtime_error{"parser did not consume entire XML document"});
247 return;
248 }
249 continuation_.resume();
250 if (ex_) {
251 // Not sure if it's useful to report this error.
252 std::ignore = XML_StopParser(p_, /* resumable */ false);
253 }
254 }
255
256 static auto handle_start_element(void* ctx, char const* name, char const** attrs) noexcept -> void {
257 auto qname = detail::split_name(name);
258 static_cast<executor*>(ctx)->try_handle_event(start_element_event{
259 .name = qname,
260 .attrs = attribute_view{qname.ns_uri, attrs},
261 });
262 }
263 static auto handle_end_element(void* ctx, char const* name) noexcept -> void {
264 static_cast<executor*>(ctx)->try_handle_event(end_element_event{
265 .name = detail::split_name(name),
266 });
267 }
268 static auto handle_character_data(void* ctx, char const* s, int len) noexcept -> void {
269 static_cast<executor*>(ctx)->try_handle_event(character_data_event{
270 .data = std::string_view{s, static_cast<std::size_t>(len)},
271 });
272 }
273 static auto handle_processing_instructions(void* ctx, char const* target, char const* data) noexcept -> void {
274 static_cast<executor*>(ctx)->try_handle_event(processing_instructions_event{
275 .target = util::lazy_zstring_view{target},
276 .data = util::lazy_zstring_view{data},
277 });
278 }
279 static auto handle_external_entity_ref(XML_Parser, char const* /* context */, char const* /* base */, char const* /* system_id */, char const* /* public_id */) noexcept -> int {
280 return XML_STATUS_ERROR;
281 }
282 static auto handle_start_namespace_decl(void* ctx, char const* prefix, char const* uri) noexcept -> void {
283 if (prefix) {
284 static_cast<executor*>(ctx)->namespaces_[std::string_view{prefix}].emplace_back(uri);
285 } else {
286 static_cast<executor*>(ctx)->default_namespace_.push_back(uri ? std::make_optional<std::string>(uri) : std::nullopt);
287 }
288 }
289 static auto handle_end_namespace_decl(void* ctx, char const* prefix) noexcept -> void {
290 if (prefix) {
291 static_cast<executor*>(ctx)->namespaces_[std::string_view{prefix}].pop_back();
292 } else {
293 static_cast<executor*>(ctx)->default_namespace_.pop_back();
294 }
295 }
296 static auto handle_xml_decl(void * ctx, char const* version, char const* encoding, int standalone) noexcept -> void {
297 static_cast<executor*>(ctx)->try_handle_event(xml_decl_event{
298 .version = util::lazy_zstring_view{version},
299 .encoding = util::lazy_zstring_view{encoding},
300 .standalone = standalone < 0 ? std::nullopt : std::make_optional(standalone > 0),
301 });
302 }
303
304 inline auto advance_flag() -> bool {
305 return advance_;
306 }
307 inline auto set_exception(std::exception_ptr ex) -> void {
308 ex_ = std::move(ex);
309 }
310 [[nodiscard]] inline auto take_exception() -> std::exception_ptr {
311 return std::exchange(ex_, nullptr);
312 }
313
314 template<class T> friend class promise;
315
316 public:
317 executor();
318
319 executor(executor const&) = delete;
320 executor(executor&&) = delete;
321 auto operator=(executor const&) -> executor& = delete;
322 auto operator=(executor&&) -> executor& = delete;
323
324 ~executor();
325
326 inline auto set_continuation(std::coroutine_handle<promise_base> c) -> void {
327 continuation_ = c;
328 }
329 inline auto set_advance_flag() -> void {
330 if (!ev_ || !std::holds_alternative<eof_event>(ev_.value())) {
331 advance_ = true;
332 }
333 }
334 inline auto event() const -> std::optional<event> const& {
335 return ev_;
336 }
337 inline auto resolve_namespace(std::string_view prefix) -> std::optional<std::string_view> {
338 if (auto it = namespaces_.find(prefix); it != namespaces_.end() && !it->second.empty())
339 return it->second.back();
340 return std::nullopt;
341 }
342 [[nodiscard]] inline auto position() -> position {
343 return {
344 .line = XML_GetCurrentLineNumber(p_),
345 .col = XML_GetCurrentColumnNumber(p_),
346 };
347 }
348
349 auto start() -> void;
350 auto read(std::string_view xml, bool is_final) -> void;
351 auto end() -> void;
352 };
353
354 template<class T>
355 class promise_returnable : public promise_base {
356 std::optional<T> returned_value_;
357
358 public:
359 using result_type = T;
360 using promise_base::promise_base;
361
362 template<class U>
363 auto return_value(U&& v) -> void {
364 returned_value_.emplace(std::forward<U>(v));
365 }
366 auto returned_value() -> T&& {
367 if (!returned_value_)
368 throw std::runtime_error{"XML coroutine did not return"};
369 return std::forward<T>(returned_value_.value());
370 }
371 };
372
373 template<>
374 class promise_returnable<void> : public promise_base {
375 public:
376 using result_type = void;
377 using promise_base::promise_base;
378
379 auto return_void() -> void {}
380 };
381
382 template<class T>
383 class promise : public promise_returnable<T> {
384 public:
385 // Called with all the coroutine's arguments.
386 // Ignoring all but the first argument, which should be the executor.
387 template<class... Args>
388 explicit promise(executor_ref executor, Args&&...)
389 : promise_returnable<T>{executor}
390 {}
391
392 auto handle() -> std::coroutine_handle<promise<T>> {
393 return {parser<T>::handle_type::from_promise(*this)};
394 }
395
396 auto get_return_object() -> parser<T> {
397 return parser<T>{handle()};
398 }
399
400 auto initial_suspend() {
401 return std::suspend_always{};
402 }
403 auto final_suspend() noexcept {
404 struct awaiter {
405 std::coroutine_handle<> h_;
406
407 [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { return false; }
408 auto await_suspend(std::coroutine_handle<>) -> std::coroutine_handle<> { return h_; }
409 constexpr auto await_resume() const noexcept -> void { return; }
410 };
411 if (this->continuation()) {
412 return awaiter{this->continuation()};
413 } else {
414 this->executor().set_continuation(nullptr);
415 return awaiter{std::noop_coroutine()};
416 }
417 }
418
419 auto unhandled_exception() -> void {
420 this->executor().set_exception(std::current_exception());
421 }
422
423 auto await_transform(current_event_t const& req) {
424 struct awaiter {
425 executor_ref executor_;
426
427 [[nodiscard]] constexpr auto await_ready() const noexcept -> bool {
428 return !executor_->advance_flag();
429 }
430 auto await_suspend(std::coroutine_handle<promise<T>> h) -> void {
431 executor_->set_continuation(h.promise().base_handle());
432 }
433 [[nodiscard]] auto await_resume() const -> event {
434 assert(executor_->event());
435 return executor_->event().value();
436 }
437 };
438 return awaiter{req.executor};
439 }
440
441 template<class U>
442 auto await_transform(parser<U> const& coro) {
443 struct [[clang::coro_await_elidable]] awaiter {
444 util::not_null<promise<U>*> next_;
445
446 [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { return false; }
447 auto await_suspend(std::coroutine_handle<promise<T>> h) -> std::coroutine_handle<> {
448 // Passed coroutine handle will be the same as parser<T>::handle_type::from_promise(*this)
449 next_->set_continuation(h.promise().base_handle());
450 return next_->handle();
451 }
452 auto await_resume() -> U {
453 // Promise is still valid since coroutine frame is still alive (and suspended):
454 // control was transferred back to this coroutine via symmetric transfer in
455 // final_suspend(). Assuming that the destructor for coro still needs to run.
456 if (auto ex = next_->executor().take_exception()) {
457 std::rethrow_exception(ex);
458 } else {
459 if constexpr (!std::is_void_v<U>) {
460 return std::move(next_->returned_value());
461 }
462 }
463 }
464 };
465 return awaiter{util::not_null{&coro.promise()}};
466 }
467 };
468
469 // Helpers for handling XML documents. Non-polymorphic functions
470 // should be marked inline to allow HALO across TU boundaries.
471
472 template<class T>
473 concept unconstrained = true;
474
475 template<class T, template<class U> concept C>
476 concept parser_of = requires {
477 typename T::result_type;
478 requires std::same_as<parser<typename T::result_type>, T>;
479 requires C<typename T::result_type>;
480 };
481
482 template<parser_of<unconstrained> T>
483 using parser_result_t = T::result_type;
484
485 template<class T, class... Args>
486 concept parser_invocable = std::invocable<T, Args...> && parser_of<std::invoke_result_t<T, Args...>, unconstrained>;
487
488 template<class Fn, class... Args>
489 requires parser_invocable<Fn, Args...>
490 using parser_invoke_result_t = parser_result_t<std::invoke_result_t<Fn, Args...>>;
491
492 template<event_type T> auto expect_event(executor_ref e) -> parser<T> {
493 auto ev = co_await current_event(e);
494 if (!std::holds_alternative<T>(ev)) {
495 auto pos = e->position();
496 throw std::runtime_error{std::format("at {}:{}: unexpected event type, have {}", pos.line, pos.col, ev.index())};
497 }
498 e->set_advance_flag();
499 co_return std::get<T>(ev);
500 }
501
502 inline auto expect_start_element(executor_ref e, qname_view want) -> parser<attribute_view> {
503 auto ev = co_await expect_event<start_element_event>(e);
504 if (ev.name != want) {
505 auto pos = e->position();
506 throw std::runtime_error{std::format("at {}:{}: unexpected element started", pos.line, pos.col)};
507 }
508 co_return ev.attrs;
509 }
510
511 inline auto allow_start_element(executor_ref e, qname_view want) -> parser<std::optional<attribute_view>> {
512 auto ev = co_await current_event(e);
513 if (auto const* pev = std::get_if<start_element_event>(&ev)) {
514 if (pev->name == want) {
515 e->set_advance_flag();
516 co_return pev->attrs;
517 }
518 }
519 co_return std::nullopt;
520 }
521
522 inline auto expect_end_element(executor_ref e, qname_view want) -> parser<void> {
523 auto ev = co_await expect_event<end_element_event>(e);
524 if (ev.name != want) {
525 throw std::runtime_error{"unexpected element ended"};
526 }
527 }
528
529 inline auto ignore_whitespace(executor_ref e) -> parser<void> {
530 auto all_whitespace = [](std::string_view s) -> bool {
531 for (auto c : s)
532 if (c != ' ' && c != '\r' && c != '\n' && c != '\t')
533 return false;
534 return true;
535 };
536
537 while (true) {
538 auto ev = co_await current_event(e);
539 if (auto const* pev = std::get_if<character_data_event>(&ev); pev && all_whitespace(pev->data)) {
540 e->set_advance_flag();
541 } else {
542 co_return;
543 }
544 }
545 }
546
547 auto expect_element(executor_ref e, qname_view want, parser_invocable<executor_ref, attribute_view> auto p)
548 -> parser<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>
549 {
550 co_await ignore_whitespace(e);
551 auto attrs = co_await expect_start_element(e, want);
552 auto&& res = co_await p(e, attrs);
553 co_await expect_end_element(e, want);
554 co_await ignore_whitespace(e);
555 co_return std::forward<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>(res);
556 }
557
558 auto allow_element(executor_ref e, qname_view want, parser_invocable<executor_ref, attribute_view> auto p)
559 -> parser<std::optional<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>>
560 {
561 co_await ignore_whitespace(e);
562 if (auto mattrs = co_await allow_start_element(e, want)) {
563 auto&& res = co_await p(e, *mattrs);
564 co_await expect_end_element(e, want);
565 co_await ignore_whitespace(e);
566 co_return std::make_optional(std::forward<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>(res));
567 }
568 co_return std::nullopt;
569 }
570
571 auto allow_element(executor_ref e, qname_view want, parser_invocable<executor_ref, attribute_view> auto p)
572 -> parser<bool>
573 requires std::is_void_v<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>
574 {
575 co_await ignore_whitespace(e);
576 if (auto mattrs = co_await allow_start_element(e, want)) {
577 co_await p(e, *mattrs);
578 co_await expect_end_element(e, want);
579 co_await ignore_whitespace(e);
580 co_return true;
581 }
582 co_return false;
583 }
584
585 inline auto ignore_contents(executor_ref e, std::optional<qname_view> muntil = std::nullopt) -> parser<void> {
586 std::size_t depth = 0;
587 while (true) {
588 auto ev = co_await current_event(e);
589 if (auto* pev = std::get_if<start_element_event>(&ev)) {
590 if (depth == 0 && muntil && pev->name == *muntil) {
591 co_return;
592 } else {
593 depth++;
594 }
595 } else if (std::holds_alternative<end_element_event>(ev)) {
596 if (depth == 0) {
597 co_return;
598 } else {
599 depth--;
600 }
601 }
602 e->set_advance_flag();
603 }
604 }
605 inline auto ignore_element_contents(executor_ref e, attribute_view) -> parser<void> {
606 co_await ignore_contents(e);
607 }
608
609 inline auto read_string(executor_ref e) -> parser<std::string> {
610 std::string s;
611 while (true) {
612 auto ev = co_await current_event(e);
613 if (auto* pev = std::get_if<character_data_event>(&ev)) {
614 e->set_advance_flag();
615 s += pev->data;
616 } else {
617 co_return s;
618 }
619 }
620 }
621 inline auto read_string_contents(executor_ref e, attribute_view) -> parser<std::string> {
622 co_return co_await read_string(e);
623 }
624
625 template<auto f>
626 auto hohalo() {
627 return []<class... Args>(Args&&... args) -> std::invoke_result_t<decltype(f), Args...> {
628 co_return co_await f(std::forward<Args>(args)...);
629 };
630 }
631
632} // namespace routemon::xml