Skip to content
Clone
69f12aa
192 lines 6.0 KB Rust
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::dispatch;
use crate::smart::error::{SmartError, SmartResult};
use crate::smart::lfs;
use crate::smart::object;
use crate::smart::rpc::GitService;
use crate::smart::service::SmartService;
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)
.layer(export::telemetry::http_trace_layer())
.layer(export::telemetry::metrics_layer())
.layer(export::middleware::AppTraceLayer::new())
}
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();
tracing::debug!(
method = %method,
space = %space,
repo = %repo,
path = %path,
"smart http request received"
);
// git 客户端(含 actions/checkout)既可能访问 /<space>/<repo>.git,也可能省略
// .git 后缀,两种形态都要落到同一个仓库。
let repo_name = repo.strip_suffix(".git").unwrap_or(&repo).to_string();
if repo_name.is_empty() {
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()),
};
let outcome = if result.is_ok() { "ok" } else { "error" };
export::metrics::git_op(op_of(&path_lower), outcome);
result.unwrap_or_else(|e| {
tracing::warn!(
method = %method,
space = %space,
repo = %repo_name,
path = %path,
error = %e,
"smart http request failed"
);
e.into_response()
})
}
fn op_of(path: &str) -> &'static str {
match path {
"info/refs" => "info_refs",
"git-upload-pack" => "upload_pack",
"git-receive-pack" => "receive_pack",
p if p.starts_with("info/lfs/") => "lfs",
_ => "objects",
}
}
/// 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
}