git-gemini-forge 0.5.2

A simple Gemini server that serves a read-only view of public repositories from a Git forge.
mod config;
mod handlers;
mod network;

use config::*;
use network::error::Error as NetworkError;
use windmark::{context::RouteContext, response::Response};

fn user(cfg: &Config, ctx: RouteContext) -> Response {
	let username: &str = ctx.parameters.get("user").expect("Path segment");
	route(handlers::user::handler(cfg, username))
}

fn user_repo(cfg: &Config, ctx: RouteContext) -> Response {
	let username: &str = ctx.parameters.get("user").expect("Path segment");
	let repo: &str = ctx.parameters.get("repo").expect("Path segment");
	route(handlers::repo::handler(cfg, username, repo))
}

fn branch(_cfg: &Config, ctx: RouteContext) -> Response {
	let _username: &str = ctx.parameters.get("user").expect("Path segment");
	let _repo: &str = ctx.parameters.get("repo").expect("Path segment");
	let _branch: &str = ctx.parameters.get("branch").expect("Path segment");
	Response::temporary_failure("That route is not implemented yet")
}

fn file(_cfg: &Config, ctx: RouteContext) -> Response {
	let _username: &str = ctx.parameters.get("user").expect("Path segment");
	let _repo: &str = ctx.parameters.get("repo").expect("Path segment");
	let _branch: &str = ctx.parameters.get("branch").expect("Path segment");
	let _file_path: &str = ctx.parameters.get("path").expect("Path segment");
	Response::temporary_failure("That route is not implemented yet")
}

#[windmark::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
	let cfg: &Config = config(); // Halts if misconfigured

	// Make sure we can connect to the forge
	let kind = cfg.forge_api().kind().to_string();
	match cfg.forge_api().get_version() {
		Ok(server_version) => {
			// Print the forge's reported version
			let version = server_version.version;
			println!("{kind} {version}");
		}
		Err(NetworkError::Unauthorized) => {
			// Warn that the forge complained at our question
			println!("Warning: We are not authorized to query metadata of this {kind} instance. Some routes may return errors.");
		}
		Err(err) => Err(err)?, // Halts if the forge is unavailable.
	};

	windmark::router::Router::new()
		.set_private_key_file(format!("{}/key.pem", cfg.certs_dir))
		.set_certificate_file(format!("{}/cert.pem", cfg.certs_dir))
		.add_header(move |_| format!("# {kind} Proxy"))
		.add_footer(|ctx: RouteContext| {
			let base: &str = ctx.url.path().split("/").collect::<Vec<&str>>()[1];
			let should_list_users = cfg.forge_list_users;
			let mode_dest: &str = if base == "users" {
				"=> / Recent Activity\n\n"
			} else if should_list_users {
				"=> /users Users\n\n"
			} else {
				"\n"
			};
			let version = cfg.crate_version;
			return format!("\n==========================\n{mode_dest}=> gemini://git.average.name/AverageHelper/git-gemini-forge Powered by git-gemini-forge v{version}")
		})
		.mount("/", |_| route(handlers::root::handler(cfg)))
		.mount("/users", |_| route(handlers::users::handler(cfg)))
		.mount("/users/", |_| route(handlers::users::handler(cfg)))
		.mount("/:user", |ctx| user(cfg, ctx))
		.mount("/:user/", |ctx| user(cfg, ctx))
		.mount("/:user/:repo", |ctx| user_repo(cfg, ctx))
		.mount("/:user/:repo/", |ctx| user_repo(cfg, ctx))
		.mount("/:user/:repo/src/branch/:branch", |ctx| branch(cfg, ctx))
		.mount("/:user/:repo/src/branch/:branch/", |ctx| branch(cfg, ctx))
		.mount("/:user/:repo/src/branch/:branch/*path", |ctx| {
			file(cfg, ctx)
		})
		// .mount("/*", windmark::not_found!("Nothing to be found here!"))
		.set_error_handler(|error| {
			let failure_addr = error.url;
			eprintln!("Failed to serve {failure_addr}");
			Response::temporary_failure("Something went wrong!")
		})
		.run()
		.await
}

/// Returns an appropriate response for the given handler result.
fn route(res: Result<Response, NetworkError>) -> Response {
	return match res {
		Ok(res) => res,
		Err(NetworkError::NetworkFailure) => {
			Response::proxy_error("Couldn't communicate with the forge")
		}
		Err(NetworkError::ResourceNotFound) => Response::not_found("Not found"),
		Err(NetworkError::Unauthorized) => Response::proxy_error(
			"We aren't authorized to communicate with the forge on this route",
		),
		Err(NetworkError::UnexpectedResponse) => {
			Response::temporary_failure("The upstream gave us a response we don't understand")
		}
	};
}