Developer SDK

Rust examples

Install the SDK · View source on GitHub ↗

integration/server/src/main.rs

// SERVER ONLY. The authenticated backend supplies user:<service-user-id> internally.
// Never pass an untrusted request field or UR client ID. Set URNETWORK_ROOT_JWT,
// URNETWORK_CLIENT_MAP (absolute file path, existing service-owned parent), and
// optional URNETWORK_API_URL. Remove crash-left .lock only after confirming it is stale.
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::{
    collections::{BTreeMap, BTreeSet},
    env,
    error::Error,
    fs::{self, OpenOptions},
    io::{Read, Write},
    path::{Path, PathBuf},
    time::{Duration, SystemTime, UNIX_EPOCH},
};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
const LIMIT: u64 = 1 << 20;
#[derive(Serialize, Deserialize)]
struct ClientMap {
    version: u32,
    clients: BTreeMap<String, String>,
}
#[derive(Serialize, Debug)]
struct ClientResult {
    client_id: String,
    by_client_jwt: String,
}
fn user_valid(user: &str) -> bool {
    let Some(s) = user.strip_prefix("user:") else {
        return false;
    };
    !s.is_empty()
        && user.len() <= 128
        && s.as_bytes()[0].is_ascii_alphanumeric()
        && s.bytes()
            .all(|b| b.is_ascii_alphanumeric() || b"_.:@-".contains(&b))
}
fn service_user(args: &[String]) -> Result<String> {
    if args.len() != 1 || !user_valid(&args[0]) {
        return Err("expected one user:<service-user-id>".into());
    }
    Ok(args[0].clone())
}
fn id_valid(id: &str) -> bool {
    id.len() == 36
        && id.bytes().enumerate().all(|(i, b)| {
            if [8, 13, 18, 23].contains(&i) {
                b == b'-'
            } else {
                b.is_ascii_digit() || (b'a'..=b'f').contains(&b)
            }
        })
}
fn endpoint(base: &str) -> Result<reqwest::Url> {
    let mut u = reqwest::Url::parse(base)?;
    let local = matches!(
        u.host_str(),
        Some("localhost" | "127.0.0.1" | "[::1]" | "::1")
    );
    if u.host().is_none()
        || !u.username().is_empty()
        || u.password().is_some()
        || u.path() != "/"
        || u.query().is_some()
        || u.fragment().is_some()
        || (u.scheme() != "https" && !(u.scheme() == "http" && local))
    {
        return Err("API must be an HTTPS origin, or explicit loopback HTTP mock".into());
    }
    u.set_path("/network/auth-client");
    Ok(u)
}
fn request_for(user: &str, client: Option<&str>) -> Result<Value> {
    if !user_valid(user) {
        return Err("invalid service key".into());
    }
    let mut body = json!({"description":format!("service {user}"),"device_spec":"urnetwork-examples/rust-server"});
    if let Some(id) = client {
        if !id_valid(id) {
            return Err("invalid mapped client".into());
        };
        body["client_id"] = json!(id)
    }
    Ok(body)
}
fn parse_response(raw: &str, expected: Option<&str>) -> Result<ClientResult> {
    let obj: Value = serde_json::from_str(raw)?;
    if !obj.is_object() || !obj["error"].is_null() {
        return Err("API failure".into());
    }
    let id = obj["client_id"].as_str().ok_or("missing client ID")?;
    let jwt = obj["by_client_jwt"].as_str().ok_or("missing scoped JWT")?;
    let parts: Vec<_> = jwt.split('.').collect();
    if !id_valid(id) || parts.len() != 3 || parts.iter().any(|p| p.is_empty()) {
        return Err("invalid scoped result".into());
    }
    let claims: Value = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1])?)?;
    if claims["client_id"].as_str() != Some(id) || expected.is_some_and(|old| old != id) {
        return Err("scoped identity mismatch".into());
    }
    // Claim comparison is consistency checking, not local signature verification.
    Ok(ClientResult {
        client_id: id.into(),
        by_client_jwt: jwt.into(),
    })
}
fn load_map(file: &Path) -> Result<ClientMap> {
    let stat = match fs::symlink_metadata(file) {
        Ok(s) => s,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            return Ok(ClientMap {
                version: 1,
                clients: BTreeMap::new(),
            });
        }
        Err(e) => return Err(e.into()),
    };
    if !stat.is_file() || stat.len() > LIMIT {
        return Err("invalid mapping file".into());
    }
    #[cfg(unix)]
    if stat.permissions().mode() & 0o077 != 0 {
        return Err("mapping must be private (0600)".into());
    }
    let map: ClientMap = serde_json::from_slice(&fs::read(file)?)?;
    if map.version != 1 {
        return Err("invalid map version".into());
    }
    let mut seen = BTreeSet::new();
    for (u, id) in &map.clients {
        if !user_valid(u) || !id_valid(id) || !seen.insert(id) {
            return Err("invalid mapped client".into());
        }
    }
    Ok(map)
}
fn unique_suffix() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos()
}
fn save_map(file: &Path, map: &ClientMap) -> Result<()> {
    let temporary = file.with_extension(format!("{}.tmp", unique_suffix()));
    let mut options = OpenOptions::new();
    options.write(true).create_new(true);
    #[cfg(unix)]
    options.mode(0o600);
    let mut output = options.open(&temporary)?;
    let result = (|| {
        output.write_all(&serde_json::to_vec(map)?)?;
        output.sync_all()?;
        drop(output);
        fs::rename(&temporary, file)?;
        Ok(())
    })();
    let _ = fs::remove_file(&temporary);
    result
}
struct Lock(PathBuf);
impl Drop for Lock {
    fn drop(&mut self) {
        let _ = fs::remove_dir(&self.0);
    }
}
fn allocate(
    user: &str,
    file: &Path,
    mut call: impl FnMut(&Value) -> Result<String>,
) -> Result<ClientResult> {
    if !file.is_absolute() {
        return Err("mapping needs absolute path".into());
    }
    let lock = PathBuf::from(format!("{}.lock", file.display()));
    fs::create_dir(&lock)?;
    let _guard = Lock(lock.clone());
    #[cfg(unix)]
    fs::set_permissions(&lock, fs::Permissions::from_mode(0o700))?;
    let mut map = load_map(file)?;
    let old = map.clients.get(user).map(String::as_str);
    let response = parse_response(&call(&request_for(user, old)?)?, old)?;
    if old.is_none() {
        if map.clients.values().any(|id| id == &response.client_id) {
            return Err("client assigned to another user".into());
        };
        map.clients.insert(user.into(), response.client_id.clone());
        save_map(file, &map)?
    }
    Ok(response)
}
fn post(url: &reqwest::Url, root: &str, body: &Value) -> Result<String> {
    if root.is_empty() || root.chars().any(char::is_whitespace) {
        return Err("set backend root JWT".into());
    }
    let client = reqwest::blocking::Client::builder()
        .timeout(Duration::from_secs(15))
        .redirect(reqwest::redirect::Policy::none())
        .build()?;
    let response = client
        .post(url.clone())
        .bearer_auth(root)
        .json(body)
        .send()?;
    if !response.status().is_success() {
        return Err("provisioning HTTP failure".into());
    }
    let mut bytes = Vec::new();
    response.take(LIMIT + 1).read_to_end(&mut bytes)?;
    if bytes.len() as u64 > LIMIT {
        return Err("response too large".into());
    };
    Ok(String::from_utf8(bytes)?)
}
fn self_test() -> Result<()> {
    let id = "11111111-1111-1111-1111-111111111111";
    let jwt = format!(
        "e30.{}.test",
        URL_SAFE_NO_PAD.encode(serde_json::to_vec(&json!({"client_id":id}))?)
    );
    let raw = json!({"client_id":id,"by_client_jwt":jwt}).to_string();
    let dir = env::temp_dir().join(format!(
        "ur-allocator-{}-{}",
        std::process::id(),
        unique_suffix()
    ));
    fs::create_dir(&dir)?;
    let file = dir.join("clients.json");
    let mut calls = vec![];
    let test: Result<()> = (|| {
        let mut mock = |body: &Value| {
            calls.push(body.clone());
            Ok(raw.clone())
        };
        assert_eq!(allocate("user:alice", &file, &mut mock)?.client_id, id);
        assert_eq!(allocate("user:alice", &file, &mut mock)?.by_client_jwt, jwt);
        assert!(calls[0].get("client_id").is_none());
        assert!(calls.iter().all(|c| c.get("source_client_id").is_none()));
        assert_eq!(calls[1]["client_id"], id);
        assert_eq!(load_map(&file)?.clients["user:alice"], id);
        assert_eq!(
            endpoint("http://127.0.0.1:1234")?.path(),
            "/network/auth-client"
        );
        for args in [
            vec![id.into()],
            vec!["user:a".into(), "--client-id".into(), id.into()],
            vec!["user:../a".into()],
        ] {
            assert!(service_user(&args).is_err())
        }
        assert!(endpoint("http://example.com").is_err());
        assert!(endpoint("https://example.com/path").is_err());
        assert!(parse_response("{\"error\":{}}", None).is_err());
        assert!(parse_response(&raw, Some("22222222-2222-2222-2222-222222222222")).is_err());
        Ok(())
    })();
    fs::remove_dir_all(dir)?;
    test?;
    println!("allocator self-test passed");
    Ok(())
}
fn run() -> Result<()> {
    let args: Vec<_> = env::args().skip(1).collect();
    if args == ["--self-test"] {
        return self_test();
    }
    let user = service_user(&args)?;
    let url = endpoint(
        &env::var("URNETWORK_API_URL").unwrap_or_else(|_| "https://api.bringyour.com".into()),
    )?;
    let root = env::var("URNETWORK_ROOT_JWT")?;
    let file = PathBuf::from(env::var("URNETWORK_CLIENT_MAP")?);
    let result = allocate(&user, &file, |body| post(&url, &root, body))?;
    println!("{}", serde_json::to_string(&result)?);
    Ok(())
}
fn main() {
    if run().is_err() {
        eprintln!(
            "allocator failed: check service key, private mapping and backend API configuration"
        );
        std::process::exit(1)
    }
}