xref: /openbmc/bmcweb/http/utility.hpp (revision 26b3630b181d1093483e4d5ebe6aeeb91a6a2976)
1 #pragma once
2 
3 #include "bmcweb_config.h"
4 
5 #include <openssl/crypto.h>
6 
7 #include <boost/callable_traits.hpp>
8 #include <boost/url/parse.hpp>
9 #include <boost/url/url.hpp>
10 #include <boost/url/url_view.hpp>
11 #include <nlohmann/json.hpp>
12 
13 #include <array>
14 #include <chrono>
15 #include <cstddef>
16 #include <cstdint>
17 #include <ctime>
18 #include <functional>
19 #include <iomanip>
20 #include <limits>
21 #include <stdexcept>
22 #include <string>
23 #include <string_view>
24 #include <tuple>
25 #include <type_traits>
26 #include <utility>
27 #include <variant>
28 
29 namespace crow
30 {
31 namespace black_magic
32 {
33 
34 enum class TypeCode : uint8_t
35 {
36     Unspecified = 0,
37     String = 1,
38     Path = 2,
39     Max = 3,
40 };
41 
42 // Remove when we have c++23
43 template <typename E>
44 constexpr typename std::underlying_type<E>::type toUnderlying(E e) noexcept
45 {
46     return static_cast<typename std::underlying_type<E>::type>(e);
47 }
48 
49 template <typename... Args>
50 struct computeParameterTagFromArgsList;
51 
52 template <>
53 struct computeParameterTagFromArgsList<>
54 {
55     static constexpr int value = 0;
56 };
57 
58 template <typename Arg, typename... Args>
59 struct computeParameterTagFromArgsList<Arg, Args...>
60 {
61     static_assert(std::is_same_v<std::string, std::decay_t<Arg>>);
62     static constexpr int subValue =
63         computeParameterTagFromArgsList<Args...>::value;
64     static constexpr int value = subValue * toUnderlying(TypeCode::String);
65 };
66 
67 inline bool isParameterTagCompatible(uint64_t a, uint64_t b)
68 {
69     while (true)
70     {
71         if (a == 0 && b == 0)
72         {
73             // Both tags were equivalent, parameters are compatible
74             return true;
75         }
76         if (a == 0 || b == 0)
77         {
78             // one of the tags had more parameters than the other
79             return false;
80         }
81         TypeCode sa = static_cast<TypeCode>(a % toUnderlying(TypeCode::Max));
82         TypeCode sb = static_cast<TypeCode>(b % toUnderlying(TypeCode::Max));
83 
84         if (sa == TypeCode::Path)
85         {
86             sa = TypeCode::String;
87         }
88         if (sb == TypeCode::Path)
89         {
90             sb = TypeCode::String;
91         }
92         if (sa != sb)
93         {
94             return false;
95         }
96         a /= toUnderlying(TypeCode::Max);
97         b /= toUnderlying(TypeCode::Max);
98     }
99 }
100 
101 constexpr inline uint64_t getParameterTag(std::string_view url)
102 {
103     uint64_t tagValue = 0;
104     size_t urlSegmentIndex = std::string_view::npos;
105 
106     size_t paramIndex = 0;
107 
108     for (size_t urlIndex = 0; urlIndex < url.size(); urlIndex++)
109     {
110         char character = url[urlIndex];
111         if (character == '<')
112         {
113             if (urlSegmentIndex != std::string_view::npos)
114             {
115                 return 0;
116             }
117             urlSegmentIndex = urlIndex;
118         }
119         if (character == '>')
120         {
121             if (urlSegmentIndex == std::string_view::npos)
122             {
123                 return 0;
124             }
125             std::string_view tag = url.substr(urlSegmentIndex,
126                                               urlIndex + 1 - urlSegmentIndex);
127 
128             // Note, this is a really lame way to do std::pow(6, paramIndex)
129             // std::pow doesn't work in constexpr in clang.
130             // Ideally in the future we'd move this to use a power of 2 packing
131             // (probably 8 instead of 6) so that these just become bit shifts
132             uint64_t insertIndex = 1;
133             for (size_t unused = 0; unused < paramIndex; unused++)
134             {
135                 insertIndex *= 3;
136             }
137 
138             if (tag == "<str>" || tag == "<string>")
139             {
140                 tagValue += insertIndex * toUnderlying(TypeCode::String);
141             }
142             if (tag == "<path>")
143             {
144                 tagValue += insertIndex * toUnderlying(TypeCode::Path);
145             }
146             paramIndex++;
147             urlSegmentIndex = std::string_view::npos;
148         }
149     }
150     if (urlSegmentIndex != std::string_view::npos)
151     {
152         return 0;
153     }
154     return tagValue;
155 }
156 
157 template <typename... T>
158 struct S
159 {
160     template <typename U>
161     using push = S<U, T...>;
162     template <typename U>
163     using push_back = S<T..., U>;
164     template <template <typename... Args> class U>
165     using rebind = U<T...>;
166 };
167 
168 template <typename F, typename Set>
169 struct CallHelper;
170 
171 template <typename F, typename... Args>
172 struct CallHelper<F, S<Args...>>
173 {
174     template <typename F1, typename... Args1,
175               typename = decltype(std::declval<F1>()(std::declval<Args1>()...))>
176     static char test(int);
177 
178     template <typename...>
179     static int test(...);
180 
181     static constexpr bool value = sizeof(test<F, Args...>(0)) == sizeof(char);
182 };
183 
184 template <uint64_t Tag>
185 struct Arguments
186 {
187     using subarguments = typename Arguments<Tag / 3>::type;
188     using type = typename subarguments::template push<std::string>;
189 };
190 
191 template <>
192 struct Arguments<0>
193 {
194     using type = S<>;
195 };
196 
197 } // namespace black_magic
198 
199 namespace utility
200 {
201 
202 template <typename T>
203 struct FunctionTraits
204 {
205     template <size_t i>
206     using arg = std::tuple_element_t<i, boost::callable_traits::args_t<T>>;
207 };
208 
209 inline std::string base64encode(std::string_view data)
210 {
211     const std::array<char, 64> key = {
212         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
213         'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
214         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
215         'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
216         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
217 
218     size_t size = data.size();
219     std::string ret;
220     ret.resize((size + 2) / 3 * 4);
221     auto it = ret.begin();
222 
223     size_t i = 0;
224     while (i < size)
225     {
226         size_t keyIndex = 0;
227 
228         keyIndex = static_cast<size_t>(data[i] & 0xFC) >> 2;
229         *it++ = key[keyIndex];
230 
231         if (i + 1 < size)
232         {
233             keyIndex = static_cast<size_t>(data[i] & 0x03) << 4;
234             keyIndex += static_cast<size_t>(data[i + 1] & 0xF0) >> 4;
235             *it++ = key[keyIndex];
236 
237             if (i + 2 < size)
238             {
239                 keyIndex = static_cast<size_t>(data[i + 1] & 0x0F) << 2;
240                 keyIndex += static_cast<size_t>(data[i + 2] & 0xC0) >> 6;
241                 *it++ = key[keyIndex];
242 
243                 keyIndex = static_cast<size_t>(data[i + 2] & 0x3F);
244                 *it++ = key[keyIndex];
245             }
246             else
247             {
248                 keyIndex = static_cast<size_t>(data[i + 1] & 0x0F) << 2;
249                 *it++ = key[keyIndex];
250                 *it++ = '=';
251             }
252         }
253         else
254         {
255             keyIndex = static_cast<size_t>(data[i] & 0x03) << 4;
256             *it++ = key[keyIndex];
257             *it++ = '=';
258             *it++ = '=';
259         }
260 
261         i += 3;
262     }
263 
264     return ret;
265 }
266 
267 // TODO this is temporary and should be deleted once base64 is refactored out of
268 // crow
269 inline bool base64Decode(std::string_view input, std::string& output)
270 {
271     static const char nop = static_cast<char>(-1);
272     // See note on encoding_data[] in above function
273     static const std::array<char, 256> decodingData = {
274         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
275         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
276         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
277         nop, 62,  nop, nop, nop, 63,  52,  53,  54,  55,  56,  57,  58,  59,
278         60,  61,  nop, nop, nop, nop, nop, nop, nop, 0,   1,   2,   3,   4,
279         5,   6,   7,   8,   9,   10,  11,  12,  13,  14,  15,  16,  17,  18,
280         19,  20,  21,  22,  23,  24,  25,  nop, nop, nop, nop, nop, nop, 26,
281         27,  28,  29,  30,  31,  32,  33,  34,  35,  36,  37,  38,  39,  40,
282         41,  42,  43,  44,  45,  46,  47,  48,  49,  50,  51,  nop, nop, nop,
283         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
284         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
285         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
286         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
287         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
288         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
289         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
290         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
291         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
292         nop, nop, nop, nop};
293 
294     size_t inputLength = input.size();
295 
296     // allocate space for output string
297     output.clear();
298     output.reserve(((inputLength + 2) / 3) * 4);
299 
300     auto getCodeValue = [](char c) {
301         auto code = static_cast<unsigned char>(c);
302         // Ensure we cannot index outside the bounds of the decoding array
303         static_assert(std::numeric_limits<decltype(code)>::max() <
304                       decodingData.size());
305         return decodingData[code];
306     };
307 
308     // for each 4-bytes sequence from the input, extract 4 6-bits sequences by
309     // dropping first two bits
310     // and regenerate into 3 8-bits sequences
311 
312     for (size_t i = 0; i < inputLength; i++)
313     {
314         char base64code0 = 0;
315         char base64code1 = 0;
316         char base64code2 = 0; // initialized to 0 to suppress warnings
317 
318         base64code0 = getCodeValue(input[i]);
319         if (base64code0 == nop)
320         { // non base64 character
321             return false;
322         }
323         if (!(++i < inputLength))
324         { // we need at least two input bytes for first
325           // byte output
326             return false;
327         }
328         base64code1 = getCodeValue(input[i]);
329         if (base64code1 == nop)
330         { // non base64 character
331             return false;
332         }
333         output +=
334             static_cast<char>((base64code0 << 2) | ((base64code1 >> 4) & 0x3));
335 
336         if (++i < inputLength)
337         {
338             char c = input[i];
339             if (c == '=')
340             { // padding , end of input
341                 return (base64code1 & 0x0f) == 0;
342             }
343             base64code2 = getCodeValue(input[i]);
344             if (base64code2 == nop)
345             { // non base64 character
346                 return false;
347             }
348             output += static_cast<char>(((base64code1 << 4) & 0xf0) |
349                                         ((base64code2 >> 2) & 0x0f));
350         }
351 
352         if (++i < inputLength)
353         {
354             char c = input[i];
355             if (c == '=')
356             { // padding , end of input
357                 return (base64code2 & 0x03) == 0;
358             }
359             char base64code3 = getCodeValue(input[i]);
360             if (base64code3 == nop)
361             { // non base64 character
362                 return false;
363             }
364             output +=
365                 static_cast<char>((((base64code2 << 6) & 0xc0) | base64code3));
366         }
367     }
368 
369     return true;
370 }
371 
372 inline bool constantTimeStringCompare(std::string_view a, std::string_view b)
373 {
374     // Important note, this function is ONLY constant time if the two input
375     // sizes are the same
376     if (a.size() != b.size())
377     {
378         return false;
379     }
380     return CRYPTO_memcmp(a.data(), b.data(), a.size()) == 0;
381 }
382 
383 struct ConstantTimeCompare
384 {
385     bool operator()(std::string_view a, std::string_view b) const
386     {
387         return constantTimeStringCompare(a, b);
388     }
389 };
390 
391 namespace details
392 {
393 inline boost::urls::url
394     appendUrlPieces(boost::urls::url& url,
395                     const std::initializer_list<std::string_view> args)
396 {
397     for (std::string_view arg : args)
398     {
399         url.segments().push_back(arg);
400     }
401     return url;
402 }
403 
404 inline boost::urls::url
405     urlFromPiecesDetail(const std::initializer_list<std::string_view> args)
406 {
407     boost::urls::url url("/");
408     appendUrlPieces(url, args);
409     return url;
410 }
411 } // namespace details
412 
413 class OrMorePaths
414 {};
415 
416 template <typename... AV>
417 inline boost::urls::url urlFromPieces(const AV... args)
418 {
419     return details::urlFromPiecesDetail({args...});
420 }
421 
422 template <typename... AV>
423 inline void appendUrlPieces(boost::urls::url& url, const AV... args)
424 {
425     details::appendUrlPieces(url, {args...});
426 }
427 
428 namespace details
429 {
430 
431 // std::reference_wrapper<std::string> - extracts segment to variable
432 //                    std::string_view - checks if segment is equal to variable
433 using UrlSegment = std::variant<std::reference_wrapper<std::string>,
434                                 std::string_view, OrMorePaths>;
435 
436 enum class UrlParseResult
437 {
438     Continue,
439     Fail,
440     Done,
441 };
442 
443 class UrlSegmentMatcherVisitor
444 {
445   public:
446     UrlParseResult operator()(std::string& output)
447     {
448         output = segment;
449         return UrlParseResult::Continue;
450     }
451 
452     UrlParseResult operator()(std::string_view expected)
453     {
454         if (segment == expected)
455         {
456             return UrlParseResult::Continue;
457         }
458         return UrlParseResult::Fail;
459     }
460 
461     UrlParseResult operator()(OrMorePaths /*unused*/)
462     {
463         return UrlParseResult::Done;
464     }
465 
466     explicit UrlSegmentMatcherVisitor(std::string_view segmentIn) :
467         segment(segmentIn)
468     {}
469 
470   private:
471     std::string_view segment;
472 };
473 
474 inline bool readUrlSegments(boost::urls::url_view url,
475                             std::initializer_list<UrlSegment>&& segments)
476 {
477     boost::urls::segments_view urlSegments = url.segments();
478 
479     if (!urlSegments.is_absolute())
480     {
481         return false;
482     }
483 
484     boost::urls::segments_view::iterator it = urlSegments.begin();
485     boost::urls::segments_view::iterator end = urlSegments.end();
486 
487     for (const auto& segment : segments)
488     {
489         if (it == end)
490         {
491             // If the request ends with an "any" path, this was successful
492             return std::holds_alternative<OrMorePaths>(segment);
493         }
494         UrlParseResult res = std::visit(UrlSegmentMatcherVisitor(*it), segment);
495         if (res == UrlParseResult::Done)
496         {
497             return true;
498         }
499         if (res == UrlParseResult::Fail)
500         {
501             return false;
502         }
503         it++;
504     }
505 
506     // There will be an empty segment at the end if the URI ends with a "/"
507     // e.g. /redfish/v1/Chassis/
508     if ((it != end) && urlSegments.back().empty())
509     {
510         it++;
511     }
512     return it == end;
513 }
514 
515 } // namespace details
516 
517 template <typename... Args>
518 inline bool readUrlSegments(boost::urls::url_view url, Args&&... args)
519 {
520     return details::readUrlSegments(url, {std::forward<Args>(args)...});
521 }
522 
523 inline boost::urls::url replaceUrlSegment(boost::urls::url_view urlView,
524                                           const uint replaceLoc,
525                                           std::string_view newSegment)
526 {
527     boost::urls::segments_view urlSegments = urlView.segments();
528     boost::urls::url url("/");
529 
530     if (!urlSegments.is_absolute())
531     {
532         return url;
533     }
534 
535     boost::urls::segments_view::iterator it = urlSegments.begin();
536     boost::urls::segments_view::iterator end = urlSegments.end();
537 
538     for (uint idx = 0; it != end; it++, idx++)
539     {
540         if (idx == replaceLoc)
541         {
542             url.segments().push_back(newSegment);
543         }
544         else
545         {
546             url.segments().push_back(*it);
547         }
548     }
549 
550     return url;
551 }
552 
553 inline std::string setProtocolDefaults(boost::urls::url_view urlView)
554 {
555     if (urlView.scheme() == "https")
556     {
557         return "https";
558     }
559     if (urlView.scheme() == "http")
560     {
561         if (bmcwebInsecureEnableHttpPushStyleEventing)
562         {
563             return "http";
564         }
565         return "";
566     }
567     return "";
568 }
569 
570 inline uint16_t setPortDefaults(boost::urls::url_view url)
571 {
572     uint16_t port = url.port_number();
573     if (port != 0)
574     {
575         // user picked a port already.
576         return port;
577     }
578 
579     // If the user hasn't explicitly stated a port, pick one explicitly for them
580     // based on the protocol defaults
581     if (url.scheme() == "http")
582     {
583         return 80;
584     }
585     if (url.scheme() == "https")
586     {
587         return 443;
588     }
589     return 0;
590 }
591 
592 inline bool validateAndSplitUrl(std::string_view destUrl, std::string& urlProto,
593                                 std::string& host, uint16_t& port,
594                                 std::string& path)
595 {
596     boost::urls::result<boost::urls::url_view> url =
597         boost::urls::parse_uri(destUrl);
598     if (!url)
599     {
600         return false;
601     }
602     urlProto = setProtocolDefaults(url.value());
603     if (urlProto.empty())
604     {
605         return false;
606     }
607 
608     port = setPortDefaults(url.value());
609 
610     host = url->encoded_host();
611 
612     path = url->encoded_path();
613     if (path.empty())
614     {
615         path = "/";
616     }
617     if (url->has_fragment())
618     {
619         path += '#';
620         path += url->encoded_fragment();
621     }
622 
623     if (url->has_query())
624     {
625         path += '?';
626         path += url->encoded_query();
627     }
628 
629     return true;
630 }
631 
632 } // namespace utility
633 } // namespace crow
634 
635 namespace nlohmann
636 {
637 template <>
638 struct adl_serializer<boost::urls::url>
639 {
640     // nlohmann requires a specific casing to look these up in adl
641     // NOLINTNEXTLINE(readability-identifier-naming)
642     static void to_json(json& j, const boost::urls::url& url)
643     {
644         j = url.buffer();
645     }
646 };
647 
648 template <>
649 struct adl_serializer<boost::urls::url_view>
650 {
651     // NOLINTNEXTLINE(readability-identifier-naming)
652     static void to_json(json& j, boost::urls::url_view url)
653     {
654         j = url.buffer();
655     }
656 };
657 } // namespace nlohmann
658