diff options
Diffstat (limited to 'git-lfs-authenticate/src')
-rw-r--r-- | git-lfs-authenticate/src/main.rs | 275 |
1 files changed, 84 insertions, 191 deletions
diff --git a/git-lfs-authenticate/src/main.rs b/git-lfs-authenticate/src/main.rs index 36d7818..accc37f 100644 --- a/git-lfs-authenticate/src/main.rs +++ b/git-lfs-authenticate/src/main.rs | |||
@@ -1,197 +1,13 @@ | |||
1 | use std::{fmt, process::ExitCode, time::Duration}; | 1 | use anyhow::{anyhow, bail, Result}; |
2 | |||
3 | use chrono::Utc; | 2 | use chrono::Utc; |
4 | use common::{Operation, ParseOperationError}; | 3 | use std::{process::ExitCode, time::Duration}; |
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 | 4 | ||
182 | fn main() -> ExitCode { | 5 | fn main() -> ExitCode { |
183 | let config = match load_config() { | 6 | let config = match Config::load() { |
184 | Ok(config) => config, | 7 | Ok(config) => config, |
185 | Err(e) => { | 8 | Err(e) => { |
186 | eprintln!("Failed to load config: {e}"); | 9 | eprintln!("Error: {e}"); |
187 | return ExitCode::FAILURE; | 10 | return ExitCode::from(2); |
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 | } | 11 | } |
196 | }; | 12 | }; |
197 | 13 | ||
@@ -199,7 +15,7 @@ fn main() -> ExitCode { | |||
199 | Ok(args) => args, | 15 | Ok(args) => args, |
200 | Err(e) => { | 16 | Err(e) => { |
201 | eprintln!("Error: {e}\n"); | 17 | eprintln!("Error: {e}\n"); |
202 | help(); | 18 | eprintln!("Usage: git-lfs-authenticate <REPO> upload/download"); |
203 | // Exit code 2 signifies bad usage of CLI. | 19 | // Exit code 2 signifies bad usage of CLI. |
204 | return ExitCode::from(2); | 20 | return ExitCode::from(2); |
205 | } | 21 | } |
@@ -217,7 +33,7 @@ fn main() -> ExitCode { | |||
217 | repo_path: &repo_name, | 33 | repo_path: &repo_name, |
218 | expires_at, | 34 | expires_at, |
219 | }, | 35 | }, |
220 | key, | 36 | config.key, |
221 | ) else { | 37 | ) else { |
222 | eprintln!("Failed to generate validation tag"); | 38 | eprintln!("Failed to generate validation tag"); |
223 | return ExitCode::FAILURE; | 39 | return ExitCode::FAILURE; |
@@ -234,3 +50,80 @@ fn main() -> ExitCode { | |||
234 | 50 | ||
235 | ExitCode::SUCCESS | 51 | ExitCode::SUCCESS |
236 | } | 52 | } |
53 | |||
54 | struct Config { | ||
55 | href_base: String, | ||
56 | key: common::Key, | ||
57 | } | ||
58 | |||
59 | impl Config { | ||
60 | fn load() -> Result<Self> { | ||
61 | let Ok(href_base) = std::env::var("GITOLFS3_HREF_BASE") else { | ||
62 | bail!("configured base URL not provided"); | ||
63 | }; | ||
64 | if !href_base.ends_with('/') { | ||
65 | bail!("configured base URL does not end with a slash"); | ||
66 | } | ||
67 | |||
68 | let Ok(key_path) = std::env::var("GITOLFS3_KEY_PATH") else { | ||
69 | bail!("key path not provided"); | ||
70 | }; | ||
71 | let key = common::load_key(&key_path).map_err(|e| anyhow!("failed to load key: {e}"))?; | ||
72 | |||
73 | Ok(Self { href_base, key }) | ||
74 | } | ||
75 | } | ||
76 | |||
77 | fn parse_cmdline() -> Result<(String, common::Operation)> { | ||
78 | let [repo_path, op_str] = get_cmdline_args::<2>()?; | ||
79 | let op: common::Operation = op_str | ||
80 | .parse() | ||
81 | .map_err(|e| anyhow!("unknown operation: {e}"))?; | ||
82 | validate_repo_path(&repo_path).map_err(|e| anyhow!("invalid repository name: {e}"))?; | ||
83 | Ok((repo_path.to_string(), op)) | ||
84 | } | ||
85 | |||
86 | fn get_cmdline_args<const N: usize>() -> Result<[String; N]> { | ||
87 | let args = std::env::args(); | ||
88 | if args.len() - 1 != N { | ||
89 | bail!("got {} argument(s), expected {}", args.len() - 1, N); | ||
90 | } | ||
91 | |||
92 | // Does not allocate. | ||
93 | const EMPTY_STRING: String = String::new(); | ||
94 | let mut values = [EMPTY_STRING; N]; | ||
95 | |||
96 | // Skip the first element; we do not care about the program name. | ||
97 | for (i, arg) in args.skip(1).enumerate() { | ||
98 | values[i] = arg | ||
99 | } | ||
100 | Ok(values) | ||
101 | } | ||
102 | |||
103 | fn validate_repo_path(path: &str) -> Result<()> { | ||
104 | if path.len() > 100 { | ||
105 | bail!("too long (more than 100 characters)"); | ||
106 | } | ||
107 | if path.contains("//") | ||
108 | || path.contains("/./") | ||
109 | || path.contains("/../") | ||
110 | || path.starts_with("./") | ||
111 | || path.starts_with("../") | ||
112 | { | ||
113 | bail!("contains one or more path elements '.' and '..'"); | ||
114 | } | ||
115 | if path.starts_with('/') { | ||
116 | bail!("starts with '/', which is not allowed"); | ||
117 | } | ||
118 | if !path.ends_with(".git") { | ||
119 | bail!("missed '.git' suffix"); | ||
120 | } | ||
121 | Ok(()) | ||
122 | } | ||
123 | |||
124 | fn repo_exists(name: &str) -> bool { | ||
125 | match std::fs::metadata(name) { | ||
126 | Ok(metadata) => metadata.is_dir(), | ||
127 | _ => false, | ||
128 | } | ||
129 | } | ||