Onboard iPhone password-store clone
This commit is contained in:
1 parent
db898152bf
commit
daf122aff3
11 files changed
+2770
-38
No files matched your search
+231
-34
@@ -788,18 +788,91 @@ impl FetchOutcome {
|
||||
}
|
||||
|
||||
impl GitRepository {
|
||||
pub fn discover_remote_branches(
|
||||
parent: &Path,
|
||||
identity: GitIdentity,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<Vec<String>, GitError> {
|
||||
Self::discover_remote_branches_with_transport(
|
||||
parent,
|
||||
identity,
|
||||
configured,
|
||||
credentials,
|
||||
&EmbeddedFetchTransport,
|
||||
control,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn discover_remote_branches_with_transport(
|
||||
parent: &Path,
|
||||
identity: GitIdentity,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
transport: &impl GitFetchTransport,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<Vec<String>, GitError> {
|
||||
control.report(GitProgressPhase::Validating)?;
|
||||
validate_https_remote(configured.url().as_str())?;
|
||||
ensure_clone_parent(parent)?;
|
||||
let temporary = private_temporary_directory(parent, "probe")?;
|
||||
let result = (|| {
|
||||
let store = Repository::open(&temporary).map_err(invalid)?;
|
||||
gix::ThreadSafeRepository::init_opts(
|
||||
store.root_path(),
|
||||
gix::create::Kind::WithWorktree,
|
||||
gix::create::Options::default(),
|
||||
isolated_options(),
|
||||
)
|
||||
.map_err(invalid)?;
|
||||
let mut repository = Self::open(&store, identity)?;
|
||||
repository.add_remote(configured.name().as_str(), configured.url().as_str())?;
|
||||
repository.fetch_with_transport_controlled(
|
||||
configured,
|
||||
credentials,
|
||||
transport,
|
||||
control,
|
||||
)?;
|
||||
repository.remote_branch_names(configured.name().as_str())
|
||||
})();
|
||||
let _ = fs::remove_dir_all(&temporary);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn clone_into(
|
||||
destination: &Path,
|
||||
identity: GitIdentity,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
) -> Result<Self, GitError> {
|
||||
Self::clone_into_with_transport(
|
||||
Self::clone_into_with_transport_controlled(
|
||||
destination,
|
||||
identity,
|
||||
configured,
|
||||
None,
|
||||
credentials,
|
||||
&EmbeddedFetchTransport,
|
||||
&GitOperationControl::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn clone_into_controlled(
|
||||
destination: &Path,
|
||||
identity: GitIdentity,
|
||||
configured: &GitRemote,
|
||||
branch: &str,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<Self, GitError> {
|
||||
Self::clone_into_with_transport_controlled(
|
||||
destination,
|
||||
identity,
|
||||
configured,
|
||||
Some(branch),
|
||||
credentials,
|
||||
&EmbeddedFetchTransport,
|
||||
control,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -810,17 +883,35 @@ impl GitRepository {
|
||||
credentials: &impl GitCredentialProvider,
|
||||
transport: &impl GitFetchTransport,
|
||||
) -> Result<Self, GitError> {
|
||||
Self::clone_into_with_transport_controlled(
|
||||
destination,
|
||||
identity,
|
||||
configured,
|
||||
None,
|
||||
credentials,
|
||||
transport,
|
||||
&GitOperationControl::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn clone_into_with_transport_controlled(
|
||||
destination: &Path,
|
||||
identity: GitIdentity,
|
||||
configured: &GitRemote,
|
||||
branch: Option<&str>,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
transport: &impl GitFetchTransport,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<Self, GitError> {
|
||||
control.report(GitProgressPhase::Validating)?;
|
||||
validate_https_remote(configured.url().as_str())?;
|
||||
if let Some(branch) = branch {
|
||||
validate_remote_name(branch)?;
|
||||
}
|
||||
let parent = destination.parent().ok_or_else(|| GitError::InvalidPath {
|
||||
path: destination.to_owned(),
|
||||
})?;
|
||||
let metadata =
|
||||
fs::symlink_metadata(parent).map_err(|_| io("inspect clone parent", parent))?;
|
||||
if !metadata.is_dir() || metadata.file_type().is_symlink() {
|
||||
return Err(GitError::UnsafeWorktreeObject {
|
||||
path: parent.to_owned(),
|
||||
});
|
||||
}
|
||||
ensure_clone_parent(parent)?;
|
||||
if let Ok(metadata) = fs::symlink_metadata(destination) {
|
||||
if !metadata.is_dir() || metadata.file_type().is_symlink() {
|
||||
return Err(GitError::UnsafeWorktreeObject {
|
||||
@@ -835,18 +926,7 @@ impl GitRepository {
|
||||
return Err(GitError::DirtyWorktree);
|
||||
}
|
||||
}
|
||||
let temporary = (0..128_u8)
|
||||
.find_map(|attempt| {
|
||||
let name = format!(".ironstorage-clone-{}-{attempt}", rand::random::<u64>());
|
||||
let candidate = parent.join(name);
|
||||
match fs::create_dir(&candidate) {
|
||||
Ok(()) => Some(Ok(candidate)),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => None,
|
||||
Err(_) => Some(Err(io("create clone directory", parent))),
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
.ok_or_else(|| io("create clone directory", parent))?;
|
||||
let temporary = private_temporary_directory(parent, "clone")?;
|
||||
let cloned = (|| {
|
||||
let store = Repository::open(&temporary).map_err(invalid)?;
|
||||
gix::ThreadSafeRepository::init_opts(
|
||||
@@ -858,17 +938,23 @@ impl GitRepository {
|
||||
.map_err(invalid)?;
|
||||
let mut repository = Self::open(&store, identity.clone())?;
|
||||
repository.add_remote(configured.name().as_str(), configured.url().as_str())?;
|
||||
repository.pull_with_transport(configured, None, credentials, transport)?;
|
||||
Ok(repository)
|
||||
repository.pull_with_transport_controlled(
|
||||
configured,
|
||||
branch,
|
||||
credentials,
|
||||
transport,
|
||||
control,
|
||||
)?;
|
||||
control.report(GitProgressPhase::Refreshing)?;
|
||||
Ok(())
|
||||
})();
|
||||
if let Err(error) = cloned {
|
||||
let _ = fs::remove_dir_all(&temporary);
|
||||
return Err(error);
|
||||
}
|
||||
drop(cloned);
|
||||
if destination.exists() {
|
||||
fs::remove_dir(destination)
|
||||
.map_err(|_| io("prepare clone destination", destination))?;
|
||||
if destination.exists() && fs::remove_dir(destination).is_err() {
|
||||
let _ = fs::remove_dir_all(&temporary);
|
||||
return Err(io("prepare clone destination", destination));
|
||||
}
|
||||
if fs::rename(&temporary, destination).is_err() {
|
||||
let _ = fs::remove_dir_all(&temporary);
|
||||
@@ -880,6 +966,42 @@ impl GitRepository {
|
||||
Self::open_at(destination.to_owned(), identity)
|
||||
}
|
||||
|
||||
fn remote_branch_names(&self, remote: &str) -> Result<Vec<String>, GitError> {
|
||||
validate_remote_name(remote)?;
|
||||
let prefix = format!("refs/remotes/{remote}/");
|
||||
let mut branches = self
|
||||
.repository
|
||||
.references()
|
||||
.map_err(invalid)?
|
||||
.remote_branches()
|
||||
.map_err(invalid)?
|
||||
.filter_map(|reference| {
|
||||
let reference = match reference {
|
||||
Ok(reference) => reference,
|
||||
Err(error) => return Some(Err(invalid(error))),
|
||||
};
|
||||
let name = match reference.name().as_bstr().to_str() {
|
||||
Ok(name) => name,
|
||||
Err(error) => return Some(Err(invalid(error))),
|
||||
};
|
||||
name.strip_prefix(&prefix)
|
||||
.filter(|name| *name != "HEAD")
|
||||
.map(|name| {
|
||||
validate_remote_name(name)?;
|
||||
Ok(name.to_owned())
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, GitError>>()?;
|
||||
branches.sort();
|
||||
branches.dedup();
|
||||
if branches.is_empty() {
|
||||
return Err(GitError::InvalidRepository(
|
||||
"the remote has no branches".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
pub fn init(store: &Repository, identity: GitIdentity) -> Result<Self, GitError> {
|
||||
let root = store.root_path().to_owned();
|
||||
if root.join(".git").exists() {
|
||||
@@ -957,12 +1079,13 @@ impl GitRepository {
|
||||
}
|
||||
|
||||
fn open_at(root: PathBuf, identity: GitIdentity) -> Result<Self, GitError> {
|
||||
let repository =
|
||||
let mut repository =
|
||||
gix::open_opts(&root, isolated_options()).map_err(|_| GitError::NotRepository)?;
|
||||
if repository.is_bare() {
|
||||
return Err(GitError::BareRepository);
|
||||
}
|
||||
validate_local_config_security(&load_local_config(&repository)?)?;
|
||||
apply_in_memory_identity(&mut repository, &identity)?;
|
||||
Ok(Self {
|
||||
root,
|
||||
repository,
|
||||
@@ -1204,18 +1327,18 @@ impl GitRepository {
|
||||
return GitError::Cancelled;
|
||||
}
|
||||
let text = error.to_string();
|
||||
let lower = text.to_ascii_lowercase();
|
||||
if text.contains("401")
|
||||
|| text.contains("403")
|
||||
|| text.to_ascii_lowercase().contains("authentication")
|
||||
|| lower.contains("authentication")
|
||||
|| (lower.contains("credential") && lower.contains("not accepted"))
|
||||
{
|
||||
GitError::AuthenticationFailed
|
||||
} else if text.to_ascii_lowercase().contains("certificate")
|
||||
|| text.to_ascii_lowercase().contains("tls")
|
||||
{
|
||||
} else if lower.contains("certificate") || lower.contains("tls") {
|
||||
GitError::TlsFailed
|
||||
} else if text.to_ascii_lowercase().contains("network")
|
||||
|| text.to_ascii_lowercase().contains("connect")
|
||||
|| text.to_ascii_lowercase().contains("dns")
|
||||
} else if lower.contains("network")
|
||||
|| lower.contains("connect")
|
||||
|| lower.contains("dns")
|
||||
{
|
||||
GitError::NetworkUnavailable
|
||||
} else {
|
||||
@@ -1878,6 +2001,7 @@ impl GitRepository {
|
||||
validate_local_config_security(&config)?;
|
||||
write_local_config(&self.repository, &config)?;
|
||||
self.repository = gix::open_opts(&self.root, isolated_options()).map_err(invalid)?;
|
||||
apply_in_memory_identity(&mut self.repository, &self.identity)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3032,6 +3156,43 @@ fn validate_remote_name(name: &str) -> Result<(), GitError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_clone_parent(parent: &Path) -> Result<(), GitError> {
|
||||
let metadata = fs::symlink_metadata(parent).map_err(|_| io("inspect clone parent", parent))?;
|
||||
if !metadata.is_dir() || metadata.file_type().is_symlink() {
|
||||
return Err(GitError::UnsafeWorktreeObject {
|
||||
path: parent.to_owned(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn private_temporary_directory(parent: &Path, purpose: &str) -> Result<PathBuf, GitError> {
|
||||
(0..128_u8)
|
||||
.find_map(|attempt| {
|
||||
let name = format!(".ironstorage-{purpose}-{}-{attempt}", rand::random::<u64>());
|
||||
let candidate = parent.join(name);
|
||||
match fs::create_dir(&candidate) {
|
||||
Ok(()) => {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
if fs::set_permissions(&candidate, fs::Permissions::from_mode(0o700))
|
||||
.is_err()
|
||||
{
|
||||
let _ = fs::remove_dir(&candidate);
|
||||
return Some(Err(io("secure private Git directory", parent)));
|
||||
}
|
||||
}
|
||||
Some(Ok(candidate))
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => None,
|
||||
Err(_) => Some(Err(io("create private Git directory", parent))),
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
.ok_or_else(|| io("create private Git directory", parent))
|
||||
}
|
||||
|
||||
fn validate_https_remote(value: &str) -> Result<url::Url, GitError> {
|
||||
let parsed = url::Url::parse(value).map_err(|_| GitError::ForbiddenRemoteUrl)?;
|
||||
if parsed.scheme() != "https"
|
||||
@@ -3215,9 +3376,45 @@ fn invalid(error: impl fmt::Display) -> GitError {
|
||||
GitError::InvalidRepository(error.to_string())
|
||||
}
|
||||
|
||||
fn apply_in_memory_identity(
|
||||
repository: &mut gix::Repository,
|
||||
identity: &GitIdentity,
|
||||
) -> Result<(), GitError> {
|
||||
let mut config = repository.config_snapshot_mut();
|
||||
config
|
||||
.set_raw_value("user.name", identity.name())
|
||||
.map_err(invalid)?;
|
||||
config
|
||||
.set_raw_value("user.email", identity.email())
|
||||
.map_err(invalid)?;
|
||||
drop(config);
|
||||
if repository.committer().is_none() {
|
||||
return Err(GitError::InvalidIdentity);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn io(operation: &'static str, path: &Path) -> GitError {
|
||||
GitError::Io {
|
||||
operation,
|
||||
path: path.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{GitIdentity, GitRepository};
|
||||
use crate::repository::Repository;
|
||||
|
||||
#[test]
|
||||
fn embedded_identity_survives_local_config_updates() {
|
||||
let temporary = tempfile::tempdir().expect("temporary repository");
|
||||
let store = Repository::open(temporary.path()).expect("store");
|
||||
let mut repository =
|
||||
GitRepository::init(&store, GitIdentity::ironstorage()).expect("initialize Git");
|
||||
repository
|
||||
.add_remote("origin", "https://example.test/password-store.git")
|
||||
.expect("add remote");
|
||||
assert!(repository.repository.committer().is_some());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user