diff options
-rw-r--r-- | Cargo.lock | 303 | ||||
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | flake.nix | 11 | ||||
-rw-r--r-- | src/database/file/mod.rs | 2 | ||||
-rw-r--r-- | src/frontend/login.rs | 292 | ||||
-rw-r--r-- | src/frontend/mod.rs | 26 | ||||
-rw-r--r-- | src/frontend/templates/mod.rs | 43 | ||||
-rw-r--r-- | src/lib.rs | 20 | ||||
-rw-r--r-- | src/main.rs | 19 | ||||
-rw-r--r-- | src/micropub/post.rs | 2 |
10 files changed, 554 insertions, 167 deletions
diff --git a/Cargo.lock b/Cargo.lock index 659cae5..d4d0544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,9 +82,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.44" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" +checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" [[package]] name = "arrayref" @@ -171,18 +171,16 @@ dependencies = [ [[package]] name = "async-h1" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5142de15b549749cce62923a50714b0d7b77f5090ced141599e78899865451" +checksum = "8101020758a4fc3a7c326cb42aa99e9fa77cbfb76987c128ad956406fe1f70a7" dependencies = [ "async-channel", "async-dup", "async-std", - "byte-pool", "futures-core", "http-types", "httparse", - "lazy_static", "log 0.4.14", "pin-project", ] @@ -226,9 +224,9 @@ dependencies = [ [[package]] name = "async-process" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b21b63ab5a0db0369deb913540af2892750e42d949faacc7a61495ac418a1692" +checksum = "83137067e3a2a6a06d67168e49e68a0957d215410473a740cea95a2425c0b7c6" dependencies = [ "async-io", "blocking", @@ -428,9 +426,9 @@ dependencies = [ [[package]] name = "blocking" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e170dbede1f740736619b776d7251cb1b9095c435c34d8ca9f57fcd2f335e9" +checksum = "046e47d4b2d391b1f6f8b407b1deb8dee56c1852ccd868becf2710f601b5f427" dependencies = [ "async-channel", "async-task", @@ -442,19 +440,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.7.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" - -[[package]] -name = "byte-pool" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca" -dependencies = [ - "crossbeam-queue", - "stable_deref_trait", -] +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" [[package]] name = "byteorder" @@ -476,9 +464,9 @@ checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" [[package]] name = "cc" -version = "1.0.70" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" [[package]] name = "cfg-if" @@ -528,9 +516,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.1" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001" +checksum = "b2b2f5d0ee456f3928812dfc8c6d9a1d592b98678f6d56db9b0cd2b7bc6c8db5" dependencies = [ "bytes", "futures-core", @@ -612,9 +600,9 @@ checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" [[package]] name = "crc32fast" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" dependencies = [ "cfg-if 1.0.0", ] @@ -716,6 +704,12 @@ dependencies = [ ] [[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + +[[package]] name = "deadpool" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -731,14 +725,14 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.16" +version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version 0.3.3", + "rustc_version 0.4.0", "syn", ] @@ -800,9 +794,9 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.28" +version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" dependencies = [ "cfg-if 1.0.0", ] @@ -847,13 +841,13 @@ dependencies = [ [[package]] name = "fd-lock" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8806dd91a06a7a403a8e596f9bfbfb34e469efbc363fc9c9713e79e26472e36" +checksum = "cfc110fe50727d46a428eed832df40affe9bf74d077cac1bf3f2718e823f14c5" dependencies = [ "cfg-if 1.0.0", "libc", - "winapi 0.3.9", + "windows-sys", ] [[package]] @@ -918,9 +912,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" +checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e" dependencies = [ "futures-channel", "futures-core", @@ -933,9 +927,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" dependencies = [ "futures-core", "futures-sink", @@ -943,15 +937,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" +checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" [[package]] name = "futures-executor" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" +checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" dependencies = [ "futures-core", "futures-task", @@ -960,9 +954,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" +checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" [[package]] name = "futures-lite" @@ -981,12 +975,10 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" +checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd" dependencies = [ - "autocfg", - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -994,23 +986,22 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" +checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" [[package]] name = "futures-task" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" +checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" [[package]] name = "futures-util" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" dependencies = [ - "autocfg", "futures-channel", "futures-core", "futures-io", @@ -1020,8 +1011,6 @@ dependencies = [ "memchr 2.4.1", "pin-project-lite 0.2.7", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] @@ -1078,9 +1067,9 @@ dependencies = [ [[package]] name = "gloo-timers" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" +checksum = "6f16c88aa13d2656ef20d1c042086b8767bbe2bdb62526894275a1b062161b2e" dependencies = [ "futures-channel", "futures-core", @@ -1220,9 +1209,9 @@ checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" [[package]] name = "instant" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", ] @@ -1260,6 +1249,7 @@ dependencies = [ "async-std", "async-trait", "chrono", + "data-encoding", "easy-scraper", "ellipse", "env_logger 0.8.4", @@ -1275,12 +1265,14 @@ dependencies = [ "newbase60", "paste", "prometheus", + "rand 0.8.4", "redis", "relative-path", "retainer", "serde", "serde_json", "serde_urlencoded", + "sha2", "surf", "tempdir", "test-logger", @@ -1331,9 +1323,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.102" +version = "0.2.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" +checksum = "f98a04dce437184842841303488f70d0188c5f51437d2a834dc097eafa909a01" [[package]] name = "lock_api" @@ -1583,9 +1575,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" +checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" [[package]] name = "percent-encoding" @@ -1594,15 +1586,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - -[[package]] name = "phf" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1702,9 +1685,9 @@ checksum = "d15b6607fa632996eb8a17c9041cb6071cb75ac057abd45dece578723ea8c7c0" [[package]] name = "polling" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92341d779fa34ea8437ef4d82d440d5e1ce3f3ff7f824aa64424cd481f9a1f25" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" dependencies = [ "cfg-if 1.0.0", "libc", @@ -1726,9 +1709,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "precomputed-hash" @@ -1743,16 +1726,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] -name = "proc-macro-nested" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" - -[[package]] name = "proc-macro2" -version = "1.0.29" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +checksum = "fb37d2df5df740e582f28f8560cf425f52bb267d872fe58358eadb554909f07a" dependencies = [ "unicode-xid", ] @@ -1790,15 +1767,15 @@ dependencies = [ [[package]] name = "protobuf" -version = "2.25.1" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23129d50f2c9355ced935fce8a08bd706ee2e7ce2b3b33bf61dace0e379ac63a" +checksum = "47c327e191621a2158159df97cdbc2e7074bb4e940275e35abf38eb3d2595754" [[package]] name = "quote" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] @@ -1933,9 +1910,9 @@ dependencies = [ [[package]] name = "redis" -version = "0.21.3" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd71bdb3d0d6e9183e675c977f652fbf8abc3b63fcb722e9abb42f82ef839b65" +checksum = "7f23ceed4c0e76b322657c2c3352ea116f9ec60a1a1aefeb3c84ed062c50865b" dependencies = [ "async-std", "async-trait", @@ -1999,9 +1976,9 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "relative-path" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9629de8974fd69c97684736786b807edd3da456d3e3f95341dd9d4cbd8f5ad6" +checksum = "73d4caf086b102ab49d0525b721594a555ab55c6556086bbe52a430ad26c3bd7" [[package]] name = "remove_dir_all" @@ -2056,11 +2033,11 @@ dependencies = [ [[package]] name = "rustc_version" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 0.11.0", + "semver 1.0.4", ] [[package]] @@ -2078,9 +2055,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" [[package]] name = "scopeguard" @@ -2124,17 +2101,14 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "semver-parser 0.7.0", + "semver-parser", ] [[package]] name = "semver" -version = "0.11.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser 0.10.2", -] +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "semver-parser" @@ -2143,15 +2117,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - -[[package]] name = "serde" version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2173,9 +2138,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.68" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" dependencies = [ "itoa", "ryu", @@ -2184,9 +2149,9 @@ dependencies = [ [[package]] name = "serde_qs" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a72808528a89fa9eca23bbb6a1eb92cb639b881357269b6510f11e50c0f8a9" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" dependencies = [ "percent-encoding", "serde", @@ -2270,15 +2235,15 @@ checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" [[package]] name = "slab" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "socket2" @@ -2368,12 +2333,13 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" [[package]] name = "string_cache" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" dependencies = [ "lazy_static", "new_debug_unreachable", + "parking_lot", "phf_shared", "precomputed-hash", "serde", @@ -2399,9 +2365,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "surf" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f856d60bdb4679fc9ec516c34093484e963431b5016a8429f85a8e74b5ccaa" +checksum = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7" dependencies = [ "async-std", "async-trait", @@ -2429,9 +2395,9 @@ checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08" [[package]] name = "syn" -version = "1.0.77" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" dependencies = [ "proc-macro2", "quote", @@ -2485,18 +2451,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -2608,9 +2574,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" dependencies = [ "tinyvec_macros", ] @@ -2623,9 +2589,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" +checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" dependencies = [ "autocfg", "bytes", @@ -2635,9 +2601,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ "bytes", "futures-core", @@ -2654,12 +2620,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" [[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - -[[package]] name = "unicase" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2670,9 +2630,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-normalization" @@ -2738,9 +2698,9 @@ checksum = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" [[package]] name = "value-bag" -version = "1.0.0-alpha.7" +version = "1.0.0-alpha.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd320e1520f94261153e96f7534476ad869c14022aee1e59af7c778075d840ae" +checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f" dependencies = [ "ctor", "sval", @@ -2919,3 +2879,46 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ca39602d5cbfa692c4b67e3bcbb2751477355141c1ed434c94da4186836ff6" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2" + +[[package]] +name = "windows_i686_gnu" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a" + +[[package]] +name = "windows_i686_msvc" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f" diff --git a/Cargo.toml b/Cargo.toml index faa822c..63adeb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ test-logger = "^0.1.0" # Simple helper to initialize env_logger before uni [dependencies] async-trait = "^0.1.50" # Type erasure for async trait methods +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 env_logger = "^0.8.3" # A logging implementation for `log` which is configured via an environment variable @@ -46,11 +47,13 @@ log = "^0.4.14" # A lightweight logging facade for Rust markdown = "^0.3.0" # Native Rust library for parsing Markdown and (outputting HTML) markup = "^0.12.0" # HTML templating engine... ok also very funny about markdown and markup... i just realized the pun... newbase60 = "^0.1.3" # A library that implements Tantek Çelik's New Base 60 +rand = "^0.8.4" # Random number generators. retainer = "^0.2.2" # Minimal async cache in Rust with support for key expirations serde_json = "^1.0.64" # A JSON serialization file format serde_urlencoded = "^0.7.0" # `x-www-form-urlencoded` meets Serde tide = "^0.16.0" # A minimal and pragmatic Rust web application framework built for rapid development relative-path = "^1.5.0" # Portable relative paths for Rust +sha2 = "^0.9.8" # SHA-2 series of algorithms for Rust [dependencies.anyhow] version = "^1.0.42" optional = true diff --git a/flake.nix b/flake.nix index 0b0e039..4366190 100644 --- a/flake.nix +++ b/flake.nix @@ -124,6 +124,12 @@ example = "/run/secrets/kittybox-shared-secret"; description = "A shared secret that will, when passed, allow unlimited editing access to database. Keep it safe."; }; + cookieSecretFile = mkOption { + type = types.str; + default = "/var/lib/kittybox/cookie_secret_key"; + example = "/run/secrets/kittybox-cookie-secret"; + description = "A secret file to encrypt cookies with the contents of. Should be at least 32 bytes in length. A random persistent file will be generated if this variable is left untouched."; + }; }; }; config = lib.mkIf cfg.enable { @@ -139,6 +145,7 @@ cfg.authorizationEndpoint cfg.internalTokenFile cfg.bind cfg.port + cfg.cookieSecretFile ]; environment = { @@ -151,6 +158,7 @@ #REDIS_URI = if (cfg.redisUri == null) then "redis://127.0.0.1:6379/" else cfg.redisUri; BACKEND_URI = cfg.backendUri; RUST_LOG = "${cfg.logLevel}"; + COOKIE_SECRET_FILE = "${cfg.cookieSecretFile}"; }; script = '' @@ -159,6 +167,9 @@ export KITTYBOX_INTERNAL_TOKEN=$(${pkgs.coreutils}/bin/cat ${cfg.internalTokenFile}) fi ''} + if [[ ${cfg.cookieSecretFile} == /var/lib/kittybox/cookie_secret_key && ! -f /var/lib/kittybox/cookie_secret_key ]]; then + cat /dev/urandom | tr -Cd '[:alnum:]' | head -c 128 > /var/lib/kittybox/cookie_secret_key + fi exec ${cfg.package}/bin/kittybox ''; diff --git a/src/database/file/mod.rs b/src/database/file/mod.rs index 4fb7f47..d556f46 100644 --- a/src/database/file/mod.rs +++ b/src/database/file/mod.rs @@ -482,11 +482,11 @@ impl Storage for FileStorage { // Hack to unwrap the Option and sieve out broken links // Broken links return None, and Stream::filter_map skips Nones. .try_filter_map(|post: Option<serde_json::Value>| async move { Ok(post) }) + .try_filter_map(|post| async move { Ok(filter_post(post, user)) }) .and_then(|mut post| async move { hydrate_author(&mut post, user, self).await; Ok(post) }) - .try_filter_map(|post| async move { Ok(filter_post(post, user)) }) .take(limit); match posts.try_collect::<Vec<serde_json::Value>>().await { diff --git a/src/frontend/login.rs b/src/frontend/login.rs new file mode 100644 index 0000000..09fa75f --- /dev/null +++ b/src/frontend/login.rs @@ -0,0 +1,292 @@ +use std::convert::TryInto; +use std::str::FromStr; +use log::{debug, error}; +use rand::Rng; +use http_types::Mime; +use tide::{Request, Response, Result}; +use serde::{Serialize, Deserialize}; +use sha2::{Sha256, Digest}; + +use crate::frontend::{FrontendError, IndiewebEndpoints}; +use crate::{ApplicationState, database::Storage}; +use crate::frontend::templates::Template; + +markup::define! { + LoginPage { + form[method="POST"] { + h1 { "Sign in with your website" } + p { + "Signing in to Kittybox might allow you to view private content " + "intended for your eyes only." + } + + section { + label[for="url"] { "Your website URL" } + input[id="url", name="url", placeholder="https://example.com/"]; + input[type="submit"]; + } + } + } +} + +pub async fn form<S: Storage>(req: Request<ApplicationState<S>>) -> Result { + let owner = req.url().origin().ascii_serialization() + "/"; + let storage = &req.state().storage; + let authorization_endpoint = req.state().authorization_endpoint.to_string(); + let token_endpoint = req.state().token_endpoint.to_string(); + let blog_name = storage.get_setting("site_name", &owner).await.unwrap_or_else(|_| "Kitty Box!".to_string()); + let feeds = storage.get_channels(&owner).await.unwrap_or_default(); + + Ok(Response::builder(200) + .body(Template { + title: "Sign in with IndieAuth", + blog_name: &blog_name, + endpoints: IndiewebEndpoints { + authorization_endpoint, + token_endpoint, + webmention: None, + microsub: None + }, + feeds, + user: req.session().get("user"), + content: LoginPage {}.to_string(), + }.to_string()) + .content_type("text/html; charset=utf-8") + .build()) +} + +#[derive(Serialize, Deserialize)] +struct LoginForm { + url: String +} + +#[derive(Serialize, Deserialize)] +struct IndieAuthClientState { + /// A random value to protect from CSRF attacks. + nonce: String, + /// The user's initial "me" value. + me: String, + /// Authorization endpoint used. + authorization_endpoint: String +} + + +#[derive(Serialize, Deserialize)] +struct IndieAuthRequestParams { + response_type: String, // can only have "code". TODO make an enum + client_id: String, // always a URL. TODO consider making a URL + redirect_uri: surf::Url, // callback URI for IndieAuth + state: String, // CSRF protection, should include randomness and be passed through + code_challenge: String, // base64-encoded PKCE challenge + code_challenge_method: String, // usually "S256". TODO make an enum + scope: Option<String>, // oAuth2 scopes to grant, + me: surf::Url, // User's entered profile URL + +} + +/// Handle login requests. Find the IndieAuth authorization endpoint and redirect to it. +pub async fn handler<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result { + let content_type = req.content_type(); + if content_type.is_none() { + return Err(FrontendError::with_code(400, "Use the login form, Luke.").into()); + } + if content_type.unwrap() != Mime::from_str("application/x-www-form-urlencoded").unwrap() { + return Err(FrontendError::with_code(400, "Login form results must be a urlencoded form").into()); + } + + let form = req.body_form::<LoginForm>().await?; // FIXME check if it returns 400 or 500 on error + let homepage_uri = surf::Url::parse(&form.url)?; + let http = &req.state().http_client; + + let mut fetch_response = http.get(&homepage_uri).send().await?; + if fetch_response.status() != 200 { + return Err(FrontendError::with_code(500, "Error fetching your authorization endpoint. Check if your website's okay.").into()); + } + + let mut authorization_endpoint: Option<surf::Url> = None; + if let Some(links) = fetch_response.header("Link") { + // NOTE: this is the same Link header parser used in src/micropub/post.rs:459. + // One should refactor it to a function to use independently and improve later + for link in links.iter().flat_map(|i| i.as_str().split(',')) { + debug!("Trying to match {} as authorization_endpoint", link); + let mut split_link = link.split(';'); + + match split_link.next() { + Some(uri) => { + if let Some(uri) = uri.strip_prefix('<').and_then(|uri| uri.strip_suffix('>')) { + debug!("uri: {}", uri); + for prop in split_link { + debug!("prop: {}", prop); + let lowercased = prop.to_ascii_lowercase(); + let trimmed = lowercased.trim(); + if trimmed == "rel=\"authorization_endpoint\"" + || trimmed == "rel=authorization_endpoint" + { + if let Ok(endpoint) = homepage_uri.join(uri) { + debug!("Found authorization endpoint {} for user {}", endpoint, homepage_uri.as_str()); + authorization_endpoint = Some(endpoint); + break; + } + } + } + } + }, + None => continue, + } + } + } + // If the authorization_endpoint is still not found after the Link parsing gauntlet, + // bring out the big guns and parse HTML to find it. + if authorization_endpoint.is_none() { + let body = fetch_response.body_string().await?; + let pattern = easy_scraper::Pattern::new(r#"<link rel="authorization_endpoint" href="{{url}}">"#) + .expect("Cannot parse the pattern for authorization_endpoint"); + let matches = pattern.matches(&body); + debug!("Matches for authorization_endpoint in HTML: {:?}", matches); + if !matches.is_empty() { + if let Ok(endpoint) = homepage_uri.join(&matches[0]["url"]) { + debug!("Found authorization endpoint {} for user {}", endpoint, homepage_uri.as_str()); + authorization_endpoint = Some(endpoint) + } + } + }; + // If even after this the authorization endpoint is still not found, bail out. + if authorization_endpoint.is_none() { + error!("Couldn't find authorization_endpoint for {}", homepage_uri.as_str()); + return Err(FrontendError::with_code(400, "Your website doesn't support the IndieAuth protocol.").into()); + } + let mut authorization_endpoint: surf::Url = authorization_endpoint.unwrap(); + let mut rng = rand::thread_rng(); + let state: String = data_encoding::BASE64URL.encode(serde_urlencoded::to_string(IndieAuthClientState { + nonce: (0..8).map(|_| { + let idx = rng.gen_range(0..INDIEAUTH_PKCE_CHARSET.len()); + INDIEAUTH_PKCE_CHARSET[idx] as char + }).collect(), + me: homepage_uri.to_string(), + authorization_endpoint: authorization_endpoint.to_string() + })?.as_bytes()); + // PKCE code generation + let code_verifier: String = (0..128).map(|_| { + let idx = rng.gen_range(0..INDIEAUTH_PKCE_CHARSET.len()); + INDIEAUTH_PKCE_CHARSET[idx] as char + }).collect(); + let mut hasher = Sha256::new(); + hasher.update(code_verifier.as_bytes()); + let code_challenge: String = data_encoding::BASE64URL.encode(&hasher.finalize()); + + authorization_endpoint.set_query(Some(&serde_urlencoded::to_string(IndieAuthRequestParams { + response_type: "code".to_string(), + client_id: req.url().origin().ascii_serialization(), + redirect_uri: req.url().join("login/callback")?, + state: state.clone(), code_challenge, + code_challenge_method: "S256".to_string(), + scope: Some("profile".to_string()), + me: homepage_uri, + })?)); + + let cookies = vec![ + format!(r#"indieauth_state="{}"; Same-Site: None; Secure; Max-Age: 600"#, state), + format!(r#"indieauth_code_verifier="{}"; Same-Site: None; Secure; Max-Age: 600"#, code_verifier) + ]; + + let cookie_header = cookies.iter().map(|i| -> http_types::headers::HeaderValue { + (i as &str).try_into().unwrap() + }).collect::<Vec<_>>(); + + Ok(Response::builder(302) + .header("Location", authorization_endpoint.to_string()) + .header("Set-Cookie", &*cookie_header) + .build()) +} + +const INDIEAUTH_PKCE_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ + abcdefghijklmnopqrstuvwxyz\ + 1234567890-._~"; + +#[derive(Deserialize)] +struct IndieAuthCallbackResponse { + code: Option<String>, + error: Option<String>, + error_description: Option<String>, + #[allow(dead_code)] + error_uri: Option<String>, + // This needs to be further decoded to receive state back and will always be present + state: String +} + +impl IndieAuthCallbackResponse { + fn is_successful(&self) -> bool { + !self.code.is_none() + } +} + +#[derive(Serialize, Deserialize)] +struct IndieAuthCodeRedeem { + grant_type: String, + code: String, + client_id: String, + redirect_uri: String, + code_verifier: String +} + +#[derive(Serialize, Deserialize)] +struct IndieWebProfile { + name: Option<String>, + url: Option<String>, + email: Option<String>, + photo: Option<String> +} + +#[derive(Serialize, Deserialize)] +struct IndieAuthResponse { + me: String, + scope: Option<String>, + access_token: Option<String>, + token_type: Option<String>, + profile: Option<IndieWebProfile> +} + +/// Handle IndieAuth parameters, fetch the final h-card and redirect the user to the homepage. +pub async fn callback<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result { + let params: IndieAuthCallbackResponse = req.query()?; + let http: &surf::Client = &req.state().http_client; + let origin = req.url().origin().ascii_serialization(); + + if req.cookie("indieauth_state").unwrap().value() != ¶ms.state { + return Err(FrontendError::with_code(400, "The state doesn't match. A possible CSRF attack was prevented. Please try again later.").into()); + } + let state: IndieAuthClientState = serde_urlencoded::from_bytes( + &data_encoding::BASE64URL.decode(params.state.as_bytes())? + )?; + + if !params.is_successful() { + return Err(FrontendError::with_code(400, &format!("The authorization endpoint indicated a following error: {:?}: {:?}", ¶ms.error, ¶ms.error_description)).into()) + } + + let authorization_endpoint = surf::Url::parse(&state.authorization_endpoint).unwrap(); + let mut code_response = http.post(authorization_endpoint) + .body_string(serde_urlencoded::to_string(IndieAuthCodeRedeem { + grant_type: "authorization_code".to_string(), + code: params.code.unwrap().to_string(), + client_id: origin.to_string(), + redirect_uri: origin + "/login/callback", + code_verifier: req.cookie("indieauth_code_verifier").unwrap().value().to_string() + })?) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("Accept", "application/json") + .send().await?; + + if code_response.status() != 200 { + return Err(FrontendError::with_code(code_response.status(), &format!("Authorization endpoint returned an error when redeeming the code: {}", code_response.body_string().await?)).into()); + } + + let json: IndieAuthResponse = code_response.body_json().await?; + drop(http); + let session = req.session_mut(); + session.insert("user", &json.me)?; + + // TODO redirect to the page user came from + Ok(Response::builder(302) + .header("Location", "/") + .build()) +} diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index 5426f7e..76114c5 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -1,3 +1,5 @@ +use std::convert::TryInto; + use crate::database::Storage; use crate::ApplicationState; use log::{error, info}; @@ -6,8 +8,9 @@ use tide::{Next, Request, Response, Result, StatusCode}; static POSTS_PER_PAGE: usize = 20; -mod templates; +pub mod login; +mod templates; use templates::{ErrorPage, MainPage, OnboardingPage, Template}; #[derive(Clone, Serialize, Deserialize)] @@ -30,11 +33,11 @@ struct FrontendError { code: StatusCode, } impl FrontendError { - pub fn with_code(code: StatusCode, msg: &str) -> Self { + pub fn with_code<C>(code: C, msg: &str) -> Self where C: TryInto<StatusCode> { Self { msg: msg.to_string(), source: None, - code, + code: code.try_into().unwrap_or_else(|_| StatusCode::InternalServerError), } } pub fn msg(&self) -> &str { @@ -227,7 +230,7 @@ pub async fn mainpage<S: Storage>(mut req: Request<ApplicationState<S>>) -> Resu let query = req.query::<QueryParams>()?; let authorization_endpoint = req.state().authorization_endpoint.to_string(); let token_endpoint = req.state().token_endpoint.to_string(); - let user: Option<String> = None; + let user: Option<String> = req.session().get("user"); #[cfg(any(not(debug_assertions), test))] let url = req.url(); @@ -260,6 +263,7 @@ pub async fn mainpage<S: Storage>(mut req: Request<ApplicationState<S>>) -> Resu microsub: None, }, feeds: Vec::default(), + user: None, content: OnboardingPage {}.to_string(), } .to_string(), @@ -288,6 +292,7 @@ pub async fn mainpage<S: Storage>(mut req: Request<ApplicationState<S>>) -> Resu .get_channels(hcard_url) .await .unwrap_or_else(|_| Vec::default()), + user, content: MainPage { feed: &feed?, card: &card?, @@ -304,7 +309,7 @@ pub async fn render_post<S: Storage>(mut req: Request<ApplicationState<S>>) -> R let query = req.query::<QueryParams>()?; let authorization_endpoint = req.state().authorization_endpoint.to_string(); let token_endpoint = req.state().token_endpoint.to_string(); - let user: Option<String> = None; + let user: Option<String> = req.session().get("user"); // This cannot error out as the URL must be valid. Or there is something horribly wrong // and we shouldn't serve this request anyway. @@ -381,6 +386,7 @@ pub async fn render_post<S: Storage>(mut req: Request<ApplicationState<S>>) -> R .get_channels(&owner) .await .unwrap_or_else(|_| Vec::default()), + user, content: template, } .to_string(), @@ -415,11 +421,16 @@ where .get_channels(&owner) .await .unwrap_or_else(|_| Vec::default()); + let user: Option<String> = request.session().get("user"); let mut res = next.run(request).await; let mut code: Option<StatusCode> = None; + let mut msg: Option<String> = None; if let Some(err) = res.downcast_error::<FrontendError>() { code = Some(err.code()); error!("Error caught while processing request: {}", err.msg()); + if err.code() == 400 { + msg = Some(err.msg().to_string()); + } let mut err: &dyn std::error::Error = err; while let Some(e) = err.source() { error!("Caused by: {}", e); @@ -439,8 +450,9 @@ where webmention: None, microsub: None, }, - feeds: feeds, - content: ErrorPage { code }.to_string(), + feeds, + user, + content: ErrorPage { code, msg }.to_string(), } .to_string(), ); diff --git a/src/frontend/templates/mod.rs b/src/frontend/templates/mod.rs index 100e16d..dbc23c9 100644 --- a/src/frontend/templates/mod.rs +++ b/src/frontend/templates/mod.rs @@ -25,7 +25,7 @@ mod onboarding; pub use onboarding::OnboardingPage; markup::define! { - Template<'a>(title: &'a str, blog_name: &'a str, endpoints: IndiewebEndpoints, feeds: Vec<MicropubChannel>, content: String) { + Template<'a>(title: &'a str, blog_name: &'a str, endpoints: IndiewebEndpoints, feeds: Vec<MicropubChannel>, user: Option<String>, content: String) { @markup::doctype() html { head { @@ -45,14 +45,22 @@ markup::define! { } } body { + // TODO Somehow compress headerbar into a menu when the screen space is tight nav#headerbar { ul { - // TODO print a list of feeds and allow jumping to them li { a#homepage[href="/"] { @blog_name } } @for feed in feeds.iter() { li { a[href=&feed.uid] { @feed.name } } } - li.shiftright { a#login[href="/login"] { "Login" } } + li.shiftright { + @if user.is_none() { + a#login[href="/login"] { "Sign in" } + } else { + span { + @user.as_ref().unwrap() " - " a#logout[href="/logout"] { "Sign out" } + } + } + } } } main { @@ -372,7 +380,7 @@ markup::define! { } @Feed { feed } } - ErrorPage(code: StatusCode) { + ErrorPage(code: StatusCode, msg: Option<String>) { h1 { @format!("HTTP {} {}", code, code.canonical_reason()) } @match code { StatusCode::Unauthorized => { @@ -399,11 +407,32 @@ markup::define! { p { "Wait, do you seriously expect my website to brew you coffee? It's not a coffee machine!" } p { - small { "I could brew you some coffee tho if we meet one day... " - small { i { "i-it's nothing personal, I just like brewing coffee, b-baka!!!~ >.<!" } } } + small { + "I could brew you some coffee tho if we meet one day... " + small { + i { + "i-it's nothing personal, I just like brewing coffee, b-baka!!!~ >.<!" + } + } + } } } - _ => { p { "It seems like you have found an error. Not to worry, it has already been logged." } } + StatusCode::BadRequest => { + @if msg.is_none() { + p { + "There was an undescribed error in your request. " + "Please try again later or with a different request." + } + } else { + p { + "There was a following error in your request: " + @msg.as_ref().unwrap() + } + } + } + _ => { + p { "It seems like you have found an error. Not to worry, it has already been logged." } + } } P { "For now, may I suggest to visit " a[href="/"] {"the main page"} " of this website?" } diff --git a/src/lib.rs b/src/lib.rs index 817bda7..eb915c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ where authorization_endpoint: surf::Url, media_endpoint: Option<String>, internal_token: Option<String>, + cookie_secret: String, http_client: surf::Client, storage: StorageBackend, } @@ -48,6 +49,13 @@ where .with(frontend::ErrorHandlerMiddleware {}) .get(frontend::mainpage) .post(frontend::onboarding_receiver); + app.at("/login") + .with(frontend::ErrorHandlerMiddleware {}) + .get(frontend::login::form) + .post(frontend::login::handler); + app.at("/login/callback") + .with(frontend::ErrorHandlerMiddleware {}) + .get(frontend::login::callback); app.at("/static/*path") .with(frontend::ErrorHandlerMiddleware {}) .get(frontend::handle_static); @@ -64,7 +72,14 @@ where app.at("/metrics").get(metrics::gather); app.with(metrics::InstrumentationMiddleware {}); - + app.with( + tide::sessions::SessionMiddleware::new( + tide::sessions::CookieStore::new(), + &app.state().cookie_secret.as_bytes() + ) + .with_cookie_name("kittybox_session") + .without_save_unchanged() + ); app } @@ -93,6 +108,7 @@ pub async fn get_app_with_file( authorization_endpoint: surf::Url, backend_uri: String, media_endpoint: Option<String>, + cookie_secret: String, internal_token: Option<String>, ) -> App<database::FileStorage> { let folder = backend_uri.strip_prefix("file://").unwrap(); @@ -102,6 +118,7 @@ pub async fn get_app_with_file( media_endpoint, authorization_endpoint, internal_token, + cookie_secret, storage: database::FileStorage::new(path).await.unwrap(), http_client: surf::Client::new(), }); @@ -128,6 +145,7 @@ pub async fn get_app_with_test_file( authorization_endpoint: Url::parse("https://indieauth.com/auth").unwrap(), storage: backend.clone(), internal_token: None, + cookie_secret: "1234567890abcdefghijklmnopqrstuvwxyz".to_string(), http_client: surf::Client::new(), }); (tempdir, backend, equip_app(app)) diff --git a/src/main.rs b/src/main.rs index aec3be0..4f5f9ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,6 +60,24 @@ async fn main() -> Result<(), std::io::Error> { let internal_token: Option<String> = env::var("KITTYBOX_INTERNAL_TOKEN").ok(); + let cookie_secret: String = match env::var("COOKIE_SECRET").ok() { + Some(value) => value, + None => { + if let Some(filename) = env::var("COOKIE_SECRET_FILE").ok() { + use async_std::io::ReadExt; + + let mut file = async_std::fs::File::open(filename).await?; + let mut temp_string = String::new(); + file.read_to_string(&mut temp_string).await?; + + temp_string + } else { + error!("COOKIE_SECRET or COOKIE_SECRET_FILE is not set, will not be able to log in users securely!"); + std::process::exit(1); + } + } + }; + let host = env::var("SERVE_AT") .ok() .unwrap_or_else(|| "0.0.0.0:8080".to_string()); @@ -73,6 +91,7 @@ async fn main() -> Result<(), std::io::Error> { authorization_endpoint, backend_uri, media_endpoint, + cookie_secret, internal_token, ) .await; diff --git a/src/micropub/post.rs b/src/micropub/post.rs index 070c822..c465a6f 100644 --- a/src/micropub/post.rs +++ b/src/micropub/post.rs @@ -483,7 +483,7 @@ async fn post_process_new_post<S: Storage>( // TODO: Replace this function once the MF2 parser is ready // A compliant parser's output format includes rels, // we could just find a Webmention one in there - let pattern = easy_scraper::Pattern::new(r#"<link href="{url}" rel="webmention">"#) + let pattern = easy_scraper::Pattern::new(r#"<link href="{{url}}" rel="webmention">"#) .expect("Pattern for webmentions couldn't be parsed"); let matches = pattern.matches(&body); if matches.is_empty() { |