OXYGENHUB
# 02f1b14af1e2 oxygen / crates / oxy-server / src / lib.rs
5.8 KiB · Rust · last changed HTTP transport: CBOR wire messages, HttpRemote client, oxy-server crate with bearer-token pushes, oxy serve 1 month ago source blame history raw
750251b2 Andy McCall ↶ 1
//! Reference HTTP server for the Oxygen wire protocol (spec §15.1, §16).
2
//!
3
//! Serves every repository directory under a root at `/{repo}/oxy/v1/...`.
4
//! TLS is expected to be terminated in front of this server for now; the
5
//! protocol's integrity does not depend on the channel (spec §15).
6
//!
7
//! Authentication: when bearer tokens are configured, `push` requires one;
8
//! reads are open. With no tokens configured everything is open (development).
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
    /// Directory containing one repository per subdirectory.
25
    pub root: PathBuf,
26
    /// Bearer tokens allowed to push; empty means no authentication.
27
    pub tokens: Vec<String>,
28
}
29
30
struct App {
31
    cfg: ServerConfig,
32
    /// One lock per repository so concurrent pushes serialize.
33
    locks: Mutex<HashMap<String, Arc<Mutex<()>>>>,
34
}
35
36
type Shared = Arc<App>;
37
38
struct Failure(StatusCode, String);
39
40
impl IntoResponse for Failure {
41
    fn into_response(self) -> Response {
42
        (self.0, self.1).into_response()
43
    }
44
}
45
46
impl From<oxy_core::repo::RepoError> for Failure {
47
    fn from(e: oxy_core::repo::RepoError) -> Failure {
48
        Failure(StatusCode::BAD_REQUEST, e.to_string())
49
    }
50
}
51
52
fn ok(body: Vec<u8>) -> Response {
53
    ([(header::CONTENT_TYPE, CONTENT_TYPE)], body).into_response()
54
}
55
56
fn valid_repo_name(name: &str) -> bool {
57
    !name.is_empty()
58
        && !name.starts_with('.')
59
        && name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
60
}
61
62
impl App {
63
    fn open(&self, name: &str) -> Result<Repo, Failure> {
64
        if !valid_repo_name(name) {
65
            return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into()));
66
        }
67
        let dir = self.cfg.root.join(name);
68
        if !dir.join(".oxy").is_dir() {
69
            return Err(Failure(StatusCode::NOT_FOUND, "no such repository".into()));
70
        }
71
        Repo::open(&dir).map_err(|e| Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
72
    }
73
74
    fn lock_for(&self, name: &str) -> Arc<Mutex<()>> {
75
        self.locks.lock().unwrap().entry(name.to_string()).or_default().clone()
76
    }
77
78
    fn authorize_push(&self, headers: &HeaderMap) -> Result<(), Failure> {
79
        if self.cfg.tokens.is_empty() {
80
            return Ok(());
81
        }
82
        let presented = headers
83
            .get(header::AUTHORIZATION)
84
            .and_then(|v| v.to_str().ok())
85
            .and_then(|v| v.strip_prefix("Bearer "))
86
            .map(str::trim);
87
        match presented {
88
            Some(t) if self.cfg.tokens.iter().any(|x| x == t) => Ok(()),
89
            _ => Err(Failure(StatusCode::UNAUTHORIZED, "a valid bearer token is required to push".into())),
90
        }
91
    }
92
}
93
94
async fn blocking<T: Send + 'static>(f: impl FnOnce() -> Result<T, Failure> + Send + 'static) -> Result<T, Failure> {
95
    tokio::task::spawn_blocking(f).await.map_err(|e| Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
96
}
97
98
async fn capabilities(State(app): State<Shared>, Path(repo): Path<String>) -> Result<Response, Failure> {
99
    blocking(move || {
100
        let r = app.open(&repo)?;
101
        Ok(ok(wire::encode_capabilities(r.id()?)))
102
    })
103
    .await
104
}
105
106
async fn refs(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_refs(r.id()?, &r.remote_refs()?)))
110
    })
111
    .await
112
}
113
114
async fn negotiate(State(app): State<Shared>, Path(repo): Path<String>, body: Bytes) -> Result<Response, Failure> {
115
    blocking(move || {
116
        let r = app.open(&repo)?;
117
        let (have, want) = wire::decode_negotiate(&body)?;
118
        Ok(ok(wire::encode_hashes(&r.negotiate(&have, &want)?)))
119
    })
120
    .await
121
}
122
123
async fn fetch(State(app): State<Shared>, Path(repo): Path<String>, body: Bytes) -> Result<Response, Failure> {
124
    blocking(move || {
125
        let r = app.open(&repo)?;
126
        let changes = wire::decode_hashes(&body)?;
127
        Ok(ok(wire::encode_bundle(&r.bundle_for(&changes)?)))
128
    })
129
    .await
130
}
131
132
async fn push(State(app): State<Shared>, Path(repo): Path<String>, headers: HeaderMap, body: Bytes) -> Result<Response, Failure> {
133
    app.authorize_push(&headers)?;
134
    blocking(move || {
135
        let r = app.open(&repo)?;
136
        let (bundle, updates) = wire::decode_push(&body)?;
137
        let lock = app.lock_for(&repo);
138
        let _guard = lock.lock().unwrap();
139
        Ok(ok(wire::encode_push_response(&r.receive_push(bundle, updates)?)))
140
    })
141
    .await
142
}
143
144
pub fn router(cfg: ServerConfig) -> Router {
145
    let app: Shared = Arc::new(App { cfg, locks: Mutex::new(HashMap::new()) });
146
    Router::new()
147
        .route("/{repo}/oxy/v1/capabilities", get(capabilities))
148
        .route("/{repo}/oxy/v1/refs", get(refs))
149
        .route("/{repo}/oxy/v1/negotiate", post(negotiate))
150
        .route("/{repo}/oxy/v1/fetch", post(fetch))
151
        .route("/{repo}/oxy/v1/push", post(push))
152
        .with_state(app)
153
}
154
155
/// Serve on an already-bound listener until the process ends.
156
pub async fn serve(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> {
157
    listener.set_nonblocking(true)?;
158
    let listener = tokio::net::TcpListener::from_std(listener)?;
159
    axum::serve(listener, router(cfg)).await
160
}
161
162
/// Build a runtime and serve; for the CLI and for tests on a background thread.
163
pub fn run_blocking(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> {
164
    let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?;
165
    rt.block_on(serve(listener, cfg))
166
}