git-gemini-forge 0.4.0

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};

#[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.
	};

	return 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 mode_dest: &str;
			if base == "users" {
				mode_dest = "/ Recent Activity";
			} else {
				mode_dest = "/users Users";
			}
			let version = cfg.crate_version;
			return format!("\n==========================\n=> {mode_dest}\n\nPowered by git-gemini-forge v{version}\n=> gemini://git.average.name/AverageHelper/git-gemini-forge View source")
		})
		.mount("/", |_| {
			route(handlers::root::handler(cfg))
		})
		.mount("/users", |_| {
			route(handlers::users::handler(cfg))
		})
		.mount("/:user", |ctx| {
			let user: &str = ctx.url.path().split("/").last().unwrap_or("");
			route(handlers::user::handler(cfg, user))
		})
		.mount("/:user/:repo", |ctx| {
			let path_segments = ctx.url.path().split("/").collect::<Vec<&str>>();
			let user: &str = path_segments[1];
			let repo: &str = path_segments[2];
			route(handlers::repo::handler(cfg, user, repo))
		})
		.mount("/:user/:repo/src/branch/:branch", |ctx| {
			let path_segments = ctx.url.path().split("/").collect::<Vec<&str>>();
			let _user: &str = path_segments[1];
			let _repo: &str = path_segments[2];
			let _branch: &str = path_segments[5];
			return Response::temporary_failure("That route is not implemented yet");
		})
		.mount("/:user/:repo/src/branch/:branch/:filename", |ctx| {
			let path_segments = ctx.url.path().split("/").collect::<Vec<&str>>();
			let _user: &str = path_segments[1];
			let _repo: &str = path_segments[2];
			let _branch: &str = path_segments[5];
			let _filename: &str = path_segments[6];
			return Response::temporary_failure("That route is not implemented yet");
		})
		// .mount("/*", windmark::not_found!("Nothing to be found here!"))
		.set_error_handler(|error| {
			let failure_addr = error.url;
			println!("Failed to serve {failure_addr}");
			return 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"),
	}
}