xref: /openbmc/bmcweb/http/routing.hpp (revision 3577e44683a5ade8ad02a6418984b56f4ca2bcac)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4 
5 #include "async_resp.hpp"
6 #include "dbus_privileges.hpp"
7 #include "http_request.hpp"
8 #include "http_response.hpp"
9 #include "logging.hpp"
10 #include "routing/baserule.hpp"
11 #include "routing/dynamicrule.hpp"
12 #include "routing/taggedrule.hpp"
13 #include "routing/trie.hpp"
14 #include "verb.hpp"
15 
16 #include <boost/beast/http/field.hpp>
17 #include <boost/beast/http/status.hpp>
18 
19 #include <algorithm>
20 #include <array>
21 #include <cstdint>
22 #include <cstdlib>
23 #include <format>
24 #include <functional>
25 #include <memory>
26 #include <optional>
27 #include <stdexcept>
28 #include <string>
29 #include <string_view>
30 #include <tuple>
31 #include <utility>
32 #include <vector>
33 
34 namespace crow
35 {
36 
37 class Router
38 {
39   public:
40     Router() = default;
41 
newRuleDynamic(const std::string & rule)42     DynamicRule& newRuleDynamic(const std::string& rule)
43     {
44         std::unique_ptr<DynamicRule> ruleObject =
45             std::make_unique<DynamicRule>(rule);
46         DynamicRule* ptr = ruleObject.get();
47         allRules.emplace_back(std::move(ruleObject));
48 
49         return *ptr;
50     }
51 
52     template <uint64_t NumArgs>
newRuleTagged(const std::string & rule)53     auto& newRuleTagged(const std::string& rule)
54     {
55         if constexpr (NumArgs == 0)
56         {
57             using RuleT = TaggedRule<>;
58             std::unique_ptr<RuleT> ruleObject = std::make_unique<RuleT>(rule);
59             RuleT* ptr = ruleObject.get();
60             allRules.emplace_back(std::move(ruleObject));
61             return *ptr;
62         }
63         else if constexpr (NumArgs == 1)
64         {
65             using RuleT = TaggedRule<std::string>;
66             std::unique_ptr<RuleT> ruleObject = std::make_unique<RuleT>(rule);
67             RuleT* ptr = ruleObject.get();
68             allRules.emplace_back(std::move(ruleObject));
69             return *ptr;
70         }
71         else if constexpr (NumArgs == 2)
72         {
73             using RuleT = TaggedRule<std::string, std::string>;
74             std::unique_ptr<RuleT> ruleObject = std::make_unique<RuleT>(rule);
75             RuleT* ptr = ruleObject.get();
76             allRules.emplace_back(std::move(ruleObject));
77             return *ptr;
78         }
79         else if constexpr (NumArgs == 3)
80         {
81             using RuleT = TaggedRule<std::string, std::string, std::string>;
82             std::unique_ptr<RuleT> ruleObject = std::make_unique<RuleT>(rule);
83             RuleT* ptr = ruleObject.get();
84             allRules.emplace_back(std::move(ruleObject));
85             return *ptr;
86         }
87         else if constexpr (NumArgs == 4)
88         {
89             using RuleT =
90                 TaggedRule<std::string, std::string, std::string, std::string>;
91             std::unique_ptr<RuleT> ruleObject = std::make_unique<RuleT>(rule);
92             RuleT* ptr = ruleObject.get();
93             allRules.emplace_back(std::move(ruleObject));
94             return *ptr;
95         }
96         else
97         {
98             using RuleT = TaggedRule<std::string, std::string, std::string,
99                                      std::string, std::string>;
100             std::unique_ptr<RuleT> ruleObject = std::make_unique<RuleT>(rule);
101             RuleT* ptr = ruleObject.get();
102             allRules.emplace_back(std::move(ruleObject));
103             return *ptr;
104         }
105         static_assert(NumArgs <= 5, "Max number of args supported is 5");
106     }
107 
108     struct PerMethod
109     {
110         std::vector<BaseRule*> rules;
111         Trie<crow::Node> trie;
112         // rule index 0 has special meaning; preallocate it to avoid
113         // duplication.
PerMethodcrow::Router::PerMethod114         PerMethod() : rules(1) {}
115 
internalAddcrow::Router::PerMethod116         void internalAdd(std::string_view rule, BaseRule* ruleObject)
117         {
118             rules.emplace_back(ruleObject);
119             trie.add(rule, static_cast<unsigned>(rules.size() - 1U));
120             // directory case:
121             //   request to `/about' url matches `/about/' rule
122             if (rule.size() > 2 && rule.back() == '/')
123             {
124                 trie.add(rule.substr(0, rule.size() - 1),
125                          static_cast<unsigned>(rules.size() - 1));
126             }
127         }
128     };
129 
internalAddRuleObject(const std::string & rule,BaseRule * ruleObject)130     void internalAddRuleObject(const std::string& rule, BaseRule* ruleObject)
131     {
132         if (ruleObject == nullptr)
133         {
134             return;
135         }
136         for (size_t method = 0; method <= maxVerbIndex; method++)
137         {
138             size_t methodBit = 1 << method;
139             if ((ruleObject->methodsBitfield & methodBit) > 0U)
140             {
141                 perMethods[method].internalAdd(rule, ruleObject);
142             }
143         }
144 
145         if (ruleObject->isNotFound)
146         {
147             notFoundRoutes.internalAdd(rule, ruleObject);
148         }
149 
150         if (ruleObject->isMethodNotAllowed)
151         {
152             methodNotAllowedRoutes.internalAdd(rule, ruleObject);
153         }
154 
155         if (ruleObject->isUpgrade)
156         {
157             upgradeRoutes.internalAdd(rule, ruleObject);
158         }
159     }
160 
validate()161     void validate()
162     {
163         for (std::unique_ptr<BaseRule>& rule : allRules)
164         {
165             if (rule)
166             {
167                 std::unique_ptr<BaseRule> upgraded = rule->upgrade();
168                 if (upgraded)
169                 {
170                     rule = std::move(upgraded);
171                 }
172                 rule->validate();
173                 internalAddRuleObject(rule->rule, rule.get());
174             }
175         }
176         for (PerMethod& perMethod : perMethods)
177         {
178             perMethod.trie.validate();
179         }
180     }
181 
182     struct FindRoute
183     {
184         BaseRule* rule = nullptr;
185         std::vector<std::string> params;
186     };
187 
188     struct FindRouteResponse
189     {
190         std::string allowHeader;
191         FindRoute route;
192     };
193 
findRouteByPerMethod(std::string_view url,const PerMethod & perMethod)194     static FindRoute findRouteByPerMethod(std::string_view url,
195                                           const PerMethod& perMethod)
196     {
197         FindRoute route;
198 
199         Trie<crow::Node>::FindResult found = perMethod.trie.find(url);
200         if (found.ruleIndex >= perMethod.rules.size())
201         {
202             throw std::runtime_error("Trie internal structure corrupted!");
203         }
204         // Found a 404 route, switch that in
205         if (found.ruleIndex != 0U)
206         {
207             route.rule = perMethod.rules[found.ruleIndex];
208             route.params = std::move(found.params);
209         }
210         return route;
211     }
212 
findRoute(const Request & req) const213     FindRouteResponse findRoute(const Request& req) const
214     {
215         FindRouteResponse findRoute;
216 
217         // Check to see if this url exists at any verb
218         for (size_t perMethodIndex = 0; perMethodIndex <= maxVerbIndex;
219              perMethodIndex++)
220         {
221             // Make sure it's safe to deference the array at that index
222             static_assert(
223                 maxVerbIndex < std::tuple_size_v<decltype(perMethods)>);
224             FindRoute route = findRouteByPerMethod(req.url().encoded_path(),
225                                                    perMethods[perMethodIndex]);
226             if (route.rule == nullptr)
227             {
228                 continue;
229             }
230             if (!findRoute.allowHeader.empty())
231             {
232                 findRoute.allowHeader += ", ";
233             }
234             HttpVerb thisVerb = static_cast<HttpVerb>(perMethodIndex);
235             findRoute.allowHeader += httpVerbToString(thisVerb);
236         }
237 
238         std::optional<HttpVerb> verb = httpVerbFromBoost(req.method());
239         if (!verb)
240         {
241             return findRoute;
242         }
243         size_t reqMethodIndex = static_cast<size_t>(*verb);
244         if (reqMethodIndex >= perMethods.size())
245         {
246             return findRoute;
247         }
248 
249         FindRoute route = findRouteByPerMethod(req.url().encoded_path(),
250                                                perMethods[reqMethodIndex]);
251         if (route.rule != nullptr)
252         {
253             findRoute.route = route;
254         }
255 
256         return findRoute;
257     }
258 
259     template <typename Adaptor>
handleUpgrade(const std::shared_ptr<Request> & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,Adaptor && adaptor)260     void handleUpgrade(const std::shared_ptr<Request>& req,
261                        const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
262                        Adaptor&& adaptor)
263     {
264         PerMethod& perMethod = upgradeRoutes;
265         Trie<crow::Node>& trie = perMethod.trie;
266         std::vector<BaseRule*>& rules = perMethod.rules;
267 
268         Trie<crow::Node>::FindResult found =
269             trie.find(req->url().encoded_path());
270         unsigned ruleIndex = found.ruleIndex;
271         if (ruleIndex == 0U)
272         {
273             BMCWEB_LOG_DEBUG("Cannot match rules {}",
274                              req->url().encoded_path());
275             asyncResp->res.result(boost::beast::http::status::not_found);
276             return;
277         }
278 
279         if (ruleIndex >= rules.size())
280         {
281             throw std::runtime_error("Trie internal structure corrupted!");
282         }
283 
284         BaseRule& rule = *rules[ruleIndex];
285 
286         BMCWEB_LOG_DEBUG("Matched rule (upgrade) '{}'", rule.rule);
287 
288         // TODO(ed) This should be able to use std::bind_front, but it doesn't
289         // appear to work with the std::move on adaptor.
290         validatePrivilege(
291             req, asyncResp, rule,
292             [req, &rule, asyncResp,
293              adaptor = std::forward<Adaptor>(adaptor)]() mutable {
294                 rule.handleUpgrade(*req, asyncResp, std::move(adaptor));
295             });
296     }
297 
handle(const std::shared_ptr<Request> & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)298     void handle(const std::shared_ptr<Request>& req,
299                 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
300     {
301         FindRouteResponse foundRoute = findRoute(*req);
302 
303         if (foundRoute.route.rule == nullptr)
304         {
305             // Couldn't find a normal route with any verb, try looking for a 404
306             // route
307             if (foundRoute.allowHeader.empty())
308             {
309                 foundRoute.route = findRouteByPerMethod(
310                     req->url().encoded_path(), notFoundRoutes);
311             }
312             else
313             {
314                 // See if we have a method not allowed (405) handler
315                 foundRoute.route = findRouteByPerMethod(
316                     req->url().encoded_path(), methodNotAllowedRoutes);
317             }
318         }
319 
320         // Fill in the allow header if it's valid
321         if (!foundRoute.allowHeader.empty())
322         {
323             asyncResp->res.addHeader(boost::beast::http::field::allow,
324                                      foundRoute.allowHeader);
325         }
326 
327         // If we couldn't find a real route or a 404 route, return a generic
328         // response
329         if (foundRoute.route.rule == nullptr)
330         {
331             if (foundRoute.allowHeader.empty())
332             {
333                 asyncResp->res.result(boost::beast::http::status::not_found);
334             }
335             else
336             {
337                 asyncResp->res.result(
338                     boost::beast::http::status::method_not_allowed);
339             }
340             return;
341         }
342 
343         BaseRule& rule = *foundRoute.route.rule;
344         std::vector<std::string> params = std::move(foundRoute.route.params);
345 
346         BMCWEB_LOG_DEBUG("Matched rule '{}' {} / {}", rule.rule,
347                          req->methodString(), rule.getMethods());
348 
349         if (req->session == nullptr)
350         {
351             rule.handle(*req, asyncResp, params);
352             return;
353         }
354         validatePrivilege(
355             req, asyncResp, rule,
356             [req, asyncResp, &rule, params = std::move(params)]() {
357                 rule.handle(*req, asyncResp, params);
358             });
359     }
360 
debugPrint()361     void debugPrint()
362     {
363         for (size_t i = 0; i < perMethods.size(); i++)
364         {
365             BMCWEB_LOG_DEBUG("{}", httpVerbToString(static_cast<HttpVerb>(i)));
366             perMethods[i].trie.debugPrint();
367         }
368     }
369 
getRoutes(const std::string & parent)370     std::vector<const std::string*> getRoutes(const std::string& parent)
371     {
372         std::vector<const std::string*> ret;
373 
374         for (const PerMethod& pm : perMethods)
375         {
376             std::vector<unsigned> x;
377             pm.trie.findRouteIndexes(parent, x);
378             for (unsigned index : x)
379             {
380                 ret.push_back(&pm.rules[index]->rule);
381             }
382         }
383         return ret;
384     }
385 
386   private:
387     std::array<PerMethod, static_cast<size_t>(HttpVerb::Max)> perMethods;
388 
389     PerMethod notFoundRoutes;
390     PerMethod upgradeRoutes;
391     PerMethod methodNotAllowedRoutes;
392 
393     std::vector<std::unique_ptr<BaseRule>> allRules;
394 };
395 } // namespace crow
396