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` and `/serverInfo`; signup, login, `/users/me` and logout; the five
12//! `/classes` verbs and the five `/roles` verbs; the five `/schemas` verbs and
13//! `DELETE /purge/:className`, all master-key only; four `/sessions` reads; and `POST /batch`.
14//! Everything else answers 404.
15//!
16//! **Two things are resolved once per HTTP request and shared by every operation in it**: the
17//! schema snapshot and the caller's expanded role list. See [`request`]. A `/batch` of twenty
18//! writes therefore expands roles once and cannot see two different schemas mid-flight, which is
19//! a correctness property rather than a performance one.
20//!
21//! **An embedder that builds the router itself must call [`AppState::ensure_indexes`] first.**
22//! [`serve`] does it for you. Mounting [`router`] into your own axum app does not, and without
23//! those indexes duplicate usernames are accepted silently, which is a data problem rather than
24//! an error anyone sees.
25
26#![forbid(unsafe_code)]
27#![cfg_attr(
28    not(test),
29    deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)
30)]
31
32pub mod auth;
33pub mod body_credentials;
34pub mod config;
35pub mod cors;
36pub mod ip_allowlist;
37pub mod params;
38pub mod request;
39pub mod response;
40pub mod routes;
41pub mod state;
42
43use std::sync::Arc;
44
45use axum::extract::FromRequestParts;
46use axum::response::{IntoResponse, Response};
47use axum::routing::{delete, get, post};
48use axum::Router;
49
50pub use auth::{Authority, Credentials, HeaderRejection, Peer};
51pub use config::{ProtectedFieldsConfig, ServerConfig};
52pub use ip_allowlist::{InvalidIpEntry, IpAllowlist};
53pub use request::RequestContext;
54pub use state::AppState;
55
56/// Extract [`Authority`] from request headers and the connection's peer address.
57///
58/// Implemented as an extractor so a route cannot forget it: a handler that wants to know who is
59/// calling has to name `Authority` in its signature, and one that does not name it cannot
60/// accidentally read a half-validated identity off the request.
61///
62/// The peer address comes from `ConnectInfo`, which [`serve`] installs. An embedder that builds
63/// the router itself and serves it without `into_make_service_with_connect_info` gets
64/// [`Peer::Unknown`], and every master-key and maintenance-key request is then refused. That is
65/// the intended direction: the alternative, treating an absent address as unfiltered, is the
66/// 0.2.0 behavior this release exists to remove.
67#[axum::async_trait]
68impl<S> FromRequestParts<S> for Authority
69where
70    Arc<ServerConfig>: axum::extract::FromRef<S>,
71    S: Send + Sync,
72{
73    type Rejection = Response;
74
75    async fn from_request_parts(
76        parts: &mut http::request::Parts,
77        state: &S,
78    ) -> Result<Self, Self::Rejection> {
79        let config = <Arc<ServerConfig> as axum::extract::FromRef<S>>::from_ref(state);
80        auth::resolve_with_peer(&config, &parts.headers, peer_of(parts)).map_err(
81            |HeaderRejection::Unauthorized| response::HttpError::unauthorized().into_response(),
82        )
83    }
84}
85
86/// The connection's peer address, read from the extension `ConnectInfo` inserts.
87///
88/// **Deliberately not a header.** `X-Forwarded-For` and `Forwarded` are written by the caller, and
89/// an allowlist that consults them admits anyone who can spell an address. Upstream is the same:
90/// `getClientIp` is `req.ip` (`middlewares.js:358-360`) and parse-server never enables Express's
91/// `trust proxy`.
92fn peer_of(parts: &http::request::Parts) -> Peer {
93    parts
94        .extensions
95        .get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
96        .map(|info| Peer::from(info.0))
97        .unwrap_or(Peer::Unknown)
98}
99
100/// Build the router.
101///
102/// The mount path is applied here, from config, and is never inferred from the request path.
103pub fn router(state: AppState) -> Router {
104    let mount = state.config().mount_path.clone();
105    // Cloned before `with_state` consumes it below, so the CORS layer can read the same config.
106    let cors_state = state.clone();
107
108    // The 0.2.0 surface, and nothing else: anything not registered here is a 404. Every route
109    // that a client can reach through a `_method` override also accepts `POST`, because the
110    // JavaScript SDK transports everything that way.
111    let api = Router::new()
112        .route("/serverInfo", get(routes::http::server_info))
113        // `/health` is credential-free upstream and is the endpoint every bring-up script polls.
114        // The SDK transports even a health check as POST with `_method: "GET"`, so accepting
115        // only GET returned 405 to `Parse.getServerHealth()`.
116        .route(
117            "/health",
118            get(routes::http::health).post(routes::http::health),
119        )
120        // Users. `POST /users` is signup and is deliberately not reachable through /classes.
121        .route("/users", post(routes::http::users_collection))
122        .route(
123            "/users/me",
124            get(routes::http::users_me).post(routes::http::users_me),
125        )
126        .route("/login", post(routes::http::login))
127        .route("/logout", post(routes::http::logout))
128        // Classes.
129        .route(
130            "/classes/:className",
131            get(routes::http::classes_collection).post(routes::http::classes_collection),
132        )
133        .route(
134            "/classes/:className/:objectId",
135            get(routes::http::classes_object)
136                .put(routes::http::classes_object)
137                .delete(routes::http::classes_object)
138                .post(routes::http::classes_object),
139        )
140        // Roles: `ClassesRouter` with `className()` pinned to `_Role` (`RolesRouter.js:3-25`).
141        .route(
142            "/roles",
143            get(routes::http::roles_collection).post(routes::http::roles_collection),
144        )
145        .route(
146            "/roles/:objectId",
147            get(routes::http::roles_object)
148                .put(routes::http::roles_object)
149                .delete(routes::http::roles_object)
150                .post(routes::http::roles_object),
151        )
152        // Sessions. `/sessions/me` is registered before `/sessions/:objectId` because upstream
153        // depends on registration order (`SessionsRouter.js:113-121`). axum matches a literal
154        // segment ahead of a parameter regardless, which the route tests assert; the order is
155        // kept anyway so the two files read the same way.
156        .route(
157            "/sessions/me",
158            get(routes::http::sessions_me).post(routes::http::sessions_me),
159        )
160        .route(
161            "/sessions",
162            get(routes::http::sessions_collection).post(routes::http::sessions_collection),
163        )
164        .route(
165            "/sessions/:objectId",
166            get(routes::http::sessions_object)
167                .delete(routes::http::sessions_object)
168                .post(routes::http::sessions_object),
169        )
170        // Schemas and purge, master key only.
171        .route(
172            "/schemas",
173            get(routes::http::schemas_collection).post(routes::http::schemas_collection),
174        )
175        .route(
176            "/schemas/:className",
177            get(routes::http::schemas_class)
178                .post(routes::http::schemas_class)
179                .put(routes::http::schemas_class)
180                .delete(routes::http::schemas_class),
181        )
182        .route(
183            "/purge/:className",
184            delete(routes::http::purge).post(routes::http::purge),
185        )
186        .route("/batch", post(routes::http::batch))
187        .with_state(state);
188
189    // The normalization layer wraps the *whole* router rather than the routes inside it, because
190    // it rewrites the request method. A layer applied to the inner router runs after axum has
191    // already matched on the original method, which turns the SDK's `POST` plus `_method: "PUT"`
192    // into a 405 instead of an update.
193    // CORS is the outermost layer, matching upstream, where `allowCrossDomain` is the first
194    // middleware on the router (`ParseServer.ts:312`). Outermost is what makes the headers appear
195    // on error responses too, and what lets an `OPTIONS` preflight be answered before anything
196    // downstream can reject it for lacking credentials it is not allowed to send yet.
197    Router::new()
198        .nest(&mount, api)
199        .layer(axum::middleware::from_fn(body_credentials::extract))
200        .layer(axum::middleware::from_fn_with_state(
201            cors_state,
202            cors::layer,
203        ))
204}
205
206/// Bind and serve. Returns the bound address, which matters when the caller asked for port 0.
207///
208/// Creates the unique indexes before binding. That used to live in the binary, which meant an
209/// embedder got a server whose `_User` collection accepted duplicate usernames: the write
210/// succeeded, no error reached the client, and the collision only surfaced later as two accounts
211/// answering to one name. Index creation is part of boot upstream too, so doing it here matches
212/// rather than extends. A failure is fatal for the same reason it is fatal upstream.
213///
214/// **Served with connect info**, because `masterKeyIps` filters on the connection's peer address
215/// and there is nowhere else to get it. Without it the two privileged keys are refused outright.
216pub async fn serve(
217    state: AppState,
218    addr: std::net::SocketAddr,
219) -> std::io::Result<(
220    std::net::SocketAddr,
221    impl std::future::Future<Output = std::io::Result<()>>,
222)> {
223    state
224        .ensure_indexes()
225        .await
226        .map_err(|e| std::io::Error::other(e.to_string()))?;
227    let listener = tokio::net::TcpListener::bind(addr).await?;
228    let bound = listener.local_addr()?;
229    let app = router(state).into_make_service_with_connect_info::<std::net::SocketAddr>();
230    Ok((bound, async move { axum::serve(listener, app).await }))
231}