aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'crates/gilti-ssh/src/main.rs')
-rw-r--r--crates/gilti-ssh/src/main.rs288+0 −288
1 files changed, 0 insertions, 288 deletions
diff --git a/crates/gilti-ssh/src/main.rs b/crates/gilti-ssh/src/main.rs
deleted file mode 100644
--- a/crates/gilti-ssh/src/main.rs
+++ /dev/null
@@ -1,288 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Nikolay Govorov
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-//! Restricted OpenSSH forced command for Git services.
-//!
-//! Every authenticated key is fully trusted. Repository directories, their
-//! configuration, and server-side hooks are trusted administrator state.
-
-const GIT_HOME: &str = "/var/lib/gilti/git";
-const REPOSITORIES: &str = "/var/lib/gilti/git/repositories";
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum GitService {
- ReceivePack,
- UploadArchive,
- UploadPack,
-}
-
-impl GitService {
- fn parse(value: &str) -> Option<Self> {
- match value {
- "git-receive-pack" => Some(Self::ReceivePack),
- "git-upload-archive" => Some(Self::UploadArchive),
- "git-upload-pack" => Some(Self::UploadPack),
- _ => None,
- }
- }
-
- fn program(self) -> &'static str {
- match self {
- Self::ReceivePack => "/usr/bin/git-receive-pack",
- Self::UploadArchive => "/usr/bin/git-upload-archive",
- Self::UploadPack => "/usr/bin/git-upload-pack",
- }
- }
-}
-
-fn main() -> std::process::ExitCode {
- // SAFETY: setting the process umask has no memory-safety implications.
- unsafe {
- libc::umask(0o077);
- }
-
- match run() {
- Ok(()) => std::process::ExitCode::SUCCESS,
- Err(error) => {
- eprintln!("gilti-ssh: {error}");
- std::process::ExitCode::FAILURE
- }
- }
-}
-
-fn run() -> Result<(), String> {
- if std::env::args_os().nth(1).as_deref() == Some(std::ffi::OsStr::new("--check")) {
- return check_installation();
- }
-
- let remote = std::env::var("SSH_CONNECTION")
- .ok()
- .and_then(|connection| connection.split_whitespace().next().map(str::to_owned))
- .ok_or_else(|| "SSH_CONNECTION is missing".to_owned())?;
- let command = match std::env::var("SSH_ORIGINAL_COMMAND") {
- Ok(command) if !command.is_empty() => command,
- _ => {
- eprintln!("gilti-ssh: authenticated connection from {remote}");
- println!("Gilti: authenticated. Shell access is disabled.");
- return Ok(());
- }
- };
- if command.contains(['\n', '\r']) {
- return Err("newlines are not allowed in SSH_ORIGINAL_COMMAND".to_owned());
- }
-
- let (service, repository) = parse_command(&command)?;
- eprintln!("gilti-ssh: {service:?} {repository} from {remote}");
- let root = std::path::Path::new(REPOSITORIES);
- let root_metadata = std::fs::symlink_metadata(root)
- .map_err(|error| format!("cannot inspect {REPOSITORIES}: {error}"))?;
- if !root_metadata.file_type().is_dir() {
- return Err(format!("{REPOSITORIES} is not a real directory"));
- }
- let path = root.join(format!("{repository}.git"));
-
- if service == GitService::ReceivePack && !path.exists() {
- create_repository(root, &path)?;
- }
- let metadata = std::fs::symlink_metadata(&path)
- .map_err(|_| format!("repository '{repository}' does not exist"))?;
- if !metadata.file_type().is_dir() {
- return Err(format!("repository '{repository}' is not a real directory"));
- }
- verify_repository_path(root, &path)?;
-
- let mut command = git_command(service.program());
- if let Some(protocol) = std::env::var_os("GIT_PROTOCOL") {
- command.env("GIT_PROTOCOL", protocol);
- }
- let error = std::os::unix::process::CommandExt::exec(command.arg(path));
- Err(format!("cannot execute {}: {error}", service.program()))
-}
-
-fn check_installation() -> Result<(), String> {
- let metadata = std::fs::symlink_metadata(REPOSITORIES)
- .map_err(|error| format!("cannot inspect {REPOSITORIES}: {error}"))?;
- if !metadata.file_type().is_dir() {
- return Err(format!("{REPOSITORIES} is not a real directory"));
- }
- for service in [
- GitService::ReceivePack,
- GitService::UploadArchive,
- GitService::UploadPack,
- ] {
- let metadata = std::fs::metadata(service.program())
- .map_err(|error| format!("cannot inspect {}: {error}", service.program()))?;
- if !metadata.is_file()
- || std::os::unix::fs::PermissionsExt::mode(&metadata.permissions()) & 0o111 == 0
- {
- return Err(format!("{} is not executable", service.program()));
- }
- }
- Ok(())
-}
-
-fn parse_command(command: &str) -> Result<(GitService, String), String> {
- let (program, argument) = command
- .split_once(' ')
- .ok_or_else(|| "only Git protocol commands are allowed".to_owned())?;
- let service = GitService::parse(program)
- .ok_or_else(|| "only Git protocol commands are allowed".to_owned())?;
- let repository = parse_repository(argument)?;
- Ok((service, repository))
-}
-
-fn parse_repository(argument: &str) -> Result<String, String> {
- let argument = argument
- .strip_prefix('\'')
- .and_then(|value| value.strip_suffix('\''));
- let argument = argument.ok_or_else(|| "repository must be single-quoted".to_owned())?;
- let argument = argument.strip_prefix('/').unwrap_or(argument);
- let argument = argument.strip_suffix(".git").unwrap_or(argument);
-
- if argument.is_empty()
- || argument.len() > 1024
- || !argument.as_bytes()[0].is_ascii_alphanumeric()
- || argument.contains("..")
- || argument.contains(".git/")
- {
- return Err("invalid repository name".to_owned());
- }
- for component in argument.split('/') {
- if component.is_empty()
- || component == "."
- || component == ".."
- || !component
- .bytes()
- .all(|byte| byte.is_ascii_alphanumeric() || b"-_.".contains(&byte))
- {
- return Err("invalid repository name".to_owned());
- }
- }
- Ok(argument.to_owned())
-}
-
-fn verify_repository_path(root: &std::path::Path, path: &std::path::Path) -> Result<(), String> {
- let root = std::fs::canonicalize(root)
- .map_err(|error| format!("cannot resolve {}: {error}", root.display()))?;
- let path = std::fs::canonicalize(path)
- .map_err(|error| format!("cannot resolve {}: {error}", path.display()))?;
- if !path.starts_with(root) {
- return Err("repository escapes the repository directory".to_owned());
- }
- Ok(())
-}
-
-fn verify_creation_parent(root: &std::path::Path, path: &std::path::Path) -> Result<(), String> {
- let mut ancestor = path
- .parent()
- .ok_or_else(|| "repository has no parent directory".to_owned())?;
- loop {
- match std::fs::symlink_metadata(ancestor) {
- Ok(metadata) => {
- if !metadata.is_dir() {
- return Err(format!("{} is not a directory", ancestor.display()));
- }
- return verify_repository_path(root, ancestor);
- }
- Err(error) if error.kind() == std::io::ErrorKind::NotFound && ancestor != root => {
- ancestor = ancestor
- .parent()
- .ok_or_else(|| "repository escapes the repository directory".to_owned())?;
- }
- Err(error) => {
- return Err(format!("cannot inspect {}: {error}", ancestor.display()));
- }
- }
- }
-}
-
-fn git_command(program: &str) -> std::process::Command {
- let mut command = std::process::Command::new(program);
- command
- .env_clear()
- .env("HOME", GIT_HOME)
- .env("USER", "git")
- .env("LOGNAME", "git")
- .env("PATH", "/usr/bin:/bin")
- .env("GIT_CONFIG_GLOBAL", "/dev/null")
- .env("GIT_CONFIG_NOSYSTEM", "1")
- .env("GIT_TERMINAL_PROMPT", "0");
- command
-}
-
-fn create_repository(root: &std::path::Path, path: &std::path::Path) -> Result<(), String> {
- verify_creation_parent(root, path)?;
- let parent = path
- .parent()
- .ok_or_else(|| "repository has no parent directory".to_owned())?;
- std::fs::create_dir_all(parent)
- .map_err(|error| format!("cannot create {}: {error}", parent.display()))?;
- verify_repository_path(root, parent)?;
-
- let status = git_command("/usr/bin/git")
- .args(["init", "--quiet", "--bare", "--initial-branch=main", "--"])
- .arg(path)
- .status()
- .map_err(|error| format!("cannot initialize {}: {error}", path.display()))?;
- if !status.success() {
- return Err(format!(
- "cannot initialize {}: git exited with {status}",
- path.display()
- ));
- }
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- #[test]
- fn parses_git_commands() {
- assert_eq!(
- super::parse_command("git-upload-pack 'group/project.git'").unwrap(),
- (super::GitService::UploadPack, "group/project".to_owned())
- );
- assert_eq!(
- super::parse_command("git-receive-pack '/project'").unwrap(),
- (super::GitService::ReceivePack, "project".to_owned())
- );
- }
-
- #[test]
- fn rejects_other_commands_and_unsafe_names() {
- for command in [
- "sh -c true",
- "git-upload-pack '../../etc/passwd'",
- "git-upload-pack 'repo..backup'",
- "git-upload-pack 'outer.git/inner'",
- "git-upload-pack '.hidden'",
- "git-upload-pack 'repo name'",
- "git-upload-pack 'repo' trailing",
- "git-upload-pack repo",
- "git-upload-pack 'repo'\nwhoami",
- ] {
- assert!(super::parse_command(command).is_err(), "accepted {command}");
- }
- }
-
- #[test]
- fn refuses_to_create_through_a_symlinked_parent() {
- static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
-
- let base = std::env::temp_dir().join(format!(
- "gilti-ssh-test-{}-{}",
- std::process::id(),
- NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
- ));
- let root = base.join("repositories");
- let outside = base.join("outside");
- std::fs::create_dir_all(&root).unwrap();
- std::fs::create_dir_all(&outside).unwrap();
- std::os::unix::fs::symlink(&outside, root.join("group")).unwrap();
-
- let repository = root.join("group/project.git");
- assert!(super::create_repository(&root, &repository).is_err());
- assert!(!outside.join("project.git").exists());
-
- std::fs::remove_dir_all(base).unwrap();
- }
-}