Skip to content

Commit ade86d2

Browse files
Support push + force push over SSH (#1323)
* Support receive-pack over SSH * Improve tracing in push routines * Improve error propagation between hooks and proxy * Improve push option handling: use predefined struct * Add `force` push option commit-id:2dbf5e93
1 parent cd9a4bb commit ade86d2

34 files changed

+626
-396
lines changed

josh-proxy/src/bin/josh-proxy.rs

+144-99
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ use josh::{josh_error, JoshError, JoshResult};
2222
use josh_rpc::calls::RequestedCommand;
2323
use serde::Serialize;
2424
use std::collections::HashMap;
25+
use std::ffi::OsStr;
2526
use std::io;
2627
use std::net::IpAddr;
27-
use std::path::PathBuf;
28+
use std::path::{Path, PathBuf};
2829
use std::process::Stdio;
2930
use std::str::FromStr;
3031
use std::sync::{Arc, RwLock};
@@ -729,14 +730,15 @@ async fn serve_namespace(
729730
params: &josh_rpc::calls::ServeNamespace,
730731
repo_path: std::path::PathBuf,
731732
namespace: &str,
733+
repo_update: RepoUpdate,
732734
) -> josh::JoshResult<()> {
733735
const SERVE_TIMEOUT: u64 = 60;
734736

735737
tracing::trace!(
736-
"serve_namespace: command: {:?}, query: {}, namespace: {}",
737-
params.command,
738-
params.query,
739-
namespace
738+
command = ?params.command,
739+
query = %params.query,
740+
namespace = %namespace,
741+
"serve_namespace",
740742
);
741743

742744
enum ServeError {
@@ -746,25 +748,24 @@ async fn serve_namespace(
746748
SubprocessExited(i32),
747749
}
748750

749-
if params.command == RequestedCommand::GitReceivePack {
750-
return Err(josh_error("Push over SSH is not supported"));
751-
}
752-
753751
let command = match params.command {
754752
RequestedCommand::GitUploadPack => "git-upload-pack",
755753
RequestedCommand::GitUploadArchive => "git-upload-archive",
756754
RequestedCommand::GitReceivePack => "git-receive-pack",
757755
};
758756

757+
let overlay_path = repo_path.join("overlay");
758+
759759
let mut process = tokio::process::Command::new(command)
760-
.arg(repo_path.join("overlay"))
761-
.current_dir(repo_path.join("overlay"))
760+
.arg(&overlay_path)
761+
.current_dir(&overlay_path)
762762
.env("GIT_DIR", &repo_path)
763763
.env("GIT_NAMESPACE", namespace)
764764
.env(
765765
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
766766
repo_path.join("mirror").join("objects"),
767767
)
768+
.env("JOSH_REPO_UPDATE", serde_json::to_string(&repo_update)?)
768769
.stdin(Stdio::piped())
769770
.stdout(Stdio::piped())
770771
.spawn()?;
@@ -912,6 +913,31 @@ fn head_ref_or_default(head_ref: &str) -> HeadRef {
912913
}
913914
}
914915

916+
fn make_repo_update(
917+
remote_url: &str,
918+
serv: Arc<JoshProxyService>,
919+
filter: josh::filter::Filter,
920+
remote_auth: RemoteAuth,
921+
meta: &MetaConfig,
922+
repo_path: &Path,
923+
ns: Arc<josh_proxy::TmpGitNamespace>,
924+
) -> RepoUpdate {
925+
let context_propagator = josh_proxy::trace::make_context_propagator();
926+
927+
RepoUpdate {
928+
refs: HashMap::new(),
929+
remote_url: remote_url.to_string(),
930+
remote_auth,
931+
port: serv.port.clone(),
932+
filter_spec: josh::filter::spec(filter),
933+
base_ns: josh::to_ns(&meta.config.repo),
934+
git_ns: ns.name().to_string(),
935+
git_dir: repo_path.display().to_string(),
936+
mirror_git_dir: serv.repo_path.join("mirror").display().to_string(),
937+
context_propagator,
938+
}
939+
}
940+
915941
async fn handle_serve_namespace_request(
916942
serv: Arc<JoshProxyService>,
917943
req: Request<hyper::Body>,
@@ -958,6 +984,9 @@ async fn handle_serve_namespace_request(
958984
));
959985
};
960986

987+
eprintln!("params: {:?}", params);
988+
eprintln!("parsed_url.upstream_repo: {:?}", parsed_url.upstream_repo);
989+
961990
let auth_socket = params.ssh_socket.clone();
962991
let remote_auth = RemoteAuth::Ssh {
963992
auth_socket: auth_socket.clone(),
@@ -1000,24 +1029,34 @@ async fn handle_serve_namespace_request(
10001029
let remote_url = upstream + meta_config.config.repo.as_str();
10011030
let head_ref = head_ref_or_default(&parsed_url.headref);
10021031

1003-
let remote_refs = [head_ref.get()];
1004-
let remote_refs = match ssh_list_refs(&remote_url, auth_socket, Some(&remote_refs)).await {
1005-
Ok(remote_refs) => remote_refs,
1006-
Err(e) => {
1007-
return Ok(make_response(
1008-
hyper::Body::from(e.to_string()),
1009-
hyper::StatusCode::FORBIDDEN,
1010-
))
1011-
}
1012-
};
1032+
let resolved_ref = match params.command {
1033+
// When pushing over SSH, we need to fetch to get new references
1034+
// for searching for unapply base, so we don't bother with additional cache checks
1035+
RequestedCommand::GitReceivePack => None,
1036+
// Otherwise, list refs - it doesn't need locking and is faster -
1037+
// and use results to potentially skip fetching
1038+
_ => {
1039+
let remote_refs = [head_ref.get()];
1040+
let remote_refs =
1041+
match ssh_list_refs(&remote_url, auth_socket, Some(&remote_refs)).await {
1042+
Ok(remote_refs) => remote_refs,
1043+
Err(e) => {
1044+
return Ok(make_response(
1045+
hyper::Body::from(e.to_string()),
1046+
hyper::StatusCode::FORBIDDEN,
1047+
))
1048+
}
1049+
};
10131050

1014-
let resolved_ref = match remote_refs.get(head_ref.get()) {
1015-
Some(resolved_ref) => resolved_ref,
1016-
None => {
1017-
return Ok(make_response(
1018-
hyper::Body::from("Could not resolve remote ref"),
1019-
hyper::StatusCode::INTERNAL_SERVER_ERROR,
1020-
))
1051+
match remote_refs.get(head_ref.get()) {
1052+
Some(resolved_ref) => Some(resolved_ref.clone()),
1053+
None => {
1054+
return Ok(make_response(
1055+
hyper::Body::from("Could not resolve remote ref"),
1056+
hyper::StatusCode::INTERNAL_SERVER_ERROR,
1057+
))
1058+
}
1059+
}
10211060
}
10221061
};
10231062

@@ -1027,7 +1066,7 @@ async fn handle_serve_namespace_request(
10271066
&remote_auth,
10281067
remote_url.to_owned(),
10291068
Some(head_ref.get()),
1030-
Some(resolved_ref),
1069+
resolved_ref.as_deref(),
10311070
false,
10321071
)
10331072
.await
@@ -1095,7 +1134,19 @@ async fn handle_serve_namespace_request(
10951134
}
10961135
};
10971136

1098-
let serve_result = serve_namespace(&params, serv.repo_path.clone(), temp_ns.name()).await;
1137+
let overlay_path = serv.repo_path.join("overlay");
1138+
let repo_update = make_repo_update(
1139+
&remote_url,
1140+
serv.clone(),
1141+
filter,
1142+
remote_auth,
1143+
&meta_config,
1144+
&overlay_path,
1145+
temp_ns.clone(),
1146+
);
1147+
1148+
let serve_result =
1149+
serve_namespace(&params, serv.repo_path.clone(), temp_ns.name(), repo_update).await;
10991150
std::mem::drop(temp_ns);
11001151

11011152
match serve_result {
@@ -1295,68 +1346,39 @@ async fn call_service(
12951346
}
12961347

12971348
let temp_ns = prepare_namespace(serv.clone(), &meta, filter, &headref).await?;
1349+
let overlay_path = serv.repo_path.join("overlay");
12981350

1299-
let repo_path = serv
1300-
.repo_path
1301-
.join("overlay")
1302-
.to_str()
1303-
.ok_or(josh::josh_error("repo_path.to_str"))?
1304-
.to_string();
1305-
1306-
let mirror_repo_path = serv
1307-
.repo_path
1308-
.join("mirror")
1309-
.to_str()
1310-
.ok_or(josh::josh_error("repo_path.to_str"))?
1311-
.to_string();
1312-
1313-
let context_propagator = {
1314-
let span = tracing::Span::current();
1315-
1316-
let mut context_propagator = HashMap::<String, String>::default();
1317-
let context = span.context();
1318-
global::get_text_map_propagator(|propagator| {
1319-
propagator.inject_context(&context, &mut context_propagator);
1320-
});
1321-
1322-
tracing::debug!("context propagator: {:?}", context_propagator);
1323-
context_propagator
1324-
};
1325-
1326-
let repo_update = josh_proxy::RepoUpdate {
1327-
refs: HashMap::new(),
1328-
remote_url: remote_url.clone(),
1351+
let repo_update = make_repo_update(
1352+
&remote_url,
1353+
serv.clone(),
1354+
filter,
13291355
remote_auth,
1330-
port: serv.port.clone(),
1331-
filter_spec: josh::filter::spec(filter),
1332-
base_ns: josh::to_ns(&meta.config.repo),
1333-
git_ns: temp_ns.name().to_string(),
1334-
git_dir: repo_path.clone(),
1335-
mirror_git_dir: mirror_repo_path.clone(),
1336-
context_propagator,
1337-
};
1356+
&meta,
1357+
&overlay_path,
1358+
temp_ns.clone(),
1359+
);
13381360

13391361
let cgi_response = async {
13401362
let mut cmd = Command::new("git");
13411363
cmd.arg("http-backend");
1342-
cmd.current_dir(&serv.repo_path.join("overlay"));
1343-
cmd.env("GIT_DIR", &repo_path);
1364+
cmd.current_dir(&overlay_path);
1365+
cmd.env("GIT_DIR", &overlay_path);
13441366
cmd.env("GIT_HTTP_EXPORT_ALL", "");
13451367
cmd.env(
13461368
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
13471369
serv.repo_path
13481370
.join("mirror")
13491371
.join("objects")
1350-
.to_str()
1351-
.ok_or(josh::josh_error("repo_path.to_str"))?,
1372+
.display()
1373+
.to_string(),
13521374
);
13531375
cmd.env("GIT_NAMESPACE", temp_ns.name());
1354-
cmd.env("GIT_PROJECT_ROOT", repo_path);
1376+
cmd.env("GIT_PROJECT_ROOT", &overlay_path);
13551377
cmd.env("JOSH_REPO_UPDATE", serde_json::to_string(&repo_update)?);
13561378
cmd.env("PATH_INFO", parsed_url.pathinfo.clone());
13571379

13581380
let (response, stderr) = hyper_cgi::do_cgi(req, cmd).await;
1359-
tracing::debug!("Git stderr: {}", String::from_utf8_lossy(&stderr));
1381+
tracing::debug!(stderr = %String::from_utf8_lossy(&stderr), "http-backend exited");
13601382

13611383
Ok::<_, JoshError>(response)
13621384
}
@@ -1655,35 +1677,41 @@ async fn run_housekeeping(local: std::path::PathBuf) -> josh::JoshResult<()> {
16551677
}
16561678
}
16571679

1680+
fn repo_update_from_env() -> josh::JoshResult<josh_proxy::RepoUpdate> {
1681+
let repo_update =
1682+
std::env::var("JOSH_REPO_UPDATE").map_err(|_| josh_error("JOSH_REPO_UPDATE not set"))?;
1683+
1684+
serde_json::from_str(&repo_update)
1685+
.map_err(|e| josh_error(&format!("Failed to parse JOSH_REPO_UPDATE: {}", e)))
1686+
}
1687+
16581688
fn pre_receive_hook() -> josh::JoshResult<i32> {
1659-
let repo_update: josh_proxy::RepoUpdate =
1660-
serde_json::from_str(&std::env::var("JOSH_REPO_UPDATE")?)?;
1689+
let repo_update = repo_update_from_env()?;
16611690

1662-
let p = std::path::PathBuf::from(repo_update.git_dir)
1691+
let push_options_path = std::path::PathBuf::from(repo_update.git_dir)
16631692
.join("refs/namespaces")
16641693
.join(repo_update.git_ns)
16651694
.join("push_options");
16661695

1667-
let n: usize = std::env::var("GIT_PUSH_OPTION_COUNT")?.parse()?;
1696+
let push_option_count: usize = std::env::var("GIT_PUSH_OPTION_COUNT")?.parse()?;
16681697

1669-
let mut push_options = std::collections::HashMap::<String, String>::new();
1670-
for i in 0..n {
1671-
let s = std::env::var(format!("GIT_PUSH_OPTION_{}", i))?;
1672-
if let [key, value] = s.as_str().split('=').collect::<Vec<_>>().as_slice() {
1673-
push_options.insert(key.to_string(), value.to_string());
1698+
let mut push_options = HashMap::<String, serde_json::Value>::new();
1699+
for i in 0..push_option_count {
1700+
let push_option = std::env::var(format!("GIT_PUSH_OPTION_{}", i))?;
1701+
if let Some((key, value)) = push_option.split_once("=") {
1702+
push_options.insert(key.into(), value.into());
16741703
} else {
1675-
push_options.insert(s, "".to_string());
1704+
push_options.insert(push_option, true.into());
16761705
}
16771706
}
16781707

1679-
std::fs::write(p, serde_json::to_string(&push_options)?)?;
1708+
std::fs::write(push_options_path, serde_json::to_string(&push_options)?)?;
16801709

16811710
Ok(0)
16821711
}
16831712

16841713
fn update_hook(refname: &str, old: &str, new: &str) -> josh::JoshResult<i32> {
1685-
let mut repo_update: josh_proxy::RepoUpdate =
1686-
serde_json::from_str(&std::env::var("JOSH_REPO_UPDATE")?)?;
1714+
let mut repo_update = repo_update_from_env()?;
16871715

16881716
repo_update
16891717
.refs
@@ -1696,24 +1724,33 @@ fn update_hook(refname: &str, old: &str, new: &str) -> josh::JoshResult<i32> {
16961724
.send();
16971725

16981726
match resp {
1699-
Ok(r) => {
1700-
let success = r.status().is_success();
1701-
if let Ok(body) = r.text() {
1702-
println!("response from upstream:\n{}\n\n", body);
1703-
} else {
1704-
println!("no upstream response");
1727+
Ok(resp) => {
1728+
let success = resp.status().is_success();
1729+
println!("upstream: response status: {}", resp.status());
1730+
1731+
match resp.text() {
1732+
Ok(text) if text.trim().is_empty() => {
1733+
println!("upstream: no response body");
1734+
}
1735+
Ok(text) => {
1736+
println!("upstream: response body:\n\n{}", text);
1737+
}
1738+
Err(err) => {
1739+
println!("upstream: warn: failed to read response body: {:?}", err);
1740+
}
17051741
}
1742+
17061743
if success {
1707-
return Ok(0);
1744+
Ok(0)
17081745
} else {
1709-
return Ok(1);
1746+
Ok(1)
17101747
}
17111748
}
17121749
Err(err) => {
17131750
tracing::warn!("/repo_update request failed {:?}", err);
1751+
Ok(1)
17141752
}
1715-
};
1716-
Ok(1)
1753+
}
17171754
}
17181755

17191756
async fn serve_graphql(
@@ -1949,8 +1986,16 @@ fn main() {
19491986

19501987
if let [a0, ..] = &std::env::args().collect::<Vec<_>>().as_slice() {
19511988
if a0.ends_with("/pre-receive") {
1952-
println!("josh-proxy");
1953-
std::process::exit(pre_receive_hook().unwrap_or(1));
1989+
eprintln!("josh-proxy: pre-receive hook");
1990+
let code = match pre_receive_hook() {
1991+
Ok(code) => code,
1992+
Err(e) => {
1993+
eprintln!("josh-proxy: pre-receive hook failed: {}", e);
1994+
std::process::exit(1);
1995+
}
1996+
};
1997+
1998+
std::process::exit(code);
19541999
}
19552000
}
19562001

0 commit comments

Comments
 (0)