use std::net::SocketAddr;
use parse_rust_mongo::MongoAdapter;
use parse_rust_server::{AppState, ServerConfig};
#[tokio::main]
async fn main() {
if let Err(e) = run().await {
eprintln!("parse-rust: {e}");
std::process::exit(1);
}
}
async fn run() -> std::io::Result<()> {
let env = |k: &str| std::env::var(k).ok();
let required = |k: &str| -> std::io::Result<String> {
std::env::var(k).map_err(|_| {
std::io::Error::other(format!(
"{k} is required. parse-rust has no default application id or master key: \
the master key bypasses every access control, so a default would be a \
published credential."
))
})
};
let app_id = required("PARSE_SERVER_APPLICATION_ID")?;
let master_key = required("PARSE_SERVER_MASTER_KEY")?;
let mut config = ServerConfig::new(app_id, master_key);
if let Some(k) = env("PARSE_SERVER_JAVASCRIPT_KEY") {
config.javascript_key = Some(k);
}
if let Some(k) = env("PARSE_SERVER_REST_API_KEY") {
config.rest_api_key = Some(k);
}
if let Some(m) = env("PARSE_SERVER_MOUNT_PATH") {
config.mount_path = m;
}
if let Some(v) = env("PARSE_SERVER_SESSION_LENGTH") {
config.session.session_length_secs = number(&v, "PARSE_SERVER_SESSION_LENGTH")?;
}
if let Some(v) = env("PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS") {
config.session.expire_inactive_sessions =
boolean(&v, "PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS")?;
}
if let Some(v) = env("PARSE_SERVER_PROTECTED_FIELDS_OWNER_EXEMPT") {
config.protected_fields_owner_exempt =
boolean(&v, "PARSE_SERVER_PROTECTED_FIELDS_OWNER_EXEMPT")?;
}
if let Some(v) = env("PARSE_SERVER_PROTECTED_FIELDS_SAVE_RESPONSE_EXEMPT") {
config.protected_fields_save_response_exempt =
boolean(&v, "PARSE_SERVER_PROTECTED_FIELDS_SAVE_RESPONSE_EXEMPT")?;
}
if let Some(v) = env("PARSE_SERVER_ENABLE_SANITIZED_ERROR_RESPONSE") {
config.enable_sanitized_error_response =
boolean(&v, "PARSE_SERVER_ENABLE_SANITIZED_ERROR_RESPONSE")?;
}
if let Some(v) = env("PARSE_SERVER_DATABASE_CREATE_INDEX_ROLE_NAME") {
config.create_index_role_name =
boolean(&v, "PARSE_SERVER_DATABASE_CREATE_INDEX_ROLE_NAME")?;
}
if let Some(v) = env("PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID") {
config.allow_custom_object_id = boolean(&v, "PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID")?;
}
if let Some(v) = env("PARSE_SERVER_ALLOW_ORIGIN") {
config.allow_origin = list(&v);
}
if let Some(v) = env("PARSE_SERVER_ALLOW_HEADERS") {
config.allow_headers = list(&v);
}
if let Some(v) = env("PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION") {
config.allow_client_class_creation =
boolean(&v, "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION")?;
}
if let Some(v) = env("PARSE_SERVER_REQUEST_COMPLEXITY_BATCH_REQUEST_LIMIT") {
config.batch_request_limit =
number(&v, "PARSE_SERVER_REQUEST_COMPLEXITY_BATCH_REQUEST_LIMIT")?;
}
if let Some(v) = env("PARSE_SERVER_MASTER_KEY_IPS") {
config.master_key_ips = ip_allowlist(&v, "PARSE_SERVER_MASTER_KEY_IPS")?;
}
if let Some(v) = env("PARSE_SERVER_PROTECTED_FIELDS") {
let mut configured = protected_fields(&v)?;
parse_rust_server::config::merge_protected_fields_defaults(
&mut configured,
config.protected_fields_owner_exempt,
);
config.protected_fields = configured;
}
let uri = env("PARSE_SERVER_DATABASE_URI")
.unwrap_or_else(|| "mongodb://127.0.0.1:27017/parse".into());
let database = database_from_uri(&uri).to_string();
let storage = MongoAdapter::connect(&uri, &database)
.await
.map_err(|e| std::io::Error::other(e.to_string()))?;
let port: u16 = env("PORT").and_then(|p| p.parse().ok()).unwrap_or(27800);
let host = env("PARSE_SERVER_HOST").unwrap_or_else(|| "127.0.0.1".into());
let ip: std::net::IpAddr = host.parse().map_err(|_| {
std::io::Error::other(format!("PARSE_SERVER_HOST is not an IP address: {host}"))
})?;
let addr = SocketAddr::new(ip, port);
let state = AppState::new(config, storage);
let (bound, server) = parse_rust_server::serve(state, addr).await?;
println!("parse-rust listening on http://{bound}");
server.await
}
fn list(value: &str) -> Vec<String> {
value.split(',').map(str::to_string).collect()
}
fn boolean(value: &str, name: &str) -> std::io::Result<bool> {
match value {
"true" | "1" => Ok(true),
"false" | "0" => Ok(false),
other => Err(std::io::Error::other(format!(
"{name} must be `true` or `false`, got {other:?}"
))),
}
}
fn number(value: &str, name: &str) -> std::io::Result<i64> {
value
.parse()
.map_err(|_| std::io::Error::other(format!("{name} must be a number, got {value:?}")))
}
fn protected_fields(value: &str) -> std::io::Result<parse_rust_server::ProtectedFieldsConfig> {
let parsed: parse_rust_server::ProtectedFieldsConfig =
serde_json::from_str(value).map_err(|e| {
std::io::Error::other(format!(
"PARSE_SERVER_PROTECTED_FIELDS must be JSON of the form \
{{\"ClassName\": {{\"entity\": [\"field\"]}}}}: {e}"
))
})?;
Ok(parsed)
}
fn ip_allowlist(value: &str, name: &str) -> std::io::Result<parse_rust_server::IpAllowlist> {
parse_rust_server::IpAllowlist::parse_env(value)
.map_err(|e| std::io::Error::other(format!("{name}: {e}")))
}
const DEFAULT_DATABASE: &str = "parse";
fn database_from_uri(uri: &str) -> &str {
let after_scheme = uri.split_once("://").map(|(_, rest)| rest).unwrap_or(uri);
let Some((_, path)) = after_scheme.split_once('/') else {
return DEFAULT_DATABASE;
};
match path.split(['?', '#']).next() {
Some(name) if !name.is_empty() => name,
_ => DEFAULT_DATABASE,
}
}
#[cfg(test)]
mod tests {
use super::database_from_uri;
#[test]
fn plain_uri() {
assert_eq!(
database_from_uri("mongodb://127.0.0.1:27017/parse"),
"parse"
);
}
#[test]
fn query_string_is_not_part_of_the_name() {
assert_eq!(
database_from_uri("mongodb://host/app?retryWrites=true&w=majority"),
"app"
);
assert_eq!(database_from_uri("mongodb://host/app#frag"), "app");
}
#[test]
fn no_database_path_falls_back_rather_than_naming_the_host() {
assert_eq!(database_from_uri("mongodb://127.0.0.1:27017"), "parse");
assert_eq!(database_from_uri("mongodb://127.0.0.1:27017/"), "parse");
assert_eq!(
database_from_uri("mongodb://127.0.0.1:27017/?tls=true"),
"parse"
);
}
#[test]
fn credentials_and_replica_sets_do_not_confuse_it() {
assert_eq!(
database_from_uri("mongodb://user:pass@a:27017,b:27017/app?replicaSet=rs0"),
"app"
);
assert_eq!(
database_from_uri("mongodb+srv://user:pass@cluster.example/app"),
"app"
);
}
}