aboutsummaryrefslogtreecommitdiffstats
path: root/git-lfs-authenticate/src/main.rs
blob: 3101c9287a1611dfa822b14d307cae0f0871fbce (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
use anyhow::{anyhow, bail, Result};
use chrono::Utc;
use serde_json::json;
use std::{process::ExitCode, time::Duration};

fn main() -> ExitCode {
    let config = match Config::load() {
        Ok(config) => config,
        Err(e) => {
            eprintln!("Error: {e}");
            return ExitCode::from(2);
        }
    };

    let (repo_name, operation) = match parse_cmdline() {
        Ok(args) => args,
        Err(e) => {
            eprintln!("Error: {e}\n");
            eprintln!("Usage: git-lfs-authenticate <REPO> upload/download");
            // Exit code 2 signifies bad usage of CLI.
            return ExitCode::from(2);
        }
    };

    if !repo_exists(&repo_name) {
        eprintln!("Error: repository does not exist");
        return ExitCode::FAILURE;
    }

    let expires_at = Utc::now() + Duration::from_secs(5 * 60);
    let Some(tag) = common::generate_tag(
        common::Claims {
            specific_claims: common::SpecificClaims::BatchApi(operation),
            repo_path: &repo_name,
            expires_at,
        },
        config.key,
    ) else {
        eprintln!("Failed to generate validation tag");
        return ExitCode::FAILURE;
    };

    let response = json!({
        "header": {
            "Authorization": format!(
                "Gitolfs3-Hmac-Sha256 {tag} {}",
                expires_at.timestamp()
            ),
        },
        "expires_at": expires_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
        "href": format!("{}{}/info/lfs", config.href_base, repo_name),
    });
    println!("{}", response.to_string());

    ExitCode::SUCCESS
}

struct Config {
    href_base: String,
    key: common::Key,
}

impl Config {
    fn load() -> Result<Self> {
        let Ok(href_base) = std::env::var("GITOLFS3_HREF_BASE") else {
            bail!("configured base URL not provided");
        };
        if !href_base.ends_with('/') {
            bail!("configured base URL does not end with a slash");
        }

        let Ok(key_path) = std::env::var("GITOLFS3_KEY_PATH") else {
            bail!("key path not provided");
        };
        let key = common::load_key(&key_path).map_err(|e| anyhow!("failed to load key: {e}"))?;

        Ok(Self { href_base, key })
    }
}

fn parse_cmdline() -> Result<(String, common::Operation)> {
    let [repo_path, op_str] = get_cmdline_args::<2>()?;
    let op: common::Operation = op_str
        .parse()
        .map_err(|e| anyhow!("unknown operation: {e}"))?;
    validate_repo_path(&repo_path).map_err(|e| anyhow!("invalid repository name: {e}"))?;
    Ok((repo_path.to_string(), op))
}

fn get_cmdline_args<const N: usize>() -> Result<[String; N]> {
    let args = std::env::args();
    if args.len() - 1 != N {
        bail!("got {} argument(s), expected {}", args.len() - 1, N);
    }

    // Does not allocate.
    const EMPTY_STRING: String = String::new();
    let mut values = [EMPTY_STRING; N];

    // Skip the first element; we do not care about the program name.
    for (i, arg) in args.skip(1).enumerate() {
        values[i] = arg
    }
    Ok(values)
}

fn validate_repo_path(path: &str) -> Result<()> {
    if path.len() > 100 {
        bail!("too long (more than 100 characters)");
    }
    if path.contains("//")
        || path.contains("/./")
        || path.contains("/../")
        || path.starts_with("./")
        || path.starts_with("../")
    {
        bail!("contains one or more path elements '.' and '..'");
    }
    if path.starts_with('/') {
        bail!("starts with '/', which is not allowed");
    }
    if !path.ends_with(".git") {
        bail!("missed '.git' suffix");
    }
    Ok(())
}

fn repo_exists(name: &str) -> bool {
    match std::fs::metadata(name) {
        Ok(metadata) => metadata.is_dir(),
        _ => false,
    }
}