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