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