42510d94f3
Bring chroot to nextcloud capability: login on Nextcloud via username="{user}~{folder-uuid}
Doing a such login will chroot the folder folder-uuid
If user has several folder as root (parent=None), the login flow will request which
folder user want to chroot
330 lines
12 KiB
Rust
330 lines
12 KiB
Rust
use axum::{
|
|
Router,
|
|
body::Body,
|
|
extract::{Path, State},
|
|
http::{Request, StatusCode},
|
|
middleware,
|
|
response::{IntoResponse, Response},
|
|
routing::{any, delete, get, post},
|
|
};
|
|
use std::sync::Arc;
|
|
|
|
use crate::common::di::AppState;
|
|
use crate::interfaces::middleware::auth::AuthUser;
|
|
use crate::interfaces::middleware::rate_limit::{RateLimiter, rate_limit_login};
|
|
use crate::interfaces::nextcloud::avatar_handler;
|
|
use crate::interfaces::nextcloud::basic_auth_middleware::basic_auth_middleware;
|
|
use crate::interfaces::nextcloud::login_v2_handler;
|
|
use crate::interfaces::nextcloud::ocs_handler;
|
|
use crate::interfaces::nextcloud::preview_handler;
|
|
use crate::interfaces::nextcloud::session::NcSession;
|
|
use crate::interfaces::nextcloud::status_handler;
|
|
use crate::interfaces::nextcloud::trashbin_handler;
|
|
use crate::interfaces::nextcloud::uploads_handler;
|
|
use crate::interfaces::nextcloud::webdav_handler;
|
|
|
|
/// Build Nextcloud routes with a pre-built `Arc<AppState>` for the middleware layer.
|
|
///
|
|
/// This is the preferred entry point — pass the real state so the Basic Auth
|
|
/// middleware can look up app passwords from the database.
|
|
pub fn nextcloud_routes_with_state(state: Arc<AppState>) -> Router<Arc<AppState>> {
|
|
// Rate limiter for NC login submit (reuses auth config values)
|
|
let nc_login_limiter = {
|
|
let rl = &state.core.config.auth.rate_limit;
|
|
Arc::new(RateLimiter::new(
|
|
rl.login_max_requests,
|
|
rl.login_window_secs,
|
|
100_000,
|
|
))
|
|
};
|
|
|
|
// Public routes — no auth required.
|
|
let public = Router::new()
|
|
.route("/status.php", get(status_handler::handle_status))
|
|
// NC connectivity check — app expects 204 to confirm server is reachable.
|
|
.route("/index.php/204", get(handle_connectivity_check))
|
|
// Bare /remote.php/dav — NC clients probe this to confirm WebDAV is available.
|
|
.route("/remote.php/dav", any(handle_dav_discovery))
|
|
.route("/remote.php/dav/", any(handle_dav_discovery))
|
|
.route(
|
|
"/index.php/login/v2",
|
|
post(login_v2_handler::handle_login_initiate),
|
|
)
|
|
.route(
|
|
"/login/v2/flow/{token}",
|
|
get(login_v2_handler::handle_login_page)
|
|
.post(login_v2_handler::handle_login_submit)
|
|
.layer(axum::middleware::from_fn_with_state(
|
|
nc_login_limiter,
|
|
rate_limit_login,
|
|
)),
|
|
)
|
|
// Drive picker submission — finalises a multi-drive flow that
|
|
// paused after password verification. Public route by design:
|
|
// the flow token + single-use `pending_user_id` slot is the
|
|
// proof of authentication. See `login_v2_handler::handle_drive_pick`.
|
|
.route(
|
|
"/login/v2/flow/{token}/drive",
|
|
post(login_v2_handler::handle_drive_pick),
|
|
)
|
|
// OIDC initiation from Nextcloud login page
|
|
.route(
|
|
"/login/v2/flow/{token}/oidc",
|
|
get(login_v2_handler::handle_login_oidc),
|
|
)
|
|
.route(
|
|
"/index.php/login/v2/poll",
|
|
post(login_v2_handler::handle_login_poll),
|
|
)
|
|
.route("/login/v2/poll", post(login_v2_handler::handle_login_poll))
|
|
// Capabilities are public — iOS app fetches them before having credentials.
|
|
.route(
|
|
"/ocs/v1.php/cloud/capabilities",
|
|
get(ocs_handler::handle_capabilities_v1),
|
|
)
|
|
.route(
|
|
"/ocs/v2.php/cloud/capabilities",
|
|
get(ocs_handler::handle_capabilities_v2),
|
|
)
|
|
// Final NC catch-alls. Any `/ocs/*` or `/remote.php/*` URL
|
|
// the routes above don't claim returns 404 here — so it's
|
|
// logged under the `http::nextcloud` access-log target the
|
|
// surrounding `.layer(access_log!(…))` in main.rs assigns,
|
|
// instead of falling through Axum's matcher to ServeDir
|
|
// and being mis-attributed to `http::web`.
|
|
//
|
|
// Concrete example: NC desktop probes
|
|
// `/ocs/v2.php/core/navigation/apps` to discover server
|
|
// features. We don't implement that endpoint; without these
|
|
// catch-alls the 404 was emitted at `http::web`, which is
|
|
// misleading for operators triaging Nextcloud client noise.
|
|
//
|
|
// Mounted on the PUBLIC sub-router (NOT behind basic-auth)
|
|
// so unknown-endpoint probes return 404 regardless of
|
|
// whether the client sent credentials. Moving them into
|
|
// `protected` would turn anonymous probes into 401
|
|
// challenges, which breaks some clients' capability-
|
|
// detection paths.
|
|
//
|
|
// Axum routes more-specific paths first, so the specific
|
|
// NC routes above (and the protected ones below) still
|
|
// claim their requests; only genuinely unmatched paths
|
|
// reach these handlers.
|
|
.route("/ocs/{*rest}", any(handle_nc_not_found))
|
|
.route("/remote.php/{*rest}", any(handle_nc_not_found));
|
|
|
|
// Protected routes — require Basic Auth via app passwords.
|
|
let protected = Router::new()
|
|
// Both v1 and v2 of the singular cloud/user endpoint return the
|
|
// same payload shape — NC's URL-versioning is a transport
|
|
// convention, not a protocol break for this endpoint. Older
|
|
// NC clients (and some third-party libraries) still hit v1
|
|
// first; without this route they get a 404 even though the
|
|
// handler exists.
|
|
.route("/ocs/v1.php/cloud/user", get(ocs_handler::handle_user_info))
|
|
.route("/ocs/v2.php/cloud/user", get(ocs_handler::handle_user_info))
|
|
.route(
|
|
"/ocs/v1.php/cloud/users/{userid}",
|
|
get(ocs_handler::handle_user_provisioning_v1),
|
|
)
|
|
.route(
|
|
"/ocs/v2.php/cloud/users/{userid}",
|
|
get(ocs_handler::handle_user_provisioning_v2),
|
|
)
|
|
.route(
|
|
"/ocs/v2.php/core/apppassword",
|
|
delete(ocs_handler::handle_revoke_apppassword),
|
|
)
|
|
.route(
|
|
"/ocs/v2.php/apps/notifications/api/v2/notifications",
|
|
get(ocs_handler::handle_notifications_list),
|
|
)
|
|
.route(
|
|
"/ocs/v2.php/apps/notifications/api/v2/push",
|
|
post(ocs_handler::handle_notifications_push),
|
|
)
|
|
.route(
|
|
"/ocs/v2.php/apps/recommendations/api/v1/recommendations",
|
|
get(ocs_handler::handle_recommendations),
|
|
)
|
|
.route(
|
|
"/ocs/v2.php/apps/files_sharing/api/v1/sharees",
|
|
get(ocs_handler::handle_sharees_search),
|
|
)
|
|
// Unified Search
|
|
.route(
|
|
"/ocs/v2.php/search/providers",
|
|
get(ocs_handler::handle_search_providers),
|
|
)
|
|
.route(
|
|
"/ocs/v2.php/search/providers/{provider_id}/search",
|
|
get(ocs_handler::handle_search),
|
|
)
|
|
.route(
|
|
"/index.php/core/preview",
|
|
get(preview_handler::handle_preview),
|
|
)
|
|
.route(
|
|
"/index.php/avatar/{user}/{size}",
|
|
get(avatar_handler::handle_avatar),
|
|
)
|
|
// NC desktop + several mobile clients fetch avatars from the
|
|
// DAV-shaped URL (with a literal `.png` extension on the
|
|
// size segment). Same SVG payload, different URL shape — the
|
|
// wrapper handler strips the extension and delegates.
|
|
.route(
|
|
"/remote.php/dav/avatars/{user}/{size}",
|
|
get(avatar_handler::handle_dav_avatar),
|
|
)
|
|
.route(
|
|
"/remote.php/dav/files/{user}/{*subpath}",
|
|
any(handle_dav_files),
|
|
)
|
|
.route("/remote.php/dav/files/{user}/", any(handle_dav_files_root))
|
|
.route("/remote.php/dav/files/{user}", any(handle_dav_files_root))
|
|
.route(
|
|
"/remote.php/dav/uploads/{user}/{upload_id}/{*rest}",
|
|
any(handle_dav_uploads),
|
|
)
|
|
.route(
|
|
"/remote.php/dav/uploads/{user}/{upload_id}",
|
|
any(handle_dav_uploads_root),
|
|
)
|
|
// Trashbin WebDAV
|
|
.route(
|
|
"/remote.php/dav/trashbin/{user}/{*subpath}",
|
|
any(handle_dav_trashbin),
|
|
)
|
|
.route(
|
|
"/remote.php/dav/trashbin/{user}/",
|
|
any(handle_dav_trashbin_root),
|
|
)
|
|
.route(
|
|
"/remote.php/dav/trashbin/{user}",
|
|
any(handle_dav_trashbin_root),
|
|
)
|
|
.route("/remote.php/webdav/{*subpath}", any(handle_legacy_webdav))
|
|
.route("/remote.php/webdav/", any(handle_legacy_webdav_root))
|
|
.route("/remote.php/webdav", any(handle_legacy_webdav_root))
|
|
.layer(middleware::from_fn_with_state(state, basic_auth_middleware));
|
|
|
|
Router::new().merge(public).merge(protected)
|
|
}
|
|
|
|
// ──────────────── Handler glue ────────────────
|
|
|
|
async fn handle_dav_files(
|
|
State(state): State<Arc<AppState>>,
|
|
Path((_url_user, subpath)): Path<(String, String)>,
|
|
session: NcSession,
|
|
req: Request<Body>,
|
|
) -> Result<Response, Response> {
|
|
webdav_handler::handle_nc_webdav(state, req, session, subpath)
|
|
.await
|
|
.map_err(|e| e.into_response())
|
|
}
|
|
|
|
async fn handle_dav_files_root(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(_url_user): Path<String>,
|
|
session: NcSession,
|
|
req: Request<Body>,
|
|
) -> Result<Response, Response> {
|
|
webdav_handler::handle_nc_webdav(state, req, session, String::new())
|
|
.await
|
|
.map_err(|e| e.into_response())
|
|
}
|
|
|
|
async fn handle_dav_uploads(
|
|
State(state): State<Arc<AppState>>,
|
|
Path((_url_user, upload_id, rest)): Path<(String, String, String)>,
|
|
session: NcSession,
|
|
req: Request<Body>,
|
|
) -> Result<Response, Response> {
|
|
uploads_handler::handle_nc_uploads(state, req, session, upload_id, rest)
|
|
.await
|
|
.map_err(|e| e.into_response())
|
|
}
|
|
|
|
async fn handle_dav_uploads_root(
|
|
State(state): State<Arc<AppState>>,
|
|
Path((_url_user, upload_id)): Path<(String, String)>,
|
|
session: NcSession,
|
|
req: Request<Body>,
|
|
) -> Result<Response, Response> {
|
|
uploads_handler::handle_nc_uploads(state, req, session, upload_id, String::new())
|
|
.await
|
|
.map_err(|e| e.into_response())
|
|
}
|
|
|
|
/// Legacy /remote.php/webdav/* — redirect to /remote.php/dav/files/{user}/*
|
|
async fn handle_legacy_webdav(Path(subpath): Path<String>, user_ext: AuthUser) -> Response {
|
|
let location = format!("/remote.php/dav/files/{}/{}", user_ext.username, subpath);
|
|
Response::builder()
|
|
.status(StatusCode::MOVED_PERMANENTLY)
|
|
.header("location", location)
|
|
.body(Body::empty())
|
|
.unwrap()
|
|
}
|
|
|
|
async fn handle_legacy_webdav_root(user_ext: AuthUser) -> Response {
|
|
let location = format!("/remote.php/dav/files/{}/", user_ext.username);
|
|
Response::builder()
|
|
.status(StatusCode::MOVED_PERMANENTLY)
|
|
.header("location", location)
|
|
.body(Body::empty())
|
|
.unwrap()
|
|
}
|
|
|
|
async fn handle_dav_trashbin(
|
|
State(state): State<Arc<AppState>>,
|
|
Path((_url_user, subpath)): Path<(String, String)>,
|
|
session: NcSession,
|
|
req: Request<Body>,
|
|
) -> Result<Response, Response> {
|
|
trashbin_handler::handle_nc_trashbin(state, req, session, subpath)
|
|
.await
|
|
.map_err(|e| e.into_response())
|
|
}
|
|
|
|
async fn handle_dav_trashbin_root(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(_url_user): Path<String>,
|
|
session: NcSession,
|
|
req: Request<Body>,
|
|
) -> Result<Response, Response> {
|
|
trashbin_handler::handle_nc_trashbin(state, req, session, String::new())
|
|
.await
|
|
.map_err(|e| e.into_response())
|
|
}
|
|
|
|
/// `GET /index.php/204` — NC app connectivity check. Returns 204 No Content.
|
|
async fn handle_connectivity_check() -> Response {
|
|
Response::builder()
|
|
.status(StatusCode::NO_CONTENT)
|
|
.body(Body::empty())
|
|
.unwrap()
|
|
}
|
|
|
|
/// Bare `/remote.php/dav` — NC clients (especially Android) probe this endpoint
|
|
/// during server discovery to confirm WebDAV is available.
|
|
async fn handle_dav_discovery() -> Response {
|
|
Response::builder()
|
|
.status(StatusCode::OK)
|
|
.header("DAV", "1, 3")
|
|
.header("Allow", "OPTIONS, GET, HEAD, PROPFIND")
|
|
.body(Body::empty())
|
|
.unwrap()
|
|
}
|
|
|
|
/// Catch-all 404 for any `/ocs/*` or `/remote.php/*` path the NC
|
|
/// router doesn't recognize. Exists purely to anchor the access-log
|
|
/// target — see the comment on the routes above for the operator
|
|
/// rationale.
|
|
async fn handle_nc_not_found() -> Response {
|
|
Response::builder()
|
|
.status(StatusCode::NOT_FOUND)
|
|
.body(Body::empty())
|
|
.unwrap()
|
|
}
|