diff options
Diffstat (limited to 'rs/git-lfs-authenticate')
-rw-r--r-- | rs/git-lfs-authenticate/Cargo.toml | 8 | ||||
-rw-r--r-- | rs/git-lfs-authenticate/src/main.rs | 236 |
2 files changed, 0 insertions, 244 deletions
diff --git a/rs/git-lfs-authenticate/Cargo.toml b/rs/git-lfs-authenticate/Cargo.toml deleted file mode 100644 index 217250f..0000000 --- a/rs/git-lfs-authenticate/Cargo.toml +++ /dev/null | |||
@@ -1,8 +0,0 @@ | |||
1 | [package] | ||
2 | name = "git-lfs-authenticate" | ||
3 | version = "0.1.0" | ||
4 | edition = "2021" | ||
5 | |||
6 | [dependencies] | ||
7 | chrono = "0.4" | ||
8 | common = { path = "../common" } | ||
diff --git a/rs/git-lfs-authenticate/src/main.rs b/rs/git-lfs-authenticate/src/main.rs deleted file mode 100644 index 36d7818..0000000 --- a/rs/git-lfs-authenticate/src/main.rs +++ /dev/null | |||
@@ -1,236 +0,0 @@ | |||
1 | use std::{fmt, process::ExitCode, time::Duration}; | ||
2 | |||
3 | use chrono::Utc; | ||
4 | use common::{Operation, ParseOperationError}; | ||
5 | |||
6 | fn help() { | ||
7 | eprintln!("Usage: git-lfs-authenticate <REPO> upload/download"); | ||
8 | } | ||
9 | |||
10 | #[derive(Debug, Eq, PartialEq, Copy, Clone)] | ||
11 | enum RepoNameError { | ||
12 | TooLong, | ||
13 | UnresolvedPath, | ||
14 | AbsolutePath, | ||
15 | MissingGitSuffix, | ||
16 | } | ||
17 | |||
18 | impl fmt::Display for RepoNameError { | ||
19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
20 | match self { | ||
21 | Self::TooLong => write!(f, "too long (more than 100 characters)"), | ||
22 | Self::UnresolvedPath => { | ||
23 | write!(f, "contains path one or more path elements '.' and '..'") | ||
24 | } | ||
25 | Self::AbsolutePath => { | ||
26 | write!(f, "starts with '/', which is not allowed") | ||
27 | } | ||
28 | Self::MissingGitSuffix => write!(f, "misses '.git' suffix"), | ||
29 | } | ||
30 | } | ||
31 | } | ||
32 | |||
33 | // Using `Result<(), E>` here instead of `Option<E>` because `None` typically signifies some error | ||
34 | // state with no further details provided. If we were to return an `Option` type, the user would | ||
35 | // have to first transform it into a `Result` type in order to use the `?` operator, meaning that | ||
36 | // they would have to the following operation to get the type into the right shape: | ||
37 | // `validate_repo_path(path).map_or(Ok(()), Err)`. That would not be very ergonomic. | ||
38 | fn validate_repo_path(path: &str) -> Result<(), RepoNameError> { | ||
39 | if path.len() > 100 { | ||
40 | return Err(RepoNameError::TooLong); | ||
41 | } | ||
42 | if path.contains("//") | ||
43 | || path.contains("/./") | ||
44 | || path.contains("/../") | ||
45 | || path.starts_with("./") | ||
46 | || path.starts_with("../") | ||
47 | { | ||
48 | return Err(RepoNameError::UnresolvedPath); | ||
49 | } | ||
50 | if path.starts_with('/') { | ||
51 | return Err(RepoNameError::AbsolutePath); | ||
52 | } | ||
53 | if !path.ends_with(".git") { | ||
54 | return Err(RepoNameError::MissingGitSuffix); | ||
55 | } | ||
56 | Ok(()) | ||
57 | } | ||
58 | |||
59 | #[derive(Debug, Eq, PartialEq, Copy, Clone)] | ||
60 | enum ParseCmdlineError { | ||
61 | UnknownOperation(ParseOperationError), | ||
62 | InvalidRepoName(RepoNameError), | ||
63 | UnexpectedArgCount(ArgCountError), | ||
64 | } | ||
65 | |||
66 | impl From<RepoNameError> for ParseCmdlineError { | ||
67 | fn from(value: RepoNameError) -> Self { | ||
68 | Self::InvalidRepoName(value) | ||
69 | } | ||
70 | } | ||
71 | |||
72 | impl From<ParseOperationError> for ParseCmdlineError { | ||
73 | fn from(value: ParseOperationError) -> Self { | ||
74 | Self::UnknownOperation(value) | ||
75 | } | ||
76 | } | ||
77 | |||
78 | impl From<ArgCountError> for ParseCmdlineError { | ||
79 | fn from(value: ArgCountError) -> Self { | ||
80 | Self::UnexpectedArgCount(value) | ||
81 | } | ||
82 | } | ||
83 | |||
84 | impl fmt::Display for ParseCmdlineError { | ||
85 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
86 | match self { | ||
87 | Self::UnknownOperation(e) => write!(f, "unknown operation: {e}"), | ||
88 | Self::InvalidRepoName(e) => write!(f, "invalid repository name: {e}"), | ||
89 | Self::UnexpectedArgCount(e) => e.fmt(f), | ||
90 | } | ||
91 | } | ||
92 | } | ||
93 | |||
94 | #[derive(Debug, Eq, PartialEq, Copy, Clone)] | ||
95 | struct ArgCountError { | ||
96 | provided: usize, | ||
97 | expected: usize, | ||
98 | } | ||
99 | |||
100 | impl fmt::Display for ArgCountError { | ||
101 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
102 | write!( | ||
103 | f, | ||
104 | "got {} argument(s), expected {}", | ||
105 | self.provided, self.expected | ||
106 | ) | ||
107 | } | ||
108 | } | ||
109 | |||
110 | fn get_cmdline_args<const N: usize>() -> Result<[String; N], ArgCountError> { | ||
111 | let args = std::env::args(); | ||
112 | if args.len() - 1 != N { | ||
113 | return Err(ArgCountError { | ||
114 | provided: args.len() - 1, | ||
115 | expected: N, | ||
116 | }); | ||
117 | } | ||
118 | |||
119 | // Does not allocate. | ||
120 | const EMPTY_STRING: String = String::new(); | ||
121 | let mut values = [EMPTY_STRING; N]; | ||
122 | |||
123 | // Skip the first element; we do not care about the program name. | ||
124 | for (i, arg) in args.skip(1).enumerate() { | ||
125 | values[i] = arg | ||
126 | } | ||
127 | Ok(values) | ||
128 | } | ||
129 | |||
130 | fn parse_cmdline() -> Result<(String, Operation), ParseCmdlineError> { | ||
131 | let [repo_path, op_str] = get_cmdline_args::<2>()?; | ||
132 | let op: Operation = op_str.parse()?; | ||
133 | validate_repo_path(&repo_path)?; | ||
134 | Ok((repo_path.to_string(), op)) | ||
135 | } | ||
136 | |||
137 | fn repo_exists(name: &str) -> bool { | ||
138 | match std::fs::metadata(name) { | ||
139 | Ok(metadata) => metadata.is_dir(), | ||
140 | _ => false, | ||
141 | } | ||
142 | } | ||
143 | |||
144 | struct Config { | ||
145 | href_base: String, | ||
146 | key_path: String, | ||
147 | } | ||
148 | |||
149 | #[derive(Debug, Eq, PartialEq, Copy, Clone)] | ||
150 | enum LoadConfigError { | ||
151 | BaseUrlNotProvided, | ||
152 | BaseUrlSlashSuffixMissing, | ||
153 | KeyPathNotProvided, | ||
154 | } | ||
155 | |||
156 | impl fmt::Display for LoadConfigError { | ||
157 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
158 | match self { | ||
159 | Self::BaseUrlNotProvided => write!(f, "base URL not provided"), | ||
160 | Self::BaseUrlSlashSuffixMissing => write!(f, "base URL does not end with slash"), | ||
161 | Self::KeyPathNotProvided => write!(f, "key path not provided"), | ||
162 | } | ||
163 | } | ||
164 | } | ||
165 | |||
166 | fn load_config() -> Result<Config, LoadConfigError> { | ||
167 | let Ok(href_base) = std::env::var("GITOLFS3_HREF_BASE") else { | ||
168 | return Err(LoadConfigError::BaseUrlNotProvided); | ||
169 | }; | ||
170 | if !href_base.ends_with('/') { | ||
171 | return Err(LoadConfigError::BaseUrlSlashSuffixMissing); | ||
172 | } | ||
173 | let Ok(key_path) = std::env::var("GITOLFS3_KEY_PATH") else { | ||
174 | return Err(LoadConfigError::KeyPathNotProvided); | ||
175 | }; | ||
176 | Ok(Config { | ||
177 | href_base, | ||
178 | key_path, | ||
179 | }) | ||
180 | } | ||
181 | |||
182 | fn main() -> ExitCode { | ||
183 | let config = match load_config() { | ||
184 | Ok(config) => config, | ||
185 | Err(e) => { | ||
186 | eprintln!("Failed to load config: {e}"); | ||
187 | return ExitCode::FAILURE; | ||
188 | } | ||
189 | }; | ||
190 | let key = match common::load_key(&config.key_path) { | ||
191 | Ok(key) => key, | ||
192 | Err(e) => { | ||
193 | eprintln!("Failed to load key: {e}"); | ||
194 | return ExitCode::FAILURE; | ||
195 | } | ||
196 | }; | ||
197 | |||
198 | let (repo_name, operation) = match parse_cmdline() { | ||
199 | Ok(args) => args, | ||
200 | Err(e) => { | ||
201 | eprintln!("Error: {e}\n"); | ||
202 | help(); | ||
203 | // Exit code 2 signifies bad usage of CLI. | ||
204 | return ExitCode::from(2); | ||
205 | } | ||
206 | }; | ||
207 | |||
208 | if !repo_exists(&repo_name) { | ||
209 | eprintln!("Error: repository does not exist"); | ||
210 | return ExitCode::FAILURE; | ||
211 | } | ||
212 | |||
213 | let expires_at = Utc::now() + Duration::from_secs(5 * 60); | ||
214 | let Some(tag) = common::generate_tag( | ||
215 | common::Claims { | ||
216 | specific_claims: common::SpecificClaims::BatchApi(operation), | ||
217 | repo_path: &repo_name, | ||
218 | expires_at, | ||
219 | }, | ||
220 | key, | ||
221 | ) else { | ||
222 | eprintln!("Failed to generate validation tag"); | ||
223 | return ExitCode::FAILURE; | ||
224 | }; | ||
225 | |||
226 | println!( | ||
227 | "{{\"header\":{{\"Authorization\":\"Gitolfs3-Hmac-Sha256 {tag} {}\"}},\ | ||
228 | \"expires_at\":\"{}\",\"href\":\"{}{}/info/lfs\"}}", | ||
229 | expires_at.timestamp(), | ||
230 | common::EscJsonFmt(&expires_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)), | ||
231 | common::EscJsonFmt(&config.href_base), | ||
232 | common::EscJsonFmt(&repo_name), | ||
233 | ); | ||
234 | |||
235 | ExitCode::SUCCESS | ||
236 | } | ||