diff options
-rw-r--r-- | .editorconfig | 8 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | 10-fagit-ssh-authorization.conf | 3 | ||||
-rw-r--r-- | Cargo.lock | 312 | ||||
-rw-r--r-- | Cargo.toml | 12 | ||||
-rw-r--r-- | Justfile | 12 | ||||
-rw-r--r-- | README.md | 50 | ||||
-rw-r--r-- | src/config.rs | 11 | ||||
-rw-r--r-- | src/login.rs | 115 | ||||
-rw-r--r-- | src/main.rs | 74 |
10 files changed, 598 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..060af7a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +end_of_line =lf +insert_final_newline=true +indent_size =4 +indent_style=tab +charset = utf-8 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/10-fagit-ssh-authorization.conf b/10-fagit-ssh-authorization.conf new file mode 100644 index 0000000..bd98f2b --- /dev/null +++ b/10-fagit-ssh-authorization.conf @@ -0,0 +1,3 @@ +AuthorizedKeysCommand /opt/fagit/fagit auth "%u" "%h" "%t" "%k" +AuthorizedKeysCommandUser root + diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c98f596 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,312 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "clap" +version = "4.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fagit" +version = "0.1.0" +dependencies = [ + "clap", + "log", + "shlex", + "systemd-journal-logger", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "value-bag", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "systemd-journal-logger" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f3848dd723f2a54ac1d96da793b32923b52de8dfcced8722516dac312a5b2a" +dependencies = [ + "log", + "rustix", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "value-bag" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1744a9d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "fagit" +version = "0.1.0" +edition = "2021" + +[dependencies] +log = "0.4.22" +shlex = "1.3.0" +systemd-journal-logger = "2.1.1" +[dependencies.clap] +version = "4.5.9" +features = ["cargo", "derive"] diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..9d39a65 --- /dev/null +++ b/Justfile @@ -0,0 +1,12 @@ +build: + cargo build + sudo cp target/debug/fagit /opt/fagit/ + sudo chown root:root /opt/fagit/fagit + +install_ssh: + sudo cp 10-fagit-ssh-authorization.conf /etc/ssh/sshd_config.d/ + + +debug: build + sudo /usr/sbin/sshd -ddd + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a520e5f --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# faGIT + +> Free Association Git + +This is a git ssh server that allows for both private and public repositories, as well as most notably for anyone to create forks simply by pushing to a branch named `pr/<somename>`. This branch can only be pushed to again by that same ssh key (or the owner of the repo). + +This is the free association in **FA**git, allowing anyone without association to the server to create pull requests, without having to use `git send-mail` (although maybe more people should learn how to use that). + +Maybe in the future i will find it in my heart to integrate a `git send-mail` receiver that creates pr branches automatically. + +## Installation + +### Basic structure + +Find a good place for the binary (you can generate one using `cargo build --release`. find yours in `target/`) to be saved. Also find a data directory. You will need to have one toml file for basic config including uber admin keys. +You will also need a `git` user. You will also need to find a repo storage folder. Here is one possible layout: + +// TODO: make all of this say that the config path are hardcoded and important to /etc/fagit/fagit.toml +``` +/opt/fagit/fagit: binary, owned by root +/opt/fagit/fagit.toml: config, owned by root +/opt/fagit/store/: folder containing repositories, owned by the git user +``` + +Note that the store folder is owned by the git user, since when actually modifying repos fagit will run as the git user. Note also that the fagit.toml contains only minimal metadata and most of the actual config can be done by pushing to the `meta.git` repo. + +### Config file + +Here is an example config file: + + +```toml +[ssh] +user = "git" # log in with the user git + +[paths] # All of these paths need to be absolute +binary = "/opt/fagit/fagit" +config = "/opt/fagit/fagit.toml" # Note that the config must have the correct path to itself +store = "/opt/fagit/store/" + +[[operator]] # Note that this admin user does not get any permissions aside from being able to access the meta repo +key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINg2WYMRKINwbH5UCqqK2qq/qW0gG1NnaALHqEyU4NzM" +``` + +### SSH authorization + +Edit `/etc/ssh/sshd_config` to include `AuthorizedKeysCommand /opt/fagit/fagit auth /opt/fagit/fagit.toml "%u" "%h" "%t" "%k"`. You might also need to set `AuthorizedKeysCommandUser root`. + + + 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("<no command>".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::<Vec<_>>(); + 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 + ); +} |