aboutsummaryrefslogtreecommitdiffstats
path: root/gitolfs3-authenticate/src
diff options
context:
space:
mode:
Diffstat (limited to 'gitolfs3-authenticate/src')
-rw-r--r--gitolfs3-authenticate/src/main.rs134
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 @@
1use anyhow::{anyhow, bail, Result};
2use chrono::Utc;
3use gitolfs3_common::{generate_tag, load_key, Claims, Key, Operation, SpecificClaims};
4use serde_json::json;
5use std::{process::ExitCode, time::Duration};
6
7fn 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
59struct Config {
60 href_base: String,
61 key: Key,
62}
63
64impl 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
82fn 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
91fn 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
108fn 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
129fn repo_exists(name: &str) -> bool {
130 match std::fs::metadata(name) {
131 Ok(metadata) => metadata.is_dir(),
132 _ => false,
133 }
134}