OXYGENHUB
# e1078e99c560 oxygenhub / crates / hub / src / protocol.rs
4.9 KiB · Rust · last changed Push log records states, heads and change counts from the push report 1 month ago source blame history raw
d23464c0 Andy McCall ↶ 1
//! The wire protocol, embedded (hub spec §3.3, §5.4): `oxy-server`'s
2
//! `Server` over a backend that resolves `owner/name`, applies visibility
3
//! and push tokens, and records pushes.
4
5
use crate::repos::{RepoName, Repos};
6
use crate::{db, Shared};
7
use axum::body::Bytes;
8
use axum::extract::{Path, State};
9
use axum::http::{HeaderMap, Method, StatusCode};
10
use axum::response::{IntoResponse, Response};
11
use hub_api::Visibility;
62d6e969 Andy McCall ↶ 12
use oxy_core::repo::{PushReport, Repo};
d23464c0 Andy McCall ↶ 13
use oxy_server::{Access, Backend, Endpoint, Failure};
14
use std::sync::Arc;
15
16
pub struct HubBackend {
17
    repos: Arc<Repos>,
18
    pool: db::Pool,
19
    push_tokens: Vec<String>,
20
    runtime: tokio::runtime::Handle,
21
}
22
23
impl HubBackend {
24
    pub fn new(repos: Arc<Repos>, pool: db::Pool, push_tokens: Vec<String>, runtime: tokio::runtime::Handle) -> HubBackend {
25
        HubBackend { repos, pool, push_tokens, runtime }
26
    }
27
28
    fn row(&self, name: &RepoName) -> Result<db::repos::RepoRow, Failure> {
29
        let pool = self.pool.clone();
30
        let (owner, repo) = (name.owner.clone(), name.name.clone());
31
        let row = tokio::task::block_in_place(|| self.runtime.block_on(db::repos::get(&pool, &owner, &repo)));
32
        match row {
33
            Ok(Some(r)) => Ok(r),
34
            Ok(None) => Err(not_found()),
35
            Err(e) => Err(Failure(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
36
        }
37
    }
38
39
    fn has_push_token(&self, headers: &HeaderMap) -> bool {
40
        match oxy_server::bearer_token(headers) {
41
            Some(t) => self.push_tokens.iter().any(|x| x == t),
42
            None => false,
43
        }
44
    }
45
}
46
47
fn not_found() -> Failure {
48
    Failure(StatusCode::NOT_FOUND, "no such repository".into())
49
}
50
51
impl Backend for HubBackend {
52
    fn open(&self, name: &str) -> Result<Repo, Failure> {
53
        let name = RepoName::parse(name).ok_or_else(not_found)?;
54
        self.repos.open(&name).map_err(|e| match e {
55
            crate::repos::RepoError::NotFound => not_found(),
56
            other => Failure(StatusCode::INTERNAL_SERVER_ERROR, other.to_string()),
57
        })
58
    }
59
60
    fn authorize(&self, name: &str, access: Access, headers: &HeaderMap) -> Result<(), Failure> {
61
        let parsed = RepoName::parse(name).ok_or_else(not_found)?;
62
        let row = self.row(&parsed)?;
63
        let token = self.has_push_token(headers);
64
        // Until accounts exist (hub 0.2) a push token is the only credential:
65
        // it reads everything and may push everywhere.
66
        if row.visibility() != Visibility::Public && !token {
67
            return Err(not_found());
68
        }
69
        match access {
70
            Access::Read => Ok(()),
71
            Access::Push if self.push_tokens.is_empty() || token => Ok(()),
72
            Access::Push => Err(Failure(StatusCode::UNAUTHORIZED, "a valid bearer token is required to push".into())),
73
        }
74
    }
75
62d6e969 Andy McCall ↶ 76
    fn after_push(&self, name: &str, report: &PushReport) {
d23464c0 Andy McCall ↶ 77
        let Some(parsed) = RepoName::parse(name) else { return };
78
        let Ok(row) = self.row(&parsed) else { return };
79
        let pool = self.pool.clone();
62d6e969 Andy McCall ↶ 80
        let results: Vec<db::repos::PushOutcome> = report
81
            .results
82
            .iter()
83
            .map(|r| {
84
                let updated = report.updated.iter().find(|u| u.name == r.name);
85
                db::repos::PushOutcome {
86
                    reference: r.name.clone(),
87
                    ok: r.ok,
88
                    reason: r.reason.clone(),
89
                    old_state: updated.map(|u| u.old_state.to_hex()).unwrap_or_default(),
90
                    new_state: updated.map(|u| u.new_state.to_hex()).unwrap_or_default(),
91
                    heads: updated.map(|u| u.heads.len() as i64).unwrap_or(0),
92
                    changes: if r.ok { report.imported.len() as i64 } else { 0 },
93
                }
94
            })
95
            .collect();
d23464c0 Andy McCall ↶ 96
        self.runtime.spawn(async move {
97
            if let Err(e) = db::repos::record_push(&pool, row.id, &results).await {
98
                tracing::warn!("recording push to {}: {e}", parsed);
99
            }
100
        });
101
    }
102
}
103
104
/// `/{owner}/{name}/oxy/v1/{endpoint}`.
105
pub async fn dispatch(
106
    State(app): State<Shared>,
107
    Path((owner, name, endpoint)): Path<(String, String, String)>,
108
    method: Method,
109
    headers: HeaderMap,
110
    body: Bytes,
111
) -> Response {
112
    let Some(endpoint) = Endpoint::parse(&endpoint) else {
113
        return Failure(StatusCode::NOT_FOUND, "no such endpoint".into()).into_response();
114
    };
115
    let Some(repo) = RepoName::new(&owner, &name) else {
116
        return not_found().into_response();
117
    };
118
    // Pushes and checkpoints write; everything else reads.
119
    let _guard = match endpoint {
120
        Endpoint::Push | Endpoint::Checkpoint => Guard::Write(app.repos.write_guard(&repo).await),
121
        _ => Guard::Read(app.repos.read_guard(&repo).await),
122
    };
123
    app.server.handle(repo.to_string(), endpoint, &method, &headers, body).await
124
}
125
126
enum Guard {
127
    Read(#[allow(dead_code)] tokio::sync::OwnedRwLockReadGuard<()>),
128
    Write(#[allow(dead_code)] tokio::sync::OwnedRwLockWriteGuard<()>),
129
}