Skip to main content

parse_rust_server/
lib.rs

1//! Parse Server as a library: router, middleware, config.
2//!
3//! **A library first, with a thin binary on top.** Native Rust triggers require the deployment
4//! to compile its own binary, and adapters are registered through a builder rather than resolved
5//! from a module name, so the primary artifact is something you link against. `parse-rust-cli`
6//! is a separate package rather than a feature of this one: feature unification means a sibling
7//! crate enabling a `cli` feature would pull its dependencies back in even for an embedder that
8//! set `default-features = false`, and a separate package cannot be re-enabled by anyone else's
9//! feature choice.
10//!
11//! Scope today: `/health`, `/serverInfo`, the five `/classes` verbs, and signup, login,
12//! `/users/me` and logout. `GET /serverInfo` was built first because it is the smallest thing
13//! that forces the whole request path into existence: mount path, header parsing, client-key
14//! validation, the master-key gate, and both error envelopes.
15//!
16//! **An embedder that builds the router itself must call [`AppState::ensure_indexes`] first.**
17//! [`serve`] does it for you. Mounting [`router`] into your own axum app does not, and without
18//! those indexes duplicate usernames are accepted silently, which is a data problem rather than
19//! an error anyone sees.
20
21#![forbid(unsafe_code)]
22#![cfg_attr(
23    not(test),
24    deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)
25)]
26
27pub mod auth;
28pub mod body_credentials;
29pub mod config;
30pub mod response;
31pub mod routes;
32pub mod sessions;
33pub mod state;
34
35use std::sync::Arc;
36
37use axum::extract::FromRequestParts;
38use axum::response::{IntoResponse, Response};
39use axum::routing::get;
40use axum::Router;
41
42pub use auth::{Authority, HeaderRejection};
43pub use config::ServerConfig;
44pub use state::AppState;
45
46/// Extract [`Authority`] from request headers.
47///
48/// Implemented as an extractor so a route cannot forget it: a handler that wants to know who is
49/// calling has to name `Authority` in its signature, and one that does not name it cannot
50/// accidentally read a half-validated identity off the request.
51#[axum::async_trait]
52impl<S> FromRequestParts<S> for Authority
53where
54    Arc<ServerConfig>: axum::extract::FromRef<S>,
55    S: Send + Sync,
56{
57    type Rejection = Response;
58
59    async fn from_request_parts(
60        parts: &mut http::request::Parts,
61        state: &S,
62    ) -> Result<Self, Self::Rejection> {
63        let config = <Arc<ServerConfig> as axum::extract::FromRef<S>>::from_ref(state);
64        auth::resolve(&config, &parts.headers).map_err(|HeaderRejection::Unauthorized| {
65            response::HttpError::unauthorized().into_response()
66        })
67    }
68}
69
70/// Build the router.
71///
72/// The mount path is applied here, from config, and is never inferred from the request path.
73pub fn router(state: AppState) -> Router {
74    let mount = state.config().mount_path.clone();
75
76    let api = Router::new()
77        .route("/serverInfo", get(routes::features::server_info))
78        // `/health` is credential-free upstream and is the endpoint every bring-up script polls.
79        // It reports liveness only until there are dependencies to report on.
80        // The SDK transports even a health check as POST with `_method: "GET"`, so accepting
81        // only GET returned 405 to `Parse.getServerHealth()`.
82        .route(
83            "/health",
84            get(routes::health::health).post(routes::health::health),
85        )
86        // Users. `POST /users` is signup and is deliberately not reachable through /classes.
87        .route("/users", axum::routing::post(routes::users::signup))
88        .route("/users/me", get(routes::users::me).post(routes::users::me))
89        .route("/login", axum::routing::post(routes::users::login))
90        .route("/logout", axum::routing::post(routes::users::logout))
91        // Classes.
92        .route(
93            "/classes/:className",
94            get(routes::classes::find).post(routes::classes::dispatch_collection),
95        )
96        .route(
97            "/classes/:className/:objectId",
98            get(routes::classes::get)
99                .put(routes::classes::update)
100                .delete(routes::classes::delete)
101                // The SDK reaches PUT and DELETE through a POST carrying `_method`.
102                .post(routes::classes::dispatch_object),
103        )
104        .with_state(state);
105
106    // The normalization layer wraps the *whole* router rather than the routes inside it, because
107    // it rewrites the request method. A layer applied to the inner router runs after axum has
108    // already matched on the original method, which turns the SDK's `POST` plus `_method: "PUT"`
109    // into a 405 instead of an update.
110    Router::new()
111        .nest(&mount, api)
112        .layer(axum::middleware::from_fn(body_credentials::extract))
113}
114
115/// Bind and serve. Returns the bound address, which matters when the caller asked for port 0.
116///
117/// Creates the unique indexes before binding. That used to live in the binary, which meant an
118/// embedder got a server whose `_User` collection accepted duplicate usernames: the write
119/// succeeded, no error reached the client, and the collision only surfaced later as two accounts
120/// answering to one name. Index creation is part of boot upstream too, so doing it here matches
121/// rather than extends. A failure is fatal for the same reason it is fatal upstream.
122pub async fn serve(
123    state: AppState,
124    addr: std::net::SocketAddr,
125) -> std::io::Result<(
126    std::net::SocketAddr,
127    impl std::future::Future<Output = std::io::Result<()>>,
128)> {
129    state
130        .ensure_indexes()
131        .await
132        .map_err(|e| std::io::Error::other(e.to_string()))?;
133    let listener = tokio::net::TcpListener::bind(addr).await?;
134    let bound = listener.local_addr()?;
135    let app = router(state);
136    Ok((bound, async move { axum::serve(listener, app).await }))
137}