Stroika Library 3.0d22
 
Loading...
Searching...
No Matches
Router.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
8#include "Stroika/Foundation/Execution/Exceptions.h"
9#include "Stroika/Foundation/IO/Network/HTTP/ClientErrorException.h"
10#include "Stroika/Foundation/IO/Network/HTTP/Headers.h"
11#include "Stroika/Foundation/IO/Network/HTTP/Methods.h"
14
15#include "Router.h"
16
17using namespace Stroika::Foundation;
20using namespace Stroika::Foundation::Execution;
21using namespace Stroika::Foundation::Memory;
23
24using namespace Stroika::Frameworks;
25using namespace Stroika::Frameworks::WebServer;
26
28using Memory::MakeSharedPtr;
29
30// Comment this in to turn on aggressive noisy DbgTrace in this module
31// #define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
32
33namespace {
34 String ExtractHostRelPath_ (const URI& url)
35 {
36 try {
37 // Note - since Stroika v3.0d9 - normalize /// etc in requests to single standard form before matching
39 if (url != normal) {
40 DbgTrace ("ExtractHostRelPath_('{}') normalized URL path to '{}'"_f, url, normal);
41 }
42 return normal.GetAbsPath<String> ().SubString (1); // According to https://tools.ietf.org/html/rfc2616#section-5.1.2 - the URI must be abs_path
43 }
44 catch (...) {
45 static const auto kException_ = ClientErrorException{HTTP::StatusCodes::kBadRequest, "request URI requires an absolute path"sv};
46 Throw (kException_);
47 }
48 }
49}
50
51/*
52 ********************************************************************************
53 ******************************* Route::Matches *********************************
54 ********************************************************************************
55 */
56bool Route::Matches (const Request& request, Sequence<String>* pathRegExpMatches) const
57{
58 return Matches (request.httpMethod, ExtractHostRelPath_ (request.url ()), request, pathRegExpMatches);
59}
60bool Route::Matches (const String& method, const String& hostRelPath, const Request& request, Sequence<String>* pathRegExpMatches) const
61{
62 if (fVerbAndPathMatch_) {
63 if (not method.Matches (fVerbAndPathMatch_->first)) {
64 return false;
65 }
66 return (pathRegExpMatches == nullptr) ? hostRelPath.Matches (fVerbAndPathMatch_->second)
67 : hostRelPath.Matches (fVerbAndPathMatch_->second, pathRegExpMatches);
68 }
69 else if (fRequestMatch_) {
70 return (*fRequestMatch_) (method, hostRelPath, request);
71 }
72 else {
74 return false;
75 }
76}
77
78/*
79 ********************************************************************************
80 ************************* WebServer::Router::Rep_ ******************************
81 ********************************************************************************
82 */
83struct Router::Rep_ final : Interceptor::_IRep {
84 static const optional<Set<String>> MapStartToNullOpt_ (const optional<Set<String>>& o)
85 {
86 // internally we treat missing as wildcard but caller may not, so map
87 optional<Set<String>> m = o;
88 if (m and m->Contains (CORSOptions::kAccessControlWildcard)) {
89 m = nullopt;
90 }
91 if (m) {
93 }
94 return m;
95 }
96 // requires all optional values filled in on corsOptions
97 Rep_ (const Sequence<Route>& routes, const CORSOptions& filledInCORSOptions)
98 : fAllowedOrigins_{MapStartToNullOpt_ (filledInCORSOptions.fAllowedOrigins)}
99 , fAllowedHeaders_{MapStartToNullOpt_ (filledInCORSOptions.fAllowedHeaders)}
100 , fAccessControlAllowCredentialsValue_{*filledInCORSOptions.fAllowCredentials ? "true"sv : "false"sv}
101 , fAccessControlMaxAgeValue_{"{}"_f(*filledInCORSOptions.fAccessControlMaxAge)}
102 , fRoutes_{routes}
103 {
104 }
105 virtual void HandleMessage (Message& m) const override
106 {
107#if USE_NOISY_TRACE_IN_THIS_MODULE_
108 Debug::TraceContextBumper ctx{"Router::Rep_::HandleMessage", "...method='{}',url='{}'"_f, m.request ().httpMethod (), m.request ().url ()};
109#endif
110 for (Iterator<tuple<RequestHandler, Sequence<String>>> handlerI = Lookup_ (m.request ()); handlerI; ++handlerI) {
111 if (Handle_Via_RequestHandler_ (m, get<Sequence<String>> (*handlerI), get<RequestHandler> (*handlerI))) {
112 return;
113 }
114 }
115 if (m.request ().httpMethod () == HTTP::Methods::kHead and Handle_HEAD_ (m)) {
116 // handled
117 }
118 else if (m.request ().httpMethod () == HTTP::Methods::kOptions) {
119 Handle_OPTIONS_ (m);
120 }
121 else {
122 if (optional<Set<String>> o = GetAllowedMethodsForRequest_ (m.request ()); o && not o->Contains (m.request ().httpMethod ())) {
123 // From 10.4.6 405 Method Not Allowed
124 // The method specified in the Request-Line is not allowed for the resource identified by the Request-URI.
125 // The response MUST include an Allow header containing a list of valid methods for the requested resource.
126 Assert (not o->empty ());
127 m.rwResponse ().rwHeaders ().allow = o;
128 static const auto kException_ = ClientErrorException{HTTP::StatusCodes::kMethodNotAllowed};
129 Throw (kException_);
130 }
131 DbgTrace ("Router 404: (...url={})"_f, m.request ().url ());
132 static const auto kException_ = ClientErrorException{HTTP::StatusCodes::kNotFound};
133 Throw (kException_);
134 }
135 }
136 nonvirtual void HandleCORSInNormallyHandledMessage_ (const Request& request, Response& response) const
137 {
138 /*
139 * Origin and Access-Control-Allow-Origin
140 * documented here: http://www.w3.org/TR/cors/#http-responses
141 *
142 * IF CORS is being used, the request will contain an Origin header, and will expect a
143 * corresponding Access-Control-Allow-Origin response header. If the response header is missing
144 * that implies a CORS failure (but if we have no header in the request - presumably no matter)
145 */
146 optional<String> allowedOrigin;
147 if (auto origin = request.headers ().origin ()) {
148 if (fAllowedOrigins_.has_value ()) {
149 // see https://fetch.spec.whatwg.org/#http-origin for hints about how to compare - not sure
150 // may need to be more flexible about how we compare, but for now a good approximation... &&& @todo docs above link say how to compar
151 String originStr = origin->ToString ();
152 if (fAllowedOrigins_->Contains (originStr)) {
153 allowedOrigin = originStr;
154 }
155 }
156 else {
157 allowedOrigin = CORSOptions::kAccessControlWildcard;
158 }
159 }
160 if (allowedOrigin) {
161 response.rwHeaders ().accessControlAllowOrigin = allowedOrigin;
162 if (fAllowedOrigins_) {
163 // see https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches to see why we need to add Vary response
164 // if response depends on origin (so not '*')
165 response.rwHeaders ().vary = Memory::NullCoalesce (response.headers ().vary ()) + String{HTTP::HeaderName::kOrigin};
166 }
167 }
168 }
169 nonvirtual Iterator<tuple<RequestHandler, Sequence<String>>> Lookup_ (const String& method, const String& hostRelPath, const Request& request) const
170 {
171 return Traversal::CreateGeneratorIterator<tuple<RequestHandler, Sequence<String>>> (
172 [=, &request, cur = this->fRoutes_.begin ()] () mutable -> optional<tuple<RequestHandler, Sequence<String>>> {
173 while (cur) {
174 Sequence<String> matches;
175 if (cur->Matches (method, hostRelPath, request, &matches)) {
176 auto result = make_tuple (cur->fHandler_, matches);
177 ++cur;
178 return result;
179 }
180 else {
181 ++cur;
182 }
183 }
184 return nullopt;
185 });
186 }
187 nonvirtual Iterator<tuple<RequestHandler, Sequence<String>>> Lookup_ (const Request& request) const
188 {
189 return Lookup_ (request.httpMethod, ExtractHostRelPath_ (request.url), request);
190 }
191 nonvirtual optional<Set<String>> GetAllowedMethodsForRequest_ (const Request& request) const
192 {
193 String hostRelPath = ExtractHostRelPath_ (request.url);
194 static const Set<String> kMethods2Try_{HTTP::Methods::kGet, HTTP::Methods::kPut, HTTP::Methods::kOptions,
195 HTTP::Methods::kDelete, HTTP::Methods::kPost, HTTP::Methods::kPatch};
196 Set<String> methods;
197 for (const String& method : kMethods2Try_) {
198 for (const Route& r : fRoutes_) {
199 if (r.Matches (method, hostRelPath, request)) {
200 methods.Add (method);
201 }
202 }
203 }
204 return methods.empty () ? nullopt : optional<Set<String>>{methods};
205 }
206 nonvirtual bool Handle_Via_RequestHandler_ (Message& message, const Sequence<String>& matches, const RequestHandler& handler) const
207 {
208 const Request& request = message.request ();
209 Response& response = message.rwResponse ();
210 HandleCORSInNormallyHandledMessage_ (request, response);
211 bool handled = false;
212 (handler) (message, matches, handled);
213 return handled;
214 }
215 nonvirtual bool Handle_HEAD_ (Message& message) const
216 {
217 const Request& request = message.request ();
218 Response& response = message.rwResponse ();
219 Sequence<String> matches;
220 for (Iterator<tuple<RequestHandler, Sequence<String>>> handlerEtc = Lookup_ (HTTP::Methods::kGet, ExtractHostRelPath_ (request.url ()), request);
221 handlerEtc; ++handlerEtc) {
222 // do something to response so 'in HEAD mode' and won't write
223 response.headMode = true;
224 HandleCORSInNormallyHandledMessage_ (request, response);
225 bool handled = false;
226 get<RequestHandler> (*handlerEtc) (message, get<Sequence<String>> (*handlerEtc), handled);
227 if (handled) [[likely]] {
228 return true;
229 }
230 }
231 return false;
232 }
233 nonvirtual void Handle_OPTIONS_ (Message& message) const
234 {
235 const Request& request = message.request ();
236 Response& response = message.rwResponse ();
237 // @todo note - This ignores - Access-Control-Request-Method - not sure how we are expected to use it?
238 auto o = GetAllowedMethodsForRequest_ (request);
239 if (o) {
240 {
241 auto& responseHeaders = response.rwHeaders ();
242 responseHeaders.Set (HTTP::HeaderName::kAccessControlAllowCredentials, fAccessControlAllowCredentialsValue_);
243 if (auto accessControlRequestHeaders = request.headers ().LookupOne (HTTP::HeaderName::kAccessControlRequestHeaders)) {
244 if (fAllowedHeaders_) {
245 // intersect requested headers with those configured to permit
246 Iterable<String> requestAccessHeaders = accessControlRequestHeaders->Tokenize ({','});
247 auto r = fAllowedHeaders_->Intersection (requestAccessHeaders);
248 if (r.empty ()) {
249 responseHeaders.Set (HTTP::HeaderName::kAccessControlAllowHeaders, String::Join (r));
250 }
251 }
252 else {
253 responseHeaders.Set (HTTP::HeaderName::kAccessControlAllowHeaders, *accessControlRequestHeaders);
254 }
255 }
256 responseHeaders.Set (HTTP::HeaderName::kAccessControlAllowMethods, String::Join (*o));
257 responseHeaders.Set (HTTP::HeaderName::kAccessControlMaxAge, fAccessControlMaxAgeValue_);
258 }
259 HandleCORSInNormallyHandledMessage_ (request, response); // include access-origin-header
260 response.status = HTTP::StatusCodes::kNoContent;
261 }
262 else {
263 DbgTrace ("Router 404: (...url={})"_f, request.url ());
264 Throw (ClientErrorException{HTTP::StatusCodes::kNotFound});
265 }
266 }
267 virtual String ToString () const override
268 {
269 return "Router ({} routes)"_f(fRoutes_.size ());
270 }
271
272 const optional<Set<String>> fAllowedOrigins_; // missing <==> '*'
273 const optional<Set<String>> fAllowedHeaders_; // missing <==> '*'
274 const String fAccessControlAllowCredentialsValue_;
275 const String fAccessControlMaxAgeValue_;
276 const Sequence<Route> fRoutes_; // no need for synchronization cuz constant - just set on construction
277};
278
279/*
280 ********************************************************************************
281 *************************** WebServer::Router **********************************
282 ********************************************************************************
283 */
284namespace {
285 CORSOptions FillIn_ (CORSOptions corsOptions)
286 {
287 corsOptions.fAllowCredentials = Memory::NullCoalesce (corsOptions.fAllowCredentials, kDefault_CORSOptions.fAllowCredentials);
288 corsOptions.fAccessControlMaxAge = Memory::NullCoalesce (corsOptions.fAccessControlMaxAge, kDefault_CORSOptions.fAccessControlMaxAge);
289 corsOptions.fAllowedHeaders = Memory::NullCoalesce (corsOptions.fAllowedHeaders, kDefault_CORSOptions.fAllowedHeaders);
290 corsOptions.fAllowedOrigins = Memory::NullCoalesce (corsOptions.fAllowedOrigins, kDefault_CORSOptions.fAllowedOrigins);
291 return corsOptions;
292 }
293}
294Router::Router (const Sequence<Route>& routes, const CORSOptions& corsOptions)
295 : inherited{MakeSharedPtr<Rep_> (routes, FillIn_ (corsOptions))}
296{
297}
298
300{
301 return _GetRep<Rep_> ().Lookup_ (request);
302}
#define AssertNotReached()
Definition Assertions.h:355
auto MakeSharedPtr(ARGS_TYPE &&... args) -> shared_ptr< T >
same as make_shared, but if type T has block allocation, then use block allocation for the 'shared pa...
#define DbgTrace
Definition Trace.h:309
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual bool Matches(const RegularExpression &regEx) const
Definition String.cpp:1134
static String Join(const Iterable< String > &list, const String &separator=", "sv)
Definition String.cpp:1693
A generalization of a vector: a container whose elements are keyed by the natural numbers.
Set<T> is a container of T, where once an item is added, additionally adds () do nothing.
nonvirtual void Add(ArgByValueType< value_type > item)
Definition Set.inl:138
ClientErrorException is to capture exceptions caused by a bad (e.g ill-formed) request.
Common::Property< String > httpMethod
typically HTTP::Methods::kGet
nonvirtual RETURN_VALUE GetAbsPath() const
Return the GetPath () value, but assuring its an absolute path.
nonvirtual URI Normalize(NormalizationStyle normalization=NormalizationStyle::eDefault) const
Produce a normalized representation of the URI.
Definition URI.cpp:267
Iterable<T> is a base class for containers which easily produce an Iterator<T> to traverse them.
Definition Iterable.h:237
nonvirtual bool empty() const
Returns true iff size() == 0.
Definition Iterable.inl:309
An Iterator<T> is a copyable object which allows traversing the contents of some container....
Definition Iterator.h:225
Common::ReadOnlyProperty< Response & > rwResponse
Definition Message.h:93
Common::ReadOnlyProperty< const Request & > request
Definition Message.h:72
this represents a HTTP request object for the WebServer module
nonvirtual bool Matches(const Request &request, Sequence< String > *pathRegExpMatches=nullptr) const
Definition Router.cpp:56
nonvirtual Iterator< tuple< RequestHandler, Sequence< String > > > Lookup(const Request &request) const
Definition Router.cpp:299
STRING_TYPE ToString(FLOAT_TYPE f, const ToStringOptions &options={})
void Throw(T &&e2Throw)
identical to builtin C++ 'throw' except that it does helpful, type dependent DbgTrace() messages firs...
Definition Throw.inl:43
optional< unsigned int > fAccessControlMaxAge
Definition CORS.h:41
optional< Set< String > > fAllowedOrigins
Definition CORS.h:49
optional< Set< String > > fAllowedHeaders
Definition CORS.h:59