use crate::config::ConfigSet;
use crate::error::Error;
use thiserror::Error;
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum ProtocolError {
#[error("protocol '{protocol}' is not allowed by GIT_ALLOW_PROTOCOL")]
NotAllowedByEnvironment {
protocol: String,
},
#[error("protocol '{protocol}' is not allowed")]
NotAllowed {
protocol: String,
},
#[error("unknown protocol.allow value '{value}' for protocol '{protocol}'")]
UnknownAllowValue {
protocol: String,
value: String,
},
}
#[derive(Clone, Debug, Default)]
pub struct ProtocolPolicyInputs {
pub git_allow_protocol: Option<String>,
pub git_protocol_from_user: Option<String>,
pub specific_allow: Option<String>,
pub blanket_allow: Option<String>,
}
pub fn check_protocol_allowed_with(
protocol: &str,
inputs: &ProtocolPolicyInputs,
) -> Result<(), ProtocolError> {
if let Some(val) = inputs.git_allow_protocol.as_deref() {
let allowed: Vec<&str> = val
.split([':', ','])
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
if allowed.contains(&protocol) {
return Ok(());
}
return Err(ProtocolError::NotAllowedByEnvironment {
protocol: protocol.to_owned(),
});
}
if let Some(ref val) = inputs.specific_allow {
return check_allow_value(protocol, val, inputs.git_protocol_from_user.as_deref());
}
if let Some(ref val) = inputs.blanket_allow {
return check_allow_value(protocol, val, inputs.git_protocol_from_user.as_deref());
}
match protocol.to_ascii_lowercase().as_str() {
"http" | "https" | "git" | "ssh" => Ok(()),
"ext" => Err(ProtocolError::NotAllowed {
protocol: protocol.to_owned(),
}),
_ => check_allow_value(protocol, "user", inputs.git_protocol_from_user.as_deref()),
}
}
fn check_allow_value(
protocol: &str,
value: &str,
git_protocol_from_user: Option<&str>,
) -> Result<(), ProtocolError> {
match value.to_lowercase().as_str() {
"always" => Ok(()),
"never" => Err(ProtocolError::NotAllowed {
protocol: protocol.to_owned(),
}),
"user" => {
if protocol_from_user(git_protocol_from_user) {
Ok(())
} else {
Err(ProtocolError::NotAllowed {
protocol: protocol.to_owned(),
})
}
}
other => Err(ProtocolError::UnknownAllowValue {
protocol: protocol.to_owned(),
value: other.to_owned(),
}),
}
}
#[must_use]
pub fn protocol_from_user(raw: Option<&str>) -> bool {
match raw {
None => true,
Some(v) => {
let v = v.trim().to_ascii_lowercase();
v.is_empty() || !matches!(v.as_str(), "0" | "false" | "no" | "off")
}
}
}
#[must_use]
pub fn parse_protocol_version_digit(s: &str) -> Option<u8> {
match s.trim() {
"0" => Some(0),
"1" => Some(1),
"2" => Some(2),
_ => None,
}
}
pub fn try_protocol_version_from_config_value(raw: &str) -> Result<u8, Error> {
parse_protocol_version_digit(raw)
.ok_or_else(|| Error::Config(format!("bad protocol version '{raw}'").into()))
}
pub fn try_effective_client_protocol_version_from_config(config: &ConfigSet) -> Result<u8, Error> {
config
.get("protocol.version")
.map_or(Ok(2), |v| try_protocol_version_from_config_value(&v))
}
#[must_use]
pub fn client_git_protocol_header_value(version: u8) -> Option<String> {
match version {
0 => None,
1 => Some("version=1".to_string()),
_ => Some("version=2".to_string()),
}
}
pub fn client_git_protocol_header_from_config(config: &ConfigSet) -> Result<Option<String>, Error> {
Ok(client_git_protocol_header_value(
try_effective_client_protocol_version_from_config(config)?,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_git_protocol_header_matches_version() {
assert_eq!(client_git_protocol_header_value(0), None);
assert_eq!(
client_git_protocol_header_value(1).as_deref(),
Some("version=1")
);
assert_eq!(
client_git_protocol_header_value(2).as_deref(),
Some("version=2")
);
}
#[test]
fn protocol_version_from_config_defaults_to_v2_header() {
let config = ConfigSet::new();
assert_eq!(
client_git_protocol_header_from_config(&config)
.expect("header")
.as_deref(),
Some("version=2")
);
}
#[test]
fn protocol_version_from_config_honors_protocol_version_key() {
let mut config = ConfigSet::new();
config
.add_command_override("protocol.version", "1")
.expect("override");
assert_eq!(
client_git_protocol_header_from_config(&config)
.expect("header")
.as_deref(),
Some("version=1")
);
}
#[test]
fn protocol_version_rejects_invalid_config_value() {
let mut config = ConfigSet::new();
config
.add_command_override("protocol.version", "9")
.expect("override");
let err = client_git_protocol_header_from_config(&config).expect_err("invalid");
assert!(
matches!(err, Error::Config(_)),
"expected ConfigError, got {err:?}"
);
}
#[test]
fn protocol_version_defaults_to_two_and_honors_config() {
let mut config = ConfigSet::new();
assert_eq!(
try_effective_client_protocol_version_from_config(&config).expect("default"),
2
);
config
.add_command_override("protocol.version", "0")
.expect("override");
assert_eq!(
try_effective_client_protocol_version_from_config(&config).expect("v0"),
0
);
}
}