a (hacky, wip) multi-tenant oidc-terminating reverse proxy, written in anger on top of pingora

basic oauth & oidc support

this implements login, logout, claim-to-header proxying, and header
stripping.

the whole domain is protected for a given domain -- there's no partial
coverage yet

+3835 -223
+2117 -13
Cargo.lock
··· 18 18 checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 19 20 20 [[package]] 21 + name = "aead" 22 + version = "0.5.2" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" 25 + dependencies = [ 26 + "crypto-common 0.1.7", 27 + "generic-array", 28 + ] 29 + 30 + [[package]] 31 + name = "aes" 32 + version = "0.8.4" 33 + source = "registry+https://github.com/rust-lang/crates.io-index" 34 + checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" 35 + dependencies = [ 36 + "cfg-if", 37 + "cipher", 38 + "cpufeatures", 39 + ] 40 + 41 + [[package]] 42 + name = "aes-gcm" 43 + version = "0.10.3" 44 + source = "registry+https://github.com/rust-lang/crates.io-index" 45 + checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" 46 + dependencies = [ 47 + "aead", 48 + "aes", 49 + "cipher", 50 + "ctr", 51 + "ghash", 52 + "subtle", 53 + ] 54 + 55 + [[package]] 56 + name = "aes-kw" 57 + version = "0.2.1" 58 + source = "registry+https://github.com/rust-lang/crates.io-index" 59 + checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c" 60 + dependencies = [ 61 + "aes", 62 + ] 63 + 64 + [[package]] 21 65 name = "ahash" 22 66 version = "0.8.12" 23 67 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 132 176 ] 133 177 134 178 [[package]] 179 + name = "argon2" 180 + version = "0.5.3" 181 + source = "registry+https://github.com/rust-lang/crates.io-index" 182 + checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" 183 + dependencies = [ 184 + "base64ct", 185 + "blake2", 186 + "cpufeatures", 187 + "password-hash", 188 + ] 189 + 190 + [[package]] 135 191 name = "arrayvec" 136 192 version = "0.7.6" 137 193 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 149 205 "nom", 150 206 "num-traits", 151 207 "rusticata-macros", 152 - "thiserror", 208 + "thiserror 1.0.69", 153 209 "time", 154 210 ] 155 211 ··· 185 241 "proc-macro2", 186 242 "quote", 187 243 "syn 2.0.114", 244 + ] 245 + 246 + [[package]] 247 + name = "atomic-polyfill" 248 + version = "1.0.3" 249 + source = "registry+https://github.com/rust-lang/crates.io-index" 250 + checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" 251 + dependencies = [ 252 + "critical-section", 188 253 ] 189 254 190 255 [[package]] ··· 198 263 version = "0.1.0" 199 264 dependencies = [ 200 265 "async-trait", 266 + "base64 0.22.1", 201 267 "color-eyre", 268 + "compact_jwt", 269 + "cookie-rs", 270 + "ed25519-dalek", 271 + "form_urlencoded", 202 272 "http", 273 + "jiff", 274 + "papaya", 203 275 "pingora", 276 + "postcard", 204 277 "prost", 205 278 "prost-reflect", 206 279 "prost-reflect-build", 280 + "rand 0.8.5", 281 + "reqwest", 207 282 "rustls", 283 + "serde", 284 + "serde_html_form", 285 + "serde_json", 286 + "sha2 0.10.9", 208 287 "tokio", 209 288 "tracing", 210 289 "tracing-subscriber", 290 + "url", 291 + "uuid", 211 292 ] 212 293 213 294 [[package]] ··· 254 335 ] 255 336 256 337 [[package]] 338 + name = "base16ct" 339 + version = "0.2.0" 340 + source = "registry+https://github.com/rust-lang/crates.io-index" 341 + checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 342 + 343 + [[package]] 344 + name = "base64" 345 + version = "0.21.7" 346 + source = "registry+https://github.com/rust-lang/crates.io-index" 347 + checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 348 + 349 + [[package]] 257 350 name = "base64" 258 351 version = "0.22.1" 259 352 source = "registry+https://github.com/rust-lang/crates.io-index" 260 353 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 261 354 262 355 [[package]] 356 + name = "base64ct" 357 + version = "1.8.3" 358 + source = "registry+https://github.com/rust-lang/crates.io-index" 359 + checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" 360 + 361 + [[package]] 362 + name = "base64urlsafedata" 363 + version = "0.5.4" 364 + source = "registry+https://github.com/rust-lang/crates.io-index" 365 + checksum = "42f7f6be94fa637132933fd0a68b9140bcb60e3d46164cb68e82a2bb8d102b3a" 366 + dependencies = [ 367 + "base64 0.21.7", 368 + "pastey", 369 + "serde", 370 + ] 371 + 372 + [[package]] 263 373 name = "beef" 264 374 version = "0.5.2" 265 375 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 283 393 source = "registry+https://github.com/rust-lang/crates.io-index" 284 394 checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" 285 395 dependencies = [ 286 - "digest", 396 + "digest 0.10.7", 287 397 ] 288 398 289 399 [[package]] ··· 296 406 ] 297 407 298 408 [[package]] 409 + name = "block-buffer" 410 + version = "0.11.0" 411 + source = "registry+https://github.com/rust-lang/crates.io-index" 412 + checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" 413 + dependencies = [ 414 + "hybrid-array", 415 + ] 416 + 417 + [[package]] 418 + name = "block-padding" 419 + version = "0.3.3" 420 + source = "registry+https://github.com/rust-lang/crates.io-index" 421 + checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" 422 + dependencies = [ 423 + "generic-array", 424 + ] 425 + 426 + [[package]] 299 427 name = "brotli" 300 428 version = "3.5.0" 301 429 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 328 456 ] 329 457 330 458 [[package]] 459 + name = "bumpalo" 460 + version = "3.19.1" 461 + source = "registry+https://github.com/rust-lang/crates.io-index" 462 + checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" 463 + 464 + [[package]] 331 465 name = "byteorder" 332 466 version = "1.5.0" 333 467 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 340 474 checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" 341 475 342 476 [[package]] 477 + name = "cbc" 478 + version = "0.1.2" 479 + source = "registry+https://github.com/rust-lang/crates.io-index" 480 + checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" 481 + dependencies = [ 482 + "cipher", 483 + ] 484 + 485 + [[package]] 343 486 name = "cc" 344 487 version = "1.2.55" 345 488 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 352 495 ] 353 496 354 497 [[package]] 498 + name = "cesu8" 499 + version = "1.1.0" 500 + source = "registry+https://github.com/rust-lang/crates.io-index" 501 + checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 502 + 503 + [[package]] 355 504 name = "cf-rustracing" 356 505 version = "1.2.1" 357 506 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 386 535 checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 387 536 388 537 [[package]] 538 + name = "cfg_aliases" 539 + version = "0.2.1" 540 + source = "registry+https://github.com/rust-lang/crates.io-index" 541 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 542 + 543 + [[package]] 389 544 name = "chrono" 390 545 version = "0.4.43" 391 546 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 395 550 ] 396 551 397 552 [[package]] 553 + name = "cipher" 554 + version = "0.4.4" 555 + source = "registry+https://github.com/rust-lang/crates.io-index" 556 + checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 557 + dependencies = [ 558 + "crypto-common 0.1.7", 559 + "inout", 560 + ] 561 + 562 + [[package]] 398 563 name = "clap" 399 564 version = "4.5.57" 400 565 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 444 609 ] 445 610 446 611 [[package]] 612 + name = "cmov" 613 + version = "0.5.2" 614 + source = "registry+https://github.com/rust-lang/crates.io-index" 615 + checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3" 616 + 617 + [[package]] 618 + name = "cobs" 619 + version = "0.3.0" 620 + source = "registry+https://github.com/rust-lang/crates.io-index" 621 + checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" 622 + dependencies = [ 623 + "thiserror 2.0.18", 624 + ] 625 + 626 + [[package]] 447 627 name = "color-eyre" 448 628 version = "0.6.5" 449 629 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 477 657 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 478 658 479 659 [[package]] 660 + name = "combine" 661 + version = "4.6.7" 662 + source = "registry+https://github.com/rust-lang/crates.io-index" 663 + checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 664 + dependencies = [ 665 + "bytes", 666 + "memchr", 667 + ] 668 + 669 + [[package]] 670 + name = "compact_jwt" 671 + version = "0.5.4" 672 + source = "registry+https://github.com/rust-lang/crates.io-index" 673 + checksum = "c08bd132a4de34edb8e03618d1de2173571bbd2150beb2324e1ba817a4c798bb" 674 + dependencies = [ 675 + "base64 0.22.1", 676 + "base64urlsafedata", 677 + "crypto-glue", 678 + "hex", 679 + "kanidm-hsm-crypto", 680 + "serde", 681 + "serde_json", 682 + "time", 683 + "tracing", 684 + "url", 685 + "uuid", 686 + ] 687 + 688 + [[package]] 689 + name = "const-oid" 690 + version = "0.9.6" 691 + source = "registry+https://github.com/rust-lang/crates.io-index" 692 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 693 + 694 + [[package]] 695 + name = "cookie-rs" 696 + version = "0.4.1" 697 + source = "registry+https://github.com/rust-lang/crates.io-index" 698 + checksum = "69deedbff42d34777a2c86794e57629898a686d84efcc28dfa8fbb515c8884ce" 699 + 700 + [[package]] 480 701 name = "core-foundation" 481 702 version = "0.9.4" 482 703 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 487 708 ] 488 709 489 710 [[package]] 711 + name = "core-foundation" 712 + version = "0.10.1" 713 + source = "registry+https://github.com/rust-lang/crates.io-index" 714 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 715 + dependencies = [ 716 + "core-foundation-sys", 717 + "libc", 718 + ] 719 + 720 + [[package]] 490 721 name = "core-foundation-sys" 491 722 version = "0.8.7" 492 723 source = "registry+https://github.com/rust-lang/crates.io-index" 493 724 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 494 725 495 726 [[package]] 727 + name = "cpufeatures" 728 + version = "0.2.17" 729 + source = "registry+https://github.com/rust-lang/crates.io-index" 730 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 731 + dependencies = [ 732 + "libc", 733 + ] 734 + 735 + [[package]] 496 736 name = "crc32fast" 497 737 version = "1.5.0" 498 738 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 500 740 dependencies = [ 501 741 "cfg-if", 502 742 ] 743 + 744 + [[package]] 745 + name = "critical-section" 746 + version = "1.2.0" 747 + source = "registry+https://github.com/rust-lang/crates.io-index" 748 + checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" 503 749 504 750 [[package]] 505 751 name = "crossbeam-queue" ··· 517 763 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 518 764 519 765 [[package]] 766 + name = "crypto-bigint" 767 + version = "0.5.5" 768 + source = "registry+https://github.com/rust-lang/crates.io-index" 769 + checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 770 + dependencies = [ 771 + "generic-array", 772 + "rand_core 0.6.4", 773 + "subtle", 774 + "zeroize", 775 + ] 776 + 777 + [[package]] 520 778 name = "crypto-common" 521 779 version = "0.1.7" 522 780 source = "registry+https://github.com/rust-lang/crates.io-index" 523 781 checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" 524 782 dependencies = [ 525 783 "generic-array", 784 + "rand_core 0.6.4", 526 785 "typenum", 527 786 ] 528 787 529 788 [[package]] 789 + name = "crypto-common" 790 + version = "0.2.0" 791 + source = "registry+https://github.com/rust-lang/crates.io-index" 792 + checksum = "211f05e03c7d03754740fd9e585de910a095d6b99f8bcfffdef8319fa02a8331" 793 + dependencies = [ 794 + "hybrid-array", 795 + "rand_core 0.10.0", 796 + ] 797 + 798 + [[package]] 799 + name = "crypto-glue" 800 + version = "0.1.13" 801 + source = "registry+https://github.com/rust-lang/crates.io-index" 802 + checksum = "b7c276323bf5cd771d8eed5a8eb7011acf74450531d01efb7f8c085d4eb2c388" 803 + dependencies = [ 804 + "aes", 805 + "aes-gcm", 806 + "aes-kw", 807 + "argon2", 808 + "cbc", 809 + "cipher", 810 + "const-oid", 811 + "crypto-common 0.1.7", 812 + "crypto-common 0.2.0", 813 + "der", 814 + "digest 0.11.0", 815 + "ecdsa", 816 + "elliptic-curve", 817 + "generic-array", 818 + "hex", 819 + "hkdf", 820 + "hmac 0.12.1", 821 + "hmac 0.13.0-rc.5", 822 + "hybrid-array", 823 + "kbkdf", 824 + "p256", 825 + "p384", 826 + "p521", 827 + "pkcs8", 828 + "rand 0.8.5", 829 + "rsa", 830 + "rustls", 831 + "sec1", 832 + "sha1", 833 + "sha2 0.10.9", 834 + "sha2 0.11.0-rc.5", 835 + "spki", 836 + "subtle", 837 + "tracing", 838 + "uuid", 839 + "x509-cert", 840 + "zeroize", 841 + ] 842 + 843 + [[package]] 844 + name = "ctr" 845 + version = "0.9.2" 846 + source = "registry+https://github.com/rust-lang/crates.io-index" 847 + checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" 848 + dependencies = [ 849 + "cipher", 850 + ] 851 + 852 + [[package]] 853 + name = "ctutils" 854 + version = "0.4.0" 855 + source = "registry+https://github.com/rust-lang/crates.io-index" 856 + checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4" 857 + dependencies = [ 858 + "cmov", 859 + ] 860 + 861 + [[package]] 862 + name = "curve25519-dalek" 863 + version = "4.1.3" 864 + source = "registry+https://github.com/rust-lang/crates.io-index" 865 + checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" 866 + dependencies = [ 867 + "cfg-if", 868 + "cpufeatures", 869 + "curve25519-dalek-derive", 870 + "digest 0.10.7", 871 + "fiat-crypto", 872 + "rustc_version", 873 + "subtle", 874 + "zeroize", 875 + ] 876 + 877 + [[package]] 878 + name = "curve25519-dalek-derive" 879 + version = "0.1.1" 880 + source = "registry+https://github.com/rust-lang/crates.io-index" 881 + checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" 882 + dependencies = [ 883 + "proc-macro2", 884 + "quote", 885 + "syn 2.0.114", 886 + ] 887 + 888 + [[package]] 530 889 name = "daemonize" 531 890 version = "0.5.0" 532 891 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 577 936 checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" 578 937 579 938 [[package]] 939 + name = "der" 940 + version = "0.7.10" 941 + source = "registry+https://github.com/rust-lang/crates.io-index" 942 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 943 + dependencies = [ 944 + "const-oid", 945 + "der_derive", 946 + "flagset", 947 + "pem-rfc7468", 948 + "zeroize", 949 + ] 950 + 951 + [[package]] 580 952 name = "der-parser" 581 953 version = "9.0.0" 582 954 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 591 963 ] 592 964 593 965 [[package]] 966 + name = "der_derive" 967 + version = "0.7.3" 968 + source = "registry+https://github.com/rust-lang/crates.io-index" 969 + checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" 970 + dependencies = [ 971 + "proc-macro2", 972 + "quote", 973 + "syn 2.0.114", 974 + ] 975 + 976 + [[package]] 594 977 name = "deranged" 595 978 version = "0.5.5" 596 979 source = "registry+https://github.com/rust-lang/crates.io-index" 597 980 checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 598 981 dependencies = [ 599 982 "powerfmt", 983 + "serde_core", 600 984 ] 601 985 602 986 [[package]] ··· 647 1031 source = "registry+https://github.com/rust-lang/crates.io-index" 648 1032 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 649 1033 dependencies = [ 650 - "block-buffer", 651 - "crypto-common", 1034 + "block-buffer 0.10.4", 1035 + "const-oid", 1036 + "crypto-common 0.1.7", 652 1037 "subtle", 653 1038 ] 654 1039 655 1040 [[package]] 1041 + name = "digest" 1042 + version = "0.11.0" 1043 + source = "registry+https://github.com/rust-lang/crates.io-index" 1044 + checksum = "f8bf3682cdec91817be507e4aa104314898b95b84d74f3d43882210101a545b6" 1045 + dependencies = [ 1046 + "block-buffer 0.11.0", 1047 + "crypto-common 0.2.0", 1048 + "ctutils", 1049 + ] 1050 + 1051 + [[package]] 656 1052 name = "displaydoc" 657 1053 version = "0.2.5" 658 1054 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 670 1066 checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 671 1067 672 1068 [[package]] 1069 + name = "ecdsa" 1070 + version = "0.16.9" 1071 + source = "registry+https://github.com/rust-lang/crates.io-index" 1072 + checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 1073 + dependencies = [ 1074 + "der", 1075 + "digest 0.10.7", 1076 + "elliptic-curve", 1077 + "rfc6979", 1078 + "signature", 1079 + "spki", 1080 + ] 1081 + 1082 + [[package]] 1083 + name = "ed25519" 1084 + version = "2.2.3" 1085 + source = "registry+https://github.com/rust-lang/crates.io-index" 1086 + checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" 1087 + dependencies = [ 1088 + "pkcs8", 1089 + "serde", 1090 + "signature", 1091 + ] 1092 + 1093 + [[package]] 1094 + name = "ed25519-dalek" 1095 + version = "2.2.0" 1096 + source = "registry+https://github.com/rust-lang/crates.io-index" 1097 + checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" 1098 + dependencies = [ 1099 + "curve25519-dalek", 1100 + "ed25519", 1101 + "rand_core 0.6.4", 1102 + "serde", 1103 + "sha2 0.10.9", 1104 + "subtle", 1105 + "zeroize", 1106 + ] 1107 + 1108 + [[package]] 673 1109 name = "either" 674 1110 version = "1.15.0" 675 1111 source = "registry+https://github.com/rust-lang/crates.io-index" 676 1112 checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 677 1113 678 1114 [[package]] 1115 + name = "elliptic-curve" 1116 + version = "0.13.8" 1117 + source = "registry+https://github.com/rust-lang/crates.io-index" 1118 + checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 1119 + dependencies = [ 1120 + "base16ct", 1121 + "crypto-bigint", 1122 + "digest 0.10.7", 1123 + "ff", 1124 + "generic-array", 1125 + "group", 1126 + "hkdf", 1127 + "pem-rfc7468", 1128 + "pkcs8", 1129 + "rand_core 0.6.4", 1130 + "sec1", 1131 + "subtle", 1132 + "zeroize", 1133 + ] 1134 + 1135 + [[package]] 1136 + name = "encoding_rs" 1137 + version = "0.8.35" 1138 + source = "registry+https://github.com/rust-lang/crates.io-index" 1139 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 1140 + dependencies = [ 1141 + "cfg-if", 1142 + ] 1143 + 1144 + [[package]] 679 1145 name = "equivalent" 680 1146 version = "1.0.2" 681 1147 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 708 1174 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 709 1175 710 1176 [[package]] 1177 + name = "ff" 1178 + version = "0.13.1" 1179 + source = "registry+https://github.com/rust-lang/crates.io-index" 1180 + checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 1181 + dependencies = [ 1182 + "rand_core 0.6.4", 1183 + "subtle", 1184 + ] 1185 + 1186 + [[package]] 1187 + name = "fiat-crypto" 1188 + version = "0.2.9" 1189 + source = "registry+https://github.com/rust-lang/crates.io-index" 1190 + checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" 1191 + 1192 + [[package]] 711 1193 name = "find-msvc-tools" 712 1194 version = "0.1.9" 713 1195 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 718 1200 version = "0.5.7" 719 1201 source = "registry+https://github.com/rust-lang/crates.io-index" 720 1202 checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" 1203 + 1204 + [[package]] 1205 + name = "flagset" 1206 + version = "0.4.7" 1207 + source = "registry+https://github.com/rust-lang/crates.io-index" 1208 + checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" 721 1209 722 1210 [[package]] 723 1211 name = "flate2" ··· 749 1237 checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" 750 1238 751 1239 [[package]] 1240 + name = "form_urlencoded" 1241 + version = "1.2.2" 1242 + source = "registry+https://github.com/rust-lang/crates.io-index" 1243 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 1244 + dependencies = [ 1245 + "percent-encoding", 1246 + ] 1247 + 1248 + [[package]] 752 1249 name = "fs_extra" 753 1250 version = "1.3.0" 754 1251 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 849 1346 source = "registry+https://github.com/rust-lang/crates.io-index" 850 1347 checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 851 1348 dependencies = [ 1349 + "serde", 852 1350 "typenum", 853 1351 "version_check", 1352 + "zeroize", 854 1353 ] 855 1354 856 1355 [[package]] ··· 860 1359 checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 861 1360 dependencies = [ 862 1361 "cfg-if", 1362 + "js-sys", 863 1363 "libc", 864 1364 "wasi", 1365 + "wasm-bindgen", 865 1366 ] 866 1367 867 1368 [[package]] ··· 871 1372 checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 872 1373 dependencies = [ 873 1374 "cfg-if", 1375 + "js-sys", 874 1376 "libc", 875 1377 "r-efi", 876 1378 "wasip2", 1379 + "wasm-bindgen", 1380 + ] 1381 + 1382 + [[package]] 1383 + name = "getrandom" 1384 + version = "0.4.1" 1385 + source = "registry+https://github.com/rust-lang/crates.io-index" 1386 + checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" 1387 + dependencies = [ 1388 + "cfg-if", 1389 + "libc", 1390 + "r-efi", 1391 + "wasip2", 1392 + "wasip3", 877 1393 ] 878 1394 879 1395 [[package]] ··· 889 1405 ] 890 1406 891 1407 [[package]] 1408 + name = "ghash" 1409 + version = "0.5.1" 1410 + source = "registry+https://github.com/rust-lang/crates.io-index" 1411 + checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" 1412 + dependencies = [ 1413 + "opaque-debug", 1414 + "polyval", 1415 + ] 1416 + 1417 + [[package]] 892 1418 name = "gimli" 893 1419 version = "0.32.3" 894 1420 source = "registry+https://github.com/rust-lang/crates.io-index" 895 1421 checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" 1422 + 1423 + [[package]] 1424 + name = "group" 1425 + version = "0.13.0" 1426 + source = "registry+https://github.com/rust-lang/crates.io-index" 1427 + checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 1428 + dependencies = [ 1429 + "ff", 1430 + "rand_core 0.6.4", 1431 + "subtle", 1432 + ] 896 1433 897 1434 [[package]] 898 1435 name = "h2" ··· 914 1451 ] 915 1452 916 1453 [[package]] 1454 + name = "hash32" 1455 + version = "0.2.1" 1456 + source = "registry+https://github.com/rust-lang/crates.io-index" 1457 + checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" 1458 + dependencies = [ 1459 + "byteorder", 1460 + ] 1461 + 1462 + [[package]] 917 1463 name = "hashbrown" 918 1464 version = "0.12.3" 919 1465 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 940 1486 ] 941 1487 942 1488 [[package]] 1489 + name = "heapless" 1490 + version = "0.7.17" 1491 + source = "registry+https://github.com/rust-lang/crates.io-index" 1492 + checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" 1493 + dependencies = [ 1494 + "atomic-polyfill", 1495 + "hash32", 1496 + "rustc_version", 1497 + "serde", 1498 + "spin", 1499 + "stable_deref_trait", 1500 + ] 1501 + 1502 + [[package]] 943 1503 name = "heck" 944 1504 version = "0.4.1" 945 1505 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 958 1518 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 959 1519 960 1520 [[package]] 1521 + name = "hkdf" 1522 + version = "0.12.4" 1523 + source = "registry+https://github.com/rust-lang/crates.io-index" 1524 + checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 1525 + dependencies = [ 1526 + "hmac 0.12.1", 1527 + ] 1528 + 1529 + [[package]] 1530 + name = "hmac" 1531 + version = "0.12.1" 1532 + source = "registry+https://github.com/rust-lang/crates.io-index" 1533 + checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 1534 + dependencies = [ 1535 + "digest 0.10.7", 1536 + ] 1537 + 1538 + [[package]] 1539 + name = "hmac" 1540 + version = "0.13.0-rc.5" 1541 + source = "registry+https://github.com/rust-lang/crates.io-index" 1542 + checksum = "ef451d73f36d8a3f93ad32c332ea01146c9650e1ec821a9b0e46c01277d544f8" 1543 + dependencies = [ 1544 + "digest 0.11.0", 1545 + ] 1546 + 1547 + [[package]] 961 1548 name = "hostname" 962 1549 version = "0.4.2" 963 1550 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 979 1566 ] 980 1567 981 1568 [[package]] 1569 + name = "http-body" 1570 + version = "1.0.1" 1571 + source = "registry+https://github.com/rust-lang/crates.io-index" 1572 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 1573 + dependencies = [ 1574 + "bytes", 1575 + "http", 1576 + ] 1577 + 1578 + [[package]] 1579 + name = "http-body-util" 1580 + version = "0.1.3" 1581 + source = "registry+https://github.com/rust-lang/crates.io-index" 1582 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 1583 + dependencies = [ 1584 + "bytes", 1585 + "futures-core", 1586 + "http", 1587 + "http-body", 1588 + "pin-project-lite", 1589 + ] 1590 + 1591 + [[package]] 982 1592 name = "httparse" 983 1593 version = "1.10.1" 984 1594 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 991 1601 checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 992 1602 993 1603 [[package]] 1604 + name = "hybrid-array" 1605 + version = "0.4.7" 1606 + source = "registry+https://github.com/rust-lang/crates.io-index" 1607 + checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" 1608 + dependencies = [ 1609 + "serde", 1610 + "typenum", 1611 + "zeroize", 1612 + ] 1613 + 1614 + [[package]] 1615 + name = "hyper" 1616 + version = "1.8.1" 1617 + source = "registry+https://github.com/rust-lang/crates.io-index" 1618 + checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 1619 + dependencies = [ 1620 + "atomic-waker", 1621 + "bytes", 1622 + "futures-channel", 1623 + "futures-core", 1624 + "h2", 1625 + "http", 1626 + "http-body", 1627 + "httparse", 1628 + "itoa", 1629 + "pin-project-lite", 1630 + "pin-utils", 1631 + "smallvec", 1632 + "tokio", 1633 + "want", 1634 + ] 1635 + 1636 + [[package]] 1637 + name = "hyper-rustls" 1638 + version = "0.27.7" 1639 + source = "registry+https://github.com/rust-lang/crates.io-index" 1640 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 1641 + dependencies = [ 1642 + "http", 1643 + "hyper", 1644 + "hyper-util", 1645 + "rustls", 1646 + "rustls-pki-types", 1647 + "tokio", 1648 + "tokio-rustls", 1649 + "tower-service", 1650 + ] 1651 + 1652 + [[package]] 1653 + name = "hyper-util" 1654 + version = "0.1.20" 1655 + source = "registry+https://github.com/rust-lang/crates.io-index" 1656 + checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" 1657 + dependencies = [ 1658 + "base64 0.22.1", 1659 + "bytes", 1660 + "futures-channel", 1661 + "futures-util", 1662 + "http", 1663 + "http-body", 1664 + "hyper", 1665 + "ipnet", 1666 + "libc", 1667 + "percent-encoding", 1668 + "pin-project-lite", 1669 + "socket2", 1670 + "system-configuration", 1671 + "tokio", 1672 + "tower-service", 1673 + "tracing", 1674 + "windows-registry", 1675 + ] 1676 + 1677 + [[package]] 1678 + name = "icu_collections" 1679 + version = "2.1.1" 1680 + source = "registry+https://github.com/rust-lang/crates.io-index" 1681 + checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 1682 + dependencies = [ 1683 + "displaydoc", 1684 + "potential_utf", 1685 + "yoke", 1686 + "zerofrom", 1687 + "zerovec", 1688 + ] 1689 + 1690 + [[package]] 1691 + name = "icu_locale_core" 1692 + version = "2.1.1" 1693 + source = "registry+https://github.com/rust-lang/crates.io-index" 1694 + checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 1695 + dependencies = [ 1696 + "displaydoc", 1697 + "litemap", 1698 + "tinystr", 1699 + "writeable", 1700 + "zerovec", 1701 + ] 1702 + 1703 + [[package]] 1704 + name = "icu_normalizer" 1705 + version = "2.1.1" 1706 + source = "registry+https://github.com/rust-lang/crates.io-index" 1707 + checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 1708 + dependencies = [ 1709 + "icu_collections", 1710 + "icu_normalizer_data", 1711 + "icu_properties", 1712 + "icu_provider", 1713 + "smallvec", 1714 + "zerovec", 1715 + ] 1716 + 1717 + [[package]] 1718 + name = "icu_normalizer_data" 1719 + version = "2.1.1" 1720 + source = "registry+https://github.com/rust-lang/crates.io-index" 1721 + checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 1722 + 1723 + [[package]] 1724 + name = "icu_properties" 1725 + version = "2.1.2" 1726 + source = "registry+https://github.com/rust-lang/crates.io-index" 1727 + checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" 1728 + dependencies = [ 1729 + "icu_collections", 1730 + "icu_locale_core", 1731 + "icu_properties_data", 1732 + "icu_provider", 1733 + "zerotrie", 1734 + "zerovec", 1735 + ] 1736 + 1737 + [[package]] 1738 + name = "icu_properties_data" 1739 + version = "2.1.2" 1740 + source = "registry+https://github.com/rust-lang/crates.io-index" 1741 + checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" 1742 + 1743 + [[package]] 1744 + name = "icu_provider" 1745 + version = "2.1.1" 1746 + source = "registry+https://github.com/rust-lang/crates.io-index" 1747 + checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 1748 + dependencies = [ 1749 + "displaydoc", 1750 + "icu_locale_core", 1751 + "writeable", 1752 + "yoke", 1753 + "zerofrom", 1754 + "zerotrie", 1755 + "zerovec", 1756 + ] 1757 + 1758 + [[package]] 1759 + name = "id-arena" 1760 + version = "2.3.0" 1761 + source = "registry+https://github.com/rust-lang/crates.io-index" 1762 + checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" 1763 + 1764 + [[package]] 994 1765 name = "ident_case" 995 1766 version = "1.0.1" 996 1767 source = "registry+https://github.com/rust-lang/crates.io-index" 997 1768 checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 998 1769 999 1770 [[package]] 1771 + name = "idna" 1772 + version = "1.1.0" 1773 + source = "registry+https://github.com/rust-lang/crates.io-index" 1774 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 1775 + dependencies = [ 1776 + "idna_adapter", 1777 + "smallvec", 1778 + "utf8_iter", 1779 + ] 1780 + 1781 + [[package]] 1782 + name = "idna_adapter" 1783 + version = "1.2.1" 1784 + source = "registry+https://github.com/rust-lang/crates.io-index" 1785 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 1786 + dependencies = [ 1787 + "icu_normalizer", 1788 + "icu_properties", 1789 + ] 1790 + 1791 + [[package]] 1000 1792 name = "indenter" 1001 1793 version = "0.3.4" 1002 1794 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1020 1812 dependencies = [ 1021 1813 "equivalent", 1022 1814 "hashbrown 0.16.1", 1815 + "serde", 1816 + "serde_core", 1817 + ] 1818 + 1819 + [[package]] 1820 + name = "inout" 1821 + version = "0.1.4" 1822 + source = "registry+https://github.com/rust-lang/crates.io-index" 1823 + checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" 1824 + dependencies = [ 1825 + "block-padding", 1826 + "generic-array", 1827 + ] 1828 + 1829 + [[package]] 1830 + name = "ipnet" 1831 + version = "2.11.0" 1832 + source = "registry+https://github.com/rust-lang/crates.io-index" 1833 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1834 + 1835 + [[package]] 1836 + name = "iri-string" 1837 + version = "0.7.10" 1838 + source = "registry+https://github.com/rust-lang/crates.io-index" 1839 + checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" 1840 + dependencies = [ 1841 + "memchr", 1842 + "serde", 1023 1843 ] 1024 1844 1025 1845 [[package]] ··· 1044 1864 checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 1045 1865 1046 1866 [[package]] 1867 + name = "jiff" 1868 + version = "0.2.20" 1869 + source = "registry+https://github.com/rust-lang/crates.io-index" 1870 + checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" 1871 + dependencies = [ 1872 + "jiff-static", 1873 + "jiff-tzdb-platform", 1874 + "log", 1875 + "portable-atomic", 1876 + "portable-atomic-util", 1877 + "serde_core", 1878 + "windows-sys 0.61.2", 1879 + ] 1880 + 1881 + [[package]] 1882 + name = "jiff-static" 1883 + version = "0.2.20" 1884 + source = "registry+https://github.com/rust-lang/crates.io-index" 1885 + checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" 1886 + dependencies = [ 1887 + "proc-macro2", 1888 + "quote", 1889 + "syn 2.0.114", 1890 + ] 1891 + 1892 + [[package]] 1893 + name = "jiff-tzdb" 1894 + version = "0.1.5" 1895 + source = "registry+https://github.com/rust-lang/crates.io-index" 1896 + checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" 1897 + 1898 + [[package]] 1899 + name = "jiff-tzdb-platform" 1900 + version = "0.1.3" 1901 + source = "registry+https://github.com/rust-lang/crates.io-index" 1902 + checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" 1903 + dependencies = [ 1904 + "jiff-tzdb", 1905 + ] 1906 + 1907 + [[package]] 1908 + name = "jni" 1909 + version = "0.21.1" 1910 + source = "registry+https://github.com/rust-lang/crates.io-index" 1911 + checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 1912 + dependencies = [ 1913 + "cesu8", 1914 + "cfg-if", 1915 + "combine", 1916 + "jni-sys", 1917 + "log", 1918 + "thiserror 1.0.69", 1919 + "walkdir", 1920 + "windows-sys 0.45.0", 1921 + ] 1922 + 1923 + [[package]] 1924 + name = "jni-sys" 1925 + version = "0.3.0" 1926 + source = "registry+https://github.com/rust-lang/crates.io-index" 1927 + checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 1928 + 1929 + [[package]] 1047 1930 name = "jobserver" 1048 1931 version = "0.1.34" 1049 1932 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1054 1937 ] 1055 1938 1056 1939 [[package]] 1940 + name = "js-sys" 1941 + version = "0.3.85" 1942 + source = "registry+https://github.com/rust-lang/crates.io-index" 1943 + checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" 1944 + dependencies = [ 1945 + "once_cell", 1946 + "wasm-bindgen", 1947 + ] 1948 + 1949 + [[package]] 1950 + name = "kanidm-hsm-crypto" 1951 + version = "0.3.5" 1952 + source = "registry+https://github.com/rust-lang/crates.io-index" 1953 + checksum = "61cafdd63d3c246fd7a7318de64e35d2c744ebb2c5a51a407a2985ad6fe29908" 1954 + dependencies = [ 1955 + "crypto-glue", 1956 + "serde", 1957 + "tracing", 1958 + ] 1959 + 1960 + [[package]] 1961 + name = "kbkdf" 1962 + version = "0.1.0-rc.1" 1963 + source = "registry+https://github.com/rust-lang/crates.io-index" 1964 + checksum = "90ac93c9768b8d587407881c98b0c3a5d3e3049daa73408ebe5bfb1ab1cb9c84" 1965 + dependencies = [ 1966 + "digest 0.11.0", 1967 + ] 1968 + 1969 + [[package]] 1057 1970 name = "lazy_static" 1058 1971 version = "1.5.0" 1059 1972 source = "registry+https://github.com/rust-lang/crates.io-index" 1060 1973 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1974 + dependencies = [ 1975 + "spin", 1976 + ] 1977 + 1978 + [[package]] 1979 + name = "leb128fmt" 1980 + version = "0.1.0" 1981 + source = "registry+https://github.com/rust-lang/crates.io-index" 1982 + checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" 1061 1983 1062 1984 [[package]] 1063 1985 name = "libc" ··· 1066 1988 checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" 1067 1989 1068 1990 [[package]] 1991 + name = "libm" 1992 + version = "0.2.16" 1993 + source = "registry+https://github.com/rust-lang/crates.io-index" 1994 + checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" 1995 + 1996 + [[package]] 1069 1997 name = "libz-ng-sys" 1070 1998 version = "1.1.23" 1071 1999 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1080 2008 version = "0.11.0" 1081 2009 source = "registry+https://github.com/rust-lang/crates.io-index" 1082 2010 checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 2011 + 2012 + [[package]] 2013 + name = "litemap" 2014 + version = "0.8.1" 2015 + source = "registry+https://github.com/rust-lang/crates.io-index" 2016 + checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 1083 2017 1084 2018 [[package]] 1085 2019 name = "local-ip-address" ··· 1151 2085 ] 1152 2086 1153 2087 [[package]] 2088 + name = "lru-slab" 2089 + version = "0.1.2" 2090 + source = "registry+https://github.com/rust-lang/crates.io-index" 2091 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 2092 + 2093 + [[package]] 1154 2094 name = "matchers" 1155 2095 version = "0.2.0" 1156 2096 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1175 2115 ] 1176 2116 1177 2117 [[package]] 2118 + name = "mime" 2119 + version = "0.3.17" 2120 + source = "registry+https://github.com/rust-lang/crates.io-index" 2121 + checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 2122 + 2123 + [[package]] 1178 2124 name = "minimal-lexical" 1179 2125 version = "0.2.1" 1180 2126 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1284 2230 ] 1285 2231 1286 2232 [[package]] 2233 + name = "num-bigint-dig" 2234 + version = "0.8.6" 2235 + source = "registry+https://github.com/rust-lang/crates.io-index" 2236 + checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" 2237 + dependencies = [ 2238 + "lazy_static", 2239 + "libm", 2240 + "num-integer", 2241 + "num-iter", 2242 + "num-traits", 2243 + "rand 0.8.5", 2244 + "smallvec", 2245 + "zeroize", 2246 + ] 2247 + 2248 + [[package]] 1287 2249 name = "num-conv" 1288 2250 version = "0.2.0" 1289 2251 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1299 2261 ] 1300 2262 1301 2263 [[package]] 2264 + name = "num-iter" 2265 + version = "0.1.45" 2266 + source = "registry+https://github.com/rust-lang/crates.io-index" 2267 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 2268 + dependencies = [ 2269 + "autocfg", 2270 + "num-integer", 2271 + "num-traits", 2272 + ] 2273 + 2274 + [[package]] 1302 2275 name = "num-traits" 1303 2276 version = "0.2.19" 1304 2277 source = "registry+https://github.com/rust-lang/crates.io-index" 1305 2278 checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1306 2279 dependencies = [ 1307 2280 "autocfg", 2281 + "libm", 1308 2282 ] 1309 2283 1310 2284 [[package]] ··· 1338 2312 checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 1339 2313 1340 2314 [[package]] 2315 + name = "opaque-debug" 2316 + version = "0.3.1" 2317 + source = "registry+https://github.com/rust-lang/crates.io-index" 2318 + checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" 2319 + 2320 + [[package]] 1341 2321 name = "openssl-probe" 1342 2322 version = "0.1.6" 1343 2323 source = "registry+https://github.com/rust-lang/crates.io-index" 1344 2324 checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 2325 + 2326 + [[package]] 2327 + name = "openssl-probe" 2328 + version = "0.2.1" 2329 + source = "registry+https://github.com/rust-lang/crates.io-index" 2330 + checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" 1345 2331 1346 2332 [[package]] 1347 2333 name = "ouroboros" ··· 1374 2360 checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" 1375 2361 1376 2362 [[package]] 2363 + name = "p256" 2364 + version = "0.13.2" 2365 + source = "registry+https://github.com/rust-lang/crates.io-index" 2366 + checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 2367 + dependencies = [ 2368 + "ecdsa", 2369 + "elliptic-curve", 2370 + "primeorder", 2371 + "sha2 0.10.9", 2372 + ] 2373 + 2374 + [[package]] 2375 + name = "p384" 2376 + version = "0.13.1" 2377 + source = "registry+https://github.com/rust-lang/crates.io-index" 2378 + checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" 2379 + dependencies = [ 2380 + "ecdsa", 2381 + "elliptic-curve", 2382 + "primeorder", 2383 + "sha2 0.10.9", 2384 + ] 2385 + 2386 + [[package]] 2387 + name = "p521" 2388 + version = "0.13.3" 2389 + source = "registry+https://github.com/rust-lang/crates.io-index" 2390 + checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" 2391 + dependencies = [ 2392 + "base16ct", 2393 + "ecdsa", 2394 + "elliptic-curve", 2395 + "primeorder", 2396 + "rand_core 0.6.4", 2397 + "sha2 0.10.9", 2398 + ] 2399 + 2400 + [[package]] 2401 + name = "papaya" 2402 + version = "0.2.3" 2403 + source = "registry+https://github.com/rust-lang/crates.io-index" 2404 + checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f" 2405 + dependencies = [ 2406 + "equivalent", 2407 + "seize", 2408 + ] 2409 + 2410 + [[package]] 1377 2411 name = "parking_lot" 1378 2412 version = "0.12.5" 1379 2413 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1397 2431 ] 1398 2432 1399 2433 [[package]] 2434 + name = "password-hash" 2435 + version = "0.5.0" 2436 + source = "registry+https://github.com/rust-lang/crates.io-index" 2437 + checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" 2438 + dependencies = [ 2439 + "base64ct", 2440 + "rand_core 0.6.4", 2441 + "subtle", 2442 + ] 2443 + 2444 + [[package]] 2445 + name = "pastey" 2446 + version = "0.1.1" 2447 + source = "registry+https://github.com/rust-lang/crates.io-index" 2448 + checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" 2449 + 2450 + [[package]] 2451 + name = "pem-rfc7468" 2452 + version = "0.7.0" 2453 + source = "registry+https://github.com/rust-lang/crates.io-index" 2454 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 2455 + dependencies = [ 2456 + "base64ct", 2457 + ] 2458 + 2459 + [[package]] 1400 2460 name = "percent-encoding" 1401 2461 version = "2.3.2" 1402 2462 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1501 2561 "log", 1502 2562 "nix", 1503 2563 "once_cell", 1504 - "openssl-probe", 2564 + "openssl-probe 0.1.6", 1505 2565 "ouroboros", 1506 2566 "parking_lot", 1507 2567 "percent-encoding", ··· 1666 2726 "pingora-error", 1667 2727 "ring", 1668 2728 "rustls", 1669 - "rustls-native-certs", 2729 + "rustls-native-certs 0.7.3", 1670 2730 "rustls-pemfile", 1671 2731 "rustls-pki-types", 1672 2732 "tokio-rustls", ··· 1686 2746 ] 1687 2747 1688 2748 [[package]] 2749 + name = "pkcs1" 2750 + version = "0.7.5" 2751 + source = "registry+https://github.com/rust-lang/crates.io-index" 2752 + checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 2753 + dependencies = [ 2754 + "der", 2755 + "pkcs8", 2756 + "spki", 2757 + ] 2758 + 2759 + [[package]] 2760 + name = "pkcs8" 2761 + version = "0.10.2" 2762 + source = "registry+https://github.com/rust-lang/crates.io-index" 2763 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 2764 + dependencies = [ 2765 + "der", 2766 + "spki", 2767 + ] 2768 + 2769 + [[package]] 1689 2770 name = "pkg-config" 1690 2771 version = "0.3.32" 1691 2772 source = "registry+https://github.com/rust-lang/crates.io-index" 1692 2773 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1693 2774 1694 2775 [[package]] 2776 + name = "polyval" 2777 + version = "0.6.2" 2778 + source = "registry+https://github.com/rust-lang/crates.io-index" 2779 + checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" 2780 + dependencies = [ 2781 + "cfg-if", 2782 + "cpufeatures", 2783 + "opaque-debug", 2784 + "universal-hash", 2785 + ] 2786 + 2787 + [[package]] 2788 + name = "portable-atomic" 2789 + version = "1.13.1" 2790 + source = "registry+https://github.com/rust-lang/crates.io-index" 2791 + checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" 2792 + 2793 + [[package]] 2794 + name = "portable-atomic-util" 2795 + version = "0.2.5" 2796 + source = "registry+https://github.com/rust-lang/crates.io-index" 2797 + checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" 2798 + dependencies = [ 2799 + "portable-atomic", 2800 + ] 2801 + 2802 + [[package]] 2803 + name = "postcard" 2804 + version = "1.1.3" 2805 + source = "registry+https://github.com/rust-lang/crates.io-index" 2806 + checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" 2807 + dependencies = [ 2808 + "cobs", 2809 + "heapless", 2810 + "serde", 2811 + ] 2812 + 2813 + [[package]] 2814 + name = "potential_utf" 2815 + version = "0.1.4" 2816 + source = "registry+https://github.com/rust-lang/crates.io-index" 2817 + checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 2818 + dependencies = [ 2819 + "zerovec", 2820 + ] 2821 + 2822 + [[package]] 1695 2823 name = "powerfmt" 1696 2824 version = "0.2.0" 1697 2825 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1714 2842 dependencies = [ 1715 2843 "proc-macro2", 1716 2844 "syn 2.0.114", 2845 + ] 2846 + 2847 + [[package]] 2848 + name = "primeorder" 2849 + version = "0.13.6" 2850 + source = "registry+https://github.com/rust-lang/crates.io-index" 2851 + checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 2852 + dependencies = [ 2853 + "elliptic-curve", 1717 2854 ] 1718 2855 1719 2856 [[package]] ··· 1772 2909 "memchr", 1773 2910 "parking_lot", 1774 2911 "protobuf", 1775 - "thiserror", 2912 + "thiserror 1.0.69", 1776 2913 ] 1777 2914 1778 2915 [[package]] ··· 1866 3003 checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" 1867 3004 1868 3005 [[package]] 3006 + name = "quinn" 3007 + version = "0.11.9" 3008 + source = "registry+https://github.com/rust-lang/crates.io-index" 3009 + checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 3010 + dependencies = [ 3011 + "bytes", 3012 + "cfg_aliases", 3013 + "pin-project-lite", 3014 + "quinn-proto", 3015 + "quinn-udp", 3016 + "rustc-hash", 3017 + "rustls", 3018 + "socket2", 3019 + "thiserror 2.0.18", 3020 + "tokio", 3021 + "tracing", 3022 + "web-time", 3023 + ] 3024 + 3025 + [[package]] 3026 + name = "quinn-proto" 3027 + version = "0.11.13" 3028 + source = "registry+https://github.com/rust-lang/crates.io-index" 3029 + checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" 3030 + dependencies = [ 3031 + "aws-lc-rs", 3032 + "bytes", 3033 + "getrandom 0.3.4", 3034 + "lru-slab", 3035 + "rand 0.9.2", 3036 + "ring", 3037 + "rustc-hash", 3038 + "rustls", 3039 + "rustls-pki-types", 3040 + "slab", 3041 + "thiserror 2.0.18", 3042 + "tinyvec", 3043 + "tracing", 3044 + "web-time", 3045 + ] 3046 + 3047 + [[package]] 3048 + name = "quinn-udp" 3049 + version = "0.5.14" 3050 + source = "registry+https://github.com/rust-lang/crates.io-index" 3051 + checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 3052 + dependencies = [ 3053 + "cfg_aliases", 3054 + "libc", 3055 + "once_cell", 3056 + "socket2", 3057 + "tracing", 3058 + "windows-sys 0.60.2", 3059 + ] 3060 + 3061 + [[package]] 1869 3062 name = "quote" 1870 3063 version = "1.0.44" 1871 3064 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1940 3133 ] 1941 3134 1942 3135 [[package]] 3136 + name = "rand_core" 3137 + version = "0.10.0" 3138 + source = "registry+https://github.com/rust-lang/crates.io-index" 3139 + checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" 3140 + 3141 + [[package]] 1943 3142 name = "redox_syscall" 1944 3143 version = "0.5.18" 1945 3144 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1978 3177 checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" 1979 3178 1980 3179 [[package]] 3180 + name = "reqwest" 3181 + version = "0.13.2" 3182 + source = "registry+https://github.com/rust-lang/crates.io-index" 3183 + checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" 3184 + dependencies = [ 3185 + "base64 0.22.1", 3186 + "bytes", 3187 + "encoding_rs", 3188 + "futures-core", 3189 + "h2", 3190 + "http", 3191 + "http-body", 3192 + "http-body-util", 3193 + "hyper", 3194 + "hyper-rustls", 3195 + "hyper-util", 3196 + "js-sys", 3197 + "log", 3198 + "mime", 3199 + "percent-encoding", 3200 + "pin-project-lite", 3201 + "quinn", 3202 + "rustls", 3203 + "rustls-pki-types", 3204 + "rustls-platform-verifier", 3205 + "serde", 3206 + "serde_json", 3207 + "sync_wrapper", 3208 + "tokio", 3209 + "tokio-rustls", 3210 + "tower", 3211 + "tower-http", 3212 + "tower-service", 3213 + "url", 3214 + "wasm-bindgen", 3215 + "wasm-bindgen-futures", 3216 + "web-sys", 3217 + ] 3218 + 3219 + [[package]] 3220 + name = "rfc6979" 3221 + version = "0.4.0" 3222 + source = "registry+https://github.com/rust-lang/crates.io-index" 3223 + checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 3224 + dependencies = [ 3225 + "hmac 0.12.1", 3226 + "subtle", 3227 + ] 3228 + 3229 + [[package]] 1981 3230 name = "ring" 1982 3231 version = "0.17.14" 1983 3232 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2011 3260 ] 2012 3261 2013 3262 [[package]] 3263 + name = "rsa" 3264 + version = "0.9.10" 3265 + source = "registry+https://github.com/rust-lang/crates.io-index" 3266 + checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" 3267 + dependencies = [ 3268 + "const-oid", 3269 + "digest 0.10.7", 3270 + "num-bigint-dig", 3271 + "num-integer", 3272 + "num-traits", 3273 + "pkcs1", 3274 + "pkcs8", 3275 + "rand_core 0.6.4", 3276 + "sha2 0.10.9", 3277 + "signature", 3278 + "spki", 3279 + "subtle", 3280 + "zeroize", 3281 + ] 3282 + 3283 + [[package]] 2014 3284 name = "rust_decimal" 2015 3285 version = "1.40.0" 2016 3286 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2025 3295 version = "0.1.27" 2026 3296 source = "registry+https://github.com/rust-lang/crates.io-index" 2027 3297 checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" 3298 + 3299 + [[package]] 3300 + name = "rustc-hash" 3301 + version = "2.1.1" 3302 + source = "registry+https://github.com/rust-lang/crates.io-index" 3303 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 2028 3304 2029 3305 [[package]] 2030 3306 name = "rustc_version" ··· 2078 3354 source = "registry+https://github.com/rust-lang/crates.io-index" 2079 3355 checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" 2080 3356 dependencies = [ 2081 - "openssl-probe", 3357 + "openssl-probe 0.1.6", 2082 3358 "rustls-pemfile", 2083 3359 "rustls-pki-types", 2084 3360 "schannel", 2085 - "security-framework", 3361 + "security-framework 2.11.1", 3362 + ] 3363 + 3364 + [[package]] 3365 + name = "rustls-native-certs" 3366 + version = "0.8.3" 3367 + source = "registry+https://github.com/rust-lang/crates.io-index" 3368 + checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" 3369 + dependencies = [ 3370 + "openssl-probe 0.2.1", 3371 + "rustls-pki-types", 3372 + "schannel", 3373 + "security-framework 3.5.1", 2086 3374 ] 2087 3375 2088 3376 [[package]] ··· 2100 3388 source = "registry+https://github.com/rust-lang/crates.io-index" 2101 3389 checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" 2102 3390 dependencies = [ 3391 + "web-time", 2103 3392 "zeroize", 2104 3393 ] 2105 3394 2106 3395 [[package]] 3396 + name = "rustls-platform-verifier" 3397 + version = "0.6.2" 3398 + source = "registry+https://github.com/rust-lang/crates.io-index" 3399 + checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" 3400 + dependencies = [ 3401 + "core-foundation 0.10.1", 3402 + "core-foundation-sys", 3403 + "jni", 3404 + "log", 3405 + "once_cell", 3406 + "rustls", 3407 + "rustls-native-certs 0.8.3", 3408 + "rustls-platform-verifier-android", 3409 + "rustls-webpki", 3410 + "security-framework 3.5.1", 3411 + "security-framework-sys", 3412 + "webpki-root-certs", 3413 + "windows-sys 0.61.2", 3414 + ] 3415 + 3416 + [[package]] 3417 + name = "rustls-platform-verifier-android" 3418 + version = "0.1.1" 3419 + source = "registry+https://github.com/rust-lang/crates.io-index" 3420 + checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" 3421 + 3422 + [[package]] 2107 3423 name = "rustls-webpki" 2108 3424 version = "0.103.9" 2109 3425 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2128 3444 checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" 2129 3445 2130 3446 [[package]] 3447 + name = "same-file" 3448 + version = "1.0.6" 3449 + source = "registry+https://github.com/rust-lang/crates.io-index" 3450 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 3451 + dependencies = [ 3452 + "winapi-util", 3453 + ] 3454 + 3455 + [[package]] 2131 3456 name = "schannel" 2132 3457 version = "0.1.28" 2133 3458 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2143 3468 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 2144 3469 2145 3470 [[package]] 3471 + name = "sec1" 3472 + version = "0.7.3" 3473 + source = "registry+https://github.com/rust-lang/crates.io-index" 3474 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 3475 + dependencies = [ 3476 + "base16ct", 3477 + "der", 3478 + "generic-array", 3479 + "pkcs8", 3480 + "subtle", 3481 + "zeroize", 3482 + ] 3483 + 3484 + [[package]] 2146 3485 name = "security-framework" 2147 3486 version = "2.11.1" 2148 3487 source = "registry+https://github.com/rust-lang/crates.io-index" 2149 3488 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2150 3489 dependencies = [ 2151 3490 "bitflags 2.10.0", 2152 - "core-foundation", 3491 + "core-foundation 0.9.4", 3492 + "core-foundation-sys", 3493 + "libc", 3494 + "security-framework-sys", 3495 + ] 3496 + 3497 + [[package]] 3498 + name = "security-framework" 3499 + version = "3.5.1" 3500 + source = "registry+https://github.com/rust-lang/crates.io-index" 3501 + checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" 3502 + dependencies = [ 3503 + "bitflags 2.10.0", 3504 + "core-foundation 0.10.1", 2153 3505 "core-foundation-sys", 2154 3506 "libc", 2155 3507 "security-framework-sys", ··· 2163 3515 dependencies = [ 2164 3516 "core-foundation-sys", 2165 3517 "libc", 3518 + ] 3519 + 3520 + [[package]] 3521 + name = "seize" 3522 + version = "0.5.1" 3523 + source = "registry+https://github.com/rust-lang/crates.io-index" 3524 + checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" 3525 + dependencies = [ 3526 + "libc", 3527 + "windows-sys 0.61.2", 2166 3528 ] 2167 3529 2168 3530 [[package]] ··· 2202 3564 ] 2203 3565 2204 3566 [[package]] 3567 + name = "serde_html_form" 3568 + version = "0.4.0" 3569 + source = "registry+https://github.com/rust-lang/crates.io-index" 3570 + checksum = "0946d52b4b7e28823148aebbeceb901012c595ad737920d504fa8634bb099e6f" 3571 + dependencies = [ 3572 + "form_urlencoded", 3573 + "indexmap 2.13.0", 3574 + "itoa", 3575 + "ryu", 3576 + "serde_core", 3577 + ] 3578 + 3579 + [[package]] 3580 + name = "serde_json" 3581 + version = "1.0.149" 3582 + source = "registry+https://github.com/rust-lang/crates.io-index" 3583 + checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" 3584 + dependencies = [ 3585 + "itoa", 3586 + "memchr", 3587 + "serde", 3588 + "serde_core", 3589 + "zmij", 3590 + ] 3591 + 3592 + [[package]] 2205 3593 name = "serde_yaml" 2206 3594 version = "0.9.34+deprecated" 2207 3595 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2220 3608 source = "registry+https://github.com/rust-lang/crates.io-index" 2221 3609 checksum = "3fa1f336066b758b7c9df34ed049c0e693a426afe2b27ff7d5b14f410ab1a132" 2222 3610 dependencies = [ 2223 - "base64", 3611 + "base64 0.22.1", 2224 3612 "indexmap 2.13.0", 2225 3613 "rust_decimal", 2226 3614 ] 2227 3615 2228 3616 [[package]] 3617 + name = "sha1" 3618 + version = "0.10.6" 3619 + source = "registry+https://github.com/rust-lang/crates.io-index" 3620 + checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 3621 + dependencies = [ 3622 + "cfg-if", 3623 + "cpufeatures", 3624 + "digest 0.10.7", 3625 + ] 3626 + 3627 + [[package]] 3628 + name = "sha2" 3629 + version = "0.10.9" 3630 + source = "registry+https://github.com/rust-lang/crates.io-index" 3631 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 3632 + dependencies = [ 3633 + "cfg-if", 3634 + "cpufeatures", 3635 + "digest 0.10.7", 3636 + ] 3637 + 3638 + [[package]] 3639 + name = "sha2" 3640 + version = "0.11.0-rc.5" 3641 + source = "registry+https://github.com/rust-lang/crates.io-index" 3642 + checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" 3643 + dependencies = [ 3644 + "cfg-if", 3645 + "cpufeatures", 3646 + "digest 0.11.0", 3647 + ] 3648 + 3649 + [[package]] 2229 3650 name = "sharded-slab" 2230 3651 version = "0.1.7" 2231 3652 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2251 3672 ] 2252 3673 2253 3674 [[package]] 3675 + name = "signature" 3676 + version = "2.2.0" 3677 + source = "registry+https://github.com/rust-lang/crates.io-index" 3678 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 3679 + dependencies = [ 3680 + "digest 0.10.7", 3681 + "rand_core 0.6.4", 3682 + ] 3683 + 3684 + [[package]] 2254 3685 name = "simd-adler32" 2255 3686 version = "0.3.8" 2256 3687 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2279 3710 ] 2280 3711 2281 3712 [[package]] 3713 + name = "spin" 3714 + version = "0.9.8" 3715 + source = "registry+https://github.com/rust-lang/crates.io-index" 3716 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 3717 + dependencies = [ 3718 + "lock_api", 3719 + ] 3720 + 3721 + [[package]] 3722 + name = "spki" 3723 + version = "0.7.3" 3724 + source = "registry+https://github.com/rust-lang/crates.io-index" 3725 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 3726 + dependencies = [ 3727 + "base64ct", 3728 + "der", 3729 + ] 3730 + 3731 + [[package]] 3732 + name = "stable_deref_trait" 3733 + version = "1.2.1" 3734 + source = "registry+https://github.com/rust-lang/crates.io-index" 3735 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 3736 + 3737 + [[package]] 2282 3738 name = "static_assertions" 2283 3739 version = "1.1.0" 2284 3740 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2341 3797 ] 2342 3798 2343 3799 [[package]] 3800 + name = "sync_wrapper" 3801 + version = "1.0.2" 3802 + source = "registry+https://github.com/rust-lang/crates.io-index" 3803 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 3804 + dependencies = [ 3805 + "futures-core", 3806 + ] 3807 + 3808 + [[package]] 2344 3809 name = "synstructure" 2345 3810 version = "0.13.2" 2346 3811 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2352 3817 ] 2353 3818 2354 3819 [[package]] 3820 + name = "system-configuration" 3821 + version = "0.7.0" 3822 + source = "registry+https://github.com/rust-lang/crates.io-index" 3823 + checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" 3824 + dependencies = [ 3825 + "bitflags 2.10.0", 3826 + "core-foundation 0.9.4", 3827 + "system-configuration-sys", 3828 + ] 3829 + 3830 + [[package]] 3831 + name = "system-configuration-sys" 3832 + version = "0.6.0" 3833 + source = "registry+https://github.com/rust-lang/crates.io-index" 3834 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 3835 + dependencies = [ 3836 + "core-foundation-sys", 3837 + "libc", 3838 + ] 3839 + 3840 + [[package]] 2355 3841 name = "tempfile" 2356 3842 version = "3.24.0" 2357 3843 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2370 3856 source = "registry+https://github.com/rust-lang/crates.io-index" 2371 3857 checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2372 3858 dependencies = [ 2373 - "thiserror-impl", 3859 + "thiserror-impl 1.0.69", 3860 + ] 3861 + 3862 + [[package]] 3863 + name = "thiserror" 3864 + version = "2.0.18" 3865 + source = "registry+https://github.com/rust-lang/crates.io-index" 3866 + checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" 3867 + dependencies = [ 3868 + "thiserror-impl 2.0.18", 2374 3869 ] 2375 3870 2376 3871 [[package]] ··· 2378 3873 version = "1.0.69" 2379 3874 source = "registry+https://github.com/rust-lang/crates.io-index" 2380 3875 checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 3876 + dependencies = [ 3877 + "proc-macro2", 3878 + "quote", 3879 + "syn 2.0.114", 3880 + ] 3881 + 3882 + [[package]] 3883 + name = "thiserror-impl" 3884 + version = "2.0.18" 3885 + source = "registry+https://github.com/rust-lang/crates.io-index" 3886 + checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" 2381 3887 dependencies = [ 2382 3888 "proc-macro2", 2383 3889 "quote", ··· 2435 3941 ] 2436 3942 2437 3943 [[package]] 3944 + name = "tinystr" 3945 + version = "0.8.2" 3946 + source = "registry+https://github.com/rust-lang/crates.io-index" 3947 + checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 3948 + dependencies = [ 3949 + "displaydoc", 3950 + "zerovec", 3951 + ] 3952 + 3953 + [[package]] 3954 + name = "tinyvec" 3955 + version = "1.10.0" 3956 + source = "registry+https://github.com/rust-lang/crates.io-index" 3957 + checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 3958 + dependencies = [ 3959 + "tinyvec_macros", 3960 + ] 3961 + 3962 + [[package]] 3963 + name = "tinyvec_macros" 3964 + version = "0.1.1" 3965 + source = "registry+https://github.com/rust-lang/crates.io-index" 3966 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 3967 + 3968 + [[package]] 3969 + name = "tls_codec" 3970 + version = "0.4.2" 3971 + source = "registry+https://github.com/rust-lang/crates.io-index" 3972 + checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" 3973 + dependencies = [ 3974 + "tls_codec_derive", 3975 + "zeroize", 3976 + ] 3977 + 3978 + [[package]] 3979 + name = "tls_codec_derive" 3980 + version = "0.4.2" 3981 + source = "registry+https://github.com/rust-lang/crates.io-index" 3982 + checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" 3983 + dependencies = [ 3984 + "proc-macro2", 3985 + "quote", 3986 + "syn 2.0.114", 3987 + ] 3988 + 3989 + [[package]] 2438 3990 name = "tokio" 2439 3991 version = "1.49.0" 2440 3992 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2507 4059 ] 2508 4060 2509 4061 [[package]] 4062 + name = "tower" 4063 + version = "0.5.3" 4064 + source = "registry+https://github.com/rust-lang/crates.io-index" 4065 + checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" 4066 + dependencies = [ 4067 + "futures-core", 4068 + "futures-util", 4069 + "pin-project-lite", 4070 + "sync_wrapper", 4071 + "tokio", 4072 + "tower-layer", 4073 + "tower-service", 4074 + ] 4075 + 4076 + [[package]] 4077 + name = "tower-http" 4078 + version = "0.6.8" 4079 + source = "registry+https://github.com/rust-lang/crates.io-index" 4080 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 4081 + dependencies = [ 4082 + "bitflags 2.10.0", 4083 + "bytes", 4084 + "futures-util", 4085 + "http", 4086 + "http-body", 4087 + "iri-string", 4088 + "pin-project-lite", 4089 + "tower", 4090 + "tower-layer", 4091 + "tower-service", 4092 + ] 4093 + 4094 + [[package]] 4095 + name = "tower-layer" 4096 + version = "0.3.3" 4097 + source = "registry+https://github.com/rust-lang/crates.io-index" 4098 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 4099 + 4100 + [[package]] 4101 + name = "tower-service" 4102 + version = "0.3.3" 4103 + source = "registry+https://github.com/rust-lang/crates.io-index" 4104 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 4105 + 4106 + [[package]] 2510 4107 name = "tracing" 2511 4108 version = "0.1.44" 2512 4109 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2597 4194 ] 2598 4195 2599 4196 [[package]] 4197 + name = "try-lock" 4198 + version = "0.2.5" 4199 + source = "registry+https://github.com/rust-lang/crates.io-index" 4200 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 4201 + 4202 + [[package]] 2600 4203 name = "typenum" 2601 4204 version = "1.19.0" 2602 4205 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2615 4218 checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" 2616 4219 2617 4220 [[package]] 4221 + name = "unicode-xid" 4222 + version = "0.2.6" 4223 + source = "registry+https://github.com/rust-lang/crates.io-index" 4224 + checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 4225 + 4226 + [[package]] 4227 + name = "universal-hash" 4228 + version = "0.5.1" 4229 + source = "registry+https://github.com/rust-lang/crates.io-index" 4230 + checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" 4231 + dependencies = [ 4232 + "crypto-common 0.1.7", 4233 + "subtle", 4234 + ] 4235 + 4236 + [[package]] 2618 4237 name = "unsafe-libyaml" 2619 4238 version = "0.2.11" 2620 4239 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2627 4246 checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2628 4247 2629 4248 [[package]] 4249 + name = "url" 4250 + version = "2.5.8" 4251 + source = "registry+https://github.com/rust-lang/crates.io-index" 4252 + checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" 4253 + dependencies = [ 4254 + "form_urlencoded", 4255 + "idna", 4256 + "percent-encoding", 4257 + "serde", 4258 + "serde_derive", 4259 + ] 4260 + 4261 + [[package]] 4262 + name = "utf8_iter" 4263 + version = "1.0.4" 4264 + source = "registry+https://github.com/rust-lang/crates.io-index" 4265 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 4266 + 4267 + [[package]] 2630 4268 name = "utf8parse" 2631 4269 version = "0.2.2" 2632 4270 source = "registry+https://github.com/rust-lang/crates.io-index" 2633 4271 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2634 4272 2635 4273 [[package]] 4274 + name = "uuid" 4275 + version = "1.21.0" 4276 + source = "registry+https://github.com/rust-lang/crates.io-index" 4277 + checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" 4278 + dependencies = [ 4279 + "getrandom 0.4.1", 4280 + "js-sys", 4281 + "serde_core", 4282 + "wasm-bindgen", 4283 + ] 4284 + 4285 + [[package]] 2636 4286 name = "valuable" 2637 4287 version = "0.1.1" 2638 4288 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2645 4295 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2646 4296 2647 4297 [[package]] 4298 + name = "walkdir" 4299 + version = "2.5.0" 4300 + source = "registry+https://github.com/rust-lang/crates.io-index" 4301 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 4302 + dependencies = [ 4303 + "same-file", 4304 + "winapi-util", 4305 + ] 4306 + 4307 + [[package]] 4308 + name = "want" 4309 + version = "0.3.1" 4310 + source = "registry+https://github.com/rust-lang/crates.io-index" 4311 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 4312 + dependencies = [ 4313 + "try-lock", 4314 + ] 4315 + 4316 + [[package]] 2648 4317 name = "wasi" 2649 4318 version = "0.11.1+wasi-snapshot-preview1" 2650 4319 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2660 4329 ] 2661 4330 2662 4331 [[package]] 4332 + name = "wasip3" 4333 + version = "0.4.0+wasi-0.3.0-rc-2026-01-06" 4334 + source = "registry+https://github.com/rust-lang/crates.io-index" 4335 + checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" 4336 + dependencies = [ 4337 + "wit-bindgen", 4338 + ] 4339 + 4340 + [[package]] 4341 + name = "wasm-bindgen" 4342 + version = "0.2.108" 4343 + source = "registry+https://github.com/rust-lang/crates.io-index" 4344 + checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" 4345 + dependencies = [ 4346 + "cfg-if", 4347 + "once_cell", 4348 + "rustversion", 4349 + "wasm-bindgen-macro", 4350 + "wasm-bindgen-shared", 4351 + ] 4352 + 4353 + [[package]] 4354 + name = "wasm-bindgen-futures" 4355 + version = "0.4.58" 4356 + source = "registry+https://github.com/rust-lang/crates.io-index" 4357 + checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" 4358 + dependencies = [ 4359 + "cfg-if", 4360 + "futures-util", 4361 + "js-sys", 4362 + "once_cell", 4363 + "wasm-bindgen", 4364 + "web-sys", 4365 + ] 4366 + 4367 + [[package]] 4368 + name = "wasm-bindgen-macro" 4369 + version = "0.2.108" 4370 + source = "registry+https://github.com/rust-lang/crates.io-index" 4371 + checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" 4372 + dependencies = [ 4373 + "quote", 4374 + "wasm-bindgen-macro-support", 4375 + ] 4376 + 4377 + [[package]] 4378 + name = "wasm-bindgen-macro-support" 4379 + version = "0.2.108" 4380 + source = "registry+https://github.com/rust-lang/crates.io-index" 4381 + checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" 4382 + dependencies = [ 4383 + "bumpalo", 4384 + "proc-macro2", 4385 + "quote", 4386 + "syn 2.0.114", 4387 + "wasm-bindgen-shared", 4388 + ] 4389 + 4390 + [[package]] 4391 + name = "wasm-bindgen-shared" 4392 + version = "0.2.108" 4393 + source = "registry+https://github.com/rust-lang/crates.io-index" 4394 + checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" 4395 + dependencies = [ 4396 + "unicode-ident", 4397 + ] 4398 + 4399 + [[package]] 4400 + name = "wasm-encoder" 4401 + version = "0.244.0" 4402 + source = "registry+https://github.com/rust-lang/crates.io-index" 4403 + checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" 4404 + dependencies = [ 4405 + "leb128fmt", 4406 + "wasmparser", 4407 + ] 4408 + 4409 + [[package]] 4410 + name = "wasm-metadata" 4411 + version = "0.244.0" 4412 + source = "registry+https://github.com/rust-lang/crates.io-index" 4413 + checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" 4414 + dependencies = [ 4415 + "anyhow", 4416 + "indexmap 2.13.0", 4417 + "wasm-encoder", 4418 + "wasmparser", 4419 + ] 4420 + 4421 + [[package]] 4422 + name = "wasmparser" 4423 + version = "0.244.0" 4424 + source = "registry+https://github.com/rust-lang/crates.io-index" 4425 + checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" 4426 + dependencies = [ 4427 + "bitflags 2.10.0", 4428 + "hashbrown 0.15.5", 4429 + "indexmap 2.13.0", 4430 + "semver", 4431 + ] 4432 + 4433 + [[package]] 4434 + name = "web-sys" 4435 + version = "0.3.85" 4436 + source = "registry+https://github.com/rust-lang/crates.io-index" 4437 + checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" 4438 + dependencies = [ 4439 + "js-sys", 4440 + "wasm-bindgen", 4441 + ] 4442 + 4443 + [[package]] 4444 + name = "web-time" 4445 + version = "1.1.0" 4446 + source = "registry+https://github.com/rust-lang/crates.io-index" 4447 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 4448 + dependencies = [ 4449 + "js-sys", 4450 + "wasm-bindgen", 4451 + ] 4452 + 4453 + [[package]] 4454 + name = "webpki-root-certs" 4455 + version = "1.0.6" 4456 + source = "registry+https://github.com/rust-lang/crates.io-index" 4457 + checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" 4458 + dependencies = [ 4459 + "rustls-pki-types", 4460 + ] 4461 + 4462 + [[package]] 4463 + name = "winapi-util" 4464 + version = "0.1.11" 4465 + source = "registry+https://github.com/rust-lang/crates.io-index" 4466 + checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 4467 + dependencies = [ 4468 + "windows-sys 0.61.2", 4469 + ] 4470 + 4471 + [[package]] 2663 4472 name = "windows-link" 2664 4473 version = "0.2.1" 2665 4474 source = "registry+https://github.com/rust-lang/crates.io-index" 2666 4475 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 2667 4476 2668 4477 [[package]] 4478 + name = "windows-registry" 4479 + version = "0.6.1" 4480 + source = "registry+https://github.com/rust-lang/crates.io-index" 4481 + checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" 4482 + dependencies = [ 4483 + "windows-link", 4484 + "windows-result", 4485 + "windows-strings", 4486 + ] 4487 + 4488 + [[package]] 4489 + name = "windows-result" 4490 + version = "0.4.1" 4491 + source = "registry+https://github.com/rust-lang/crates.io-index" 4492 + checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 4493 + dependencies = [ 4494 + "windows-link", 4495 + ] 4496 + 4497 + [[package]] 4498 + name = "windows-strings" 4499 + version = "0.5.1" 4500 + source = "registry+https://github.com/rust-lang/crates.io-index" 4501 + checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 4502 + dependencies = [ 4503 + "windows-link", 4504 + ] 4505 + 4506 + [[package]] 4507 + name = "windows-sys" 4508 + version = "0.45.0" 4509 + source = "registry+https://github.com/rust-lang/crates.io-index" 4510 + checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 4511 + dependencies = [ 4512 + "windows-targets 0.42.2", 4513 + ] 4514 + 4515 + [[package]] 2669 4516 name = "windows-sys" 2670 4517 version = "0.52.0" 2671 4518 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2703 4550 2704 4551 [[package]] 2705 4552 name = "windows-targets" 4553 + version = "0.42.2" 4554 + source = "registry+https://github.com/rust-lang/crates.io-index" 4555 + checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 4556 + dependencies = [ 4557 + "windows_aarch64_gnullvm 0.42.2", 4558 + "windows_aarch64_msvc 0.42.2", 4559 + "windows_i686_gnu 0.42.2", 4560 + "windows_i686_msvc 0.42.2", 4561 + "windows_x86_64_gnu 0.42.2", 4562 + "windows_x86_64_gnullvm 0.42.2", 4563 + "windows_x86_64_msvc 0.42.2", 4564 + ] 4565 + 4566 + [[package]] 4567 + name = "windows-targets" 2706 4568 version = "0.52.6" 2707 4569 source = "registry+https://github.com/rust-lang/crates.io-index" 2708 4570 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" ··· 2736 4598 2737 4599 [[package]] 2738 4600 name = "windows_aarch64_gnullvm" 4601 + version = "0.42.2" 4602 + source = "registry+https://github.com/rust-lang/crates.io-index" 4603 + checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 4604 + 4605 + [[package]] 4606 + name = "windows_aarch64_gnullvm" 2739 4607 version = "0.52.6" 2740 4608 source = "registry+https://github.com/rust-lang/crates.io-index" 2741 4609 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" ··· 2745 4613 version = "0.53.1" 2746 4614 source = "registry+https://github.com/rust-lang/crates.io-index" 2747 4615 checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 4616 + 4617 + [[package]] 4618 + name = "windows_aarch64_msvc" 4619 + version = "0.42.2" 4620 + source = "registry+https://github.com/rust-lang/crates.io-index" 4621 + checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 2748 4622 2749 4623 [[package]] 2750 4624 name = "windows_aarch64_msvc" ··· 2760 4634 2761 4635 [[package]] 2762 4636 name = "windows_i686_gnu" 4637 + version = "0.42.2" 4638 + source = "registry+https://github.com/rust-lang/crates.io-index" 4639 + checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 4640 + 4641 + [[package]] 4642 + name = "windows_i686_gnu" 2763 4643 version = "0.52.6" 2764 4644 source = "registry+https://github.com/rust-lang/crates.io-index" 2765 4645 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" ··· 2784 4664 2785 4665 [[package]] 2786 4666 name = "windows_i686_msvc" 4667 + version = "0.42.2" 4668 + source = "registry+https://github.com/rust-lang/crates.io-index" 4669 + checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 4670 + 4671 + [[package]] 4672 + name = "windows_i686_msvc" 2787 4673 version = "0.52.6" 2788 4674 source = "registry+https://github.com/rust-lang/crates.io-index" 2789 4675 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" ··· 2796 4682 2797 4683 [[package]] 2798 4684 name = "windows_x86_64_gnu" 4685 + version = "0.42.2" 4686 + source = "registry+https://github.com/rust-lang/crates.io-index" 4687 + checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 4688 + 4689 + [[package]] 4690 + name = "windows_x86_64_gnu" 2799 4691 version = "0.52.6" 2800 4692 source = "registry+https://github.com/rust-lang/crates.io-index" 2801 4693 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" ··· 2808 4700 2809 4701 [[package]] 2810 4702 name = "windows_x86_64_gnullvm" 4703 + version = "0.42.2" 4704 + source = "registry+https://github.com/rust-lang/crates.io-index" 4705 + checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 4706 + 4707 + [[package]] 4708 + name = "windows_x86_64_gnullvm" 2811 4709 version = "0.52.6" 2812 4710 source = "registry+https://github.com/rust-lang/crates.io-index" 2813 4711 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" ··· 2820 4718 2821 4719 [[package]] 2822 4720 name = "windows_x86_64_msvc" 4721 + version = "0.42.2" 4722 + source = "registry+https://github.com/rust-lang/crates.io-index" 4723 + checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 4724 + 4725 + [[package]] 4726 + name = "windows_x86_64_msvc" 2823 4727 version = "0.52.6" 2824 4728 source = "registry+https://github.com/rust-lang/crates.io-index" 2825 4729 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" ··· 2835 4739 version = "0.51.0" 2836 4740 source = "registry+https://github.com/rust-lang/crates.io-index" 2837 4741 checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" 4742 + dependencies = [ 4743 + "wit-bindgen-rust-macro", 4744 + ] 4745 + 4746 + [[package]] 4747 + name = "wit-bindgen-core" 4748 + version = "0.51.0" 4749 + source = "registry+https://github.com/rust-lang/crates.io-index" 4750 + checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" 4751 + dependencies = [ 4752 + "anyhow", 4753 + "heck 0.5.0", 4754 + "wit-parser", 4755 + ] 4756 + 4757 + [[package]] 4758 + name = "wit-bindgen-rust" 4759 + version = "0.51.0" 4760 + source = "registry+https://github.com/rust-lang/crates.io-index" 4761 + checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" 4762 + dependencies = [ 4763 + "anyhow", 4764 + "heck 0.5.0", 4765 + "indexmap 2.13.0", 4766 + "prettyplease", 4767 + "syn 2.0.114", 4768 + "wasm-metadata", 4769 + "wit-bindgen-core", 4770 + "wit-component", 4771 + ] 4772 + 4773 + [[package]] 4774 + name = "wit-bindgen-rust-macro" 4775 + version = "0.51.0" 4776 + source = "registry+https://github.com/rust-lang/crates.io-index" 4777 + checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" 4778 + dependencies = [ 4779 + "anyhow", 4780 + "prettyplease", 4781 + "proc-macro2", 4782 + "quote", 4783 + "syn 2.0.114", 4784 + "wit-bindgen-core", 4785 + "wit-bindgen-rust", 4786 + ] 4787 + 4788 + [[package]] 4789 + name = "wit-component" 4790 + version = "0.244.0" 4791 + source = "registry+https://github.com/rust-lang/crates.io-index" 4792 + checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" 4793 + dependencies = [ 4794 + "anyhow", 4795 + "bitflags 2.10.0", 4796 + "indexmap 2.13.0", 4797 + "log", 4798 + "serde", 4799 + "serde_derive", 4800 + "serde_json", 4801 + "wasm-encoder", 4802 + "wasm-metadata", 4803 + "wasmparser", 4804 + "wit-parser", 4805 + ] 4806 + 4807 + [[package]] 4808 + name = "wit-parser" 4809 + version = "0.244.0" 4810 + source = "registry+https://github.com/rust-lang/crates.io-index" 4811 + checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" 4812 + dependencies = [ 4813 + "anyhow", 4814 + "id-arena", 4815 + "indexmap 2.13.0", 4816 + "log", 4817 + "semver", 4818 + "serde", 4819 + "serde_derive", 4820 + "serde_json", 4821 + "unicode-xid", 4822 + "wasmparser", 4823 + ] 4824 + 4825 + [[package]] 4826 + name = "writeable" 4827 + version = "0.6.2" 4828 + source = "registry+https://github.com/rust-lang/crates.io-index" 4829 + checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 4830 + 4831 + [[package]] 4832 + name = "x509-cert" 4833 + version = "0.2.5" 4834 + source = "registry+https://github.com/rust-lang/crates.io-index" 4835 + checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" 4836 + dependencies = [ 4837 + "const-oid", 4838 + "der", 4839 + "sha1", 4840 + "signature", 4841 + "spki", 4842 + "tls_codec", 4843 + ] 2838 4844 2839 4845 [[package]] 2840 4846 name = "x509-parser" ··· 2849 4855 "nom", 2850 4856 "oid-registry", 2851 4857 "rusticata-macros", 2852 - "thiserror", 4858 + "thiserror 1.0.69", 2853 4859 "time", 2854 4860 ] 2855 4861 ··· 2860 4866 checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 2861 4867 2862 4868 [[package]] 4869 + name = "yoke" 4870 + version = "0.8.1" 4871 + source = "registry+https://github.com/rust-lang/crates.io-index" 4872 + checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 4873 + dependencies = [ 4874 + "stable_deref_trait", 4875 + "yoke-derive", 4876 + "zerofrom", 4877 + ] 4878 + 4879 + [[package]] 4880 + name = "yoke-derive" 4881 + version = "0.8.1" 4882 + source = "registry+https://github.com/rust-lang/crates.io-index" 4883 + checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 4884 + dependencies = [ 4885 + "proc-macro2", 4886 + "quote", 4887 + "syn 2.0.114", 4888 + "synstructure", 4889 + ] 4890 + 4891 + [[package]] 2863 4892 name = "zerocopy" 2864 4893 version = "0.8.39" 2865 4894 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2880 4909 ] 2881 4910 2882 4911 [[package]] 4912 + name = "zerofrom" 4913 + version = "0.1.6" 4914 + source = "registry+https://github.com/rust-lang/crates.io-index" 4915 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 4916 + dependencies = [ 4917 + "zerofrom-derive", 4918 + ] 4919 + 4920 + [[package]] 4921 + name = "zerofrom-derive" 4922 + version = "0.1.6" 4923 + source = "registry+https://github.com/rust-lang/crates.io-index" 4924 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 4925 + dependencies = [ 4926 + "proc-macro2", 4927 + "quote", 4928 + "syn 2.0.114", 4929 + "synstructure", 4930 + ] 4931 + 4932 + [[package]] 2883 4933 name = "zeroize" 2884 4934 version = "1.8.2" 2885 4935 source = "registry+https://github.com/rust-lang/crates.io-index" 2886 4936 checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 4937 + dependencies = [ 4938 + "serde", 4939 + "zeroize_derive", 4940 + ] 4941 + 4942 + [[package]] 4943 + name = "zeroize_derive" 4944 + version = "1.4.3" 4945 + source = "registry+https://github.com/rust-lang/crates.io-index" 4946 + checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" 4947 + dependencies = [ 4948 + "proc-macro2", 4949 + "quote", 4950 + "syn 2.0.114", 4951 + ] 4952 + 4953 + [[package]] 4954 + name = "zerotrie" 4955 + version = "0.2.3" 4956 + source = "registry+https://github.com/rust-lang/crates.io-index" 4957 + checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 4958 + dependencies = [ 4959 + "displaydoc", 4960 + "yoke", 4961 + "zerofrom", 4962 + ] 4963 + 4964 + [[package]] 4965 + name = "zerovec" 4966 + version = "0.11.5" 4967 + source = "registry+https://github.com/rust-lang/crates.io-index" 4968 + checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 4969 + dependencies = [ 4970 + "yoke", 4971 + "zerofrom", 4972 + "zerovec-derive", 4973 + ] 4974 + 4975 + [[package]] 4976 + name = "zerovec-derive" 4977 + version = "0.11.2" 4978 + source = "registry+https://github.com/rust-lang/crates.io-index" 4979 + checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 4980 + dependencies = [ 4981 + "proc-macro2", 4982 + "quote", 4983 + "syn 2.0.114", 4984 + ] 4985 + 4986 + [[package]] 4987 + name = "zmij" 4988 + version = "1.0.21" 4989 + source = "registry+https://github.com/rust-lang/crates.io-index" 4990 + checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" 2887 4991 2888 4992 [[package]] 2889 4993 name = "zstd"
+16
Cargo.toml
··· 5 5 6 6 [dependencies] 7 7 async-trait = "0.1.89" 8 + base64 = "0.22.1" 8 9 color-eyre = "0.6.5" 10 + compact_jwt = { version = "0.5.4", features = ["unsafe_release_without_verify"] } 11 + cookie-rs = "0.4.1" 12 + ed25519-dalek = { version = "2.2.0", features = ["rand_core", "serde"] } 13 + form_urlencoded = "1.2.2" 9 14 http = "1.4.0" 15 + jiff = "0.2.20" 16 + papaya = "0.2.3" 10 17 pingora = { version = "0.7.0", features = ["rustls", "proxy", "lb"] } 18 + postcard = { version = "1.1.3" } 11 19 prost = "0.14.3" 12 20 prost-reflect = { version = "0.16.3", features = ["derive", "text-format"] } 21 + rand = "0.8.5" # has to match ed25519-dalek 22 + reqwest = { version = "0.13.2", features = ["json"] } 13 23 rustls = { version = "0.23.36", features = ["aws-lc-rs"] } 24 + serde = { version = "1.0.228", features = ["derive"] } 25 + serde_html_form = "0.4.0" 26 + serde_json = "1.0.149" 27 + sha2 = "0.10.9" 14 28 tokio = { version = "1.49.0", features = ["fs"] } 15 29 tracing = "0.1.44" 16 30 tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } 31 + url = { version = "2.5.8", features = ["serde"] } 32 + uuid = { version = "1.21.0", features = ["serde", "v4"] } 17 33 18 34 [build-dependencies] 19 35 # use prost-reflect-build & prost-reflect so that we can deserialize
+6 -6
flake.lock
··· 2 2 "nodes": { 3 3 "nixpkgs": { 4 4 "locked": { 5 - "lastModified": 1770337958, 6 - "narHash": "sha256-YhLXsYeFN4BUnZBOU6ZAi69ycZMRrDSqEXC81QfwU70=", 5 + "lastModified": 1771119812, 6 + "narHash": "sha256-Nqo9LfbAz6QPEGeEcoA09u89PIbSAxrkXtLVSUq8BHY=", 7 7 "owner": "NixOS", 8 8 "repo": "nixpkgs", 9 - "rev": "f22526981d4d5dcbc8d4c776d09964c9bb7f902b", 9 + "rev": "cd2664195c274d53ba5430006533f3d15ebe1f1f", 10 10 "type": "github" 11 11 }, 12 12 "original": { ··· 42 42 "nixpkgs": "nixpkgs_2" 43 43 }, 44 44 "locked": { 45 - "lastModified": 1770260791, 46 - "narHash": "sha256-ADTBfENFjRVDQMcCycyX/pAy6NFI/Ct6Mrar3gsmXI0=", 45 + "lastModified": 1771125043, 46 + "narHash": "sha256-ldf/s49n6rOAxl7pYLJGGS1N/assoHkCOWdEdLyNZkc=", 47 47 "owner": "oxalica", 48 48 "repo": "rust-overlay", 49 - "rev": "42ec85352e419e601775c57256a52f6d48a39906", 49 + "rev": "4912f951a26dc8142b176be2c2ad834319dc06e8", 50 50 "type": "github" 51 51 }, 52 52 "original": {
+12 -15
src/config/format.proto
··· 9 9 10 10 // bind to tcp ports, with optional tls 11 11 repeated TCPBinding bind_to_tcp = 2; 12 - // bind to unix domain sockets 13 - repeated UDSBinding bind_to_uds = 4; 14 12 15 13 // lower-level pingora config 16 14 Pingora pingora = 3; ··· 19 17 message Domain { 20 18 // require oidc auth if this is set 21 19 optional OIDC oidc_auth = 1; 22 - 23 - // TODO: ACME challenge hosting support natively? 24 20 25 21 // https backends 26 22 repeated HTTPSBackend https = 3; ··· 55 51 56 52 Scopes scopes = 4; 57 53 Claims claims = 5; 54 + 55 + // per oidc core v1-with-errata-2§3.1.3.7 point 6, we _may_ skip validation 56 + // of the id token if it was received over tls. which it will be, in our 57 + // case. some folks may want to be extra paranoid, but generally you either 58 + // trust tls, or you can't trust discovery, and thus can't trust the jwks info, 59 + // so default this to false. 60 + bool validate_with_jwk = 6; 61 + 62 + // where to redirect to on logout 63 + string logout_url = 7; 58 64 } 59 65 60 66 message Scopes { ··· 97 103 // set an `X-Forwarded-Proto`-style header to the original scheme of the request 98 104 optional string x_forwarded_proto = 3; 99 105 // set an `X-Real-IP`-style header (i.e. _just_ the remote address) 100 - repeated string remote_addr = 4; 106 + optional string remote_addr = 4; 101 107 102 108 // always clear these headers 103 109 repeated string always_clear = 5; ··· 143 149 // tls, if desired 144 150 optional TLS tls = 2; 145 151 146 - // TODO: surface tcp options from pingora 147 - } 148 - message UDSBinding { 149 - message Permissions { 150 - uint32 mode = 1; 151 - } 152 - // socket path 153 - string path = 1; 154 - // permissions to set on the socket path 155 - optional Permissions permissions = 2; 152 + // TODO(feature): surface tcp options from pingora 156 153 }
+85
src/cookies.rs
··· 1 + //! # Cookie handling 2 + //! 3 + //! cookies are stored as postcard-encoded ed25519-signed tokens, [Protocol Buffer Tokens], which 4 + //! this is inspired by. They contain session ids, _not_ the actual access token, which are stored 5 + //! in an in-memory session store. 6 + //! 7 + //! the signing key is generated on server start this does mean server restarts invalidate active 8 + //! sessions. this is not a big deal for my current usecase, and we could probably do secret 9 + //! handover or have configurable secrets should the need arise. 10 + //! 11 + //! [Protocol Buffer Tokens]: https://fly.io/blog/api-tokens-a-tedious-survey/ 12 + 13 + use std::marker::PhantomData; 14 + 15 + use base64::Engine as _; 16 + use base64::prelude::BASE64_STANDARD; 17 + use color_eyre::Result; 18 + use serde::de::DeserializeOwned; 19 + use serde::{Deserialize, Serialize}; 20 + 21 + #[derive(Serialize, Deserialize)] 22 + pub struct Signed<MSG> { 23 + signature: ed25519_dalek::Signature, 24 + message: Vec<u8>, 25 + _kind: PhantomData<MSG>, 26 + } 27 + impl<MSG: DeserializeOwned + Versioned> Signed<MSG> { 28 + pub fn contents(raw: &str, key: &ed25519_dalek::SigningKey) -> Result<MSG, ()> { 29 + let raw = BASE64_STANDARD.decode(raw).map_err(drop)?; 30 + // timing threats: technically, someone could try to discover the appropriate shape of our 31 + // tokens by seeing if we early-return from the postcard::from_bytes message. 32 + // this doesn't seem like a huge threat, since we're not encrypting our cookies, so the 33 + // structure is already pretty obvious 34 + 35 + let envelope: Self = postcard::from_bytes(&raw).map_err(drop)?; 36 + key.verify(&envelope.message, &envelope.signature) 37 + .map_err(drop)?; 38 + let (version_num, msg_raw): (u64, _) = 39 + postcard::take_from_bytes(&envelope.message).map_err(drop)?; 40 + // we're past the signature part, so we know this is ours. 41 + // worst we're getting here is an attempt to check if old tokens still work, 42 + // so we don't need to be as stressed about timing sensitivity 43 + if version_num != MSG::VERSION { 44 + return Err(()); 45 + } 46 + postcard::from_bytes(msg_raw).map_err(drop) 47 + } 48 + } 49 + 50 + impl<MSG: Serialize + Versioned> Signed<MSG> { 51 + pub fn sign(msg: MSG, key: &ed25519_dalek::SigningKey) -> Result<String, ()> { 52 + use ed25519_dalek::ed25519::signature::Signer as _; 53 + 54 + let raw = { 55 + let raw = Vec::new(); 56 + let raw = postcard::to_extend(&MSG::VERSION, raw).map_err(drop)?; 57 + postcard::to_extend(&msg, raw).map_err(drop)? 58 + }; 59 + let signature = key.sign(&raw); 60 + let envelope = Self { 61 + signature, 62 + message: raw, 63 + _kind: Default::default(), 64 + }; 65 + let raw = postcard::to_extend(&envelope, Vec::new()).map_err(drop)?; 66 + Ok(BASE64_STANDARD.encode(raw)) 67 + } 68 + } 69 + 70 + pub trait Versioned { 71 + const VERSION: u64; 72 + } 73 + 74 + #[derive(Serialize, Deserialize)] 75 + pub struct CookieMessage { 76 + // NB: per rfc:draft-ietf-oauth-v2-1#7.1.3.4, since we're signing our cookies and not encrypting 77 + // them, we can't store the access token directly. instead we'll store the session id, which 78 + // we'll use to look up the token in a session store. 79 + pub session_id: u64, 80 + } 81 + impl Versioned for CookieMessage { 82 + const VERSION: u64 = 1; 83 + } 84 + 85 + pub type CookieContents = Signed<CookieMessage>;
+669
src/gateway.rs
··· 1 + //! the actual gateway implementation 2 + 3 + use std::collections::HashMap; 4 + use std::ops::ControlFlow; 5 + use std::sync::Arc; 6 + 7 + use async_trait::async_trait; 8 + use cookie_rs::CookieJar; 9 + use http::status::StatusCode; 10 + use http::{HeaderName, HeaderValue}; 11 + use pingora::lb::selection::consistent::KetamaHashing; 12 + use pingora::prelude::*; 13 + use url::Url; 14 + 15 + use crate::gateway::oidc::{InProgressAuth, SESSION_COOKIE_NAME, UserInfo}; 16 + use crate::httputil::{internal_error, internal_error_from, status_error, status_error_from}; 17 + use crate::oauth::auth_code_flow; 18 + use crate::{config, cookies, httputil}; 19 + 20 + pub mod oidc; 21 + 22 + /// per-domain information about backends and such 23 + pub struct DomainInfo { 24 + /// the load balancer to use to select backends 25 + pub balancer: Arc<LoadBalancer<KetamaHashing>>, 26 + /// whether or not we allow insecure connections from clients 27 + pub tls_mode: config::format::domain::TlsMode, 28 + /// the sni name of this domain, used to pass to backends 29 + pub sni_name: String, 30 + /// auth settings for this domain, if any 31 + pub oidc: Option<oidc::Info>, 32 + /// headers to mangle for requests on this domain 33 + pub headers: config::format::ManageHeaders, 34 + } 35 + 36 + /// the actual gateway logic 37 + pub struct AuthGateway { 38 + /// all known domains and their corresponding backends & settings 39 + pub domains: HashMap<String, DomainInfo>, 40 + } 41 + 42 + impl AuthGateway { 43 + /// fetch the domain info for this request 44 + fn domain_info<'s>(&'s self, session: &Session) -> Result<&'s DomainInfo> { 45 + let req = session.req_header(); 46 + // TODO(potential-bug): afaict, right now, afaict, pingora a) does not check that SNI matches the `Host` 47 + // header, b) does not support extracting the SNI info on rustls, so we'll have to switch 48 + // to boringssl and implement that ourselves T_T 49 + let host = req 50 + .headers 51 + .get(http::header::HOST) 52 + .ok_or_else(status_error( 53 + "no host set", 54 + ErrorSource::Downstream, 55 + StatusCode::BAD_REQUEST, 56 + ))? 57 + .to_str() 58 + .map_err(|e| { 59 + Error::because( 60 + ErrorType::HTTPStatus(StatusCode::BAD_REQUEST.into()), 61 + "no host", 62 + e, 63 + ) 64 + })?; 65 + let info = self.domains.get(host).ok_or_else(status_error( 66 + "unknown host", 67 + ErrorSource::Downstream, 68 + StatusCode::SERVICE_UNAVAILABLE, 69 + ))?; 70 + 71 + Ok(info) 72 + } 73 + 74 + /// mangle general headers, per [`config::format::ManageHeaders`] 75 + async fn strip_and_apply_general_headers( 76 + &self, 77 + session: &mut Session, 78 + info: &DomainInfo, 79 + is_https: bool, 80 + ) -> Result<()> { 81 + let remote_addr = session.client_addr().and_then(|addr| match addr { 82 + pingora::protocols::l4::socket::SocketAddr::Inet(socket_addr) => { 83 + Some(socket_addr.ip().to_string()) 84 + } 85 + pingora::protocols::l4::socket::SocketAddr::Unix(_) => None, 86 + }); 87 + let req = session.req_header_mut(); 88 + if let Some(header) = &info.headers.host { 89 + // TODO(cleanup): preprocess all header names 90 + let name = HeaderName::from_bytes(header.as_bytes()) 91 + .map_err(internal_error_from("invalid claim-to-header header name"))?; 92 + let val = req 93 + .headers 94 + .get(http::header::HOST) 95 + .expect("we had to have this to look up our backend") 96 + .clone(); 97 + req.headers.insert(name, val); 98 + } 99 + if let Some(header) = &info.headers.x_forwarded_for 100 + && let Some(addr) = &remote_addr 101 + { 102 + let name = HeaderName::from_bytes(header.as_bytes()) 103 + .map_err(internal_error_from("invalid claim-to-header header name"))?; 104 + let mut val = req 105 + .headers 106 + .get("x-forwarded-for") 107 + .map(|v| v.as_bytes()) 108 + .unwrap_or(b"") 109 + .to_owned(); 110 + val.extend(b","); 111 + val.extend(addr.as_bytes()); 112 + let val = HeaderValue::from_bytes(&val) 113 + .map_err(internal_error_from("invalid remote-addr header value"))?; 114 + req.headers.insert(name, val); 115 + } 116 + if let Some(header) = &info.headers.x_forwarded_proto { 117 + let name = HeaderName::from_bytes(header.as_bytes()) 118 + .map_err(internal_error_from("invalid claim-to-header header name"))?; 119 + req.headers.insert( 120 + name, 121 + HeaderValue::from_static(if is_https { "https" } else { "http" }), 122 + ); 123 + } 124 + if let Some(header) = &info.headers.remote_addr 125 + && let Some(addr) = &remote_addr 126 + { 127 + let name = HeaderName::from_bytes(header.as_bytes()) 128 + .map_err(internal_error_from("invalid claim-to-header header name"))?; 129 + let val = HeaderValue::from_str(addr) 130 + .map_err(internal_error_from("invalid remote-addr header value"))?; 131 + req.headers.insert(name, val); 132 + } 133 + 134 + Ok(()) 135 + } 136 + 137 + /// check auth, starting the flow if necessary 138 + async fn check_auth( 139 + &self, 140 + session: &mut Session, 141 + auth_info: &oidc::Info, 142 + ctx: &mut AuthCtx, 143 + ) -> Result<ControlFlow<()>> { 144 + use auth_code_flow::code_request; 145 + 146 + let req = session.req_header_mut(); 147 + let cookies = httputil::cookie_jar(req)?.unwrap_or_default(); 148 + 149 + let auth_cookie = cookies 150 + .get(SESSION_COOKIE_NAME) 151 + .map(|c| c.value()) 152 + .and_then(|c| cookies::CookieContents::contents(c, &auth_info.cookie_signing_key).ok()); 153 + { 154 + // auth_info map pin 155 + let sessions = auth_info.sessions.pin(); 156 + if let Some(valid_session) = auth_cookie 157 + .and_then(|c| sessions.get(&c.session_id)) 158 + .filter(|sess| sess.expires_at > jiff::Timestamp::now()) 159 + { 160 + if let Some(claim_map) = &auth_info.config.claims { 161 + for (claim, header) in &claim_map.claim_to_header { 162 + match valid_session.claims.get(claim) { 163 + Some(val) => { 164 + let val = HeaderValue::from_bytes(val.as_bytes()) 165 + .map_err(internal_error_from("invalid claim value"))?; 166 + let name = HeaderName::from_bytes(header.as_bytes()).map_err( 167 + internal_error_from("invalid claim-to-header header name"), 168 + )?; 169 + req.headers.insert(name, val) 170 + } 171 + None => req.headers.remove(header), 172 + }; 173 + } 174 + } 175 + 176 + ctx.session_valid = true; 177 + 178 + return Ok(ControlFlow::Continue(())); 179 + } 180 + } 181 + 182 + // otherwise! start the auth flow 183 + let meta_cache = auth_info.get_or_cache_metadata().await?; 184 + 185 + // TODO(cleanup): precompute scopes 186 + let redirect_info = code_request::redirect_to_auth_server( 187 + (&meta_cache.metadata).into(), 188 + code_request::Data::new( 189 + &auth_info.config.client_id, 190 + &auth_info 191 + .config 192 + .scopes 193 + .as_ref() 194 + .map(|s| { 195 + s.required 196 + .iter() 197 + .fold(auth_code_flow::Scopes::base_scopes(), |scopes, scope| { 198 + scopes.add_scope(scope) 199 + }) 200 + }) 201 + .unwrap_or(auth_code_flow::Scopes::base_scopes()), 202 + // technically this is a spec violate, but it's useful for testing 203 + &Url::parse(&format!( 204 + "https://{domain}/{path}", 205 + domain = auth_info.domain, 206 + path = OAUTH_CONTINUE_PATH, 207 + )) 208 + .map_err(internal_error_from( 209 + "unable to construct redirect url from domain", 210 + ))?, 211 + ), 212 + ) 213 + .map_err(internal_error_from("unable to construct redirect"))?; 214 + 215 + if auth_info 216 + .auth_states 217 + .pin() 218 + .try_insert( 219 + redirect_info.state, 220 + InProgressAuth { 221 + code_verifier: redirect_info.code_verifier, 222 + original_path: req.uri.path().to_string(), 223 + }, 224 + ) 225 + .is_err() 226 + { 227 + // this is _extremely_ unlikely to happen, but worth checking anyway 228 + return Err(internal_error("state id collision")()); 229 + }; 230 + 231 + httputil::redirect_response(session, redirect_info.url.as_str(), |_, _| Ok(())).await?; 232 + Ok(ControlFlow::Break(())) 233 + } 234 + 235 + /// continue auth from inbound redirects, or logout from a logout redirect 236 + async fn receive_redirect( 237 + &self, 238 + session: &mut Session, 239 + info: &DomainInfo, 240 + ) -> Result<ControlFlow<()>> { 241 + use auth_code_flow::{code_response, token_request, token_response}; 242 + 243 + let req = session.req_header(); 244 + let Some(pq) = req.uri.path_and_query() else { 245 + return Ok(ControlFlow::Continue(())); 246 + }; 247 + let Some(auth_info) = &info.oidc else { 248 + return Ok(ControlFlow::Continue(())); 249 + }; 250 + 251 + if pq.path() == OAUTH_LOGOUT_PATH { 252 + let Some(mut cookies) = httputil::cookie_jar(req)? else { 253 + // we're not logged in, just return fine 254 + httputil::redirect_response(session, &auth_info.config.logout_url, |_, _| Ok(())) 255 + .await?; 256 + return Ok(ControlFlow::Break(())); 257 + }; 258 + 259 + { 260 + let Some(raw) = cookies 261 + .get(SESSION_COOKIE_NAME) 262 + .map(|raw| raw.value().to_string()) 263 + else { 264 + // we're not logged in, just return fine 265 + httputil::redirect_response(session, &auth_info.config.logout_url, |_, _| { 266 + Ok(()) 267 + }) 268 + .await?; 269 + return Ok(ControlFlow::Break(())); 270 + }; 271 + cookies.remove(SESSION_COOKIE_NAME); 272 + 273 + let Some(cookies::CookieMessage { session_id }) = 274 + cookies::CookieContents::contents(&raw, &auth_info.cookie_signing_key).ok() 275 + else { 276 + // invalid cookie, just ignore 277 + httputil::redirect_response(session, &auth_info.config.logout_url, |_, _| { 278 + Ok(()) 279 + }) 280 + .await?; 281 + return Ok(ControlFlow::Break(())); 282 + }; 283 + auth_info.sessions.pin().remove(&session_id); 284 + }; 285 + 286 + httputil::redirect_response(session, &auth_info.config.logout_url, |_, _| Ok(())) 287 + .await?; 288 + return Ok(ControlFlow::Break(())); 289 + } 290 + 291 + if let Some(auth_info) = &info.oidc 292 + && pq.path().starts_with("/") 293 + && &pq.path()[1..] == OAUTH_CONTINUE_PATH 294 + { 295 + let Some(query) = pq.query() else { 296 + session 297 + .respond_error(StatusCode::BAD_REQUEST.into()) 298 + .await?; 299 + return Ok(ControlFlow::Break(())); 300 + }; 301 + 302 + let Some(meta_cache) = auth_info.meta_cache.lock().await.as_ref().cloned() else { 303 + // if we don't already have discovery metadata, something's real weird, cause 304 + // how did we start the flow 305 + session 306 + .respond_error(StatusCode::BAD_REQUEST.into()) 307 + .await?; 308 + return Ok(ControlFlow::Break(())); 309 + }; 310 + 311 + let status = 312 + code_response::receive_redirect(query, meta_cache.metadata.issuer.as_str()) 313 + .map_err(status_error_from( 314 + "unable to deserialize oauth2 response", 315 + ErrorSource::Internal, 316 + StatusCode::BAD_REQUEST, 317 + ))?; 318 + let resp = match status { 319 + Ok(resp) => resp, 320 + Err(err) => { 321 + auth_info.auth_states.pin().remove(&err.state); 322 + match err.error { 323 + code_response::ErrorType::AccessDenied => { 324 + session.respond_error(StatusCode::FORBIDDEN.into()).await?; 325 + return Ok(ControlFlow::Break(())); 326 + } 327 + code_response::ErrorType::TemporarilyUnavailable => { 328 + session 329 + .respond_error(StatusCode::SERVICE_UNAVAILABLE.into()) 330 + .await?; 331 + return Ok(ControlFlow::Break(())); 332 + } 333 + _ => { 334 + session 335 + .respond_error(StatusCode::INTERNAL_SERVER_ERROR.into()) 336 + .await?; 337 + return Ok(ControlFlow::Break(())); 338 + } 339 + } 340 + } 341 + }; 342 + 343 + let Some(in_progress) = auth_info.auth_states.pin().remove(&resp.state).cloned() else { 344 + session 345 + .respond_error(StatusCode::BAD_REQUEST.into()) 346 + .await?; 347 + return Ok(ControlFlow::Break(())); 348 + }; 349 + 350 + let mut body = String::new(); 351 + let redirect_uri = &format!( 352 + "https://{domain}/{path}", 353 + domain = auth_info.domain, 354 + path = OAUTH_CONTINUE_PATH, 355 + ); 356 + let token_req = token_request::request_access_token( 357 + (&meta_cache.metadata).into(), 358 + token_request::Data { 359 + code: resp, 360 + client_id: &auth_info.config.client_id, 361 + client_secret: &auth_info.client_secret, 362 + // this should not be a clone, but it's a weird quirk of our threadsafe 363 + // hashmap choice 364 + code_verifier: in_progress.code_verifier, 365 + redirect_uri, 366 + }, 367 + &mut body, 368 + ) 369 + .map_err(internal_error_from("unable produce access token request"))?; 370 + 371 + let resp: token_response::Valid = { 372 + let client = reqwest::Client::new(); 373 + let resp = client 374 + .post(token_req.url.as_str().to_string()) 375 + .header( 376 + http::header::CONTENT_TYPE, 377 + "application/x-www-form-urlencoded", 378 + ) 379 + .body(body) 380 + .send() 381 + .await 382 + .map_err(internal_error_from("unable to make token request"))?; 383 + if resp.status() == StatusCode::BAD_REQUEST { 384 + let _resp: token_response::Error = resp 385 + .json() 386 + .await 387 + .map_err(internal_error_from("unable to deserialize response"))?; 388 + session 389 + .respond_error(StatusCode::BAD_REQUEST.into()) 390 + .await?; 391 + return Ok(ControlFlow::Break(())); 392 + // error per [the rfc][ref:draft-ietf-oauth-v2-1#3.2.4] 393 + } else if resp.status() == StatusCode::NOT_FOUND { 394 + // maybe it moved? try fetching the info again later 395 + auth_info.clear_metadata_cache().await; 396 + } else if !resp.status().is_success() { 397 + session 398 + .respond_error(StatusCode::INTERNAL_SERVER_ERROR.into()) 399 + .await?; 400 + return Ok(ControlFlow::Break(())); 401 + } 402 + resp.json() 403 + .await 404 + .map_err(internal_error_from("unable to deserialize token response"))? 405 + }; 406 + 407 + use std::str::FromStr as _; 408 + let id_token = compact_jwt::JwtUnverified::from_str( 409 + &resp 410 + .id_token 411 + .ok_or_else(internal_error("no id token in response"))?, 412 + ) 413 + .map_err(internal_error_from("unable to deserialize id token"))?; 414 + 415 + // will be some if we had the option "verify" turned on, will be none otherwise 416 + // (will never be none if the option is on but we couldn't fetch the token) 417 + let id_token: compact_jwt::Jwt<()> = match &meta_cache.jws_verifier { 418 + Some(verifier) => { 419 + use compact_jwt::JwsVerifier as _; 420 + verifier 421 + .verify(&id_token) 422 + .map_err(internal_error_from("unable to verify id token"))? 423 + } 424 + None => { 425 + use compact_jwt::JwsVerifier as _; 426 + let verifier = 427 + compact_jwt::dangernoverify::JwsDangerReleaseWithoutVerify::default(); 428 + verifier 429 + .verify(&id_token) 430 + .map_err(internal_error_from("unable to deserialize id_token to jwt"))? 431 + } 432 + }; 433 + 434 + // per https://openid.net/specs/openid-connect-core-1_0-final.html#TokenResponseValidation, we _must_ to check 435 + // - iss (must match the expected issuer) 436 + // - aud (must match our client_id) 437 + // - exp (must expire in the future) 438 + if id_token 439 + .iss 440 + .is_none_or(|iss| iss != meta_cache.metadata.issuer.as_str()) 441 + { 442 + return Err(internal_error("issuer mismatch on id token")()); 443 + } 444 + if id_token 445 + .aud 446 + .is_none_or(|aud| aud != auth_info.config.client_id) 447 + { 448 + return Err(internal_error("audience mismatch on id token")()); 449 + } 450 + let expires_at = jiff::Timestamp::from_second( 451 + id_token 452 + .exp 453 + .ok_or_else(internal_error("missing exp on token"))?, 454 + ) 455 + .map_err(internal_error_from("unable to parse exp as timestamp"))?; 456 + if expires_at < jiff::Timestamp::now() { 457 + session 458 + .respond_error(StatusCode::INTERNAL_SERVER_ERROR.into()) 459 + .await?; 460 + return Ok(ControlFlow::Break(())); 461 + } 462 + 463 + let user_info = UserInfo { 464 + expires_at, 465 + claims: id_token 466 + .claims 467 + .into_iter() 468 + // [`serde_json::Value`] implements display that's just "semi-infallible 469 + // serialize" 470 + .map(|(k, v)| (k, v.to_string())) 471 + .collect(), 472 + }; 473 + let expiry = user_info.expires_at; 474 + let mut rng = rand::rngs::OsRng; 475 + use rand::Rng as _; 476 + let session_id = rng.r#gen(); 477 + auth_info.sessions.pin().insert(session_id, user_info); 478 + let cookie = cookies::CookieContents::sign( 479 + cookies::CookieMessage { session_id }, 480 + &auth_info.cookie_signing_key, 481 + ) 482 + .map_err(|()| internal_error("unable to sign cookie")())?; 483 + 484 + let url = format!( 485 + "https://{domain}{original_path}", 486 + domain = auth_info.domain, 487 + original_path = in_progress.original_path 488 + ); 489 + httputil::redirect_response(session, &url, |resp, session| { 490 + let mut cookies = httputil::cookie_jar(session.req_header())?.unwrap_or_default(); 491 + cookies.set( 492 + cookie_rs::Cookie::builder(SESSION_COOKIE_NAME, cookie) 493 + .http_only(true) 494 + .secure(true) 495 + // utc technically potentially different than gmt, but this is just advisory (we enforce 496 + // elsewhere), so it's ok 497 + .max_age( 498 + std::time::Duration::try_from(expiry - jiff::Timestamp::now()) 499 + .expect("formed from timestamps, can't have relative parts"), 500 + ) 501 + .path("/") 502 + .build(), 503 + ); 504 + cookies 505 + .as_header_values() 506 + .into_iter() 507 + .try_for_each(|cookie| { 508 + let val = HeaderValue::from_bytes(cookie.as_bytes()) 509 + .map_err(internal_error_from("bad cookie header value"))?; 510 + 511 + resp.append_header(http::header::SET_COOKIE, val)?; 512 + Ok::<_, Box<Error>>(()) 513 + })?; 514 + Ok(()) 515 + }) 516 + .await?; 517 + return Ok(ControlFlow::Break(())); 518 + } 519 + 520 + Ok(ControlFlow::Continue(())) 521 + } 522 + } 523 + 524 + pub struct AuthCtx { 525 + session_valid: bool, 526 + } 527 + 528 + /// the oauth2 redirect path, without the leading slash 529 + const OAUTH_CONTINUE_PATH: &str = ".oauth2/continue"; 530 + /// the logout/cookie-clear path, _with_ the leading slash 531 + const OAUTH_LOGOUT_PATH: &str = "/.oauth2/logout"; 532 + 533 + #[async_trait] 534 + impl ProxyHttp for AuthGateway { 535 + type CTX = AuthCtx; 536 + fn new_ctx(&self) -> Self::CTX { 537 + AuthCtx { 538 + session_valid: false, 539 + } 540 + } 541 + 542 + async fn request_filter(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result<bool> { 543 + let info = self.domain_info(session)?; 544 + 545 + // check if we need to terminate the connection cause someone sent us an http request and we 546 + // don't allow that 547 + let is_https = session 548 + .digest() 549 + .and_then(|d| d.ssl_digest.as_ref()) 550 + .is_some(); 551 + if !is_https { 552 + use config::format::domain::TlsMode; 553 + match info.tls_mode { 554 + TlsMode::Only => { 555 + // we should just drop the connection, although people should really just be 556 + // using HSTS 557 + session.shutdown().await; 558 + return Ok(true); 559 + } 560 + TlsMode::UnsafeAllowHttp => {} 561 + } 562 + } 563 + 564 + // next, check if we're in the middle of an oauth flow 565 + match self.receive_redirect(session, info).await? { 566 + ControlFlow::Continue(()) => {} 567 + ControlFlow::Break(()) => return Ok(true), 568 + } 569 + 570 + // finally check our actual auth state, starting the auth flow as needed 571 + if let Some(auth_info) = &info.oidc { 572 + match self.check_auth(session, auth_info, ctx).await? { 573 + ControlFlow::Continue(()) => {} 574 + ControlFlow::Break(()) => return Ok(true), 575 + } 576 + } 577 + 578 + // we're past auth and are processing as normal, proceed 579 + self.strip_and_apply_general_headers(session, info, is_https) 580 + .await?; 581 + 582 + Ok(false) 583 + } 584 + 585 + async fn upstream_peer( 586 + &self, 587 + session: &mut Session, 588 + _ctx: &mut Self::CTX, 589 + ) -> Result<Box<HttpPeer>> { 590 + fn client_addr_key(sock_addr: &pingora::protocols::l4::socket::SocketAddr) -> Vec<u8> { 591 + use pingora::protocols::l4::socket::SocketAddr; 592 + match sock_addr { 593 + SocketAddr::Inet(socket_addr) => match socket_addr { 594 + std::net::SocketAddr::V4(v4) => Vec::from(v4.ip().octets()), 595 + std::net::SocketAddr::V6(v6) => Vec::from(v6.ip().octets()), 596 + }, 597 + _ => unreachable!(), 598 + } 599 + } 600 + 601 + let backends = self.domain_info(session)?; 602 + let backend = backends 603 + .balancer 604 + // NB: this means that CGNAT, other proxies, etc will? consistently hit the same 605 + // backend, so we might wanna take that into consideration. fine for now, this is 606 + // currently for personal use ;-) 607 + .select( 608 + &client_addr_key(session.client_addr().ok_or_else(status_error( 609 + "no client address", 610 + ErrorSource::Downstream, 611 + StatusCode::BAD_REQUEST, 612 + ))?), /* lb on client address */ 613 + 256, 614 + ) 615 + .ok_or_else(status_error( 616 + "no available backends", 617 + ErrorSource::Upstream, 618 + StatusCode::SERVICE_UNAVAILABLE, 619 + ))?; 620 + 621 + let needs_tls = backend 622 + .ext 623 + .get::<BackendData>() 624 + .map(|d| d.tls) 625 + .unwrap_or(true); 626 + 627 + Ok(Box::new(HttpPeer::new( 628 + backend, 629 + needs_tls, 630 + backends.sni_name.to_string(), 631 + ))) 632 + } 633 + 634 + async fn response_filter( 635 + &self, 636 + _session: &mut Session, 637 + upstream_response: &mut ResponseHeader, 638 + ctx: &mut Self::CTX, 639 + ) -> Result<()> 640 + where 641 + Self::CTX: Send + Sync, 642 + { 643 + // if we had no valid session, clear the cookie 644 + if !ctx.session_valid { 645 + let mut cookies = CookieJar::default(); 646 + cookies.remove(SESSION_COOKIE_NAME); 647 + 648 + cookies.as_header_values().into_iter().try_for_each(|v| { 649 + let v = HeaderValue::from_bytes(v.as_bytes()) 650 + .map_err(internal_error_from("invalid clear cookie header value"))?; 651 + 652 + upstream_response 653 + .headers 654 + .append(http::header::SET_COOKIE, v); 655 + Ok::<_, Box<Error>>(()) 656 + })?; 657 + } 658 + Ok(()) 659 + } 660 + } 661 + 662 + /// additional data stored in the load balancer's backend structure 663 + /// 664 + /// for use in [`AuthGateway::upstream_peer`] 665 + #[derive(Clone)] 666 + pub struct BackendData { 667 + /// does the backend want tls 668 + pub tls: bool, 669 + }
+193
src/gateway/oidc.rs
··· 1 + use std::collections::HashMap; 2 + use std::sync::Arc; 3 + 4 + use color_eyre::eyre::Context as _; 5 + use http::status::StatusCode; 6 + use pingora::prelude::*; 7 + use tokio::sync::Mutex as TokioMutex; 8 + use url::Url; 9 + 10 + use crate::config; 11 + use crate::httputil::{internal_error, internal_error_from, status_error, status_error_from}; 12 + use crate::oauth; 13 + 14 + /// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#cookie_prefixes 15 + pub const SESSION_COOKIE_NAME: &str = "__Host-Http-oauth-session"; 16 + 17 + /// active session user information 18 + pub struct UserInfo { 19 + /// when this session expires, from the id token `exp` claim 20 + pub expires_at: jiff::Timestamp, 21 + /// all other non-default claims attached to the id token 22 + pub claims: HashMap<String, String>, 23 + } 24 + 25 + /// in-progress auth flow state 26 + #[derive(Clone)] // only needed cause papaya 27 + pub struct InProgressAuth { 28 + /// the code verifier, whence the code challenge was derived 29 + pub code_verifier: String, 30 + /// the original path we were trying to go to on this domain, 31 + /// for redirection once the auth flow is over 32 + pub original_path: String, 33 + } 34 + 35 + /// cache of auth server metadata and associated bits 36 + pub struct MetadataCache { 37 + /// the metadata itself 38 + pub metadata: oauth::metadata::AuthServerMetadata, 39 + /// the fetched, parsed jwks info 40 + /// "none" means validation was disabled in our config, _NOT_ "we couldn't fetch the jwks" 41 + pub jws_verifier: Option<compact_jwt::JwsEs256Verifier>, 42 + } 43 + 44 + /// overall auth info 45 + pub struct Info { 46 + /// the raw config from our config file 47 + pub config: config::format::Oidc, 48 + // needs to be tokio because we need to hold it across an await point 49 + /// cache of auth server metadata 50 + pub meta_cache: TokioMutex<Option<Arc<MetadataCache>>>, 51 + /// the current in-progress authorization flows, bound to states submitted to the auth serve 52 + pub auth_states: papaya::HashMap<uuid::Uuid, InProgressAuth>, 53 + /// the currently active sessions, by id from the session id cookie 54 + pub sessions: papaya::HashMap<u64, UserInfo>, 55 + /// the root domain 56 + pub domain: String, 57 + /// the oauth client secret 58 + pub client_secret: String, 59 + 60 + /// the signing key used to sign session cookies 61 + pub cookie_signing_key: ed25519_dalek::SigningKey, 62 + } 63 + impl Info { 64 + /// clear the metadata cache 65 + pub async fn clear_metadata_cache(&self) { 66 + *self.meta_cache.lock().await = None; 67 + } 68 + /// get the metadata, or cache it (and the accompanying jwks data if needed) if it hasn't get 69 + /// been fetched 70 + pub async fn get_or_cache_metadata(&self) -> Result<Arc<MetadataCache>> { 71 + let mut cache = self.meta_cache.lock().await; 72 + if let Some(ref meta) = *cache { 73 + return Ok(meta.clone()); 74 + } 75 + let discovery_url = { 76 + let url = Url::parse(&self.config.discovery_url_base) 77 + .map_err(internal_error_from("invalid discovery url"))?; 78 + oauth::metadata::oidc_discovery_uri(&url) 79 + .map_err(internal_error_from("invalid discovery url suffix"))? 80 + }; 81 + 82 + let meta: oauth::metadata::AuthServerMetadata = { 83 + let resp = reqwest::Client::new() 84 + .get(discovery_url.as_str()) 85 + .header(http::header::ACCEPT, "application/json") 86 + .send() 87 + .await 88 + .map_err(internal_error_from("unable to fetch oauth metadata doc"))?; 89 + 90 + if !resp.status().is_success() { 91 + return Err(status_error( 92 + "unable to fetch discovery info", 93 + ErrorSource::Internal, 94 + StatusCode::SERVICE_UNAVAILABLE, 95 + )()); 96 + } 97 + resp.json().await.map_err(internal_error_from( 98 + "unable to deserialize oauth metadata doc", 99 + ))? 100 + }; 101 + 102 + meta.generally_as_expected().map_err(internal_error_from( 103 + "auth server not generally as expected/required", 104 + ))?; 105 + 106 + let jws_verifier = if self.config.validate_with_jwk { 107 + if !meta 108 + .id_token_signing_alg_values_supported 109 + .as_ref() 110 + .is_some_and(|s| s.contains(&oauth::metadata::SigningAlgValue::ES256)) 111 + { 112 + return Err(internal_error("es256 signing not supported by endpoint")()); 113 + } 114 + 115 + let Some(jwks_uri) = &meta.jwks_uri else { 116 + return Err(internal_error( 117 + "jwks not available or es256 signing not supported by endpoint", 118 + )()); 119 + }; 120 + let resp = reqwest::Client::new() 121 + .get(jwks_uri.as_str()) 122 + .header(http::header::ACCEPT, "application/json") 123 + .send() 124 + .await 125 + .map_err(status_error_from( 126 + "unable to fetch jwks", 127 + ErrorSource::Internal, 128 + StatusCode::SERVICE_UNAVAILABLE, 129 + ))?; 130 + if !resp.status().is_success() { 131 + return Err(status_error( 132 + "unable to fetch jwks", 133 + ErrorSource::Internal, 134 + StatusCode::SERVICE_UNAVAILABLE, 135 + )()); 136 + } 137 + let jwks: compact_jwt::JwkKeySet = resp 138 + .json() 139 + .await 140 + .map_err(internal_error_from("unable to deserialize jwks"))?; 141 + // per oidc discovery v1 section 3, this either contains only signing keys, or has 142 + // keys with a `use` option. we're going to choose to only support 1 key per use 143 + // here and require a use anyway. this whole thing is so poorly specified. the jwt 144 + // ecosystem really is a tire fire 145 + Some( 146 + jwks.keys 147 + .iter() 148 + .filter_map(|key| { 149 + let compact_jwt::Jwk::EC { use_: r#use, .. } = &key else { 150 + return None; 151 + }; 152 + if !r#use 153 + .as_ref() 154 + .is_some_and(|r#use| r#use == &compact_jwt::JwkUse::Sig) 155 + { 156 + return None; 157 + } 158 + compact_jwt::JwsEs256Verifier::try_from(key).ok() 159 + }) 160 + .next() 161 + .ok_or_else(internal_error("no sig keys availabe from jwks"))?, 162 + ) 163 + } else { 164 + None 165 + }; 166 + 167 + Ok(cache 168 + .insert(Arc::new(MetadataCache { 169 + metadata: meta, 170 + jws_verifier, 171 + })) 172 + .clone()) 173 + } 174 + 175 + pub fn from_config(config: config::format::Oidc, domain: String) -> color_eyre::Result<Self> { 176 + let mut rng = rand::rngs::OsRng; 177 + // TODO(feature): check & warn on permissions here? 178 + let client_secret = std::fs::read_to_string(&config.client_secret_path) 179 + .context("reading client secret")? 180 + .trim() 181 + .to_string(); 182 + Ok(Self { 183 + config, 184 + meta_cache: TokioMutex::new(None), 185 + auth_states: Default::default(), 186 + sessions: Default::default(), 187 + client_secret, 188 + 189 + cookie_signing_key: ed25519_dalek::SigningKey::generate(&mut rng), 190 + domain, 191 + }) 192 + } 193 + }
+118
src/httputil.rs
··· 1 + //! http utilities 2 + 3 + use http::StatusCode; 4 + use pingora::prelude::*; 5 + 6 + /// closure that returns the given status with no cause 7 + /// 8 + /// use with [`Option::ok_or_else`] 9 + pub fn status_error( 10 + why: &'static str, 11 + src: ErrorSource, 12 + status: StatusCode, 13 + ) -> impl FnOnce() -> Box<Error> { 14 + move || { 15 + Error::create( 16 + ErrorType::HTTPStatus(status.into()), 17 + src, 18 + Some(why.into()), 19 + None, 20 + ) 21 + } 22 + } 23 + /// closure that returns `500 Internal Server Error`, marked as caused by the error returned to 24 + /// the given closure 25 + /// 26 + /// use with [`Result::map_err`] 27 + pub fn internal_error_from<E>(why: &'static str) -> impl FnOnce(E) -> Box<Error> 28 + where 29 + E: Into<Box<dyn ErrorTrait + Send + Sync>>, 30 + { 31 + move |cause| { 32 + Error::create( 33 + ErrorType::HTTPStatus(StatusCode::INTERNAL_SERVER_ERROR.into()), 34 + ErrorSource::Internal, 35 + Some(why.into()), 36 + Some(cause.into()), 37 + ) 38 + } 39 + } 40 + 41 + /// closure that returns `500 Internal Server Error` with no cause 42 + /// 43 + /// use with [`Option::ok_or_else`] 44 + pub fn internal_error(why: &'static str) -> impl FnOnce() -> Box<Error> { 45 + move || { 46 + Error::create( 47 + ErrorType::HTTPStatus(StatusCode::INTERNAL_SERVER_ERROR.into()), 48 + ErrorSource::Internal, 49 + Some(why.into()), 50 + None, 51 + ) 52 + } 53 + } 54 + /// closure that returns the given status, marked as caused by the error returned to the given closure 55 + /// 56 + /// use with [`Result::map_err`] 57 + pub fn status_error_from<E>( 58 + why: &'static str, 59 + src: ErrorSource, 60 + status: http::StatusCode, 61 + ) -> impl FnOnce(E) -> Box<Error> 62 + where 63 + E: Into<Box<dyn ErrorTrait + Send + Sync>>, 64 + { 65 + move |cause| { 66 + Error::create( 67 + ErrorType::HTTPStatus(status.into()), 68 + src, 69 + Some(why.into()), 70 + Some(cause.into()), 71 + ) 72 + } 73 + } 74 + 75 + /// redirect to the given location 76 + /// 77 + /// the given callback can be used to inject additional headers, like `set-cookie` 78 + pub async fn redirect_response( 79 + session: &mut Session, 80 + to: &str, 81 + bld_resp_header: impl FnOnce(&mut ResponseHeader, &Session) -> Result<()>, 82 + ) -> Result<()> { 83 + session.set_keepalive(None); 84 + session 85 + .write_response_header( 86 + Box::new({ 87 + // per <rfc:draft-ietf-oauth-v2-1#1.6>, any redirect is fine save 307, but HTTP 302 seems to 88 + // be their example. 89 + let mut resp = ResponseHeader::build(StatusCode::FOUND, Some(0))?; 90 + resp.insert_header(http::header::LOCATION, to)?; 91 + bld_resp_header(&mut resp, session)?; 92 + resp 93 + }), 94 + true, 95 + ) 96 + .await?; 97 + session.finish_body().await?; 98 + Ok(()) 99 + } 100 + 101 + /// fetch the cookies for the current request 102 + pub fn cookie_jar(req: &'_ RequestHeader) -> Result<Option<cookie_rs::CookieJar<'_>>> { 103 + use cookie_rs::CookieJar; 104 + 105 + let Some(raw) = req.headers.get(http::header::COOKIE) else { 106 + return Ok(None); 107 + }; 108 + Ok(Some( 109 + raw.to_str() 110 + .map_err(Box::<dyn ErrorTrait + Send + Sync>::from) 111 + .and_then(|c| Ok(CookieJar::parse(c)?)) 112 + .map_err(status_error_from( 113 + "bad cookie header", 114 + ErrorSource::Downstream, 115 + StatusCode::BAD_REQUEST, 116 + ))?, 117 + )) 118 + }
+23 -189
src/main.rs
··· 1 1 use std::collections::HashMap; 2 - use std::sync::Arc; 3 2 4 - use async_trait::async_trait; 5 3 use color_eyre::eyre::Context as _; 6 - use http::status::StatusCode; 7 4 use pingora::lb; 8 5 use pingora::lb::selection::consistent::KetamaHashing; 9 - use pingora::modules::http::HttpModules; 10 - use pingora::modules::http::compression::ResponseCompressionBuilder; 11 6 use pingora::prelude::*; 12 7 13 - mod config; 14 - 15 - struct BackendInfo { 16 - balancer: Arc<LoadBalancer<KetamaHashing>>, 17 - tls_mode: config::format::domain::TlsMode, 18 - name: String, 19 - // TODO: force ssl 20 - } 21 - 22 - pub struct AuthGateway { 23 - backends: HashMap<String, BackendInfo>, 24 - } 25 - 26 - fn status_error( 27 - why: &'static str, 28 - src: ErrorSource, 29 - status: http::StatusCode, 30 - ) -> impl FnOnce() -> Box<Error> { 31 - move || { 32 - Error::create( 33 - ErrorType::HTTPStatus(status.into()), 34 - src, 35 - Some(why.into()), 36 - None, 37 - ) 38 - } 39 - } 40 - 41 - impl AuthGateway { 42 - fn backend_info<'s>(&'s self, session: &Session) -> Result<&'s BackendInfo> { 43 - let req = session.req_header(); 44 - // TODO: afaict, right now, afaict, pingora a) does not check that SNI matches the `Host` 45 - // header, b) does not support extracting the SNI info on rustls, so we'll have to switch 46 - // to boringssl and implement that ourselves T_T 47 - let host = req 48 - .headers 49 - .get(http::header::HOST) 50 - .ok_or_else(status_error( 51 - "no host set", 52 - ErrorSource::Downstream, 53 - StatusCode::BAD_REQUEST, 54 - ))? 55 - .to_str() 56 - .map_err(|e| { 57 - Error::because( 58 - ErrorType::HTTPStatus(StatusCode::BAD_REQUEST.into()), 59 - "no host", 60 - e, 61 - ) 62 - })?; 63 - let info = self.backends.get(host).ok_or_else(status_error( 64 - "unknown host", 65 - ErrorSource::Downstream, 66 - StatusCode::SERVICE_UNAVAILABLE, 67 - ))?; 68 - 69 - Ok(info) 70 - } 71 - } 72 - 73 - #[async_trait] 74 - impl ProxyHttp for AuthGateway { 75 - type CTX = (); 76 - fn new_ctx(&self) -> Self::CTX {} 77 - 78 - fn init_downstream_modules(&self, modules: &mut HttpModules) { 79 - // TODO: make this configurable? 80 - modules.add_module(ResponseCompressionBuilder::enable(1)); 81 - } 82 - 83 - async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> { 84 - // check if this is http, and redirect 85 - // TODO: maybe should be a module? 86 - let is_https = session 87 - .digest() 88 - .and_then(|d| d.ssl_digest.as_ref()) 89 - .is_some(); 90 - if !is_https { 91 - use config::format::domain::TlsMode; 92 - let info = self.backend_info(session)?; 93 - match info.tls_mode { 94 - TlsMode::Only => { 95 - // we should just drop the connection, although people should really just be 96 - // using HSTS 97 - session.shutdown().await; 98 - return Ok(true); 99 - } 100 - TlsMode::UnsafeAllowHttp => {} 101 - } 102 - } 103 - 104 - Ok(false) 105 - } 106 - 107 - async fn upstream_peer(&self, session: &mut Session, _ctx: &mut ()) -> Result<Box<HttpPeer>> { 108 - fn client_addr_key(sock_addr: &pingora::protocols::l4::socket::SocketAddr) -> Vec<u8> { 109 - use pingora::protocols::l4::socket::SocketAddr; 110 - match sock_addr { 111 - SocketAddr::Inet(socket_addr) => match socket_addr { 112 - std::net::SocketAddr::V4(v4) => Vec::from(v4.ip().octets()), 113 - std::net::SocketAddr::V6(v6) => Vec::from(v6.ip().octets()), 114 - }, 115 - // TODO: this is... not a great key for hashing 116 - SocketAddr::Unix(_socket_addr) => vec![], 117 - } 118 - } 119 - 120 - let backends = self.backend_info(session)?; 121 - let backend = backends 122 - .balancer 123 - // NB: this means that CGNAT, other proxies, etc will? consistently hit the same 124 - // backend, so we might wanna take that into consideration. fine for now, this is 125 - // currently for personal use ;-) 126 - .select( 127 - &client_addr_key(session.client_addr().ok_or_else(status_error( 128 - "no client address", 129 - ErrorSource::Downstream, 130 - StatusCode::BAD_REQUEST, 131 - ))?), /* lb on client address */ 132 - 256, 133 - ) 134 - .ok_or_else(status_error( 135 - "no available backends", 136 - ErrorSource::Upstream, 137 - StatusCode::SERVICE_UNAVAILABLE, 138 - ))?; 139 - 140 - let needs_tls = backend 141 - .ext 142 - .get::<BackendData>() 143 - .map(|d| d.tls) 144 - .unwrap_or(true); 145 - 146 - Ok(Box::new(HttpPeer::new( 147 - backend, 148 - needs_tls, 149 - backends.name.to_string(), 150 - ))) 151 - } 152 - 153 - // TODO: upstream_request_filter to insert the right headers 154 - 155 - async fn response_filter( 156 - &self, 157 - _session: &mut Session, 158 - _upstream_response: &mut ResponseHeader, 159 - _ctx: &mut Self::CTX, 160 - ) -> Result<()> 161 - where 162 - Self::CTX: Send + Sync, 163 - { 164 - Ok(()) 165 - } 166 - 167 - // TODO: logging 168 - } 8 + use self::gateway::{AuthGateway, BackendData, DomainInfo, oidc}; 169 9 170 - #[derive(Clone)] 171 - struct BackendData { 172 - tls: bool, 173 - } 10 + mod config; 11 + mod cookies; 12 + mod gateway; 13 + mod httputil; 14 + mod oauth; 174 15 175 - fn balancer( 176 - domains: &HashMap<String, config::format::Domain>, 177 - ) -> color_eyre::Result<( 16 + /// constructed load balancer, with [backend info][`BackendInfo`] to be passed to [`AuthGateway`] 17 + type BalancerInfo = ( 178 18 Vec<pingora::services::background::GenBackgroundService<LoadBalancer<KetamaHashing>>>, 179 - HashMap<String, BackendInfo>, 180 - )> { 19 + HashMap<String, DomainInfo>, 20 + ); 21 + 22 + /// construct the load balancer and initialize the [backend info][`BackendInfo`] for the 23 + /// [`AuthGateway`] 24 + fn balancer(domains: &HashMap<String, config::format::Domain>) -> color_eyre::Result<BalancerInfo> { 181 25 use lb::{self, Backend, discovery}; 182 26 use pingora::protocols::l4::socket::SocketAddr; 183 27 ··· 224 68 .collect::<color_eyre::Result<_>>() 225 69 .context("constucting backends for domain")?; 226 70 let backends = lb::Backends::new(discovery::Static::new(backends)); 227 - // TODO: allow configuring healthchecks 228 71 let balancer = LoadBalancer::from_backends(backends); 229 72 let svc = background_service("health checking", balancer); 230 73 231 - let info = BackendInfo { 74 + let info = DomainInfo { 232 75 balancer: svc.task(), 233 76 tls_mode: config::format::domain::TlsMode::try_from(domain.tls_mode) 234 77 .context("invalid tls mode")?, 235 - name: name.clone(), 78 + sni_name: name.clone(), 79 + oidc: domain 80 + .oidc_auth 81 + .clone() 82 + .map(|config| oidc::Info::from_config(config, name.clone())) 83 + .transpose()?, 84 + headers: domain.manage_headers.clone().unwrap_or_default(), 236 85 }; 237 86 238 87 balancers.insert(name.clone(), info); ··· 244 93 245 94 fn main() -> color_eyre::Result<()> { 246 95 use color_eyre::eyre::eyre; 247 - 248 - use std::os::unix::fs::PermissionsExt as _; 249 96 250 97 tracing_subscriber::fmt().init(); 251 98 color_eyre::install()?; ··· 271 118 272 119 let (balancer_svcs, balancers) = 273 120 balancer(&config.domains).context("setting up load balancing")?; 274 - let mut gateway = http_proxy_service( 275 - &server.configuration, 276 - AuthGateway { 277 - backends: balancers, 278 - }, 279 - ); 121 + let mut gateway = http_proxy_service(&server.configuration, AuthGateway { domains: balancers }); 280 122 for binding in config.bind_to_tcp { 281 123 match binding.tls { 282 124 Some(tls) => gateway ··· 284 126 .context("setting up tls")?, 285 127 None => gateway.add_tcp(&binding.addr), 286 128 } 287 - } 288 - for binding in config.bind_to_uds { 289 - gateway.add_uds( 290 - &binding.path, 291 - binding 292 - .permissions 293 - .map(|p| std::fs::Permissions::from_mode(p.mode)), 294 - ); 295 129 } 296 130 297 131 balancer_svcs
+596
src/oauth.rs
··· 1 + //! # Background 2 + //! 3 + //! so! there exists oauth & oidc packages. they're what kanidm uses to implement oidc. 4 + //! unfortunately, they're poorly maintained on the client side (e.g. their reqwest bindings don't 5 + //! work with the latest version of reqwest), and rather clunkily implemented through callbacks 6 + //! instead of typestate, which mean they're tightly bound to exact implementation details. 7 + //! 8 + //! this is... annoying. so we have this instead 9 + //! 10 + //! # Overview 11 + //! 12 + //! this implementation is based on [oauth2.1]. this means, basically, it's [oauth2.0] with best 13 + //! practices applied. 14 + //! 15 + //! the oidc half is based on [oidc core 1.0 + errata 2][oidc1], but with... some of the more 16 + //! ill-advised ignorable parts duely ignored. 17 + //! 18 + //! [oauth2.1]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1 19 + //! [oauth2.0]: https://www.rfc-editor.org/rfc/rfc8414.html 20 + //! [oidc1]: https://openid.net/specs/openid-connect-core-1_0.html 21 + 22 + /// # [OAuth 2.0 Authorization Server Metadata][rfc:8414] and related helpers 23 + /// 24 + /// most enums here also have additional values specified in the [iana registry]. 25 + /// 26 + /// [iana registry]: https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml 27 + pub mod metadata { 28 + use std::collections::HashSet; 29 + 30 + use serde::Deserialize; 31 + use url::Url; 32 + 33 + /// [OAuth 2.0 Authorization Server Metadata][rfc:8414#2] 34 + /// 35 + /// see the rfc for field descriptions. 36 + #[derive(Deserialize)] 37 + pub struct AuthServerMetadata { 38 + pub issuer: Url, 39 + // > \[authorization_endpoint\] is REQUIRED unless no grant types are supported that use the 40 + // > authorization endpoint 41 + // 42 + // we require the authorization flow, so we leave this as required 43 + pub authorization_endpoint: Url, 44 + // same here 45 + pub token_endpoint: Url, 46 + pub jwks_uri: Option<Url>, 47 + pub response_types_supported: HashSet<ResponseType>, 48 + pub response_modes_supported: Option<HashSet<ResponseMode>>, 49 + pub grant_types_supported: Option<HashSet<GrantType>>, 50 + // defaults to [`TokenEndpointAuthMethod::ClientSecretBasic`], but oauth 2.1 [adds in post 51 + // too][rfc:draft-ietf-v2.1#10.1] 52 + pub token_endpoint_auth_methods_supported: Option<HashSet<AuthMethod>>, 53 + pub code_challenge_methods_supported: Option<HashSet<CodeChallengeMethod>>, 54 + 55 + // per https://openid.net/specs/openid-connect-discovery-1_0.html 56 + pub id_token_signing_alg_values_supported: Option<HashSet<SigningAlgValue>>, 57 + // per the spec, extra fields are defined in [OIDC Discovery 1.0 with errata 58 + // 2][oidc-discovery-1]. 59 + // 60 + // the rfc also contains a bunch of extra fields that we don't use, so aren't captured 61 + // here 62 + // 63 + // [oidc-discovery-1]: https://openid.net/specs/openid-connect-discovery-1_0.html 64 + } 65 + impl AuthServerMetadata { 66 + /// check if this metadata conforms to our expectations of a modern oauth v2.1 & oidc core 67 + /// v1 server 68 + pub fn generally_as_expected(&self) -> color_eyre::Result<()> { 69 + use color_eyre::eyre::eyre; 70 + if !self.response_types_supported.contains(&ResponseType::Code) { 71 + return Err(eyre!("response type `code` not supported by auth server")); 72 + } 73 + // if this is missing, assume query is supported 74 + if !ResponseMode::Query.is_supported_for(self.response_modes_supported.as_ref()) { 75 + return Err(eyre!("response mode `query` not supported by auth server")); 76 + } 77 + if !GrantType::AuthorizationCode.is_supported_for(self.grant_types_supported.as_ref()) { 78 + return Err(eyre!( 79 + "grant type `authorization_code` not supported by auth server" 80 + )); 81 + } 82 + if !AuthMethod::ClientSecretPost 83 + .is_supported_for(self.token_endpoint_auth_methods_supported.as_ref()) 84 + { 85 + return Err(eyre!( 86 + "client_secret_post auth method not supported, not a valid oauth 2.1 server, and honestly not a good oauth 2.0 server either" 87 + )); 88 + } 89 + if self 90 + .code_challenge_methods_supported 91 + .as_ref() 92 + .is_none_or(|m| !m.contains(&CodeChallengeMethod::S256)) 93 + { 94 + return Err(eyre!( 95 + "auth server does not support pkce, or does not support S256 pkce" 96 + )); 97 + } 98 + if self.authorization_endpoint.host() != self.issuer.host() { 99 + return Err(eyre!( 100 + "authorization endpoint not on issuer server: {} vs {}", 101 + self.authorization_endpoint.as_str(), 102 + self.issuer.as_str() 103 + )); 104 + } 105 + if self.token_endpoint.host() != self.issuer.host() { 106 + return Err(eyre!("token endpoint not on issuer server")); 107 + } 108 + if self 109 + .jwks_uri 110 + .as_ref() 111 + .is_some_and(|jwks_uri| jwks_uri.host() != self.issuer.host()) 112 + { 113 + return Err(eyre!("jwks uri not on issuer server")); 114 + } 115 + // the rest need to be checked if we ever use them 116 + 117 + // signing methods only checked if we want to actually verify the id tokens (see the 118 + // config) 119 + 120 + Ok(()) 121 + } 122 + } 123 + 124 + /// [OAuth 2.0 Dynamic Client Registration response types][rfc:7591#2] 125 + /// 126 + /// as linked in the [`AuthServerMetadata::response_types_supported`] specification. 127 + #[derive(Deserialize, Eq, PartialEq, Hash)] 128 + #[serde(rename_all = "snake_case")] 129 + pub enum ResponseType { 130 + Code, 131 + Token, 132 + #[serde(untagged)] 133 + Other(String), 134 + } 135 + 136 + /// per <rfc8414#2> this is a mix of [OAuth.Response], and [OAuth.Post] from the openid folks. 137 + /// 138 + /// 139 + /// [OAuth.Response]: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html 140 + /// [OAuth.Post]: https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html 141 + /// 142 + /// as linked in the [`AuthServerMetadata::response_types_supported`] specification. 143 + /// 144 + /// defaults to [`ResponseMode::Query`] and [`ResponseMode::Fragment`] if missing 145 + #[derive(Deserialize, Eq, PartialEq, Hash)] 146 + #[serde(rename_all = "snake_case")] 147 + pub enum ResponseMode { 148 + Query, 149 + Fragment, 150 + FormPost, 151 + #[serde(untagged)] 152 + Other(String), 153 + } 154 + impl ResponseMode { 155 + pub fn is_supported_for(&self, options: Option<&HashSet<Self>>) -> bool { 156 + match options { 157 + Some(opts) => opts.contains(self), 158 + // per the field documentation (see [`Self`]) 159 + None if *self == Self::Query => true, 160 + None if *self == Self::Fragment => true, 161 + None => false, 162 + } 163 + } 164 + } 165 + 166 + /// [OAuth 2.0 Dynamic Client Registration grant types][rfc:7591#2] 167 + /// 168 + /// as linked in the [`AuthServerMetadata::grant_types_supported`] specification. 169 + /// 170 + /// defaults to [`GrantType::AuthorizationCode`] and [`GrantType::Implicit`] if missing per 171 + /// the rfc, but oauth 2.1 [removes the implict grant][rfc:draft-ietf-v2.1#10.1] 172 + #[derive(Deserialize, Eq, PartialEq, Hash)] 173 + #[serde(rename_all = "snake_case")] 174 + pub enum GrantType { 175 + AuthorizationCode, 176 + Implicit, 177 + Password, 178 + ClientCredentials, 179 + RefreshToken, 180 + #[serde(rename = "urn:ietf:params:oauth:grant-type:jwt-bearer")] 181 + UrnIetfParamsOauthGrantTypeJwtBearer, 182 + #[serde(rename = "urn:ietf:params:oauth:grant-type:saml2-bearer")] 183 + UrnIetfParamsOauthGrantTypeSaml2Bearer, 184 + #[serde(untagged)] 185 + Other(String), 186 + } 187 + impl GrantType { 188 + pub fn is_supported_for(&self, options: Option<&HashSet<Self>>) -> bool { 189 + match options { 190 + Some(opts) => opts.contains(self), 191 + // per the field documentation (see [`Self`]) 192 + None if *self == Self::AuthorizationCode => true, 193 + // NB: oauth 2.1 removes the implicit grant 194 + // None if *self == Self::Implicit => true, 195 + None => false, 196 + } 197 + } 198 + } 199 + /// [OAuth 2.0 Dynamic Client Registration token endpoint auth methods][rfc:7591#2] 200 + /// 201 + /// as linked in the [`AuthServerMetadata::token_endpoint_auth_methods_supported`] specification. 202 + #[derive(Deserialize, Eq, PartialEq, Hash)] 203 + #[serde(rename_all = "snake_case")] 204 + pub enum AuthMethod { 205 + None, 206 + ClientSecretPost, 207 + ClientSecretBasic, 208 + #[serde(untagged)] 209 + Other(String), 210 + } 211 + impl AuthMethod { 212 + pub fn is_supported_for(&self, options: Option<&HashSet<Self>>) -> bool { 213 + match options { 214 + Some(opts) => opts.contains(self), 215 + // per the field documentation (see [`Self`]), this MUST be supported 216 + None if *self == Self::ClientSecretBasic => true, 217 + // per <rfc:draft-ietf-oauth-v2.1#2.5>, servers MUST support this too 218 + None if *self == Self::ClientSecretPost => true, 219 + None => false, 220 + } 221 + } 222 + } 223 + /// JWT signing alg values 224 + /// 225 + /// as linked in the [`AuthServerMetadata::token_endpoint_auth_signing_alg_values_supported`] specification. 226 + #[derive(Deserialize, Eq, PartialEq, Hash, Debug)] 227 + pub enum SigningAlgValue { 228 + /// NB: this must be rejected, but we capture it here to avoid it going into [`Self::Other`] 229 + None, 230 + RS256, 231 + ES256, 232 + #[serde(untagged)] 233 + Other(String), 234 + } 235 + /// [PKCE challenge methods][rfc:7636#4.3] 236 + /// 237 + /// as linked in the [`AuthServerMetadata::code_challenge_methods_supported`] specification. 238 + #[derive(Deserialize, Eq, PartialEq, Hash)] 239 + pub enum CodeChallengeMethod { 240 + /// should never be used, but we want to catch it so it doesn't go in Other 241 + Plain, 242 + S256, 243 + #[serde(untagged)] 244 + Other(String), 245 + } 246 + 247 + /// get the [oidc discovery 1.0+errata 2][oidc-discovery-1] well-known endpoint for a given base 248 + /// url 249 + /// 250 + /// users are expected to perform (both) issuer-id transformations themselves, if need-be 251 + /// (per [rfc:8414#5]) 252 + /// 253 + /// [oidc-discovery-1]: https://openid.net/specs/openid-connect-discovery-1_0.html 254 + pub fn oidc_discovery_uri(base_url: &Url) -> color_eyre::Result<Url> { 255 + Ok(base_url.join(".well-known/openid-configuration")?) 256 + } 257 + } 258 + 259 + pub mod auth_code_flow { 260 + //! [`metadata::GrantType::AuthorizationCode`] flow 261 + //! 262 + //! # [oauth 2.1 authorization code grant][rfc:draft-ietf-oauth-v2.1#4.1] 263 + //! 264 + //! 1. [`self::code_request::redirect_to_auth_server`] 265 + //! 2. [`self::code_response::receive_redirect`] 266 + //! 3. [`self::token_request::request_access_token`] 267 + //! 4. deserialize response into either [`self::token_response::Valid`] or 268 + //! [`self::token_response::Error`] 269 + 270 + use std::borrow::Cow; 271 + 272 + use serde::Deserialize; 273 + 274 + /// auto-join/split authorization code scopes 275 + #[derive(Deserialize)] 276 + #[serde(try_from = "Cow<'_, str>")] 277 + pub struct Scopes<'u>(Cow<'u, str>); 278 + 279 + #[allow(clippy::infallible_try_from, reason = "required for serde")] 280 + impl<'u> TryFrom<Cow<'u, str>> for Scopes<'u> { 281 + type Error = std::convert::Infallible; 282 + 283 + fn try_from(value: Cow<'u, str>) -> std::result::Result<Self, Self::Error> { 284 + Ok(Self(value)) 285 + } 286 + } 287 + impl<'u> Scopes<'u> { 288 + pub fn base_scopes() -> Self { 289 + Self(Cow::Borrowed("openid")) 290 + } 291 + pub fn add_scope(mut self, scope: impl AsRef<str>) -> Self { 292 + match &mut self.0 { 293 + Cow::Borrowed(b) => { 294 + self.0 = format!("{b} {}", scope.as_ref()).into(); 295 + } 296 + Cow::Owned(v) => { 297 + v.push(' '); 298 + v.push_str(scope.as_ref()); 299 + } 300 + } 301 + self 302 + } 303 + } 304 + impl<'u, S: AsRef<str>> FromIterator<S> for Scopes<'u> { 305 + fn from_iter<T: IntoIterator<Item = S>>(iter: T) -> Self { 306 + Self( 307 + iter.into_iter() 308 + .fold(String::new(), |mut acc, elem| { 309 + if !acc.is_empty() { 310 + acc.push(' '); 311 + } 312 + acc.push_str(elem.as_ref()); 313 + acc 314 + }) 315 + .into(), 316 + ) 317 + } 318 + } 319 + 320 + /// Step 1 321 + pub mod code_request { 322 + 323 + use color_eyre::Result; 324 + use url::Url; 325 + 326 + use super::super::metadata::AuthServerMetadata; 327 + use super::Scopes; 328 + 329 + /// data used to construct the initial authorization code browser redirect url 330 + pub struct Data<'u> { 331 + // owned cause it's unique every time 332 + code_verifier: String, 333 + client_id: &'u str, 334 + scope: &'u Scopes<'u>, 335 + state: uuid::Uuid, 336 + redirect_uri: &'u Url, 337 + } 338 + impl<'u> Data<'u> { 339 + pub fn new(client_id: &'u str, scope: &'u Scopes<'u>, redirect_uri: &'u Url) -> Self { 340 + use base64::prelude::*; 341 + use rand::Rng as _; 342 + use sha2::Digest as _; 343 + 344 + let mut rng = rand::rngs::OsRng; 345 + Self { 346 + code_verifier: BASE64_URL_SAFE_NO_PAD 347 + .encode(sha2::Sha256::digest(rng.r#gen::<[u8; 32]>())), 348 + client_id, 349 + scope, 350 + state: uuid::Uuid::new_v4(), 351 + redirect_uri, 352 + } 353 + } 354 + } 355 + 356 + /// slice of [`super::metadata::AuthServerMetadata`] needed for the initial 357 + /// [`redirect_to_auth_server`] call 358 + pub struct Metadata<'u> { 359 + authorization_endpoint: &'u Url, 360 + } 361 + impl<'u> From<&'u AuthServerMetadata> for Metadata<'u> { 362 + fn from(orig: &'u AuthServerMetadata) -> Self { 363 + Self { 364 + authorization_endpoint: &orig.authorization_endpoint, 365 + } 366 + } 367 + } 368 + 369 + /// the information required to start the authorization code flow and redirect a browser 370 + pub struct RedirectInfo { 371 + /// the url to send to the browser 372 + pub url: Url, 373 + /// the code verifier, to use when submitting the token request 374 + pub code_verifier: String, 375 + /// the state, to use to associate the authorization server's response back to the code 376 + /// verifier and such 377 + pub state: uuid::Uuid, 378 + } 379 + 380 + /// construct the url used to redirect the user to the authorization server login 381 + /// 382 + /// take the returned state and use it to save the code verifier 383 + pub fn redirect_to_auth_server( 384 + meta: Metadata<'_>, 385 + params: Data<'_>, 386 + ) -> Result<RedirectInfo> { 387 + let mut url = meta.authorization_endpoint.clone(); 388 + let mut query = url.query_pairs_mut(); 389 + query.append_pair("response_type", "code"); 390 + query.append_pair("client_id", params.client_id); 391 + { 392 + use base64::prelude::*; 393 + use sha2::Digest as _; 394 + 395 + query.append_pair("code_challenge_method", "S256"); 396 + let challenge = 397 + BASE64_URL_SAFE.encode(sha2::Sha256::digest(params.code_verifier.as_bytes())); 398 + query.append_pair("code_challenge", &challenge); 399 + } 400 + 401 + // NB: there's some optional oidc parameters here, but they're mostly worth skipping 402 + // the main one that's useful is the nonce, but pkce takes the place of that and is 403 + // more broadly standardized in oauth2 v2.1 404 + 405 + query.append_pair("redirect_uri", params.redirect_uri.as_str()); 406 + query.append_pair("scope", &params.scope.0); 407 + query.append_pair("state", &params.state.to_string()); 408 + drop(query); 409 + 410 + Ok(RedirectInfo { 411 + url, 412 + code_verifier: params.code_verifier, 413 + state: params.state, 414 + }) 415 + } 416 + } 417 + 418 + /// Step 2 419 + pub mod code_response { 420 + use std::borrow::Cow; 421 + 422 + use color_eyre::Result; 423 + use serde::Deserialize; 424 + 425 + /// types of errors that a server can respond with, having failed the initial auth code request 426 + #[derive(Deserialize)] 427 + #[serde(rename_all = "snake_case")] 428 + pub enum ErrorType<'u> { 429 + // oauth 2.1 per [rfc:draft-ietf-oauth-v2-1#4.1.2.1] 430 + InvalidRequest, 431 + UnauthorizedClient, 432 + AccessDenied, 433 + UnsupportedResponseType, 434 + InvalidScope, 435 + ServerError, 436 + TemporarilyUnavailable, 437 + 438 + // TODO(on-oss): 439 + // [oidc-core-1](https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.6) 440 + // defines some, but we don't really care about those for now, so they can go in "other" 441 + 442 + // anything else a server happens to randomly shove in there 443 + #[serde(untagged)] 444 + #[allow( 445 + dead_code, 446 + reason = "gonna make use of these shortly to surface errors better" 447 + )] 448 + Other(Cow<'u, str>), 449 + } 450 + /// an error that the server responds with when an authorization code request fails 451 + #[allow( 452 + dead_code, 453 + reason = "gonna make use of these shortly to surface errors better" 454 + )] 455 + #[derive(Deserialize)] 456 + pub struct Error<'u> { 457 + pub error: ErrorType<'u>, 458 + pub error_description: Option<Cow<'u, str>>, 459 + pub error_uri: Option<Cow<'u, str>>, 460 + pub state: uuid::Uuid, 461 + pub iss: Option<Cow<'u, str>>, 462 + } 463 + 464 + /// the query parameters for a successful authorization code response 465 + pub struct Response<'u> { 466 + pub code: Cow<'u, str>, 467 + pub state: uuid::Uuid, 468 + } 469 + 470 + pub fn receive_redirect<'u>( 471 + query: &'u str, 472 + issuer: &'u str, 473 + ) -> Result<Result<Response<'u>, Error<'u>>> { 474 + #[derive(Deserialize)] 475 + #[serde(untagged)] 476 + enum Params<'u> { 477 + Valid { 478 + code: Cow<'u, str>, 479 + state: uuid::Uuid, 480 + iss: Option<Cow<'u, str>>, 481 + }, 482 + Error(Error<'u>), 483 + } 484 + 485 + let params = 486 + Params::deserialize(serde_html_form::Deserializer::from_bytes(query.as_bytes()))?; 487 + let (code, state, iss) = match params { 488 + Params::Valid { code, state, iss } => (code, state, iss), 489 + Params::Error(err) => return Ok(Err(err)), 490 + }; 491 + if iss.as_ref().is_some_and(|iss| iss != issuer) { 492 + // NB: it's unlikely to happen except via a misconfiguration, but technically this 493 + // could cause us to leak our in_progress states 494 + // per [rfc:draft-ietf-oauth-v2-1#7.14], we _must_ validate this if present 495 + return Err(color_eyre::eyre::eyre!("issuer mismatch")); 496 + } 497 + 498 + Ok(Ok(Response { code, state })) 499 + } 500 + } 501 + 502 + /// Step 3 503 + pub mod token_request { 504 + use color_eyre::Result; 505 + use url::Url; 506 + 507 + use super::super::metadata::AuthServerMetadata; 508 + use super::code_response::Response; 509 + 510 + /// slice of [`super::metadata::AuthServerMetadata`] needed for the initial 511 + /// [`request_access_token`] call 512 + pub struct Metadata<'u> { 513 + token_endpoint: &'u Url, 514 + } 515 + impl<'u> From<&'u AuthServerMetadata> for Metadata<'u> { 516 + fn from(orig: &'u AuthServerMetadata) -> Self { 517 + Self { 518 + token_endpoint: &orig.token_endpoint, 519 + } 520 + } 521 + } 522 + 523 + pub struct Data<'u> { 524 + pub code: Response<'u>, // owned, is consumed 525 + pub client_id: &'u str, 526 + pub client_secret: &'u str, 527 + // grant type is hardcoded for this 528 + pub code_verifier: String, // owned, consumed 529 + pub redirect_uri: &'u str, 530 + } 531 + 532 + pub struct Request<'u> { 533 + pub url: &'u Url, 534 + } 535 + 536 + /// Step 4: request an access token 537 + pub fn request_access_token<'u, 'm: 'u, BODY: form_urlencoded::Target>( 538 + meta: Metadata<'m>, 539 + data: Data<'u>, 540 + body: BODY, 541 + ) -> Result<Request<'u>> { 542 + let mut body = form_urlencoded::Serializer::new(body); 543 + body.append_pair("grant_type", "authorization_code"); 544 + body.append_pair("code", &data.code.code); 545 + body.append_pair("redirect_uri", data.redirect_uri); // oauth 2.0 only 546 + body.append_pair("client_id", data.client_id); 547 + body.append_pair("code_verifier", &data.code_verifier); 548 + body.append_pair("client_secret", data.client_secret); 549 + 550 + Ok(Request { 551 + url: meta.token_endpoint, 552 + }) 553 + } 554 + } 555 + 556 + /// Step 4 557 + pub mod token_response { 558 + use serde::Deserialize; 559 + use std::borrow::Cow; 560 + 561 + #[derive(Deserialize)] 562 + pub struct Valid<'u> { 563 + /// required per 564 + /// [oidc-core-1](https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.3.3) 565 + /// when oidc is in play 566 + pub id_token: Option<Cow<'u, str>>, 567 + } 568 + 569 + #[derive(Deserialize)] 570 + #[serde(rename_all = "snake_case")] 571 + pub enum ErrorType<'u> { 572 + // oauth 2.1 per [rfc:draft-ietf-oauth-v2-1#4.3.2] 573 + InvalidRequest, 574 + InvalidClient, 575 + InvalidGrant, 576 + UnauthorizedClient, 577 + UnsupportedGrantType, 578 + InvalidScope, 579 + 580 + // anything else a server happens to randomly shove in there 581 + #[serde(untagged)] 582 + #[allow(dead_code, reason = "deserialization purposes")] 583 + Other(Cow<'u, str>), 584 + } 585 + #[derive(Deserialize)] 586 + #[allow( 587 + dead_code, 588 + reason = "gonna make use of these shortly to surface errors better" 589 + )] 590 + pub struct Error<'u> { 591 + pub error: ErrorType<'u>, 592 + pub error_description: Option<Cow<'u, str>>, 593 + pub error_uri: Option<Cow<'u, str>>, 594 + } 595 + } 596 + }