xref: /openbmc/bmcweb/http/utility.hpp (revision f263e09c)
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 } // namespace details
405 
406 class OrMorePaths
407 {};
408 
409 template <typename... AV>
410 inline void appendUrlPieces(boost::urls::url& url, const AV... args)
411 {
412     details::appendUrlPieces(url, {args...});
413 }
414 
415 namespace details
416 {
417 
418 // std::reference_wrapper<std::string> - extracts segment to variable
419 //                    std::string_view - checks if segment is equal to variable
420 using UrlSegment = std::variant<std::reference_wrapper<std::string>,
421                                 std::string_view, OrMorePaths>;
422 
423 enum class UrlParseResult
424 {
425     Continue,
426     Fail,
427     Done,
428 };
429 
430 class UrlSegmentMatcherVisitor
431 {
432   public:
433     UrlParseResult operator()(std::string& output)
434     {
435         output = segment;
436         return UrlParseResult::Continue;
437     }
438 
439     UrlParseResult operator()(std::string_view expected)
440     {
441         if (segment == expected)
442         {
443             return UrlParseResult::Continue;
444         }
445         return UrlParseResult::Fail;
446     }
447 
448     UrlParseResult operator()(OrMorePaths /*unused*/)
449     {
450         return UrlParseResult::Done;
451     }
452 
453     explicit UrlSegmentMatcherVisitor(std::string_view segmentIn) :
454         segment(segmentIn)
455     {}
456 
457   private:
458     std::string_view segment;
459 };
460 
461 inline bool readUrlSegments(boost::urls::url_view url,
462                             std::initializer_list<UrlSegment>&& segments)
463 {
464     boost::urls::segments_view urlSegments = url.segments();
465 
466     if (!urlSegments.is_absolute())
467     {
468         return false;
469     }
470 
471     boost::urls::segments_view::iterator it = urlSegments.begin();
472     boost::urls::segments_view::iterator end = urlSegments.end();
473 
474     for (const auto& segment : segments)
475     {
476         if (it == end)
477         {
478             // If the request ends with an "any" path, this was successful
479             return std::holds_alternative<OrMorePaths>(segment);
480         }
481         UrlParseResult res = std::visit(UrlSegmentMatcherVisitor(*it), segment);
482         if (res == UrlParseResult::Done)
483         {
484             return true;
485         }
486         if (res == UrlParseResult::Fail)
487         {
488             return false;
489         }
490         it++;
491     }
492 
493     // There will be an empty segment at the end if the URI ends with a "/"
494     // e.g. /redfish/v1/Chassis/
495     if ((it != end) && urlSegments.back().empty())
496     {
497         it++;
498     }
499     return it == end;
500 }
501 
502 } // namespace details
503 
504 template <typename... Args>
505 inline bool readUrlSegments(boost::urls::url_view url, Args&&... args)
506 {
507     return details::readUrlSegments(url, {std::forward<Args>(args)...});
508 }
509 
510 inline boost::urls::url replaceUrlSegment(boost::urls::url_view urlView,
511                                           const uint replaceLoc,
512                                           std::string_view newSegment)
513 {
514     boost::urls::segments_view urlSegments = urlView.segments();
515     boost::urls::url url("/");
516 
517     if (!urlSegments.is_absolute())
518     {
519         return url;
520     }
521 
522     boost::urls::segments_view::iterator it = urlSegments.begin();
523     boost::urls::segments_view::iterator end = urlSegments.end();
524 
525     for (uint idx = 0; it != end; it++, idx++)
526     {
527         if (idx == replaceLoc)
528         {
529             url.segments().push_back(newSegment);
530         }
531         else
532         {
533             url.segments().push_back(*it);
534         }
535     }
536 
537     return url;
538 }
539 
540 inline std::string setProtocolDefaults(boost::urls::url_view urlView)
541 {
542     if (urlView.scheme() == "https")
543     {
544         return "https";
545     }
546     if (urlView.scheme() == "http")
547     {
548         if (bmcwebInsecureEnableHttpPushStyleEventing)
549         {
550             return "http";
551         }
552         return "";
553     }
554     return "";
555 }
556 
557 inline uint16_t setPortDefaults(boost::urls::url_view url)
558 {
559     uint16_t port = url.port_number();
560     if (port != 0)
561     {
562         // user picked a port already.
563         return port;
564     }
565 
566     // If the user hasn't explicitly stated a port, pick one explicitly for them
567     // based on the protocol defaults
568     if (url.scheme() == "http")
569     {
570         return 80;
571     }
572     if (url.scheme() == "https")
573     {
574         return 443;
575     }
576     return 0;
577 }
578 
579 inline bool validateAndSplitUrl(std::string_view destUrl, std::string& urlProto,
580                                 std::string& host, uint16_t& port,
581                                 std::string& path)
582 {
583     boost::urls::result<boost::urls::url_view> url =
584         boost::urls::parse_uri(destUrl);
585     if (!url)
586     {
587         return false;
588     }
589     urlProto = setProtocolDefaults(url.value());
590     if (urlProto.empty())
591     {
592         return false;
593     }
594 
595     port = setPortDefaults(url.value());
596 
597     host = url->encoded_host();
598 
599     path = url->encoded_path();
600     if (path.empty())
601     {
602         path = "/";
603     }
604     if (url->has_fragment())
605     {
606         path += '#';
607         path += url->encoded_fragment();
608     }
609 
610     if (url->has_query())
611     {
612         path += '?';
613         path += url->encoded_query();
614     }
615 
616     return true;
617 }
618 
619 } // namespace utility
620 } // namespace crow
621 
622 namespace nlohmann
623 {
624 template <>
625 struct adl_serializer<boost::urls::url>
626 {
627     // nlohmann requires a specific casing to look these up in adl
628     // NOLINTNEXTLINE(readability-identifier-naming)
629     static void to_json(json& j, const boost::urls::url& url)
630     {
631         j = url.buffer();
632     }
633 };
634 
635 template <>
636 struct adl_serializer<boost::urls::url_view>
637 {
638     // NOLINTNEXTLINE(readability-identifier-naming)
639     static void to_json(json& j, boost::urls::url_view url)
640     {
641         j = url.buffer();
642     }
643 };
644 } // namespace nlohmann
645