diff options
| author | Rutger Broekhoff | 2024-04-29 19:18:56 +0200 | 
|---|---|---|
| committer | Rutger Broekhoff | 2024-04-29 19:18:56 +0200 | 
| commit | a7f9c8de31231b9fd9c67c57db659f7b01f1a3b0 (patch) | |
| tree | 040c518d243769c1920b8201a07a626f4e5934cf /gitolfs3-authenticate/src | |
| parent | 80c4d49ad7f590f5af1304939fdaea7baf13142e (diff) | |
| download | gitolfs3-a7f9c8de31231b9fd9c67c57db659f7b01f1a3b0.tar.gz gitolfs3-a7f9c8de31231b9fd9c67c57db659f7b01f1a3b0.zip | |
Rename crates (and therefore commands)
Diffstat (limited to 'gitolfs3-authenticate/src')
| -rw-r--r-- | gitolfs3-authenticate/src/main.rs | 134 | 
1 files changed, 134 insertions, 0 deletions
| 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 @@ | |||
| 1 | use anyhow::{anyhow, bail, Result}; | ||
| 2 | use chrono::Utc; | ||
| 3 | use gitolfs3_common::{generate_tag, load_key, Claims, Key, Operation, SpecificClaims}; | ||
| 4 | use serde_json::json; | ||
| 5 | use std::{process::ExitCode, time::Duration}; | ||
| 6 | |||
| 7 | fn main() -> ExitCode { | ||
| 8 | let config = match Config::load() { | ||
| 9 | Ok(config) => config, | ||
| 10 | Err(e) => { | ||
| 11 | eprintln!("Error: {e}"); | ||
| 12 | return ExitCode::from(2); | ||
| 13 | } | ||
| 14 | }; | ||
| 15 | |||
| 16 | let (repo_name, operation) = match parse_cmdline() { | ||
| 17 | Ok(args) => args, | ||
| 18 | Err(e) => { | ||
| 19 | eprintln!("Error: {e}\n"); | ||
| 20 | eprintln!("Usage: git-lfs-authenticate <REPO> upload/download"); | ||
| 21 | // Exit code 2 signifies bad usage of CLI. | ||
| 22 | return ExitCode::from(2); | ||
| 23 | } | ||
| 24 | }; | ||
| 25 | |||
| 26 | if !repo_exists(&repo_name) { | ||
| 27 | eprintln!("Error: repository does not exist"); | ||
| 28 | return ExitCode::FAILURE; | ||
| 29 | } | ||
| 30 | |||
| 31 | let expires_at = Utc::now() + Duration::from_secs(5 * 60); | ||
| 32 | let Some(tag) = generate_tag( | ||
| 33 | Claims { | ||
| 34 | specific_claims: SpecificClaims::BatchApi(operation), | ||
| 35 | repo_path: &repo_name, | ||
| 36 | expires_at, | ||
| 37 | }, | ||
| 38 | config.key, | ||
| 39 | ) else { | ||
| 40 | eprintln!("Failed to generate validation tag"); | ||
| 41 | return ExitCode::FAILURE; | ||
| 42 | }; | ||
| 43 | |||
| 44 | let response = json!({ | ||
| 45 | "header": { | ||
| 46 | "Authorization": format!( | ||
| 47 | "Gitolfs3-Hmac-Sha256 {tag} {}", | ||
| 48 | expires_at.timestamp() | ||
| 49 | ), | ||
| 50 | }, | ||
| 51 | "expires_at": expires_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true), | ||
| 52 | "href": format!("{}{}/info/lfs", config.href_base, repo_name), | ||
| 53 | }); | ||
| 54 | println!("{}", response); | ||
| 55 | |||
| 56 | ExitCode::SUCCESS | ||
| 57 | } | ||
| 58 | |||
| 59 | struct Config { | ||
| 60 | href_base: String, | ||
| 61 | key: Key, | ||
| 62 | } | ||
| 63 | |||
| 64 | impl Config { | ||
| 65 | fn load() -> Result<Self> { | ||
| 66 | let Ok(href_base) = std::env::var("GITOLFS3_HREF_BASE") else { | ||
| 67 | bail!("configured base URL not provided"); | ||
| 68 | }; | ||
| 69 | if !href_base.ends_with('/') { | ||
| 70 | bail!("configured base URL does not end with a slash"); | ||
| 71 | } | ||
| 72 | |||
| 73 | let Ok(key_path) = std::env::var("GITOLFS3_KEY_PATH") else { | ||
| 74 | bail!("key path not provided"); | ||
| 75 | }; | ||
| 76 | let key = load_key(&key_path).map_err(|e| anyhow!("failed to load key: {e}"))?; | ||
| 77 | |||
| 78 | Ok(Self { href_base, key }) | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | fn parse_cmdline() -> Result<(String, Operation)> { | ||
| 83 | let [repo_path, op_str] = get_cmdline_args::<2>()?; | ||
| 84 | let op: Operation = op_str | ||
| 85 | .parse() | ||
| 86 | .map_err(|e| anyhow!("unknown operation: {e}"))?; | ||
| 87 | validate_repo_path(&repo_path).map_err(|e| anyhow!("invalid repository name: {e}"))?; | ||
| 88 | Ok((repo_path.to_string(), op)) | ||
| 89 | } | ||
| 90 | |||
| 91 | fn get_cmdline_args<const N: usize>() -> Result<[String; N]> { | ||
| 92 | let args = std::env::args(); | ||
| 93 | if args.len() - 1 != N { | ||
| 94 | bail!("got {} argument(s), expected {}", args.len() - 1, N); | ||
| 95 | } | ||
| 96 | |||
| 97 | // Does not allocate. | ||
| 98 | const EMPTY_STRING: String = String::new(); | ||
| 99 | let mut values = [EMPTY_STRING; N]; | ||
| 100 | |||
| 101 | // Skip the first element; we do not care about the program name. | ||
| 102 | for (i, arg) in args.skip(1).enumerate() { | ||
| 103 | values[i] = arg | ||
| 104 | } | ||
| 105 | Ok(values) | ||
| 106 | } | ||
| 107 | |||
| 108 | fn validate_repo_path(path: &str) -> Result<()> { | ||
| 109 | if path.len() > 100 { | ||
| 110 | bail!("too long (more than 100 characters)"); | ||
| 111 | } | ||
| 112 | if path.contains("//") | ||
| 113 | || path.contains("/./") | ||
| 114 | || path.contains("/../") | ||
| 115 | || path.starts_with("./") | ||
| 116 | || path.starts_with("../") | ||
| 117 | { | ||
| 118 | bail!("contains one or more path elements '.' and '..'"); | ||
| 119 | } | ||
| 120 | if path.starts_with('/') { | ||
| 121 | bail!("starts with '/', which is not allowed"); | ||
| 122 | } | ||
| 123 | if !path.ends_with(".git") { | ||
| 124 | bail!("missed '.git' suffix"); | ||
| 125 | } | ||
| 126 | Ok(()) | ||
| 127 | } | ||
| 128 | |||
| 129 | fn repo_exists(name: &str) -> bool { | ||
| 130 | match std::fs::metadata(name) { | ||
| 131 | Ok(metadata) => metadata.is_dir(), | ||
| 132 | _ => false, | ||
| 133 | } | ||
| 134 | } | ||