1 #pragma once 2 3 #include <bmcweb_config.h> 4 #include <openssl/crypto.h> 5 6 #include <boost/callable_traits.hpp> 7 #include <boost/date_time/posix_time/posix_time.hpp> 8 #include <boost/url/url.hpp> 9 #include <nlohmann/json.hpp> 10 11 #include <array> 12 #include <chrono> 13 #include <cstddef> 14 #include <cstdint> 15 #include <ctime> 16 #include <functional> 17 #include <limits> 18 #include <stdexcept> 19 #include <string> 20 #include <string_view> 21 #include <tuple> 22 #include <type_traits> 23 #include <utility> 24 #include <variant> 25 26 namespace crow 27 { 28 namespace black_magic 29 { 30 31 enum class TypeCode : uint8_t 32 { 33 Unspecified = 0, 34 Integer = 1, 35 UnsignedInteger = 2, 36 Float = 3, 37 String = 4, 38 Path = 5, 39 Max = 6, 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 T> 50 constexpr TypeCode getParameterTag() 51 { 52 if constexpr (std::is_same_v<int, T>) 53 { 54 return TypeCode::Integer; 55 } 56 if constexpr (std::is_same_v<char, T>) 57 { 58 return TypeCode::Integer; 59 } 60 if constexpr (std::is_same_v<short, T>) 61 { 62 return TypeCode::Integer; 63 } 64 if constexpr (std::is_same_v<long, T>) 65 { 66 return TypeCode::Integer; 67 } 68 if constexpr (std::is_same_v<long long, T>) 69 { 70 return TypeCode::Integer; 71 } 72 if constexpr (std::is_same_v<unsigned int, T>) 73 { 74 return TypeCode::UnsignedInteger; 75 } 76 if constexpr (std::is_same_v<unsigned char, T>) 77 { 78 return TypeCode::UnsignedInteger; 79 } 80 if constexpr (std::is_same_v<unsigned short, T>) 81 { 82 return TypeCode::UnsignedInteger; 83 } 84 if constexpr (std::is_same_v<unsigned long, T>) 85 { 86 return TypeCode::UnsignedInteger; 87 } 88 if constexpr (std::is_same_v<unsigned long long, T>) 89 { 90 return TypeCode::UnsignedInteger; 91 } 92 if constexpr (std::is_same_v<double, T>) 93 { 94 return TypeCode::Float; 95 } 96 if constexpr (std::is_same_v<std::string, T>) 97 { 98 return TypeCode::String; 99 } 100 return TypeCode::Unspecified; 101 } 102 103 template <typename... Args> 104 struct computeParameterTagFromArgsList; 105 106 template <> 107 struct computeParameterTagFromArgsList<> 108 { 109 static constexpr int value = 0; 110 }; 111 112 template <typename Arg, typename... Args> 113 struct computeParameterTagFromArgsList<Arg, Args...> 114 { 115 static constexpr int subValue = 116 computeParameterTagFromArgsList<Args...>::value; 117 static constexpr int value = 118 getParameterTag<typename std::decay<Arg>::type>() != 119 TypeCode::Unspecified 120 ? static_cast<unsigned long>(subValue * 121 toUnderlying(TypeCode::Max)) + 122 static_cast<uint64_t>( 123 getParameterTag<typename std::decay<Arg>::type>()) 124 : subValue; 125 }; 126 127 inline bool isParameterTagCompatible(uint64_t a, uint64_t b) 128 { 129 while (true) 130 { 131 if (a == 0) 132 { 133 return b == 0; 134 } 135 if (b == 0) 136 { 137 return a == 0; 138 } 139 TypeCode sa = static_cast<TypeCode>(a % toUnderlying(TypeCode::Max)); 140 TypeCode sb = static_cast<TypeCode>(b % toUnderlying(TypeCode::Max)); 141 142 if (sa == TypeCode::Path) 143 { 144 sa = TypeCode::String; 145 } 146 if (sb == TypeCode::Path) 147 { 148 sb = TypeCode::String; 149 } 150 if (sa != sb) 151 { 152 return false; 153 } 154 a /= toUnderlying(TypeCode::Max); 155 b /= toUnderlying(TypeCode::Max); 156 } 157 return false; 158 } 159 160 constexpr inline uint64_t getParameterTag(std::string_view url) 161 { 162 uint64_t tagValue = 0; 163 size_t urlSegmentIndex = std::string_view::npos; 164 165 size_t paramIndex = 0; 166 167 for (size_t urlIndex = 0; urlIndex < url.size(); urlIndex++) 168 { 169 char character = url[urlIndex]; 170 if (character == '<') 171 { 172 if (urlSegmentIndex != std::string_view::npos) 173 { 174 return 0; 175 } 176 urlSegmentIndex = urlIndex; 177 } 178 if (character == '>') 179 { 180 if (urlSegmentIndex == std::string_view::npos) 181 { 182 return 0; 183 } 184 std::string_view tag = 185 url.substr(urlSegmentIndex, urlIndex + 1 - urlSegmentIndex); 186 187 // Note, this is a really lame way to do std::pow(6, paramIndex) 188 // std::pow doesn't work in constexpr in clang. 189 // Ideally in the future we'd move this to use a power of 2 packing 190 // (probably 8 instead of 6) so that these just become bit shifts 191 uint64_t insertIndex = 1; 192 for (size_t unused = 0; unused < paramIndex; unused++) 193 { 194 insertIndex *= 6; 195 } 196 197 if (tag == "<int>") 198 { 199 tagValue += insertIndex * toUnderlying(TypeCode::Integer); 200 } 201 if (tag == "<uint>") 202 { 203 tagValue += 204 insertIndex * toUnderlying(TypeCode::UnsignedInteger); 205 } 206 if (tag == "<float>" || tag == "<double>") 207 { 208 tagValue += insertIndex * toUnderlying(TypeCode::Float); 209 } 210 if (tag == "<str>" || tag == "<string>") 211 { 212 tagValue += insertIndex * toUnderlying(TypeCode::String); 213 } 214 if (tag == "<path>") 215 { 216 tagValue += insertIndex * toUnderlying(TypeCode::Path); 217 } 218 paramIndex++; 219 urlSegmentIndex = std::string_view::npos; 220 } 221 } 222 if (urlSegmentIndex != std::string_view::npos) 223 { 224 return 0; 225 } 226 return tagValue; 227 } 228 229 template <typename... T> 230 struct S 231 { 232 template <typename U> 233 using push = S<U, T...>; 234 template <typename U> 235 using push_back = S<T..., U>; 236 template <template <typename... Args> class U> 237 using rebind = U<T...>; 238 }; 239 240 template <typename F, typename Set> 241 struct CallHelper; 242 243 template <typename F, typename... Args> 244 struct CallHelper<F, S<Args...>> 245 { 246 template <typename F1, typename... Args1, 247 typename = decltype(std::declval<F1>()(std::declval<Args1>()...))> 248 static char test(int); 249 250 template <typename...> 251 static int test(...); 252 253 static constexpr bool value = sizeof(test<F, Args...>(0)) == sizeof(char); 254 }; 255 256 template <uint64_t N> 257 struct SingleTagToType 258 {}; 259 260 template <> 261 struct SingleTagToType<1> 262 { 263 using type = int64_t; 264 }; 265 266 template <> 267 struct SingleTagToType<2> 268 { 269 using type = uint64_t; 270 }; 271 272 template <> 273 struct SingleTagToType<3> 274 { 275 using type = double; 276 }; 277 278 template <> 279 struct SingleTagToType<4> 280 { 281 using type = std::string; 282 }; 283 284 template <> 285 struct SingleTagToType<5> 286 { 287 using type = std::string; 288 }; 289 290 template <uint64_t Tag> 291 struct Arguments 292 { 293 using subarguments = typename Arguments<Tag / 6>::type; 294 using type = typename subarguments::template push< 295 typename SingleTagToType<Tag % 6>::type>; 296 }; 297 298 template <> 299 struct Arguments<0> 300 { 301 using type = S<>; 302 }; 303 304 template <typename T> 305 struct Promote 306 { 307 using type = T; 308 }; 309 310 template <typename T> 311 using PromoteT = typename Promote<T>::type; 312 313 template <> 314 struct Promote<char> 315 { 316 using type = int64_t; 317 }; 318 template <> 319 struct Promote<short> 320 { 321 using type = int64_t; 322 }; 323 template <> 324 struct Promote<int> 325 { 326 using type = int64_t; 327 }; 328 template <> 329 struct Promote<long> 330 { 331 using type = int64_t; 332 }; 333 template <> 334 struct Promote<long long> 335 { 336 using type = int64_t; 337 }; 338 template <> 339 struct Promote<unsigned char> 340 { 341 using type = uint64_t; 342 }; 343 template <> 344 struct Promote<unsigned short> 345 { 346 using type = uint64_t; 347 }; 348 template <> 349 struct Promote<unsigned int> 350 { 351 using type = uint64_t; 352 }; 353 template <> 354 struct Promote<unsigned long> 355 { 356 using type = uint64_t; 357 }; 358 template <> 359 struct Promote<unsigned long long> 360 { 361 using type = uint64_t; 362 }; 363 364 } // namespace black_magic 365 366 namespace utility 367 { 368 369 template <typename T> 370 struct FunctionTraits 371 { 372 template <size_t i> 373 using arg = std::tuple_element_t<i, boost::callable_traits::args_t<T>>; 374 }; 375 376 inline std::string base64encode(const std::string_view data) 377 { 378 const std::array<char, 64> key = { 379 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 380 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 381 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 382 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 383 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; 384 385 size_t size = data.size(); 386 std::string ret; 387 ret.resize((size + 2) / 3 * 4); 388 auto it = ret.begin(); 389 390 size_t i = 0; 391 while (i < size) 392 { 393 size_t keyIndex = 0; 394 395 keyIndex = static_cast<size_t>(data[i] & 0xFC) >> 2; 396 *it++ = key[keyIndex]; 397 398 if (i + 1 < size) 399 { 400 keyIndex = static_cast<size_t>(data[i] & 0x03) << 4; 401 keyIndex += static_cast<size_t>(data[i + 1] & 0xF0) >> 4; 402 *it++ = key[keyIndex]; 403 404 if (i + 2 < size) 405 { 406 keyIndex = static_cast<size_t>(data[i + 1] & 0x0F) << 2; 407 keyIndex += static_cast<size_t>(data[i + 2] & 0xC0) >> 6; 408 *it++ = key[keyIndex]; 409 410 keyIndex = static_cast<size_t>(data[i + 2] & 0x3F); 411 *it++ = key[keyIndex]; 412 } 413 else 414 { 415 keyIndex = static_cast<size_t>(data[i + 1] & 0x0F) << 2; 416 *it++ = key[keyIndex]; 417 *it++ = '='; 418 } 419 } 420 else 421 { 422 keyIndex = static_cast<size_t>(data[i] & 0x03) << 4; 423 *it++ = key[keyIndex]; 424 *it++ = '='; 425 *it++ = '='; 426 } 427 428 i += 3; 429 } 430 431 return ret; 432 } 433 434 // TODO this is temporary and should be deleted once base64 is refactored out of 435 // crow 436 inline bool base64Decode(const std::string_view input, std::string& output) 437 { 438 static const char nop = static_cast<char>(-1); 439 // See note on encoding_data[] in above function 440 static const std::array<char, 256> decodingData = { 441 nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 442 nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 443 nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 444 nop, 62, nop, nop, nop, 63, 52, 53, 54, 55, 56, 57, 58, 59, 445 60, 61, nop, nop, nop, nop, nop, nop, nop, 0, 1, 2, 3, 4, 446 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 447 19, 20, 21, 22, 23, 24, 25, nop, nop, nop, nop, nop, nop, 26, 448 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 449 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, nop, nop, nop, 450 nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 451 nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 452 nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 453 nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 454 nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 455 nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 456 nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 457 nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 458 nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 459 nop, nop, nop, nop}; 460 461 size_t inputLength = input.size(); 462 463 // allocate space for output string 464 output.clear(); 465 output.reserve(((inputLength + 2) / 3) * 4); 466 467 auto getCodeValue = [](char c) { 468 auto code = static_cast<unsigned char>(c); 469 // Ensure we cannot index outside the bounds of the decoding array 470 static_assert(std::numeric_limits<decltype(code)>::max() < 471 decodingData.size()); 472 return decodingData[code]; 473 }; 474 475 // for each 4-bytes sequence from the input, extract 4 6-bits sequences by 476 // dropping first two bits 477 // and regenerate into 3 8-bits sequences 478 479 for (size_t i = 0; i < inputLength; i++) 480 { 481 char base64code0 = 0; 482 char base64code1 = 0; 483 char base64code2 = 0; // initialized to 0 to suppress warnings 484 char base64code3 = 0; 485 486 base64code0 = getCodeValue(input[i]); 487 if (base64code0 == nop) 488 { // non base64 character 489 return false; 490 } 491 if (!(++i < inputLength)) 492 { // we need at least two input bytes for first 493 // byte output 494 return false; 495 } 496 base64code1 = getCodeValue(input[i]); 497 if (base64code1 == nop) 498 { // non base64 character 499 return false; 500 } 501 output += 502 static_cast<char>((base64code0 << 2) | ((base64code1 >> 4) & 0x3)); 503 504 if (++i < inputLength) 505 { 506 char c = input[i]; 507 if (c == '=') 508 { // padding , end of input 509 return (base64code1 & 0x0f) == 0; 510 } 511 base64code2 = getCodeValue(input[i]); 512 if (base64code2 == nop) 513 { // non base64 character 514 return false; 515 } 516 output += static_cast<char>(((base64code1 << 4) & 0xf0) | 517 ((base64code2 >> 2) & 0x0f)); 518 } 519 520 if (++i < inputLength) 521 { 522 char c = input[i]; 523 if (c == '=') 524 { // padding , end of input 525 return (base64code2 & 0x03) == 0; 526 } 527 base64code3 = getCodeValue(input[i]); 528 if (base64code3 == nop) 529 { // non base64 character 530 return false; 531 } 532 output += 533 static_cast<char>((((base64code2 << 6) & 0xc0) | base64code3)); 534 } 535 } 536 537 return true; 538 } 539 540 namespace details 541 { 542 constexpr uint64_t maxMilliSeconds = 253402300799999; 543 constexpr uint64_t maxSeconds = 253402300799; 544 inline std::string getDateTime(boost::posix_time::milliseconds timeSinceEpoch) 545 { 546 boost::posix_time::ptime epoch(boost::gregorian::date(1970, 1, 1)); 547 boost::posix_time::ptime time = epoch + timeSinceEpoch; 548 // append zero offset to the end according to the Redfish spec for Date-Time 549 return boost::posix_time::to_iso_extended_string(time) + "+00:00"; 550 } 551 } // namespace details 552 553 // Returns the formatted date time string. 554 // Note that the maximum supported date is 9999-12-31T23:59:59+00:00, if 555 // the given |secondsSinceEpoch| is too large, we return the maximum supported 556 // date. This behavior is to avoid exceptions throwed by Boost. 557 inline std::string getDateTimeUint(uint64_t secondsSinceEpoch) 558 { 559 secondsSinceEpoch = std::min(secondsSinceEpoch, details::maxSeconds); 560 boost::posix_time::seconds boostSeconds(secondsSinceEpoch); 561 return details::getDateTime( 562 boost::posix_time::milliseconds(boostSeconds.total_milliseconds())); 563 } 564 565 // Returns the formatted date time string. 566 // Note that the maximum supported date is 9999-12-31T23:59:59.999+00:00, if 567 // the given |millisSecondsSinceEpoch| is too large, we return the maximum 568 // supported date. 569 inline std::string getDateTimeUintMs(uint64_t milliSecondsSinceEpoch) 570 { 571 milliSecondsSinceEpoch = 572 std::min(details::maxMilliSeconds, milliSecondsSinceEpoch); 573 return details::getDateTime( 574 boost::posix_time::milliseconds(milliSecondsSinceEpoch)); 575 } 576 577 inline std::string getDateTimeStdtime(std::time_t secondsSinceEpoch) 578 { 579 // secondsSinceEpoch >= maxSeconds 580 if constexpr (std::cmp_less_equal(details::maxSeconds, 581 std::numeric_limits<std::time_t>::max())) 582 { 583 if (std::cmp_greater_equal(secondsSinceEpoch, details::maxSeconds)) 584 { 585 secondsSinceEpoch = details::maxSeconds; 586 } 587 } 588 boost::posix_time::ptime time = 589 boost::posix_time::from_time_t(secondsSinceEpoch); 590 return boost::posix_time::to_iso_extended_string(time) + "+00:00"; 591 } 592 593 /** 594 * Returns the current Date, Time & the local Time Offset 595 * infromation in a pair 596 * 597 * @param[in] None 598 * 599 * @return std::pair<std::string, std::string>, which consist 600 * of current DateTime & the TimeOffset strings respectively. 601 */ 602 inline std::pair<std::string, std::string> getDateTimeOffsetNow() 603 { 604 std::time_t time = std::time(nullptr); 605 std::string dateTime = getDateTimeStdtime(time); 606 607 /* extract the local Time Offset value from the 608 * recevied dateTime string. 609 */ 610 std::string timeOffset("Z00:00"); 611 std::size_t lastPos = dateTime.size(); 612 std::size_t len = timeOffset.size(); 613 if (lastPos > len) 614 { 615 timeOffset = dateTime.substr(lastPos - len); 616 } 617 618 return std::make_pair(dateTime, timeOffset); 619 } 620 621 inline bool constantTimeStringCompare(const std::string_view a, 622 const std::string_view b) 623 { 624 // Important note, this function is ONLY constant time if the two input 625 // sizes are the same 626 if (a.size() != b.size()) 627 { 628 return false; 629 } 630 return CRYPTO_memcmp(a.data(), b.data(), a.size()) == 0; 631 } 632 633 struct ConstantTimeCompare 634 { 635 bool operator()(const std::string_view a, const std::string_view b) const 636 { 637 return constantTimeStringCompare(a, b); 638 } 639 }; 640 641 namespace details 642 { 643 inline boost::urls::url 644 urlFromPiecesDetail(const std::initializer_list<std::string_view> args) 645 { 646 boost::urls::url url("/"); 647 for (const std::string_view& arg : args) 648 { 649 url.segments().push_back(arg); 650 } 651 return url; 652 } 653 } // namespace details 654 655 template <typename... AV> 656 inline boost::urls::url urlFromPieces(const AV... args) 657 { 658 return details::urlFromPiecesDetail({args...}); 659 } 660 661 namespace details 662 { 663 664 // std::reference_wrapper<std::string> - extracts segment to variable 665 // std::string_view - checks if segment is equal to variable 666 using UrlSegment = 667 std::variant<std::reference_wrapper<std::string>, std::string_view>; 668 669 class UrlSegmentMatcherVisitor 670 { 671 public: 672 bool operator()(std::string& output) 673 { 674 output = std::string_view(segment.data(), segment.size()); 675 return true; 676 } 677 678 bool operator()(std::string_view expected) 679 { 680 return std::string_view(segment.data(), segment.size()) == expected; 681 } 682 683 explicit UrlSegmentMatcherVisitor( 684 const boost::urls::string_value& segmentIn) : 685 segment(segmentIn) 686 {} 687 688 private: 689 const boost::urls::string_value& segment; 690 }; 691 692 inline bool readUrlSegments(const boost::urls::url_view& urlView, 693 std::initializer_list<UrlSegment>&& segments) 694 { 695 const boost::urls::segments_view& urlSegments = urlView.segments(); 696 697 if (!urlSegments.is_absolute() || segments.size() != urlSegments.size()) 698 { 699 return false; 700 } 701 702 boost::urls::segments_view::iterator it = urlSegments.begin(); 703 boost::urls::segments_view::iterator end = urlSegments.end(); 704 705 for (const auto& segment : segments) 706 { 707 if (!std::visit(UrlSegmentMatcherVisitor(*it), segment)) 708 { 709 return false; 710 } 711 it++; 712 } 713 return true; 714 } 715 716 } // namespace details 717 718 template <typename... Args> 719 inline bool readUrlSegments(const boost::urls::url_view& urlView, 720 Args&&... args) 721 { 722 return details::readUrlSegments(urlView, {std::forward<Args>(args)...}); 723 } 724 725 inline std::string setProtocolDefaults(const boost::urls::url_view& url) 726 { 727 if (url.scheme() == "https") 728 { 729 return "https"; 730 } 731 if (url.scheme() == "http") 732 { 733 if (bmcwebInsecureEnableHttpPushStyleEventing) 734 { 735 return "http"; 736 } 737 return ""; 738 } 739 return ""; 740 } 741 742 inline uint16_t setPortDefaults(const boost::urls::url_view& url) 743 { 744 uint16_t port = url.port_number(); 745 if (port != 0) 746 { 747 // user picked a port already. 748 return port; 749 } 750 751 // If the user hasn't explicitly stated a port, pick one explicitly for them 752 // based on the protocol defaults 753 if (url.scheme() == "http") 754 { 755 return 80; 756 } 757 if (url.scheme() == "https") 758 { 759 return 443; 760 } 761 return 0; 762 } 763 764 inline bool validateAndSplitUrl(std::string_view destUrl, std::string& urlProto, 765 std::string& host, uint16_t& port, 766 std::string& path) 767 { 768 boost::string_view urlBoost(destUrl.data(), destUrl.size()); 769 boost::urls::result<boost::urls::url_view> url = 770 boost::urls::parse_uri(urlBoost); 771 if (!url) 772 { 773 return false; 774 } 775 urlProto = setProtocolDefaults(url.value()); 776 if (urlProto.empty()) 777 { 778 return false; 779 } 780 781 port = setPortDefaults(url.value()); 782 783 host = std::string_view(url->encoded_host().data(), 784 url->encoded_host().size()); 785 786 path = std::string_view(url->encoded_path().data(), 787 url->encoded_path().size()); 788 if (path.empty()) 789 { 790 path = "/"; 791 } 792 if (url->has_fragment()) 793 { 794 path += '#'; 795 path += std::string_view(url->encoded_fragment().data(), 796 url->encoded_fragment().size()); 797 } 798 799 if (url->has_query()) 800 { 801 path += '?'; 802 path += std::string_view(url->encoded_query().data(), 803 url->encoded_query().size()); 804 } 805 806 return true; 807 } 808 809 } // namespace utility 810 } // namespace crow 811 812 namespace nlohmann 813 { 814 template <> 815 struct adl_serializer<boost::urls::url> 816 { 817 // nlohmann requires a specific casing to look these up in adl 818 // NOLINTNEXTLINE(readability-identifier-naming) 819 static void to_json(json& j, const boost::urls::url& url) 820 { 821 j = url.string(); 822 } 823 }; 824 825 template <> 826 struct adl_serializer<boost::urls::url_view> 827 { 828 // NOLINTNEXTLINE(readability-identifier-naming) 829 static void to_json(json& j, const boost::urls::url_view& url) 830 { 831 j = url.string(); 832 } 833 }; 834 } // namespace nlohmann 835