//! HTTP: routes requests to the state, one SQLite connection per thread. use std::io::Read; use std::path::Path; use std::sync::Arc; use anyhow::Result; use serde::Serialize; use serde_json::json; use tiny_http::{Header, Method, Request, Response, Server}; use vcs_cli::host::LandRequest; use vcs_cli::wire::{self, Hold, MineFile, PutChange, Rule, SaveRecord, ShareRequest}; use vcs_core::Id; use crate::state::{Access, Problem, Res, State, problem}; /// Biggest request body accepted (a bundle of a big change). const MAX_BODY: u64 = 512 << 20; pub fn serve(data: &Path, listen: &str, threads: usize) -> Result<()> { // Create the schema once before the workers start. State::open(data)?; let server = Arc::new(Server::http(listen).map_err(|e| anyhow::anyhow!("can't listen on {listen}: {e}"))?); eprintln!("vcs-server on http://{listen}, data in {}", data.display()); let mut handles = Vec::new(); for _ in 0..threads.max(1) { let server = server.clone(); let data = data.to_path_buf(); handles.push(std::thread::spawn(move || { let state = State::open(&data).expect("open state"); for request in server.incoming_requests() { handle(&state, request); } })); } for h in handles { let _ = h.join(); } Ok(()) } enum Body { Json(serde_json::Value), Bytes(Vec), } fn handle(state: &State, mut request: Request) { let started = std::time::Instant::now(); let method = request.method().clone(); let url = request.url().to_string(); let result = route(state, &mut request); let (status, body) = match result { Ok((status, body)) => (status, body), Err(p) => (p.status, Body::Json(json!({ "error": { "code": p.code, "message": p.message, "holder": p.holder } }))), }; let response = match body { Body::Json(v) => Response::from_data(serde_json::to_vec(&v).unwrap_or_default()) .with_status_code(status) .with_header(Header::from_bytes("content-type", "application/json").unwrap()), Body::Bytes(b) => Response::from_data(b).with_status_code(status).with_header(Header::from_bytes("content-type", "application/octet-stream").unwrap()), }; if std::env::var_os("VCS_SERVER_LOG").is_some() { eprintln!("{method} {url} → {status} in {:?}", started.elapsed()); } let _ = request.respond(response); } fn ok(value: T) -> Res<(u16, Body)> { Ok((200, Body::Json(serde_json::to_value(value).map_err(|e| problem(500, "failed", e.to_string()))?))) } fn read_body(request: &mut Request) -> Res> { let mut body = Vec::new(); request.as_reader().take(MAX_BODY + 1).read_to_end(&mut body).map_err(|e| problem(400, "bad_usage", e.to_string()))?; if body.len() as u64 > MAX_BODY { return Err(problem(413, "too_big", "request too big")); } Ok(body) } fn json_body(request: &mut Request) -> Res { let body = read_body(request)?; serde_json::from_slice(&body).map_err(|e| problem(400, "bad_usage", format!("bad request body: {e}"))) } fn query(url: &str, key: &str) -> Option { url.split_once('?')?.1.split('&').find_map(|kv| kv.strip_prefix(&format!("{key}=")).map(str::to_string)) } fn route(state: &State, request: &mut Request) -> Res<(u16, Body)> { let url = request.url().to_string(); let path = url.split('?').next().unwrap_or(""); let method = request.method().clone(); if path == "/.well-known/vcs.json" { return ok(json!({ "api": "/v1" })); } let Some(rest) = path.strip_prefix("/v1/") else { return Err(problem(404, "not_found", "no such endpoint")) }; let parts: Vec<&str> = rest.split('/').filter(|p| !p.is_empty()).collect(); let token = request .headers() .iter() .find(|h| h.field.equiv("authorization")) .and_then(|h| h.value.as_str().strip_prefix("Bearer ").map(str::to_string)); let user = match token { Some(t) => Some(state.user_for_token(&t)?.ok_or_else(|| problem(401, "not_logged_in", "this token isn't valid here"))?), None => None, }; match (&method, parts.as_slice()) { (Method::Get, ["me"]) => { let me = user.ok_or_else(|| problem(401, "not_logged_in", "sign in first"))?; ok(me) } (Method::Post, ["projects"]) => { let me = user.ok_or_else(|| problem(401, "not_logged_in", "sign in first"))?; #[derive(serde::Deserialize)] struct New { owner: String, name: String, #[serde(default)] public: bool, } let new: New = json_body(request)?; if !state.may_create_under(&new.owner, &me.handle)? { return Err(problem(403, "not_allowed", format!("{} belongs to someone else", new.owner))); } let key = format!("{}/{}", new.owner, new.name); if state.project_exists(&key)?.is_some() { return Err(problem(409, "exists", format!("{key} already exists"))); } state.add_project(&key, &me.handle, new.public)?; let access = state.access(&key, Some(me))?; ok(view(state, &key, &access)?) } (_, ["projects", owner, name, tail @ ..]) => { let key = format!("{owner}/{name}"); let access = state.access(&key, user)?; project_route(state, request, &method, &key, &access, tail, &url) } _ => Err(problem(404, "not_found", "no such endpoint")), } } fn view(state: &State, key: &str, access: &Access) -> Res { let (owner, name) = key.split_once('/').unwrap(); Ok(wire::ProjectView { owner: owner.into(), name: name.into(), public: state.public(key)?, main: state.main_for(key, access)?, can: access.can.clone(), git: None, git_stopped: None }) } fn need_user(access: &Access) -> Res<()> { if access.user.is_none() { return Err(problem(401, "not_logged_in", "sign in first")); } Ok(()) } fn project_route(state: &State, request: &mut Request, method: &Method, key: &str, access: &Access, tail: &[&str], url: &str) -> Res<(u16, Body)> { match (method, tail) { (Method::Get, []) => ok(view(state, key, access)?), (Method::Get, ["versions"]) => { let since: u64 = query(url, "since").and_then(|s| s.parse().ok()).unwrap_or(0); let limit: i64 = query(url, "limit").and_then(|s| s.parse().ok()).unwrap_or(5000).min(5000); let versions = state.versions_since(key, since, limit)?; ok(json!({ "versions": state.versions_for(key, access, versions)? })) } (Method::Post, ["objects", "missing"]) => { need_user(access)?; let ids = wire::decode_ids(&read_body(request)?); Ok((200, Body::Bytes(wire::encode_ids(&state.missing(&ids))))) } (Method::Post, ["objects"]) => { need_user(access)?; let body = read_body(request)?; let stored = state.store.ingest(&body).map_err(|e| problem(400, "bad_usage", format!("{e:#}")))?; ok(json!({ "stored": stored })) } (Method::Get, ["trees", tree]) => { let tree = Id::parse(tree).ok_or_else(|| problem(400, "bad_usage", "not a tree id"))?; if !state.may_read_tree(key, access, &tree)? { return Err(problem(404, "not_found", "no such tree")); } let have = query(url, "have").and_then(|h| Id::parse(&h)).filter(|h| state.may_read_tree(key, access, h).unwrap_or(false)); Ok((200, Body::Bytes(state.tree_bundle(&tree, have.as_ref())?))) } (Method::Get, ["changes"]) => ok(json!({ "changes": state.changes(key, access)? })), (Method::Get, ["changes", prefix]) => { let (change, saves) = state.find_change(key, access, prefix)?; ok(json!({ "change": change, "saves": saves })) } (Method::Put, ["changes", id]) => { let put: PutChange = json_body(request)?; if put.change.id != *id { return Err(problem(400, "bad_usage", "change id mismatch")); } ok(json!({ "change": state.put_change(key, access, put.change, &put.workspace, &put.label)? })) } (Method::Post, ["changes", id, "hold"]) => { let hold: Hold = json_body(request)?; ok(json!({ "previous": state.hold(key, access, id, &hold.workspace, &hold.label)? })) } (Method::Post, ["changes", id, "saves"]) => { let save: SaveRecord = json_body(request)?; state.add_save(key, access, id, &save)?; ok(json!({})) } (Method::Post, ["changes", id, "access"]) => { let share: ShareRequest = json_body(request)?; ok(json!({ "shared": state.share(key, access, id, &share.who, share.edit)? })) } (Method::Post, ["import"]) => { #[derive(serde::Deserialize)] struct Import { versions: Vec, } let import: Import = json_body(request)?; let main = state.import(key, access, import.versions)?; ok(json!({ "main": main })) } // This server has no git view: git IDs from an import aren't needed. (Method::Post, ["git", "ids"]) => { if !access.can.admin { return Err(problem(403, "not_allowed", "only the project's owners can do that")); } let _ = read_body(request)?; ok(json!({ "added": 0 })) } (Method::Post, ["git", "resume"]) => Err(problem(400, "bad_usage", "this server has no git views")), (Method::Post, ["land"]) => { let land: LandRequest = json_body(request)?; ok(state.land(key, access, &land)?) } (Method::Get, ["rules"]) => ok(json!({ "rules": state.rules(key)? })), (Method::Put, ["rules"]) => { let rule: Rule = json_body(request)?; ok(json!({ "rules": state.set_rule(key, access, &rule)? })) } (Method::Get, ["mine"]) => { need_user(access)?; ok(json!({ "files": state.mine(key, &access.user.as_ref().unwrap().handle)? })) } (Method::Put, ["mine"]) => { need_user(access)?; #[derive(serde::Deserialize)] struct Files { files: Vec, } let files: Files = json_body(request)?; state.set_mine(key, &access.user.as_ref().unwrap().handle, &files.files)?; ok(json!({})) } (Method::Get, ["mine", "bundle"]) => { need_user(access)?; let files = state.mine(key, &access.user.as_ref().unwrap().handle)?; let mut ids = Vec::new(); for f in files { ids.extend(state.store.file_parts(&f.file)?); } Ok((200, Body::Bytes(state.store.bundle(&ids)?))) } _ => Err(Problem { status: 404, code: "not_found", message: "no such endpoint".into(), holder: None }), } }