geo302 0.1.2

A simple geoIP-based redirect proxy
use crate::config::parse_config;
use crate::geo::{Continent, Geo};
use crate::healthcheck::check_health;
use crate::mirror::{ContinentMap, Mirror, MirrorVec};
use crate::rejects::{handle_rejection, BrokenPath, MirrorsUnavailable};
use std::net::{IpAddr, SocketAddr};
use std::str::FromStr;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use warp::http::header::{
    HeaderMap, HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue,
};
use warp::http::Uri;
use warp::path::FullPath;
use warp::reply::Reply;
use warp::Filter;

mod config;
mod filters;
mod geo;
mod healthcheck;
mod mirror;
mod rejects;

#[derive(Error, Debug)]
pub enum HeaderError {
    #[error("{0}")]
    InvalidHeaderName(InvalidHeaderName),
    #[error("{0}")]
    InvalidHeaderValue(InvalidHeaderValue),
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let config_path = std::env::args()
        .nth(1)
        .unwrap_or_else(|| "geo302.toml".to_owned());

    let config = parse_config(config_path)?;
    let host: SocketAddr = config.host.parse()?;
    let ip_header_recursive = config.ip_headers_recursive;
    let continent_map = ContinentMap::from_config(&config)?;
    let geo = Arc::new(Geo::from_config(&config)?);
    let check_interval = Duration::new(config.healthckeck_interval.get().into(), 0);
    let ip_header_names = config.ip_headers;
    let response_headers = config
        .response_headers
        .iter()
        .map(|(name, value)| {
            let name = match HeaderName::from_str(name) {
                Ok(name) => name,
                Err(err) => return Err(HeaderError::InvalidHeaderName(err)),
            };
            let value = match HeaderValue::from_str(value) {
                Ok(value) => value,
                Err(err) => return Err(HeaderError::InvalidHeaderValue(err)),
            };
            Ok((name, value))
        })
        .collect::<Result<HeaderMap, _>>()?;

    simple_logger::init_with_level(config.log_level)?;

    check_health(continent_map.all_mirrors(), check_interval);

    let logs = warp::log::custom(|info| {
        log::info!(
            "{} {} {} {}",
            info.remote_addr()
                .map_or_else(|| "_".into(), |addr| format!("{}", addr.ip())),
            info.method(),
            info.path(),
            info.status(),
        )
    });

    let routes = warp::get()
        .and(filters::client_ip(ip_header_names, ip_header_recursive))
        .map(
            move |ip: Option<IpAddr>| match ip.and_then(|ip| geo.try_lookup_continent(ip).ok()) {
                Some(continent) => continent_map.get(continent),
                None => continent_map.get_default(),
            },
        )
        .and(warp::path::full())
        .and(
            warp::query::raw()
                .map(Option::Some)
                .or_else(|_err| async { Ok::<_, warp::Rejection>((None,)) }),
        )
        .and_then(
            |mirrors: MirrorVec, path: FullPath, query: Option<String>| async move {
                let mirror = {
                    let mut it_mirrors = mirrors.iter();
                    loop {
                        match it_mirrors.next() {
                            Some(mirror) => {
                                if mirror.available.load(Ordering::Acquire) {
                                    break mirror;
                                }
                            }
                            None => return Err(warp::reject::custom(MirrorsUnavailable)),
                        }
                    }
                };
                let url = {
                    let mut url = mirror
                        .upstream
                        .join(path.as_str().trim_start_matches('/'))
                        .map_err(|_| warp::reject::custom(BrokenPath))?;
                    url.set_query(query.as_deref());
                    url
                };

                Ok(warp::redirect::found(url.as_str().parse::<Uri>().unwrap()).into_response())
            },
        )
        .with(warp::filters::reply::headers(response_headers))
        .recover(handle_rejection)
        .with(logs);

    warp::serve(routes).run(host).await;
    Ok(())
}