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 <REPO> 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<Self> {
+        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<const N: usize>() -> 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