fix(dvcs): refactor askpass to pass token via env, gate --stateless-rpc for archive uploads, update handlers
zhenyi committed
Modified dvcs/git/gitcli.rs dvcs/git/gitcli.rs +15 −11 View dvcs/git/gitcli.rs
@@ -46,11 +46,11 @@
4646 match auth {
4747 RemoteAuth::None | RemoteAuth::SshKey(_) => {}
4848 RemoteAuth::Token(token) => {
49 let helper = write_askpass_helper(token)?;
50 let helper_path = helper.path.clone();
49 let helper = write_askpass_helper()?;
5150 cmd = cmd
52 .env("GIT_ASKPASS", &helper_path)
53 .env("GIT_TERMINAL_PROMPT", "0");
51 .env("GIT_ASKPASS", &helper.path)
52 .env("GIT_TERMINAL_PROMPT", "0")
53 .env("GIT_ASKPASS_TOKEN", token);
5454 askpass = Some(helper);
5555 }
5656 }
@@ -73,18 +73,22 @@
7373 }
7474}
7575
76fn write_askpass_helper(token: &str) -> Result<AskPassHelper, Error> {
76fn write_askpass_helper() -> Result<AskPassHelper, Error> {
7777 let dir = std::env::temp_dir();
78 let path = dir.join(format!("git-askpass-{}.sh", std::process::id()));
79 let script = format!(
80 "#!/bin/sh\nprintf '%s\\n' '{}'\n",
81 token.replace('\'', "'\\''")
82 );
78 let nonce = std::time::SystemTime::now()
79 .duration_since(std::time::UNIX_EPOCH)
80 .map(|d| d.subsec_nanos())
81 .unwrap_or(0);
82 let path = dir.join(format!(
83 "git-askpass-{}-{nonce:x}.sh",
84 std::process::id()
85 ));
86 let script = "#!/bin/sh\nprintf '%s\\n' \"$GIT_ASKPASS_TOKEN\"\n";
8387 std::fs::write(&path, script).map_err(|e| Error::Io(e.to_string()))?;
8488 #[cfg(unix)]
8589 {
8690 use std::os::unix::fs::PermissionsExt;
87 let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700));
91 let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
8892 }
8993 Ok(AskPassHelper { path })
9094}
Modified dvcs/gitsmart/handler/lfs.rs dvcs/gitsmart/handler/lfs.rs +16 −1 View dvcs/gitsmart/handler/lfs.rs
@@ -80,8 +80,21 @@
8080 );
8181
8282 let mut objects = Vec::with_capacity(req.objects.len());
83 let max_size = state.config.max_lfs_object_size;
8384 for obj in req.objects {
8485 let oid_str = obj.oid.clone();
86 if obj.size < 0 || obj.size as u64 > max_size {
87 objects.push(LfsBatchObject {
88 oid: obj.oid,
89 size: obj.size,
90 actions: None,
91 error: Some(git::lfs::types::LfsError {
92 code: 413,
93 message: format!("object size exceeds limit ({max_size} bytes)"),
94 }),
95 });
96 continue;
97 }
8598 let item = match req.operation {
8699 LfsOperation::Upload => {
87100 let present = lfs
@@ -216,7 +229,9 @@
216229 raw_stream
217230 };
218231
219 let limited = enforce_limit(stream, MAX_LFS_BODY);
232 let max_size = usize::try_from(state.config.max_lfs_object_size)
233 .unwrap_or(usize::MAX);
234 let limited = enforce_limit(stream, max_size);
220235 repo.lfs.put_object_stream(&oid, limited).await?;
221236 Ok(StatusCode::OK.into_response())
222237}
Modified dvcs/gitsmart/handler/mod.rs dvcs/gitsmart/handler/mod.rs +8 −1 View dvcs/gitsmart/handler/mod.rs
@@ -209,11 +209,18 @@
209209 .and_then(|v| v.to_str().ok())
210210 .map(|v| v.to_ascii_lowercase());
211211 if encoding.as_deref() == Some("gzip") {
212 let mut decoder = GzDecoder::new(bytes.as_ref());
212 let decoder = GzDecoder::new(bytes.as_ref());
213213 let mut out = Vec::new();
214 let cap = limit as u64 + 1;
214215 decoder
216 .take(cap)
215217 .read_to_end(&mut out)
216218 .map_err(|e| SmartError::BadRequest(format!("failed to decompress gzip body: {e}")))?;
219 if out.len() as u64 > limit as u64 {
220 return Err(SmartError::BadRequest(format!(
221 "decompressed body exceeds limit of {limit} bytes"
222 )));
223 }
217224 Ok(Bytes::from(out))
218225 } else {
219226 Ok(bytes)
Modified dvcs/gitsmart/service.rs dvcs/gitsmart/service.rs +6 −1 View dvcs/gitsmart/service.rs
@@ -89,11 +89,16 @@
8989 cache: self.cache.clone(),
9090 expire_ttl: self.config.object_cache_ttl,
9191 };
92 let lfs = Arc::new(gitlfs::DefaultLfsStore::new(
93 self.storage.clone(),
94 self.lfs_prefix(resolved),
95 ));
9296 Ok(git::GitRepository::new(
9397 self.bare_path(resolved),
9498 oc,
9599 self.storage.clone(),
96 ))
100 )
101 .with_lfs_store(lfs))
97102 }
98103}
99104
Modified dvcs/gitssh/handler/channel.rs dvcs/gitssh/handler/channel.rs +5 −4 View dvcs/gitssh/handler/channel.rs
@@ -128,10 +128,11 @@
128128 let _timer = crate::metrics::SubprocessTimer::start();
129129 let result: Result<(), crate::error::SshError> = async {
130130 let mut cmd = tokio::process::Command::new("git");
131 cmd.current_dir(op.bare())
132 .arg(op.service_arg())
133 .arg("--stateless-rpc")
134 .env("GIT_DIR", op.bare())
131 cmd.current_dir(op.bare()).arg(op.service_arg());
132 if !op.is_upload_archive() {
133 cmd.arg("--stateless-rpc");
134 }
135 cmd.env("GIT_DIR", op.bare())
135136 .env("GIT_TERMINAL_PROMPT", "0")
136137 .stdin(std::process::Stdio::piped())
137138 .stdout(std::process::Stdio::piped())
Modified dvcs/gitssh/handler/command.rs dvcs/gitssh/handler/command.rs +2 −1 View dvcs/gitssh/handler/command.rs
@@ -82,7 +82,8 @@
8282
8383 let op = match service {
8484 GitService::UploadPack => GitOperation::UploadPack { bare: bare.clone() },
85 _ => GitOperation::ReceivePack { bare: bare.clone() },
85 GitService::UploadArchive => GitOperation::UploadArchive { bare: bare.clone() },
86 GitService::ReceivePack => GitOperation::ReceivePack { bare: bare.clone() },
8687 };
8788 let state = self.channels.entry(channel).or_default();
8889 state.git_op = Some(op);
Modified dvcs/gitssh/handler/git_op.rs dvcs/gitssh/handler/git_op.rs +45 −1 View dvcs/gitssh/handler/git_op.rs
@@ -4,12 +4,15 @@
44pub enum GitOperation {
55 UploadPack { bare: PathBuf },
66 ReceivePack { bare: PathBuf },
7 UploadArchive { bare: PathBuf },
78}
89
910impl GitOperation {
1011 pub fn bare(&self) -> &PathBuf {
1112 match self {
12 Self::UploadPack { bare } | Self::ReceivePack { bare } => bare,
13 Self::UploadPack { bare }
14 | Self::ReceivePack { bare }
15 | Self::UploadArchive { bare } => bare,
1316 }
1417 }
1518
@@ -21,6 +24,47 @@
2124 match self {
2225 Self::UploadPack { .. } => "upload-pack",
2326 Self::ReceivePack { .. } => "receive-pack",
27 Self::UploadArchive { .. } => "upload-archive",
2428 }
2529 }
30
31 pub fn is_upload_archive(&self) -> bool {
32 matches!(self, Self::UploadArchive { .. })
33 }
34}
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39 use crate::git_service::GitService;
40
41 #[test]
42 fn service_to_operation_is_not_write_by_default() {
43 assert!(!GitService::UploadPack.is_write());
44 assert!(!GitService::UploadArchive.is_write());
45 assert!(GitService::ReceivePack.is_write());
46 }
47
48 #[test]
49 fn upload_archive_is_read_only_operation() {
50 let op = GitOperation::UploadArchive {
51 bare: PathBuf::from("/tmp/nonexistent-repo.git"),
52 };
53 assert!(!op.is_write());
54 assert_eq!(op.service_arg(), "upload-archive");
55 assert!(op.is_upload_archive());
56 }
57
58 #[test]
59 fn receive_pack_is_the_only_write_operation() {
60 let rp = GitOperation::ReceivePack {
61 bare: PathBuf::from("/tmp/r.git"),
62 };
63 let up = GitOperation::UploadPack {
64 bare: PathBuf::from("/tmp/u.git"),
65 };
66 assert!(rp.is_write());
67 assert!(!up.is_write());
68 assert!(!up.is_upload_archive());
69 }
2670}