A better Rust ATProto crate
1

Configure Feed

Select the types of activity you want to include in your feed.

removed rouille dep for localhost redirect one-shot

author nonbinary.computer date (Jun 7, 2026, 8:21 PM -0400) commit 47a37254 parent 3eae0e36 change-id zvtumkry
+208 -448
+9 -264
Cargo.lock
··· 24 24 checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 25 25 26 26 [[package]] 27 - name = "adler32" 28 - version = "1.2.0" 29 - source = "registry+https://github.com/rust-lang/crates.io-index" 30 - checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 31 - 32 - [[package]] 33 27 name = "aead" 34 28 version = "0.5.2" 35 29 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 92 86 ] 93 87 94 88 [[package]] 95 - name = "alloc-no-stdlib" 96 - version = "2.0.4" 97 - source = "registry+https://github.com/rust-lang/crates.io-index" 98 - checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 99 - 100 - [[package]] 101 - name = "alloc-stdlib" 102 - version = "0.2.2" 103 - source = "registry+https://github.com/rust-lang/crates.io-index" 104 - checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 105 - dependencies = [ 106 - "alloc-no-stdlib", 107 - ] 108 - 109 - [[package]] 110 89 name = "allocator-api2" 111 90 version = "0.2.21" 112 91 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 223 202 dependencies = [ 224 203 "stable_deref_trait", 225 204 ] 226 - 227 - [[package]] 228 - name = "ascii" 229 - version = "1.1.0" 230 - source = "registry+https://github.com/rust-lang/crates.io-index" 231 - checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" 232 205 233 206 [[package]] 234 207 name = "async-compression" ··· 480 453 481 454 [[package]] 482 455 name = "base64" 483 - version = "0.13.1" 484 - source = "registry+https://github.com/rust-lang/crates.io-index" 485 - checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 486 - 487 - [[package]] 488 - name = "base64" 489 456 version = "0.22.1" 490 457 source = "registry+https://github.com/rust-lang/crates.io-index" 491 458 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" ··· 583 550 ] 584 551 585 552 [[package]] 586 - name = "brotli" 587 - version = "3.5.0" 588 - source = "registry+https://github.com/rust-lang/crates.io-index" 589 - checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" 590 - dependencies = [ 591 - "alloc-no-stdlib", 592 - "alloc-stdlib", 593 - "brotli-decompressor", 594 - ] 595 - 596 - [[package]] 597 - name = "brotli-decompressor" 598 - version = "2.5.1" 599 - source = "registry+https://github.com/rust-lang/crates.io-index" 600 - checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" 601 - dependencies = [ 602 - "alloc-no-stdlib", 603 - "alloc-stdlib", 604 - ] 605 - 606 - [[package]] 607 - name = "buf_redux" 608 - version = "0.8.4" 609 - source = "registry+https://github.com/rust-lang/crates.io-index" 610 - checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" 611 - dependencies = [ 612 - "memchr", 613 - "safemem", 614 - ] 615 - 616 - [[package]] 617 553 name = "buffer" 618 554 version = "0.1.9" 619 555 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 722 658 ] 723 659 724 660 [[package]] 725 - name = "chunked_transfer" 726 - version = "1.5.0" 727 - source = "registry+https://github.com/rust-lang/crates.io-index" 728 - checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" 729 - 730 - [[package]] 731 661 name = "ciborium" 732 662 version = "0.2.2" 733 663 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 916 846 checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" 917 847 dependencies = [ 918 848 "aes-gcm", 919 - "base64 0.22.1", 849 + "base64", 920 850 "percent-encoding", 921 851 "rand 0.8.5", 922 852 "subtle", ··· 1189 1119 ] 1190 1120 1191 1121 [[package]] 1192 - name = "deflate" 1193 - version = "1.0.0" 1194 - source = "registry+https://github.com/rust-lang/crates.io-index" 1195 - checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" 1196 - dependencies = [ 1197 - "adler32", 1198 - "gzip-header", 1199 - ] 1200 - 1201 - [[package]] 1202 1122 name = "der" 1203 1123 version = "0.7.10" 1204 1124 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1551 1471 checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" 1552 1472 1553 1473 [[package]] 1554 - name = "filetime" 1555 - version = "0.2.27" 1556 - source = "registry+https://github.com/rust-lang/crates.io-index" 1557 - checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" 1558 - dependencies = [ 1559 - "cfg-if", 1560 - "libc", 1561 - "libredox", 1562 - ] 1563 - 1564 - [[package]] 1565 1474 name = "find-msvc-tools" 1566 1475 version = "0.1.9" 1567 1476 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1871 1780 ] 1872 1781 1873 1782 [[package]] 1874 - name = "gzip-header" 1875 - version = "1.0.0" 1876 - source = "registry+https://github.com/rust-lang/crates.io-index" 1877 - checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" 1878 - dependencies = [ 1879 - "crc32fast", 1880 - ] 1881 - 1882 - [[package]] 1883 1783 name = "h2" 1884 1784 version = "0.4.13" 1885 1785 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1981 1881 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 1982 1882 1983 1883 [[package]] 1984 - name = "hermit-abi" 1985 - version = "0.5.2" 1986 - source = "registry+https://github.com/rust-lang/crates.io-index" 1987 - checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 1988 - 1989 - [[package]] 1990 1884 name = "hex" 1991 1885 version = "0.4.3" 1992 1886 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2169 2063 source = "registry+https://github.com/rust-lang/crates.io-index" 2170 2064 checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" 2171 2065 dependencies = [ 2172 - "base64 0.22.1", 2066 + "base64", 2173 2067 "bytes", 2174 2068 "futures-channel", 2175 2069 "futures-util", ··· 2544 2438 "axum-extra", 2545 2439 "axum-macros", 2546 2440 "axum-test", 2547 - "base64 0.22.1", 2441 + "base64", 2548 2442 "bytes", 2549 2443 "chrono", 2550 2444 "clap", ··· 2590 2484 name = "jacquard-common" 2591 2485 version = "0.12.0-beta.2" 2592 2486 dependencies = [ 2593 - "base64 0.22.1", 2487 + "base64", 2594 2488 "bon", 2595 2489 "bytes", 2596 2490 "chrono", ··· 2737 2631 name = "jacquard-oauth" 2738 2632 version = "0.12.0-beta.2" 2739 2633 dependencies = [ 2740 - "base64 0.22.1", 2634 + "base64", 2741 2635 "bytes", 2742 2636 "chrono", 2743 2637 "dashmap", ··· 2755 2649 "p256", 2756 2650 "p384", 2757 2651 "rand 0.8.5", 2758 - "rouille", 2759 2652 "serde", 2760 2653 "serde_html_form", 2761 2654 "serde_json", ··· 3010 2903 checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" 3011 2904 3012 2905 [[package]] 3013 - name = "libredox" 3014 - version = "0.1.15" 3015 - source = "registry+https://github.com/rust-lang/crates.io-index" 3016 - checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" 3017 - dependencies = [ 3018 - "bitflags", 3019 - "libc", 3020 - "plain", 3021 - "redox_syscall 0.7.3", 3022 - ] 3023 - 3024 - [[package]] 3025 2906 name = "linked-hash-map" 3026 2907 version = "0.5.6" 3027 2908 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3248 3129 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 3249 3130 3250 3131 [[package]] 3251 - name = "mime_guess" 3252 - version = "2.0.5" 3253 - source = "registry+https://github.com/rust-lang/crates.io-index" 3254 - checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 3255 - dependencies = [ 3256 - "mime", 3257 - "unicase", 3258 - ] 3259 - 3260 - [[package]] 3261 3132 name = "mini-moka-wasm" 3262 3133 version = "0.10.99" 3263 3134 dependencies = [ ··· 3347 3218 "core2", 3348 3219 "serde", 3349 3220 "unsigned-varint 0.8.0", 3350 - ] 3351 - 3352 - [[package]] 3353 - name = "multipart" 3354 - version = "0.18.0" 3355 - source = "registry+https://github.com/rust-lang/crates.io-index" 3356 - checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" 3357 - dependencies = [ 3358 - "buf_redux", 3359 - "httparse", 3360 - "log", 3361 - "mime", 3362 - "mime_guess", 3363 - "quick-error 1.2.3", 3364 - "rand 0.8.5", 3365 - "safemem", 3366 - "tempfile", 3367 - "twoway", 3368 3221 ] 3369 3222 3370 3223 [[package]] ··· 3544 3397 ] 3545 3398 3546 3399 [[package]] 3547 - name = "num_cpus" 3548 - version = "1.17.0" 3549 - source = "registry+https://github.com/rust-lang/crates.io-index" 3550 - checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" 3551 - dependencies = [ 3552 - "hermit-abi", 3553 - "libc", 3554 - ] 3555 - 3556 - [[package]] 3557 - name = "num_threads" 3558 - version = "0.1.7" 3559 - source = "registry+https://github.com/rust-lang/crates.io-index" 3560 - checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 3561 - dependencies = [ 3562 - "libc", 3563 - ] 3564 - 3565 - [[package]] 3566 3400 name = "objc2" 3567 3401 version = "0.6.4" 3568 3402 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3689 3523 dependencies = [ 3690 3524 "cfg-if", 3691 3525 "libc", 3692 - "redox_syscall 0.5.18", 3526 + "redox_syscall", 3693 3527 "smallvec", 3694 3528 "windows-link", 3695 3529 ] ··· 3831 3665 version = "0.3.32" 3832 3666 source = "registry+https://github.com/rust-lang/crates.io-index" 3833 3667 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 3834 - 3835 - [[package]] 3836 - name = "plain" 3837 - version = "0.2.3" 3838 - source = "registry+https://github.com/rust-lang/crates.io-index" 3839 - checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" 3840 3668 3841 3669 [[package]] 3842 3670 name = "png" ··· 4239 4067 ] 4240 4068 4241 4069 [[package]] 4242 - name = "redox_syscall" 4243 - version = "0.7.3" 4244 - source = "registry+https://github.com/rust-lang/crates.io-index" 4245 - checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" 4246 - dependencies = [ 4247 - "bitflags", 4248 - ] 4249 - 4250 - [[package]] 4251 4070 name = "ref-cast" 4252 4071 version = "1.0.25" 4253 4072 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4308 4127 source = "registry+https://github.com/rust-lang/crates.io-index" 4309 4128 checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 4310 4129 dependencies = [ 4311 - "base64 0.22.1", 4130 + "base64", 4312 4131 "bytes", 4313 4132 "encoding_rs", 4314 4133 "futures-core", ··· 4401 4220 checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad" 4402 4221 4403 4222 [[package]] 4404 - name = "rouille" 4405 - version = "3.6.2" 4406 - source = "registry+https://github.com/rust-lang/crates.io-index" 4407 - checksum = "3716fbf57fc1084d7a706adf4e445298d123e4a44294c4e8213caf1b85fcc921" 4408 - dependencies = [ 4409 - "base64 0.13.1", 4410 - "brotli", 4411 - "chrono", 4412 - "deflate", 4413 - "filetime", 4414 - "multipart", 4415 - "percent-encoding", 4416 - "rand 0.8.5", 4417 - "serde", 4418 - "serde_derive", 4419 - "serde_json", 4420 - "sha1_smol", 4421 - "threadpool", 4422 - "time", 4423 - "tiny_http", 4424 - "url", 4425 - ] 4426 - 4427 - [[package]] 4428 4223 name = "rsa" 4429 4224 version = "0.9.10" 4430 4225 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4578 4373 checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" 4579 4374 4580 4375 [[package]] 4581 - name = "safemem" 4582 - version = "0.3.3" 4583 - source = "registry+https://github.com/rust-lang/crates.io-index" 4584 - checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 4585 - 4586 - [[package]] 4587 4376 name = "same-file" 4588 4377 version = "1.0.6" 4589 4378 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4809 4598 source = "registry+https://github.com/rust-lang/crates.io-index" 4810 4599 checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" 4811 4600 dependencies = [ 4812 - "base64 0.22.1", 4601 + "base64", 4813 4602 "chrono", 4814 4603 "hex", 4815 4604 "serde_core", ··· 4840 4629 "cpufeatures", 4841 4630 "digest", 4842 4631 ] 4843 - 4844 - [[package]] 4845 - name = "sha1_smol" 4846 - version = "1.0.1" 4847 - source = "registry+https://github.com/rust-lang/crates.io-index" 4848 - checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" 4849 4632 4850 4633 [[package]] 4851 4634 name = "sha2" ··· 5218 5001 ] 5219 5002 5220 5003 [[package]] 5221 - name = "threadpool" 5222 - version = "1.8.1" 5223 - source = "registry+https://github.com/rust-lang/crates.io-index" 5224 - checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" 5225 - dependencies = [ 5226 - "num_cpus", 5227 - ] 5228 - 5229 - [[package]] 5230 5004 name = "tiff" 5231 5005 version = "0.6.1" 5232 5006 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5259 5033 dependencies = [ 5260 5034 "deranged", 5261 5035 "itoa", 5262 - "libc", 5263 5036 "num-conv", 5264 - "num_threads", 5265 5037 "powerfmt", 5266 5038 "serde_core", 5267 5039 "time-core", ··· 5282 5054 dependencies = [ 5283 5055 "num-conv", 5284 5056 "time-core", 5285 - ] 5286 - 5287 - [[package]] 5288 - name = "tiny_http" 5289 - version = "0.12.0" 5290 - source = "registry+https://github.com/rust-lang/crates.io-index" 5291 - checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" 5292 - dependencies = [ 5293 - "ascii", 5294 - "chunked_transfer", 5295 - "httpdate", 5296 - "log", 5297 5057 ] 5298 5058 5299 5059 [[package]] ··· 5717 5477 ] 5718 5478 5719 5479 [[package]] 5720 - name = "twoway" 5721 - version = "0.1.8" 5722 - source = "registry+https://github.com/rust-lang/crates.io-index" 5723 - checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" 5724 - dependencies = [ 5725 - "memchr", 5726 - ] 5727 - 5728 - [[package]] 5729 5480 name = "typeid" 5730 5481 version = "1.0.3" 5731 5482 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5766 5517 version = "0.1.4" 5767 5518 source = "registry+https://github.com/rust-lang/crates.io-index" 5768 5519 checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" 5769 - 5770 - [[package]] 5771 - name = "unicase" 5772 - version = "2.9.0" 5773 - source = "registry+https://github.com/rust-lang/crates.io-index" 5774 - checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" 5775 5520 5776 5521 [[package]] 5777 5522 name = "unicode-ident" ··· 5913 5658 checksum = "0ae7c6870b98c838123f22cac9a594cbe2d74ea48d79271c08f8c9e680b40fac" 5914 5659 dependencies = [ 5915 5660 "ansi_colours", 5916 - "base64 0.22.1", 5661 + "base64", 5917 5662 "console", 5918 5663 "crossterm", 5919 5664 "image",
+3 -4
crates/jacquard-oauth/Cargo.toml
··· 14 14 15 15 [features] 16 16 default = [] 17 - loopback = ["dep:rouille"] 17 + loopback = [] 18 18 browser-open = ["dep:webbrowser"] 19 19 tracing = ["dep:tracing"] 20 20 websocket = ["jacquard-common/websocket"] ··· 52 52 tracing = { workspace = true, optional = true } 53 53 smallvec.workspace = true 54 54 55 - [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 56 - tokio = { workspace = true, features = ["rt", "net", "time"] } 57 - rouille = { version = "3.6.2", optional = true } 55 + [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] 56 + tokio = { workspace = true, features = ["rt", "net", "time", "io-util"] } 58 57 59 58 60 59 [target.'cfg(target_arch = "wasm32")'.dependencies]
+4
crates/jacquard-oauth/src/error.rs
··· 107 107 #[error("timeout")] 108 108 #[diagnostic(code(jacquard_oauth::callback::timeout))] 109 109 Timeout, 110 + /// The local loopback callback server could not start or accept callbacks. 111 + #[error("loopback callback server error: {0}")] 112 + #[diagnostic(code(jacquard_oauth::callback::loopback_server))] 113 + LoopbackServer(String), 110 114 /// An error occurred resolving permission sets during session creation. 111 115 #[cfg(feature = "scope-check")] 112 116 #[error("scope resolution failed: {detail}")]
+4 -1
crates/jacquard-oauth/src/lib.rs
··· 78 78 pub const FALLBACK_ALG: &str = "ES256"; 79 79 80 80 /// Loopback server helpers for the local redirect-based OAuth flow. 81 - #[cfg(feature = "loopback")] 81 + #[cfg(all( 82 + feature = "loopback", 83 + not(all(target_arch = "wasm32", target_os = "unknown")) 84 + ))] 82 85 pub mod loopback;
+188 -179
crates/jacquard-oauth/src/loopback.rs
··· 43 43 //! ``` 44 44 //! 45 45 //! 46 - #![cfg(feature = "loopback")] 46 + #![cfg(all( 47 + feature = "loopback", 48 + not(all(target_arch = "wasm32", target_os = "unknown")) 49 + ))] 47 50 use crate::{ 48 51 atproto::AtprotoClientMetadata, 49 52 authstore::{ClientAuthStore, OAuthSessionMatch}, ··· 57 60 use jacquard_common::deps::fluent_uri::Uri; 58 61 use jacquard_common::session::{SessionHint, SessionSelector, SessionStoreError}; 59 62 use jacquard_common::types::{did::Did, string::Handle}; 60 - use rouille::Server; 61 - use smol_str::{SmolStr, ToSmolStr}; 63 + use smol_str::SmolStr; 62 64 use std::net::SocketAddr; 63 - use tokio::sync::mpsc; 65 + use tokio::{ 66 + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, 67 + net::{TcpListener, TcpStream, ToSocketAddrs}, 68 + sync::{mpsc, oneshot}, 69 + }; 64 70 65 71 fn oauth_hint_from_input(input: &str) -> SessionHint<SmolStr> { 66 72 if let Ok(did) = Did::new(input) { ··· 118 124 false 119 125 } 120 126 121 - fn create_callback_router( 122 - request: &rouille::Request, 123 - tx: mpsc::Sender<CallbackParams>, 124 - ) -> rouille::Response { 125 - rouille::router!(request, 126 - (GET) (/oauth/callback) => { 127 - let state = request.get_param("state").unwrap(); 128 - let code = request.get_param("code").unwrap(); 129 - let iss = request.get_param("iss").unwrap(); 130 - let callback_params = CallbackParams { 131 - state: Some(state.to_smolstr()), 132 - code: code.to_smolstr(), 133 - iss: Some(iss.to_smolstr()), 134 - }; 135 - tx.try_send(callback_params).unwrap(); 136 - rouille::Response::text("Logged in!") 137 - }, 138 - _ => rouille::Response::empty_404() 139 - ) 127 + async fn handle_callback_connection(mut stream: TcpStream, tx: mpsc::Sender<CallbackParams>) { 128 + let Some(Some(params)) = read_callback_params(&mut stream).await else { 129 + let _ = write_http_response(&mut stream, 404, "Not found").await; 130 + return; 131 + }; 132 + 133 + match tx.try_send(params) { 134 + Ok(()) => { 135 + let _ = write_http_response(&mut stream, 200, "Logged in!").await; 136 + } 137 + Err(_) => { 138 + let _ = write_http_response(&mut stream, 500, "Could not deliver OAuth callback").await; 139 + } 140 + } 141 + } 142 + 143 + async fn read_callback_params(stream: &mut TcpStream) -> Option<Option<CallbackParams>> { 144 + let mut reader = BufReader::new(stream); 145 + let mut request_line = String::new(); 146 + reader.read_line(&mut request_line).await.ok()?; 147 + let mut parts = request_line.split_whitespace(); 148 + let method = parts.next()?; 149 + let target = parts.next()?; 150 + if method != "GET" { 151 + return Some(None); 152 + } 153 + let (path, query) = target.split_once('?').unwrap_or((target, "")); 154 + if path != "/oauth/callback" { 155 + return Some(None); 156 + } 157 + serde_html_form::from_str(query).ok().map(Some) 158 + } 159 + 160 + async fn write_http_response( 161 + stream: &mut TcpStream, 162 + status: u16, 163 + body: &str, 164 + ) -> std::io::Result<()> { 165 + let reason = match status { 166 + 200 => "OK", 167 + 404 => "Not Found", 168 + 500 => "Internal Server Error", 169 + _ => "OK", 170 + }; 171 + let response = format!( 172 + "HTTP/1.1 {status} {reason}\r\ncontent-type: text/plain; charset=utf-8\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", 173 + body.len(), 174 + body 175 + ); 176 + stream.write_all(response.as_bytes()).await 140 177 } 141 178 142 179 /// Handle to a running loopback callback server, used to await the OAuth redirect. 143 180 pub struct CallbackHandle { 144 - #[allow(dead_code)] 145 - server_handle: std::thread::JoinHandle<()>, 146 - server_stop: std::sync::mpsc::Sender<()>, 181 + server_handle: tokio::task::JoinHandle<()>, 182 + server_stop: oneshot::Sender<()>, 147 183 callback_rx: mpsc::Receiver<CallbackParams>, 148 184 } 149 185 ··· 155 191 /// 156 192 /// Use in combination with [`handle_localhost_callback`] to handle the 157 193 /// callback for the localhost loopback server. 158 - pub fn one_shot_server(addr: SocketAddr) -> (SocketAddr, CallbackHandle) { 194 + pub async fn one_shot_server( 195 + addr: impl ToSocketAddrs, 196 + ) -> std::io::Result<(SocketAddr, CallbackHandle)> { 159 197 let (tx, callback_rx) = mpsc::channel(5); 160 - let server = Server::new(addr, move |request| { 161 - create_callback_router(request, tx.clone()) 162 - }) 163 - .expect("Could not start server"); 164 - let (server_handle, server_stop) = server.stoppable(); 198 + let listener = TcpListener::bind(addr).await?; 199 + let local_addr = listener.local_addr()?; 200 + let (server_stop, mut stop_rx) = oneshot::channel(); 201 + let server_handle = tokio::spawn(async move { 202 + loop { 203 + tokio::select! { 204 + _ = &mut stop_rx => break, 205 + accepted = listener.accept() => { 206 + match accepted { 207 + Ok((stream, _)) => { 208 + tokio::spawn(handle_callback_connection(stream, tx.clone())); 209 + } 210 + Err(_) => break, 211 + } 212 + } 213 + } 214 + } 215 + }); 165 216 let handle = CallbackHandle { 166 217 server_handle, 167 218 server_stop, 168 219 callback_rx, 169 220 }; 170 - (addr, handle) 221 + Ok((local_addr, handle)) 222 + } 223 + 224 + async fn wait_for_callback( 225 + handle: CallbackHandle, 226 + timeout_ms: u64, 227 + ) -> Result<CallbackParams, OAuthError> { 228 + let CallbackHandle { 229 + server_handle, 230 + server_stop, 231 + mut callback_rx, 232 + } = handle; 233 + let cb = tokio::time::timeout( 234 + std::time::Duration::from_millis(timeout_ms), 235 + callback_rx.recv(), 236 + ) 237 + .await; 238 + let _ = server_stop.send(()); 239 + let _ = server_handle.await; 240 + if let Ok(Some(cb)) = cb { 241 + Ok(cb) 242 + } else { 243 + Err(OAuthError::Callback(CallbackError::Timeout)) 244 + } 171 245 } 172 246 173 247 /// Handles the OAuth callback for the localhost loopback server. ··· 187 261 T: OAuthResolver + DpopExt + Send + Sync + 'static, 188 262 S: ClientAuthStore + Send + Sync + 'static, 189 263 { 190 - // Await callback or timeout 191 - let mut callback_rx = handle.callback_rx; 192 - let cb = tokio::time::timeout( 193 - std::time::Duration::from_millis(cfg.timeout_ms), 194 - callback_rx.recv(), 195 - ) 196 - .await; 197 - // trigger shutdown 198 - let _ = handle.server_stop.send(()); 199 - if let Ok(Some(cb)) = cb { 200 - // Handle callback and create a session 201 - Ok(flow_client.callback(cb).await?) 202 - } else { 203 - Err(OAuthError::Callback(CallbackError::Timeout)) 204 - } 264 + Ok(flow_client 265 + .callback(wait_for_callback(handle, cfg.timeout_ms).await?) 266 + .await?) 205 267 } 206 268 207 269 /// Handles the OAuth callback for the localhost loopback server. ··· 226 288 + 'static, 227 289 S: ClientAuthStore + Send + Sync + 'static, 228 290 { 229 - // Await callback or timeout 230 - let mut callback_rx = handle.callback_rx; 231 - let cb = tokio::time::timeout( 232 - std::time::Duration::from_millis(cfg.timeout_ms), 233 - callback_rx.recv(), 234 - ) 235 - .await; 236 - // trigger shutdown 237 - let _ = handle.server_stop.send(()); 238 - if let Ok(Some(cb)) = cb { 239 - // Handle callback and create a session 240 - Ok(flow_client.callback(cb).await?) 291 + Ok(flow_client 292 + .callback(wait_for_callback(handle, cfg.timeout_ms).await?) 293 + .await?) 294 + } 295 + 296 + fn loopback_port(cfg: &LoopbackConfig) -> u16 { 297 + match cfg.port { 298 + LoopbackPort::Fixed(port) => port, 299 + LoopbackPort::Ephemeral => 0, 300 + } 301 + } 302 + 303 + fn redirect_host(host: &str) -> String { 304 + if host.contains(':') && !host.starts_with('[') { 305 + format!("[{host}]") 241 306 } else { 242 - Err(OAuthError::Callback(CallbackError::Timeout)) 307 + host.to_owned() 243 308 } 244 309 } 245 310 246 - #[cfg(not(feature = "scope-check"))] 247 311 impl<T, S> OAuthClient<T, S> 248 312 where 249 313 T: OAuthResolver + DpopExt + Send + Sync + 'static, 250 314 S: ClientAuthStore + Send + Sync + 'static, 251 315 { 252 - /// Drive the full OAuth flow using a local loopback server. 253 - /// 254 - /// This uses localhost OAuth and an ephemeral in-process web server to 255 - /// handle the OAuth callback redirect. It has a bunch of nice friendly 256 - /// defaults to help you get started and will basically drive the *entire* 257 - /// callback flow itself. 258 - /// 259 - /// Best used for development and for small CLI applications that don't 260 - /// require long session lengths. For long-running unattended sessions, 261 - /// app passwords (via CredentialSession in the jacquard crate) remain 262 - /// the best option. For more complex OAuth, or if you want more control 263 - /// over the process, use the other methods on OAuthClient. 264 - /// 265 - /// 'input' parameter is what you type in the login box (usually, your handle) 266 - /// for it to look up your PDS and redirect to its authentication interface. 267 - /// 268 - /// If the `browser-open` feature is enabled, this will open a web browser 269 - /// for you to authenticate with your PDS. It will also print the 270 - /// callback url to the console for you to copy. 271 - pub async fn login_with_local_server( 316 + async fn start_loopback_flow( 272 317 &self, 273 - input: impl AsRef<str>, 318 + input: &str, 274 319 opts: AuthorizeOptions<SmolStr>, 275 - cfg: LoopbackConfig, 276 - ) -> crate::error::Result<super::client::OAuthSession<T, S>> { 277 - let port = match cfg.port { 278 - LoopbackPort::Fixed(p) => p, 279 - LoopbackPort::Ephemeral => 0, 280 - }; 281 - // TODO: fix this to it also accepts ipv6 and properly finds a free port 282 - let bind_addr: SocketAddr = format!("0.0.0.0:{}", port) 283 - .parse() 284 - .expect("invalid loopback host/port"); 285 - let (local_addr, handle) = one_shot_server(bind_addr); 320 + cfg: &LoopbackConfig, 321 + ) -> crate::error::Result<(OAuthClient<T, S>, CallbackHandle)> { 322 + let (local_addr, handle) = one_shot_server((cfg.host.as_str(), loopback_port(cfg))) 323 + .await 324 + .map_err(|err| OAuthError::Callback(CallbackError::LoopbackServer(err.to_string())))?; 286 325 println!("Listening on {}", local_addr); 287 326 288 - let client_data = self.build_localhost_client_data(&cfg, &opts, local_addr); 289 - // Build client using store and resolver 327 + let client_data = self.build_localhost_client_data(cfg, &opts, local_addr); 290 328 let flow_client = OAuthClient::new_with_shared( 291 329 self.registry.store.clone(), 292 330 self.client.clone(), 293 331 client_data, 294 332 ); 295 333 296 - // Start auth and get authorization URL 297 - let auth_url = flow_client.start_auth(input.as_ref(), opts).await?; 298 - // Print URL for copy/paste 334 + let auth_url = flow_client.start_auth(input, opts).await?; 299 335 println!("To authenticate with your PDS, visit:\n{}\n", auth_url); 300 - // Optionally open browser 301 336 if cfg.open_browser { 302 337 let _ = try_open_in_browser(&auth_url); 303 338 } 304 339 305 - handle_localhost_callback(handle, &flow_client, &cfg).await 340 + Ok((flow_client, handle)) 306 341 } 307 342 308 343 /// Builds a [`crate::session::ClientData`] for use with the local loopback server method of OAuth. ··· 312 347 opts: &AuthorizeOptions<SmolStr>, 313 348 local_addr: SocketAddr, 314 349 ) -> crate::session::ClientData<SmolStr> { 315 - let redirect_uri = format!("http://{}:{}/oauth/callback", cfg.host, local_addr.port(),); 350 + let redirect_uri = format!( 351 + "http://{}:{}/oauth/callback", 352 + redirect_host(&cfg.host), 353 + local_addr.port(), 354 + ); 316 355 let redirect = Uri::parse(redirect_uri).unwrap(); 317 356 318 357 let scopes = if opts.scopes.is_empty() { ··· 328 367 .into_static() 329 368 } 330 369 370 + async fn restore_matching_session( 371 + &self, 372 + input: &str, 373 + ) -> crate::error::Result<Option<super::client::OAuthSession<T, S>>> 374 + where 375 + S: SessionSelector<OAuthSessionMatch, Error = SessionStoreError>, 376 + { 377 + let hint = oauth_hint_from_input(input); 378 + if let Some(matched) = self.registry.store.select_session(&hint).await? { 379 + Ok(Some( 380 + self.restore(&matched.key.did, matched.key.session_id.as_str()) 381 + .await?, 382 + )) 383 + } else { 384 + Ok(None) 385 + } 386 + } 387 + } 388 + 389 + #[cfg(not(feature = "scope-check"))] 390 + impl<T, S> OAuthClient<T, S> 391 + where 392 + T: OAuthResolver + DpopExt + Send + Sync + 'static, 393 + S: ClientAuthStore + Send + Sync + 'static, 394 + { 395 + /// Drive the full OAuth flow using a local loopback server. 396 + /// 397 + /// This uses localhost OAuth and an ephemeral in-process web server to 398 + /// handle the OAuth callback redirect. It has friendly defaults to drive 399 + /// the entire callback flow for development and small CLI applications. 400 + pub async fn login_with_local_server( 401 + &self, 402 + input: impl AsRef<str>, 403 + opts: AuthorizeOptions<SmolStr>, 404 + cfg: LoopbackConfig, 405 + ) -> crate::error::Result<super::client::OAuthSession<T, S>> { 406 + let (flow_client, handle) = self.start_loopback_flow(input.as_ref(), opts, &cfg).await?; 407 + handle_localhost_callback(handle, &flow_client, &cfg).await 408 + } 409 + 331 410 /// Resume a stored session for the input identity, or drive the full OAuth flow using a local loopback server. 332 411 pub async fn resume_or_login_with_local_server( 333 412 &self, ··· 339 418 S: SessionSelector<OAuthSessionMatch, Error = SessionStoreError>, 340 419 { 341 420 let input_ref = input.as_ref(); 342 - let hint = oauth_hint_from_input(input_ref); 343 - if let Some(matched) = self.registry.store.select_session(&hint).await? { 344 - return self 345 - .restore(&matched.key.did, matched.key.session_id.as_str()) 346 - .await; 421 + if let Some(session) = self.restore_matching_session(input_ref).await? { 422 + return Ok(session); 347 423 } 348 424 self.login_with_local_server(input_ref, opts, cfg).await 349 425 } ··· 363 439 /// Drive the full OAuth flow using a local loopback server. 364 440 /// 365 441 /// This uses localhost OAuth and an ephemeral in-process web server to 366 - /// handle the OAuth callback redirect. It has a bunch of nice friendly 367 - /// defaults to help you get started and will basically drive the *entire* 368 - /// callback flow itself. 369 - /// 370 - /// Best used for development and for small CLI applications that don't 371 - /// require long session lengths. For long-running unattended sessions, 372 - /// app passwords (via CredentialSession in the jacquard crate) remain 373 - /// the best option. For more complex OAuth, or if you want more control 374 - /// over the process, use the other methods on OAuthClient. 375 - /// 376 - /// 'input' parameter is what you type in the login box (usually, your handle) 377 - /// for it to look up your PDS and redirect to its authentication interface. 378 - /// 379 - /// If the `browser-open` feature is enabled, this will open a web browser 380 - /// for you to authenticate with your PDS. It will also print the 381 - /// callback url to the console for you to copy. 442 + /// handle the OAuth callback redirect. It has friendly defaults to drive 443 + /// the entire callback flow for development and small CLI applications. 382 444 pub async fn login_with_local_server( 383 445 &self, 384 446 input: impl AsRef<str>, 385 447 opts: AuthorizeOptions<SmolStr>, 386 448 cfg: LoopbackConfig, 387 449 ) -> crate::error::Result<super::client::OAuthSession<T, S>> { 388 - let port = match cfg.port { 389 - LoopbackPort::Fixed(p) => p, 390 - LoopbackPort::Ephemeral => 0, 391 - }; 392 - // TODO: fix this to it also accepts ipv6 and properly finds a free port 393 - let bind_addr: SocketAddr = format!("0.0.0.0:{}", port) 394 - .parse() 395 - .expect("invalid loopback host/port"); 396 - let (local_addr, handle) = one_shot_server(bind_addr); 397 - println!("Listening on {}", local_addr); 398 - 399 - let client_data = self.build_localhost_client_data(&cfg, &opts, local_addr); 400 - // Build client using store and resolver 401 - let flow_client = OAuthClient::new_with_shared( 402 - self.registry.store.clone(), 403 - self.client.clone(), 404 - client_data, 405 - ); 406 - 407 - // Start auth and get authorization URL 408 - let auth_url = flow_client.start_auth(input.as_ref(), opts).await?; 409 - // Print URL for copy/paste 410 - println!("To authenticate with your PDS, visit:\n{}\n", auth_url); 411 - // Optionally open browser 412 - if cfg.open_browser { 413 - let _ = try_open_in_browser(&auth_url); 414 - } 415 - 450 + let (flow_client, handle) = self.start_loopback_flow(input.as_ref(), opts, &cfg).await?; 416 451 handle_localhost_callback(handle, &flow_client, &cfg).await 417 452 } 418 453 ··· 427 462 S: SessionSelector<OAuthSessionMatch, Error = SessionStoreError>, 428 463 { 429 464 let input_ref = input.as_ref(); 430 - let hint = oauth_hint_from_input(input_ref); 431 - if let Some(matched) = self.registry.store.select_session(&hint).await? { 432 - return self 433 - .restore(&matched.key.did, matched.key.session_id.as_str()) 434 - .await; 465 + if let Some(session) = self.restore_matching_session(input_ref).await? { 466 + return Ok(session); 435 467 } 436 468 self.login_with_local_server(input_ref, opts, cfg).await 437 - } 438 - 439 - /// Builds a [`crate::session::ClientData`] for use with the local loopback server method of OAuth. 440 - pub fn build_localhost_client_data( 441 - &self, 442 - cfg: &LoopbackConfig, 443 - opts: &AuthorizeOptions<SmolStr>, 444 - local_addr: SocketAddr, 445 - ) -> crate::session::ClientData<SmolStr> { 446 - let redirect_uri = format!("http://{}:{}/oauth/callback", cfg.host, local_addr.port(),); 447 - let redirect = Uri::parse(redirect_uri).unwrap(); 448 - 449 - let scopes = if opts.scopes.is_empty() { 450 - Some(self.registry.client_data.config.scopes.clone()) 451 - } else { 452 - Some(opts.scopes.clone()) 453 - }; 454 - 455 - crate::session::ClientData { 456 - keyset: self.registry.client_data.keyset.clone(), 457 - config: AtprotoClientMetadata::new_localhost(Some(vec![redirect]), scopes), 458 - } 459 - .into_static() 460 469 } 461 470 }