mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 21:42:34 +00:00
Checkpoint: Migration to axum
This commit is contained in:
627
Cargo.lock
generated
627
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@@ -32,11 +32,8 @@ debug = 0
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
uuid = { version = "1.11", features = ["v4", "fast-rng"] }
|
uuid = { version = "1.11", features = ["v4", "fast-rng"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
actix-web = "4.11"
|
axum = "0.8"
|
||||||
tracing = { version = "0.1", features = ["async-await"] }
|
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"] }
|
anyhow = { version = "1.0", features = ["backtrace"] }
|
||||||
serde = { version = "1.0", features = ["serde_derive", "derive", "rc"] }
|
serde = { version = "1.0", features = ["serde_derive", "derive", "rc"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
@@ -63,7 +60,7 @@ quick-xml = { version = "0.37" }
|
|||||||
rust-embed = "8.5"
|
rust-embed = "8.5"
|
||||||
futures-core = "0.3.31"
|
futures-core = "0.3.31"
|
||||||
hex = { version = "0.4.3", features = ["serde"] }
|
hex = { version = "0.4.3", features = ["serde"] }
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0"
|
||||||
itertools = "0.14"
|
itertools = "0.14"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
derive_more = { version = "2.0", features = [
|
derive_more = { version = "2.0", features = [
|
||||||
@@ -75,7 +72,7 @@ derive_more = { version = "2.0", features = [
|
|||||||
"display",
|
"display",
|
||||||
] }
|
] }
|
||||||
askama = { version = "0.14", features = ["serde_json"] }
|
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 = { version = "0.8", default-features = false, features = [
|
||||||
"sqlx-sqlite",
|
"sqlx-sqlite",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -86,7 +83,6 @@ sqlx = { version = "0.8", default-features = false, features = [
|
|||||||
"migrate",
|
"migrate",
|
||||||
"json",
|
"json",
|
||||||
] }
|
] }
|
||||||
http_02 = { package = "http", version = "0.2" } # actix-web uses a very outdated version
|
|
||||||
http = "1.3"
|
http = "1.3"
|
||||||
headers = "0.4"
|
headers = "0.4"
|
||||||
strum = "0.27"
|
strum = "0.27"
|
||||||
@@ -95,7 +91,8 @@ serde_json = { version = "1.0", features = ["raw_value"] }
|
|||||||
sqlx-sqlite = { version = "0.8", features = ["bundled"] }
|
sqlx-sqlite = { version = "0.8", features = ["bundled"] }
|
||||||
ical = { version = "0.11", features = ["generator", "serde"] }
|
ical = { version = "0.11", features = ["generator", "serde"] }
|
||||||
toml = "0.8"
|
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_dav_push = { path = "./crates/dav_push/" }
|
||||||
rustical_store = { path = "./crates/store/" }
|
rustical_store = { path = "./crates/store/" }
|
||||||
rustical_store_sqlite = { path = "./crates/store_sqlite/" }
|
rustical_store_sqlite = { path = "./crates/store_sqlite/" }
|
||||||
@@ -104,10 +101,11 @@ rustical_carddav = { path = "./crates/carddav/" }
|
|||||||
rustical_frontend = { path = "./crates/frontend/" }
|
rustical_frontend = { path = "./crates/frontend/" }
|
||||||
rustical_xml = { path = "./crates/xml/" }
|
rustical_xml = { path = "./crates/xml/" }
|
||||||
rustical_oidc = { path = "./crates/oidc/" }
|
rustical_oidc = { path = "./crates/oidc/" }
|
||||||
rustical_ical = { path = "./crates/ical/", features = ["actix"] }
|
rustical_ical = { path = "./crates/ical/"}
|
||||||
chrono-tz = "0.10"
|
chrono-tz = "0.10"
|
||||||
chrono-humanize = "0.2"
|
chrono-humanize = "0.2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
axum-extra = { version = "0.10", features = ["typed-header"] }
|
||||||
rrule = "0.14"
|
rrule = "0.14"
|
||||||
argon2 = "0.5"
|
argon2 = "0.5"
|
||||||
rpassword = "7.3"
|
rpassword = "7.3"
|
||||||
@@ -129,9 +127,8 @@ clap = { version = "4.5", features = ["derive", "env"] }
|
|||||||
rustical_store = { workspace = true }
|
rustical_store = { workspace = true }
|
||||||
rustical_store_sqlite = { workspace = true }
|
rustical_store_sqlite = { workspace = true }
|
||||||
rustical_caldav = { workspace = true }
|
rustical_caldav = { workspace = true }
|
||||||
rustical_carddav = { workspace = true }
|
rustical_carddav.workspace = true
|
||||||
rustical_frontend = { workspace = true }
|
rustical_frontend = { workspace = true }
|
||||||
actix-web = { workspace = true }
|
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
@@ -140,8 +137,8 @@ anyhow = { workspace = true }
|
|||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
tracing-actix-web = { workspace = true }
|
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
|
||||||
opentelemetry = { version = "0.30", optional = true }
|
opentelemetry = { version = "0.30", optional = true }
|
||||||
opentelemetry-otlp = { version = "0.30", optional = true, features = [
|
opentelemetry-otlp = { version = "0.30", optional = true, features = [
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ repository.workspace = true
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = { workspace = true }
|
axum.workspace = true
|
||||||
|
axum-extra.workspace = true
|
||||||
|
tower.workspace = true
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
quick-xml = { workspace = true }
|
quick-xml = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-actix-web = { workspace = true }
|
|
||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
derive_more = { workspace = true }
|
derive_more = { workspace = true }
|
||||||
actix-web-httpauth = { workspace = true }
|
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
@@ -30,3 +30,4 @@ uuid.workspace = true
|
|||||||
rustical_dav_push.workspace = true
|
rustical_dav_push.workspace = true
|
||||||
rustical_ical.workspace = true
|
rustical_ical.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
|
headers.workspace = true
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::calendar::prop::SupportedCalendarComponentSet;
|
use crate::calendar::prop::SupportedCalendarComponentSet;
|
||||||
use actix_web::HttpResponse;
|
use crate::calendar::resource::CalendarResourceService;
|
||||||
use actix_web::web::{Data, Path};
|
use axum::extract::{Path, State};
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use http::StatusCode;
|
||||||
use rustical_ical::CalendarObjectType;
|
use rustical_ical::CalendarObjectType;
|
||||||
use rustical_store::auth::User;
|
use rustical_store::auth::User;
|
||||||
use rustical_store::{Calendar, CalendarStore};
|
use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
|
||||||
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
|
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use tracing_actix_web::RootSpan;
|
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug)]
|
#[derive(XmlDeserialize, Clone, Debug)]
|
||||||
pub struct MkcolCalendarProp {
|
pub struct MkcolCalendarProp {
|
||||||
@@ -48,15 +49,13 @@ struct MkcalendarRequest {
|
|||||||
set: PropElement,
|
set: PropElement,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(parent = root_span.id(), skip(store, root_span))]
|
#[instrument(skip(cal_store))]
|
||||||
pub async fn route_mkcalendar<C: CalendarStore>(
|
pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
|
||||||
path: Path<(String, String)>,
|
Path((principal, cal_id)): Path<(String, String)>,
|
||||||
body: String,
|
|
||||||
user: User,
|
user: User,
|
||||||
store: Data<C>,
|
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
||||||
root_span: RootSpan,
|
body: String,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<Response, Error> {
|
||||||
let (principal, cal_id) = path.into_inner();
|
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
}
|
}
|
||||||
@@ -87,12 +86,10 @@ pub async fn route_mkcalendar<C: CalendarStore>(
|
|||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
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.
|
// 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 :)
|
// However, it works without one but breaks on iPadOS when using an empty one :)
|
||||||
Ok(()) => Ok(HttpResponse::Created()
|
Ok(()) => Ok(StatusCode::CREATED.into_response()),
|
||||||
.insert_header(("Cache-Control", "no-cache"))
|
|
||||||
.body("")),
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
dbg!(err.to_string());
|
dbg!(err.to_string());
|
||||||
Err(err.into())
|
Err(err.into())
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
pub mod mkcalendar;
|
pub mod mkcalendar;
|
||||||
pub mod post;
|
// pub mod post;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::calendar::resource::{CalendarResource, CalendarResourceService};
|
use crate::calendar::resource::{CalendarResource, CalendarResourceService};
|
||||||
use actix_web::http::header;
|
use axum::extract::{Path, State};
|
||||||
use actix_web::web::{Data, Path};
|
use axum::response::Response;
|
||||||
use actix_web::{HttpRequest, HttpResponse};
|
|
||||||
use rustical_dav::privileges::UserPrivilege;
|
use rustical_dav::privileges::UserPrivilege;
|
||||||
use rustical_dav::resource::Resource;
|
use rustical_dav::resource::Resource;
|
||||||
use rustical_dav_push::register::PushRegister;
|
use rustical_dav_push::register::PushRegister;
|
||||||
@@ -10,18 +9,14 @@ use rustical_store::auth::User;
|
|||||||
use rustical_store::{CalendarStore, Subscription, SubscriptionStore};
|
use rustical_store::{CalendarStore, Subscription, SubscriptionStore};
|
||||||
use rustical_xml::XmlDocument;
|
use rustical_xml::XmlDocument;
|
||||||
use tracing::instrument;
|
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<C: CalendarStore, S: SubscriptionStore>(
|
pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
|
||||||
path: Path<(String, String)>,
|
Path((principal, cal_id)): Path<(String, String)>,
|
||||||
body: String,
|
|
||||||
user: User,
|
user: User,
|
||||||
resource_service: Data<CalendarResourceService<C, S>>,
|
State(resource_service): State<CalendarResourceService<C, S>>,
|
||||||
root_span: RootSpan,
|
body: String,
|
||||||
req: HttpRequest,
|
) -> Result<Response, Error> {
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
let (principal, cal_id) = path.into_inner();
|
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::{Error, calendar_object::resource::CalendarObjectPropWrapperName};
|
use crate::{Error, calendar_object::resource::CalendarObjectPropWrapperName};
|
||||||
use actix_web::dev::{Path, ResourceDef};
|
|
||||||
use rustical_dav::xml::PropfindType;
|
use rustical_dav::xml::PropfindType;
|
||||||
use rustical_ical::CalendarObject;
|
use rustical_ical::CalendarObject;
|
||||||
use rustical_store::CalendarStore;
|
use rustical_store::CalendarStore;
|
||||||
@@ -23,23 +22,25 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
|
|||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
store: &C,
|
store: &C,
|
||||||
) -> Result<(Vec<CalendarObject>, Vec<String>), Error> {
|
) -> Result<(Vec<CalendarObject>, Vec<String>), Error> {
|
||||||
let resource_def = ResourceDef::prefix(path).join(&ResourceDef::new("/{object_id}.ics"));
|
|
||||||
|
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
let mut not_found = vec![];
|
let mut not_found = vec![];
|
||||||
|
|
||||||
for href in &cal_query.href {
|
for href in &cal_query.href {
|
||||||
let mut path = Path::new(href.as_str());
|
if let Some(filename) = href.strip_prefix(path) {
|
||||||
if !resource_def.capture_match_info(&mut path) {
|
if let Some(object_id) = filename.strip_suffix(".ics") {
|
||||||
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 {
|
match store.get_object(principal, cal_id, object_id).await {
|
||||||
Ok(object) => result.push(object),
|
Ok(object) => result.push(object),
|
||||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
not_found.push(href.to_owned());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
not_found.push(href.to_owned());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((result, not_found))
|
Ok((result, not_found))
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
CalDavPrincipalUri, Error,
|
CalDavPrincipalUri, Error,
|
||||||
|
calendar::resource::CalendarResourceService,
|
||||||
calendar_object::resource::{
|
calendar_object::resource::{
|
||||||
CalendarObjectPropWrapper, CalendarObjectPropWrapperName, CalendarObjectResource,
|
CalendarObjectPropWrapper, CalendarObjectPropWrapperName, CalendarObjectResource,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use actix_web::{
|
use axum::{
|
||||||
HttpRequest, Responder,
|
Extension,
|
||||||
web::{Data, Path},
|
extract::{OriginalUri, Path, State},
|
||||||
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use calendar_multiget::{CalendarMultigetRequest, get_objects_calendar_multiget};
|
use calendar_multiget::{CalendarMultigetRequest, get_objects_calendar_multiget};
|
||||||
use calendar_query::{CalendarQueryRequest, get_objects_calendar_query};
|
use calendar_query::{CalendarQueryRequest, get_objects_calendar_query};
|
||||||
@@ -19,7 +21,7 @@ use rustical_dav::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use rustical_ical::CalendarObject;
|
use rustical_ical::CalendarObject;
|
||||||
use rustical_store::{CalendarStore, auth::User};
|
use rustical_store::{CalendarStore, SubscriptionStore, auth::User};
|
||||||
use rustical_xml::{XmlDeserialize, XmlDocument};
|
use rustical_xml::{XmlDeserialize, XmlDocument};
|
||||||
use sync_collection::handle_sync_collection;
|
use sync_collection::handle_sync_collection;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
@@ -85,16 +87,15 @@ fn objects_response(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(req, cal_store))]
|
#[instrument(skip(cal_store))]
|
||||||
pub async fn route_report_calendar<C: CalendarStore>(
|
pub async fn route_report_calendar<C: CalendarStore, S: SubscriptionStore>(
|
||||||
path: Path<(String, String)>,
|
Path((principal, cal_id)): Path<(String, String)>,
|
||||||
body: String,
|
|
||||||
user: User,
|
user: User,
|
||||||
req: HttpRequest,
|
Extension(puri): Extension<CalDavPrincipalUri>,
|
||||||
puri: Data<CalDavPrincipalUri>,
|
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
||||||
cal_store: Data<C>,
|
OriginalUri(uri): OriginalUri,
|
||||||
) -> Result<impl Responder, Error> {
|
body: String,
|
||||||
let (principal, cal_id) = path.into_inner();
|
) -> Result<impl IntoResponse, Error> {
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
}
|
}
|
||||||
@@ -107,20 +108,12 @@ pub async fn route_report_calendar<C: CalendarStore>(
|
|||||||
let objects =
|
let objects =
|
||||||
get_objects_calendar_query(cal_query, &principal, &cal_id, cal_store.as_ref())
|
get_objects_calendar_query(cal_query, &principal, &cal_id, cal_store.as_ref())
|
||||||
.await?;
|
.await?;
|
||||||
objects_response(
|
objects_response(objects, vec![], uri.path(), &principal, &puri, &user, props)?
|
||||||
objects,
|
|
||||||
vec![],
|
|
||||||
req.path(),
|
|
||||||
&principal,
|
|
||||||
puri.as_ref(),
|
|
||||||
&user,
|
|
||||||
props,
|
|
||||||
)?
|
|
||||||
}
|
}
|
||||||
ReportRequest::CalendarMultiget(cal_multiget) => {
|
ReportRequest::CalendarMultiget(cal_multiget) => {
|
||||||
let (objects, not_found) = get_objects_calendar_multiget(
|
let (objects, not_found) = get_objects_calendar_multiget(
|
||||||
cal_multiget,
|
cal_multiget,
|
||||||
req.path(),
|
uri.path(),
|
||||||
&principal,
|
&principal,
|
||||||
&cal_id,
|
&cal_id,
|
||||||
cal_store.as_ref(),
|
cal_store.as_ref(),
|
||||||
@@ -129,9 +122,9 @@ pub async fn route_report_calendar<C: CalendarStore>(
|
|||||||
objects_response(
|
objects_response(
|
||||||
objects,
|
objects,
|
||||||
not_found,
|
not_found,
|
||||||
req.path(),
|
uri.path(),
|
||||||
&principal,
|
&principal,
|
||||||
puri.as_ref(),
|
&puri,
|
||||||
&user,
|
&user,
|
||||||
props,
|
props,
|
||||||
)?
|
)?
|
||||||
@@ -139,8 +132,8 @@ pub async fn route_report_calendar<C: CalendarStore>(
|
|||||||
ReportRequest::SyncCollection(sync_collection) => {
|
ReportRequest::SyncCollection(sync_collection) => {
|
||||||
handle_sync_collection(
|
handle_sync_collection(
|
||||||
sync_collection,
|
sync_collection,
|
||||||
req.path(),
|
uri.path(),
|
||||||
puri.as_ref(),
|
&puri,
|
||||||
&user,
|
&user,
|
||||||
&principal,
|
&principal,
|
||||||
&cal_id,
|
&cal_id,
|
||||||
|
|||||||
@@ -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 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 crate::{CalDavPrincipalUri, Error};
|
||||||
use actix_web::http::Method;
|
|
||||||
use actix_web::web::{self};
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use axum::extract::Request;
|
||||||
|
use axum::handler::Handler;
|
||||||
|
use axum::response::Response;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
|
use futures_util::future::BoxFuture;
|
||||||
use rustical_dav::extensions::{
|
use rustical_dav::extensions::{
|
||||||
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
|
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
|
||||||
};
|
};
|
||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
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::xml::{HrefElement, Resourcetype, ResourcetypeInner};
|
||||||
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
|
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
|
||||||
use rustical_ical::CalDateTime;
|
use rustical_ical::CalDateTime;
|
||||||
@@ -21,8 +22,10 @@ use rustical_store::auth::User;
|
|||||||
use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
|
use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
|
||||||
use rustical_xml::{EnumVariants, PropName};
|
use rustical_xml::{EnumVariants, PropName};
|
||||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||||
|
use std::convert::Infallible;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tower::Service;
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||||
#[xml(unit_variants_ident = "CalendarPropName")]
|
#[xml(unit_variants_ident = "CalendarPropName")]
|
||||||
@@ -313,6 +316,15 @@ pub struct CalendarResourceService<C: CalendarStore, S: SubscriptionStore> {
|
|||||||
pub(crate) sub_store: Arc<S>,
|
pub(crate) sub_store: Arc<S>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<C: CalendarStore, S: SubscriptionStore> Clone for CalendarResourceService<C, S> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
cal_store: self.cal_store.clone(),
|
||||||
|
sub_store: self.sub_store.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<C: CalendarStore, S: SubscriptionStore> CalendarResourceService<C, S> {
|
impl<C: CalendarStore, S: SubscriptionStore> CalendarResourceService<C, S> {
|
||||||
pub fn new(cal_store: Arc<C>, sub_store: Arc<S>) -> Self {
|
pub fn new(cal_store: Arc<C>, sub_store: Arc<S>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -386,17 +398,21 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn actix_scope(self) -> actix_web::Scope {
|
impl<C: CalendarStore, S: SubscriptionStore> AxumMethods for CalendarResourceService<C, S> {
|
||||||
let report_method = web::method(Method::from_str("REPORT").unwrap());
|
fn report() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
let mkcalendar_method = web::method(Method::from_str("MKCALENDAR").unwrap());
|
Some(|state, req| {
|
||||||
web::scope("/{calendar_id}")
|
let mut service = Handler::with_state(route_report_calendar::<C, S>, state);
|
||||||
.service(CalendarObjectResourceService::new(self.cal_store.clone()).actix_scope())
|
Box::pin(Service::call(&mut service, req))
|
||||||
.service(
|
})
|
||||||
self.actix_resource()
|
}
|
||||||
.route(report_method.to(route_report_calendar::<C>))
|
|
||||||
.route(mkcalendar_method.to(route_mkcalendar::<C>))
|
fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>>
|
||||||
.post(route_post::<C, S>),
|
{
|
||||||
)
|
Some(|state, req| {
|
||||||
|
let mut service = Handler::with_state(route_mkcalendar::<C, S>, state);
|
||||||
|
Box::pin(Service::call(&mut service, req))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,70 @@
|
|||||||
|
use super::resource::CalendarObjectPathComponents;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
use crate::calendar_object::resource::CalendarObjectResourceService;
|
||||||
use crate::error::Precondition;
|
use crate::error::Precondition;
|
||||||
use actix_web::HttpRequest;
|
use axum::body::Body;
|
||||||
use actix_web::HttpResponse;
|
use axum::extract::{Path, State};
|
||||||
use actix_web::http::header;
|
use axum::response::{IntoResponse, Response};
|
||||||
use actix_web::http::header::HeaderValue;
|
use axum_extra::TypedHeader;
|
||||||
use actix_web::web::{Data, Path};
|
use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||||
|
use http::StatusCode;
|
||||||
use rustical_ical::CalendarObject;
|
use rustical_ical::CalendarObject;
|
||||||
use rustical_store::CalendarStore;
|
use rustical_store::CalendarStore;
|
||||||
use rustical_store::auth::User;
|
use rustical_store::auth::User;
|
||||||
|
use std::str::FromStr;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use tracing_actix_web::RootSpan;
|
|
||||||
|
|
||||||
use super::resource::CalendarObjectPathComponents;
|
#[instrument(skip(cal_store))]
|
||||||
|
|
||||||
#[instrument(parent = root_span.id(), skip(store, root_span))]
|
|
||||||
pub async fn get_event<C: CalendarStore>(
|
pub async fn get_event<C: CalendarStore>(
|
||||||
path: Path<CalendarObjectPathComponents>,
|
Path(CalendarObjectPathComponents {
|
||||||
store: Data<C>,
|
|
||||||
user: User,
|
|
||||||
root_span: RootSpan,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
let CalendarObjectPathComponents {
|
|
||||||
principal,
|
principal,
|
||||||
calendar_id,
|
calendar_id,
|
||||||
object_id,
|
object_id,
|
||||||
} = path.into_inner();
|
}): Path<CalendarObjectPathComponents>,
|
||||||
|
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
|
||||||
|
user: User,
|
||||||
|
) -> Result<Response, Error> {
|
||||||
if !user.is_principal(&principal) {
|
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) {
|
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)
|
.get_object(&principal, &calendar_id, &object_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
let mut resp = Response::builder().status(StatusCode::OK);
|
||||||
.insert_header(("ETag", event.get_etag()))
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
.insert_header(("Content-Type", "text/calendar"))
|
hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap());
|
||||||
.body(event.get_ics().to_owned()))
|
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<C: CalendarStore>(
|
pub async fn put_event<C: CalendarStore>(
|
||||||
path: Path<CalendarObjectPathComponents>,
|
Path(CalendarObjectPathComponents {
|
||||||
store: Data<C>,
|
|
||||||
body: String,
|
|
||||||
user: User,
|
|
||||||
req: HttpRequest,
|
|
||||||
root_span: RootSpan,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
let CalendarObjectPathComponents {
|
|
||||||
principal,
|
principal,
|
||||||
calendar_id,
|
calendar_id,
|
||||||
object_id,
|
object_id,
|
||||||
} = path.into_inner();
|
}): Path<CalendarObjectPathComponents>,
|
||||||
|
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
|
||||||
|
user: User,
|
||||||
|
if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||||
|
body: String,
|
||||||
|
) -> Result<Response, Error> {
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Ok(HttpResponse::Unauthorized().finish());
|
return Err(crate::Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let overwrite =
|
let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
|
||||||
Some(&HeaderValue::from_static("*")) != req.headers().get(header::IF_NONE_MATCH);
|
if_none_match == IfNoneMatch::any()
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
let object = match CalendarObject::from_ics(object_id, body) {
|
let object = match CalendarObject::from_ics(object_id, body) {
|
||||||
Ok(obj) => obj,
|
Ok(obj) => obj,
|
||||||
@@ -73,9 +72,9 @@ pub async fn put_event<C: CalendarStore>(
|
|||||||
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
|
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
store
|
cal_store
|
||||||
.put_object(principal, calendar_id, object, overwrite)
|
.put_object(principal, calendar_id, object, overwrite)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Created().finish())
|
Ok(StatusCode::CREATED.into_response())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,35 @@
|
|||||||
use super::methods::{get_event, put_event};
|
// use super::methods::{get_event, put_event};
|
||||||
use crate::{CalDavPrincipalUri, Error};
|
use crate::{
|
||||||
use actix_web::web;
|
CalDavPrincipalUri, Error,
|
||||||
|
calendar_object::methods::{get_event, put_event},
|
||||||
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use axum::{extract::Request, handler::Handler, response::Response};
|
||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
|
use futures_util::future::BoxFuture;
|
||||||
use rustical_dav::{
|
use rustical_dav::{
|
||||||
extensions::{CommonPropertiesExtension, CommonPropertiesProp},
|
extensions::{CommonPropertiesExtension, CommonPropertiesProp},
|
||||||
privileges::UserPrivilegeSet,
|
privileges::UserPrivilegeSet,
|
||||||
resource::{PrincipalUri, Resource, ResourceService},
|
resource::{AxumMethods, PrincipalUri, Resource, ResourceService},
|
||||||
xml::Resourcetype,
|
xml::Resourcetype,
|
||||||
};
|
};
|
||||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||||
use rustical_store::{CalendarStore, auth::User};
|
use rustical_store::{CalendarStore, auth::User};
|
||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Deserializer};
|
||||||
use std::sync::Arc;
|
use std::{convert::Infallible, sync::Arc};
|
||||||
|
use tower::Service;
|
||||||
|
|
||||||
pub struct CalendarObjectResourceService<C: CalendarStore> {
|
pub struct CalendarObjectResourceService<C: CalendarStore> {
|
||||||
cal_store: Arc<C>,
|
pub(crate) cal_store: Arc<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: CalendarStore> Clone for CalendarObjectResourceService<C> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
cal_store: self.cal_store.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: CalendarStore> CalendarObjectResourceService<C> {
|
impl<C: CalendarStore> CalendarObjectResourceService<C> {
|
||||||
@@ -130,10 +143,23 @@ impl Resource for CalendarObjectResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize_ics_name<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||||
|
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)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct CalendarObjectPathComponents {
|
pub struct CalendarObjectPathComponents {
|
||||||
pub principal: String,
|
pub principal: String,
|
||||||
pub calendar_id: String,
|
pub calendar_id: String,
|
||||||
|
#[serde(deserialize_with = "deserialize_ics_name")]
|
||||||
pub object_id: String,
|
pub object_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,12 +206,19 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn actix_scope(self) -> actix_web::Scope {
|
impl<C: CalendarStore> AxumMethods for CalendarObjectResourceService<C> {
|
||||||
web::scope("/{object_id}.ics").service(
|
fn get() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
self.actix_resource()
|
Some(|state, req| {
|
||||||
.get(get_event::<C>)
|
let mut service = Handler::with_state(get_event::<C>, state);
|
||||||
.put(put_event::<C>),
|
Box::pin(Service::call(&mut service, req))
|
||||||
)
|
})
|
||||||
|
}
|
||||||
|
fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
|
Some(|state, req| {
|
||||||
|
let mut service = Handler::with_state(put_event::<C>, state);
|
||||||
|
Box::pin(Service::call(&mut service, req))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use crate::calendar::resource::{CalendarResource, CalendarResourceService};
|
use crate::calendar::resource::CalendarResource;
|
||||||
use crate::{CalDavPrincipalUri, Error};
|
use crate::{CalDavPrincipalUri, Error};
|
||||||
use actix_web::web;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
|
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
|
||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
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::xml::{Resourcetype, ResourcetypeInner};
|
||||||
use rustical_store::auth::User;
|
use rustical_store::auth::User;
|
||||||
use rustical_store::{CalendarStore, SubscriptionStore};
|
use rustical_store::{CalendarStore, SubscriptionStore};
|
||||||
@@ -67,6 +66,16 @@ pub struct CalendarSetResourceService<C: CalendarStore, S: SubscriptionStore> {
|
|||||||
sub_store: Arc<S>,
|
sub_store: Arc<S>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<C: CalendarStore, S: SubscriptionStore> Clone for CalendarSetResourceService<C, S> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
name: self.name,
|
||||||
|
cal_store: self.cal_store.clone(),
|
||||||
|
sub_store: self.sub_store.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<C: CalendarStore, S: SubscriptionStore> CalendarSetResourceService<C, S> {
|
impl<C: CalendarStore, S: SubscriptionStore> CalendarSetResourceService<C, S> {
|
||||||
pub fn new(name: &'static str, cal_store: Arc<C>, sub_store: Arc<S>) -> Self {
|
pub fn new(name: &'static str, cal_store: Arc<C>, sub_store: Arc<S>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -116,16 +125,5 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarSetReso
|
|||||||
})
|
})
|
||||||
.collect())
|
.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<C: CalendarStore, S: SubscriptionStore> AxumMethods for CalendarSetResourceService<C, S> {}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use actix_web::{
|
use axum::{
|
||||||
HttpResponse,
|
body::Body,
|
||||||
http::{StatusCode, header::ContentType},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
use headers::{ContentType, HeaderMapExt};
|
||||||
|
use http::StatusCode;
|
||||||
use rustical_xml::{XmlSerialize, XmlSerializeRoot};
|
use rustical_xml::{XmlSerialize, XmlSerializeRoot};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
@@ -12,22 +14,18 @@ pub enum Precondition {
|
|||||||
ValidCalendarData,
|
ValidCalendarData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix_web::ResponseError for Precondition {
|
impl IntoResponse for Precondition {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn into_response(self) -> axum::response::Response {
|
||||||
StatusCode::PRECONDITION_FAILED
|
|
||||||
}
|
|
||||||
fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
|
|
||||||
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
||||||
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
|
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) {
|
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();
|
||||||
}
|
}
|
||||||
|
let mut res = Response::builder().status(StatusCode::PRECONDITION_FAILED);
|
||||||
HttpResponse::PreconditionFailed()
|
res.headers_mut().unwrap().typed_insert(ContentType::xml());
|
||||||
.content_type(ContentType::xml())
|
res.body(Body::from(output)).unwrap()
|
||||||
.body(String::from_utf8(output).unwrap())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,8 +59,8 @@ pub enum Error {
|
|||||||
PreconditionFailed(Precondition),
|
PreconditionFailed(Precondition),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix_web::ResponseError for Error {
|
impl Error {
|
||||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
pub fn status_code(&self) -> StatusCode {
|
||||||
match self {
|
match self {
|
||||||
Error::StoreError(err) => match err {
|
Error::StoreError(err) => match err {
|
||||||
rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
|
rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
|
||||||
@@ -78,16 +76,13 @@ impl actix_web::ResponseError for Error {
|
|||||||
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
|
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Error::NotFound => StatusCode::NOT_FOUND,
|
Error::NotFound => StatusCode::NOT_FOUND,
|
||||||
Error::IcalError(err) => err.status_code(),
|
Error::IcalError(err) => err.status_code(),
|
||||||
Error::PreconditionFailed(err) => err.status_code(),
|
Error::PreconditionFailed(_err) => StatusCode::PRECONDITION_FAILED,
|
||||||
}
|
|
||||||
}
|
|
||||||
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
|
|
||||||
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()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Error {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
(self.status_code(), self.to_string()).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
use actix_web::HttpResponse;
|
use axum::{Extension, Router};
|
||||||
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 derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
use principal::PrincipalResourceService;
|
use principal::PrincipalResourceService;
|
||||||
use rustical_dav::resource::{PrincipalUri, ResourceService};
|
use rustical_dav::resource::{PrincipalUri, ResourceService};
|
||||||
use rustical_dav::resources::RootResourceService;
|
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 rustical_store::{AddressbookStore, CalendarStore, ContactBirthdayStore, SubscriptionStore};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use subscription::subscription_resource;
|
|
||||||
|
|
||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
pub mod calendar_object;
|
pub mod calendar_object;
|
||||||
pub mod calendar_set;
|
pub mod calendar_set;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod principal;
|
pub mod principal;
|
||||||
mod subscription;
|
// mod subscription;
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
|
use crate::calendar::resource::CalendarResourceService;
|
||||||
|
use crate::calendar_object::resource::CalendarObjectResourceService;
|
||||||
|
use crate::calendar_set::CalendarSetResourceService;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Constructor)]
|
#[derive(Debug, Clone, Constructor)]
|
||||||
pub struct CalDavPrincipalUri(&'static str);
|
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
|
// pub fn caldav_service<
|
||||||
/// methods of a resource
|
// AP: AuthenticationProvider,
|
||||||
fn options_handler() -> ErrorHandlers<BoxBody> {
|
// AS: AddressbookStore,
|
||||||
ErrorHandlers::new().handler(StatusCode::METHOD_NOT_ALLOWED, |res| {
|
// C: CalendarStore,
|
||||||
Ok(ErrorHandlerResponse::Response(
|
// S: SubscriptionStore,
|
||||||
if res.request().method() == Method::OPTIONS {
|
// >(
|
||||||
let mut response = HttpResponse::Ok();
|
// prefix: &'static str,
|
||||||
response.insert_header((
|
// auth_provider: Arc<AP>,
|
||||||
HeaderName::from_static("dav"),
|
// store: Arc<C>,
|
||||||
// https://datatracker.ietf.org/doc/html/rfc4918#section-18
|
// addr_store: Arc<AS>,
|
||||||
HeaderValue::from_static(
|
// subscription_store: Arc<S>,
|
||||||
"1, 3, access-control, calendar-access, extended-mkcol, webdav-push",
|
// ) -> 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) {
|
pub fn caldav_router<
|
||||||
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<
|
|
||||||
AP: AuthenticationProvider,
|
AP: AuthenticationProvider,
|
||||||
AS: AddressbookStore,
|
AS: AddressbookStore,
|
||||||
C: CalendarStore,
|
C: CalendarStore,
|
||||||
@@ -69,22 +72,53 @@ pub fn caldav_service<
|
|||||||
store: Arc<C>,
|
store: Arc<C>,
|
||||||
addr_store: Arc<AS>,
|
addr_store: Arc<AS>,
|
||||||
subscription_store: Arc<S>,
|
subscription_store: Arc<S>,
|
||||||
) -> impl HttpServiceFactory {
|
) -> Router {
|
||||||
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store));
|
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store));
|
||||||
|
let principal_service = PrincipalResourceService {
|
||||||
RootResourceService::<_, User, CalDavPrincipalUri>::new(PrincipalResourceService {
|
|
||||||
auth_provider: auth_provider.clone(),
|
auth_provider: auth_provider.clone(),
|
||||||
sub_store: subscription_store.clone(),
|
sub_store: subscription_store.clone(),
|
||||||
birthday_store: birthday_store.clone(),
|
birthday_store: birthday_store.clone(),
|
||||||
cal_store: store.clone(),
|
cal_store: store.clone(),
|
||||||
})
|
};
|
||||||
.actix_scope()
|
|
||||||
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
Router::new()
|
||||||
.wrap(options_handler())
|
.route_service(
|
||||||
.app_data(Data::from(store.clone()))
|
"/",
|
||||||
.app_data(Data::from(birthday_store.clone()))
|
RootResourceService::<_, User, CalDavPrincipalUri>::new(principal_service.clone())
|
||||||
.app_data(Data::new(CalDavPrincipalUri::new(
|
.axum_service(),
|
||||||
format!("{prefix}/principal").leak(),
|
)
|
||||||
)))
|
.route_service("/principal/{principal}", principal_service.axum_service())
|
||||||
.service(subscription_resource(subscription_store))
|
.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)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use crate::calendar_set::{CalendarSetResource, CalendarSetResourceService};
|
use crate::calendar_set::CalendarSetResource;
|
||||||
use crate::{CalDavPrincipalUri, Error};
|
use crate::{CalDavPrincipalUri, Error};
|
||||||
use actix_web::web;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
|
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
|
||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
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::xml::{HrefElement, Resourcetype, ResourcetypeInner};
|
||||||
use rustical_store::auth::user::PrincipalType;
|
use rustical_store::auth::user::PrincipalType;
|
||||||
use rustical_store::auth::{AuthenticationProvider, User};
|
use rustical_store::auth::{AuthenticationProvider, User};
|
||||||
@@ -194,25 +193,9 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: Ca
|
|||||||
),
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fn actix_scope(self) -> actix_web::Scope {
|
|
||||||
web::scope("/principal/{principal}")
|
impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: CalendarStore>
|
||||||
.service(
|
AxumMethods for PrincipalResourceService<AP, S, CS, BS>
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ repository.workspace = true
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = { workspace = true }
|
axum.workspace = true
|
||||||
|
axum-extra.workspace = true
|
||||||
|
tower.workspace = true
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
quick-xml = { workspace = true }
|
quick-xml = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-actix-web = { workspace = true }
|
|
||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
derive_more = { workspace = true }
|
derive_more = { workspace = true }
|
||||||
actix-web-httpauth = { workspace = true }
|
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use super::resource::AddressObjectPathComponents;
|
use super::resource::AddressObjectPathComponents;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
use crate::address_object::resource::AddressObjectResourceService;
|
||||||
use crate::addressbook::resource::AddressbookResource;
|
use crate::addressbook::resource::AddressbookResource;
|
||||||
use actix_web::HttpRequest;
|
use axum::body::Body;
|
||||||
use actix_web::HttpResponse;
|
use axum::extract::{Path, State};
|
||||||
use actix_web::http::header;
|
use axum::response::{IntoResponse, Response};
|
||||||
use actix_web::http::header::HeaderValue;
|
use axum_extra::TypedHeader;
|
||||||
use actix_web::web::{Data, Path};
|
use axum_extra::headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||||
|
use http::StatusCode;
|
||||||
use rustical_dav::privileges::UserPrivilege;
|
use rustical_dav::privileges::UserPrivilege;
|
||||||
use rustical_dav::resource::Resource;
|
use rustical_dav::resource::Resource;
|
||||||
use rustical_ical::AddressObject;
|
use rustical_ical::AddressObject;
|
||||||
use rustical_store::AddressbookStore;
|
use rustical_store::AddressbookStore;
|
||||||
use rustical_store::auth::User;
|
use rustical_store::auth::User;
|
||||||
use tracing::instrument;
|
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<AS: AddressbookStore>(
|
pub async fn get_object<AS: AddressbookStore>(
|
||||||
path: Path<AddressObjectPathComponents>,
|
Path(AddressObjectPathComponents {
|
||||||
store: Data<AS>,
|
|
||||||
user: User,
|
|
||||||
root_span: RootSpan,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
let AddressObjectPathComponents {
|
|
||||||
principal,
|
principal,
|
||||||
addressbook_id,
|
addressbook_id,
|
||||||
object_id,
|
object_id,
|
||||||
} = path.into_inner();
|
}): Path<AddressObjectPathComponents>,
|
||||||
|
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
|
||||||
|
user: User,
|
||||||
|
) -> Result<Response, Error> {
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let addressbook = store
|
let addressbook = addr_store
|
||||||
.get_addressbook(&principal, &addressbook_id, false)
|
.get_addressbook(&principal, &addressbook_id, false)
|
||||||
.await?;
|
.await?;
|
||||||
let addressbook_resource = AddressbookResource(addressbook);
|
let addressbook_resource = AddressbookResource(addressbook);
|
||||||
@@ -42,42 +42,43 @@ pub async fn get_object<AS: AddressbookStore>(
|
|||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let object = store
|
let object = addr_store
|
||||||
.get_object(&principal, &addressbook_id, &object_id, false)
|
.get_object(&principal, &addressbook_id, &object_id, false)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
let mut resp = Response::builder().status(StatusCode::OK);
|
||||||
.insert_header(("ETag", object.get_etag()))
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
.insert_header(("Content-Type", "text/vcard"))
|
hdrs.typed_insert(ETag::from_str(&object.get_etag()).unwrap());
|
||||||
.body(object.get_vcf().to_owned()))
|
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<AS: AddressbookStore>(
|
pub async fn put_object<AS: AddressbookStore>(
|
||||||
path: Path<AddressObjectPathComponents>,
|
Path(AddressObjectPathComponents {
|
||||||
store: Data<AS>,
|
|
||||||
body: String,
|
|
||||||
user: User,
|
|
||||||
req: HttpRequest,
|
|
||||||
root_span: RootSpan,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
let AddressObjectPathComponents {
|
|
||||||
principal,
|
principal,
|
||||||
addressbook_id,
|
addressbook_id,
|
||||||
object_id,
|
object_id,
|
||||||
} = path.into_inner();
|
}): Path<AddressObjectPathComponents>,
|
||||||
|
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
|
||||||
|
user: User,
|
||||||
|
if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||||
|
body: String,
|
||||||
|
) -> Result<Response, Error> {
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let overwrite =
|
let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
|
||||||
Some(&HeaderValue::from_static("*")) != req.headers().get(header::IF_NONE_MATCH);
|
if_none_match == IfNoneMatch::any()
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
let object = AddressObject::from_vcf(object_id, body)?;
|
let object = AddressObject::from_vcf(object_id, body)?;
|
||||||
store
|
addr_store
|
||||||
.put_object(principal, addressbook_id, object, overwrite)
|
.put_object(principal, addressbook_id, object, overwrite)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Created().finish())
|
Ok(StatusCode::CREATED.into_response())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,34 @@
|
|||||||
use crate::{CardDavPrincipalUri, Error};
|
use crate::{CardDavPrincipalUri, Error};
|
||||||
use actix_web::web;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use axum::{extract::Request, handler::Handler, response::Response};
|
||||||
use derive_more::derive::{Constructor, From, Into};
|
use derive_more::derive::{Constructor, From, Into};
|
||||||
|
use futures_util::future::BoxFuture;
|
||||||
use rustical_dav::{
|
use rustical_dav::{
|
||||||
extensions::{CommonPropertiesExtension, CommonPropertiesProp},
|
extensions::{CommonPropertiesExtension, CommonPropertiesProp},
|
||||||
privileges::UserPrivilegeSet,
|
privileges::UserPrivilegeSet,
|
||||||
resource::{PrincipalUri, Resource, ResourceService},
|
resource::{AxumMethods, PrincipalUri, Resource, ResourceService},
|
||||||
xml::Resourcetype,
|
xml::Resourcetype,
|
||||||
};
|
};
|
||||||
use rustical_ical::AddressObject;
|
use rustical_ical::AddressObject;
|
||||||
use rustical_store::{AddressbookStore, auth::User};
|
use rustical_store::{AddressbookStore, auth::User};
|
||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
use std::{convert::Infallible, sync::Arc};
|
||||||
|
use tower::Service;
|
||||||
|
|
||||||
use super::methods::{get_object, put_object};
|
use super::methods::{get_object, put_object};
|
||||||
|
|
||||||
#[derive(Constructor)]
|
#[derive(Constructor)]
|
||||||
pub struct AddressObjectResourceService<AS: AddressbookStore> {
|
pub struct AddressObjectResourceService<AS: AddressbookStore> {
|
||||||
addr_store: Arc<AS>,
|
pub(crate) addr_store: Arc<AS>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<AS: AddressbookStore> Clone for AddressObjectResourceService<AS> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
addr_store: self.addr_store.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||||
@@ -148,13 +158,20 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
impl<AS: AddressbookStore> AxumMethods for AddressObjectResourceService<AS> {
|
||||||
fn actix_scope(self) -> actix_web::Scope {
|
fn get() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
web::scope("/{object_id}.vcf").service(
|
Some(|state, req| {
|
||||||
self.actix_resource()
|
let mut service = Handler::with_state(get_object::<AS>, state);
|
||||||
.get(get_object::<AS>)
|
Box::pin(Service::call(&mut service, req))
|
||||||
.put(put_object::<AS>),
|
})
|
||||||
)
|
}
|
||||||
|
|
||||||
|
fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
|
Some(|state, req| {
|
||||||
|
let mut service = Handler::with_state(put_object::<AS>, state);
|
||||||
|
Box::pin(Service::call(&mut service, req))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use crate::Error;
|
use crate::{Error, addressbook::resource::AddressbookResourceService};
|
||||||
use actix_web::web::Path;
|
use axum::{
|
||||||
use actix_web::{HttpResponse, web::Data};
|
extract::{Path, State},
|
||||||
use rustical_store::{Addressbook, AddressbookStore, auth::User};
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use http::StatusCode;
|
||||||
|
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::User};
|
||||||
use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag};
|
use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use tracing_actix_web::RootSpan;
|
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
pub struct Resourcetype {
|
pub struct Resourcetype {
|
||||||
@@ -39,15 +41,13 @@ struct MkcolRequest {
|
|||||||
set: PropElement<MkcolAddressbookProp>,
|
set: PropElement<MkcolAddressbookProp>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(parent = root_span.id(), skip(store, root_span))]
|
#[instrument(skip(addr_store))]
|
||||||
pub async fn route_mkcol<AS: AddressbookStore>(
|
pub async fn route_mkcol<AS: AddressbookStore, S: SubscriptionStore>(
|
||||||
path: Path<(String, String)>,
|
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||||
body: String,
|
|
||||||
user: User,
|
user: User,
|
||||||
store: Data<AS>,
|
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||||
root_span: RootSpan,
|
body: String,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<Response, Error> {
|
||||||
let (principal, addressbook_id) = path.into_inner();
|
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ pub async fn route_mkcol<AS: AddressbookStore>(
|
|||||||
push_topic: uuid::Uuid::new_v4().to_string(),
|
push_topic: uuid::Uuid::new_v4().to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match store
|
match addr_store
|
||||||
.get_addressbook(&principal, &addressbook_id, true)
|
.get_addressbook(&principal, &addressbook_id, true)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -74,7 +74,11 @@ pub async fn route_mkcol<AS: AddressbookStore>(
|
|||||||
}
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// oh no, there's a conflict
|
// 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) => {
|
Err(err) => {
|
||||||
// some other error
|
// some other error
|
||||||
@@ -82,12 +86,10 @@ pub async fn route_mkcol<AS: AddressbookStore>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match store.insert_addressbook(addressbook).await {
|
match addr_store.insert_addressbook(addressbook).await {
|
||||||
// TODO: The spec says we should return a mkcol-response.
|
// TODO: The spec says we should return a mkcol-response.
|
||||||
// However, it works without one but breaks on iPadOS when using an empty one :)
|
// However, it works without one but breaks on iPadOS when using an empty one :)
|
||||||
Ok(()) => Ok(HttpResponse::Created()
|
Ok(()) => Ok(StatusCode::CREATED.into_response()),
|
||||||
.insert_header(("Cache-Control", "no-cache"))
|
|
||||||
.body("")),
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
dbg!(err.to_string());
|
dbg!(err.to_string());
|
||||||
Err(err.into())
|
Err(err.into())
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
pub mod mkcol;
|
pub mod mkcol;
|
||||||
pub mod post;
|
// pub mod post;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ use crate::{
|
|||||||
AddressObjectPropWrapper, AddressObjectPropWrapperName, AddressObjectResource,
|
AddressObjectPropWrapper, AddressObjectPropWrapperName, AddressObjectResource,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use actix_web::dev::{Path, ResourceDef};
|
|
||||||
|
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use rustical_dav::{
|
use rustical_dav::{
|
||||||
resource::{PrincipalUri, Resource},
|
resource::{PrincipalUri, Resource},
|
||||||
@@ -32,27 +30,28 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
|||||||
addressbook_id: &str,
|
addressbook_id: &str,
|
||||||
store: &AS,
|
store: &AS,
|
||||||
) -> Result<(Vec<AddressObject>, Vec<String>), Error> {
|
) -> Result<(Vec<AddressObject>, Vec<String>), Error> {
|
||||||
let resource_def = ResourceDef::prefix(path).join(&ResourceDef::new("/{object_id}.vcf"));
|
|
||||||
|
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
let mut not_found = vec![];
|
let mut not_found = vec![];
|
||||||
|
|
||||||
for href in &addressbook_multiget.href {
|
for href in &addressbook_multiget.href {
|
||||||
let mut path = Path::new(href.as_str());
|
if let Some(filename) = href.strip_prefix(path) {
|
||||||
if !resource_def.capture_match_info(&mut path) {
|
if let Some(object_id) = filename.strip_suffix(".vcf") {
|
||||||
not_found.push(href.to_owned());
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let object_id = path.get("object_id").unwrap();
|
|
||||||
match store
|
match store
|
||||||
.get_object(principal, addressbook_id, object_id, false)
|
.get_object(principal, addressbook_id, object_id, false)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(object) => result.push(object),
|
Ok(object) => result.push(object),
|
||||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
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()),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
not_found.push(href.to_owned());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
not_found.push(href.to_owned());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((result, not_found))
|
Ok((result, not_found))
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
use crate::{CardDavPrincipalUri, Error, address_object::resource::AddressObjectPropWrapperName};
|
use crate::{
|
||||||
use actix_web::{
|
CardDavPrincipalUri, Error, address_object::resource::AddressObjectPropWrapperName,
|
||||||
HttpRequest, Responder,
|
addressbook::resource::AddressbookResourceService,
|
||||||
web::{Data, Path},
|
|
||||||
};
|
};
|
||||||
use addressbook_multiget::{AddressbookMultigetRequest, handle_addressbook_multiget};
|
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_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 rustical_xml::{XmlDeserialize, XmlDocument};
|
||||||
use sync_collection::handle_sync_collection;
|
use sync_collection::handle_sync_collection;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
@@ -30,16 +34,15 @@ impl ReportRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(req, addr_store))]
|
#[instrument(skip(addr_store))]
|
||||||
pub async fn route_report_addressbook<AS: AddressbookStore>(
|
pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
|
||||||
path: Path<(String, String)>,
|
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||||
body: String,
|
|
||||||
user: User,
|
user: User,
|
||||||
req: HttpRequest,
|
OriginalUri(uri): OriginalUri,
|
||||||
puri: Data<CardDavPrincipalUri>,
|
Extension(puri): Extension<CardDavPrincipalUri>,
|
||||||
addr_store: Data<AS>,
|
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||||
) -> Result<impl Responder, Error> {
|
body: String,
|
||||||
let (principal, addressbook_id) = path.into_inner();
|
) -> Result<impl IntoResponse, Error> {
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
return Err(Error::Unauthorized);
|
return Err(Error::Unauthorized);
|
||||||
}
|
}
|
||||||
@@ -51,8 +54,8 @@ pub async fn route_report_addressbook<AS: AddressbookStore>(
|
|||||||
handle_addressbook_multiget(
|
handle_addressbook_multiget(
|
||||||
addr_multiget,
|
addr_multiget,
|
||||||
request.props(),
|
request.props(),
|
||||||
req.path(),
|
uri.path(),
|
||||||
puri.as_ref(),
|
&puri,
|
||||||
&user,
|
&user,
|
||||||
&principal,
|
&principal,
|
||||||
&addressbook_id,
|
&addressbook_id,
|
||||||
@@ -63,8 +66,8 @@ pub async fn route_report_addressbook<AS: AddressbookStore>(
|
|||||||
ReportRequest::SyncCollection(sync_collection) => {
|
ReportRequest::SyncCollection(sync_collection) => {
|
||||||
handle_sync_collection(
|
handle_sync_collection(
|
||||||
sync_collection,
|
sync_collection,
|
||||||
req.path(),
|
uri.path(),
|
||||||
puri.as_ref(),
|
&puri,
|
||||||
&user,
|
&user,
|
||||||
&principal,
|
&principal,
|
||||||
&addressbook_id,
|
&addressbook_id,
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
use super::methods::mkcol::route_mkcol;
|
use super::methods::mkcol::route_mkcol;
|
||||||
use super::methods::post::route_post;
|
|
||||||
use super::methods::report::route_report_addressbook;
|
use super::methods::report::route_report_addressbook;
|
||||||
use super::prop::{SupportedAddressData, SupportedReportSet};
|
use super::prop::{SupportedAddressData, SupportedReportSet};
|
||||||
use crate::address_object::resource::{AddressObjectResource, AddressObjectResourceService};
|
use crate::address_object::resource::AddressObjectResource;
|
||||||
use crate::{CardDavPrincipalUri, Error};
|
use crate::{CardDavPrincipalUri, Error};
|
||||||
use actix_web::http::Method;
|
|
||||||
use actix_web::web;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use axum::extract::Request;
|
||||||
|
use axum::handler::Handler;
|
||||||
|
use axum::response::Response;
|
||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
|
use futures_util::future::BoxFuture;
|
||||||
use rustical_dav::extensions::{
|
use rustical_dav::extensions::{
|
||||||
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
|
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
|
||||||
};
|
};
|
||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
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::xml::{Resourcetype, ResourcetypeInner};
|
||||||
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
|
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
|
||||||
use rustical_store::auth::User;
|
use rustical_store::auth::User;
|
||||||
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore};
|
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore};
|
||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
use std::str::FromStr;
|
use std::convert::Infallible;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tower::Service;
|
||||||
|
|
||||||
pub struct AddressbookResourceService<AS: AddressbookStore, S: SubscriptionStore> {
|
pub struct AddressbookResourceService<AS: AddressbookStore, S: SubscriptionStore> {
|
||||||
pub(crate) addr_store: Arc<AS>,
|
pub(crate) addr_store: Arc<AS>,
|
||||||
@@ -35,6 +37,15 @@ impl<A: AddressbookStore, S: SubscriptionStore> AddressbookResourceService<A, S>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<A: AddressbookStore, S: SubscriptionStore> Clone for AddressbookResourceService<A, S> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
addr_store: self.addr_store.clone(),
|
||||||
|
sub_store: self.sub_store.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||||
#[xml(unit_variants_ident = "AddressbookPropName")]
|
#[xml(unit_variants_ident = "AddressbookPropName")]
|
||||||
pub enum AddressbookProp {
|
pub enum AddressbookProp {
|
||||||
@@ -255,18 +266,20 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
impl<AS: AddressbookStore, S: SubscriptionStore> AxumMethods for AddressbookResourceService<AS, S> {
|
||||||
fn actix_scope(self) -> actix_web::Scope {
|
fn report() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
let mkcol_method = web::method(Method::from_str("MKCOL").unwrap());
|
Some(|state, req| {
|
||||||
let report_method = web::method(Method::from_str("REPORT").unwrap());
|
let mut service = Handler::with_state(route_report_addressbook::<AS, S>, state);
|
||||||
web::scope("/{addressbook_id}")
|
Box::pin(Service::call(&mut service, req))
|
||||||
.service(AddressObjectResourceService::<AS>::new(self.addr_store.clone()).actix_scope())
|
})
|
||||||
.service(
|
}
|
||||||
self.actix_resource()
|
|
||||||
.route(mkcol_method.to(route_mkcol::<AS>))
|
fn mkcol() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||||
.route(report_method.to(route_report_addressbook::<AS>))
|
Some(|state, req| {
|
||||||
.post(route_post::<AS, S>),
|
let mut service = Handler::with_state(route_mkcol::<AS, S>, state);
|
||||||
)
|
Box::pin(Service::call(&mut service, req))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use actix_web::{HttpResponse, http::StatusCode};
|
use axum::response::IntoResponse;
|
||||||
|
use http::StatusCode;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@@ -28,8 +29,8 @@ pub enum Error {
|
|||||||
IcalError(#[from] rustical_ical::Error),
|
IcalError(#[from] rustical_ical::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix_web::ResponseError for Error {
|
impl Error {
|
||||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
pub fn status_code(&self) -> StatusCode {
|
||||||
match self {
|
match self {
|
||||||
Error::StoreError(err) => match err {
|
Error::StoreError(err) => match err {
|
||||||
rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
|
rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
|
||||||
@@ -38,8 +39,7 @@ impl actix_web::ResponseError for Error {
|
|||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
},
|
},
|
||||||
Error::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
Error::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Error::DavError(err) => StatusCode::try_from(err.status_code().as_u16())
|
Error::DavError(err) => err.status_code(),
|
||||||
.expect("Just converting between versions"),
|
|
||||||
Error::Unauthorized => StatusCode::UNAUTHORIZED,
|
Error::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||||
Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
|
Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
|
||||||
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
|
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
@@ -47,12 +47,10 @@ impl actix_web::ResponseError for Error {
|
|||||||
Self::IcalError(err) => err.status_code(),
|
Self::IcalError(err) => err.status_code(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
|
}
|
||||||
error!("Error: {self}");
|
|
||||||
match self {
|
impl IntoResponse for Error {
|
||||||
Error::DavError(err) => err.error_response(),
|
fn into_response(self) -> axum::response::Response {
|
||||||
Error::IcalError(err) => err.error_response(),
|
(self.status_code(), self.to_string()).into_response()
|
||||||
_ => HttpResponse::build(self.status_code()).body(self.to_string()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
use actix_web::{
|
use crate::address_object::resource::AddressObjectResourceService;
|
||||||
HttpResponse,
|
use crate::addressbook::resource::AddressbookResourceService;
|
||||||
body::BoxBody,
|
use axum::{Extension, Router};
|
||||||
dev::{HttpServiceFactory, ServiceResponse},
|
|
||||||
http::{
|
|
||||||
Method, StatusCode,
|
|
||||||
header::{self, HeaderName, HeaderValue},
|
|
||||||
},
|
|
||||||
middleware::{ErrorHandlerResponse, ErrorHandlers},
|
|
||||||
web::Data,
|
|
||||||
};
|
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
use principal::PrincipalResourceService;
|
use principal::PrincipalResourceService;
|
||||||
use rustical_dav::resource::{PrincipalUri, ResourceService};
|
use rustical_dav::resource::{PrincipalUri, ResourceService};
|
||||||
use rustical_dav::resources::RootResourceService;
|
use rustical_dav::resources::RootResourceService;
|
||||||
|
use rustical_store::auth::middleware::AuthenticationLayer;
|
||||||
use rustical_store::{
|
use rustical_store::{
|
||||||
AddressbookStore, SubscriptionStore,
|
AddressbookStore, SubscriptionStore,
|
||||||
auth::{AuthenticationMiddleware, AuthenticationProvider, User},
|
auth::{AuthenticationProvider, User},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
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
|
pub fn carddav_router<AP: AuthenticationProvider, A: AddressbookStore, S: SubscriptionStore>(
|
||||||
/// methods of a resource
|
|
||||||
fn options_handler() -> ErrorHandlers<BoxBody> {
|
|
||||||
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<AP: AuthenticationProvider, A: AddressbookStore, S: SubscriptionStore>(
|
|
||||||
prefix: &'static str,
|
prefix: &'static str,
|
||||||
auth_provider: Arc<AP>,
|
auth_provider: Arc<AP>,
|
||||||
store: Arc<A>,
|
store: Arc<A>,
|
||||||
subscription_store: Arc<S>,
|
subscription_store: Arc<S>,
|
||||||
) -> impl HttpServiceFactory {
|
) -> Router {
|
||||||
RootResourceService::<_, User, CardDavPrincipalUri>::new(
|
let principal_service = PrincipalResourceService::new(
|
||||||
PrincipalResourceService::<_, _, S>::new(
|
|
||||||
store.clone(),
|
store.clone(),
|
||||||
auth_provider.clone(),
|
auth_provider.clone(),
|
||||||
subscription_store.clone(),
|
subscription_store.clone(),
|
||||||
),
|
);
|
||||||
|
Router::new()
|
||||||
|
.route_service(
|
||||||
|
"/",
|
||||||
|
RootResourceService::<_, User, CardDavPrincipalUri>::new(principal_service.clone())
|
||||||
|
.axum_service(),
|
||||||
)
|
)
|
||||||
.actix_scope()
|
.route_service("/principal/{principal}", principal_service.axum_service())
|
||||||
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
.route_service(
|
||||||
.wrap(options_handler())
|
"/principal/{principal}/{addressbook_id}",
|
||||||
.app_data(Data::from(store.clone()))
|
AddressbookResourceService::new(store.clone(), subscription_store.clone())
|
||||||
.app_data(Data::new(CardDavPrincipalUri::new(
|
.axum_service(),
|
||||||
format!("{prefix}/principal").leak(),
|
)
|
||||||
)))
|
.route_service(
|
||||||
// TODO: Add endpoint to delete subscriptions
|
"/principal/{principal}/{addressbook_id}/{object_id}",
|
||||||
|
AddressObjectResourceService::new(store.clone()).axum_service(),
|
||||||
|
)
|
||||||
|
.layer(AuthenticationLayer::new(auth_provider))
|
||||||
|
.layer(Extension(CardDavPrincipalUri(prefix)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use crate::addressbook::resource::{AddressbookResource, AddressbookResourceService};
|
use crate::addressbook::resource::AddressbookResource;
|
||||||
use crate::{CardDavPrincipalUri, Error};
|
use crate::{CardDavPrincipalUri, Error};
|
||||||
use actix_web::web;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
|
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
|
||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
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::xml::{HrefElement, Resourcetype, ResourcetypeInner};
|
||||||
use rustical_store::auth::{AuthenticationProvider, User};
|
use rustical_store::auth::{AuthenticationProvider, User};
|
||||||
use rustical_store::{AddressbookStore, SubscriptionStore};
|
use rustical_store::{AddressbookStore, SubscriptionStore};
|
||||||
@@ -175,16 +174,9 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
|
|||||||
.map(|addressbook| (addressbook.id.to_owned(), addressbook.into()))
|
.map(|addressbook| (addressbook.id.to_owned(), addressbook.into()))
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fn actix_scope(self) -> actix_web::Scope {
|
|
||||||
web::scope("/principal/{principal}")
|
impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> AxumMethods
|
||||||
.service(
|
for PrincipalResourceService<A, AP, S>
|
||||||
AddressbookResourceService::<_, S>::new(
|
{
|
||||||
self.addr_store.clone(),
|
|
||||||
self.sub_store.clone(),
|
|
||||||
)
|
|
||||||
.actix_scope(),
|
|
||||||
)
|
|
||||||
.service(self.actix_resource())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,10 @@ repository.workspace = true
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
actix = ["dep:actix-web", "dep:tracing-actix-web", "dep:http_02"]
|
|
||||||
axum = ["dep:axum", "dep:axum-extra", "dep:tower"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.8", optional = true }
|
axum = { version = "0.8" }
|
||||||
axum-extra = { version = "0.10", optional = true, features = ["typed-header"] }
|
tower = { version = "0.5" }
|
||||||
tower = { version = "0.5", optional = true }
|
axum-extra.workspace = true
|
||||||
|
|
||||||
http_02 = { workspace = true, optional = true }
|
|
||||||
|
|
||||||
rustical_xml.workspace = true
|
rustical_xml.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
@@ -29,6 +24,4 @@ derive_more.workspace = true
|
|||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
actix-web = { workspace = true, optional = true }
|
|
||||||
tracing-actix-web = { workspace = true, optional = true }
|
|
||||||
headers.workspace = true
|
headers.workspace = true
|
||||||
|
|||||||
@@ -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 {
|
impl axum::response::IntoResponse for Error {
|
||||||
fn into_response(self) -> axum::response::Response {
|
fn into_response(self) -> axum::response::Response {
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
#[cfg(feature = "actix")]
|
|
||||||
use actix_web::{HttpRequest, ResponseError};
|
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
use axum::{body::Body, extract::FromRequestParts, response::IntoResponse};
|
use axum::{body::Body, extract::FromRequestParts, response::IntoResponse};
|
||||||
use futures_util::future::{Ready, err, ok};
|
|
||||||
use rustical_xml::{ValueDeserialize, ValueSerialize, XmlError};
|
use rustical_xml::{ValueDeserialize, ValueSerialize, XmlError};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -10,14 +6,6 @@ use thiserror::Error;
|
|||||||
#[error("Invalid Depth header")]
|
#[error("Invalid Depth header")]
|
||||||
pub struct InvalidDepthHeader;
|
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 {
|
impl IntoResponse for InvalidDepthHeader {
|
||||||
fn into_response(self) -> axum::response::Response {
|
fn into_response(self) -> axum::response::Response {
|
||||||
axum::response::Response::builder()
|
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<Result<Self, Self::Error>>;
|
|
||||||
|
|
||||||
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<S: Send + Sync> FromRequestParts<S> for Depth {
|
impl<S: Send + Sync> FromRequestParts<S> for Depth {
|
||||||
type Rejection = InvalidDepthHeader;
|
type Rejection = InvalidDepthHeader;
|
||||||
|
|
||||||
async fn from_request_parts(
|
async fn from_request_parts(
|
||||||
parts: &mut axum::http::request::Parts,
|
parts: &mut axum::http::request::Parts,
|
||||||
state: &S,
|
_state: &S,
|
||||||
) -> Result<Self, Self::Rejection> {
|
) -> Result<Self, Self::Rejection> {
|
||||||
if let Some(depth_header) = parts.headers.get("Depth") {
|
if let Some(depth_header) = parts.headers.get("Depth") {
|
||||||
depth_header.as_bytes().try_into()
|
depth_header.as_bytes().try_into()
|
||||||
|
|||||||
@@ -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;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
#[error("Invalid Overwrite header")]
|
#[error("Invalid Overwrite header")]
|
||||||
pub struct InvalidOverwriteHeader;
|
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)]
|
#[derive(Debug, PartialEq, Default)]
|
||||||
pub enum Overwrite {
|
pub enum Overwrite {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -38,25 +28,3 @@ impl TryFrom<&[u8]> for Overwrite {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "actix")]
|
|
||||||
impl FromRequest for Overwrite {
|
|
||||||
type Error = InvalidOverwriteHeader;
|
|
||||||
type Future = Ready<Result<Self, Self::Error>>;
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ pub mod privileges;
|
|||||||
pub mod resource;
|
pub mod resource;
|
||||||
pub mod resources;
|
pub mod resources;
|
||||||
pub mod xml;
|
pub mod xml;
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
pub trait Principal: std::fmt::Debug + Clone + Send + Sync + 'static {
|
pub trait Principal: std::fmt::Debug + Clone + Send + Sync + 'static {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ use axum::body::Body;
|
|||||||
use futures_util::future::BoxFuture;
|
use futures_util::future::BoxFuture;
|
||||||
use headers::Allow;
|
use headers::Allow;
|
||||||
use http::{Method, Request, Response};
|
use http::{Method, Request, Response};
|
||||||
use std::{convert::Infallible, str::FromStr, sync::Arc};
|
use std::{convert::Infallible, str::FromStr};
|
||||||
|
|
||||||
pub type MethodFunction<Service> =
|
pub type MethodFunction<Service> =
|
||||||
fn(Arc<Service>, Request<Body>) -> BoxFuture<'static, Result<Response<Body>, Infallible>>;
|
fn(Service, Request<Body>) -> BoxFuture<'static, Result<Response<Body>, Infallible>>;
|
||||||
|
|
||||||
pub trait AxumMethods: Sized + Send + Sync + 'static {
|
pub trait AxumMethods: Sized + Send + Sync + 'static {
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::methods::{axum_route_propfind, axum_route_proppatch};
|
|||||||
use crate::resource::{ResourceService, axum_methods::AxumMethods};
|
use crate::resource::{ResourceService, axum_methods::AxumMethods};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
|
extract::FromRequestParts,
|
||||||
handler::Handler,
|
handler::Handler,
|
||||||
http::{Request, Response},
|
http::{Request, Response},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
@@ -9,16 +10,16 @@ use axum::{
|
|||||||
use futures_util::future::BoxFuture;
|
use futures_util::future::BoxFuture;
|
||||||
use headers::HeaderMapExt;
|
use headers::HeaderMapExt;
|
||||||
use http::{HeaderValue, StatusCode};
|
use http::{HeaderValue, StatusCode};
|
||||||
use std::{convert::Infallible, sync::Arc};
|
use std::convert::Infallible;
|
||||||
use tower::Service;
|
use tower::Service;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AxumService<RS: ResourceService + AxumMethods> {
|
pub struct AxumService<RS: ResourceService + AxumMethods> {
|
||||||
resource_service: Arc<RS>,
|
resource_service: RS,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<RS: ResourceService + AxumMethods> AxumService<RS> {
|
impl<RS: ResourceService + AxumMethods> AxumService<RS> {
|
||||||
pub fn new(resource_service: Arc<RS>) -> Self {
|
pub fn new(resource_service: RS) -> Self {
|
||||||
Self { resource_service }
|
Self { resource_service }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +28,7 @@ impl<RS: ResourceService + AxumMethods + Clone + Send + Sync> Service<Request<Bo
|
|||||||
for AxumService<RS>
|
for AxumService<RS>
|
||||||
where
|
where
|
||||||
RS::Error: IntoResponse + Send + Sync + 'static,
|
RS::Error: IntoResponse + Send + Sync + 'static,
|
||||||
|
RS::Principal: FromRequestParts<RS>,
|
||||||
{
|
{
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
type Response = Response<Body>;
|
type Response = Response<Body>;
|
||||||
|
|||||||
@@ -2,77 +2,15 @@ use crate::Error;
|
|||||||
use crate::privileges::UserPrivilege;
|
use crate::privileges::UserPrivilege;
|
||||||
use crate::resource::Resource;
|
use crate::resource::Resource;
|
||||||
use crate::resource::ResourceService;
|
use crate::resource::ResourceService;
|
||||||
#[cfg(feature = "axum")]
|
use axum::extract::{Path, State};
|
||||||
use axum::extract::{Extension, Path, State};
|
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
use axum_extra::TypedHeader;
|
use axum_extra::TypedHeader;
|
||||||
use headers::Header;
|
use headers::{IfMatch, IfNoneMatch};
|
||||||
use headers::{HeaderValue, IfMatch, IfNoneMatch};
|
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
use http::HeaderMap;
|
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<R: ResourceService>(
|
|
||||||
path: actix_web::web::Path<R::PathComponents>,
|
|
||||||
req: actix_web::HttpRequest,
|
|
||||||
principal: R::Principal,
|
|
||||||
resource_service: actix_web::web::Data<R>,
|
|
||||||
root_span: tracing_actix_web::RootSpan,
|
|
||||||
) -> Result<actix_web::HttpResponse, R::Error> {
|
|
||||||
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<R: ResourceService>(
|
pub(crate) async fn axum_route_delete<R: ResourceService>(
|
||||||
Path(path): Path<R::PathComponents>,
|
Path(path): Path<R::PathComponents>,
|
||||||
State(resource_service): State<Arc<R>>,
|
State(resource_service): State<R>,
|
||||||
Extension(principal): Extension<R::Principal>,
|
principal: R::Principal,
|
||||||
if_match: Option<TypedHeader<IfMatch>>,
|
if_match: Option<TypedHeader<IfMatch>>,
|
||||||
if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||||
header_map: HeaderMap,
|
header_map: HeaderMap,
|
||||||
@@ -84,7 +22,7 @@ pub(crate) async fn axum_route_delete<R: ResourceService>(
|
|||||||
route_delete(
|
route_delete(
|
||||||
&path,
|
&path,
|
||||||
&principal,
|
&principal,
|
||||||
resource_service.as_ref(),
|
&resource_service,
|
||||||
no_trash,
|
no_trash,
|
||||||
if_match.map(|hdr| hdr.0),
|
if_match.map(|hdr| hdr.0),
|
||||||
if_none_match.map(|hdr| hdr.0),
|
if_none_match.map(|hdr| hdr.0),
|
||||||
|
|||||||
@@ -2,17 +2,6 @@ mod delete;
|
|||||||
mod propfind;
|
mod propfind;
|
||||||
mod proppatch;
|
mod proppatch;
|
||||||
|
|
||||||
#[cfg(feature = "actix")]
|
|
||||||
pub(crate) use delete::actix_route_delete;
|
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
pub(crate) use delete::axum_route_delete;
|
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;
|
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;
|
pub(crate) use proppatch::axum_route_proppatch;
|
||||||
|
|||||||
@@ -7,47 +7,15 @@ use crate::resource::ResourceService;
|
|||||||
use crate::xml::MultistatusElement;
|
use crate::xml::MultistatusElement;
|
||||||
use crate::xml::PropfindElement;
|
use crate::xml::PropfindElement;
|
||||||
use crate::xml::PropfindType;
|
use crate::xml::PropfindType;
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
use axum::extract::{Extension, OriginalUri, Path, State};
|
use axum::extract::{Extension, OriginalUri, Path, State};
|
||||||
use rustical_xml::PropName;
|
use rustical_xml::PropName;
|
||||||
use rustical_xml::XmlDocument;
|
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<R: ResourceService>(
|
|
||||||
path: ::actix_web::web::Path<R::PathComponents>,
|
|
||||||
body: String,
|
|
||||||
req: ::actix_web::HttpRequest,
|
|
||||||
user: R::Principal,
|
|
||||||
depth: Depth,
|
|
||||||
root_span: tracing_actix_web::RootSpan,
|
|
||||||
resource_service: ::actix_web::web::Data<R>,
|
|
||||||
puri: ::actix_web::web::Data<R::PrincipalUri>,
|
|
||||||
) -> Result<
|
|
||||||
MultistatusElement<<R::Resource as Resource>::Prop, <R::MemberType as Resource>::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<R: ResourceService>(
|
pub(crate) async fn axum_route_propfind<R: ResourceService>(
|
||||||
Path(path): Path<R::PathComponents>,
|
Path(path): Path<R::PathComponents>,
|
||||||
State(resource_service): State<Arc<R>>,
|
State(resource_service): State<R>,
|
||||||
depth: Depth,
|
depth: Depth,
|
||||||
Extension(principal): Extension<R::Principal>,
|
principal: R::Principal,
|
||||||
uri: OriginalUri,
|
uri: OriginalUri,
|
||||||
Extension(puri): Extension<R::PrincipalUri>,
|
Extension(puri): Extension<R::PrincipalUri>,
|
||||||
body: String,
|
body: String,
|
||||||
@@ -61,7 +29,7 @@ pub(crate) async fn axum_route_propfind<R: ResourceService>(
|
|||||||
&body,
|
&body,
|
||||||
&principal,
|
&principal,
|
||||||
&depth,
|
&depth,
|
||||||
resource_service.as_ref(),
|
&resource_service,
|
||||||
&puri,
|
&puri,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -115,7 +83,7 @@ pub(crate) async fn route_propfind<R: ResourceService>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = resource.propfind_typed(path, &propfind_self.prop, puri, &principal)?;
|
let response = resource.propfind_typed(path, &propfind_self.prop, puri, principal)?;
|
||||||
|
|
||||||
Ok(MultistatusElement {
|
Ok(MultistatusElement {
|
||||||
responses: vec![response],
|
responses: vec![response],
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::privileges::UserPrivilege;
|
use crate::privileges::UserPrivilege;
|
||||||
use std::sync::Arc;
|
|
||||||
use crate::resource::Resource;
|
use crate::resource::Resource;
|
||||||
use crate::resource::ResourceService;
|
use crate::resource::ResourceService;
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
use axum::extract::{Extension, OriginalUri, Path, State};
|
|
||||||
use crate::xml::MultistatusElement;
|
use crate::xml::MultistatusElement;
|
||||||
use crate::xml::TagList;
|
use crate::xml::TagList;
|
||||||
use crate::xml::multistatus::{PropstatElement, PropstatWrapper, ResponseElement};
|
use crate::xml::multistatus::{PropstatElement, PropstatWrapper, ResponseElement};
|
||||||
|
use axum::extract::{OriginalUri, Path, State};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use quick_xml::name::Namespace;
|
use quick_xml::name::Namespace;
|
||||||
use rustical_xml::NamespaceOwned;
|
use rustical_xml::NamespaceOwned;
|
||||||
@@ -17,7 +15,6 @@ use rustical_xml::XmlDeserialize;
|
|||||||
use rustical_xml::XmlDocument;
|
use rustical_xml::XmlDocument;
|
||||||
use rustical_xml::XmlRootTag;
|
use rustical_xml::XmlRootTag;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug)]
|
#[derive(XmlDeserialize, Clone, Debug)]
|
||||||
#[xml(untagged)]
|
#[xml(untagged)]
|
||||||
@@ -64,46 +61,14 @@ enum Operation<T: XmlDeserialize> {
|
|||||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||||
struct PropertyupdateElement<T: XmlDeserialize>(#[xml(ty = "untagged", flatten)] Vec<Operation<T>>);
|
struct PropertyupdateElement<T: XmlDeserialize>(#[xml(ty = "untagged", flatten)] Vec<Operation<T>>);
|
||||||
|
|
||||||
#[cfg(feature = "actix")]
|
|
||||||
#[instrument(parent = root_span.id(), skip(path, req, root_span, resource_service))]
|
|
||||||
pub(crate) async fn actix_route_proppatch<R: ResourceService>(
|
|
||||||
path: actix_web::web::Path<R::PathComponents>,
|
|
||||||
body: String,
|
|
||||||
req: actix_web::HttpRequest,
|
|
||||||
principal: R::Principal,
|
|
||||||
root_span: tracing_actix_web::RootSpan,
|
|
||||||
resource_service: actix_web::web::Data<R>,
|
|
||||||
) -> Result<MultistatusElement<String, String>, 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<R: ResourceService>(
|
pub(crate) async fn axum_route_proppatch<R: ResourceService>(
|
||||||
Path(path): Path<R::PathComponents>,
|
Path(path): Path<R::PathComponents>,
|
||||||
State(resource_service): State<Arc<R>>,
|
State(resource_service): State<R>,
|
||||||
Extension(principal): Extension<R::Principal>,
|
principal: R::Principal,
|
||||||
uri: OriginalUri,
|
uri: OriginalUri,
|
||||||
body: String,
|
body: String,
|
||||||
) -> Result<
|
) -> Result<MultistatusElement<String, String>, R::Error> {
|
||||||
MultistatusElement<String, String>,
|
route_proppatch(&path, uri.path(), &body, &principal, &resource_service).await
|
||||||
R::Error,
|
|
||||||
> {
|
|
||||||
route_proppatch(
|
|
||||||
&path,
|
|
||||||
uri.path(),
|
|
||||||
&body,
|
|
||||||
&principal,
|
|
||||||
resource_service.as_ref(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn route_proppatch<R: ResourceService>(
|
pub(crate) async fn route_proppatch<R: ResourceService>(
|
||||||
@@ -118,10 +83,10 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
|
|||||||
// Extract operations
|
// Extract operations
|
||||||
let PropertyupdateElement::<SetPropertyPropWrapperWrapper<<R::Resource as Resource>::Prop>>(
|
let PropertyupdateElement::<SetPropertyPropWrapperWrapper<<R::Resource as Resource>::Prop>>(
|
||||||
operations,
|
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 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) {
|
if !privileges.has(&UserPrivilege::Write) {
|
||||||
return Err(Error::Unauthorized.into());
|
return Err(Error::Unauthorized.into());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,17 +12,13 @@ use rustical_xml::{EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlSe
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
mod axum_methods;
|
mod axum_methods;
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
mod axum_service;
|
mod axum_service;
|
||||||
mod methods;
|
mod methods;
|
||||||
mod principal_uri;
|
mod principal_uri;
|
||||||
mod resource_service;
|
mod resource_service;
|
||||||
|
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
pub use axum_methods::AxumMethods;
|
pub use axum_methods::AxumMethods;
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
pub use axum_service::AxumService;
|
pub use axum_service::AxumService;
|
||||||
pub use principal_uri::PrincipalUri;
|
pub use principal_uri::PrincipalUri;
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
#[cfg(feature = "actix")]
|
|
||||||
use super::methods::{actix_route_delete, actix_route_propfind, actix_route_proppatch};
|
|
||||||
use super::{PrincipalUri, Resource};
|
use super::{PrincipalUri, Resource};
|
||||||
use crate::Principal;
|
use crate::Principal;
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
use crate::resource::{AxumMethods, AxumService};
|
use crate::resource::{AxumMethods, AxumService};
|
||||||
#[cfg(feature = "actix")]
|
|
||||||
use actix_web::{http::Method, web, web::Data};
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{str::FromStr, sync::Arc};
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ResourceService: Sized + Send + Sync + 'static {
|
pub trait ResourceService: Sized + Send + Sync + 'static {
|
||||||
@@ -28,11 +22,6 @@ pub trait ResourceService: Sized + Send + Sync + 'static {
|
|||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn test_get_members(&self, _path: &Self::PathComponents) -> Result<String, Self::Error> {
|
|
||||||
// ) -> Result<Vec<Self::MemberType>, Self::Error> {
|
|
||||||
Ok("asd".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
_path: &Self::PathComponents,
|
_path: &Self::PathComponents,
|
||||||
@@ -54,36 +43,10 @@ pub trait ResourceService: Sized + Send + Sync + 'static {
|
|||||||
Err(crate::Error::Unauthorized.into())
|
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::<Self>),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
web::method(Method::from_str("PROPPATCH").unwrap())
|
|
||||||
.to(actix_route_proppatch::<Self>),
|
|
||||||
)
|
|
||||||
.delete(actix_route_delete::<Self>)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<Self>
|
fn axum_service(self) -> AxumService<Self>
|
||||||
where
|
where
|
||||||
Self: Clone + Send + Sync + AxumMethods,
|
Self: Clone + Send + Sync + AxumMethods,
|
||||||
{
|
{
|
||||||
AxumService::new(Arc::new(self))
|
AxumService::new(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::extensions::{
|
|||||||
CommonPropertiesExtension, CommonPropertiesProp, CommonPropertiesPropName,
|
CommonPropertiesExtension, CommonPropertiesProp, CommonPropertiesPropName,
|
||||||
};
|
};
|
||||||
use crate::privileges::UserPrivilegeSet;
|
use crate::privileges::UserPrivilegeSet;
|
||||||
use crate::resource::{PrincipalUri, Resource, ResourceService};
|
use crate::resource::{AxumMethods, PrincipalUri, Resource, ResourceService};
|
||||||
use crate::xml::{Resourcetype, ResourcetypeInner};
|
use crate::xml::{Resourcetype, ResourcetypeInner};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
@@ -74,15 +74,9 @@ impl<PRS: ResourceService<Principal = P> + Clone, P: Principal, PURI: PrincipalU
|
|||||||
async fn get_resource(&self, _: &()) -> Result<Self::Resource, Self::Error> {
|
async fn get_resource(&self, _: &()) -> Result<Self::Resource, Self::Error> {
|
||||||
Ok(RootResource::<PRS::Resource, P>::default())
|
Ok(RootResource::<PRS::Resource, P>::default())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#[cfg(feature = "actix")]
|
|
||||||
fn actix_scope(self) -> actix_web::Scope
|
impl<PRS: ResourceService<Principal = P> + Clone, P: Principal, PURI: PrincipalUri> AxumMethods
|
||||||
where
|
for RootResourceService<PRS, P, PURI>
|
||||||
Self::Error: actix_web::ResponseError,
|
{
|
||||||
Self::Principal: actix_web::FromRequest,
|
|
||||||
{
|
|
||||||
actix_web::web::scope("")
|
|
||||||
.service(self.0.clone().actix_scope())
|
|
||||||
.service(self.actix_resource())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
use crate::xml::TagList;
|
use crate::xml::TagList;
|
||||||
#[cfg(feature = "actix")]
|
|
||||||
use actix_web::{
|
|
||||||
HttpRequest, HttpResponse, Responder, ResponseError, body::BoxBody, http::header::ContentType,
|
|
||||||
};
|
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use quick_xml::name::Namespace;
|
use quick_xml::name::Namespace;
|
||||||
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
||||||
@@ -108,24 +104,6 @@ impl<T1: XmlSerialize, T2: XmlSerialize> Default for MultistatusElement<T1, T2>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "actix")]
|
|
||||||
impl<T1: XmlSerialize, T2: XmlSerialize> Responder for MultistatusElement<T1, T2> {
|
|
||||||
type Body = BoxBody;
|
|
||||||
|
|
||||||
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
|
|
||||||
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
|
impl<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
|
||||||
for MultistatusElement<T1, T2>
|
for MultistatusElement<T1, T2>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustical_xml.workspace = true
|
rustical_xml.workspace = true
|
||||||
actix-web = { workspace = true }
|
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
quick-xml = { workspace = true }
|
quick-xml = { workspace = true }
|
||||||
@@ -18,7 +17,6 @@ itertools = { workspace = true }
|
|||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
derive_more = { workspace = true }
|
derive_more = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-actix-web = { workspace = true }
|
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
rustical_dav.workspace = true
|
rustical_dav.workspace = true
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ repository.workspace = true
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
tower.workspace = true
|
||||||
|
http.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
askama.workspace = true
|
askama.workspace = true
|
||||||
async-trait.workspace = true
|
|
||||||
askama_web.workspace = true
|
askama_web.workspace = true
|
||||||
actix-session.workspace = true
|
async-trait.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
actix-web.workspace = true
|
|
||||||
rustical_store.workspace = true
|
rustical_store.workspace = true
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
futures-core.workspace = true
|
futures-core.workspace = true
|
||||||
@@ -27,3 +28,5 @@ uuid.workspace = true
|
|||||||
url.workspace = true
|
url.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
rustical_oidc.workspace = true
|
rustical_oidc.workspace = true
|
||||||
|
axum-extra.workspace= true
|
||||||
|
headers.workspace = true
|
||||||
|
|||||||
@@ -1,97 +1,72 @@
|
|||||||
use std::marker::PhantomData;
|
use axum::{
|
||||||
|
RequestExt,
|
||||||
use actix_web::{
|
body::Body,
|
||||||
body::BoxBody,
|
extract::{Path, Request},
|
||||||
dev::{
|
response::{IntoResponse, Response},
|
||||||
HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest, ServiceResponse,
|
|
||||||
},
|
|
||||||
http::{header, Method},
|
|
||||||
HttpResponse,
|
|
||||||
};
|
};
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::BoxFuture;
|
||||||
|
use headers::{ContentType, ETag, HeaderMapExt};
|
||||||
|
use http::{Method, StatusCode};
|
||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
|
use std::{convert::Infallible, marker::PhantomData, str::FromStr};
|
||||||
|
use tower::Service;
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
#[derive(Clone, RustEmbed)]
|
||||||
#[folder = "public/assets"]
|
#[folder = "public/assets"]
|
||||||
pub struct Assets;
|
pub struct Assets;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct EmbedService<E>
|
pub struct EmbedService<E>
|
||||||
where
|
where
|
||||||
E: 'static + RustEmbed,
|
E: 'static + RustEmbed,
|
||||||
{
|
{
|
||||||
_embed: PhantomData<E>,
|
_embed: PhantomData<E>,
|
||||||
prefix: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E> EmbedService<E>
|
impl<E> EmbedService<E>
|
||||||
where
|
where
|
||||||
E: 'static + RustEmbed,
|
E: 'static + RustEmbed,
|
||||||
{
|
{
|
||||||
pub fn new(prefix: String) -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
prefix,
|
|
||||||
_embed: PhantomData,
|
_embed: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E> HttpServiceFactory for EmbedService<E>
|
impl<E> Service<Request> for EmbedService<E>
|
||||||
where
|
where
|
||||||
E: 'static + RustEmbed,
|
E: 'static + RustEmbed,
|
||||||
{
|
{
|
||||||
fn register(self, config: &mut actix_web::dev::AppService) {
|
type Response = Response;
|
||||||
let resource_def = if config.is_root() {
|
type Error = Infallible;
|
||||||
ResourceDef::root_prefix(&self.prefix)
|
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
} else {
|
|
||||||
ResourceDef::prefix(&self.prefix)
|
#[inline]
|
||||||
};
|
fn poll_ready(
|
||||||
config.register_service(resource_def, None, self, None);
|
&mut self,
|
||||||
|
_cx: &mut std::task::Context<'_>,
|
||||||
|
) -> std::task::Poll<Result<(), Self::Error>> {
|
||||||
|
Ok(()).into()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<E> ServiceFactory<ServiceRequest> for EmbedService<E>
|
#[inline]
|
||||||
where
|
fn call(&mut self, mut req: Request) -> Self::Future {
|
||||||
E: 'static + RustEmbed,
|
|
||||||
{
|
|
||||||
type Response = ServiceResponse;
|
|
||||||
type Error = actix_web::Error;
|
|
||||||
type Config = ();
|
|
||||||
type Service = EmbedService<E>;
|
|
||||||
type InitError = ();
|
|
||||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
|
||||||
|
|
||||||
fn new_service(&self, _: ()) -> Self::Future {
|
|
||||||
let prefix = self.prefix.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
Ok(Self {
|
|
||||||
prefix,
|
|
||||||
_embed: PhantomData,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E> Service<ServiceRequest> for EmbedService<E>
|
|
||||||
where
|
|
||||||
E: 'static + RustEmbed,
|
|
||||||
{
|
|
||||||
type Response = ServiceResponse<BoxBody>;
|
|
||||||
type Error = actix_web::Error;
|
|
||||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
|
||||||
|
|
||||||
actix_web::dev::always_ready!();
|
|
||||||
|
|
||||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
if req.method() != Method::GET && req.method() != Method::HEAD {
|
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) => {
|
Some(file) => {
|
||||||
let data = file.data;
|
let data = file.data;
|
||||||
let hash = hex::encode(file.metadata.sha256_hash());
|
let hash = hex::encode(file.metadata.sha256_hash());
|
||||||
|
let etag = format!("\"{hash}\"");
|
||||||
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||||
|
|
||||||
let body = if req.method() == Method::HEAD {
|
let body = if req.method() == Method::HEAD {
|
||||||
@@ -99,14 +74,13 @@ where
|
|||||||
} else {
|
} else {
|
||||||
data
|
data
|
||||||
};
|
};
|
||||||
Ok(req.into_response(
|
let mut res = Response::builder().status(StatusCode::OK);
|
||||||
HttpResponse::Ok()
|
let hdrs = res.headers_mut().unwrap();
|
||||||
.content_type(mime)
|
hdrs.typed_insert(ContentType::from(mime));
|
||||||
.insert_header((header::ETAG, hash))
|
hdrs.typed_insert(ETag::from_str(&etag).unwrap());
|
||||||
.body(body),
|
Ok(res.body(Body::from(body)).unwrap())
|
||||||
))
|
|
||||||
}
|
}
|
||||||
None => Ok(req.into_response(HttpResponse::NotFound())),
|
None => Ok(StatusCode::NOT_FOUND.into_response()),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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::Template;
|
||||||
use askama_web::WebTemplate;
|
// use askama_web::WebTemplate;
|
||||||
use assets::{Assets, EmbedService};
|
// use assets::{Assets, EmbedService};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use axum::{Extension, Router, response::IntoResponse, routing::get};
|
||||||
|
use http::Uri;
|
||||||
use rand::{Rng, distributions::Alphanumeric};
|
use rand::{Rng, distributions::Alphanumeric};
|
||||||
use routes::{
|
use rustical_oidc::OidcConfig;
|
||||||
addressbook::{route_addressbook, route_addressbook_restore},
|
// use routes::{
|
||||||
calendar::{route_calendar, route_calendar_restore},
|
// addressbook::{route_addressbook, route_addressbook_restore},
|
||||||
login::{route_get_login, route_post_login, route_post_logout},
|
// 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, OidcServiceConfig, UserStore, configure_oidc};
|
||||||
use rustical_store::{
|
use rustical_store::{
|
||||||
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
||||||
auth::{AuthenticationMiddleware, AuthenticationProvider, User, user::AppToken},
|
auth::{
|
||||||
|
AuthenticationMiddleware, AuthenticationProvider, User, middleware::AuthenticationLayer,
|
||||||
|
user::AppToken,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -40,6 +33,11 @@ pub const ROUTE_USER_NAMED: &str = "frontend_user_named";
|
|||||||
|
|
||||||
pub use config::{FrontendConfig, generate_frontend_secret};
|
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 {
|
pub fn generate_app_token() -> String {
|
||||||
rand::thread_rng()
|
rand::thread_rng()
|
||||||
.sample_iter(Alphanumeric)
|
.sample_iter(Alphanumeric)
|
||||||
@@ -48,313 +46,312 @@ pub fn generate_app_token() -> String {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template, WebTemplate)]
|
// #[derive(Template, WebTemplate)]
|
||||||
#[template(path = "pages/user.html")]
|
// #[template(path = "pages/user.html")]
|
||||||
struct UserPage {
|
// struct UserPage {
|
||||||
pub user: User,
|
// pub user: User,
|
||||||
pub app_tokens: Vec<AppToken>,
|
// pub app_tokens: Vec<AppToken>,
|
||||||
pub calendars: Vec<Calendar>,
|
// pub calendars: Vec<Calendar>,
|
||||||
pub deleted_calendars: Vec<Calendar>,
|
// pub deleted_calendars: Vec<Calendar>,
|
||||||
pub addressbooks: Vec<Addressbook>,
|
// pub addressbooks: Vec<Addressbook>,
|
||||||
pub deleted_addressbooks: Vec<Addressbook>,
|
// pub deleted_addressbooks: Vec<Addressbook>,
|
||||||
pub is_apple: bool,
|
// pub is_apple: bool,
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
|
// async fn route_user_named<CS: CalendarStore, AS: AddressbookStore, AP: AuthenticationProvider>(
|
||||||
|
// path: Path<String>,
|
||||||
|
// cal_store: Data<CS>,
|
||||||
|
// addr_store: Data<AS>,
|
||||||
|
// auth_provider: Data<AP>,
|
||||||
|
// 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<User>, 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<AP: AuthenticationProvider>(
|
||||||
|
// user: User,
|
||||||
|
// auth_provider: Data<AP>,
|
||||||
|
// path: Path<String>,
|
||||||
|
// Form(PostAppTokenForm { apple, name }): Form<PostAppTokenForm>,
|
||||||
|
// req: HttpRequest,
|
||||||
|
// ) -> Result<HttpResponse, rustical_store::Error> {
|
||||||
|
// 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<AP: AuthenticationProvider>(
|
||||||
|
// user: User,
|
||||||
|
// auth_provider: Data<AP>,
|
||||||
|
// path: Path<(String, String)>,
|
||||||
|
// ) -> Result<Redirect, rustical_store::Error> {
|
||||||
|
// 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<B>(
|
||||||
|
// res: ServiceResponse<B>,
|
||||||
|
// ) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||||
|
// 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#"<!Doctype html>
|
||||||
|
// <html>
|
||||||
|
// <head>
|
||||||
|
// <meta http-equiv="refresh" content="1; url={login_url}" />
|
||||||
|
// </head>
|
||||||
|
// <body>
|
||||||
|
// Unauthorized, redirecting to <a href="{login_url}">login page</a>
|
||||||
|
// </body>
|
||||||
|
// <html>
|
||||||
|
// "#
|
||||||
|
// ));
|
||||||
|
//
|
||||||
|
// 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<impl SessionStore> {
|
||||||
|
// 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<CS: CalendarStore, AS: AddressbookStore, AP: AuthenticationProvider>(
|
pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
|
||||||
path: Path<String>,
|
|
||||||
cal_store: Data<CS>,
|
|
||||||
addr_store: Data<AS>,
|
|
||||||
auth_provider: Data<AP>,
|
|
||||||
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<User>, 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<AP: AuthenticationProvider>(
|
|
||||||
user: User,
|
|
||||||
auth_provider: Data<AP>,
|
|
||||||
path: Path<String>,
|
|
||||||
Form(PostAppTokenForm { apple, name }): Form<PostAppTokenForm>,
|
|
||||||
req: HttpRequest,
|
|
||||||
) -> Result<HttpResponse, rustical_store::Error> {
|
|
||||||
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<AP: AuthenticationProvider>(
|
|
||||||
user: User,
|
|
||||||
auth_provider: Data<AP>,
|
|
||||||
path: Path<(String, String)>,
|
|
||||||
) -> Result<Redirect, rustical_store::Error> {
|
|
||||||
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<B>(
|
|
||||||
res: ServiceResponse<B>,
|
|
||||||
) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
|
||||||
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#"<!Doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="refresh" content="1; url={login_url}" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
Unauthorized, redirecting to <a href="{login_url}">login page</a>
|
|
||||||
</body>
|
|
||||||
<html>
|
|
||||||
"#
|
|
||||||
));
|
|
||||||
|
|
||||||
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<impl SessionStore> {
|
|
||||||
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<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
|
|
||||||
cfg: &mut web::ServiceConfig,
|
|
||||||
auth_provider: Arc<AP>,
|
auth_provider: Arc<AP>,
|
||||||
cal_store: Arc<CS>,
|
cal_store: Arc<CS>,
|
||||||
addr_store: Arc<AS>,
|
addr_store: Arc<AS>,
|
||||||
frontend_config: FrontendConfig,
|
frontend_config: FrontendConfig,
|
||||||
oidc_config: Option<OidcConfig>,
|
oidc_config: Option<OidcConfig>,
|
||||||
) {
|
) -> Router {
|
||||||
let mut scope = web::scope("")
|
let mut router = Router::new().layer(AuthenticationLayer::new(auth_provider.clone()));
|
||||||
.wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
|
router = router
|
||||||
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
.route("/login", get(route_get_login).post(route_post_login::<AP>))
|
||||||
.wrap(session_middleware(frontend_config.secret_key))
|
.route_service("/assets/{*file}", EmbedService::<Assets>::new())
|
||||||
.app_data(Data::from(auth_provider.clone()))
|
.layer(Extension(auth_provider.clone()))
|
||||||
.app_data(Data::from(cal_store.clone()))
|
.layer(Extension(cal_store.clone()))
|
||||||
.app_data(Data::from(addr_store.clone()))
|
.layer(Extension(addr_store.clone()))
|
||||||
.app_data(Data::new(frontend_config.clone()))
|
.layer(Extension(frontend_config.clone()))
|
||||||
.app_data(Data::new(oidc_config.clone()))
|
.layer(Extension(oidc_config.clone()));
|
||||||
.service(EmbedService::<Assets>::new("/assets".to_owned()))
|
// .wrap(session_middleware(frontend_config.secret_key))
|
||||||
.service(web::resource("").route(web::method(Method::GET).to(route_root)))
|
// .service(web::resource("").route(web::method(Method::GET).to(route_root)))
|
||||||
.service(
|
// .service(
|
||||||
web::resource("/user")
|
// web::resource("/user")
|
||||||
.get(route_get_home)
|
// .get(route_get_home)
|
||||||
.name(ROUTE_NAME_HOME),
|
// .name(ROUTE_NAME_HOME),
|
||||||
)
|
// )
|
||||||
.service(
|
// .service(
|
||||||
web::resource("/user/{user}")
|
// web::resource("/user/{user}")
|
||||||
.get(route_user_named::<CS, AS, AP>)
|
// .get(route_user_named::<CS, AS, AP>)
|
||||||
.name(ROUTE_USER_NAMED),
|
// .name(ROUTE_USER_NAMED),
|
||||||
)
|
// )
|
||||||
// App token management
|
// // App token management
|
||||||
.service(web::resource("/user/{user}/app_token").post(route_post_app_token::<AP>))
|
// .service(web::resource("/user/{user}/app_token").post(route_post_app_token::<AP>))
|
||||||
.service(
|
// .service(
|
||||||
// POST because HTML5 forms don't support DELETE method
|
// // POST because HTML5 forms don't support DELETE method
|
||||||
web::resource("/user/{user}/app_token/{id}/delete").post(route_delete_app_token::<AP>),
|
// web::resource("/user/{user}/app_token/{id}/delete").post(route_delete_app_token::<AP>),
|
||||||
)
|
// )
|
||||||
// Calendar
|
// // Calendar
|
||||||
.service(web::resource("/user/{user}/calendar/{calendar}").get(route_calendar::<CS>))
|
// .service(web::resource("/user/{user}/calendar/{calendar}").get(route_calendar::<CS>))
|
||||||
.service(
|
// .service(
|
||||||
web::resource("/user/{user}/calendar/{calendar}/restore")
|
// web::resource("/user/{user}/calendar/{calendar}/restore")
|
||||||
.post(route_calendar_restore::<CS>),
|
// .post(route_calendar_restore::<CS>),
|
||||||
)
|
// )
|
||||||
// Addressbook
|
// // Addressbook
|
||||||
.service(
|
// .service(
|
||||||
web::resource("/user/{user}/addressbook/{addressbook}").get(route_addressbook::<AS>),
|
// web::resource("/user/{user}/addressbook/{addressbook}").get(route_addressbook::<AS>),
|
||||||
)
|
// )
|
||||||
.service(
|
// .service(
|
||||||
web::resource("/user/{user}/addressbook/{addressbook}/restore")
|
// web::resource("/user/{user}/addressbook/{addressbook}/restore")
|
||||||
.post(route_addressbook_restore::<AS>),
|
// .post(route_addressbook_restore::<AS>),
|
||||||
)
|
// )
|
||||||
// Login
|
// // Login
|
||||||
.service(
|
// .service(
|
||||||
web::resource("/login")
|
// web::resource("/login")
|
||||||
.name("frontend_login")
|
// .name("frontend_login")
|
||||||
.get(route_get_login)
|
// .get(route_get_login)
|
||||||
.post(route_post_login::<AP>),
|
// .post(route_post_login::<AP>),
|
||||||
)
|
// )
|
||||||
.service(
|
// .service(
|
||||||
web::resource("/logout")
|
// web::resource("/logout")
|
||||||
.name("frontend_logout")
|
// .name("frontend_logout")
|
||||||
.post(route_post_logout),
|
// .post(route_post_logout),
|
||||||
);
|
// );
|
||||||
|
|
||||||
if let Some(oidc_config) = oidc_config {
|
// if let Some(oidc_config) = oidc_config {
|
||||||
scope = scope.service(web::scope("/login/oidc").configure(|cfg| {
|
// scope = scope.service(web::scope("/login/oidc").configure(|cfg| {
|
||||||
configure_oidc(
|
// configure_oidc(
|
||||||
cfg,
|
// cfg,
|
||||||
oidc_config,
|
// oidc_config,
|
||||||
OidcServiceConfig {
|
// OidcServiceConfig {
|
||||||
default_redirect_route_name: ROUTE_NAME_HOME,
|
// default_redirect_route_name: ROUTE_NAME_HOME,
|
||||||
session_key_user_id: "user",
|
// session_key_user_id: "user",
|
||||||
},
|
// },
|
||||||
Arc::new(OidcUserStore(auth_provider.clone())),
|
// Arc::new(OidcUserStore(auth_provider.clone())),
|
||||||
)
|
// )
|
||||||
}));
|
// }));
|
||||||
}
|
// }
|
||||||
|
|
||||||
cfg.service(scope);
|
router
|
||||||
}
|
|
||||||
|
|
||||||
struct OidcUserStore<AP: AuthenticationProvider>(Arc<AP>);
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
|
|
||||||
type Error = rustical_store::Error;
|
|
||||||
|
|
||||||
async fn user_exists(&self, id: &str) -> Result<bool, Self::Error> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
// struct OidcUserStore<AP: AuthenticationProvider>(Arc<AP>);
|
||||||
|
//
|
||||||
|
// #[async_trait]
|
||||||
|
// impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
|
||||||
|
// type Error = rustical_store::Error;
|
||||||
|
//
|
||||||
|
// async fn user_exists(&self, id: &str) -> Result<bool, Self::Error> {
|
||||||
|
// 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
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
use actix_web::{
|
|
||||||
http::StatusCode,
|
|
||||||
middleware::ErrorHandlers,
|
|
||||||
web::{self, Data, ServiceConfig},
|
|
||||||
};
|
|
||||||
use chrono::{DateTime, Utc};
|
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 rustical_store::auth::{AuthenticationMiddleware, AuthenticationProvider};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
mod routes;
|
// mod routes;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct NextcloudFlow {
|
struct NextcloudFlow {
|
||||||
@@ -47,32 +42,32 @@ pub struct NextcloudFlows {
|
|||||||
flows: RwLock<HashMap<String, NextcloudFlow>>,
|
flows: RwLock<HashMap<String, NextcloudFlow>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::{session_middleware, unauthorized_handler};
|
// use crate::{session_middleware, unauthorized_handler};
|
||||||
|
//
|
||||||
pub fn configure_nextcloud_login<AP: AuthenticationProvider>(
|
// pub fn configure_nextcloud_login<AP: AuthenticationProvider>(
|
||||||
cfg: &mut ServiceConfig,
|
// cfg: &mut ServiceConfig,
|
||||||
nextcloud_flows_state: Arc<NextcloudFlows>,
|
// nextcloud_flows_state: Arc<NextcloudFlows>,
|
||||||
auth_provider: Arc<AP>,
|
// auth_provider: Arc<AP>,
|
||||||
frontend_secret: [u8; 64],
|
// frontend_secret: [u8; 64],
|
||||||
) {
|
// ) {
|
||||||
cfg.service(
|
// cfg.service(
|
||||||
web::scope("/index.php/login/v2")
|
// web::scope("/index.php/login/v2")
|
||||||
.wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
|
// .wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
|
||||||
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
// .wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||||
.wrap(session_middleware(frontend_secret))
|
// .wrap(session_middleware(frontend_secret))
|
||||||
.app_data(Data::from(nextcloud_flows_state))
|
// .app_data(Data::from(nextcloud_flows_state))
|
||||||
.app_data(Data::from(auth_provider.clone()))
|
// .app_data(Data::from(auth_provider.clone()))
|
||||||
.service(web::resource("").post(post_nextcloud_login))
|
// .service(web::resource("").post(post_nextcloud_login))
|
||||||
.service(
|
// .service(
|
||||||
web::resource("/poll/{flow}")
|
// web::resource("/poll/{flow}")
|
||||||
.name("nc_login_poll")
|
// .name("nc_login_poll")
|
||||||
.post(post_nextcloud_poll::<AP>),
|
// .post(post_nextcloud_poll::<AP>),
|
||||||
)
|
// )
|
||||||
.service(
|
// .service(
|
||||||
web::resource("/flow/{flow}")
|
// web::resource("/flow/{flow}")
|
||||||
.name("nc_login_flow")
|
// .name("nc_login_flow")
|
||||||
.get(get_nextcloud_flow)
|
// .get(get_nextcloud_flow)
|
||||||
.post(post_nextcloud_flow),
|
// .post(post_nextcloud_flow),
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{FrontendConfig, OidcConfig};
|
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::Template;
|
||||||
use askama_web::WebTemplate;
|
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 rustical_store::auth::AuthenticationProvider;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
@@ -30,29 +31,28 @@ pub struct GetLoginQuery {
|
|||||||
redirect_uri: Option<String>,
|
redirect_uri: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(req, config, oidc_config))]
|
#[instrument(skip(config, oidc_config))]
|
||||||
pub async fn route_get_login(
|
pub async fn route_get_login(
|
||||||
Query(GetLoginQuery { redirect_uri }): Query<GetLoginQuery>,
|
Query(GetLoginQuery { redirect_uri }): Query<GetLoginQuery>,
|
||||||
req: HttpRequest,
|
Extension(config): Extension<FrontendConfig>,
|
||||||
config: Data<FrontendConfig>,
|
Extension(oidc_config): Extension<Option<OidcConfig>>,
|
||||||
oidc_config: Data<Option<OidcConfig>>,
|
) -> Response {
|
||||||
) -> HttpResponse {
|
// let oidc_data = oidc_config
|
||||||
let oidc_data = oidc_config
|
// .as_ref()
|
||||||
.as_ref()
|
// .as_ref()
|
||||||
.as_ref()
|
// .map(|oidc_config| OidcProviderData {
|
||||||
.map(|oidc_config| OidcProviderData {
|
// name: &oidc_config.name,
|
||||||
name: &oidc_config.name,
|
// redirect_url: req
|
||||||
redirect_url: req
|
// .url_for_static(ROUTE_NAME_OIDC_LOGIN)
|
||||||
.url_for_static(ROUTE_NAME_OIDC_LOGIN)
|
// .unwrap()
|
||||||
.unwrap()
|
// .to_string(),
|
||||||
.to_string(),
|
// });
|
||||||
});
|
|
||||||
LoginPage {
|
LoginPage {
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
allow_password_login: config.allow_password_login,
|
allow_password_login: config.allow_password_login,
|
||||||
oidc_data,
|
oidc_data: None,
|
||||||
}
|
}
|
||||||
.respond_to(&req)
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -62,43 +62,38 @@ pub struct PostLoginForm {
|
|||||||
redirect_uri: Option<String>,
|
redirect_uri: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(req, password, auth_provider, session, config))]
|
// #[instrument(skip(password, auth_provider, config))]
|
||||||
pub async fn route_post_login<AP: AuthenticationProvider>(
|
pub async fn route_post_login<AP: AuthenticationProvider>(
|
||||||
req: HttpRequest,
|
Extension(auth_provider): Extension<Arc<AP>>,
|
||||||
|
Extension(config): Extension<FrontendConfig>,
|
||||||
|
OriginalUri(orig_uri): OriginalUri,
|
||||||
Form(PostLoginForm {
|
Form(PostLoginForm {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
}): Form<PostLoginForm>,
|
}): Form<PostLoginForm>,
|
||||||
session: Session,
|
) -> Response {
|
||||||
auth_provider: Data<AP>,
|
|
||||||
config: Data<FrontendConfig>,
|
|
||||||
) -> HttpResponse {
|
|
||||||
if !config.allow_password_login {
|
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
|
// Ensure that redirect_uri never goes cross-origin
|
||||||
let default_redirect = "/frontend/user".to_string();
|
let default_redirect = "/frontend/user".to_string();
|
||||||
let redirect_uri = redirect_uri.unwrap_or(default_redirect.clone());
|
let redirect_uri = redirect_uri.unwrap_or(default_redirect.clone());
|
||||||
let redirect_uri = req
|
// let redirect_uri = orig_uri
|
||||||
.full_url()
|
// .join(&redirect_uri)
|
||||||
.join(&redirect_uri)
|
// .ok()
|
||||||
.ok()
|
// .and_then(|uri| orig_uri.make_relative(&uri))
|
||||||
.and_then(|uri| req.full_url().make_relative(&uri))
|
// .unwrap_or(default_redirect);
|
||||||
.unwrap_or(default_redirect);
|
|
||||||
|
|
||||||
if let Ok(Some(user)) = auth_provider.validate_password(&username, &password).await {
|
if let Ok(Some(user)) = auth_provider.validate_password(&username, &password).await {
|
||||||
session.insert("user", user.id).unwrap();
|
// session.insert("user", user.id).unwrap();
|
||||||
Redirect::to(redirect_uri)
|
Redirect::to(&redirect_uri).into_response()
|
||||||
.see_other()
|
|
||||||
.respond_to(&req)
|
|
||||||
.map_into_boxed_body()
|
|
||||||
} else {
|
} else {
|
||||||
ErrorUnauthorized("Unauthorized").error_response()
|
StatusCode::UNAUTHORIZED.into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn route_post_logout(req: HttpRequest, session: Session) -> Redirect {
|
pub async fn route_post_logout() -> Redirect {
|
||||||
session.remove("user");
|
// session.remove("user");
|
||||||
Redirect::to(req.url_for_static("frontend_login").unwrap().to_string()).see_other()
|
Redirect::to("/")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
pub mod addressbook;
|
// pub mod addressbook;
|
||||||
pub mod calendar;
|
// pub mod calendar;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ edition.workspace = true
|
|||||||
description.workspace = true
|
description.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
actix = ["dep:actix-web"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
@@ -20,4 +18,4 @@ regex.workspace = true
|
|||||||
rrule.workspace = true
|
rrule.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
actix-web = { workspace = true, optional = true }
|
axum.workspace = true
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use axum::{http::StatusCode, response::IntoResponse};
|
||||||
|
|
||||||
use crate::CalDateTimeError;
|
use crate::CalDateTimeError;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@@ -21,15 +23,18 @@ pub enum Error {
|
|||||||
RRuleError(#[from] rrule::RRuleError),
|
RRuleError(#[from] rrule::RRuleError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "actix")]
|
impl Error {
|
||||||
impl actix_web::ResponseError for Error {
|
pub fn status_code(&self) -> StatusCode {
|
||||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
|
||||||
match self {
|
match self {
|
||||||
Self::InvalidData(_) => actix_web::http::StatusCode::BAD_REQUEST,
|
Self::InvalidData(_) => StatusCode::BAD_REQUEST,
|
||||||
Self::MissingCalendar | Self::MissingContact => {
|
Self::MissingCalendar | Self::MissingContact => StatusCode::BAD_REQUEST,
|
||||||
actix_web::http::StatusCode::BAD_REQUEST
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
}
|
|
||||||
_ => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Error {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
(self.status_code(), self.to_string()).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ repository.workspace = true
|
|||||||
openidconnect.workspace = true
|
openidconnect.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
actix-web.workspace = true
|
|
||||||
actix-session.workspace = true
|
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
axum_session = "0.16"
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use actix_session::SessionInsertError;
|
use axum::http::StatusCode;
|
||||||
use actix_web::{
|
use axum::response::IntoResponse;
|
||||||
HttpResponse, ResponseError, body::BoxBody, error::UrlGenerationError, http::StatusCode,
|
|
||||||
};
|
|
||||||
use openidconnect::{ClaimsVerificationError, ConfigurationError, url::ParseError};
|
use openidconnect::{ClaimsVerificationError, ConfigurationError, url::ParseError};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@@ -9,28 +7,18 @@ pub enum OidcError {
|
|||||||
#[error("Cannot generate redirect url, something's not configured correctly")]
|
#[error("Cannot generate redirect url, something's not configured correctly")]
|
||||||
OidcParseError(#[from] ParseError),
|
OidcParseError(#[from] ParseError),
|
||||||
|
|
||||||
#[error("Cannot generate redirect url, something's not configured correctly")]
|
|
||||||
ActixUrlGenerationError(#[from] UrlGenerationError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
OidcConfigurationError(#[from] ConfigurationError),
|
OidcConfigurationError(#[from] ConfigurationError),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
OidcClaimsVerificationError(#[from] ClaimsVerificationError),
|
OidcClaimsVerificationError(#[from] ClaimsVerificationError),
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
SessionInsertError(#[from] SessionInsertError),
|
|
||||||
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Other(&'static str),
|
Other(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for OidcError {
|
impl IntoResponse for OidcError {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn into_response(self) -> axum::response::Response {
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
|
||||||
}
|
|
||||||
|
|
||||||
fn error_response(&self) -> HttpResponse<BoxBody> {
|
|
||||||
HttpResponse::build(self.status_code()).body(self.to_string())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use actix_session::Session;
|
use axum::{
|
||||||
use actix_web::{
|
Extension, Form,
|
||||||
HttpRequest, HttpResponse, Responder, ResponseError,
|
extract::{Query, Request, State},
|
||||||
http::StatusCode,
|
response::{IntoResponse, Redirect, Response},
|
||||||
web::{self, Data, Form, Query, Redirect, ServiceConfig},
|
|
||||||
};
|
};
|
||||||
pub use config::OidcConfig;
|
pub use config::OidcConfig;
|
||||||
use config::UserIdClaim;
|
use config::UserIdClaim;
|
||||||
@@ -93,47 +92,44 @@ pub struct GetOidcForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Endpoint that redirects to the authorize endpoint of the OIDC service
|
/// Endpoint that redirects to the authorize endpoint of the OIDC service
|
||||||
pub async fn route_post_oidc(
|
// pub async fn route_post_oidc(
|
||||||
req: HttpRequest,
|
// Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>,
|
||||||
Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>,
|
// State(oidc_config): State<OidcConfig>,
|
||||||
oidc_config: Data<OidcConfig>,
|
// // session: Session,
|
||||||
session: Session,
|
// req: Request,
|
||||||
) -> Result<HttpResponse, OidcError> {
|
// ) -> Result<Response, OidcError> {
|
||||||
let http_client = get_http_client();
|
// let http_client = get_http_client();
|
||||||
let oidc_client = get_oidc_client(
|
// let oidc_client = get_oidc_client(
|
||||||
oidc_config.as_ref().clone(),
|
// oidc_config.clone(),
|
||||||
&http_client,
|
// &http_client,
|
||||||
RedirectUrl::new(req.url_for_static(ROUTE_NAME_OIDC_CALLBACK)?.to_string())?,
|
// RedirectUrl::new(req.url_for_static(ROUTE_NAME_OIDC_CALLBACK)?.to_string())?,
|
||||||
)
|
// )
|
||||||
.await?;
|
// .await?;
|
||||||
|
//
|
||||||
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
// let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||||
|
//
|
||||||
let (auth_url, csrf_token, nonce) = oidc_client
|
// let (auth_url, csrf_token, nonce) = oidc_client
|
||||||
.authorize_url(
|
// .authorize_url(
|
||||||
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
|
// AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
|
||||||
CsrfToken::new_random,
|
// CsrfToken::new_random,
|
||||||
Nonce::new_random,
|
// Nonce::new_random,
|
||||||
)
|
// )
|
||||||
.add_scopes(oidc_config.scopes.clone())
|
// .add_scopes(oidc_config.scopes.clone())
|
||||||
.set_pkce_challenge(pkce_challenge)
|
// .set_pkce_challenge(pkce_challenge)
|
||||||
.url();
|
// .url();
|
||||||
|
//
|
||||||
session.insert(
|
// // session.insert(
|
||||||
SESSION_KEY_OIDC_STATE,
|
// // SESSION_KEY_OIDC_STATE,
|
||||||
OidcState {
|
// // OidcState {
|
||||||
state: csrf_token,
|
// // state: csrf_token,
|
||||||
nonce,
|
// // nonce,
|
||||||
pkce_verifier,
|
// // pkce_verifier,
|
||||||
redirect_uri,
|
// // redirect_uri,
|
||||||
},
|
// // },
|
||||||
)?;
|
// // )?;
|
||||||
|
//
|
||||||
Ok(Redirect::to(auth_url.to_string())
|
// Ok(Redirect::to(auth_url.as_str()).into_response())
|
||||||
.see_other()
|
// }
|
||||||
.respond_to(&req)
|
|
||||||
.map_into_boxed_body())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct AuthCallbackQuery {
|
pub struct AuthCallbackQuery {
|
||||||
@@ -142,124 +138,124 @@ pub struct AuthCallbackQuery {
|
|||||||
state: String,
|
state: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle callback from IdP page
|
// Handle callback from IdP page
|
||||||
pub async fn route_get_oidc_callback<US: UserStore>(
|
// pub async fn route_get_oidc_callback<US: UserStore>(
|
||||||
req: HttpRequest,
|
// Extension(oidc_config): Extension<OidcConfig>,
|
||||||
oidc_config: Data<OidcConfig>,
|
// session: Session,
|
||||||
session: Session,
|
// Extension(user_store): Extension<US>,
|
||||||
user_store: Data<US>,
|
// Query(AuthCallbackQuery { code, iss, state }): Query<AuthCallbackQuery>,
|
||||||
Query(AuthCallbackQuery { code, iss, state }): Query<AuthCallbackQuery>,
|
// State(service_config): State<OidcServiceConfig>,
|
||||||
service_config: Data<OidcServiceConfig>,
|
// req: Request,
|
||||||
) -> Result<HttpResponse, OidcError> {
|
// ) -> Result<Response, OidcError> {
|
||||||
assert_eq!(iss, oidc_config.issuer);
|
// assert_eq!(iss, oidc_config.issuer);
|
||||||
let oidc_state = session
|
// let oidc_state = session
|
||||||
.remove_as::<OidcState>(SESSION_KEY_OIDC_STATE)
|
// .remove_as::<OidcState>(SESSION_KEY_OIDC_STATE)
|
||||||
.ok_or(OidcError::Other("No local OIDC state"))?
|
// .ok_or(OidcError::Other("No local OIDC state"))?
|
||||||
.map_err(|_| OidcError::Other("Error parsing 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<GroupAdditionalClaims, CoreGenderClaim> = 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);
|
// pub fn configure_oidc<US: UserStore>(
|
||||||
|
// cfg: &mut ServiceConfig,
|
||||||
let http_client = get_http_client();
|
// oidc_config: OidcConfig,
|
||||||
let oidc_client = get_oidc_client(
|
// service_config: OidcServiceConfig,
|
||||||
oidc_config.get_ref().clone(),
|
// user_store: Arc<US>,
|
||||||
&http_client,
|
// ) {
|
||||||
RedirectUrl::new(req.url_for_static(ROUTE_NAME_OIDC_CALLBACK)?.to_string())?,
|
// cfg.app_data(Data::new(oidc_config))
|
||||||
)
|
// .app_data(Data::new(service_config))
|
||||||
.await?;
|
// .app_data(Data::from(user_store))
|
||||||
|
// .service(
|
||||||
let token_response = oidc_client
|
// web::resource("")
|
||||||
.exchange_code(code)?
|
// .name(ROUTE_NAME_OIDC_LOGIN)
|
||||||
.set_pkce_verifier(oidc_state.pkce_verifier)
|
// .post(route_post_oidc),
|
||||||
.request_async(&http_client)
|
// )
|
||||||
.await
|
// .service(
|
||||||
.map_err(|_| OidcError::Other("Error requesting token"))?;
|
// web::resource("/callback")
|
||||||
let id_claims = token_response
|
// .name(ROUTE_NAME_OIDC_CALLBACK)
|
||||||
.id_token()
|
// .get(route_get_oidc_callback::<US>),
|
||||||
.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<GroupAdditionalClaims, CoreGenderClaim> = 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<US: UserStore>(
|
|
||||||
cfg: &mut ServiceConfig,
|
|
||||||
oidc_config: OidcConfig,
|
|
||||||
service_config: OidcServiceConfig,
|
|
||||||
user_store: Arc<US>,
|
|
||||||
) {
|
|
||||||
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::<US>),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use actix_web::ResponseError;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait UserStore: 'static {
|
pub trait UserStore: 'static + Send {
|
||||||
type Error: ResponseError;
|
type Error: IntoResponse;
|
||||||
|
|
||||||
async fn user_exists(&self, id: &str) -> Result<bool, Self::Error>;
|
async fn user_exists(&self, id: &str) -> Result<bool, Self::Error>;
|
||||||
async fn insert_user(&self, id: &str) -> Result<(), Self::Error>;
|
async fn insert_user(&self, id: &str) -> Result<(), Self::Error>;
|
||||||
|
|||||||
@@ -16,12 +16,9 @@ chrono = { workspace = true }
|
|||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
actix-web = { workspace = true }
|
|
||||||
actix-session = { workspace = true }
|
|
||||||
actix-web-httpauth = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
chrono-tz = { workspace = true }
|
chrono-tz = { workspace = true }
|
||||||
derive_more = { workspace = true }
|
derive_more = { workspace = true, features = ["as_ref"] }
|
||||||
rustical_xml.workspace = true
|
rustical_xml.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
@@ -29,7 +26,12 @@ uuid.workspace = true
|
|||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
rustical_dav.workspace = true
|
rustical_dav.workspace = true
|
||||||
rustical_ical.workspace = true
|
rustical_ical.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
http.workspace = true
|
||||||
rrule.workspace = true
|
rrule.workspace = true
|
||||||
|
headers.workspace = true
|
||||||
|
tower.workspace = true
|
||||||
|
futures-core.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = { workspace = true }
|
rstest = { workspace = true }
|
||||||
|
|||||||
@@ -1,103 +1,91 @@
|
|||||||
use super::AuthenticationProvider;
|
use super::AuthenticationProvider;
|
||||||
use actix_session::Session;
|
use axum::{extract::Request, response::Response};
|
||||||
use actix_web::{
|
use futures_core::future::BoxFuture;
|
||||||
FromRequest, HttpMessage,
|
use headers::{Authorization, HeaderMapExt, authorization::Basic};
|
||||||
dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready},
|
|
||||||
http::header::Header,
|
|
||||||
};
|
|
||||||
use actix_web_httpauth::headers::authorization::{Authorization, Basic};
|
|
||||||
use std::{
|
use std::{
|
||||||
future::{Future, Ready, ready},
|
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
use tower::{Layer, Service};
|
||||||
use tracing::{Instrument, info_span};
|
use tracing::{Instrument, info_span};
|
||||||
|
|
||||||
pub struct AuthenticationMiddleware<AP: AuthenticationProvider> {
|
pub struct AuthenticationLayer<AP: AuthenticationProvider> {
|
||||||
auth_provider: Arc<AP>,
|
auth_provider: Arc<AP>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<AP: AuthenticationProvider> AuthenticationMiddleware<AP> {
|
impl<AP: AuthenticationProvider> Clone for AuthenticationLayer<AP> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
auth_provider: self.auth_provider.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<AP: AuthenticationProvider> AuthenticationLayer<AP> {
|
||||||
pub fn new(auth_provider: Arc<AP>) -> Self {
|
pub fn new(auth_provider: Arc<AP>) -> Self {
|
||||||
Self { auth_provider }
|
Self { auth_provider }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<AP: AuthenticationProvider, S, B> Transform<S, ServiceRequest> for AuthenticationMiddleware<AP>
|
impl<S, AP: AuthenticationProvider> Layer<S> for AuthenticationLayer<AP> {
|
||||||
where
|
type Service = AuthenticationMiddleware<S, AP>;
|
||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
|
|
||||||
S::Future: 'static,
|
|
||||||
B: 'static,
|
|
||||||
AP: 'static,
|
|
||||||
{
|
|
||||||
type Error = actix_web::Error;
|
|
||||||
type Response = ServiceResponse<B>;
|
|
||||||
type InitError = ();
|
|
||||||
type Transform = InnerAuthenticationMiddleware<S, AP>;
|
|
||||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
|
||||||
|
|
||||||
fn new_transform(&self, service: S) -> Self::Future {
|
fn layer(&self, inner: S) -> Self::Service {
|
||||||
ready(Ok(InnerAuthenticationMiddleware {
|
Self::Service {
|
||||||
service: Arc::new(service),
|
inner,
|
||||||
auth_provider: Arc::clone(&self.auth_provider),
|
auth_provider: self.auth_provider.clone(),
|
||||||
}))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct InnerAuthenticationMiddleware<S, AP: AuthenticationProvider> {
|
pub struct AuthenticationMiddleware<S, AP: AuthenticationProvider> {
|
||||||
service: Arc<S>,
|
inner: S,
|
||||||
auth_provider: Arc<AP>,
|
auth_provider: Arc<AP>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, B, AP> Service<ServiceRequest> for InnerAuthenticationMiddleware<S, AP>
|
impl<S: Clone, AP: AuthenticationProvider> Clone for AuthenticationMiddleware<S, AP> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
auth_provider: self.auth_provider.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Clone, AP: AuthenticationProvider> Service<Request> for AuthenticationMiddleware<S, AP>
|
||||||
where
|
where
|
||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
|
S: Service<Request, Response = Response> + Send + 'static,
|
||||||
S::Future: 'static,
|
S::Future: Send + 'static,
|
||||||
AP: AuthenticationProvider,
|
|
||||||
{
|
{
|
||||||
type Response = ServiceResponse<B>;
|
type Response = S::Response;
|
||||||
type Error = actix_web::Error;
|
type Error = S::Error;
|
||||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
|
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
forward_ready!(service);
|
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
self.inner.poll_ready(cx)
|
||||||
|
}
|
||||||
|
|
||||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
fn call(&mut self, mut request: Request) -> Self::Future {
|
||||||
let service = Arc::clone(&self.service);
|
let auth_header: Option<Authorization<Basic>> = request.headers().typed_get();
|
||||||
let auth_provider = Arc::clone(&self.auth_provider);
|
let ap = self.auth_provider.clone();
|
||||||
|
let mut inner = self.inner.clone();
|
||||||
|
|
||||||
|
// request.extensions_mut();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
if let Ok(auth) = Authorization::<Basic>::parse(req.request()) {
|
if let Some(auth) = auth_header {
|
||||||
let user_id = auth.as_ref().user_id();
|
let user_id = auth.username();
|
||||||
if let Some(password) = auth.as_ref().password() {
|
let password = auth.password();
|
||||||
if let Ok(Some(user)) = auth_provider
|
if let Ok(Some(user)) = ap
|
||||||
.validate_app_token(user_id, password)
|
.validate_app_token(user_id, password)
|
||||||
.instrument(info_span!("validate_user_token"))
|
.instrument(info_span!("validate_user_token"))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
req.extensions_mut().insert(user);
|
request.extensions_mut().insert(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
let response = inner.call(request).await?;
|
||||||
|
Ok(response)
|
||||||
// Extract user from session cookie
|
|
||||||
if let Ok(session) = Session::extract(req.request()).await {
|
|
||||||
match session.get::<String>("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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
use actix_web::{
|
use axum::{
|
||||||
FromRequest, HttpMessage, HttpResponse, ResponseError,
|
body::Body,
|
||||||
body::BoxBody,
|
extract::FromRequestParts,
|
||||||
http::{StatusCode, header},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
|
use http::{HeaderValue, StatusCode, header};
|
||||||
use rustical_xml::ValueSerialize;
|
use rustical_xml::ValueSerialize;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::fmt::Display;
|
||||||
fmt::Display,
|
|
||||||
future::{Ready, ready},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::Secret;
|
use crate::Secret;
|
||||||
|
|
||||||
@@ -121,33 +119,28 @@ impl rustical_dav::Principal for User {
|
|||||||
#[derive(Clone, Debug, Display)]
|
#[derive(Clone, Debug, Display)]
|
||||||
pub struct UnauthorizedError;
|
pub struct UnauthorizedError;
|
||||||
|
|
||||||
impl ResponseError for UnauthorizedError {
|
impl IntoResponse for UnauthorizedError {
|
||||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
fn into_response(self) -> axum::response::Response {
|
||||||
StatusCode::UNAUTHORIZED
|
let mut resp = Response::builder().status(StatusCode::UNAUTHORIZED);
|
||||||
}
|
resp.headers_mut().unwrap().insert(
|
||||||
fn error_response(&self) -> HttpResponse<BoxBody> {
|
|
||||||
HttpResponse::build(StatusCode::UNAUTHORIZED)
|
|
||||||
.insert_header((
|
|
||||||
header::WWW_AUTHENTICATE,
|
header::WWW_AUTHENTICATE,
|
||||||
r#"Basic realm="RustiCal", charset="UTF-8""#,
|
HeaderValue::from_static(r#"Basic realm="RustiCal", charset="UTF-8""#),
|
||||||
))
|
);
|
||||||
.finish()
|
resp.body(Body::empty()).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromRequest for User {
|
impl<S: Send + Sync + Clone> FromRequestParts<S> for User {
|
||||||
type Error = UnauthorizedError;
|
type Rejection = UnauthorizedError;
|
||||||
type Future = Ready<Result<Self, Self::Error>>;
|
|
||||||
|
|
||||||
fn from_request(
|
async fn from_request_parts(
|
||||||
req: &actix_web::HttpRequest,
|
parts: &mut http::request::Parts,
|
||||||
_payload: &mut actix_web::dev::Payload,
|
_state: &S,
|
||||||
) -> Self::Future {
|
) -> Result<Self, Self::Rejection> {
|
||||||
ready(
|
parts
|
||||||
req.extensions()
|
.extensions
|
||||||
.get::<Self>()
|
.get::<Self>()
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or(UnauthorizedError),
|
.ok_or(UnauthorizedError)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use actix_web::{ResponseError, http::StatusCode};
|
use http::StatusCode;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@@ -27,8 +27,8 @@ pub enum Error {
|
|||||||
IcalError(#[from] rustical_ical::Error),
|
IcalError(#[from] rustical_ical::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for Error {
|
impl Error {
|
||||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
pub fn status_code(&self) -> StatusCode {
|
||||||
match self {
|
match self {
|
||||||
Self::NotFound => StatusCode::NOT_FOUND,
|
Self::NotFound => StatusCode::NOT_FOUND,
|
||||||
Self::AlreadyExists => StatusCode::CONFLICT,
|
Self::AlreadyExists => StatusCode::CONFLICT,
|
||||||
|
|||||||
94
src/app.rs
94
src/app.rs
@@ -1,16 +1,14 @@
|
|||||||
use actix_web::body::MessageBody;
|
use axum::Router;
|
||||||
use actix_web::dev::{ServiceFactory, ServiceRequest, ServiceResponse};
|
use axum::response::Redirect;
|
||||||
use actix_web::middleware::NormalizePath;
|
use axum::routing::get;
|
||||||
use actix_web::{App, web};
|
use rustical_caldav::caldav_router;
|
||||||
use rustical_caldav::caldav_service;
|
use rustical_carddav::carddav_router;
|
||||||
use rustical_carddav::carddav_service;
|
use rustical_frontend::nextcloud_login::NextcloudFlows;
|
||||||
use rustical_frontend::nextcloud_login::{NextcloudFlows, configure_nextcloud_login};
|
use rustical_frontend::{FrontendConfig, frontend_router};
|
||||||
use rustical_frontend::{FrontendConfig, configure_frontend};
|
|
||||||
use rustical_oidc::OidcConfig;
|
use rustical_oidc::OidcConfig;
|
||||||
use rustical_store::auth::AuthenticationProvider;
|
use rustical_store::auth::AuthenticationProvider;
|
||||||
use rustical_store::{AddressbookStore, CalendarStore, SubscriptionStore};
|
use rustical_store::{AddressbookStore, CalendarStore, SubscriptionStore};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing_actix_web::TracingLogger;
|
|
||||||
|
|
||||||
use crate::config::NextcloudLoginConfig;
|
use crate::config::NextcloudLoginConfig;
|
||||||
|
|
||||||
@@ -24,61 +22,61 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
|||||||
oidc_config: Option<OidcConfig>,
|
oidc_config: Option<OidcConfig>,
|
||||||
nextcloud_login_config: NextcloudLoginConfig,
|
nextcloud_login_config: NextcloudLoginConfig,
|
||||||
nextcloud_flows_state: Arc<NextcloudFlows>,
|
nextcloud_flows_state: Arc<NextcloudFlows>,
|
||||||
) -> App<
|
) -> Router {
|
||||||
impl ServiceFactory<
|
let mut router = Router::new()
|
||||||
ServiceRequest,
|
.nest(
|
||||||
Response = ServiceResponse<impl MessageBody>,
|
"/caldav",
|
||||||
Config = (),
|
caldav_router(
|
||||||
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(
|
|
||||||
"/caldav",
|
"/caldav",
|
||||||
auth_provider.clone(),
|
auth_provider.clone(),
|
||||||
cal_store.clone(),
|
cal_store.clone(),
|
||||||
addr_store.clone(),
|
addr_store.clone(),
|
||||||
subscription_store.clone(),
|
subscription_store.clone(),
|
||||||
)))
|
),
|
||||||
.service(web::scope("/carddav").service(carddav_service(
|
)
|
||||||
|
.nest(
|
||||||
|
"/carddav",
|
||||||
|
carddav_router(
|
||||||
"/carddav",
|
"/carddav",
|
||||||
auth_provider.clone(),
|
auth_provider.clone(),
|
||||||
addr_store.clone(),
|
addr_store.clone(),
|
||||||
subscription_store,
|
subscription_store.clone(),
|
||||||
)))
|
),
|
||||||
.service(
|
)
|
||||||
web::scope("/.well-known")
|
.route(
|
||||||
.service(web::redirect("/caldav", "/caldav"))
|
"/.well-known/caldav",
|
||||||
.service(web::redirect("/carddav", "/carddav")),
|
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 {
|
if frontend_config.enabled {
|
||||||
app = app
|
router = router
|
||||||
.service(web::scope("/frontend").configure(|cfg| {
|
.nest(
|
||||||
configure_frontend(
|
"/frontend",
|
||||||
cfg,
|
frontend_router(
|
||||||
auth_provider.clone(),
|
auth_provider.clone(),
|
||||||
cal_store.clone(),
|
cal_store.clone(),
|
||||||
addr_store.clone(),
|
addr_store.clone(),
|
||||||
frontend_config,
|
frontend_config,
|
||||||
oidc_config,
|
oidc_config,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}))
|
.route("/", get(async || Redirect::to("/frontend")));
|
||||||
.service(web::redirect("/", "/frontend").see_other());
|
|
||||||
}
|
}
|
||||||
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,
|
||||||
|
// )
|
||||||
|
// });
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
61
src/main.rs
61
src/main.rs
@@ -1,6 +1,4 @@
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use actix_web::HttpServer;
|
|
||||||
use actix_web::http::KeepAlive;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use app::make_app;
|
use app::make_app;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
@@ -105,8 +103,7 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
let nextcloud_flows = Arc::new(NextcloudFlows::default());
|
let nextcloud_flows = Arc::new(NextcloudFlows::default());
|
||||||
|
|
||||||
HttpServer::new(move || {
|
let app = make_app(
|
||||||
make_app(
|
|
||||||
addr_store.clone(),
|
addr_store.clone(),
|
||||||
cal_store.clone(),
|
cal_store.clone(),
|
||||||
subscription_store.clone(),
|
subscription_store.clone(),
|
||||||
@@ -115,57 +112,15 @@ async fn main() -> Result<()> {
|
|||||||
config.oidc.clone(),
|
config.oidc.clone(),
|
||||||
config.nextcloud_login.clone(),
|
config.nextcloud_login.clone(),
|
||||||
nextcloud_flows.clone(),
|
nextcloud_flows.clone(),
|
||||||
)
|
);
|
||||||
})
|
|
||||||
.bind((config.http.host, config.http.port))?
|
let listener = tokio::net::TcpListener::bind(&format!(
|
||||||
// Workaround for a weird bug where
|
"{}:{}",
|
||||||
// new requests might timeout since they cannot properly reuse the connection
|
config.http.host, config.http.port
|
||||||
// https://github.com/lennart-k/rustical/issues/10
|
))
|
||||||
.keep_alive(KeepAlive::Disabled)
|
|
||||||
.run()
|
|
||||||
.await?;
|
.await?;
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user