From 2e54681af0bc76ed22ce43f8126a029e600ece93 Mon Sep 17 00:00:00 2001 From: Vika Date: Mon, 21 Feb 2022 07:34:55 +0300 Subject: Add a module for IndieAuth bearer token auth require_token() uses a token endpoint URI and an HTTP client to query the token endpoint and return a User object if the user was authorized, or rejecting with IndieAuthError if not. It is recommended to use recover() and catch the IndieAuthError at the application level to show a "not authorized" error message to the user. This function is more intended for API consumption, but is general enough to permit using in other scenarios. TODO: make a variant that returns Option instead of rejecting --- Cargo.lock | 535 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 8 +- flake.nix | 10 +- src/indieauth.rs | 446 +++++++++++++++++++++++++--------------------- src/lib.rs | 10 +- 5 files changed, 798 insertions(+), 211 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b054ff..6694e2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,15 @@ version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + [[package]] name = "assert-json-diff" version = "2.0.1" @@ -203,6 +212,32 @@ dependencies = [ "event-listener", ] +[[package]] +name = "async-object-pool" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb901c30ebc2fc4ab46395bbfbdba9542c16559d853645d75190c3056caf3bc" +dependencies = [ + "async-std", +] + +[[package]] +name = "async-process" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83137067e3a2a6a06d67168e49e68a0957d215410473a740cea95a2425c0b7c6" +dependencies = [ + "async-io", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "libc", + "once_cell", + "signal-hook", + "winapi 0.3.9", +] + [[package]] name = "async-std" version = "1.10.0" @@ -213,6 +248,7 @@ dependencies = [ "async-global-executor", "async-io", "async-lock", + "async-process", "crossbeam-utils", "futures-channel", "futures-core", @@ -293,6 +329,32 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "basic-cookies" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb53b6b315f924c7f113b162e53b3901c05fc9966baf84d201dfcc7432a4bb38" +dependencies = [ + "lalrpop", + "lalrpop-util", + "regex 1.5.4", +] + +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -386,6 +448,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" +[[package]] +name = "castaway" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" + [[package]] name = "cc" version = "1.0.72" @@ -518,6 +586,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.2" @@ -583,6 +657,37 @@ dependencies = [ "cipher", ] +[[package]] +name = "curl" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de97b894edd5b5bcceef8b78d7da9b75b1d2f2f9a910569d0bde3dd31d84939" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "winapi 0.3.9", +] + +[[package]] +name = "curl-sys" +version = "0.4.52+curl-7.81.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b8c2d1023ea5fded5b7b892e4b8e95f70038a421126a056761a84246a28971" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "winapi 0.3.9", +] + [[package]] name = "data-encoding" version = "2.3.2" @@ -602,6 +707,12 @@ dependencies = [ "syn", ] +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + [[package]] name = "difference" version = "2.0.0" @@ -627,6 +738,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "discard" version = "1.0.4" @@ -659,6 +791,12 @@ dependencies = [ "regex 1.5.4", ] +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "ellipse" version = "0.2.0" @@ -668,6 +806,24 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "ena" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3" +dependencies = [ + "log 0.4.14", +] + +[[package]] +name = "encoding_rs" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.3.5" @@ -717,6 +873,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + [[package]] name = "flate2" version = "1.0.22" @@ -1084,6 +1246,34 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "httpmock" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c159c4fc205e6c1a9b325cb7ec135d13b5f47188ce175dabb76ec847f331d9bd" +dependencies = [ + "assert-json-diff", + "async-object-pool", + "async-trait", + "base64", + "basic-cookies", + "crossbeam-utils", + "form_urlencoded", + "futures-util", + "hyper", + "isahc", + "lazy_static", + "levenshtein", + "log 0.4.14", + "regex 1.5.4", + "serde", + "serde_json", + "serde_regex", + "similar", + "tokio", + "url", +] + [[package]] name = "humantime" version = "2.1.0" @@ -1114,6 +1304,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +dependencies = [ + "http", + "hyper", + "log 0.4.14", + "rustls", + "tokio", + "tokio-rustls", + "webpki-roots", +] + [[package]] name = "idna" version = "0.2.3" @@ -1150,6 +1355,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "isahc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d140e84730d325378912ede32d7cd53ef1542725503b3353e5ec8113c7c6f588" +dependencies = [ + "async-channel", + "castaway", + "crossbeam-utils", + "curl", + "curl-sys", + "encoding_rs", + "event-listener", + "futures-lite", + "http", + "log 0.4.14", + "mime", + "once_cell", + "polling", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", +] + +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -1187,6 +1428,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "bytes", "chrono", "data-encoding", "easy-scraper", @@ -1196,7 +1438,9 @@ dependencies = [ "futures", "futures-util", "http-types", + "httpmock", "hyper", + "hyper-rustls", "lazy_static", "log 0.4.14", "markdown", @@ -1241,18 +1485,78 @@ dependencies = [ "log 0.4.14", ] +[[package]] +name = "lalrpop" +version = "0.19.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852b75a095da6b69da8c5557731c3afd06525d4f655a4fc1c799e2ec8bc4dce4" +dependencies = [ + "ascii-canvas", + "atty", + "bit-set", + "diff", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex 1.5.4", + "regex-syntax 0.6.25", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.19.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6d265705249fe209280676d8f68887859fa42e1d34f342fc05bd47726a5e188" +dependencies = [ + "regex 1.5.4", +] + [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "levenshtein" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" + [[package]] name = "libc" version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" +[[package]] +name = "libnghttp2-sys" +version = "0.1.7+1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "libz-sys" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.5" @@ -1506,6 +1810,25 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking" version = "2.0.0" @@ -1549,6 +1872,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "phf" version = "0.8.0" @@ -1603,6 +1936,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pico-args" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" + [[package]] name = "pin-project" version = "1.0.10" @@ -1641,6 +1980,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d15b6607fa632996eb8a17c9041cb6071cb75ac057abd45dece578723ea8c7c0" +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + [[package]] name = "polling" version = "2.2.0" @@ -1901,6 +2246,16 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.3", + "redox_syscall", +] + [[package]] name = "regex" version = "0.1.80" @@ -1964,6 +2319,21 @@ dependencies = [ "rand 0.8.4", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "rustc_version" version = "0.2.3" @@ -1982,6 +2352,24 @@ dependencies = [ "semver 1.0.4", ] +[[package]] +name = "rustls" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b323592e3164322f5b193dc4302e4e36cd8d37158a712d664efae1a5c2791700" +dependencies = [ + "log 0.4.14", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + [[package]] name = "ryu" version = "1.0.6" @@ -1994,6 +2382,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + [[package]] name = "scoped-tls" version = "1.0.0" @@ -2006,6 +2404,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "selectors" version = "0.22.0" @@ -2089,6 +2497,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex 1.5.4", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.0" @@ -2141,6 +2559,16 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "signal-hook" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -2150,6 +2578,12 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3" + [[package]] name = "siphasher" version = "0.3.7" @@ -2162,6 +2596,17 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +[[package]] +name = "sluice" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" +dependencies = [ + "async-channel", + "futures-core", + "futures-io", +] + [[package]] name = "smallvec" version = "1.7.0" @@ -2178,6 +2623,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2320,6 +2771,17 @@ dependencies = [ "utf-8", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi 0.3.9", +] + [[package]] name = "termcolor" version = "1.1.2" @@ -2432,6 +2894,15 @@ dependencies = [ "syn", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.5.1" @@ -2477,6 +2948,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + [[package]] name = "tokio-stream" version = "0.1.8" @@ -2517,9 +2999,21 @@ dependencies = [ "cfg-if", "log 0.4.14", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.22" @@ -2529,6 +3023,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -2596,6 +3100,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.2.2" @@ -2631,6 +3141,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.3" @@ -2771,6 +3287,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449" +dependencies = [ + "webpki", +] + [[package]] name = "wepoll-ffi" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index 8b43c63..88eb6d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,9 +30,11 @@ mockito = "^0.30.0" # HTTP mocking for Rust. tempdir = "^0.3.7" # A library for managing a temporary directory and deleting all contents when it's dropped paste = "^1.0.5" # Macros for all your token pasting needs test-logger = "^0.1.0" # Simple helper to initialize env_logger before unit and integration tests +httpmock = "^0.6" # HTTP mocking library that allows you to simulate responses from HTTP based services [dependencies] async-trait = "^0.1.50" # Type erasure for async trait methods +bytes = "^1.1.0" data-encoding = "^2.3.2" # Efficient and customizable data-encoding functions like base64, base32, and hex easy-scraper = "^0.2.0" # HTML scraping library focused on ease of use ellipse = "^0.2.0" # Truncate and ellipsize strings in a human-friendly way @@ -80,4 +82,8 @@ default-features = false features = ["multipart", "compression"] [dependencies.hyper] version = "^0.14.17" -features = ["client", "stream", "runtime"] \ No newline at end of file +features = ["client", "stream", "runtime"] +[dependencies.hyper-rustls] +version = "^0.23.0" +default-features = false +features = ["webpki-tokio", "http1", "http2", "tls12", "logging"] \ No newline at end of file diff --git a/flake.nix b/flake.nix index 18d9751..c1827cd 100644 --- a/flake.nix +++ b/flake.nix @@ -187,14 +187,17 @@ }; rust-bin = pkgs.rust-bin.stable.latest; packages = { - kittybox = { stdenv, lib, redis, naersk-lib }: + kittybox = { stdenv, lib, openssl, pkg-config, naersk-lib }: naersk-lib.buildPackage { pname = "kittybox"; version = "0.1.0"; src = ./.; - checkInputs = [ redis ]; + checkInputs = [ openssl.dev ]; + nativeBuildInputs = [ pkg-config ]; + nativeCheckInputs = [ pkg-config ]; + doCheck = stdenv.hostPlatform == stdenv.targetPlatform; meta = with lib.meta; { @@ -259,9 +262,10 @@ name = "rust-dev-shell"; nativeBuildInputs = with pkgs; [ pkg-config lld + # required for httpmock, not actually used + openssl.dev (rust-bin.default.override { extensions = [ "rust-src" ]; }) (rust-analyzer.override { rustPlatform = with rust-bin; { rustLibSrc = rust-src; }; }) - redis ]; }; }); diff --git a/src/indieauth.rs b/src/indieauth.rs index f8f862b..305452a 100644 --- a/src/indieauth.rs +++ b/src/indieauth.rs @@ -1,14 +1,6 @@ -use async_trait::async_trait; -#[allow(unused_imports)] -use log::{error, info}; -use std::sync::Arc; -use tide::prelude::*; -#[allow(unused_imports)] -use tide::{Next, Request, Response, Result}; use url::Url; - -use crate::database; -use crate::ApplicationState; +use serde::{Serialize, Deserialize}; +use warp::{Filter, Rejection}; #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub struct User { @@ -17,6 +9,71 @@ pub struct User { scope: String, } +#[derive(Debug, Clone, PartialEq, Copy)] +pub enum ErrorKind { + PermissionDenied, + NotAuthorized, + TokenEndpointError, + JsonParsing, + Other +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct TokenEndpointError { + error: String, + error_description: String +} + +#[derive(Debug)] +pub struct IndieAuthError { + source: Option>, + kind: ErrorKind, + msg: String +} + +impl std::error::Error for IndieAuthError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source.as_ref().map(|e| e.as_ref() as &dyn std::error::Error) + } +} + +impl std::fmt::Display for IndieAuthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match match self.kind { + ErrorKind::TokenEndpointError => write!(f, "token endpoint returned an error: "), + ErrorKind::JsonParsing => write!(f, "error while parsing token endpoint response: "), + ErrorKind::NotAuthorized => write!(f, "token endpoint did not recognize the token: "), + ErrorKind::PermissionDenied => write!(f, "token endpoint rejected the token: "), + ErrorKind::Other => write!(f, "token endpoint communication error: "), + } { + Ok(_) => write!(f, "{}", self.msg), + Err(err) => Err(err) + } + } +} + +impl From for IndieAuthError { + fn from(err: serde_json::Error) -> Self { + Self { + msg: format!("{}", err), + source: Some(Box::new(err)), + kind: ErrorKind::JsonParsing, + } + } +} + +impl From for IndieAuthError { + fn from(err: hyper::Error) -> Self { + Self { + msg: format!("{}", err), + source: Some(Box::new(err)), + kind: ErrorKind::Other, + } + } +} + +impl warp::reject::Reject for IndieAuthError {} + impl User { pub fn check_scope(&self, scope: &str) -> bool { self.scopes().any(|i| i == scope) @@ -33,207 +90,106 @@ impl User { } } -#[cfg(any(not(debug_assertions), test))] -async fn get_token_data( - token: String, - token_endpoint: &http_types::Url, - http_client: &surf::Client, -) -> (http_types::StatusCode, Option) { - match http_client - .get(token_endpoint) - .header("Authorization", token) - .header("Accept", "application/json") - .send() - .await - { - Ok(mut resp) => { - if resp.status() == 200 { - match resp.body_json::().await { - Ok(user) => { - info!( - "Token endpoint request successful. Validated user: {}", - user.me - ); - (resp.status(), Some(user)) - } - Err(err) => { - error!( - "Token endpoint parsing error (HTTP status {}): {}", - resp.status(), - err - ); - (http_types::StatusCode::InternalServerError, None) - } - } - } else { - error!("Token endpoint returned non-200: {}", resp.status()); - (resp.status(), None) - } - } - Err(err) => { - error!("Token endpoint connection error: {}", err); - (http_types::StatusCode::InternalServerError, None) - } - } -} +// TODO: consider making this a generic +type HttpClient = hyper::Client>, hyper::Body>; -pub struct IndieAuthMiddleware { - #[allow(dead_code)] // it's not really dead since it's only dead in debug scope - cache: Arc>, - monitor_task: Option>, -} -impl IndieAuthMiddleware { - /// Create a new instance of IndieAuthMiddleware. - /// - /// Note that creating a new instance automatically launches a task - /// to garbage-collect stale cache entries. Please do not create - /// instances willy-nilly because of that. - pub fn new() -> Self { - let cache: Arc> = Arc::new(retainer::Cache::new()); - let cache_clone = cache.clone(); - let task = async_std::task::spawn(async move { - cache_clone - .monitor(4, 0.1, std::time::Duration::from_secs(30)) - .await - }); - - #[cfg(all(debug_assertions, not(test)))] - error!("ATTENTION: You are running in debug mode. NO REQUESTS TO TOKEN ENDPOINT WILL BE MADE. YOU WILL BE PROCEEDING WITH DEBUG USER CREDENTIALS. DO NOT RUN LIKE THIS IN PRODUCTION."); +pub fn require_token(token_endpoint: String, http: HttpClient) -> impl Filter { + // It might be OK to panic here, because we're still inside the initialisation sequence for now. + // Proper error handling on the top of this should be used though. + let token_endpoint_uri = hyper::Uri::try_from(&token_endpoint) + .expect("Couldn't parse the token endpoint URI!"); + warp::any() + .map(move || token_endpoint_uri.clone()) + .and(warp::any().map(move || http.clone())) + .and(warp::header::("Authorization")) + .and_then(|token_endpoint, http: HttpClient, token| async move { + let request = hyper::Request::builder() + .method(hyper::Method::GET) + .uri(token_endpoint) + .header("Authorization", token) + .header("Accept", "application/json") + .body(hyper::Body::from("")) + // TODO is it acceptable to panic here? + .unwrap(); - Self { - cache, - monitor_task: Some(task), - } - } -} -impl Drop for IndieAuthMiddleware { - fn drop(&mut self) { - // Cancel the task, or a VERY FUNNY thing might occur. - // If I understand this correctly, keeping a task active - // WILL keep an active reference to a value, so I'm pretty sure - // that something VERY FUNNY might occur whenever `cache` is dropped - // and its related task is not cancelled. So let's cancel it so - // [`cache`] can be dropped once and for all. - - // First, get the ownership of a task, sneakily switching it out with None - // (wow, this is sneaky, didn't know Safe Rust could even do that!!!) - // (it is safe tho cuz None is no nullptr and dereferencing it doesn't cause unsafety) - // (could cause a VERY FUNNY race condition to occur though - // if you tried to refer to the value in another thread!) - let task = std::mem::take(&mut self.monitor_task) - .expect("Dropped IndieAuthMiddleware TWICE? Impossible!"); - // Then cancel the task, using another task to request cancellation. - // Because apparently you can't run async code from Drop... - // This should drop the last reference for the [`cache`], - // allowing it to be dropped. - async_std::task::spawn(async move { task.cancel().await }); - } -} -#[async_trait] -impl tide::Middleware> for IndieAuthMiddleware -where - B: database::Storage + Send + Sync + Clone, -{ - #[cfg(all(not(test), debug_assertions))] - async fn handle( - &self, - mut req: Request>, - next: Next<'_, ApplicationState>, - ) -> Result { - req.set_ext(User::new( - "https://localhost:8080/", - "https://curl.haxx.se/", - "create update delete undelete media", - )); - Ok(next.run(req).await) - } - #[cfg(any(not(debug_assertions), test))] - async fn handle( - &self, - mut req: Request>, - next: Next<'_, ApplicationState>, - ) -> Result { - let header = req.header("Authorization"); - match header { - None => { - // TODO: move that to the request handling functions - // or make a middleware that refuses to accept unauthenticated requests - Ok(Response::builder(401) - .body(json!({ - "error": "unauthorized", - "error_description": "Please provide an access token." - })) - .build()) - } - Some(value) => { - match &req.state().internal_token { - Some(token) => { - if token - == &value - .last() - .to_string() - .split(' ') - .skip(1) - .collect::() - { - req.set_ext::(User::new( - "", // no user ID here - "https://kittybox.fireburn.ru/", - "update delete undelete media kittybox_internal:do_what_thou_wilt", - )); - return Ok(next.run(req).await); + use hyper::StatusCode; + + match http.request(request).await { + Ok(mut res) => match res.status() { + StatusCode::OK => { + use hyper::body::HttpBody; + use bytes::BufMut; + let mut buf: Vec = Vec::default(); + while let Some(chunk) = res.body_mut().data().await { + if let Err(err) = chunk { + return Err(IndieAuthError::from(err).into()); + } + buf.put(chunk.unwrap()); + } + match serde_json::from_slice(&buf) { + Ok(user) => Ok(user), + Err(err) => { + if let Ok(json) = serde_json::from_slice::(&buf) { + if Some(false) == json["active"].as_bool() { + Err(IndieAuthError { + source: None, + kind: ErrorKind::NotAuthorized, + msg: "The token endpoint deemed the token as not \"active\".".to_string() + }.into()) + } else { + Err(IndieAuthError::from(err).into()) + } + } else { + Err(IndieAuthError::from(err).into()) + } + } } - } - None => {} - } - let endpoint = &req.state().token_endpoint; - let http_client = &req.state().http_client; - let token = value.last().to_string(); - match self.cache.get(&token).await { - Some(user) => { - req.set_ext::(user.clone()); - Ok(next.run(req).await) }, - None => match get_token_data(value.last().to_string(), endpoint, http_client).await { - (http_types::StatusCode::Ok, Some(user)) => { - // Note that this can run multiple requests before the value appears in the cache. - // This seems to be in line with some other implementations of a function cache - // (e.g. the [`cached`](https://lib.rs/crates/cached) crate and Python's `functools.lru_cache`) - // - // TODO: ensure the duration is no more than the token's remaining time until expiration - // (in case the expiration time is defined on the token - AFAIK currently non-standard in IndieAuth) - self.cache.insert(token, user.clone(), std::time::Duration::from_secs(600)).await; - req.set_ext(user); - Ok(next.run(req).await) - }, - // TODO: Refactor to return Err(IndieAuthError) so downstream middleware could catch it - // and present a prettier interface to the error (maybe even hiding data from the user) - (http_types::StatusCode::InternalServerError, None) => { - Ok(Response::builder(500).body(json!({ - "error": "token_endpoint_fail", - "error_description": "Token endpoint made a boo-boo and refused to answer." - })).build()) - }, - (_, None) => { - Ok(Response::builder(401).body(json!({ - "error": "unauthorized", - "error_description": "The token endpoint refused to accept your token." - })).build()) - }, - (_, Some(_)) => { - // This shouldn't happen. - panic!("The token validation function has caught rabies and returns malformed responses. Aborting."); + StatusCode::BAD_REQUEST => { + use hyper::body::HttpBody; + use bytes::BufMut; + let mut buf: Vec = Vec::default(); + while let Some(chunk) = res.body_mut().data().await { + if let Err(err) = chunk { + return Err(IndieAuthError::from(err).into()); + } + buf.put(chunk.unwrap()); + } + match serde_json::from_slice::(&buf) { + Ok(err) => { + if err.error == "unauthorized" { + Err(IndieAuthError { + source: None, + kind: ErrorKind::NotAuthorized, + msg: err.error_description + }.into()) + } else { + Err(IndieAuthError { + source: None, + kind: ErrorKind::TokenEndpointError, + msg: err.error_description + }.into()) + } + }, + Err(err) => Err(IndieAuthError::from(err).into()) } - } - } + }, + _ => Err(IndieAuthError { + source: None, + msg: format!("Token endpoint returned {}", res.status()).to_string(), + kind: ErrorKind::TokenEndpointError + }.into()) + }, + Err(err) => Err(warp::reject::custom(IndieAuthError::from(err))) } - } - } + }) } #[cfg(test)] mod tests { - use super::*; + use super::{HttpClient, User, IndieAuthError, require_token}; + use httpmock::prelude::*; + #[test] fn user_scopes_are_checkable() { let user = User::new( @@ -245,4 +201,92 @@ mod tests { assert!(user.check_scope("create")); assert!(!user.check_scope("delete")); } + + fn get_http_client() -> HttpClient { + let builder = hyper::Client::builder(); + let https = hyper_rustls::HttpsConnectorBuilder::new() + .with_webpki_roots() + .https_or_http() + .enable_http1() + .enable_http2() + .build(); + builder.build(https) + } + + #[tokio::test] + async fn test_require_token_with_token() { + let server = MockServer::start_async().await; + server.mock_async(|when, then| { + when.path("/token") + .header("Authorization", "Bearer token"); + + then.status(200) + .header("Content-Type", "application/json") + .json_body(serde_json::to_value(User::new( + "https://fireburn.ru/", + "https://quill.p3k.io/", + "create update media", + )).unwrap()); + }).await; + + let filter = require_token(server.url("/token"), get_http_client()); + + let res: User = warp::test::request() + .path("/") + .header("Authorization", "Bearer token") + .filter(&filter) + .await + .unwrap(); + + assert_eq!(res.me.as_str(), "https://fireburn.ru/") + } + + #[tokio::test] + async fn test_require_token_fake_token() { + let server = MockServer::start_async().await; + server.mock_async(|when, then| { + when.path("/refuse_token"); + + then.status(200) + .json_body(serde_json::json!({"active": false})); + }).await; + + let filter = require_token(server.url("/refuse_token"), get_http_client()); + + let res = warp::test::request() + .path("/") + .header("Authorization", "Bearer token") + .filter(&filter) + .await + .unwrap_err(); + + let err: &IndieAuthError = res.find().unwrap(); + assert_eq!(err.kind, super::ErrorKind::NotAuthorized); + } + + #[tokio::test] + async fn test_require_token_400_error_unauthorized() { + let server = MockServer::start_async().await; + server.mock_async(|when, then| { + when.path("/refuse_token_with_400"); + + then.status(400) + .json_body(serde_json::json!({ + "error": "unauthorized", + "error_description": "The token provided was malformed" + })); + }).await; + + let filter = require_token(server.url("/refuse_token_with_400"), get_http_client()); + + let res = warp::test::request() + .path("/") + .header("Authorization", "Bearer token") + .filter(&filter) + .await + .unwrap_err(); + + let err: &IndieAuthError = res.find().unwrap(); + assert_eq!(err.kind, super::ErrorKind::NotAuthorized); + } } diff --git a/src/lib.rs b/src/lib.rs index 2585227..93e4593 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,12 @@ -//use tide::{Request, Response}; +#[allow(unused_imports)] use warp::Filter; -/*pub mod database; -mod frontend; -mod indieauth; -mod micropub;*/ + pub mod metrics; /// Database abstraction layer for Kittybox, allowing the CMS to work with any kind of database. pub mod database; pub mod micropub; -//pub mod indieauth; +pub mod indieauth; +//pub mod frontend; /*use crate::indieauth::IndieAuthMiddleware; use crate::micropub::CORSMiddleware;*/ -- cgit 1.4.1