From e4b9a9b40838777d4d017001fef00e1c57efbb0c Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Fri, 19 Jul 2024 01:41:53 +0200 Subject: Init --- src/config.rs | 11 ++++++ src/login.rs | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 74 +++++++++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 src/config.rs create mode 100644 src/login.rs create mode 100644 src/main.rs (limited to 'src') diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..5e8cc5d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,11 @@ +use std::path::Path; + +pub fn ssh_user() -> &'static str { + return "git"; +} +pub fn binary_path() -> &'static str { + return "/opt/fagit/fagit"; +} +pub fn repo_store() -> &'static Path { + return Path::new("/opt/fagit/store/"); +} diff --git a/src/login.rs b/src/login.rs new file mode 100644 index 0000000..ca12c50 --- /dev/null +++ b/src/login.rs @@ -0,0 +1,115 @@ +use std::path::{Path, PathBuf}; + +use crate::Login; + +use clap::Args; +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command()] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + #[command(name = "git-upload-pack")] + GitUploadPack(GitUploadPack), + #[command(name = "git-receive-pack")] + GitReceivePack(GitUploadPack), // TODO: own args +} +#[derive(Args)] +struct GitUploadPack { + repo_path: String, +} +pub(crate) fn do_login(login: &Login) { + let command = std::env::var("SSH_ORIGINAL_COMMAND").unwrap_or("".to_owned()); + log::info!( + "Performed token login for {} {} with command: {}", + login.keytype, + login.keydata, + command + ); + let user_token = get_user_token(login.keytype.to_owned(), login.keydata.to_owned()); + let mut command = shlex::split(&command).unwrap(); + command.insert(0, "fagit-dummy".to_owned()); + let args = Cli::parse_from(command.iter()); + match args.command { + Commands::GitUploadPack(upload_pack) => { + let repo_path = canonicalize_repo(&upload_pack.repo_path, &user_token); + if can_read(&repo_path, &user_token) { + let mut child = std::process::Command::new("git-upload-pack") + .args([repo_path.repo_path().as_os_str()]) + .spawn() + .unwrap(); + child.wait().unwrap(); + } + } + Commands::GitReceivePack(upload_pack) => { + let repo_path = canonicalize_repo(&upload_pack.repo_path, &user_token); + if can_write(&repo_path, &user_token) { + let mut child = std::process::Command::new("git-receive-pack") + .args([repo_path.repo_path().as_os_str()]) + .spawn() + .unwrap(); + child.wait().unwrap(); + } + } + } +} +struct UserToken { + keytype: String, + keydata: String, + + is_operator: bool, +} +struct RepoId(String); +impl RepoId { + fn is_meta_repo(&self) -> bool { + return self.0.starts_with("meta/") || self.0 == "meta.git"; + } + fn repo_path(&self) -> PathBuf { + return Path::join(crate::config::repo_store(), self.0.clone()); + } +} + +fn canonicalize_repo(path: &str, user: &UserToken) -> RepoId { + let mut path = path.to_owned(); + path.make_ascii_lowercase(); + if path.starts_with("/") { + path.remove(0); + } + if path.ends_with("/") { + path.remove(path.len() - 1); + } + if !path.ends_with(".git") { + path += ".git"; + } + // TODO: properly check directory traversal or smth here. just a regex should do the trick + return RepoId(path); +} + +fn can_write(repo_path: &RepoId, user_token: &UserToken) -> bool { + if repo_path.is_meta_repo() { + return user_token.is_operator; + } + return true; +} +fn can_read(repo_path: &RepoId, user_token: &UserToken) -> bool { + if repo_path.is_meta_repo() { + return user_token.is_operator; + } + return true; +} + +fn get_user_token(keytype: String, keydata: String) -> UserToken { + // TODO: settings + let is_operator = "ssh-ed25519" == keytype + && "AAAAC3NzaC1lZDI1NTE5AAAAINg2WYMRKINwbH5UCqqK2qq/qW0gG1NnaALHqEyU4NzM" == keydata; + return UserToken { + keytype, + keydata, + is_operator, + }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cb5a926 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,74 @@ +use std::io::{Read, Write}; +use std::path::Path; + +use clap::Args; +use clap::{Parser, Subcommand}; +mod config; + +#[derive(Parser)] +#[command(version, about, long_about=include_str!("../README.md"))] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Auth(Auth), + Login(Login), +} +#[derive(Args)] +struct Auth { + user: String, + home: String, + keytype: String, + keydata: String, +} +#[derive(Args)] +struct Login { + keytype: String, + keydata: String, +} + +fn main() { + systemd_journal_logger::JournalLog::new() + .unwrap() + .with_extra_fields(vec![("VERSION", env!("CARGO_PKG_VERSION"))]) + .with_syslog_identifier("fagit".to_string()) + .install() + .unwrap(); + log::set_max_level(log::LevelFilter::Info); + let args = std::env::args().collect::>(); + if args.len() == 0 { + println!(include_str!("../README.md")); + } + let cli = Cli::parse(); + match &cli.command { + Commands::Auth(auth) => find_auth_keys(auth), + Commands::Login(login) => crate::login::do_login(login), + } +} +mod login; + +fn find_auth_keys(auth: &Auth) { + if auth.user != config::ssh_user() { + let authorized_keys_path = Path::new(&auth.home).join(".ssh/authorized_keys_path"); + let mut data = std::fs::File::open(authorized_keys_path).unwrap(); + let mut vec = vec![]; + data.read_to_end(&mut vec).unwrap(); + std::io::stdout().write_all(&vec).unwrap(); + return; + } + // TODO: escape shit in here properly + let login_command = format!( + "{} login {} {}", + config::binary_path(), + auth.keytype, + auth.keydata + ); + println!("command=\"{}\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict {} {}", + login_command, + auth.keytype, + auth.keydata + ); +} -- cgit