| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
use axum::body::Bytes; |
| 11 |
use axum::extract::{Path, State}; |
| 12 |
use axum::http::{header, HeaderMap, StatusCode}; |
| 13 |
use axum::response::{IntoResponse, Response}; |
| 14 |
use axum::routing::{get, post}; |
| 15 |
use axum::Router; |
| 16 |
use oxy_core::protocol::{wire, CONTENT_TYPE}; |
| 17 |
use oxy_core::repo::Repo; |
| 18 |
use std::collections::HashMap; |
| 19 |
use std::path::PathBuf; |
| 20 |
use std::sync::{Arc, Mutex}; |
| 21 |
|
| 22 |
#[derive(Clone, Debug)] |
| 23 |
pub struct ServerConfig { |
| 24 |
|
| 25 |
pub root: PathBuf, |
| 26 |
|
| 27 |
pub tokens: Vec<String>, |
| 28 |
|
| 29 |
pub tls: Option<TlsFiles>, |
| 30 |
} |
| 31 |
|
| 32 |
#[derive(Clone, Debug)] |
| 33 |
pub struct TlsFiles { |
| 34 |
pub cert: PathBuf, |
| 35 |
pub key: PathBuf, |
| 36 |
} |
| 37 |
|
| 38 |
struct App { |
| 39 |
cfg: ServerConfig, |
| 40 |
|
| 41 |
locks: Mutex<HashMap<String, Arc<Mutex<()>>>>, |
| 42 |
} |
| 43 |
|
| 44 |
type Shared = Arc<App>; |
| 45 |
|
| 46 |
struct Failure(StatusCode, String); |
| 47 |
|
| 48 |
impl IntoResponse for Failure { |
| 49 |
fn into_response(self) -> Response { |
| 50 |
(self.0, self.1).into_response() |
| 51 |
} |
| 52 |
} |
| 53 |
|
| 54 |
impl From<oxy_core::repo::RepoError> for Failure { |
| 55 |
fn from(e: oxy_core::repo::RepoError) -> Failure { |
| 56 |
Failure(StatusCode::BAD_REQUEST, e.to_string()) |
| 57 |
} |
| 58 |
} |
| 59 |
|
| 60 |
fn ok(body: Vec<u8>) -> Response { |
| 61 |
([(header::CONTENT_TYPE, CONTENT_TYPE)], body).into_response() |
| 62 |
} |
| 63 |
|
| 64 |
fn valid_repo_name(name: &str) -> bool { |
| 65 |
!name.is_empty() |
| 66 |
&& !name.starts_with('.') |
| 67 |
&& name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) |
| 68 |
} |
| 69 |
|
| 70 |
impl App { |
| 71 |
fn open(&self, name: &str) -> Result<Repo, Failure> { |
| 72 |
if !valid_repo_name(name) { |
| 73 |
return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into())); |
| 74 |
} |
| 75 |
let dir = self.cfg.root.join(name); |
| 76 |
if !dir.join(".oxy").is_dir() { |
| 77 |
return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into())); |
| 78 |
} |
| 79 |
Repo::open(&dir).map_err(|e| Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) |
| 80 |
} |
| 81 |
|
| 82 |
fn lock_for(&self, name: &str) -> Arc<Mutex<()>> { |
| 83 |
self.locks.lock().unwrap().entry(name.to_string()).or_default().clone() |
| 84 |
} |
| 85 |
|
| 86 |
fn authorize_push(&self, headers: &HeaderMap) -> Result<(), Failure> { |
| 87 |
if self.cfg.tokens.is_empty() { |
| 88 |
return Ok(()); |
| 89 |
} |
| 90 |
let presented = headers |
| 91 |
.get(header::AUTHORIZATION) |
| 92 |
.and_then(|v| v.to_str().ok()) |
| 93 |
.and_then(|v| v.strip_prefix("Bearer ")) |
| 94 |
.map(str::trim); |
| 95 |
match presented { |
| 96 |
Some(t) if self.cfg.tokens.iter().any(|x| x == t) => Ok(()), |
| 97 |
_ => Err(Failure(StatusCode::UNAUTHORIZED, "a valid bearer token is required to push".into())), |
| 98 |
} |
| 99 |
} |
| 100 |
} |
| 101 |
|
| 102 |
async fn blocking<T: Send + 'static>(f: impl FnOnce() -> Result<T, Failure> + Send + 'static) -> Result<T, Failure> { |
| 103 |
tokio::task::spawn_blocking(f).await.map_err(|e| Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? |
| 104 |
} |
| 105 |
|
| 106 |
async fn capabilities(State(app): State<Shared>, Path(repo): Path<String>) -> Result<Response, Failure> { |
| 107 |
blocking(move || { |
| 108 |
let r = app.open(&repo)?; |
| 109 |
Ok(ok(wire::encode_capabilities(r.id()?))) |
| 110 |
}) |
| 111 |
.await |
| 112 |
} |
| 113 |
|
| 114 |
async fn refs(State(app): State<Shared>, Path(repo): Path<String>) -> Result<Response, Failure> { |
| 115 |
blocking(move || { |
| 116 |
let r = app.open(&repo)?; |
| 117 |
Ok(ok(wire::encode_refs(r.id()?, &r.remote_refs()?))) |
| 118 |
}) |
| 119 |
.await |
| 120 |
} |
| 121 |
|
| 122 |
async fn negotiate(State(app): State<Shared>, Path(repo): Path<String>, body: Bytes) -> Result<Response, Failure> { |
| 123 |
blocking(move || { |
| 124 |
let r = app.open(&repo)?; |
| 125 |
let (have, want) = wire::decode_negotiate(&body)?; |
| 126 |
Ok(ok(wire::encode_hashes(&r.negotiate(&have, &want)?))) |
| 127 |
}) |
| 128 |
.await |
| 129 |
} |
| 130 |
|
| 131 |
async fn fetch(State(app): State<Shared>, Path(repo): Path<String>, body: Bytes) -> Result<Response, Failure> { |
| 132 |
blocking(move || { |
| 133 |
let r = app.open(&repo)?; |
| 134 |
let (changes, max_blob) = wire::decode_fetch(&body)?; |
| 135 |
Ok(ok(wire::encode_bundle(&r.bundle_for_filtered(&changes, max_blob)?))) |
| 136 |
}) |
| 137 |
.await |
| 138 |
} |
| 139 |
|
| 140 |
async fn checkpoint(State(app): State<Shared>, Path(repo): Path<String>, body: Bytes) -> Result<Response, Failure> { |
| 141 |
blocking(move || { |
| 142 |
let r = app.open(&repo)?; |
| 143 |
let heads = wire::decode_hashes(&body)?; |
| 144 |
let lock = app.lock_for(&repo); |
| 145 |
let _guard = lock.lock().unwrap(); |
| 146 |
Ok(ok(wire::encode_bundle(&r.checkpoint_bundle_for(&heads)?))) |
| 147 |
}) |
| 148 |
.await |
| 149 |
} |
| 150 |
|
| 151 |
async fn shallow(State(app): State<Shared>, Path(repo): Path<String>, body: Bytes) -> Result<Response, Failure> { |
| 152 |
blocking(move || { |
| 153 |
let r = app.open(&repo)?; |
| 154 |
let (heads, depth) = wire::decode_shallow(&body)?; |
| 155 |
Ok(ok(wire::encode_hashes(&r.shallow_changes(&heads, depth)?))) |
| 156 |
}) |
| 157 |
.await |
| 158 |
} |
| 159 |
|
| 160 |
async fn objects(State(app): State<Shared>, Path(repo): Path<String>, body: Bytes) -> Result<Response, Failure> { |
| 161 |
blocking(move || { |
| 162 |
let r = app.open(&repo)?; |
| 163 |
let refs = wire::decode_hashes(&body)?; |
| 164 |
let bundle = oxy_core::protocol::Bundle { objects: r.objects_for(&refs)?, ..Default::default() }; |
| 165 |
Ok(ok(wire::encode_bundle(&bundle))) |
| 166 |
}) |
| 167 |
.await |
| 168 |
} |
| 169 |
|
| 170 |
async fn push(State(app): State<Shared>, Path(repo): Path<String>, headers: HeaderMap, body: Bytes) -> Result<Response, Failure> { |
| 171 |
app.authorize_push(&headers)?; |
| 172 |
blocking(move || { |
| 173 |
let r = app.open(&repo)?; |
| 174 |
let (bundle, updates, policy) = wire::decode_push(&body)?; |
| 175 |
let lock = app.lock_for(&repo); |
| 176 |
let _guard = lock.lock().unwrap(); |
| 177 |
Ok(ok(wire::encode_push_response(&r.receive_push(bundle, updates, policy)?))) |
| 178 |
}) |
| 179 |
.await |
| 180 |
} |
| 181 |
|
| 182 |
pub fn router(cfg: ServerConfig) -> Router { |
| 183 |
let app: Shared = Arc::new(App { cfg, locks: Mutex::new(HashMap::new()) }); |
| 184 |
Router::new() |
| 185 |
.route("/{repo}/oxy/v1/capabilities", get(capabilities)) |
| 186 |
.route("/{repo}/oxy/v1/refs", get(refs)) |
| 187 |
.route("/{repo}/oxy/v1/negotiate", post(negotiate)) |
| 188 |
.route("/{repo}/oxy/v1/fetch", post(fetch)) |
| 189 |
.route("/{repo}/oxy/v1/objects", post(objects)) |
| 190 |
.route("/{repo}/oxy/v1/checkpoint", post(checkpoint)) |
| 191 |
.route("/{repo}/oxy/v1/shallow", post(shallow)) |
| 192 |
.route("/{repo}/oxy/v1/push", post(push)) |
| 193 |
.with_state(app) |
| 194 |
} |
| 195 |
|
| 196 |
|
| 197 |
|
| 198 |
pub async fn serve(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> { |
| 199 |
listener.set_nonblocking(true)?; |
| 200 |
match cfg.tls.clone() { |
| 201 |
Some(tls) => { |
| 202 |
let _ = rustls::crypto::ring::default_provider().install_default(); |
| 203 |
let rustls_cfg = axum_server::tls_rustls::RustlsConfig::from_pem_file(&tls.cert, &tls.key) |
| 204 |
.await |
| 205 |
.map_err(|e| std::io::Error::other(format!("loading TLS files: {e}")))?; |
| 206 |
axum_server::from_tcp_rustls(listener, rustls_cfg)?.serve(router(cfg).into_make_service()).await |
| 207 |
} |
| 208 |
None => { |
| 209 |
let listener = tokio::net::TcpListener::from_std(listener)?; |
| 210 |
axum::serve(listener, router(cfg)).await |
| 211 |
} |
| 212 |
} |
| 213 |
} |
| 214 |
|
| 215 |
|
| 216 |
pub fn run_blocking(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> { |
| 217 |
let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?; |
| 218 |
rt.block_on(serve(listener, cfg)) |
| 219 |
} |