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