From 95889e3df143576350233e1768e68b372972ebaa Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 8 Jun 2025 14:10:12 +0200 Subject: [PATCH] Checkpoint: Migration to axum --- Cargo.lock | 627 ++++------------- Cargo.toml | 21 +- crates/caldav/Cargo.toml | 7 +- .../caldav/src/calendar/methods/mkcalendar.rs | 29 +- crates/caldav/src/calendar/methods/mod.rs | 2 +- crates/caldav/src/calendar/methods/post.rs | 19 +- .../methods/report/calendar_multiget.rs | 25 +- .../caldav/src/calendar/methods/report/mod.rs | 47 +- crates/caldav/src/calendar/resource.rs | 52 +- crates/caldav/src/calendar_object/methods.rs | 81 ++- crates/caldav/src/calendar_object/resource.rs | 59 +- crates/caldav/src/calendar_set/mod.rs | 28 +- crates/caldav/src/error.rs | 47 +- crates/caldav/src/lib.rs | 132 ++-- crates/caldav/src/principal/mod.rs | 31 +- crates/carddav/Cargo.toml | 6 +- crates/carddav/src/address_object/methods.rs | 73 +- crates/carddav/src/address_object/resource.rs | 39 +- .../carddav/src/addressbook/methods/mkcol.rs | 40 +- crates/carddav/src/addressbook/methods/mod.rs | 2 +- .../methods/report/addressbook_multiget.rs | 33 +- .../src/addressbook/methods/report/mod.rs | 39 +- crates/carddav/src/addressbook/resource.rs | 49 +- crates/carddav/src/error.rs | 22 +- crates/carddav/src/lib.rs | 85 +-- crates/carddav/src/principal/mod.rs | 22 +- crates/dav/Cargo.toml | 13 +- crates/dav/src/error.rs | 24 - crates/dav/src/header/depth.rs | 37 +- crates/dav/src/header/overwrite.rs | 32 - crates/dav/src/lib.rs | 1 - crates/dav/src/resource/axum_methods.rs | 4 +- crates/dav/src/resource/axum_service.rs | 8 +- crates/dav/src/resource/methods/delete.rs | 72 +- crates/dav/src/resource/methods/mod.rs | 11 - crates/dav/src/resource/methods/propfind.rs | 40 +- crates/dav/src/resource/methods/proppatch.rs | 49 +- crates/dav/src/resource/mod.rs | 4 - crates/dav/src/resource/resource_service.rs | 39 +- crates/dav/src/resources/root.rs | 18 +- crates/dav/src/xml/multistatus.rs | 22 - crates/dav_push/Cargo.toml | 2 - crates/frontend/Cargo.toml | 9 +- crates/frontend/src/assets.rs | 106 ++- crates/frontend/src/lib.rs | 643 +++++++++--------- crates/frontend/src/nextcloud_login/mod.rs | 67 +- crates/frontend/src/routes/login.rs | 87 ++- crates/frontend/src/routes/mod.rs | 5 +- crates/ical/Cargo.toml | 4 +- crates/ical/src/error.rs | 21 +- crates/oidc/Cargo.toml | 4 +- crates/oidc/src/error.rs | 22 +- crates/oidc/src/lib.rs | 328 +++++---- crates/oidc/src/user_store.rs | 6 +- crates/store/Cargo.toml | 10 +- crates/store/src/auth/middleware.rs | 128 ++-- crates/store/src/auth/user.rs | 57 +- crates/store/src/error.rs | 6 +- src/app.rs | 108 ++- src/main.rs | 77 +-- 60 files changed, 1476 insertions(+), 2205 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a912de6..529310a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,221 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44dfe5c9e0004c623edc65391dfd51daa201e7e30ebd9c9bedf873048ec32bc2" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "base64 0.22.1", - "bitflags", - "brotli", - "bytes", - "bytestring", - "derive_more 2.0.1", - "encoding_rs", - "flate2", - "foldhash", - "futures-core", - "h2 0.3.26", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.1", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-session" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efe6976a74f34f1b6d07a6c05aadc0ed0359304a7781c367fa5b4029418db08f" -dependencies = [ - "actix-service", - "actix-utils", - "actix-web", - "anyhow", - "derive_more 1.0.0", - "rand 0.8.5", - "serde", - "serde_json", - "tracing", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more 2.0.1", - "encoding_rs", - "foldhash", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "actix-web-httpauth" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456348ed9dcd72a13a1f4a660449fafdecee9ac8205552e286809eb5b0b29bd3" -dependencies = [ - "actix-utils", - "actix-web", - "base64 0.22.1", - "futures-core", - "futures-util", - "log", - "pin-project-lite", -] - [[package]] name = "addr2line" version = "0.24.2" @@ -276,21 +61,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -431,10 +201,11 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a91fdeb04bf77d96234780cdd58fc221eb10de7031e1782a22f40fc8ac1a313" dependencies = [ - "actix-web", "askama", "askama_web_derive", + "axum-core", "bytes", + "http", ] [[package]] @@ -499,7 +270,7 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http 1.3.1", + "http", "http-body", "http-body-util", "hyper", @@ -531,7 +302,7 @@ checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http", "http-body", "http-body-util", "mime", @@ -554,7 +325,7 @@ dependencies = [ "bytes", "futures-util", "headers", - "http 1.3.1", + "http", "http-body", "http-body-util", "mime", @@ -566,6 +337,37 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum_session" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cbd59ac11f92412bd86308ce6c9d579e4a6ab627a2c7074afa5e4ef7144900d" +dependencies = [ + "aes-gcm", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "chrono", + "cookie", + "dashmap", + "forwarded-header-value", + "futures", + "hmac", + "http", + "http-body", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.12", + "tokio", + "tower-layer", + "tower-service", + "tracing", + "uuid", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -587,12 +389,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base64" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" - [[package]] name = "base64" version = "0.21.7" @@ -647,27 +443,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "brotli" -version = "8.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bumpalo" version = "3.17.0" @@ -692,23 +467,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -[[package]] -name = "bytestring" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" -dependencies = [ - "bytes", -] - [[package]] name = "cc" version = "1.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" dependencies = [ - "jobserver", - "libc", "shlex", ] @@ -842,17 +606,14 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "cookie" -version = "0.16.2" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ "aes-gcm", - "base64 0.20.0", - "hkdf", - "hmac", + "base64 0.22.1", "percent-encoding", "rand 0.8.5", - "sha2", "subtle", "time", "version_check", @@ -888,15 +649,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -1006,6 +758,20 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "der" version = "0.7.10" @@ -1027,34 +793,13 @@ dependencies = [ "serde", ] -[[package]] -name = "derive_more" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" -dependencies = [ - "derive_more-impl 1.0.0", -] - [[package]] name = "derive_more" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ - "derive_more-impl 2.0.1", -] - -[[package]] -name = "derive_more-impl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", + "derive_more-impl", ] [[package]] @@ -1239,16 +984,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "flate2" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "flume" version = "0.11.1" @@ -1281,6 +1016,31 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror 1.0.69", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1360,6 +1120,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1442,25 +1203,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.9.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.10" @@ -1472,7 +1214,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.3.1", + "http", "indexmap 2.9.0", "slab", "tokio", @@ -1486,6 +1228,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.3" @@ -1515,7 +1263,7 @@ dependencies = [ "base64 0.22.1", "bytes", "headers-core", - "http 1.3.1", + "http", "httpdate", "mime", "sha1", @@ -1527,7 +1275,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.3.1", + "http", ] [[package]] @@ -1572,17 +1320,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.3.1" @@ -1601,7 +1338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http", ] [[package]] @@ -1612,7 +1349,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http", "http-body", "pin-project-lite", ] @@ -1638,8 +1375,8 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.10", - "http 1.3.1", + "h2", + "http", "http-body", "httparse", "httpdate", @@ -1656,7 +1393,7 @@ version = "0.27.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" dependencies = [ - "http 1.3.1", + "http", "hyper", "hyper-util", "rustls", @@ -1691,7 +1428,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.3.1", + "http", "http-body", "hyper", "ipnet", @@ -1851,12 +1588,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - [[package]] name = "indexmap" version = "1.9.3" @@ -1940,16 +1671,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "jobserver" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - [[package]] name = "js-sys" version = "0.3.77" @@ -1960,12 +1681,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "lazy_static" version = "1.5.0" @@ -2004,23 +1719,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - [[package]] name = "lock_api" version = "0.4.13" @@ -2106,16 +1804,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.59.0", ] [[package]] -name = "mutually_exclusive_features" -version = "0.1.0" +name = "nonempty" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" [[package]] name = "nu-ansi-term" @@ -2189,7 +1886,7 @@ dependencies = [ "base64 0.22.1", "chrono", "getrandom 0.2.16", - "http 1.3.1", + "http", "rand 0.8.5", "reqwest", "serde", @@ -2238,7 +1935,7 @@ dependencies = [ "dyn-clone", "ed25519-dalek", "hmac", - "http 1.3.1", + "http", "itertools 0.10.5", "log", "oauth2", @@ -2280,7 +1977,7 @@ checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" dependencies = [ "async-trait", "bytes", - "http 1.3.1", + "http", "opentelemetry", "reqwest", ] @@ -2291,7 +1988,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" dependencies = [ - "http 1.3.1", + "http", "opentelemetry", "opentelemetry-http", "opentelemetry-proto", @@ -2866,12 +2563,6 @@ dependencies = [ "regex-syntax 0.8.5", ] -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - [[package]] name = "regex-syntax" version = "0.6.29" @@ -2902,8 +2593,8 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.4.10", - "http 1.3.1", + "h2", + "http", "http-body", "http-body-util", "hyper", @@ -3113,10 +2804,10 @@ dependencies = [ name = "rustical" version = "0.1.0" dependencies = [ - "actix-web", "anyhow", "argon2", "async-trait", + "axum", "clap", "figment", "opentelemetry", @@ -3142,7 +2833,6 @@ dependencies = [ "tokio", "toml", "tracing", - "tracing-actix-web", "tracing-opentelemetry", "tracing-subscriber", "uuid", @@ -3152,15 +2842,16 @@ dependencies = [ name = "rustical_caldav" version = "0.1.0" dependencies = [ - "actix-web", - "actix-web-httpauth", "async-trait", + "axum", + "axum-extra", "base64 0.22.1", "chrono", "chrono-tz", - "derive_more 2.0.1", + "derive_more", "futures-util", - "http 1.3.1", + "headers", + "http", "quick-xml", "rustical_dav", "rustical_dav_push", @@ -3171,8 +2862,8 @@ dependencies = [ "sha2", "thiserror 2.0.12", "tokio", + "tower", "tracing", - "tracing-actix-web", "url", "uuid", ] @@ -3181,14 +2872,14 @@ dependencies = [ name = "rustical_carddav" version = "0.1.0" dependencies = [ - "actix-web", - "actix-web-httpauth", "async-trait", + "axum", + "axum-extra", "base64 0.22.1", "chrono", - "derive_more 2.0.1", + "derive_more", "futures-util", - "http 1.3.1", + "http", "quick-xml", "rustical_dav", "rustical_dav_push", @@ -3198,8 +2889,8 @@ dependencies = [ "serde", "thiserror 2.0.12", "tokio", + "tower", "tracing", - "tracing-actix-web", "url", "uuid", ] @@ -3208,15 +2899,13 @@ dependencies = [ name = "rustical_dav" version = "0.1.0" dependencies = [ - "actix-web", "async-trait", "axum", "axum-extra", - "derive_more 2.0.1", + "derive_more", "futures-util", "headers", - "http 0.2.12", - "http 1.3.1", + "http", "itertools 0.14.0", "log", "quick-xml", @@ -3226,18 +2915,16 @@ dependencies = [ "tokio", "tower", "tracing", - "tracing-actix-web", ] [[package]] name = "rustical_dav_push" version = "0.1.0" dependencies = [ - "actix-web", "async-trait", - "derive_more 2.0.1", + "derive_more", "futures-util", - "http 1.3.1", + "http", "itertools 0.14.0", "log", "quick-xml", @@ -3249,22 +2936,23 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tracing", - "tracing-actix-web", ] [[package]] name = "rustical_frontend" version = "0.1.0" dependencies = [ - "actix-session", - "actix-web", "askama", "askama_web", "async-trait", + "axum", + "axum-extra", "chrono", "chrono-humanize", "futures-core", + "headers", "hex", + "http", "mime_guess", "rand 0.8.5", "rust-embed", @@ -3273,6 +2961,7 @@ dependencies = [ "serde", "thiserror 2.0.12", "tokio", + "tower", "tracing", "url", "uuid", @@ -3282,10 +2971,10 @@ dependencies = [ name = "rustical_ical" version = "0.1.0" dependencies = [ - "actix-web", + "axum", "chrono", "chrono-tz", - "derive_more 2.0.1", + "derive_more", "ical", "lazy_static", "regex", @@ -3300,9 +2989,9 @@ dependencies = [ name = "rustical_oidc" version = "0.1.0" dependencies = [ - "actix-session", - "actix-web", "async-trait", + "axum", + "axum_session", "openidconnect", "reqwest", "serde", @@ -3313,15 +3002,16 @@ dependencies = [ name = "rustical_store" version = "0.1.0" dependencies = [ - "actix-session", - "actix-web", - "actix-web-httpauth", "anyhow", "async-trait", + "axum", "chrono", "chrono-tz", "clap", - "derive_more 2.0.1", + "derive_more", + "futures-core", + "headers", + "http", "ical", "lazy_static", "rand 0.8.5", @@ -3337,6 +3027,7 @@ dependencies = [ "sha2", "thiserror 2.0.12", "tokio", + "tower", "tracing", "uuid", ] @@ -3347,7 +3038,7 @@ version = "0.1.0" dependencies = [ "async-trait", "chrono", - "derive_more 2.0.1", + "derive_more", "password-auth", "password-hash", "pbkdf2", @@ -4150,7 +3841,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "bytes", - "http 1.3.1", + "http", "http-body", "http-body-util", "hyper", @@ -4195,7 +3886,7 @@ dependencies = [ "bitflags", "bytes", "futures-util", - "http 1.3.1", + "http", "http-body", "iri-string", "pin-project-lite", @@ -4228,19 +3919,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-actix-web" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2340b7722695166c7fc9b3e3cd1166e7c74fedb9075b8f0c74d3822d2e41caf5" -dependencies = [ - "actix-web", - "mutually_exclusive_features", - "pin-project", - "tracing", - "uuid", -] - [[package]] name = "tracing-attributes" version = "0.1.28" @@ -4418,6 +4096,7 @@ dependencies = [ "getrandom 0.3.3", "js-sys", "rand 0.9.1", + "serde", "wasm-bindgen", ] @@ -4971,31 +4650,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index 54ebc58..e5c81eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,11 +32,8 @@ debug = 0 [workspace.dependencies] uuid = { version = "1.11", features = ["v4", "fast-rng"] } async-trait = "0.1" -actix-web = "4.11" +axum = "0.8" tracing = { version = "0.1", features = ["async-await"] } -tracing-actix-web = "0.7" -actix-session = { version = "0.10", features = ["cookie-session"] } -actix-web-httpauth = "0.8" anyhow = { version = "1.0", features = ["backtrace"] } serde = { version = "1.0", features = ["serde_derive", "derive", "rc"] } futures-util = "0.3" @@ -63,7 +60,7 @@ quick-xml = { version = "0.37" } rust-embed = "8.5" futures-core = "0.3.31" hex = { version = "0.4.3", features = ["serde"] } -mime_guess = "2.0.5" +mime_guess = "2.0" itertools = "0.14" log = "0.4" derive_more = { version = "2.0", features = [ @@ -75,7 +72,7 @@ derive_more = { version = "2.0", features = [ "display", ] } askama = { version = "0.14", features = ["serde_json"] } -askama_web = { version = "0.14.0", features = ["actix-web-4"] } +askama_web = { version = "0.14.0", features = ["axum-0.8"] } sqlx = { version = "0.8", default-features = false, features = [ "sqlx-sqlite", "uuid", @@ -86,7 +83,6 @@ sqlx = { version = "0.8", default-features = false, features = [ "migrate", "json", ] } -http_02 = { package = "http", version = "0.2" } # actix-web uses a very outdated version http = "1.3" headers = "0.4" strum = "0.27" @@ -95,7 +91,8 @@ serde_json = { version = "1.0", features = ["raw_value"] } sqlx-sqlite = { version = "0.8", features = ["bundled"] } ical = { version = "0.11", features = ["generator", "serde"] } toml = "0.8" -rustical_dav = { path = "./crates/dav/", features = ["actix"] } +tower = "0.5" +rustical_dav = { path = "./crates/dav/" } rustical_dav_push = { path = "./crates/dav_push/" } rustical_store = { path = "./crates/store/" } rustical_store_sqlite = { path = "./crates/store_sqlite/" } @@ -104,10 +101,11 @@ rustical_carddav = { path = "./crates/carddav/" } rustical_frontend = { path = "./crates/frontend/" } rustical_xml = { path = "./crates/xml/" } rustical_oidc = { path = "./crates/oidc/" } -rustical_ical = { path = "./crates/ical/", features = ["actix"] } +rustical_ical = { path = "./crates/ical/"} chrono-tz = "0.10" chrono-humanize = "0.2" rand = "0.8" +axum-extra = { version = "0.10", features = ["typed-header"] } rrule = "0.14" argon2 = "0.5" rpassword = "7.3" @@ -129,9 +127,8 @@ clap = { version = "4.5", features = ["derive", "env"] } rustical_store = { workspace = true } rustical_store_sqlite = { workspace = true } rustical_caldav = { workspace = true } -rustical_carddav = { workspace = true } +rustical_carddav.workspace = true rustical_frontend = { workspace = true } -actix-web = { workspace = true } toml = { workspace = true } serde = { workspace = true } tokio = { workspace = true } @@ -140,8 +137,8 @@ anyhow = { workspace = true } clap.workspace = true sqlx = { workspace = true } async-trait = { workspace = true } -tracing-actix-web = { workspace = true } uuid.workspace = true +axum.workspace = true opentelemetry = { version = "0.30", optional = true } opentelemetry-otlp = { version = "0.30", optional = true, features = [ diff --git a/crates/caldav/Cargo.toml b/crates/caldav/Cargo.toml index 23fabda..1103f03 100644 --- a/crates/caldav/Cargo.toml +++ b/crates/caldav/Cargo.toml @@ -7,15 +7,15 @@ repository.workspace = true publish = false [dependencies] -actix-web = { workspace = true } +axum.workspace = true +axum-extra.workspace = true +tower.workspace = true async-trait = { workspace = true } thiserror = { workspace = true } quick-xml = { workspace = true } tracing = { workspace = true } -tracing-actix-web = { workspace = true } futures-util = { workspace = true } derive_more = { workspace = true } -actix-web-httpauth = { workspace = true } base64 = { workspace = true } serde = { workspace = true } tokio = { workspace = true } @@ -30,3 +30,4 @@ uuid.workspace = true rustical_dav_push.workspace = true rustical_ical.workspace = true http.workspace = true +headers.workspace = true diff --git a/crates/caldav/src/calendar/methods/mkcalendar.rs b/crates/caldav/src/calendar/methods/mkcalendar.rs index d113cc4..fca3e00 100644 --- a/crates/caldav/src/calendar/methods/mkcalendar.rs +++ b/crates/caldav/src/calendar/methods/mkcalendar.rs @@ -1,13 +1,14 @@ use crate::Error; use crate::calendar::prop::SupportedCalendarComponentSet; -use actix_web::HttpResponse; -use actix_web::web::{Data, Path}; +use crate::calendar::resource::CalendarResourceService; +use axum::extract::{Path, State}; +use axum::response::{IntoResponse, Response}; +use http::StatusCode; use rustical_ical::CalendarObjectType; use rustical_store::auth::User; -use rustical_store::{Calendar, CalendarStore}; +use rustical_store::{Calendar, CalendarStore, SubscriptionStore}; use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag}; use tracing::instrument; -use tracing_actix_web::RootSpan; #[derive(XmlDeserialize, Clone, Debug)] pub struct MkcolCalendarProp { @@ -48,15 +49,13 @@ struct MkcalendarRequest { set: PropElement, } -#[instrument(parent = root_span.id(), skip(store, root_span))] -pub async fn route_mkcalendar( - path: Path<(String, String)>, - body: String, +#[instrument(skip(cal_store))] +pub async fn route_mkcalendar( + Path((principal, cal_id)): Path<(String, String)>, user: User, - store: Data, - root_span: RootSpan, -) -> Result { - let (principal, cal_id) = path.into_inner(); + State(CalendarResourceService { cal_store, .. }): State>, + body: String, +) -> Result { if !user.is_principal(&principal) { return Err(Error::Unauthorized); } @@ -87,12 +86,10 @@ pub async fn route_mkcalendar( ]), }; - match store.insert_calendar(calendar).await { + match cal_store.insert_calendar(calendar).await { // The spec says we should return a mkcalendar-response but I don't know what goes into it. // However, it works without one but breaks on iPadOS when using an empty one :) - Ok(()) => Ok(HttpResponse::Created() - .insert_header(("Cache-Control", "no-cache")) - .body("")), + Ok(()) => Ok(StatusCode::CREATED.into_response()), Err(err) => { dbg!(err.to_string()); Err(err.into()) diff --git a/crates/caldav/src/calendar/methods/mod.rs b/crates/caldav/src/calendar/methods/mod.rs index bd97f27..a4e758f 100644 --- a/crates/caldav/src/calendar/methods/mod.rs +++ b/crates/caldav/src/calendar/methods/mod.rs @@ -1,3 +1,3 @@ pub mod mkcalendar; -pub mod post; +// pub mod post; pub mod report; diff --git a/crates/caldav/src/calendar/methods/post.rs b/crates/caldav/src/calendar/methods/post.rs index 094c723..ba75b0e 100644 --- a/crates/caldav/src/calendar/methods/post.rs +++ b/crates/caldav/src/calendar/methods/post.rs @@ -1,8 +1,7 @@ use crate::Error; use crate::calendar::resource::{CalendarResource, CalendarResourceService}; -use actix_web::http::header; -use actix_web::web::{Data, Path}; -use actix_web::{HttpRequest, HttpResponse}; +use axum::extract::{Path, State}; +use axum::response::Response; use rustical_dav::privileges::UserPrivilege; use rustical_dav::resource::Resource; use rustical_dav_push::register::PushRegister; @@ -10,18 +9,14 @@ use rustical_store::auth::User; use rustical_store::{CalendarStore, Subscription, SubscriptionStore}; use rustical_xml::XmlDocument; use tracing::instrument; -use tracing_actix_web::RootSpan; -#[instrument(parent = root_span.id(), skip(resource_service, root_span, req))] +#[instrument(skip(resource_service))] pub async fn route_post( - path: Path<(String, String)>, - body: String, + Path((principal, cal_id)): Path<(String, String)>, user: User, - resource_service: Data>, - root_span: RootSpan, - req: HttpRequest, -) -> Result { - let (principal, cal_id) = path.into_inner(); + State(resource_service): State>, + body: String, +) -> Result { if !user.is_principal(&principal) { return Err(Error::Unauthorized); } diff --git a/crates/caldav/src/calendar/methods/report/calendar_multiget.rs b/crates/caldav/src/calendar/methods/report/calendar_multiget.rs index 58157b1..63a9973 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_multiget.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_multiget.rs @@ -1,5 +1,4 @@ use crate::{Error, calendar_object::resource::CalendarObjectPropWrapperName}; -use actix_web::dev::{Path, ResourceDef}; use rustical_dav::xml::PropfindType; use rustical_ical::CalendarObject; use rustical_store::CalendarStore; @@ -23,23 +22,25 @@ pub async fn get_objects_calendar_multiget( cal_id: &str, store: &C, ) -> Result<(Vec, Vec), Error> { - let resource_def = ResourceDef::prefix(path).join(&ResourceDef::new("/{object_id}.ics")); - let mut result = vec![]; let mut not_found = vec![]; for href in &cal_query.href { - let mut path = Path::new(href.as_str()); - if !resource_def.capture_match_info(&mut path) { + if let Some(filename) = href.strip_prefix(path) { + if let Some(object_id) = filename.strip_suffix(".ics") { + match store.get_object(principal, cal_id, object_id).await { + Ok(object) => result.push(object), + Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()), + Err(err) => return Err(err.into()), + }; + } else { + not_found.push(href.to_owned()); + continue; + } + } else { not_found.push(href.to_owned()); continue; - }; - let object_id = path.get("object_id").unwrap(); - match store.get_object(principal, cal_id, object_id).await { - Ok(object) => result.push(object), - Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()), - Err(err) => return Err(err.into()), - }; + } } Ok((result, not_found)) diff --git a/crates/caldav/src/calendar/methods/report/mod.rs b/crates/caldav/src/calendar/methods/report/mod.rs index e674235..32a770d 100644 --- a/crates/caldav/src/calendar/methods/report/mod.rs +++ b/crates/caldav/src/calendar/methods/report/mod.rs @@ -1,12 +1,14 @@ use crate::{ CalDavPrincipalUri, Error, + calendar::resource::CalendarResourceService, calendar_object::resource::{ CalendarObjectPropWrapper, CalendarObjectPropWrapperName, CalendarObjectResource, }, }; -use actix_web::{ - HttpRequest, Responder, - web::{Data, Path}, +use axum::{ + Extension, + extract::{OriginalUri, Path, State}, + response::IntoResponse, }; use calendar_multiget::{CalendarMultigetRequest, get_objects_calendar_multiget}; use calendar_query::{CalendarQueryRequest, get_objects_calendar_query}; @@ -19,7 +21,7 @@ use rustical_dav::{ }, }; use rustical_ical::CalendarObject; -use rustical_store::{CalendarStore, auth::User}; +use rustical_store::{CalendarStore, SubscriptionStore, auth::User}; use rustical_xml::{XmlDeserialize, XmlDocument}; use sync_collection::handle_sync_collection; use tracing::instrument; @@ -85,16 +87,15 @@ fn objects_response( }) } -#[instrument(skip(req, cal_store))] -pub async fn route_report_calendar( - path: Path<(String, String)>, - body: String, +#[instrument(skip(cal_store))] +pub async fn route_report_calendar( + Path((principal, cal_id)): Path<(String, String)>, user: User, - req: HttpRequest, - puri: Data, - cal_store: Data, -) -> Result { - let (principal, cal_id) = path.into_inner(); + Extension(puri): Extension, + State(CalendarResourceService { cal_store, .. }): State>, + OriginalUri(uri): OriginalUri, + body: String, +) -> Result { if !user.is_principal(&principal) { return Err(Error::Unauthorized); } @@ -107,20 +108,12 @@ pub async fn route_report_calendar( let objects = get_objects_calendar_query(cal_query, &principal, &cal_id, cal_store.as_ref()) .await?; - objects_response( - objects, - vec![], - req.path(), - &principal, - puri.as_ref(), - &user, - props, - )? + objects_response(objects, vec![], uri.path(), &principal, &puri, &user, props)? } ReportRequest::CalendarMultiget(cal_multiget) => { let (objects, not_found) = get_objects_calendar_multiget( cal_multiget, - req.path(), + uri.path(), &principal, &cal_id, cal_store.as_ref(), @@ -129,9 +122,9 @@ pub async fn route_report_calendar( objects_response( objects, not_found, - req.path(), + uri.path(), &principal, - puri.as_ref(), + &puri, &user, props, )? @@ -139,8 +132,8 @@ pub async fn route_report_calendar( ReportRequest::SyncCollection(sync_collection) => { handle_sync_collection( sync_collection, - req.path(), - puri.as_ref(), + uri.path(), + &puri, &user, &principal, &cal_id, diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index e171066..a5fd9c5 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -1,19 +1,20 @@ -use super::methods::mkcalendar::route_mkcalendar; -use super::methods::post::route_post; -use super::methods::report::route_report_calendar; use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData, SupportedReportSet}; -use crate::calendar_object::resource::{CalendarObjectResource, CalendarObjectResourceService}; +use crate::calendar::methods::mkcalendar::route_mkcalendar; +use crate::calendar::methods::report::route_report_calendar; +use crate::calendar_object::resource::CalendarObjectResource; use crate::{CalDavPrincipalUri, Error}; -use actix_web::http::Method; -use actix_web::web::{self}; use async_trait::async_trait; +use axum::extract::Request; +use axum::handler::Handler; +use axum::response::Response; use chrono::{DateTime, Utc}; use derive_more::derive::{From, Into}; +use futures_util::future::BoxFuture; use rustical_dav::extensions::{ CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp, }; use rustical_dav::privileges::UserPrivilegeSet; -use rustical_dav::resource::{PrincipalUri, Resource, ResourceService}; +use rustical_dav::resource::{AxumMethods, PrincipalUri, Resource, ResourceService}; use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner}; use rustical_dav_push::{DavPushExtension, DavPushExtensionProp}; use rustical_ical::CalDateTime; @@ -21,8 +22,10 @@ use rustical_store::auth::User; use rustical_store::{Calendar, CalendarStore, SubscriptionStore}; use rustical_xml::{EnumVariants, PropName}; use rustical_xml::{XmlDeserialize, XmlSerialize}; +use std::convert::Infallible; use std::str::FromStr; use std::sync::Arc; +use tower::Service; #[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[xml(unit_variants_ident = "CalendarPropName")] @@ -313,6 +316,15 @@ pub struct CalendarResourceService { pub(crate) sub_store: Arc, } +impl Clone for CalendarResourceService { + fn clone(&self) -> Self { + Self { + cal_store: self.cal_store.clone(), + sub_store: self.sub_store.clone(), + } + } +} + impl CalendarResourceService { pub fn new(cal_store: Arc, sub_store: Arc) -> Self { Self { @@ -386,17 +398,21 @@ impl ResourceService for CalendarResourc .await?; Ok(()) } +} - fn actix_scope(self) -> actix_web::Scope { - let report_method = web::method(Method::from_str("REPORT").unwrap()); - let mkcalendar_method = web::method(Method::from_str("MKCALENDAR").unwrap()); - web::scope("/{calendar_id}") - .service(CalendarObjectResourceService::new(self.cal_store.clone()).actix_scope()) - .service( - self.actix_resource() - .route(report_method.to(route_report_calendar::)) - .route(mkcalendar_method.to(route_mkcalendar::)) - .post(route_post::), - ) +impl AxumMethods for CalendarResourceService { + fn report() -> Option BoxFuture<'static, Result>> { + Some(|state, req| { + let mut service = Handler::with_state(route_report_calendar::, state); + Box::pin(Service::call(&mut service, req)) + }) + } + + fn mkcalendar() -> Option BoxFuture<'static, Result>> + { + Some(|state, req| { + let mut service = Handler::with_state(route_mkcalendar::, state); + Box::pin(Service::call(&mut service, req)) + }) } } diff --git a/crates/caldav/src/calendar_object/methods.rs b/crates/caldav/src/calendar_object/methods.rs index 9199e3d..7caceee 100644 --- a/crates/caldav/src/calendar_object/methods.rs +++ b/crates/caldav/src/calendar_object/methods.rs @@ -1,71 +1,70 @@ +use super::resource::CalendarObjectPathComponents; use crate::Error; +use crate::calendar_object::resource::CalendarObjectResourceService; use crate::error::Precondition; -use actix_web::HttpRequest; -use actix_web::HttpResponse; -use actix_web::http::header; -use actix_web::http::header::HeaderValue; -use actix_web::web::{Data, Path}; +use axum::body::Body; +use axum::extract::{Path, State}; +use axum::response::{IntoResponse, Response}; +use axum_extra::TypedHeader; +use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch}; +use http::StatusCode; use rustical_ical::CalendarObject; use rustical_store::CalendarStore; use rustical_store::auth::User; +use std::str::FromStr; use tracing::instrument; -use tracing_actix_web::RootSpan; -use super::resource::CalendarObjectPathComponents; - -#[instrument(parent = root_span.id(), skip(store, root_span))] +#[instrument(skip(cal_store))] pub async fn get_event( - path: Path, - store: Data, - user: User, - root_span: RootSpan, -) -> Result { - let CalendarObjectPathComponents { + Path(CalendarObjectPathComponents { principal, calendar_id, object_id, - } = path.into_inner(); - + }): Path, + State(CalendarObjectResourceService { cal_store }): State>, + user: User, +) -> Result { if !user.is_principal(&principal) { - return Ok(HttpResponse::Unauthorized().body("")); + return Err(crate::Error::Unauthorized); } - let calendar = store.get_calendar(&principal, &calendar_id).await?; + let calendar = cal_store.get_calendar(&principal, &calendar_id).await?; if !user.is_principal(&calendar.principal) { - return Ok(HttpResponse::Unauthorized().body("")); + return Err(crate::Error::Unauthorized); } - let event = store + let event = cal_store .get_object(&principal, &calendar_id, &object_id) .await?; - Ok(HttpResponse::Ok() - .insert_header(("ETag", event.get_etag())) - .insert_header(("Content-Type", "text/calendar")) - .body(event.get_ics().to_owned())) + let mut resp = Response::builder().status(StatusCode::OK); + let hdrs = resp.headers_mut().unwrap(); + hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap()); + hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap()); + Ok(resp.body(Body::new(event.get_ics().to_owned())).unwrap()) } -#[instrument(parent = root_span.id(), skip(store, req, root_span))] +#[instrument(skip(cal_store))] pub async fn put_event( - path: Path, - store: Data, - body: String, - user: User, - req: HttpRequest, - root_span: RootSpan, -) -> Result { - let CalendarObjectPathComponents { + Path(CalendarObjectPathComponents { principal, calendar_id, object_id, - } = path.into_inner(); - + }): Path, + State(CalendarObjectResourceService { cal_store }): State>, + user: User, + if_none_match: Option>, + body: String, +) -> Result { if !user.is_principal(&principal) { - return Ok(HttpResponse::Unauthorized().finish()); + return Err(crate::Error::Unauthorized); } - let overwrite = - Some(&HeaderValue::from_static("*")) != req.headers().get(header::IF_NONE_MATCH); + let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match { + if_none_match == IfNoneMatch::any() + } else { + true + }; let object = match CalendarObject::from_ics(object_id, body) { Ok(obj) => obj, @@ -73,9 +72,9 @@ pub async fn put_event( return Err(Error::PreconditionFailed(Precondition::ValidCalendarData)); } }; - store + cal_store .put_object(principal, calendar_id, object, overwrite) .await?; - Ok(HttpResponse::Created().finish()) + Ok(StatusCode::CREATED.into_response()) } diff --git a/crates/caldav/src/calendar_object/resource.rs b/crates/caldav/src/calendar_object/resource.rs index 29a8a04..50f5ee4 100644 --- a/crates/caldav/src/calendar_object/resource.rs +++ b/crates/caldav/src/calendar_object/resource.rs @@ -1,22 +1,35 @@ -use super::methods::{get_event, put_event}; -use crate::{CalDavPrincipalUri, Error}; -use actix_web::web; +// use super::methods::{get_event, put_event}; +use crate::{ + CalDavPrincipalUri, Error, + calendar_object::methods::{get_event, put_event}, +}; use async_trait::async_trait; +use axum::{extract::Request, handler::Handler, response::Response}; use derive_more::derive::{From, Into}; +use futures_util::future::BoxFuture; use rustical_dav::{ extensions::{CommonPropertiesExtension, CommonPropertiesProp}, privileges::UserPrivilegeSet, - resource::{PrincipalUri, Resource, ResourceService}, + resource::{AxumMethods, PrincipalUri, Resource, ResourceService}, xml::Resourcetype, }; use rustical_ical::{CalendarObject, UtcDateTime}; use rustical_store::{CalendarStore, auth::User}; use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize}; -use serde::Deserialize; -use std::sync::Arc; +use serde::{Deserialize, Deserializer}; +use std::{convert::Infallible, sync::Arc}; +use tower::Service; pub struct CalendarObjectResourceService { - cal_store: Arc, + pub(crate) cal_store: Arc, +} + +impl Clone for CalendarObjectResourceService { + fn clone(&self) -> Self { + Self { + cal_store: self.cal_store.clone(), + } + } } impl CalendarObjectResourceService { @@ -130,10 +143,23 @@ impl Resource for CalendarObjectResource { } } +fn deserialize_ics_name<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let name: String = Deserialize::deserialize(deserializer)?; + if let Some(object_id) = name.strip_suffix(".ics") { + Ok(object_id.to_owned()) + } else { + Err(serde::de::Error::custom("Missing .ics extension")) + } +} + #[derive(Debug, Clone, Deserialize)] pub struct CalendarObjectPathComponents { pub principal: String, pub calendar_id: String, + #[serde(deserialize_with = "deserialize_ics_name")] pub object_id: String, } @@ -180,12 +206,19 @@ impl ResourceService for CalendarObjectResourceService { .await?; Ok(()) } +} - fn actix_scope(self) -> actix_web::Scope { - web::scope("/{object_id}.ics").service( - self.actix_resource() - .get(get_event::) - .put(put_event::), - ) +impl AxumMethods for CalendarObjectResourceService { + fn get() -> Option BoxFuture<'static, Result>> { + Some(|state, req| { + let mut service = Handler::with_state(get_event::, state); + Box::pin(Service::call(&mut service, req)) + }) + } + fn put() -> Option BoxFuture<'static, Result>> { + Some(|state, req| { + let mut service = Handler::with_state(put_event::, state); + Box::pin(Service::call(&mut service, req)) + }) } } diff --git a/crates/caldav/src/calendar_set/mod.rs b/crates/caldav/src/calendar_set/mod.rs index d15ea14..315ee1f 100644 --- a/crates/caldav/src/calendar_set/mod.rs +++ b/crates/caldav/src/calendar_set/mod.rs @@ -1,10 +1,9 @@ -use crate::calendar::resource::{CalendarResource, CalendarResourceService}; +use crate::calendar::resource::CalendarResource; use crate::{CalDavPrincipalUri, Error}; -use actix_web::web; use async_trait::async_trait; use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp}; use rustical_dav::privileges::UserPrivilegeSet; -use rustical_dav::resource::{PrincipalUri, Resource, ResourceService}; +use rustical_dav::resource::{AxumMethods, PrincipalUri, Resource, ResourceService}; use rustical_dav::xml::{Resourcetype, ResourcetypeInner}; use rustical_store::auth::User; use rustical_store::{CalendarStore, SubscriptionStore}; @@ -67,6 +66,16 @@ pub struct CalendarSetResourceService { sub_store: Arc, } +impl Clone for CalendarSetResourceService { + fn clone(&self) -> Self { + Self { + name: self.name, + cal_store: self.cal_store.clone(), + sub_store: self.sub_store.clone(), + } + } +} + impl CalendarSetResourceService { pub fn new(name: &'static str, cal_store: Arc, sub_store: Arc) -> Self { Self { @@ -116,16 +125,5 @@ impl ResourceService for CalendarSetReso }) .collect()) } - - fn actix_scope(self) -> actix_web::Scope { - web::scope(&format!("/{}", self.name)) - .service( - CalendarResourceService::<_, S>::new( - self.cal_store.clone(), - self.sub_store.clone(), - ) - .actix_scope(), - ) - .service(self.actix_resource()) - } } +impl AxumMethods for CalendarSetResourceService {} diff --git a/crates/caldav/src/error.rs b/crates/caldav/src/error.rs index 00bac42..750f91a 100644 --- a/crates/caldav/src/error.rs +++ b/crates/caldav/src/error.rs @@ -1,7 +1,9 @@ -use actix_web::{ - HttpResponse, - http::{StatusCode, header::ContentType}, +use axum::{ + body::Body, + response::{IntoResponse, Response}, }; +use headers::{ContentType, HeaderMapExt}; +use http::StatusCode; use rustical_xml::{XmlSerialize, XmlSerializeRoot}; use tracing::error; @@ -12,22 +14,18 @@ pub enum Precondition { ValidCalendarData, } -impl actix_web::ResponseError for Precondition { - fn status_code(&self) -> StatusCode { - StatusCode::PRECONDITION_FAILED - } - fn error_response(&self) -> HttpResponse { +impl IntoResponse for Precondition { + fn into_response(self) -> axum::response::Response { let mut output: Vec<_> = b"\n".into(); let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4); - let error = rustical_dav::xml::ErrorElement(self); + let error = rustical_dav::xml::ErrorElement(&self); if let Err(err) = error.serialize_root(&mut writer) { - return rustical_dav::Error::from(err).error_response(); + return rustical_dav::Error::from(err).into_response(); } - - HttpResponse::PreconditionFailed() - .content_type(ContentType::xml()) - .body(String::from_utf8(output).unwrap()) + let mut res = Response::builder().status(StatusCode::PRECONDITION_FAILED); + res.headers_mut().unwrap().typed_insert(ContentType::xml()); + res.body(Body::from(output)).unwrap() } } @@ -61,8 +59,8 @@ pub enum Error { PreconditionFailed(Precondition), } -impl actix_web::ResponseError for Error { - fn status_code(&self) -> actix_web::http::StatusCode { +impl Error { + pub fn status_code(&self) -> StatusCode { match self { Error::StoreError(err) => match err { rustical_store::Error::NotFound => StatusCode::NOT_FOUND, @@ -78,16 +76,13 @@ impl actix_web::ResponseError for Error { Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR, Error::NotFound => StatusCode::NOT_FOUND, Error::IcalError(err) => err.status_code(), - Error::PreconditionFailed(err) => err.status_code(), - } - } - fn error_response(&self) -> actix_web::HttpResponse { - error!("Error: {self}"); - match self { - Error::DavError(err) => err.error_response(), - Error::IcalError(err) => err.error_response(), - Error::PreconditionFailed(err) => err.error_response(), - _ => HttpResponse::build(self.status_code()).body(self.to_string()), + Error::PreconditionFailed(_err) => StatusCode::PRECONDITION_FAILED, } } } + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + (self.status_code(), self.to_string()).into_response() + } +} diff --git a/crates/caldav/src/lib.rs b/crates/caldav/src/lib.rs index 0f2db32..7eafff7 100644 --- a/crates/caldav/src/lib.rs +++ b/crates/caldav/src/lib.rs @@ -1,28 +1,26 @@ -use actix_web::HttpResponse; -use actix_web::body::BoxBody; -use actix_web::dev::{HttpServiceFactory, ServiceResponse}; -use actix_web::http::header::{self, HeaderName, HeaderValue}; -use actix_web::http::{Method, StatusCode}; -use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; -use actix_web::web::Data; +use axum::{Extension, Router}; use derive_more::Constructor; use principal::PrincipalResourceService; use rustical_dav::resource::{PrincipalUri, ResourceService}; use rustical_dav::resources::RootResourceService; -use rustical_store::auth::{AuthenticationMiddleware, AuthenticationProvider, User}; +use rustical_store::auth::middleware::AuthenticationLayer; +use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::{AddressbookStore, CalendarStore, ContactBirthdayStore, SubscriptionStore}; use std::sync::Arc; -use subscription::subscription_resource; pub mod calendar; pub mod calendar_object; pub mod calendar_set; pub mod error; pub mod principal; -mod subscription; +// mod subscription; pub use error::Error; +use crate::calendar::resource::CalendarResourceService; +use crate::calendar_object::resource::CalendarObjectResourceService; +use crate::calendar_set::CalendarSetResourceService; + #[derive(Debug, Clone, Constructor)] pub struct CalDavPrincipalUri(&'static str); @@ -32,33 +30,38 @@ impl PrincipalUri for CalDavPrincipalUri { } } -/// Quite a janky implementation but the default METHOD_NOT_ALLOWED response gives us the allowed -/// methods of a resource -fn options_handler() -> ErrorHandlers { - ErrorHandlers::new().handler(StatusCode::METHOD_NOT_ALLOWED, |res| { - Ok(ErrorHandlerResponse::Response( - if res.request().method() == Method::OPTIONS { - let mut response = HttpResponse::Ok(); - response.insert_header(( - HeaderName::from_static("dav"), - // https://datatracker.ietf.org/doc/html/rfc4918#section-18 - HeaderValue::from_static( - "1, 3, access-control, calendar-access, extended-mkcol, webdav-push", - ), - )); +// pub fn caldav_service< +// AP: AuthenticationProvider, +// AS: AddressbookStore, +// C: CalendarStore, +// S: SubscriptionStore, +// >( +// prefix: &'static str, +// auth_provider: Arc, +// store: Arc, +// addr_store: Arc, +// subscription_store: Arc, +// ) -> impl HttpServiceFactory { +// let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store)); +// +// RootResourceService::<_, User, CalDavPrincipalUri>::new(PrincipalResourceService { +// auth_provider: auth_provider.clone(), +// sub_store: subscription_store.clone(), +// birthday_store: birthday_store.clone(), +// cal_store: store.clone(), +// }) +// .actix_scope() +// .wrap(AuthenticationMiddleware::new(auth_provider.clone())) +// .wrap(options_handler()) +// .app_data(Data::from(store.clone())) +// .app_data(Data::from(birthday_store.clone())) +// .app_data(Data::new(CalDavPrincipalUri::new( +// format!("{prefix}/principal").leak(), +// ))) +// .service(subscription_resource(subscription_store)) +// } - if let Some(allow) = res.headers().get(header::ALLOW) { - response.insert_header((header::ALLOW, allow.to_owned())); - } - ServiceResponse::new(res.into_parts().0, response.finish()).map_into_right_body() - } else { - res.map_into_left_body() - }, - )) - }) -} - -pub fn caldav_service< +pub fn caldav_router< AP: AuthenticationProvider, AS: AddressbookStore, C: CalendarStore, @@ -69,22 +72,53 @@ pub fn caldav_service< store: Arc, addr_store: Arc, subscription_store: Arc, -) -> impl HttpServiceFactory { +) -> Router { let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store)); - - RootResourceService::<_, User, CalDavPrincipalUri>::new(PrincipalResourceService { + let principal_service = PrincipalResourceService { auth_provider: auth_provider.clone(), sub_store: subscription_store.clone(), birthday_store: birthday_store.clone(), cal_store: store.clone(), - }) - .actix_scope() - .wrap(AuthenticationMiddleware::new(auth_provider.clone())) - .wrap(options_handler()) - .app_data(Data::from(store.clone())) - .app_data(Data::from(birthday_store.clone())) - .app_data(Data::new(CalDavPrincipalUri::new( - format!("{prefix}/principal").leak(), - ))) - .service(subscription_resource(subscription_store)) + }; + + Router::new() + .route_service( + "/", + RootResourceService::<_, User, CalDavPrincipalUri>::new(principal_service.clone()) + .axum_service(), + ) + .route_service("/principal/{principal}", principal_service.axum_service()) + .route_service( + "/principal/{principal}/calendar", + CalendarSetResourceService::new("calendar", store.clone(), subscription_store.clone()) + .axum_service(), + ) + .route_service( + "/principal/{principal}/calendar/{calendar_id}", + CalendarResourceService::new(store.clone(), subscription_store.clone()).axum_service(), + ) + .route_service( + "/principal/{principal}/calendar/{calendar_id}/{object_id}", + CalendarObjectResourceService::new(store.clone()).axum_service(), + ) + .route_service( + "/principal/{principal}/birthdays", + CalendarSetResourceService::new( + "birthdays", + birthday_store.clone(), + subscription_store.clone(), + ) + .axum_service(), + ) + .route_service( + "/principal/{principal}/birthdays/{calendar_id}", + CalendarResourceService::new(birthday_store.clone(), subscription_store.clone()) + .axum_service(), + ) + .route_service( + "/principal/{principal}/birthdays/{calendar_id}/{object_id}", + CalendarObjectResourceService::new(birthday_store.clone()).axum_service(), + ) + .layer(AuthenticationLayer::new(auth_provider)) + .layer(Extension(CalDavPrincipalUri(prefix))) } diff --git a/crates/caldav/src/principal/mod.rs b/crates/caldav/src/principal/mod.rs index 57987dc..828a302 100644 --- a/crates/caldav/src/principal/mod.rs +++ b/crates/caldav/src/principal/mod.rs @@ -1,10 +1,9 @@ -use crate::calendar_set::{CalendarSetResource, CalendarSetResourceService}; +use crate::calendar_set::CalendarSetResource; use crate::{CalDavPrincipalUri, Error}; -use actix_web::web; use async_trait::async_trait; use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp}; use rustical_dav::privileges::UserPrivilegeSet; -use rustical_dav::resource::{PrincipalUri, Resource, ResourceService}; +use rustical_dav::resource::{AxumMethods, PrincipalUri, Resource, ResourceService}; use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner}; use rustical_store::auth::user::PrincipalType; use rustical_store::auth::{AuthenticationProvider, User}; @@ -194,25 +193,9 @@ impl actix_web::Scope { - web::scope("/principal/{principal}") - .service( - CalendarSetResourceService::<_, S>::new( - "calendar", - self.cal_store.clone(), - self.sub_store.clone(), - ) - .actix_scope(), - ) - .service( - CalendarSetResourceService::<_, S>::new( - "birthdays", - self.birthday_store.clone(), - self.sub_store.clone(), - ) - .actix_scope(), - ) - .service(self.actix_resource()) - } +} + +impl + AxumMethods for PrincipalResourceService +{ } diff --git a/crates/carddav/Cargo.toml b/crates/carddav/Cargo.toml index 5e30893..250ce33 100644 --- a/crates/carddav/Cargo.toml +++ b/crates/carddav/Cargo.toml @@ -7,15 +7,15 @@ repository.workspace = true publish = false [dependencies] -actix-web = { workspace = true } +axum.workspace = true +axum-extra.workspace = true +tower.workspace = true async-trait = { workspace = true } thiserror = { workspace = true } quick-xml = { workspace = true } tracing = { workspace = true } -tracing-actix-web = { workspace = true } futures-util = { workspace = true } derive_more = { workspace = true } -actix-web-httpauth = { workspace = true } base64 = { workspace = true } serde = { workspace = true } tokio = { workspace = true } diff --git a/crates/carddav/src/address_object/methods.rs b/crates/carddav/src/address_object/methods.rs index 0e48563..d562ceb 100644 --- a/crates/carddav/src/address_object/methods.rs +++ b/crates/carddav/src/address_object/methods.rs @@ -1,37 +1,37 @@ +use std::str::FromStr; + use super::resource::AddressObjectPathComponents; use crate::Error; +use crate::address_object::resource::AddressObjectResourceService; use crate::addressbook::resource::AddressbookResource; -use actix_web::HttpRequest; -use actix_web::HttpResponse; -use actix_web::http::header; -use actix_web::http::header::HeaderValue; -use actix_web::web::{Data, Path}; +use axum::body::Body; +use axum::extract::{Path, State}; +use axum::response::{IntoResponse, Response}; +use axum_extra::TypedHeader; +use axum_extra::headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch}; +use http::StatusCode; use rustical_dav::privileges::UserPrivilege; use rustical_dav::resource::Resource; use rustical_ical::AddressObject; use rustical_store::AddressbookStore; use rustical_store::auth::User; use tracing::instrument; -use tracing_actix_web::RootSpan; -#[instrument(parent = root_span.id(), skip(store, root_span))] +#[instrument(skip(addr_store))] pub async fn get_object( - path: Path, - store: Data, - user: User, - root_span: RootSpan, -) -> Result { - let AddressObjectPathComponents { + Path(AddressObjectPathComponents { principal, addressbook_id, object_id, - } = path.into_inner(); - + }): Path, + State(AddressObjectResourceService { addr_store }): State>, + user: User, +) -> Result { if !user.is_principal(&principal) { return Err(Error::Unauthorized); } - let addressbook = store + let addressbook = addr_store .get_addressbook(&principal, &addressbook_id, false) .await?; let addressbook_resource = AddressbookResource(addressbook); @@ -42,42 +42,43 @@ pub async fn get_object( return Err(Error::Unauthorized); } - let object = store + let object = addr_store .get_object(&principal, &addressbook_id, &object_id, false) .await?; - Ok(HttpResponse::Ok() - .insert_header(("ETag", object.get_etag())) - .insert_header(("Content-Type", "text/vcard")) - .body(object.get_vcf().to_owned())) + let mut resp = Response::builder().status(StatusCode::OK); + let hdrs = resp.headers_mut().unwrap(); + hdrs.typed_insert(ETag::from_str(&object.get_etag()).unwrap()); + hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap()); + Ok(resp.body(Body::new(object.get_vcf().to_owned())).unwrap()) } -#[instrument(parent = root_span.id(), skip(store, req, root_span))] +#[instrument(skip(addr_store, body))] pub async fn put_object( - path: Path, - store: Data, - body: String, - user: User, - req: HttpRequest, - root_span: RootSpan, -) -> Result { - let AddressObjectPathComponents { + Path(AddressObjectPathComponents { principal, addressbook_id, object_id, - } = path.into_inner(); - + }): Path, + State(AddressObjectResourceService { addr_store }): State>, + user: User, + if_none_match: Option>, + body: String, +) -> Result { if !user.is_principal(&principal) { return Err(Error::Unauthorized); } - let overwrite = - Some(&HeaderValue::from_static("*")) != req.headers().get(header::IF_NONE_MATCH); + let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match { + if_none_match == IfNoneMatch::any() + } else { + true + }; let object = AddressObject::from_vcf(object_id, body)?; - store + addr_store .put_object(principal, addressbook_id, object, overwrite) .await?; - Ok(HttpResponse::Created().finish()) + Ok(StatusCode::CREATED.into_response()) } diff --git a/crates/carddav/src/address_object/resource.rs b/crates/carddav/src/address_object/resource.rs index 389780d..84df360 100644 --- a/crates/carddav/src/address_object/resource.rs +++ b/crates/carddav/src/address_object/resource.rs @@ -1,24 +1,34 @@ use crate::{CardDavPrincipalUri, Error}; -use actix_web::web; use async_trait::async_trait; +use axum::{extract::Request, handler::Handler, response::Response}; use derive_more::derive::{Constructor, From, Into}; +use futures_util::future::BoxFuture; use rustical_dav::{ extensions::{CommonPropertiesExtension, CommonPropertiesProp}, privileges::UserPrivilegeSet, - resource::{PrincipalUri, Resource, ResourceService}, + resource::{AxumMethods, PrincipalUri, Resource, ResourceService}, xml::Resourcetype, }; use rustical_ical::AddressObject; use rustical_store::{AddressbookStore, auth::User}; use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize}; use serde::Deserialize; -use std::sync::Arc; +use std::{convert::Infallible, sync::Arc}; +use tower::Service; use super::methods::{get_object, put_object}; #[derive(Constructor)] pub struct AddressObjectResourceService { - addr_store: Arc, + pub(crate) addr_store: Arc, +} + +impl Clone for AddressObjectResourceService { + fn clone(&self) -> Self { + Self { + addr_store: self.addr_store.clone(), + } + } } #[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] @@ -148,13 +158,20 @@ impl ResourceService for AddressObjectResourceService .await?; Ok(()) } +} - #[inline] - fn actix_scope(self) -> actix_web::Scope { - web::scope("/{object_id}.vcf").service( - self.actix_resource() - .get(get_object::) - .put(put_object::), - ) +impl AxumMethods for AddressObjectResourceService { + fn get() -> Option BoxFuture<'static, Result>> { + Some(|state, req| { + let mut service = Handler::with_state(get_object::, state); + Box::pin(Service::call(&mut service, req)) + }) + } + + fn put() -> Option BoxFuture<'static, Result>> { + Some(|state, req| { + let mut service = Handler::with_state(put_object::, state); + Box::pin(Service::call(&mut service, req)) + }) } } diff --git a/crates/carddav/src/addressbook/methods/mkcol.rs b/crates/carddav/src/addressbook/methods/mkcol.rs index 732c2e9..fbf8e4e 100644 --- a/crates/carddav/src/addressbook/methods/mkcol.rs +++ b/crates/carddav/src/addressbook/methods/mkcol.rs @@ -1,10 +1,12 @@ -use crate::Error; -use actix_web::web::Path; -use actix_web::{HttpResponse, web::Data}; -use rustical_store::{Addressbook, AddressbookStore, auth::User}; +use crate::{Error, addressbook::resource::AddressbookResourceService}; +use axum::{ + extract::{Path, State}, + response::{IntoResponse, Response}, +}; +use http::StatusCode; +use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::User}; use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag}; use tracing::instrument; -use tracing_actix_web::RootSpan; #[derive(XmlDeserialize, Clone, Debug, PartialEq)] pub struct Resourcetype { @@ -39,15 +41,13 @@ struct MkcolRequest { set: PropElement, } -#[instrument(parent = root_span.id(), skip(store, root_span))] -pub async fn route_mkcol( - path: Path<(String, String)>, - body: String, +#[instrument(skip(addr_store))] +pub async fn route_mkcol( + Path((principal, addressbook_id)): Path<(String, String)>, user: User, - store: Data, - root_span: RootSpan, -) -> Result { - let (principal, addressbook_id) = path.into_inner(); + State(AddressbookResourceService { addr_store, .. }): State>, + body: String, +) -> Result { if !user.is_principal(&principal) { return Err(Error::Unauthorized); } @@ -65,7 +65,7 @@ pub async fn route_mkcol( push_topic: uuid::Uuid::new_v4().to_string(), }; - match store + match addr_store .get_addressbook(&principal, &addressbook_id, true) .await { @@ -74,7 +74,11 @@ pub async fn route_mkcol( } Ok(_) => { // oh no, there's a conflict - return Ok(HttpResponse::Conflict().body("An addressbook already exists at this URI")); + return Ok(( + StatusCode::CONFLICT, + "An addressbook already exists at this URI", + ) + .into_response()); } Err(err) => { // some other error @@ -82,12 +86,10 @@ pub async fn route_mkcol( } } - match store.insert_addressbook(addressbook).await { + match addr_store.insert_addressbook(addressbook).await { // TODO: The spec says we should return a mkcol-response. // However, it works without one but breaks on iPadOS when using an empty one :) - Ok(()) => Ok(HttpResponse::Created() - .insert_header(("Cache-Control", "no-cache")) - .body("")), + Ok(()) => Ok(StatusCode::CREATED.into_response()), Err(err) => { dbg!(err.to_string()); Err(err.into()) diff --git a/crates/carddav/src/addressbook/methods/mod.rs b/crates/carddav/src/addressbook/methods/mod.rs index 36ea3d9..c6afcf7 100644 --- a/crates/carddav/src/addressbook/methods/mod.rs +++ b/crates/carddav/src/addressbook/methods/mod.rs @@ -1,3 +1,3 @@ pub mod mkcol; -pub mod post; +// pub mod post; pub mod report; diff --git a/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs b/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs index 9dbee21..320f702 100644 --- a/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs +++ b/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs @@ -4,8 +4,6 @@ use crate::{ AddressObjectPropWrapper, AddressObjectPropWrapperName, AddressObjectResource, }, }; -use actix_web::dev::{Path, ResourceDef}; - use http::StatusCode; use rustical_dav::{ resource::{PrincipalUri, Resource}, @@ -32,27 +30,28 @@ pub async fn get_objects_addressbook_multiget( addressbook_id: &str, store: &AS, ) -> Result<(Vec, Vec), Error> { - let resource_def = ResourceDef::prefix(path).join(&ResourceDef::new("/{object_id}.vcf")); - let mut result = vec![]; let mut not_found = vec![]; for href in &addressbook_multiget.href { - let mut path = Path::new(href.as_str()); - if !resource_def.capture_match_info(&mut path) { + if let Some(filename) = href.strip_prefix(path) { + if let Some(object_id) = filename.strip_suffix(".vcf") { + match store + .get_object(principal, addressbook_id, object_id, false) + .await + { + Ok(object) => result.push(object), + Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()), + Err(err) => return Err(err.into()), + }; + } else { + not_found.push(href.to_owned()); + continue; + } + } else { not_found.push(href.to_owned()); continue; - }; - let object_id = path.get("object_id").unwrap(); - match store - .get_object(principal, addressbook_id, object_id, false) - .await - { - Ok(object) => result.push(object), - Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()), - // TODO: Maybe add error handling on a per-object basis - Err(err) => return Err(err.into()), - }; + } } Ok((result, not_found)) diff --git a/crates/carddav/src/addressbook/methods/report/mod.rs b/crates/carddav/src/addressbook/methods/report/mod.rs index e09b64d..82594a1 100644 --- a/crates/carddav/src/addressbook/methods/report/mod.rs +++ b/crates/carddav/src/addressbook/methods/report/mod.rs @@ -1,11 +1,15 @@ -use crate::{CardDavPrincipalUri, Error, address_object::resource::AddressObjectPropWrapperName}; -use actix_web::{ - HttpRequest, Responder, - web::{Data, Path}, +use crate::{ + CardDavPrincipalUri, Error, address_object::resource::AddressObjectPropWrapperName, + addressbook::resource::AddressbookResourceService, }; use addressbook_multiget::{AddressbookMultigetRequest, handle_addressbook_multiget}; +use axum::{ + Extension, + extract::{OriginalUri, Path, State}, + response::IntoResponse, +}; use rustical_dav::xml::{PropfindType, sync_collection::SyncCollectionRequest}; -use rustical_store::{AddressbookStore, auth::User}; +use rustical_store::{AddressbookStore, SubscriptionStore, auth::User}; use rustical_xml::{XmlDeserialize, XmlDocument}; use sync_collection::handle_sync_collection; use tracing::instrument; @@ -30,16 +34,15 @@ impl ReportRequest { } } -#[instrument(skip(req, addr_store))] -pub async fn route_report_addressbook( - path: Path<(String, String)>, - body: String, +#[instrument(skip(addr_store))] +pub async fn route_report_addressbook( + Path((principal, addressbook_id)): Path<(String, String)>, user: User, - req: HttpRequest, - puri: Data, - addr_store: Data, -) -> Result { - let (principal, addressbook_id) = path.into_inner(); + OriginalUri(uri): OriginalUri, + Extension(puri): Extension, + State(AddressbookResourceService { addr_store, .. }): State>, + body: String, +) -> Result { if !user.is_principal(&principal) { return Err(Error::Unauthorized); } @@ -51,8 +54,8 @@ pub async fn route_report_addressbook( handle_addressbook_multiget( addr_multiget, request.props(), - req.path(), - puri.as_ref(), + uri.path(), + &puri, &user, &principal, &addressbook_id, @@ -63,8 +66,8 @@ pub async fn route_report_addressbook( ReportRequest::SyncCollection(sync_collection) => { handle_sync_collection( sync_collection, - req.path(), - puri.as_ref(), + uri.path(), + &puri, &user, &principal, &addressbook_id, diff --git a/crates/carddav/src/addressbook/resource.rs b/crates/carddav/src/addressbook/resource.rs index 04976d9..e247bc1 100644 --- a/crates/carddav/src/addressbook/resource.rs +++ b/crates/carddav/src/addressbook/resource.rs @@ -1,25 +1,27 @@ use super::methods::mkcol::route_mkcol; -use super::methods::post::route_post; use super::methods::report::route_report_addressbook; use super::prop::{SupportedAddressData, SupportedReportSet}; -use crate::address_object::resource::{AddressObjectResource, AddressObjectResourceService}; +use crate::address_object::resource::AddressObjectResource; use crate::{CardDavPrincipalUri, Error}; -use actix_web::http::Method; -use actix_web::web; use async_trait::async_trait; +use axum::extract::Request; +use axum::handler::Handler; +use axum::response::Response; use derive_more::derive::{From, Into}; +use futures_util::future::BoxFuture; use rustical_dav::extensions::{ CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp, }; use rustical_dav::privileges::UserPrivilegeSet; -use rustical_dav::resource::{PrincipalUri, Resource, ResourceService}; +use rustical_dav::resource::{AxumMethods, PrincipalUri, Resource, ResourceService}; use rustical_dav::xml::{Resourcetype, ResourcetypeInner}; use rustical_dav_push::{DavPushExtension, DavPushExtensionProp}; use rustical_store::auth::User; use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore}; use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize}; -use std::str::FromStr; +use std::convert::Infallible; use std::sync::Arc; +use tower::Service; pub struct AddressbookResourceService { pub(crate) addr_store: Arc, @@ -35,6 +37,15 @@ impl AddressbookResourceService } } +impl Clone for AddressbookResourceService { + fn clone(&self) -> Self { + Self { + addr_store: self.addr_store.clone(), + sub_store: self.sub_store.clone(), + } + } +} + #[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[xml(unit_variants_ident = "AddressbookPropName")] pub enum AddressbookProp { @@ -255,18 +266,20 @@ impl ResourceService .await?; Ok(()) } +} - #[inline] - fn actix_scope(self) -> actix_web::Scope { - let mkcol_method = web::method(Method::from_str("MKCOL").unwrap()); - let report_method = web::method(Method::from_str("REPORT").unwrap()); - web::scope("/{addressbook_id}") - .service(AddressObjectResourceService::::new(self.addr_store.clone()).actix_scope()) - .service( - self.actix_resource() - .route(mkcol_method.to(route_mkcol::)) - .route(report_method.to(route_report_addressbook::)) - .post(route_post::), - ) +impl AxumMethods for AddressbookResourceService { + fn report() -> Option BoxFuture<'static, Result>> { + Some(|state, req| { + let mut service = Handler::with_state(route_report_addressbook::, state); + Box::pin(Service::call(&mut service, req)) + }) + } + + fn mkcol() -> Option BoxFuture<'static, Result>> { + Some(|state, req| { + let mut service = Handler::with_state(route_mkcol::, state); + Box::pin(Service::call(&mut service, req)) + }) } } diff --git a/crates/carddav/src/error.rs b/crates/carddav/src/error.rs index 07beb2b..5bc0688 100644 --- a/crates/carddav/src/error.rs +++ b/crates/carddav/src/error.rs @@ -1,4 +1,5 @@ -use actix_web::{HttpResponse, http::StatusCode}; +use axum::response::IntoResponse; +use http::StatusCode; use tracing::error; #[derive(Debug, thiserror::Error)] @@ -28,8 +29,8 @@ pub enum Error { IcalError(#[from] rustical_ical::Error), } -impl actix_web::ResponseError for Error { - fn status_code(&self) -> actix_web::http::StatusCode { +impl Error { + pub fn status_code(&self) -> StatusCode { match self { Error::StoreError(err) => match err { rustical_store::Error::NotFound => StatusCode::NOT_FOUND, @@ -38,8 +39,7 @@ impl actix_web::ResponseError for Error { _ => StatusCode::INTERNAL_SERVER_ERROR, }, Error::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR, - Error::DavError(err) => StatusCode::try_from(err.status_code().as_u16()) - .expect("Just converting between versions"), + Error::DavError(err) => err.status_code(), Error::Unauthorized => StatusCode::UNAUTHORIZED, Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST, Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR, @@ -47,12 +47,10 @@ impl actix_web::ResponseError for Error { Self::IcalError(err) => err.status_code(), } } - fn error_response(&self) -> actix_web::HttpResponse { - error!("Error: {self}"); - match self { - Error::DavError(err) => err.error_response(), - Error::IcalError(err) => err.error_response(), - _ => HttpResponse::build(self.status_code()).body(self.to_string()), - } +} + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + (self.status_code(), self.to_string()).into_response() } } diff --git a/crates/carddav/src/lib.rs b/crates/carddav/src/lib.rs index 2ac6b51..3b7ef89 100644 --- a/crates/carddav/src/lib.rs +++ b/crates/carddav/src/lib.rs @@ -1,22 +1,15 @@ -use actix_web::{ - HttpResponse, - body::BoxBody, - dev::{HttpServiceFactory, ServiceResponse}, - http::{ - Method, StatusCode, - header::{self, HeaderName, HeaderValue}, - }, - middleware::{ErrorHandlerResponse, ErrorHandlers}, - web::Data, -}; +use crate::address_object::resource::AddressObjectResourceService; +use crate::addressbook::resource::AddressbookResourceService; +use axum::{Extension, Router}; use derive_more::Constructor; pub use error::Error; use principal::PrincipalResourceService; use rustical_dav::resource::{PrincipalUri, ResourceService}; use rustical_dav::resources::RootResourceService; +use rustical_store::auth::middleware::AuthenticationLayer; use rustical_store::{ AddressbookStore, SubscriptionStore, - auth::{AuthenticationMiddleware, AuthenticationProvider, User}, + auth::{AuthenticationProvider, User}, }; use std::sync::Arc; @@ -34,51 +27,33 @@ impl PrincipalUri for CardDavPrincipalUri { } } -/// Quite a janky implementation but the default METHOD_NOT_ALLOWED response gives us the allowed -/// methods of a resource -fn options_handler() -> ErrorHandlers { - ErrorHandlers::new().handler(StatusCode::METHOD_NOT_ALLOWED, |res| { - Ok(ErrorHandlerResponse::Response( - if res.request().method() == Method::OPTIONS { - let mut response = HttpResponse::Ok(); - response.insert_header(( - HeaderName::from_static("dav"), - // https://datatracker.ietf.org/doc/html/rfc4918#section-18 - HeaderValue::from_static( - "1, 3, access-control, addressbook, extended-mkcol, webdav-push", - ), - )); - - if let Some(allow) = res.headers().get(header::ALLOW) { - response.insert_header((header::ALLOW, allow.to_owned())); - } - ServiceResponse::new(res.into_parts().0, response.finish()).map_into_right_body() - } else { - res.map_into_left_body() - }, - )) - }) -} - -pub fn carddav_service( +pub fn carddav_router( prefix: &'static str, auth_provider: Arc, store: Arc, subscription_store: Arc, -) -> impl HttpServiceFactory { - RootResourceService::<_, User, CardDavPrincipalUri>::new( - PrincipalResourceService::<_, _, S>::new( - store.clone(), - auth_provider.clone(), - subscription_store.clone(), - ), - ) - .actix_scope() - .wrap(AuthenticationMiddleware::new(auth_provider.clone())) - .wrap(options_handler()) - .app_data(Data::from(store.clone())) - .app_data(Data::new(CardDavPrincipalUri::new( - format!("{prefix}/principal").leak(), - ))) - // TODO: Add endpoint to delete subscriptions +) -> Router { + let principal_service = PrincipalResourceService::new( + store.clone(), + auth_provider.clone(), + subscription_store.clone(), + ); + Router::new() + .route_service( + "/", + RootResourceService::<_, User, CardDavPrincipalUri>::new(principal_service.clone()) + .axum_service(), + ) + .route_service("/principal/{principal}", principal_service.axum_service()) + .route_service( + "/principal/{principal}/{addressbook_id}", + AddressbookResourceService::new(store.clone(), subscription_store.clone()) + .axum_service(), + ) + .route_service( + "/principal/{principal}/{addressbook_id}/{object_id}", + AddressObjectResourceService::new(store.clone()).axum_service(), + ) + .layer(AuthenticationLayer::new(auth_provider)) + .layer(Extension(CardDavPrincipalUri(prefix))) } diff --git a/crates/carddav/src/principal/mod.rs b/crates/carddav/src/principal/mod.rs index fb09d2e..81284bd 100644 --- a/crates/carddav/src/principal/mod.rs +++ b/crates/carddav/src/principal/mod.rs @@ -1,10 +1,9 @@ -use crate::addressbook::resource::{AddressbookResource, AddressbookResourceService}; +use crate::addressbook::resource::AddressbookResource; use crate::{CardDavPrincipalUri, Error}; -use actix_web::web; use async_trait::async_trait; use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp}; use rustical_dav::privileges::UserPrivilegeSet; -use rustical_dav::resource::{PrincipalUri, Resource, ResourceService}; +use rustical_dav::resource::{AxumMethods, PrincipalUri, Resource, ResourceService}; use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner}; use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::{AddressbookStore, SubscriptionStore}; @@ -175,16 +174,9 @@ impl Reso .map(|addressbook| (addressbook.id.to_owned(), addressbook.into())) .collect()) } - - fn actix_scope(self) -> actix_web::Scope { - web::scope("/principal/{principal}") - .service( - AddressbookResourceService::<_, S>::new( - self.addr_store.clone(), - self.sub_store.clone(), - ) - .actix_scope(), - ) - .service(self.actix_resource()) - } +} + +impl AxumMethods + for PrincipalResourceService +{ } diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 675ba40..6264094 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -7,15 +7,10 @@ repository.workspace = true publish = false [features] -actix = ["dep:actix-web", "dep:tracing-actix-web", "dep:http_02"] -axum = ["dep:axum", "dep:axum-extra", "dep:tower"] - [dependencies] -axum = { version = "0.8", optional = true } -axum-extra = { version = "0.10", optional = true, features = ["typed-header"] } -tower = { version = "0.5", optional = true } - -http_02 = { workspace = true, optional = true } +axum = { version = "0.8" } +tower = { version = "0.5" } +axum-extra.workspace = true rustical_xml.workspace = true async-trait.workspace = true @@ -29,6 +24,4 @@ derive_more.workspace = true tracing.workspace = true tokio.workspace = true http.workspace = true -actix-web = { workspace = true, optional = true } -tracing-actix-web = { workspace = true, optional = true } headers.workspace = true diff --git a/crates/dav/src/error.rs b/crates/dav/src/error.rs index e1d7b11..7bda3b4 100644 --- a/crates/dav/src/error.rs +++ b/crates/dav/src/error.rs @@ -53,30 +53,6 @@ impl Error { } } -#[cfg(feature = "actix")] -impl actix_web::error::ResponseError for Error { - fn status_code(&self) -> actix_web::http::StatusCode { - self.status_code() - .as_u16() - .try_into() - .expect("Just converting between versions") - } - - fn error_response(&self) -> actix_web::HttpResponse { - use actix_web::ResponseError; - - error!("Error: {self}"); - match self { - Error::Unauthorized => actix_web::HttpResponse::build(ResponseError::status_code(self)) - .append_header(("WWW-Authenticate", "Basic")) - .body(self.to_string()), - _ => actix_web::HttpResponse::build(ResponseError::status_code(self)) - .body(self.to_string()), - } - } -} - -#[cfg(feature = "axum")] impl axum::response::IntoResponse for Error { fn into_response(self) -> axum::response::Response { use axum::body::Body; diff --git a/crates/dav/src/header/depth.rs b/crates/dav/src/header/depth.rs index 4b47ede..3ece458 100644 --- a/crates/dav/src/header/depth.rs +++ b/crates/dav/src/header/depth.rs @@ -1,8 +1,4 @@ -#[cfg(feature = "actix")] -use actix_web::{HttpRequest, ResponseError}; -#[cfg(feature = "axum")] use axum::{body::Body, extract::FromRequestParts, response::IntoResponse}; -use futures_util::future::{Ready, err, ok}; use rustical_xml::{ValueDeserialize, ValueSerialize, XmlError}; use thiserror::Error; @@ -10,14 +6,6 @@ use thiserror::Error; #[error("Invalid Depth header")] pub struct InvalidDepthHeader; -#[cfg(feature = "actix")] -impl ResponseError for InvalidDepthHeader { - fn status_code(&self) -> actix_web::http::StatusCode { - http_02::StatusCode::BAD_REQUEST - } -} - -#[cfg(feature = "axum")] impl IntoResponse for InvalidDepthHeader { fn into_response(self) -> axum::response::Response { axum::response::Response::builder() @@ -71,35 +59,12 @@ impl TryFrom<&[u8]> for Depth { } } -#[cfg(feature = "actix")] -impl actix_web::FromRequest for Depth { - type Error = InvalidDepthHeader; - type Future = Ready>; - - fn extract(req: &HttpRequest) -> Self::Future { - if let Some(depth_header) = req.headers().get("Depth") { - match depth_header.as_bytes().try_into() { - Ok(depth) => ok(depth), - Err(e) => err(e), - } - } else { - // default depth - ok(Depth::Zero) - } - } - - fn from_request(req: &HttpRequest, _payload: &mut actix_web::dev::Payload) -> Self::Future { - Self::extract(req) - } -} - -#[cfg(feature = "axum")] impl FromRequestParts for Depth { type Rejection = InvalidDepthHeader; async fn from_request_parts( parts: &mut axum::http::request::Parts, - state: &S, + _state: &S, ) -> Result { if let Some(depth_header) = parts.headers.get("Depth") { depth_header.as_bytes().try_into() diff --git a/crates/dav/src/header/overwrite.rs b/crates/dav/src/header/overwrite.rs index c9d52ad..2aade9d 100644 --- a/crates/dav/src/header/overwrite.rs +++ b/crates/dav/src/header/overwrite.rs @@ -1,19 +1,9 @@ -#[cfg(feature = "actix")] -use actix_web::{FromRequest, HttpRequest, ResponseError, http::StatusCode}; -use futures_util::future::{Ready, err, ok}; use thiserror::Error; #[derive(Error, Debug)] #[error("Invalid Overwrite header")] pub struct InvalidOverwriteHeader; -#[cfg(feature = "actix")] -impl ResponseError for InvalidOverwriteHeader { - fn status_code(&self) -> actix_web::http::StatusCode { - StatusCode::BAD_REQUEST - } -} - #[derive(Debug, PartialEq, Default)] pub enum Overwrite { #[default] @@ -38,25 +28,3 @@ impl TryFrom<&[u8]> for Overwrite { } } } - -#[cfg(feature = "actix")] -impl FromRequest for Overwrite { - type Error = InvalidOverwriteHeader; - type Future = Ready>; - - fn extract(req: &HttpRequest) -> Self::Future { - if let Some(overwrite_header) = req.headers().get("Overwrite") { - match overwrite_header.as_bytes().try_into() { - Ok(depth) => ok(depth), - Err(e) => err(e), - } - } else { - // default depth - ok(Overwrite::F) - } - } - - fn from_request(req: &HttpRequest, _payload: &mut actix_web::dev::Payload) -> Self::Future { - Self::extract(req) - } -} diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index b40fc83..e23d682 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -6,7 +6,6 @@ pub mod privileges; pub mod resource; pub mod resources; pub mod xml; - pub use error::Error; pub trait Principal: std::fmt::Debug + Clone + Send + Sync + 'static { diff --git a/crates/dav/src/resource/axum_methods.rs b/crates/dav/src/resource/axum_methods.rs index b9c3883..0a9c6d3 100644 --- a/crates/dav/src/resource/axum_methods.rs +++ b/crates/dav/src/resource/axum_methods.rs @@ -2,10 +2,10 @@ use axum::body::Body; use futures_util::future::BoxFuture; use headers::Allow; use http::{Method, Request, Response}; -use std::{convert::Infallible, str::FromStr, sync::Arc}; +use std::{convert::Infallible, str::FromStr}; pub type MethodFunction = - fn(Arc, Request) -> BoxFuture<'static, Result, Infallible>>; + fn(Service, Request) -> BoxFuture<'static, Result, Infallible>>; pub trait AxumMethods: Sized + Send + Sync + 'static { #[inline] diff --git a/crates/dav/src/resource/axum_service.rs b/crates/dav/src/resource/axum_service.rs index a361778..286f285 100644 --- a/crates/dav/src/resource/axum_service.rs +++ b/crates/dav/src/resource/axum_service.rs @@ -2,6 +2,7 @@ use super::methods::{axum_route_propfind, axum_route_proppatch}; use crate::resource::{ResourceService, axum_methods::AxumMethods}; use axum::{ body::Body, + extract::FromRequestParts, handler::Handler, http::{Request, Response}, response::IntoResponse, @@ -9,16 +10,16 @@ use axum::{ use futures_util::future::BoxFuture; use headers::HeaderMapExt; use http::{HeaderValue, StatusCode}; -use std::{convert::Infallible, sync::Arc}; +use std::convert::Infallible; use tower::Service; #[derive(Clone)] pub struct AxumService { - resource_service: Arc, + resource_service: RS, } impl AxumService { - pub fn new(resource_service: Arc) -> Self { + pub fn new(resource_service: RS) -> Self { Self { resource_service } } } @@ -27,6 +28,7 @@ impl Service where RS::Error: IntoResponse + Send + Sync + 'static, + RS::Principal: FromRequestParts, { type Error = Infallible; type Response = Response; diff --git a/crates/dav/src/resource/methods/delete.rs b/crates/dav/src/resource/methods/delete.rs index b743bf6..7f31262 100644 --- a/crates/dav/src/resource/methods/delete.rs +++ b/crates/dav/src/resource/methods/delete.rs @@ -2,77 +2,15 @@ use crate::Error; use crate::privileges::UserPrivilege; use crate::resource::Resource; use crate::resource::ResourceService; -#[cfg(feature = "axum")] -use axum::extract::{Extension, Path, State}; -#[cfg(feature = "axum")] +use axum::extract::{Path, State}; use axum_extra::TypedHeader; -use headers::Header; -use headers::{HeaderValue, IfMatch, IfNoneMatch}; -#[cfg(feature = "axum")] +use headers::{IfMatch, IfNoneMatch}; use http::HeaderMap; -use itertools::Itertools; -#[cfg(feature = "axum")] -use std::sync::Arc; -use tracing::instrument; -#[cfg(feature = "actix")] -#[instrument(parent = root_span.id(), skip(path, req, root_span, resource_service))] -pub async fn actix_route_delete( - path: actix_web::web::Path, - req: actix_web::HttpRequest, - principal: R::Principal, - resource_service: actix_web::web::Data, - root_span: tracing_actix_web::RootSpan, -) -> Result { - let no_trash = req - .headers() - .get("X-No-Trashbin") - .map(|val| matches!(val.to_str(), Ok("1"))) - .unwrap_or(false); - - // This weird conversion stuff is because we want to use the headers library (to be - // framework-agnostic in the future) which uses http==1.0, - // while actix-web still uses http==0.2 - let if_match = req - .headers() - .get_all(http_02::header::IF_MATCH) - .map(|val_02| HeaderValue::from_bytes(val_02.as_bytes()).unwrap()) - .collect_vec(); - let if_none_match = req - .headers() - .get_all(http_02::header::IF_NONE_MATCH) - .map(|val_02| HeaderValue::from_bytes(val_02.as_bytes()).unwrap()) - .collect_vec(); - - let if_match = if if_match.is_empty() { - None - } else { - Some(IfMatch::decode(&mut if_match.iter()).unwrap()) - }; - let if_none_match = if if_none_match.is_empty() { - None - } else { - Some(IfNoneMatch::decode(&mut if_none_match.iter()).unwrap()) - }; - - route_delete( - &path.into_inner(), - &principal, - resource_service.as_ref(), - no_trash, - if_match, - if_none_match, - ) - .await?; - - Ok(actix_web::HttpResponse::Ok().body("")) -} - -#[cfg(feature = "axum")] pub(crate) async fn axum_route_delete( Path(path): Path, - State(resource_service): State>, - Extension(principal): Extension, + State(resource_service): State, + principal: R::Principal, if_match: Option>, if_none_match: Option>, header_map: HeaderMap, @@ -84,7 +22,7 @@ pub(crate) async fn axum_route_delete( route_delete( &path, &principal, - resource_service.as_ref(), + &resource_service, no_trash, if_match.map(|hdr| hdr.0), if_none_match.map(|hdr| hdr.0), diff --git a/crates/dav/src/resource/methods/mod.rs b/crates/dav/src/resource/methods/mod.rs index 9a45420..84eec52 100644 --- a/crates/dav/src/resource/methods/mod.rs +++ b/crates/dav/src/resource/methods/mod.rs @@ -2,17 +2,6 @@ mod delete; mod propfind; mod proppatch; -#[cfg(feature = "actix")] -pub(crate) use delete::actix_route_delete; -#[cfg(feature = "axum")] pub(crate) use delete::axum_route_delete; - -#[cfg(feature = "actix")] -pub(crate) use propfind::actix_route_propfind; -#[cfg(feature = "axum")] pub(crate) use propfind::axum_route_propfind; - -#[cfg(feature = "actix")] -pub(crate) use proppatch::actix_route_proppatch; -#[cfg(feature = "axum")] pub(crate) use proppatch::axum_route_proppatch; diff --git a/crates/dav/src/resource/methods/propfind.rs b/crates/dav/src/resource/methods/propfind.rs index cbc1ad0..d34575e 100644 --- a/crates/dav/src/resource/methods/propfind.rs +++ b/crates/dav/src/resource/methods/propfind.rs @@ -7,47 +7,15 @@ use crate::resource::ResourceService; use crate::xml::MultistatusElement; use crate::xml::PropfindElement; use crate::xml::PropfindType; -#[cfg(feature = "axum")] use axum::extract::{Extension, OriginalUri, Path, State}; use rustical_xml::PropName; use rustical_xml::XmlDocument; -use std::sync::Arc; -use tracing::instrument; -#[cfg(feature = "actix")] -#[instrument(parent = root_span.id(), skip(path, req, root_span, resource_service, puri))] -#[allow(clippy::type_complexity)] -pub(crate) async fn actix_route_propfind( - path: ::actix_web::web::Path, - body: String, - req: ::actix_web::HttpRequest, - user: R::Principal, - depth: Depth, - root_span: tracing_actix_web::RootSpan, - resource_service: ::actix_web::web::Data, - puri: ::actix_web::web::Data, -) -> Result< - MultistatusElement<::Prop, ::Prop>, - R::Error, -> { - route_propfind( - &path.into_inner(), - req.path(), - &body, - &user, - &depth, - resource_service.as_ref(), - puri.as_ref(), - ) - .await -} - -#[cfg(feature = "axum")] pub(crate) async fn axum_route_propfind( Path(path): Path, - State(resource_service): State>, + State(resource_service): State, depth: Depth, - Extension(principal): Extension, + principal: R::Principal, uri: OriginalUri, Extension(puri): Extension, body: String, @@ -61,7 +29,7 @@ pub(crate) async fn axum_route_propfind( &body, &principal, &depth, - resource_service.as_ref(), + &resource_service, &puri, ) .await @@ -115,7 +83,7 @@ pub(crate) async fn route_propfind( } } - let response = resource.propfind_typed(path, &propfind_self.prop, puri, &principal)?; + let response = resource.propfind_typed(path, &propfind_self.prop, puri, principal)?; Ok(MultistatusElement { responses: vec![response], diff --git a/crates/dav/src/resource/methods/proppatch.rs b/crates/dav/src/resource/methods/proppatch.rs index c3935c0..d06ab26 100644 --- a/crates/dav/src/resource/methods/proppatch.rs +++ b/crates/dav/src/resource/methods/proppatch.rs @@ -1,13 +1,11 @@ use crate::Error; use crate::privileges::UserPrivilege; -use std::sync::Arc; use crate::resource::Resource; use crate::resource::ResourceService; -#[cfg(feature = "axum")] -use axum::extract::{Extension, OriginalUri, Path, State}; use crate::xml::MultistatusElement; use crate::xml::TagList; use crate::xml::multistatus::{PropstatElement, PropstatWrapper, ResponseElement}; +use axum::extract::{OriginalUri, Path, State}; use http::StatusCode; use quick_xml::name::Namespace; use rustical_xml::NamespaceOwned; @@ -17,7 +15,6 @@ use rustical_xml::XmlDeserialize; use rustical_xml::XmlDocument; use rustical_xml::XmlRootTag; use std::str::FromStr; -use tracing::instrument; #[derive(XmlDeserialize, Clone, Debug)] #[xml(untagged)] @@ -64,46 +61,14 @@ enum Operation { #[xml(ns = "crate::namespace::NS_DAV")] struct PropertyupdateElement(#[xml(ty = "untagged", flatten)] Vec>); -#[cfg(feature = "actix")] -#[instrument(parent = root_span.id(), skip(path, req, root_span, resource_service))] -pub(crate) async fn actix_route_proppatch( - path: actix_web::web::Path, - body: String, - req: actix_web::HttpRequest, - principal: R::Principal, - root_span: tracing_actix_web::RootSpan, - resource_service: actix_web::web::Data, -) -> Result, R::Error> { - route_proppatch( - &path.into_inner(), - req.path(), - &body, - &principal, - resource_service.as_ref(), - ) - .await -} - - -#[cfg(feature = "axum")] pub(crate) async fn axum_route_proppatch( Path(path): Path, - State(resource_service): State>, - Extension(principal): Extension, + State(resource_service): State, + principal: R::Principal, uri: OriginalUri, body: String, -) -> Result< - MultistatusElement, - R::Error, -> { - route_proppatch( - &path, - uri.path(), - &body, - &principal, - resource_service.as_ref(), - ) - .await +) -> Result, R::Error> { + route_proppatch(&path, uri.path(), &body, &principal, &resource_service).await } pub(crate) async fn route_proppatch( @@ -118,10 +83,10 @@ pub(crate) async fn route_proppatch( // Extract operations let PropertyupdateElement::::Prop>>( operations, - ) = XmlDocument::parse_str(&body).map_err(Error::XmlError)?; + ) = XmlDocument::parse_str(body).map_err(Error::XmlError)?; let mut resource = resource_service.get_resource(path_components).await?; - let privileges = resource.get_user_privileges(&principal)?; + let privileges = resource.get_user_privileges(principal)?; if !privileges.has(&UserPrivilege::Write) { return Err(Error::Unauthorized.into()); } diff --git a/crates/dav/src/resource/mod.rs b/crates/dav/src/resource/mod.rs index 2773e6f..676939f 100644 --- a/crates/dav/src/resource/mod.rs +++ b/crates/dav/src/resource/mod.rs @@ -12,17 +12,13 @@ use rustical_xml::{EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlSe use std::collections::HashSet; use std::str::FromStr; -#[cfg(feature = "axum")] mod axum_methods; -#[cfg(feature = "axum")] mod axum_service; mod methods; mod principal_uri; mod resource_service; -#[cfg(feature = "axum")] pub use axum_methods::AxumMethods; -#[cfg(feature = "axum")] pub use axum_service::AxumService; pub use principal_uri::PrincipalUri; diff --git a/crates/dav/src/resource/resource_service.rs b/crates/dav/src/resource/resource_service.rs index 45ea74f..2766248 100644 --- a/crates/dav/src/resource/resource_service.rs +++ b/crates/dav/src/resource/resource_service.rs @@ -1,14 +1,8 @@ -#[cfg(feature = "actix")] -use super::methods::{actix_route_delete, actix_route_propfind, actix_route_proppatch}; use super::{PrincipalUri, Resource}; use crate::Principal; -#[cfg(feature = "axum")] use crate::resource::{AxumMethods, AxumService}; -#[cfg(feature = "actix")] -use actix_web::{http::Method, web, web::Data}; use async_trait::async_trait; use serde::Deserialize; -use std::{str::FromStr, sync::Arc}; #[async_trait] pub trait ResourceService: Sized + Send + Sync + 'static { @@ -28,11 +22,6 @@ pub trait ResourceService: Sized + Send + Sync + 'static { Ok(vec![]) } - async fn test_get_members(&self, _path: &Self::PathComponents) -> Result { - // ) -> Result, Self::Error> { - Ok("asd".to_string()) - } - async fn get_resource( &self, _path: &Self::PathComponents, @@ -54,36 +43,10 @@ pub trait ResourceService: Sized + Send + Sync + 'static { Err(crate::Error::Unauthorized.into()) } - #[cfg(feature = "actix")] - #[inline] - fn actix_resource(self) -> actix_web::Resource - where - Self::Error: actix_web::ResponseError, - Self::Principal: actix_web::FromRequest, - { - web::resource("") - .app_data(Data::new(self)) - .route( - web::method(Method::from_str("PROPFIND").unwrap()).to(actix_route_propfind::), - ) - .route( - web::method(Method::from_str("PROPPATCH").unwrap()) - .to(actix_route_proppatch::), - ) - .delete(actix_route_delete::) - } - - #[cfg(feature = "actix")] - fn actix_scope(self) -> actix_web::Scope - where - Self::Error: actix_web::ResponseError, - Self::Principal: actix_web::FromRequest; - - #[cfg(feature = "axum")] fn axum_service(self) -> AxumService where Self: Clone + Send + Sync + AxumMethods, { - AxumService::new(Arc::new(self)) + AxumService::new(self) } } diff --git a/crates/dav/src/resources/root.rs b/crates/dav/src/resources/root.rs index d900c4e..7c13c5b 100644 --- a/crates/dav/src/resources/root.rs +++ b/crates/dav/src/resources/root.rs @@ -3,7 +3,7 @@ use crate::extensions::{ CommonPropertiesExtension, CommonPropertiesProp, CommonPropertiesPropName, }; use crate::privileges::UserPrivilegeSet; -use crate::resource::{PrincipalUri, Resource, ResourceService}; +use crate::resource::{AxumMethods, PrincipalUri, Resource, ResourceService}; use crate::xml::{Resourcetype, ResourcetypeInner}; use async_trait::async_trait; use std::marker::PhantomData; @@ -74,15 +74,9 @@ impl + Clone, P: Principal, PURI: PrincipalU async fn get_resource(&self, _: &()) -> Result { Ok(RootResource::::default()) } - - #[cfg(feature = "actix")] - fn actix_scope(self) -> actix_web::Scope - where - Self::Error: actix_web::ResponseError, - Self::Principal: actix_web::FromRequest, - { - actix_web::web::scope("") - .service(self.0.clone().actix_scope()) - .service(self.actix_resource()) - } +} + +impl + Clone, P: Principal, PURI: PrincipalUri> AxumMethods + for RootResourceService +{ } diff --git a/crates/dav/src/xml/multistatus.rs b/crates/dav/src/xml/multistatus.rs index 665b455..ecaf5a5 100644 --- a/crates/dav/src/xml/multistatus.rs +++ b/crates/dav/src/xml/multistatus.rs @@ -1,8 +1,4 @@ use crate::xml::TagList; -#[cfg(feature = "actix")] -use actix_web::{ - HttpRequest, HttpResponse, Responder, ResponseError, body::BoxBody, http::header::ContentType, -}; use http::StatusCode; use quick_xml::name::Namespace; use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot}; @@ -108,24 +104,6 @@ impl Default for MultistatusElement } } -#[cfg(feature = "actix")] -impl Responder for MultistatusElement { - type Body = BoxBody; - - fn respond_to(self, _req: &HttpRequest) -> HttpResponse { - let mut output: Vec<_> = b"\n".into(); - let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4); - if let Err(err) = self.serialize_root(&mut writer) { - return crate::Error::from(err).error_response(); - } - - HttpResponse::MultiStatus() - .content_type(ContentType::xml()) - .body(String::from_utf8(output).unwrap()) - } -} - -#[cfg(feature = "axum")] impl axum::response::IntoResponse for MultistatusElement { diff --git a/crates/dav_push/Cargo.toml b/crates/dav_push/Cargo.toml index 31eb391..ff6983a 100644 --- a/crates/dav_push/Cargo.toml +++ b/crates/dav_push/Cargo.toml @@ -8,7 +8,6 @@ publish = false [dependencies] rustical_xml.workspace = true -actix-web = { workspace = true } async-trait = { workspace = true } futures-util = { workspace = true } quick-xml = { workspace = true } @@ -18,7 +17,6 @@ itertools = { workspace = true } log = { workspace = true } derive_more = { workspace = true } tracing = { workspace = true } -tracing-actix-web = { workspace = true } reqwest.workspace = true tokio.workspace = true rustical_dav.workspace = true diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index c86b188..845919e 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -7,14 +7,15 @@ repository.workspace = true publish = false [dependencies] +tower.workspace = true +http.workspace = true +axum.workspace = true askama.workspace = true -async-trait.workspace = true askama_web.workspace = true -actix-session.workspace = true +async-trait.workspace = true serde.workspace = true thiserror.workspace = true tokio.workspace = true -actix-web.workspace = true rustical_store.workspace = true rust-embed.workspace = true futures-core.workspace = true @@ -27,3 +28,5 @@ uuid.workspace = true url.workspace = true tracing.workspace = true rustical_oidc.workspace = true +axum-extra.workspace= true +headers.workspace = true diff --git a/crates/frontend/src/assets.rs b/crates/frontend/src/assets.rs index 6e99886..fe31e01 100644 --- a/crates/frontend/src/assets.rs +++ b/crates/frontend/src/assets.rs @@ -1,97 +1,72 @@ -use std::marker::PhantomData; - -use actix_web::{ - body::BoxBody, - dev::{ - HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest, ServiceResponse, - }, - http::{header, Method}, - HttpResponse, +use axum::{ + RequestExt, + body::Body, + extract::{Path, Request}, + response::{IntoResponse, Response}, }; -use futures_core::future::LocalBoxFuture; +use futures_core::future::BoxFuture; +use headers::{ContentType, ETag, HeaderMapExt}; +use http::{Method, StatusCode}; use rust_embed::RustEmbed; +use std::{convert::Infallible, marker::PhantomData, str::FromStr}; +use tower::Service; -#[derive(RustEmbed)] +#[derive(Clone, RustEmbed)] #[folder = "public/assets"] pub struct Assets; +#[derive(Clone)] pub struct EmbedService where E: 'static + RustEmbed, { _embed: PhantomData, - prefix: String, } impl EmbedService where E: 'static + RustEmbed, { - pub fn new(prefix: String) -> Self { + pub fn new() -> Self { Self { - prefix, _embed: PhantomData, } } } -impl HttpServiceFactory for EmbedService +impl Service for EmbedService where E: 'static + RustEmbed, { - fn register(self, config: &mut actix_web::dev::AppService) { - let resource_def = if config.is_root() { - ResourceDef::root_prefix(&self.prefix) - } else { - ResourceDef::prefix(&self.prefix) - }; - config.register_service(resource_def, None, self, None); + type Response = Response; + type Error = Infallible; + type Future = BoxFuture<'static, Result>; + + #[inline] + fn poll_ready( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Ok(()).into() } -} -impl ServiceFactory for EmbedService -where - E: 'static + RustEmbed, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Config = (); - type Service = EmbedService; - type InitError = (); - type Future = LocalBoxFuture<'static, Result>; - - fn new_service(&self, _: ()) -> Self::Future { - let prefix = self.prefix.clone(); - Box::pin(async move { - Ok(Self { - prefix, - _embed: PhantomData, - }) - }) - } -} - -impl Service for EmbedService -where - E: 'static + RustEmbed, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Future = LocalBoxFuture<'static, Result>; - - actix_web::dev::always_ready!(); - - fn call(&self, req: ServiceRequest) -> Self::Future { + #[inline] + fn call(&mut self, mut req: Request) -> Self::Future { Box::pin(async move { if req.method() != Method::GET && req.method() != Method::HEAD { - return Ok(req.into_response(HttpResponse::MethodNotAllowed())); + return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response()); } - let path = req.match_info().unprocessed().trim_start_matches('/'); + let path: String = if let Ok(Path(path)) = req.extract_parts().await.unwrap() { + path + } else { + return Ok(StatusCode::NOT_FOUND.into_response()); + }; - match E::get(path) { + match E::get(&path) { Some(file) => { let data = file.data; let hash = hex::encode(file.metadata.sha256_hash()); + let etag = format!("\"{hash}\""); let mime = mime_guess::from_path(path).first_or_octet_stream(); let body = if req.method() == Method::HEAD { @@ -99,14 +74,13 @@ where } else { data }; - Ok(req.into_response( - HttpResponse::Ok() - .content_type(mime) - .insert_header((header::ETAG, hash)) - .body(body), - )) + let mut res = Response::builder().status(StatusCode::OK); + let hdrs = res.headers_mut().unwrap(); + hdrs.typed_insert(ContentType::from(mime)); + hdrs.typed_insert(ETag::from_str(&etag).unwrap()); + Ok(res.body(Body::from(body)).unwrap()) } - None => Ok(req.into_response(HttpResponse::NotFound())), + None => Ok(StatusCode::NOT_FOUND.into_response()), } }) } diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index 8678f8b..04ea169 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -1,30 +1,23 @@ -use actix_session::{ - SessionMiddleware, - config::CookieContentSecurity, - storage::{CookieSessionStore, SessionStore}, -}; -use actix_web::{ - HttpRequest, HttpResponse, Responder, - cookie::{Key, SameSite}, - dev::ServiceResponse, - http::{Method, StatusCode, header}, - middleware::{ErrorHandlerResponse, ErrorHandlers}, - web::{self, Data, Form, Path, Redirect}, -}; use askama::Template; -use askama_web::WebTemplate; -use assets::{Assets, EmbedService}; +// use askama_web::WebTemplate; +// use assets::{Assets, EmbedService}; use async_trait::async_trait; +use axum::{Extension, Router, response::IntoResponse, routing::get}; +use http::Uri; use rand::{Rng, distributions::Alphanumeric}; -use routes::{ - addressbook::{route_addressbook, route_addressbook_restore}, - calendar::{route_calendar, route_calendar_restore}, - login::{route_get_login, route_post_login, route_post_logout}, -}; -use rustical_oidc::{OidcConfig, OidcServiceConfig, UserStore, configure_oidc}; +use rustical_oidc::OidcConfig; +// use routes::{ +// addressbook::{route_addressbook, route_addressbook_restore}, +// calendar::{route_calendar, route_calendar_restore}, +// login::{route_get_login, route_post_login, route_post_logout}, +// }; +// use rustical_oidc::{OidcConfig, OidcServiceConfig, UserStore, configure_oidc}; use rustical_store::{ Addressbook, AddressbookStore, Calendar, CalendarStore, - auth::{AuthenticationMiddleware, AuthenticationProvider, User, user::AppToken}, + auth::{ + AuthenticationMiddleware, AuthenticationProvider, User, middleware::AuthenticationLayer, + user::AppToken, + }, }; use serde::Deserialize; use std::sync::Arc; @@ -40,6 +33,11 @@ pub const ROUTE_USER_NAMED: &str = "frontend_user_named"; pub use config::{FrontendConfig, generate_frontend_secret}; +use crate::{ + assets::{Assets, EmbedService}, + routes::login::{route_get_login, route_post_login}, +}; + pub fn generate_app_token() -> String { rand::thread_rng() .sample_iter(Alphanumeric) @@ -48,313 +46,312 @@ pub fn generate_app_token() -> String { .collect() } -#[derive(Template, WebTemplate)] -#[template(path = "pages/user.html")] -struct UserPage { - pub user: User, - pub app_tokens: Vec, - pub calendars: Vec, - pub deleted_calendars: Vec, - pub addressbooks: Vec, - pub deleted_addressbooks: Vec, - pub is_apple: bool, -} +// #[derive(Template, WebTemplate)] +// #[template(path = "pages/user.html")] +// struct UserPage { +// pub user: User, +// pub app_tokens: Vec, +// pub calendars: Vec, +// pub deleted_calendars: Vec, +// pub addressbooks: Vec, +// pub deleted_addressbooks: Vec, +// pub is_apple: bool, +// } +// +// async fn route_user_named( +// path: Path, +// cal_store: Data, +// addr_store: Data, +// auth_provider: Data, +// user: User, +// req: HttpRequest, +// ) -> impl Responder { +// let user_id = path.into_inner(); +// if user_id != user.id { +// return actix_web::HttpResponse::Unauthorized().body("Unauthorized"); +// } +// +// let mut calendars = vec![]; +// for group in user.memberships() { +// calendars.extend(cal_store.get_calendars(group).await.unwrap()); +// } +// +// let mut deleted_calendars = vec![]; +// for group in user.memberships() { +// deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap()); +// } +// +// let mut addressbooks = vec![]; +// for group in user.memberships() { +// addressbooks.extend(addr_store.get_addressbooks(group).await.unwrap()); +// } +// +// let mut deleted_addressbooks = vec![]; +// for group in user.memberships() { +// deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap()); +// } +// +// let is_apple = req +// .headers() +// .get(header::USER_AGENT) +// .and_then(|user_agent| user_agent.to_str().ok()) +// .map(|ua| ua.contains("Apple") || ua.contains("Mac OS")) +// .unwrap_or_default(); +// +// UserPage { +// app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(), +// calendars, +// deleted_calendars, +// addressbooks, +// deleted_addressbooks, +// user, +// is_apple, +// } +// .respond_to(&req) +// } +// +// async fn route_get_home(user: User, req: HttpRequest) -> Redirect { +// Redirect::to( +// req.url_for(ROUTE_USER_NAMED, &[user.id]) +// .unwrap() +// .to_string(), +// ) +// .see_other() +// } +// +// async fn route_root(user: Option, req: HttpRequest) -> impl Responder { +// let redirect_url = match user { +// Some(_) => req.url_for_static(ROUTE_NAME_HOME).unwrap(), +// None => req +// .resource_map() +// .url_for::<[_; 0], String>(&req, "frontend_login", []) +// .unwrap(), +// }; +// web::Redirect::to(redirect_url.to_string()).permanent() +// } +// +// #[derive(Template)] +// #[template(path = "apple_configuration/template.xml")] +// pub struct AppleConfig { +// token_name: String, +// account_description: String, +// hostname: String, +// caldav_principal_url: String, +// carddav_principal_url: String, +// user: String, +// token: String, +// caldav_profile_uuid: Uuid, +// carddav_profile_uuid: Uuid, +// plist_uuid: Uuid, +// } +// +// #[derive(Debug, Clone, Deserialize)] +// pub(crate) struct PostAppTokenForm { +// name: String, +// #[serde(default)] +// apple: bool, +// } +// +// async fn route_post_app_token( +// user: User, +// auth_provider: Data, +// path: Path, +// Form(PostAppTokenForm { apple, name }): Form, +// req: HttpRequest, +// ) -> Result { +// assert!(!name.is_empty()); +// assert_eq!(path.into_inner(), user.id); +// let token = generate_app_token(); +// auth_provider +// .add_app_token(&user.id, name.to_owned(), token.clone()) +// .await?; +// if apple { +// let hostname = req.full_url().host_str().unwrap().to_owned(); +// let profile = AppleConfig { +// token_name: name, +// account_description: format!("{}@{}", &user.id, &hostname), +// hostname, +// caldav_principal_url: req +// .url_for("caldav_principal", [&user.id]) +// .unwrap() +// .to_string(), +// carddav_principal_url: req +// .url_for("carddav_principal", [&user.id]) +// .unwrap() +// .to_string(), +// user: user.id.to_owned(), +// token, +// caldav_profile_uuid: Uuid::new_v4(), +// carddav_profile_uuid: Uuid::new_v4(), +// plist_uuid: Uuid::new_v4(), +// } +// .render() +// .unwrap(); +// Ok(HttpResponse::Ok() +// .insert_header(header::ContentDisposition::attachment(format!( +// "rustical-{}.mobileconfig", +// user.id +// ))) +// .insert_header(( +// header::CONTENT_TYPE, +// "application/x-apple-aspen-config; charset=utf-8", +// )) +// .body(profile)) +// } else { +// Ok(HttpResponse::Ok().body(token)) +// } +// } +// +// async fn route_delete_app_token( +// user: User, +// auth_provider: Data, +// path: Path<(String, String)>, +// ) -> Result { +// let (path_user, token_id) = path.into_inner(); +// assert_eq!(path_user, user.id); +// auth_provider.remove_app_token(&user.id, &token_id).await?; +// Ok(Redirect::to("/frontend/user").see_other()) +// } +// +// pub(crate) fn unauthorized_handler( +// res: ServiceResponse, +// ) -> actix_web::Result> { +// let (req, _) = res.into_parts(); +// let redirect_uri = req.uri().to_string(); +// let mut login_url = req.url_for_static("frontend_login").unwrap(); +// login_url +// .query_pairs_mut() +// .append_pair("redirect_uri", &redirect_uri); +// let login_url = login_url.to_string(); +// +// let response = HttpResponse::Unauthorized().body(format!( +// r#" +// +// +// +// +// +// Unauthorized, redirecting to login page +// +// +// "# +// )); +// +// let res = ServiceResponse::new(req, response) +// .map_into_boxed_body() +// .map_into_right_body(); +// +// Ok(ErrorHandlerResponse::Response(res)) +// } +// +// pub fn session_middleware(frontend_secret: [u8; 64]) -> SessionMiddleware { +// SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&frontend_secret)) +// .cookie_secure(true) +// .cookie_same_site(SameSite::Strict) +// .cookie_content_security(CookieContentSecurity::Private) +// .build() +// } -async fn route_user_named( - path: Path, - cal_store: Data, - addr_store: Data, - auth_provider: Data, - user: User, - req: HttpRequest, -) -> impl Responder { - let user_id = path.into_inner(); - if user_id != user.id { - return actix_web::HttpResponse::Unauthorized().body("Unauthorized"); - } - - let mut calendars = vec![]; - for group in user.memberships() { - calendars.extend(cal_store.get_calendars(group).await.unwrap()); - } - - let mut deleted_calendars = vec![]; - for group in user.memberships() { - deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap()); - } - - let mut addressbooks = vec![]; - for group in user.memberships() { - addressbooks.extend(addr_store.get_addressbooks(group).await.unwrap()); - } - - let mut deleted_addressbooks = vec![]; - for group in user.memberships() { - deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap()); - } - - let is_apple = req - .headers() - .get(header::USER_AGENT) - .and_then(|user_agent| user_agent.to_str().ok()) - .map(|ua| ua.contains("Apple") || ua.contains("Mac OS")) - .unwrap_or_default(); - - UserPage { - app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(), - calendars, - deleted_calendars, - addressbooks, - deleted_addressbooks, - user, - is_apple, - } - .respond_to(&req) -} - -async fn route_get_home(user: User, req: HttpRequest) -> Redirect { - Redirect::to( - req.url_for(ROUTE_USER_NAMED, &[user.id]) - .unwrap() - .to_string(), - ) - .see_other() -} - -async fn route_root(user: Option, req: HttpRequest) -> impl Responder { - let redirect_url = match user { - Some(_) => req.url_for_static(ROUTE_NAME_HOME).unwrap(), - None => req - .resource_map() - .url_for::<[_; 0], String>(&req, "frontend_login", []) - .unwrap(), - }; - web::Redirect::to(redirect_url.to_string()).permanent() -} - -#[derive(Template)] -#[template(path = "apple_configuration/template.xml")] -pub struct AppleConfig { - token_name: String, - account_description: String, - hostname: String, - caldav_principal_url: String, - carddav_principal_url: String, - user: String, - token: String, - caldav_profile_uuid: Uuid, - carddav_profile_uuid: Uuid, - plist_uuid: Uuid, -} - -#[derive(Debug, Clone, Deserialize)] -pub(crate) struct PostAppTokenForm { - name: String, - #[serde(default)] - apple: bool, -} - -async fn route_post_app_token( - user: User, - auth_provider: Data, - path: Path, - Form(PostAppTokenForm { apple, name }): Form, - req: HttpRequest, -) -> Result { - assert!(!name.is_empty()); - assert_eq!(path.into_inner(), user.id); - let token = generate_app_token(); - auth_provider - .add_app_token(&user.id, name.to_owned(), token.clone()) - .await?; - if apple { - let hostname = req.full_url().host_str().unwrap().to_owned(); - let profile = AppleConfig { - token_name: name, - account_description: format!("{}@{}", &user.id, &hostname), - hostname, - caldav_principal_url: req - .url_for("caldav_principal", [&user.id]) - .unwrap() - .to_string(), - carddav_principal_url: req - .url_for("carddav_principal", [&user.id]) - .unwrap() - .to_string(), - user: user.id.to_owned(), - token, - caldav_profile_uuid: Uuid::new_v4(), - carddav_profile_uuid: Uuid::new_v4(), - plist_uuid: Uuid::new_v4(), - } - .render() - .unwrap(); - Ok(HttpResponse::Ok() - .insert_header(header::ContentDisposition::attachment(format!( - "rustical-{}.mobileconfig", - user.id - ))) - .insert_header(( - header::CONTENT_TYPE, - "application/x-apple-aspen-config; charset=utf-8", - )) - .body(profile)) - } else { - Ok(HttpResponse::Ok().body(token)) - } -} - -async fn route_delete_app_token( - user: User, - auth_provider: Data, - path: Path<(String, String)>, -) -> Result { - let (path_user, token_id) = path.into_inner(); - assert_eq!(path_user, user.id); - auth_provider.remove_app_token(&user.id, &token_id).await?; - Ok(Redirect::to("/frontend/user").see_other()) -} - -pub(crate) fn unauthorized_handler( - res: ServiceResponse, -) -> actix_web::Result> { - let (req, _) = res.into_parts(); - let redirect_uri = req.uri().to_string(); - let mut login_url = req.url_for_static("frontend_login").unwrap(); - login_url - .query_pairs_mut() - .append_pair("redirect_uri", &redirect_uri); - let login_url = login_url.to_string(); - - let response = HttpResponse::Unauthorized().body(format!( - r#" - - - - - - Unauthorized, redirecting to login page - - - "# - )); - - let res = ServiceResponse::new(req, response) - .map_into_boxed_body() - .map_into_right_body(); - - Ok(ErrorHandlerResponse::Response(res)) -} - -pub fn session_middleware(frontend_secret: [u8; 64]) -> SessionMiddleware { - SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&frontend_secret)) - .cookie_secure(true) - .cookie_same_site(SameSite::Strict) - .cookie_content_security(CookieContentSecurity::Private) - .build() -} - -pub fn configure_frontend( - cfg: &mut web::ServiceConfig, +pub fn frontend_router( auth_provider: Arc, cal_store: Arc, addr_store: Arc, frontend_config: FrontendConfig, oidc_config: Option, -) { - let mut scope = web::scope("") - .wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler)) - .wrap(AuthenticationMiddleware::new(auth_provider.clone())) - .wrap(session_middleware(frontend_config.secret_key)) - .app_data(Data::from(auth_provider.clone())) - .app_data(Data::from(cal_store.clone())) - .app_data(Data::from(addr_store.clone())) - .app_data(Data::new(frontend_config.clone())) - .app_data(Data::new(oidc_config.clone())) - .service(EmbedService::::new("/assets".to_owned())) - .service(web::resource("").route(web::method(Method::GET).to(route_root))) - .service( - web::resource("/user") - .get(route_get_home) - .name(ROUTE_NAME_HOME), - ) - .service( - web::resource("/user/{user}") - .get(route_user_named::) - .name(ROUTE_USER_NAMED), - ) - // App token management - .service(web::resource("/user/{user}/app_token").post(route_post_app_token::)) - .service( - // POST because HTML5 forms don't support DELETE method - web::resource("/user/{user}/app_token/{id}/delete").post(route_delete_app_token::), - ) - // Calendar - .service(web::resource("/user/{user}/calendar/{calendar}").get(route_calendar::)) - .service( - web::resource("/user/{user}/calendar/{calendar}/restore") - .post(route_calendar_restore::), - ) - // Addressbook - .service( - web::resource("/user/{user}/addressbook/{addressbook}").get(route_addressbook::), - ) - .service( - web::resource("/user/{user}/addressbook/{addressbook}/restore") - .post(route_addressbook_restore::), - ) - // Login - .service( - web::resource("/login") - .name("frontend_login") - .get(route_get_login) - .post(route_post_login::), - ) - .service( - web::resource("/logout") - .name("frontend_logout") - .post(route_post_logout), - ); +) -> Router { + let mut router = Router::new().layer(AuthenticationLayer::new(auth_provider.clone())); + router = router + .route("/login", get(route_get_login).post(route_post_login::)) + .route_service("/assets/{*file}", EmbedService::::new()) + .layer(Extension(auth_provider.clone())) + .layer(Extension(cal_store.clone())) + .layer(Extension(addr_store.clone())) + .layer(Extension(frontend_config.clone())) + .layer(Extension(oidc_config.clone())); + // .wrap(session_middleware(frontend_config.secret_key)) + // .service(web::resource("").route(web::method(Method::GET).to(route_root))) + // .service( + // web::resource("/user") + // .get(route_get_home) + // .name(ROUTE_NAME_HOME), + // ) + // .service( + // web::resource("/user/{user}") + // .get(route_user_named::) + // .name(ROUTE_USER_NAMED), + // ) + // // App token management + // .service(web::resource("/user/{user}/app_token").post(route_post_app_token::)) + // .service( + // // POST because HTML5 forms don't support DELETE method + // web::resource("/user/{user}/app_token/{id}/delete").post(route_delete_app_token::), + // ) + // // Calendar + // .service(web::resource("/user/{user}/calendar/{calendar}").get(route_calendar::)) + // .service( + // web::resource("/user/{user}/calendar/{calendar}/restore") + // .post(route_calendar_restore::), + // ) + // // Addressbook + // .service( + // web::resource("/user/{user}/addressbook/{addressbook}").get(route_addressbook::), + // ) + // .service( + // web::resource("/user/{user}/addressbook/{addressbook}/restore") + // .post(route_addressbook_restore::), + // ) + // // Login + // .service( + // web::resource("/login") + // .name("frontend_login") + // .get(route_get_login) + // .post(route_post_login::), + // ) + // .service( + // web::resource("/logout") + // .name("frontend_logout") + // .post(route_post_logout), + // ); - if let Some(oidc_config) = oidc_config { - scope = scope.service(web::scope("/login/oidc").configure(|cfg| { - configure_oidc( - cfg, - oidc_config, - OidcServiceConfig { - default_redirect_route_name: ROUTE_NAME_HOME, - session_key_user_id: "user", - }, - Arc::new(OidcUserStore(auth_provider.clone())), - ) - })); - } + // if let Some(oidc_config) = oidc_config { + // scope = scope.service(web::scope("/login/oidc").configure(|cfg| { + // configure_oidc( + // cfg, + // oidc_config, + // OidcServiceConfig { + // default_redirect_route_name: ROUTE_NAME_HOME, + // session_key_user_id: "user", + // }, + // Arc::new(OidcUserStore(auth_provider.clone())), + // ) + // })); + // } - cfg.service(scope); -} - -struct OidcUserStore(Arc); - -#[async_trait] -impl UserStore for OidcUserStore { - type Error = rustical_store::Error; - - async fn user_exists(&self, id: &str) -> Result { - Ok(self.0.get_principal(id).await?.is_some()) - } - - async fn insert_user(&self, id: &str) -> Result<(), Self::Error> { - self.0 - .insert_principal( - User { - id: id.to_owned(), - displayname: None, - principal_type: Default::default(), - password: None, - memberships: vec![], - }, - false, - ) - .await - } + router } +// +// struct OidcUserStore(Arc); +// +// #[async_trait] +// impl UserStore for OidcUserStore { +// type Error = rustical_store::Error; +// +// async fn user_exists(&self, id: &str) -> Result { +// Ok(self.0.get_principal(id).await?.is_some()) +// } +// +// async fn insert_user(&self, id: &str) -> Result<(), Self::Error> { +// self.0 +// .insert_principal( +// User { +// id: id.to_owned(), +// displayname: None, +// principal_type: Default::default(), +// password: None, +// memberships: vec![], +// }, +// false, +// ) +// .await +// } +// } diff --git a/crates/frontend/src/nextcloud_login/mod.rs b/crates/frontend/src/nextcloud_login/mod.rs index 065db12..0900673 100644 --- a/crates/frontend/src/nextcloud_login/mod.rs +++ b/crates/frontend/src/nextcloud_login/mod.rs @@ -1,16 +1,11 @@ -use actix_web::{ - http::StatusCode, - middleware::ErrorHandlers, - web::{self, Data, ServiceConfig}, -}; use chrono::{DateTime, Utc}; -use routes::{get_nextcloud_flow, post_nextcloud_flow, post_nextcloud_login, post_nextcloud_poll}; +// use routes::{get_nextcloud_flow, post_nextcloud_flow, post_nextcloud_login, post_nextcloud_poll}; use rustical_store::auth::{AuthenticationMiddleware, AuthenticationProvider}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; -mod routes; +// mod routes; #[derive(Debug, Clone)] struct NextcloudFlow { @@ -47,32 +42,32 @@ pub struct NextcloudFlows { flows: RwLock>, } -use crate::{session_middleware, unauthorized_handler}; - -pub fn configure_nextcloud_login( - cfg: &mut ServiceConfig, - nextcloud_flows_state: Arc, - auth_provider: Arc, - frontend_secret: [u8; 64], -) { - cfg.service( - web::scope("/index.php/login/v2") - .wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler)) - .wrap(AuthenticationMiddleware::new(auth_provider.clone())) - .wrap(session_middleware(frontend_secret)) - .app_data(Data::from(nextcloud_flows_state)) - .app_data(Data::from(auth_provider.clone())) - .service(web::resource("").post(post_nextcloud_login)) - .service( - web::resource("/poll/{flow}") - .name("nc_login_poll") - .post(post_nextcloud_poll::), - ) - .service( - web::resource("/flow/{flow}") - .name("nc_login_flow") - .get(get_nextcloud_flow) - .post(post_nextcloud_flow), - ), - ); -} +// use crate::{session_middleware, unauthorized_handler}; +// +// pub fn configure_nextcloud_login( +// cfg: &mut ServiceConfig, +// nextcloud_flows_state: Arc, +// auth_provider: Arc, +// frontend_secret: [u8; 64], +// ) { +// cfg.service( +// web::scope("/index.php/login/v2") +// .wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler)) +// .wrap(AuthenticationMiddleware::new(auth_provider.clone())) +// .wrap(session_middleware(frontend_secret)) +// .app_data(Data::from(nextcloud_flows_state)) +// .app_data(Data::from(auth_provider.clone())) +// .service(web::resource("").post(post_nextcloud_login)) +// .service( +// web::resource("/poll/{flow}") +// .name("nc_login_poll") +// .post(post_nextcloud_poll::), +// ) +// .service( +// web::resource("/flow/{flow}") +// .name("nc_login_flow") +// .get(get_nextcloud_flow) +// .post(post_nextcloud_flow), +// ), +// ); +// } diff --git a/crates/frontend/src/routes/login.rs b/crates/frontend/src/routes/login.rs index d708732..cb27d80 100644 --- a/crates/frontend/src/routes/login.rs +++ b/crates/frontend/src/routes/login.rs @@ -1,13 +1,14 @@ +use std::sync::Arc; + use crate::{FrontendConfig, OidcConfig}; -use actix_session::Session; -use actix_web::{ - HttpRequest, HttpResponse, Responder, - error::{ErrorNotFound, ErrorUnauthorized}, - web::{Data, Form, Query, Redirect}, -}; use askama::Template; use askama_web::WebTemplate; -use rustical_oidc::ROUTE_NAME_OIDC_LOGIN; +use axum::{ + Extension, Form, + extract::{OriginalUri, Query}, + response::{IntoResponse, Redirect, Response}, +}; +use http::StatusCode; use rustical_store::auth::AuthenticationProvider; use serde::Deserialize; use tracing::instrument; @@ -30,29 +31,28 @@ pub struct GetLoginQuery { redirect_uri: Option, } -#[instrument(skip(req, config, oidc_config))] +#[instrument(skip(config, oidc_config))] pub async fn route_get_login( Query(GetLoginQuery { redirect_uri }): Query, - req: HttpRequest, - config: Data, - oidc_config: Data>, -) -> HttpResponse { - let oidc_data = oidc_config - .as_ref() - .as_ref() - .map(|oidc_config| OidcProviderData { - name: &oidc_config.name, - redirect_url: req - .url_for_static(ROUTE_NAME_OIDC_LOGIN) - .unwrap() - .to_string(), - }); + Extension(config): Extension, + Extension(oidc_config): Extension>, +) -> Response { + // let oidc_data = oidc_config + // .as_ref() + // .as_ref() + // .map(|oidc_config| OidcProviderData { + // name: &oidc_config.name, + // redirect_url: req + // .url_for_static(ROUTE_NAME_OIDC_LOGIN) + // .unwrap() + // .to_string(), + // }); LoginPage { redirect_uri, allow_password_login: config.allow_password_login, - oidc_data, + oidc_data: None, } - .respond_to(&req) + .into_response() } #[derive(Deserialize)] @@ -62,43 +62,38 @@ pub struct PostLoginForm { redirect_uri: Option, } -#[instrument(skip(req, password, auth_provider, session, config))] +// #[instrument(skip(password, auth_provider, config))] pub async fn route_post_login( - req: HttpRequest, + Extension(auth_provider): Extension>, + Extension(config): Extension, + OriginalUri(orig_uri): OriginalUri, Form(PostLoginForm { username, password, redirect_uri, }): Form, - session: Session, - auth_provider: Data, - config: Data, -) -> HttpResponse { +) -> Response { if !config.allow_password_login { - return ErrorNotFound("Password authentication disabled").error_response(); + return StatusCode::METHOD_NOT_ALLOWED.into_response(); } // Ensure that redirect_uri never goes cross-origin let default_redirect = "/frontend/user".to_string(); let redirect_uri = redirect_uri.unwrap_or(default_redirect.clone()); - let redirect_uri = req - .full_url() - .join(&redirect_uri) - .ok() - .and_then(|uri| req.full_url().make_relative(&uri)) - .unwrap_or(default_redirect); + // let redirect_uri = orig_uri + // .join(&redirect_uri) + // .ok() + // .and_then(|uri| orig_uri.make_relative(&uri)) + // .unwrap_or(default_redirect); if let Ok(Some(user)) = auth_provider.validate_password(&username, &password).await { - session.insert("user", user.id).unwrap(); - Redirect::to(redirect_uri) - .see_other() - .respond_to(&req) - .map_into_boxed_body() + // session.insert("user", user.id).unwrap(); + Redirect::to(&redirect_uri).into_response() } else { - ErrorUnauthorized("Unauthorized").error_response() + StatusCode::UNAUTHORIZED.into_response() } } -pub async fn route_post_logout(req: HttpRequest, session: Session) -> Redirect { - session.remove("user"); - Redirect::to(req.url_for_static("frontend_login").unwrap().to_string()).see_other() +pub async fn route_post_logout() -> Redirect { + // session.remove("user"); + Redirect::to("/") } diff --git a/crates/frontend/src/routes/mod.rs b/crates/frontend/src/routes/mod.rs index 2004a1c..2d914de 100644 --- a/crates/frontend/src/routes/mod.rs +++ b/crates/frontend/src/routes/mod.rs @@ -1,4 +1,3 @@ -pub mod addressbook; -pub mod calendar; +// pub mod addressbook; +// pub mod calendar; pub mod login; - diff --git a/crates/ical/Cargo.toml b/crates/ical/Cargo.toml index ce20bed..f4df6f2 100644 --- a/crates/ical/Cargo.toml +++ b/crates/ical/Cargo.toml @@ -5,8 +5,6 @@ edition.workspace = true description.workspace = true repository.workspace = true -[features] -actix = ["dep:actix-web"] [dependencies] chrono.workspace = true @@ -20,4 +18,4 @@ regex.workspace = true rrule.workspace = true serde.workspace = true sha2.workspace = true -actix-web = { workspace = true, optional = true } +axum.workspace = true diff --git a/crates/ical/src/error.rs b/crates/ical/src/error.rs index ebccf18..0821e5f 100644 --- a/crates/ical/src/error.rs +++ b/crates/ical/src/error.rs @@ -1,3 +1,5 @@ +use axum::{http::StatusCode, response::IntoResponse}; + use crate::CalDateTimeError; #[derive(Debug, thiserror::Error)] @@ -21,15 +23,18 @@ pub enum Error { RRuleError(#[from] rrule::RRuleError), } -#[cfg(feature = "actix")] -impl actix_web::ResponseError for Error { - fn status_code(&self) -> actix_web::http::StatusCode { +impl Error { + pub fn status_code(&self) -> StatusCode { match self { - Self::InvalidData(_) => actix_web::http::StatusCode::BAD_REQUEST, - Self::MissingCalendar | Self::MissingContact => { - actix_web::http::StatusCode::BAD_REQUEST - } - _ => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, + Self::InvalidData(_) => StatusCode::BAD_REQUEST, + Self::MissingCalendar | Self::MissingContact => StatusCode::BAD_REQUEST, + _ => StatusCode::INTERNAL_SERVER_ERROR, } } } + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + (self.status_code(), self.to_string()).into_response() + } +} diff --git a/crates/oidc/Cargo.toml b/crates/oidc/Cargo.toml index 4da29f8..1ecef6b 100644 --- a/crates/oidc/Cargo.toml +++ b/crates/oidc/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true openidconnect.workspace = true serde.workspace = true reqwest.workspace = true -actix-web.workspace = true -actix-session.workspace = true thiserror.workspace = true async-trait.workspace = true +axum.workspace = true +axum_session = "0.16" diff --git a/crates/oidc/src/error.rs b/crates/oidc/src/error.rs index 605b025..43e06c4 100644 --- a/crates/oidc/src/error.rs +++ b/crates/oidc/src/error.rs @@ -1,7 +1,5 @@ -use actix_session::SessionInsertError; -use actix_web::{ - HttpResponse, ResponseError, body::BoxBody, error::UrlGenerationError, http::StatusCode, -}; +use axum::http::StatusCode; +use axum::response::IntoResponse; use openidconnect::{ClaimsVerificationError, ConfigurationError, url::ParseError}; #[derive(Debug, thiserror::Error)] @@ -9,28 +7,18 @@ pub enum OidcError { #[error("Cannot generate redirect url, something's not configured correctly")] OidcParseError(#[from] ParseError), - #[error("Cannot generate redirect url, something's not configured correctly")] - ActixUrlGenerationError(#[from] UrlGenerationError), - #[error(transparent)] OidcConfigurationError(#[from] ConfigurationError), #[error(transparent)] OidcClaimsVerificationError(#[from] ClaimsVerificationError), - #[error(transparent)] - SessionInsertError(#[from] SessionInsertError), - #[error("{0}")] Other(&'static str), } -impl ResponseError for OidcError { - fn status_code(&self) -> StatusCode { - StatusCode::INTERNAL_SERVER_ERROR - } - - fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code()).body(self.to_string()) +impl IntoResponse for OidcError { + fn into_response(self) -> axum::response::Response { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() } } diff --git a/crates/oidc/src/lib.rs b/crates/oidc/src/lib.rs index bf61d64..2cb64a0 100644 --- a/crates/oidc/src/lib.rs +++ b/crates/oidc/src/lib.rs @@ -1,8 +1,7 @@ -use actix_session::Session; -use actix_web::{ - HttpRequest, HttpResponse, Responder, ResponseError, - http::StatusCode, - web::{self, Data, Form, Query, Redirect, ServiceConfig}, +use axum::{ + Extension, Form, + extract::{Query, Request, State}, + response::{IntoResponse, Redirect, Response}, }; pub use config::OidcConfig; use config::UserIdClaim; @@ -93,47 +92,44 @@ pub struct GetOidcForm { } /// Endpoint that redirects to the authorize endpoint of the OIDC service -pub async fn route_post_oidc( - req: HttpRequest, - Form(GetOidcForm { redirect_uri }): Form, - oidc_config: Data, - session: Session, -) -> Result { - let http_client = get_http_client(); - let oidc_client = get_oidc_client( - oidc_config.as_ref().clone(), - &http_client, - RedirectUrl::new(req.url_for_static(ROUTE_NAME_OIDC_CALLBACK)?.to_string())?, - ) - .await?; - - let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); - - let (auth_url, csrf_token, nonce) = oidc_client - .authorize_url( - AuthenticationFlow::::AuthorizationCode, - CsrfToken::new_random, - Nonce::new_random, - ) - .add_scopes(oidc_config.scopes.clone()) - .set_pkce_challenge(pkce_challenge) - .url(); - - session.insert( - SESSION_KEY_OIDC_STATE, - OidcState { - state: csrf_token, - nonce, - pkce_verifier, - redirect_uri, - }, - )?; - - Ok(Redirect::to(auth_url.to_string()) - .see_other() - .respond_to(&req) - .map_into_boxed_body()) -} +// pub async fn route_post_oidc( +// Form(GetOidcForm { redirect_uri }): Form, +// State(oidc_config): State, +// // session: Session, +// req: Request, +// ) -> Result { +// let http_client = get_http_client(); +// let oidc_client = get_oidc_client( +// oidc_config.clone(), +// &http_client, +// RedirectUrl::new(req.url_for_static(ROUTE_NAME_OIDC_CALLBACK)?.to_string())?, +// ) +// .await?; +// +// let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); +// +// let (auth_url, csrf_token, nonce) = oidc_client +// .authorize_url( +// AuthenticationFlow::::AuthorizationCode, +// CsrfToken::new_random, +// Nonce::new_random, +// ) +// .add_scopes(oidc_config.scopes.clone()) +// .set_pkce_challenge(pkce_challenge) +// .url(); +// +// // session.insert( +// // SESSION_KEY_OIDC_STATE, +// // OidcState { +// // state: csrf_token, +// // nonce, +// // pkce_verifier, +// // redirect_uri, +// // }, +// // )?; +// +// Ok(Redirect::to(auth_url.as_str()).into_response()) +// } #[derive(Debug, Clone, Deserialize)] pub struct AuthCallbackQuery { @@ -142,124 +138,124 @@ pub struct AuthCallbackQuery { state: String, } -/// Handle callback from IdP page -pub async fn route_get_oidc_callback( - req: HttpRequest, - oidc_config: Data, - session: Session, - user_store: Data, - Query(AuthCallbackQuery { code, iss, state }): Query, - service_config: Data, -) -> Result { - assert_eq!(iss, oidc_config.issuer); - let oidc_state = session - .remove_as::(SESSION_KEY_OIDC_STATE) - .ok_or(OidcError::Other("No local OIDC state"))? - .map_err(|_| OidcError::Other("Error parsing OIDC state"))?; +// Handle callback from IdP page +// pub async fn route_get_oidc_callback( +// Extension(oidc_config): Extension, +// session: Session, +// Extension(user_store): Extension, +// Query(AuthCallbackQuery { code, iss, state }): Query, +// State(service_config): State, +// req: Request, +// ) -> Result { +// assert_eq!(iss, oidc_config.issuer); +// let oidc_state = session +// .remove_as::(SESSION_KEY_OIDC_STATE) +// .ok_or(OidcError::Other("No local OIDC state"))? +// .map_err(|_| OidcError::Other("Error parsing OIDC state"))?; +// +// assert_eq!(oidc_state.state.secret(), &state); +// +// let http_client = get_http_client(); +// let oidc_client = get_oidc_client( +// oidc_config.clone(), +// &http_client, +// RedirectUrl::new(req.url_for_static(ROUTE_NAME_OIDC_CALLBACK)?.to_string())?, +// ) +// .await?; +// +// let token_response = oidc_client +// .exchange_code(code)? +// .set_pkce_verifier(oidc_state.pkce_verifier) +// .request_async(&http_client) +// .await +// .map_err(|_| OidcError::Other("Error requesting token"))?; +// let id_claims = token_response +// .id_token() +// .ok_or(OidcError::Other("OIDC provider did not return an ID token"))? +// .claims(&oidc_client.id_token_verifier(), &oidc_state.nonce)?; +// +// let user_info_claims: UserInfoClaims = oidc_client +// .user_info( +// token_response.access_token().clone(), +// Some(id_claims.subject().clone()), +// )? +// .request_async(&http_client) +// .await +// .map_err(|_| OidcError::Other("Error fetching user info"))?; +// +// if let Some(require_group) = &oidc_config.require_group { +// if !user_info_claims +// .additional_claims() +// .groups +// .contains(require_group) +// { +// return Ok(HttpResponse::build(StatusCode::UNAUTHORIZED) +// .body("User is not in an authorized group to use RustiCal")); +// } +// } +// +// let user_id = match oidc_config.claim_userid { +// UserIdClaim::Sub => user_info_claims.subject().to_string(), +// UserIdClaim::PreferredUsername => user_info_claims +// .preferred_username() +// .ok_or(OidcError::Other("Missing preferred_username claim"))? +// .to_string(), +// }; +// +// match user_store.user_exists(&user_id).await { +// Ok(false) => { +// // User does not exist +// if !oidc_config.allow_sign_up { +// return Ok(HttpResponse::Unauthorized().body("User sign up disabled")); +// } +// // Create new user +// if let Err(err) = user_store.insert_user(&user_id).await { +// return Ok(err.error_response()); +// } +// } +// Ok(true) => {} +// Err(err) => { +// return Ok(err.error_response()); +// } +// } +// +// let default_redirect = req +// .url_for_static(service_config.default_redirect_route_name)? +// .to_string(); +// let redirect_uri = oidc_state.redirect_uri.unwrap_or(default_redirect.clone()); +// let redirect_uri = req +// .full_url() +// .join(&redirect_uri) +// .ok() +// .and_then(|uri| req.full_url().make_relative(&uri)) +// .unwrap_or(default_redirect); +// +// // Complete login flow +// session.insert(service_config.session_key_user_id, user_id.clone())?; +// +// Ok(Redirect::to(redirect_uri) +// .temporary() +// .respond_to(&req) +// .map_into_boxed_body()) +// } - assert_eq!(oidc_state.state.secret(), &state); - - let http_client = get_http_client(); - let oidc_client = get_oidc_client( - oidc_config.get_ref().clone(), - &http_client, - RedirectUrl::new(req.url_for_static(ROUTE_NAME_OIDC_CALLBACK)?.to_string())?, - ) - .await?; - - let token_response = oidc_client - .exchange_code(code)? - .set_pkce_verifier(oidc_state.pkce_verifier) - .request_async(&http_client) - .await - .map_err(|_| OidcError::Other("Error requesting token"))?; - let id_claims = token_response - .id_token() - .ok_or(OidcError::Other("OIDC provider did not return an ID token"))? - .claims(&oidc_client.id_token_verifier(), &oidc_state.nonce)?; - - let user_info_claims: UserInfoClaims = oidc_client - .user_info( - token_response.access_token().clone(), - Some(id_claims.subject().clone()), - )? - .request_async(&http_client) - .await - .map_err(|_| OidcError::Other("Error fetching user info"))?; - - if let Some(require_group) = &oidc_config.require_group { - if !user_info_claims - .additional_claims() - .groups - .contains(require_group) - { - return Ok(HttpResponse::build(StatusCode::UNAUTHORIZED) - .body("User is not in an authorized group to use RustiCal")); - } - } - - let user_id = match oidc_config.claim_userid { - UserIdClaim::Sub => user_info_claims.subject().to_string(), - UserIdClaim::PreferredUsername => user_info_claims - .preferred_username() - .ok_or(OidcError::Other("Missing preferred_username claim"))? - .to_string(), - }; - - match user_store.user_exists(&user_id).await { - Ok(false) => { - // User does not exist - if !oidc_config.allow_sign_up { - return Ok(HttpResponse::Unauthorized().body("User sign up disabled")); - } - // Create new user - if let Err(err) = user_store.insert_user(&user_id).await { - return Ok(err.error_response()); - } - } - Ok(true) => {} - Err(err) => { - return Ok(err.error_response()); - } - } - - let default_redirect = req - .url_for_static(service_config.default_redirect_route_name)? - .to_string(); - let redirect_uri = oidc_state.redirect_uri.unwrap_or(default_redirect.clone()); - let redirect_uri = req - .full_url() - .join(&redirect_uri) - .ok() - .and_then(|uri| req.full_url().make_relative(&uri)) - .unwrap_or(default_redirect); - - // Complete login flow - session.insert(service_config.session_key_user_id, user_id.clone())?; - - Ok(Redirect::to(redirect_uri) - .temporary() - .respond_to(&req) - .map_into_boxed_body()) -} - -pub fn configure_oidc( - cfg: &mut ServiceConfig, - oidc_config: OidcConfig, - service_config: OidcServiceConfig, - user_store: Arc, -) { - cfg.app_data(Data::new(oidc_config)) - .app_data(Data::new(service_config)) - .app_data(Data::from(user_store)) - .service( - web::resource("") - .name(ROUTE_NAME_OIDC_LOGIN) - .post(route_post_oidc), - ) - .service( - web::resource("/callback") - .name(ROUTE_NAME_OIDC_CALLBACK) - .get(route_get_oidc_callback::), - ); -} +// pub fn configure_oidc( +// cfg: &mut ServiceConfig, +// oidc_config: OidcConfig, +// service_config: OidcServiceConfig, +// user_store: Arc, +// ) { +// cfg.app_data(Data::new(oidc_config)) +// .app_data(Data::new(service_config)) +// .app_data(Data::from(user_store)) +// .service( +// web::resource("") +// .name(ROUTE_NAME_OIDC_LOGIN) +// .post(route_post_oidc), +// ) +// .service( +// web::resource("/callback") +// .name(ROUTE_NAME_OIDC_CALLBACK) +// .get(route_get_oidc_callback::), +// ); +// } diff --git a/crates/oidc/src/user_store.rs b/crates/oidc/src/user_store.rs index 40d5485..6c3f6a4 100644 --- a/crates/oidc/src/user_store.rs +++ b/crates/oidc/src/user_store.rs @@ -1,9 +1,9 @@ -use actix_web::ResponseError; use async_trait::async_trait; +use axum::response::IntoResponse; #[async_trait] -pub trait UserStore: 'static { - type Error: ResponseError; +pub trait UserStore: 'static + Send { + type Error: IntoResponse; async fn user_exists(&self, id: &str) -> Result; async fn insert_user(&self, id: &str) -> Result<(), Self::Error>; diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index b872f4c..7669edb 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -16,12 +16,9 @@ chrono = { workspace = true } regex = { workspace = true } lazy_static = { workspace = true } thiserror = { workspace = true } -actix-web = { workspace = true } -actix-session = { workspace = true } -actix-web-httpauth = { workspace = true } tracing = { workspace = true } chrono-tz = { workspace = true } -derive_more = { workspace = true } +derive_more = { workspace = true, features = ["as_ref"] } rustical_xml.workspace = true tokio.workspace = true rand.workspace = true @@ -29,7 +26,12 @@ uuid.workspace = true clap.workspace = true rustical_dav.workspace = true rustical_ical.workspace = true +axum.workspace = true +http.workspace = true rrule.workspace = true +headers.workspace = true +tower.workspace = true +futures-core.workspace = true [dev-dependencies] rstest = { workspace = true } diff --git a/crates/store/src/auth/middleware.rs b/crates/store/src/auth/middleware.rs index 70dfde4..628f774 100644 --- a/crates/store/src/auth/middleware.rs +++ b/crates/store/src/auth/middleware.rs @@ -1,103 +1,91 @@ use super::AuthenticationProvider; -use actix_session::Session; -use actix_web::{ - FromRequest, HttpMessage, - dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready}, - http::header::Header, -}; -use actix_web_httpauth::headers::authorization::{Authorization, Basic}; +use axum::{extract::Request, response::Response}; +use futures_core::future::BoxFuture; +use headers::{Authorization, HeaderMapExt, authorization::Basic}; use std::{ - future::{Future, Ready, ready}, pin::Pin, sync::Arc, + task::{Context, Poll}, }; +use tower::{Layer, Service}; use tracing::{Instrument, info_span}; -pub struct AuthenticationMiddleware { +pub struct AuthenticationLayer { auth_provider: Arc, } -impl AuthenticationMiddleware { +impl Clone for AuthenticationLayer { + fn clone(&self) -> Self { + Self { + auth_provider: self.auth_provider.clone(), + } + } +} + +impl AuthenticationLayer { pub fn new(auth_provider: Arc) -> Self { Self { auth_provider } } } -impl Transform for AuthenticationMiddleware -where - S: Service, Error = actix_web::Error> + 'static, - S::Future: 'static, - B: 'static, - AP: 'static, -{ - type Error = actix_web::Error; - type Response = ServiceResponse; - type InitError = (); - type Transform = InnerAuthenticationMiddleware; - type Future = Ready>; +impl Layer for AuthenticationLayer { + type Service = AuthenticationMiddleware; - fn new_transform(&self, service: S) -> Self::Future { - ready(Ok(InnerAuthenticationMiddleware { - service: Arc::new(service), - auth_provider: Arc::clone(&self.auth_provider), - })) + fn layer(&self, inner: S) -> Self::Service { + Self::Service { + inner, + auth_provider: self.auth_provider.clone(), + } } } -pub struct InnerAuthenticationMiddleware { - service: Arc, +pub struct AuthenticationMiddleware { + inner: S, auth_provider: Arc, } -impl Service for InnerAuthenticationMiddleware +impl Clone for AuthenticationMiddleware { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + auth_provider: self.auth_provider.clone(), + } + } +} + +impl Service for AuthenticationMiddleware where - S: Service, Error = actix_web::Error> + 'static, - S::Future: 'static, - AP: AuthenticationProvider, + S: Service + Send + 'static, + S::Future: Send + 'static, { - type Response = ServiceResponse; - type Error = actix_web::Error; - type Future = Pin>>>; + type Response = S::Response; + type Error = S::Error; + type Future = BoxFuture<'static, Result>; - forward_ready!(service); + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } - fn call(&self, req: ServiceRequest) -> Self::Future { - let service = Arc::clone(&self.service); - let auth_provider = Arc::clone(&self.auth_provider); + fn call(&mut self, mut request: Request) -> Self::Future { + let auth_header: Option> = request.headers().typed_get(); + let ap = self.auth_provider.clone(); + let mut inner = self.inner.clone(); + // request.extensions_mut(); Box::pin(async move { - if let Ok(auth) = Authorization::::parse(req.request()) { - let user_id = auth.as_ref().user_id(); - if let Some(password) = auth.as_ref().password() { - if let Ok(Some(user)) = auth_provider - .validate_app_token(user_id, password) - .instrument(info_span!("validate_user_token")) - .await - { - req.extensions_mut().insert(user); - } + if let Some(auth) = auth_header { + let user_id = auth.username(); + let password = auth.password(); + if let Ok(Some(user)) = ap + .validate_app_token(user_id, password) + .instrument(info_span!("validate_user_token")) + .await + { + request.extensions_mut().insert(user); } } - - // Extract user from session cookie - if let Ok(session) = Session::extract(req.request()).await { - match session.get::("user") { - Ok(Some(user_id)) => match auth_provider.get_principal(&user_id).await { - Ok(Some(user)) => { - req.extensions_mut().insert(user); - } - Ok(None) => {} - Err(err) => { - dbg!(err); - } - }, - Ok(None) => {} - Err(err) => { - dbg!(err); - } - }; - } - service.call(req).await + let response = inner.call(request).await?; + Ok(response) }) } } diff --git a/crates/store/src/auth/user.rs b/crates/store/src/auth/user.rs index 2d160fa..87b089b 100644 --- a/crates/store/src/auth/user.rs +++ b/crates/store/src/auth/user.rs @@ -1,16 +1,14 @@ -use actix_web::{ - FromRequest, HttpMessage, HttpResponse, ResponseError, - body::BoxBody, - http::{StatusCode, header}, +use axum::{ + body::Body, + extract::FromRequestParts, + response::{IntoResponse, Response}, }; use chrono::{DateTime, Utc}; use derive_more::Display; +use http::{HeaderValue, StatusCode, header}; use rustical_xml::ValueSerialize; use serde::{Deserialize, Serialize}; -use std::{ - fmt::Display, - future::{Ready, ready}, -}; +use std::fmt::Display; use crate::Secret; @@ -121,33 +119,28 @@ impl rustical_dav::Principal for User { #[derive(Clone, Debug, Display)] pub struct UnauthorizedError; -impl ResponseError for UnauthorizedError { - fn status_code(&self) -> actix_web::http::StatusCode { - StatusCode::UNAUTHORIZED - } - fn error_response(&self) -> HttpResponse { - HttpResponse::build(StatusCode::UNAUTHORIZED) - .insert_header(( - header::WWW_AUTHENTICATE, - r#"Basic realm="RustiCal", charset="UTF-8""#, - )) - .finish() +impl IntoResponse for UnauthorizedError { + fn into_response(self) -> axum::response::Response { + let mut resp = Response::builder().status(StatusCode::UNAUTHORIZED); + resp.headers_mut().unwrap().insert( + header::WWW_AUTHENTICATE, + HeaderValue::from_static(r#"Basic realm="RustiCal", charset="UTF-8""#), + ); + resp.body(Body::empty()).unwrap() } } -impl FromRequest for User { - type Error = UnauthorizedError; - type Future = Ready>; +impl FromRequestParts for User { + type Rejection = UnauthorizedError; - fn from_request( - req: &actix_web::HttpRequest, - _payload: &mut actix_web::dev::Payload, - ) -> Self::Future { - ready( - req.extensions() - .get::() - .cloned() - .ok_or(UnauthorizedError), - ) + async fn from_request_parts( + parts: &mut http::request::Parts, + _state: &S, + ) -> Result { + parts + .extensions + .get::() + .cloned() + .ok_or(UnauthorizedError) } } diff --git a/crates/store/src/error.rs b/crates/store/src/error.rs index b338b43..8820a6a 100644 --- a/crates/store/src/error.rs +++ b/crates/store/src/error.rs @@ -1,4 +1,4 @@ -use actix_web::{ResponseError, http::StatusCode}; +use http::StatusCode; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -27,8 +27,8 @@ pub enum Error { IcalError(#[from] rustical_ical::Error), } -impl ResponseError for Error { - fn status_code(&self) -> actix_web::http::StatusCode { +impl Error { + pub fn status_code(&self) -> StatusCode { match self { Self::NotFound => StatusCode::NOT_FOUND, Self::AlreadyExists => StatusCode::CONFLICT, diff --git a/src/app.rs b/src/app.rs index 25e0034..10a1050 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,16 +1,14 @@ -use actix_web::body::MessageBody; -use actix_web::dev::{ServiceFactory, ServiceRequest, ServiceResponse}; -use actix_web::middleware::NormalizePath; -use actix_web::{App, web}; -use rustical_caldav::caldav_service; -use rustical_carddav::carddav_service; -use rustical_frontend::nextcloud_login::{NextcloudFlows, configure_nextcloud_login}; -use rustical_frontend::{FrontendConfig, configure_frontend}; +use axum::Router; +use axum::response::Redirect; +use axum::routing::get; +use rustical_caldav::caldav_router; +use rustical_carddav::carddav_router; +use rustical_frontend::nextcloud_login::NextcloudFlows; +use rustical_frontend::{FrontendConfig, frontend_router}; use rustical_oidc::OidcConfig; use rustical_store::auth::AuthenticationProvider; use rustical_store::{AddressbookStore, CalendarStore, SubscriptionStore}; use std::sync::Arc; -use tracing_actix_web::TracingLogger; use crate::config::NextcloudLoginConfig; @@ -24,61 +22,61 @@ pub fn make_app( oidc_config: Option, nextcloud_login_config: NextcloudLoginConfig, nextcloud_flows_state: Arc, -) -> App< - impl ServiceFactory< - ServiceRequest, - Response = ServiceResponse, - Config = (), - InitError = (), - Error = actix_web::Error, - >, -> { - let mut app = App::new() - // .wrap(Logger::new("[%s] %r")) - .wrap(TracingLogger::default()) - .wrap(NormalizePath::trim()) - .service(web::scope("/caldav").service(caldav_service( +) -> Router { + let mut router = Router::new() + .nest( "/caldav", - auth_provider.clone(), - cal_store.clone(), - addr_store.clone(), - subscription_store.clone(), - ))) - .service(web::scope("/carddav").service(carddav_service( + caldav_router( + "/caldav", + auth_provider.clone(), + cal_store.clone(), + addr_store.clone(), + subscription_store.clone(), + ), + ) + .nest( "/carddav", - auth_provider.clone(), - addr_store.clone(), - subscription_store, - ))) - .service( - web::scope("/.well-known") - .service(web::redirect("/caldav", "/caldav")) - .service(web::redirect("/carddav", "/carddav")), + carddav_router( + "/carddav", + auth_provider.clone(), + addr_store.clone(), + subscription_store.clone(), + ), + ) + .route( + "/.well-known/caldav", + get(async || Redirect::permanent("/caldav")), + ) + .route( + "/.well-known/carddav", + get(async || Redirect::permanent("/caldav")), ); - if nextcloud_login_config.enabled { - app = app.configure(|cfg| { - configure_nextcloud_login( - cfg, - nextcloud_flows_state, - auth_provider.clone(), - frontend_config.secret_key, - ) - }); - } if frontend_config.enabled { - app = app - .service(web::scope("/frontend").configure(|cfg| { - configure_frontend( - cfg, + router = router + .nest( + "/frontend", + frontend_router( auth_provider.clone(), cal_store.clone(), addr_store.clone(), frontend_config, oidc_config, - ) - })) - .service(web::redirect("/", "/frontend").see_other()); + ), + ) + .route("/", get(async || Redirect::to("/frontend"))); } - app + + router + + // if nextcloud_login_config.enabled { + // app = app.configure(|cfg| { + // configure_nextcloud_login( + // cfg, + // nextcloud_flows_state, + // auth_provider.clone(), + // frontend_config.secret_key, + // ) + // }); + // } } diff --git a/src/main.rs b/src/main.rs index 6c4cbeb..fc533bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,4 @@ use crate::config::Config; -use actix_web::HttpServer; -use actix_web::http::KeepAlive; use anyhow::Result; use app::make_app; use clap::{Parser, Subcommand}; @@ -105,67 +103,24 @@ async fn main() -> Result<()> { let nextcloud_flows = Arc::new(NextcloudFlows::default()); - HttpServer::new(move || { - make_app( - addr_store.clone(), - cal_store.clone(), - subscription_store.clone(), - principal_store.clone(), - config.frontend.clone(), - config.oidc.clone(), - config.nextcloud_login.clone(), - nextcloud_flows.clone(), - ) - }) - .bind((config.http.host, config.http.port))? - // Workaround for a weird bug where - // new requests might timeout since they cannot properly reuse the connection - // https://github.com/lennart-k/rustical/issues/10 - .keep_alive(KeepAlive::Disabled) - .run() + let app = make_app( + addr_store.clone(), + cal_store.clone(), + subscription_store.clone(), + principal_store.clone(), + config.frontend.clone(), + config.oidc.clone(), + config.nextcloud_login.clone(), + nextcloud_flows.clone(), + ); + + let listener = tokio::net::TcpListener::bind(&format!( + "{}:{}", + config.http.host, config.http.port + )) .await?; + axum::serve(listener, app).await?; } } Ok(()) } - -#[cfg(test)] -mod tests { - use crate::{app::make_app, config::NextcloudLoginConfig, get_data_stores}; - use actix_web::{http::StatusCode, test::TestRequest}; - use rustical_frontend::nextcloud_login::NextcloudFlows; - use rustical_frontend::{FrontendConfig, generate_frontend_secret}; - use std::sync::Arc; - - #[tokio::test] - async fn test_main() { - let (addr_store, cal_store, subscription_store, principal_store, _update_recv) = - get_data_stores( - true, - &crate::config::DataStoreConfig::Sqlite(crate::config::SqliteDataStoreConfig { - db_url: "".to_owned(), - }), - ) - .await - .unwrap(); - - let app = make_app( - addr_store, - cal_store, - subscription_store, - principal_store, - FrontendConfig { - enabled: false, - secret_key: generate_frontend_secret(), - allow_password_login: false, - }, - None, - NextcloudLoginConfig { enabled: false }, - Arc::new(NextcloudFlows::default()), - ); - let app = actix_web::test::init_service(app).await; - let req = TestRequest::get().uri("/").to_request(); - let resp = actix_web::test::call_service(&app, req).await; - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } -}