aboutsummaryrefslogtreecommitdiffstats
path: root/git-lfs-authenticate
diff options
context:
space:
mode:
Diffstat (limited to 'git-lfs-authenticate')
-rw-r--r--git-lfs-authenticate/Cargo.toml1
-rw-r--r--git-lfs-authenticate/src/main.rs275
2 files changed, 85 insertions, 191 deletions
diff --git a/git-lfs-authenticate/Cargo.toml b/git-lfs-authenticate/Cargo.toml
index 217250f..f4ab4d7 100644
--- a/git-lfs-authenticate/Cargo.toml
+++ b/git-lfs-authenticate/Cargo.toml
@@ -4,5 +4,6 @@ version = "0.1.0"
4edition = "2021" 4edition = "2021"
5 5
6[dependencies] 6[dependencies]
7anyhow = "1.0"
7chrono = "0.4" 8chrono = "0.4"
8common = { path = "../common" } 9common = { path = "../common" }
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 @@
1use std::{fmt, process::ExitCode, time::Duration}; 1use anyhow::{anyhow, bail, Result};
2
3use chrono::Utc; 2use chrono::Utc;
4use common::{Operation, ParseOperationError}; 3use std::{process::ExitCode, time::Duration};
5
6fn help() {
7 eprintln!("Usage: git-lfs-authenticate <REPO> upload/download");
8}
9
10#[derive(Debug, Eq, PartialEq, Copy, Clone)]
11enum RepoNameError {
12 TooLong,
13 UnresolvedPath,
14 AbsolutePath,
15 MissingGitSuffix,
16}
17
18impl 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.
38fn 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)]
60enum ParseCmdlineError {
61 UnknownOperation(ParseOperationError),
62 InvalidRepoName(RepoNameError),
63 UnexpectedArgCount(ArgCountError),
64}
65
66impl From<RepoNameError> for ParseCmdlineError {
67 fn from(value: RepoNameError) -> Self {
68 Self::InvalidRepoName(value)
69 }
70}
71
72impl From<ParseOperationError> for ParseCmdlineError {
73 fn from(value: ParseOperationError) -> Self {
74 Self::UnknownOperation(value)
75 }
76}
77
78impl From<ArgCountError> for ParseCmdlineError {
79 fn from(value: ArgCountError) -> Self {
80 Self::UnexpectedArgCount(value)
81 }
82}
83
84impl 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)]
95struct ArgCountError {
96 provided: usize,
97 expected: usize,
98}
99
100impl 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
110fn 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
130fn 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
137fn repo_exists(name: &str) -> bool {
138 match std::fs::metadata(name) {
139 Ok(metadata) => metadata.is_dir(),
140 _ => false,
141 }
142}
143
144struct Config {
145 href_base: String,
146 key_path: String,
147}
148
149#[derive(Debug, Eq, PartialEq, Copy, Clone)]
150enum LoadConfigError {
151 BaseUrlNotProvided,
152 BaseUrlSlashSuffixMissing,
153 KeyPathNotProvided,
154}
155
156impl 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
166fn 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
182fn main() -> ExitCode { 5fn 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
54struct Config {
55 href_base: String,
56 key: common::Key,
57}
58
59impl 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
77fn 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
86fn 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
103fn 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
124fn repo_exists(name: &str) -> bool {
125 match std::fs::metadata(name) {
126 Ok(metadata) => metadata.is_dir(),
127 _ => false,
128 }
129}