xref: /openbmc/bmcweb/http/utility.hpp (revision 2682a0e76f9a77dc42b20d317850277e3ac3a33c)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4 
5 #include "bmcweb_config.h"
6 
7 #include "str_utility.hpp"
8 
9 #include <sys/types.h>
10 
11 #include <boost/url/segments_view.hpp>
12 #include <boost/url/url.hpp>
13 #include <boost/url/url_view_base.hpp>
14 #include <nlohmann/adl_serializer.hpp>
15 #include <nlohmann/json.hpp>
16 
17 #include <array>
18 #include <bit>
19 #include <concepts>
20 #include <cstddef>
21 #include <cstdint>
22 #include <ctime>
23 #include <functional>
24 #include <initializer_list>
25 #include <limits>
26 #include <string>
27 #include <string_view>
28 #include <type_traits>
29 #include <utility>
30 #include <variant>
31 
32 namespace crow
33 {
34 namespace utility
35 {
36 
getParameterTag(std::string_view url)37 constexpr uint64_t getParameterTag(std::string_view url)
38 {
39     uint64_t tagValue = 0;
40     size_t urlSegmentIndex = std::string_view::npos;
41 
42     for (size_t urlIndex = 0; urlIndex < url.size(); urlIndex++)
43     {
44         char character = url[urlIndex];
45         if (character == '<')
46         {
47             if (urlSegmentIndex != std::string_view::npos)
48             {
49                 return 0;
50             }
51             urlSegmentIndex = urlIndex;
52         }
53         if (character == '>')
54         {
55             if (urlSegmentIndex == std::string_view::npos)
56             {
57                 return 0;
58             }
59             std::string_view tag =
60                 url.substr(urlSegmentIndex, urlIndex + 1 - urlSegmentIndex);
61 
62             if (tag == "<str>" || tag == "<string>")
63             {
64                 tagValue++;
65             }
66             if (tag == "<path>")
67             {
68                 tagValue++;
69             }
70             urlSegmentIndex = std::string_view::npos;
71         }
72     }
73     if (urlSegmentIndex != std::string_view::npos)
74     {
75         return 0;
76     }
77     return tagValue;
78 }
79 
80 constexpr static std::array<char, 64> base64key = {
81     'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
82     'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
83     'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
84     'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
85     '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
86 
87 static constexpr char nop = static_cast<char>(-1);
getDecodeTable(bool urlSafe)88 constexpr std::array<char, 256> getDecodeTable(bool urlSafe)
89 {
90     std::array<char, 256> decodeTable{};
91     decodeTable.fill(nop);
92 
93     for (size_t index = 0; index < base64key.size(); index++)
94     {
95         char character = base64key[index];
96         decodeTable[std::bit_cast<uint8_t>(character)] =
97             static_cast<char>(index);
98     }
99 
100     if (urlSafe)
101     {
102         // Urlsafe decode tables replace the last two characters with - and _
103         decodeTable['+'] = nop;
104         decodeTable['/'] = nop;
105         decodeTable['-'] = 62;
106         decodeTable['_'] = 63;
107     }
108 
109     return decodeTable;
110 }
111 
112 class Base64Encoder
113 {
114     char overflow1 = '\0';
115     char overflow2 = '\0';
116     uint8_t overflowCount = 0;
117 
118     // Takes 3 ascii chars, and encodes them as 4 base64 chars
encodeTriple(char first,char second,char third,std::string & output)119     static void encodeTriple(char first, char second, char third,
120                              std::string& output)
121     {
122         size_t keyIndex = 0;
123 
124         keyIndex = static_cast<size_t>(first & 0xFC) >> 2;
125         output += base64key[keyIndex];
126 
127         keyIndex = static_cast<size_t>(first & 0x03) << 4;
128         keyIndex += static_cast<size_t>(second & 0xF0) >> 4;
129         output += base64key[keyIndex];
130 
131         keyIndex = static_cast<size_t>(second & 0x0F) << 2;
132         keyIndex += static_cast<size_t>(third & 0xC0) >> 6;
133         output += base64key[keyIndex];
134 
135         keyIndex = static_cast<size_t>(third & 0x3F);
136         output += base64key[keyIndex];
137     }
138 
139   public:
140     // Accepts a partial string to encode, and writes the encoded characters to
141     // the output stream. requires subsequently calling finalize to complete
142     // stream.
encode(std::string_view data,std::string & output)143     void encode(std::string_view data, std::string& output)
144     {
145         // Encode the last round of overflow chars first
146         if (overflowCount == 2)
147         {
148             if (!data.empty())
149             {
150                 encodeTriple(overflow1, overflow2, data[0], output);
151                 overflowCount = 0;
152                 data.remove_prefix(1);
153             }
154         }
155         else if (overflowCount == 1)
156         {
157             if (data.size() >= 2)
158             {
159                 encodeTriple(overflow1, data[0], data[1], output);
160                 overflowCount = 0;
161                 data.remove_prefix(2);
162             }
163         }
164 
165         while (data.size() >= 3)
166         {
167             encodeTriple(data[0], data[1], data[2], output);
168             data.remove_prefix(3);
169         }
170 
171         if (!data.empty() && overflowCount == 0)
172         {
173             overflow1 = data[0];
174             overflowCount++;
175             data.remove_prefix(1);
176         }
177 
178         if (!data.empty() && overflowCount == 1)
179         {
180             overflow2 = data[0];
181             overflowCount++;
182             data.remove_prefix(1);
183         }
184     }
185 
186     // Completes a base64 output, by writing any MOD(3) characters to the
187     // output, as well as any required trailing =
finalize(std::string & output)188     void finalize(std::string& output)
189     {
190         if (overflowCount == 0)
191         {
192             return;
193         }
194         size_t keyIndex = static_cast<size_t>(overflow1 & 0xFC) >> 2;
195         output += base64key[keyIndex];
196 
197         keyIndex = static_cast<size_t>(overflow1 & 0x03) << 4;
198         if (overflowCount == 2)
199         {
200             keyIndex += static_cast<size_t>(overflow2 & 0xF0) >> 4;
201             output += base64key[keyIndex];
202             keyIndex = static_cast<size_t>(overflow2 & 0x0F) << 2;
203             output += base64key[keyIndex];
204         }
205         else
206         {
207             output += base64key[keyIndex];
208             output += '=';
209         }
210         output += '=';
211         overflowCount = 0;
212     }
213 
214     // Returns the required output buffer in characters for an input of size
215     // inputSize
encodedSize(size_t inputSize)216     static size_t constexpr encodedSize(size_t inputSize)
217     {
218         // Base64 encodes 3 character blocks as 4 character blocks
219         // With a possibility of 2 trailing = characters
220         return (inputSize + 2) / 3 * 4;
221     }
222 };
223 
base64encode(std::string_view data)224 inline std::string base64encode(std::string_view data)
225 {
226     // Encodes a 3 character stream into a 4 character stream
227     std::string out;
228     Base64Encoder base64;
229     out.reserve(Base64Encoder::encodedSize(data.size()));
230     base64.encode(data, out);
231     base64.finalize(out);
232     return out;
233 }
234 
createBasicAuthHeader(std::string_view username,std::string_view password)235 inline std::string createBasicAuthHeader(std::string_view username,
236                                          std::string_view password)
237 {
238     std::string credentials = "Basic ";
239     Base64Encoder enc;
240     enc.encode(username, credentials);
241     enc.encode(":", credentials);
242     enc.encode(password, credentials);
243     enc.finalize(credentials);
244     return credentials;
245 }
246 
247 template <bool urlsafe = false>
base64Decode(std::string_view input,std::string & output)248 inline bool base64Decode(std::string_view input, std::string& output)
249 {
250     size_t inputLength = input.size();
251 
252     // allocate space for output string
253     output.clear();
254     output.reserve(((inputLength + 2) / 3) * 4);
255 
256     static constexpr auto decodingData = getDecodeTable(urlsafe);
257 
258     auto getCodeValue = [](char c) {
259         auto code = static_cast<unsigned char>(c);
260         // Ensure we cannot index outside the bounds of the decoding array
261         static_assert(
262             std::numeric_limits<decltype(code)>::max() < decodingData.size());
263         return decodingData[code];
264     };
265 
266     // for each 4-bytes sequence from the input, extract 4 6-bits sequences by
267     // dropping first two bits
268     // and regenerate into 3 8-bits sequences
269 
270     for (size_t i = 0; i < inputLength; i++)
271     {
272         char base64code0 = 0;
273         char base64code1 = 0;
274         char base64code2 = 0; // initialized to 0 to suppress warnings
275 
276         base64code0 = getCodeValue(input[i]);
277         if (base64code0 == nop)
278         {
279             // non base64 character
280             return false;
281         }
282         if (!(++i < inputLength))
283         {
284             // we need at least two input bytes for first byte output
285             return false;
286         }
287         base64code1 = getCodeValue(input[i]);
288         if (base64code1 == nop)
289         {
290             // non base64 character
291             return false;
292         }
293         output +=
294             static_cast<char>((base64code0 << 2) | ((base64code1 >> 4) & 0x3));
295 
296         if (++i < inputLength)
297         {
298             char c = input[i];
299             if (c == '=')
300             {
301                 // padding , end of input
302                 return (base64code1 & 0x0f) == 0;
303             }
304             base64code2 = getCodeValue(input[i]);
305             if (base64code2 == nop)
306             {
307                 // non base64 character
308                 return false;
309             }
310             output += static_cast<char>(
311                 ((base64code1 << 4) & 0xf0) | ((base64code2 >> 2) & 0x0f));
312         }
313 
314         if (++i < inputLength)
315         {
316             char c = input[i];
317             if (c == '=')
318             {
319                 // padding , end of input
320                 return (base64code2 & 0x03) == 0;
321             }
322             char base64code3 = getCodeValue(input[i]);
323             if (base64code3 == nop)
324             {
325                 // non base64 character
326                 return false;
327             }
328             output +=
329                 static_cast<char>((((base64code2 << 6) & 0xc0) | base64code3));
330         }
331     }
332 
333     return true;
334 }
335 
336 class OrMorePaths
337 {};
338 
339 template <typename... AV>
appendUrlPieces(boost::urls::url & url,AV &&...args)340 inline void appendUrlPieces(boost::urls::url& url, AV&&... args)
341 {
342     // Unclear the correct fix here.
343     // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay)
344     for (const std::string_view arg : {args...})
345     {
346         url.segments().push_back(arg);
347     }
348 }
349 
350 namespace details
351 {
352 
353 // std::reference_wrapper<std::string> - extracts segment to variable
354 //                    std::string_view - checks if segment is equal to variable
355 using UrlSegment = std::variant<std::reference_wrapper<std::string>,
356                                 std::string_view, OrMorePaths>;
357 
358 enum class UrlParseResult
359 {
360     Continue,
361     Fail,
362     Done,
363 };
364 
365 class UrlSegmentMatcherVisitor
366 {
367   public:
operator ()(std::string & output)368     UrlParseResult operator()(std::string& output)
369     {
370         output = segment;
371         return UrlParseResult::Continue;
372     }
373 
operator ()(std::string_view expected)374     UrlParseResult operator()(std::string_view expected)
375     {
376         if (segment == expected)
377         {
378             return UrlParseResult::Continue;
379         }
380         return UrlParseResult::Fail;
381     }
382 
operator ()(OrMorePaths)383     UrlParseResult operator()(OrMorePaths /*unused*/)
384     {
385         return UrlParseResult::Done;
386     }
387 
UrlSegmentMatcherVisitor(std::string_view segmentIn)388     explicit UrlSegmentMatcherVisitor(std::string_view segmentIn) :
389         segment(segmentIn)
390     {}
391 
392   private:
393     std::string_view segment;
394 };
395 
readUrlSegments(const boost::urls::url_view_base & url,std::initializer_list<UrlSegment> segments)396 inline bool readUrlSegments(const boost::urls::url_view_base& url,
397                             std::initializer_list<UrlSegment> segments)
398 {
399     const boost::urls::segments_view& urlSegments = url.segments();
400 
401     if (!urlSegments.is_absolute())
402     {
403         return false;
404     }
405 
406     boost::urls::segments_view::const_iterator it = urlSegments.begin();
407     boost::urls::segments_view::const_iterator end = urlSegments.end();
408 
409     std::string fragment = url.fragment();
410     std::vector<std::string> fragmentParts;
411     bmcweb::split(fragmentParts, fragment, '/');
412     auto fragIt = fragmentParts.begin();
413     auto fragEnd = fragmentParts.end();
414 
415     // Url fragments start with a /, so we need to skip the first empty string
416     if (fragIt != fragEnd)
417     {
418         if (fragIt->empty())
419         {
420             fragIt++;
421         }
422     }
423 
424     // There will be an empty segment at the end if the URI ends with a "/"
425     // e.g. /redfish/v1/Chassis/
426     if ((it != end) && urlSegments.back().empty())
427     {
428         end--;
429     }
430 
431     for (const auto& segment : segments)
432     {
433         UrlParseResult res = UrlParseResult::Fail;
434         if (it == end)
435         {
436             if (fragIt == fragEnd)
437             {
438                 return std::holds_alternative<OrMorePaths>(segment);
439             }
440             res = std::visit(UrlSegmentMatcherVisitor(*fragIt), segment);
441             fragIt++;
442         }
443         else
444         {
445             res = std::visit(UrlSegmentMatcherVisitor(*it), segment);
446             it++;
447         }
448         if (res == UrlParseResult::Done)
449         {
450             return true;
451         }
452         if (res == UrlParseResult::Fail)
453         {
454             return false;
455         }
456     }
457 
458     return it == end;
459 }
460 
461 } // namespace details
462 
463 template <typename... Args>
readUrlSegments(const boost::urls::url_view_base & url,Args &&...args)464 inline bool readUrlSegments(const boost::urls::url_view_base& url,
465                             Args&&... args)
466 {
467     return details::readUrlSegments(url, {std::forward<Args>(args)...});
468 }
469 
replaceUrlSegment(const boost::urls::url_view_base & urlView,const uint replaceLoc,std::string_view newSegment)470 inline boost::urls::url replaceUrlSegment(
471     const boost::urls::url_view_base& urlView, const uint replaceLoc,
472     std::string_view newSegment)
473 {
474     const boost::urls::segments_view& urlSegments = urlView.segments();
475     boost::urls::url url("/");
476 
477     if (!urlSegments.is_absolute())
478     {
479         return url;
480     }
481 
482     boost::urls::segments_view::iterator it = urlSegments.begin();
483     boost::urls::segments_view::iterator end = urlSegments.end();
484 
485     for (uint idx = 0; it != end; it++, idx++)
486     {
487         if (idx == replaceLoc)
488         {
489             url.segments().push_back(newSegment);
490         }
491         else
492         {
493             url.segments().push_back(*it);
494         }
495     }
496 
497     return url;
498 }
499 
setProtocolDefaults(boost::urls::url & url,std::string_view protocol)500 inline void setProtocolDefaults(boost::urls::url& url,
501                                 std::string_view protocol)
502 {
503     if (url.has_scheme())
504     {
505         return;
506     }
507     if (protocol == "Redfish" || protocol.empty())
508     {
509         if (url.port_number() == 443)
510         {
511             url.set_scheme("https");
512         }
513         if (url.port_number() == 80)
514         {
515             if constexpr (BMCWEB_INSECURE_PUSH_STYLE_NOTIFICATION)
516             {
517                 url.set_scheme("http");
518             }
519         }
520     }
521     else if (protocol == "SNMPv2c")
522     {
523         url.set_scheme("snmp");
524     }
525 }
526 
setPortDefaults(boost::urls::url & url)527 inline void setPortDefaults(boost::urls::url& url)
528 {
529     uint16_t port = url.port_number();
530     if (port != 0)
531     {
532         return;
533     }
534 
535     // If the user hasn't explicitly stated a port, pick one explicitly for them
536     // based on the protocol defaults
537     if (url.scheme() == "http")
538     {
539         url.set_port_number(80);
540     }
541     if (url.scheme() == "https")
542     {
543         url.set_port_number(443);
544     }
545     if (url.scheme() == "snmp")
546     {
547         url.set_port_number(162);
548     }
549 }
550 
551 } // namespace utility
552 } // namespace crow
553 
554 namespace nlohmann
555 {
556 template <std::derived_from<boost::urls::url_view_base> URL>
557 struct adl_serializer<URL>
558 {
559     // NOLINTNEXTLINE(readability-identifier-naming)
to_jsonnlohmann::adl_serializer560     static void to_json(json& j, const URL& url)
561     {
562         j = url.buffer();
563     }
564 };
565 } // namespace nlohmann
566