2ae9196
ZhenYi
feat(lib/git): add HTTP handler tree with routes and errors across analysis, archives, blobs, branches, commits, conflicts, content, diffs, hooks, lfs, logs, maintenance, packs, patches, pipelines, refs, remotes, repos, signatures, submodules, tags, trees
2ae9196
use std::collections::HashMap;
use axum::Router;
use axum::body::Body;
use axum::extract::{Path, Query, Request, State};
use axum::http::{HeaderMap, Method};
use axum::response::{IntoResponse, Response};
use crate::share::access::{BasicAuth, allow_repo_read};
use crate::smart::error::{SmartError, SmartResult};
use crate::smart::lfs;
use crate::smart::object;
use crate::smart::rpc::GitService;
use crate::smart::service::SmartService;
use crate::smart::dispatch;
pub fn router(service: SmartService) -> Router {
Router::new()
.route(
"/{space}/{repo}/{*path}",
axum::routing::get(dispatch)
.post(dispatch)
.put(dispatch)
.head(dispatch),
)
.with_state(service)
}
async fn dispatch(
State(service): State<SmartService>,
Path((space, repo, path)): Path<(String, String, String)>,
Query(query): Query<HashMap<String, String>>,
headers: HeaderMap,
auth: BasicAuth,
req: Request<Body>,
) -> Response {
let method = req.method().clone();
let body = req.into_body();
let repo_name = match repo.strip_suffix(".git") {
Some(name) if !name.is_empty() => name.to_string(),
_ => return SmartError::BadRequest("expected '<repo>.git' in URL".into()).into_response(),
};
let path_lower = path.to_lowercase();
let result: SmartResult<Response> = match (path_lower.as_str(), method.clone()) {
("info/refs", m) if m == Method::GET || m == Method::HEAD => {
let service_kind = query.get("service").cloned();
match dispatch::info_refs(
&service,
&space,
&repo_name,
&headers,
&auth,
service_kind.as_deref(),
)
.await
{
Ok(mut resp) => {
if m == Method::HEAD {
*resp.body_mut() = Body::empty();
}
Ok(resp)
}
Err(e) => Err(e),
}
}
("git-upload-pack", Method::POST) => {
dispatch::rpc(
&service,
&space,
&repo_name,
&headers,
body,
&auth,
GitService::UploadPack,
)
.await
}
("git-receive-pack", Method::POST) => {
dispatch::rpc(
&service,
&space,
&repo_name,
&headers,
body,
&auth,
GitService::ReceivePack,
)
.await
}
(p, _) if p.starts_with("info/lfs/") => {
lfs::dispatch(&service, &space, &repo_name, &headers, p, method.clone(), body, &auth).await
}
(_, Method::GET) => {
let resp = match service.resolve(&space, &repo_name).await {
Ok(ctx) => match allow_repo_read(&service.git, &auth, &ctx).await {
Ok(_) => {
if !is_served_path(&path) {
SmartError::Forbidden("path not served".into()).into_response()
} else {
let bare = service.bare_path(&ctx);
match object::serve(&bare, &path).await {
Ok(resp) => resp,
Err(e) => e.into_response(),
}
}
}
Err(e) => e.into_response(),
},
Err(e) => e.into_response(),
};
Ok(resp)
},
_ => Ok(SmartError::BadRequest("method not allowed".into()).into_response()),
};
result.unwrap_or_else(|e| e.into_response())
}
/// Whitelist of paths the GET fallback may serve. The dumb HTTP protocol
/// fetches `info/refs`, `HEAD`, `objects/info/packs`, `objects/pack/*`
/// and `objects/<xx>/<rest>`; everything else (notably `config`,
/// `hooks/*`, `packed-refs`, `info/exclude`) is internal and must not be
/// exposed anonymously.
fn is_served_path(path: &str) -> bool {
if path == "info/refs" || path == "HEAD" {
return true;
}
if let Some(rest) = path.strip_prefix("objects/") {
if rest.starts_with("pack/") {
return true;
}
if rest == "info/packs" || rest.starts_with("info/") {
return true;
}
// objects/<2-char dir>/<38 hex> = loose object fetch
if rest.len() >= 3 && rest.as_bytes()[2] == b'/' && rest.is_ascii() {
let prefix = &rest[..2];
let after = &rest[3..];
if prefix.chars().all(|c| c.is_ascii_hexdigit()) && !after.is_empty() {
return true;
}
}
return false;
}
false
}