git-gemini-forge 0.6.6

A simple Gemini server that serves a read-only view of public repositories from a Git forge.
use crate::network::{Api, ForgeApiKind, forgejo::ForgejoApi, gitlab::GitLabApi};
use reqwest::header::{HeaderValue, InvalidHeaderValue};
use secrecy::{CloneableSecret, SecretBox, zeroize::Zeroize};
use url::Url;

#[derive(Clone, Debug)]
pub struct Config {
	/// The directory to look for certificate files. (e.g. ".certs")
	pub certs_dir: String,

	/// The kind of git forge software we're dealing with. (e.g. "forgejo")
	pub forge_kind: ForgeApiKind,

	/// Whether we're permitted to list users. (e.g. true)
	pub forge_list_users: bool,

	/// A value that grants us permission to call the forge's API.
	pub forge_secret_key: Option<SecretHeader>,

	/// The URL where the forge lives. (e.g. "https://git.average.name")
	pub forge_url: Url,

	/// The port on which to serve the capsule. (e.g. "1965")
	pub port: u16,
}

#[derive(Clone)]
pub(crate) struct ZeroizedHeaderValue(Vec<u8>);
impl CloneableSecret for ZeroizedHeaderValue {}
impl Zeroize for ZeroizedHeaderValue {
	fn zeroize(&mut self) {
		self.0.zeroize();
	}
}
impl ZeroizedHeaderValue {
	/// Checks that the given string is a valid header value. Only byte values
	/// between 32 and 255 (inclusive) are permitted, excluding byte 127 (DEL).
	pub(crate) fn try_from(value: String) -> Result<Self, InvalidHeaderValue> {
		let mut value = HeaderValue::try_from(value)?;
		value.set_sensitive(true);
		Ok(Self::from(value))
	}

	pub(crate) fn from(value: HeaderValue) -> Self {
		Self(value.as_bytes().to_owned())
	}

	/// Constructs a sensitive header value.
	pub(crate) fn as_header_value(&self) -> HeaderValue {
		let mut value = HeaderValue::from_bytes(&self.0).expect("value already checked");
		value.set_sensitive(true);
		value
	}

	pub(crate) fn with_prefix(&self, prefix: &str) -> Self {
		let value = [prefix.as_bytes(), self.0.as_slice()].concat();
		Self(value)
	}
}

pub(crate) type SecretHeader = SecretBox<ZeroizedHeaderValue>;

impl From<ZeroizedHeaderValue> for SecretHeader {
	fn from(value: ZeroizedHeaderValue) -> Self {
		Self::new(Box::new(value))
	}
}

impl Default for Config {
	fn default() -> Self {
		Self {
			certs_dir: String::from(".certs"),
			forge_kind: ForgeApiKind::Forgejo,
			forge_list_users: false,
			forge_secret_key: None,
			forge_url: Url::parse("http://localhost:3000").expect("Valid default URL"),
			port: 1965,
		}
	}
}

impl Config {
	/// Constructs a forge API sentinel.
	// TODO: Maybe make this generic?
	pub fn forge_api(&self) -> Api {
		// TODO: Only instantiate the ForgeApi once.
		let kind = self.forge_kind;
		let forge_url = self.forge_url.clone();
		let private_token = &self.forge_secret_key;
		println!("Preparing {kind} API");

		match kind {
			ForgeApiKind::Forgejo => {
				Api::Forgejo(ForgejoApi::from_url(forge_url, kind, private_token))
			}
			ForgeApiKind::Gitea => Api::Gitea(ForgejoApi::from_url(forge_url, kind, private_token)),
			ForgeApiKind::GitLab => Api::GitLab(GitLabApi::from_url(forge_url, private_token)),
		}
	}
}