Skip to main content

parse_rust_server/
body_credentials.rs

1//! Normalizing what the JavaScript SDK actually sends.
2//!
3//! The SDK does not speak the REST API the documentation describes. Everything is a `POST` with a
4//! `text/plain` body, and the method, the credentials and the query parameters all travel inside
5//! that body. It does this so a browser never sends a CORS preflight.
6//!
7//! Upstream normalizes all of it before routing, in three places:
8//!
9//! - `allowMethodOverride` (`middlewares.js:425-433`) rewrites the method from `_method`.
10//! - `handleParseHeaders` (`:111-198`) reads `_ApplicationId`, `_JavaScriptKey`, `_MasterKey`,
11//!   `_SessionToken`, `_InstallationId`, `_ContentType` and friends from the body, and **deletes
12//!   them**, which is why a saved Parse object never grows an `_ApplicationId` field.
13//! - `ClassesRouter` merges `req.body` with the decoded query string, so a `where` sent in the
14//!   body reaches the same code as one sent in the URL.
15//!
16//! Doing all three here, in one layer, is deliberate. Spreading it across the extractor and each
17//! route is how a check ends up applied on one path and forgotten on another.
18
19use axum::body::{to_bytes, Body};
20use axum::extract::Request;
21use axum::middleware::Next;
22use axum::response::Response;
23use serde_json::Value as Json;
24
25use crate::auth::headers;
26
27/// The method the client asked for through `_method`, when it differs from the transport method.
28///
29/// Read by the `POST` dispatchers on the class and user routes.
30#[derive(Debug, Clone)]
31pub struct MethodOverride(pub http::Method);
32
33/// Body key to header name.
34const CREDENTIALS: [(&str, &str); 6] = [
35    ("_ApplicationId", headers::APP_ID),
36    ("_JavaScriptKey", headers::JAVASCRIPT_KEY),
37    ("_MasterKey", headers::MASTER_KEY),
38    ("_MaintenanceKey", headers::MAINTENANCE_KEY),
39    ("_SessionToken", headers::SESSION_TOKEN),
40    ("_InstallationId", headers::INSTALLATION_ID),
41];
42
43/// Keys that carry no authority but must still be removed, or they become fields on saved objects.
44const DISCARDED: [&str; 4] = [
45    "_ClientVersion",
46    "_RevocableSession",
47    "_noBody",
48    "_ContentType",
49];
50
51/// Upper bound on a buffered body. Without one, a request could exhaust memory.
52const MAX_BODY: usize = 20 * 1024 * 1024;
53
54pub async fn extract(request: Request, next: Next) -> Response {
55    let (mut parts, body) = request.into_parts();
56
57    let bytes = match to_bytes(body, MAX_BODY).await {
58        Ok(b) => b,
59        Err(_) => return next.run(Request::from_parts(parts, Body::empty())).await,
60    };
61
62    // Not a JSON object body: nothing to normalize. Covers every GET.
63    let Ok(Json::Object(mut map)) = serde_json::from_slice::<Json>(&bytes) else {
64        return next
65            .run(Request::from_parts(parts, Body::from(bytes)))
66            .await;
67    };
68
69    // 1. Credentials into headers. An explicit header wins, because upstream consults the body
70    //    only when the header appId is missing or unknown.
71    for (body_key, header_name) in CREDENTIALS {
72        let Some(Json::String(value)) = map.shift_remove(body_key) else {
73            continue;
74        };
75        if parts.headers.contains_key(header_name) {
76            continue;
77        }
78        if let (Ok(name), Ok(val)) = (
79            http::HeaderName::from_bytes(header_name.as_bytes()),
80            http::HeaderValue::from_str(&value),
81        ) {
82            parts.headers.insert(name, val);
83        }
84    }
85    for key in DISCARDED {
86        map.shift_remove(key);
87    }
88
89    // 2. Method override. The SDK sends every read as `POST` with `_method: "GET"`.
90    //
91    // **The method is NOT rewritten here, and that is not a stylistic choice.** axum matches the
92    // request method before a `Router::layer` runs, verified by observation: a POST carrying
93    // `_method: "PUT"` produced a 405 no matter where the layer was attached. So the intended
94    // method travels in an extension and the routes dispatch on it explicitly, which is
95    // deterministic and testable rather than dependent on middleware ordering inside the
96    // framework.
97    let overridden = match map.shift_remove("_method") {
98        Some(Json::String(m)) => m.parse::<http::Method>().ok(),
99        _ => None,
100    };
101    if let Some(method) = overridden.clone() {
102        parts.extensions.insert(MethodOverride(method));
103    }
104
105    // 3. For a read, the remaining body keys are query parameters. Object and array values are
106    //    re-encoded as JSON text, which is the form they take in a real query string.
107    let effective = overridden.clone().unwrap_or_else(|| parts.method.clone());
108    if effective == http::Method::GET || effective == http::Method::DELETE {
109        let mut pairs: Vec<(String, String)> = Vec::new();
110        for (k, v) in &map {
111            let value = match v {
112                Json::String(s) => s.clone(),
113                other => other.to_string(),
114            };
115            pairs.push((k.clone(), value));
116        }
117        if !pairs.is_empty() {
118            let existing = parts.uri.query().unwrap_or("").to_string();
119            let mut serializer = form_urlencoded::Serializer::new(String::new());
120            for (k, v) in pairs {
121                serializer.append_pair(&k, &v);
122            }
123            let merged = if existing.is_empty() {
124                serializer.finish()
125            } else {
126                format!("{existing}&{}", serializer.finish())
127            };
128            let path = parts.uri.path().to_string();
129            if let Ok(uri) = format!("{path}?{merged}").parse::<http::Uri>() {
130                parts.uri = uri;
131            }
132        }
133        // A GET carries no body.
134        let trace = std::env::var("PARSE_RUST_TRACE").is_ok();
135        let (m, u) = (parts.method.clone(), parts.uri.clone());
136        let res = next.run(Request::from_parts(parts, Body::empty())).await;
137        if trace {
138            eprintln!("[trace] {m} {u} -> {}", res.status());
139        }
140        return res;
141    }
142
143    // The body has just been shown to parse as JSON, so declaring it as such is a statement of
144    // fact. axum's `Json` extractor requires the header; Express's parser does not, and the SDK
145    // sends `text/plain`.
146    parts.headers.insert(
147        http::header::CONTENT_TYPE,
148        http::HeaderValue::from_static("application/json"),
149    );
150
151    let body = match serde_json::to_vec(&Json::Object(map)) {
152        Ok(v) => Body::from(v),
153        Err(_) => Body::from(bytes),
154    };
155    let trace = std::env::var("PARSE_RUST_TRACE").is_ok();
156    let (m, u) = (parts.method.clone(), parts.uri.clone());
157    let res = next.run(Request::from_parts(parts, body)).await;
158    if trace {
159        eprintln!("[trace] {m} {u} -> {}", res.status());
160    }
161    res
162}