From a7f9c8de31231b9fd9c67c57db659f7b01f1a3b0 Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Mon, 29 Apr 2024 19:18:56 +0200 Subject: Rename crates (and therefore commands) --- gitolfs3-authenticate/Cargo.toml | 10 +++ gitolfs3-authenticate/src/main.rs | 134 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 gitolfs3-authenticate/Cargo.toml create mode 100644 gitolfs3-authenticate/src/main.rs (limited to 'gitolfs3-authenticate') diff --git a/gitolfs3-authenticate/Cargo.toml b/gitolfs3-authenticate/Cargo.toml new file mode 100644 index 0000000..5725abc --- /dev/null +++ b/gitolfs3-authenticate/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "gitolfs3-authenticate" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +chrono = "0.4" +gitolfs3-common = { path = "../gitolfs3-common" } +serde_json = "1" diff --git a/gitolfs3-authenticate/src/main.rs b/gitolfs3-authenticate/src/main.rs new file mode 100644 index 0000000..771f185 --- /dev/null +++ b/gitolfs3-authenticate/src/main.rs @@ -0,0 +1,134 @@ +use anyhow::{anyhow, bail, Result}; +use chrono::Utc; +use gitolfs3_common::{generate_tag, load_key, Claims, Key, Operation, SpecificClaims}; +use serde_json::json; +use std::{process::ExitCode, time::Duration}; + +fn main() -> ExitCode { + let config = match Config::load() { + Ok(config) => config, + Err(e) => { + eprintln!("Error: {e}"); + return ExitCode::from(2); + } + }; + + let (repo_name, operation) = match parse_cmdline() { + Ok(args) => args, + Err(e) => { + eprintln!("Error: {e}\n"); + eprintln!("Usage: git-lfs-authenticate upload/download"); + // Exit code 2 signifies bad usage of CLI. + return ExitCode::from(2); + } + }; + + if !repo_exists(&repo_name) { + eprintln!("Error: repository does not exist"); + return ExitCode::FAILURE; + } + + let expires_at = Utc::now() + Duration::from_secs(5 * 60); + let Some(tag) = generate_tag( + Claims { + specific_claims: SpecificClaims::BatchApi(operation), + repo_path: &repo_name, + expires_at, + }, + config.key, + ) else { + eprintln!("Failed to generate validation tag"); + return ExitCode::FAILURE; + }; + + let response = json!({ + "header": { + "Authorization": format!( + "Gitolfs3-Hmac-Sha256 {tag} {}", + expires_at.timestamp() + ), + }, + "expires_at": expires_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true), + "href": format!("{}{}/info/lfs", config.href_base, repo_name), + }); + println!("{}", response); + + ExitCode::SUCCESS +} + +struct Config { + href_base: String, + key: Key, +} + +impl Config { + fn load() -> Result { + let Ok(href_base) = std::env::var("GITOLFS3_HREF_BASE") else { + bail!("configured base URL not provided"); + }; + if !href_base.ends_with('/') { + bail!("configured base URL does not end with a slash"); + } + + let Ok(key_path) = std::env::var("GITOLFS3_KEY_PATH") else { + bail!("key path not provided"); + }; + let key = load_key(&key_path).map_err(|e| anyhow!("failed to load key: {e}"))?; + + Ok(Self { href_base, key }) + } +} + +fn parse_cmdline() -> Result<(String, Operation)> { + let [repo_path, op_str] = get_cmdline_args::<2>()?; + let op: Operation = op_str + .parse() + .map_err(|e| anyhow!("unknown operation: {e}"))?; + validate_repo_path(&repo_path).map_err(|e| anyhow!("invalid repository name: {e}"))?; + Ok((repo_path.to_string(), op)) +} + +fn get_cmdline_args() -> Result<[String; N]> { + let args = std::env::args(); + if args.len() - 1 != N { + bail!("got {} argument(s), expected {}", args.len() - 1, N); + } + + // Does not allocate. + const EMPTY_STRING: String = String::new(); + let mut values = [EMPTY_STRING; N]; + + // Skip the first element; we do not care about the program name. + for (i, arg) in args.skip(1).enumerate() { + values[i] = arg + } + Ok(values) +} + +fn validate_repo_path(path: &str) -> Result<()> { + if path.len() > 100 { + bail!("too long (more than 100 characters)"); + } + if path.contains("//") + || path.contains("/./") + || path.contains("/../") + || path.starts_with("./") + || path.starts_with("../") + { + bail!("contains one or more path elements '.' and '..'"); + } + if path.starts_with('/') { + bail!("starts with '/', which is not allowed"); + } + if !path.ends_with(".git") { + bail!("missed '.git' suffix"); + } + Ok(()) +} + +fn repo_exists(name: &str) -> bool { + match std::fs::metadata(name) { + Ok(metadata) => metadata.is_dir(), + _ => false, + } +} -- cgit v1.2.3