use axum::{ extract::{ self, Query, State, Extension, }, headers, http::Request, handler::Handler, middleware::{self, Next}, response::Response, Router, routing::get, TypedHeader, }; use axum_client_ip::SecureClientIp; use clap::Parser; use lazy_static::lazy_static; use regex::Regex; use serde::{Deserialize,Serialize}; use tera::Tera; use tower::ServiceBuilder; use tower_http::services::ServeDir; use trust_dns_resolver::TokioAsyncResolver; use tokio::signal::unix::{ signal, SignalKind, }; use tokio::task; use std::collections::HashMap; use std::fs; use std::net::IpAddr; use std::sync::Arc; mod config; mod geoip; mod ipinfo; mod ratelimit; mod settings; mod simple_dns; mod templating_engine; mod idna; use crate::geoip::{ QueryAsn, QueryLocation, AsnResult, LocationResult, }; use crate::idna::IdnaName; use crate::simple_dns::DnsLookupResult; use crate::settings::*; use crate::templating_engine::View; use crate::ipinfo::{AddressCast,AddressInfo,AddressScope}; #[derive(Deserialize, Serialize, Clone)] pub struct SettingsQuery { format: Option, lang: Option, dns: Option, } #[derive(Deserialize, Serialize, Clone)] pub struct SearchQuery { query: Option, } #[derive(Serialize, Clone)] pub struct IpResult { address: IpAddr, hostname: Option, asn: Option, location: Option, ip_info: AddressInfo, used_dns_resolver: Option, } // We need this one to hide the partial lookup field when irelevant pub fn not(b: &bool) -> bool { !b } #[derive(Serialize, Default, Clone)] pub struct DigResult { records: simple_dns::DnsLookupResult, #[serde(skip_serializing_if = "IdnaName::was_ascii")] idn: IdnaName, #[serde(skip_serializing_if = "not")] partial_lookup: bool, used_dns_resolver: String, } struct ServiceSharedState { templating_engine: templating_engine::Engine, dns_resolvers: HashMap, asn_db: geoip::MMDBCarrier, location_db: geoip::MMDBCarrier, config: config::EchoIpServiceConfig, } // Stores configuration that is derived from the original configuration #[derive(Clone)] struct DerivedConfiguration { dns_resolver_selectables: Vec, } #[derive(Parser)] #[command(author, version, long_about="A web service that tells you your ip-address and more …")] struct CliArgs { #[arg(short, long)] config: Option, #[arg(short, long)] listen_on: Option, #[arg(short, long)] template_location: Option, #[arg(short,long)] extra_config: Option, #[arg(short,long)] static_location: Option, } fn match_domain_hidden_list(domain: &String, hidden_list: &Vec) -> bool { let name = domain.trim_end_matches("."); for suffix in hidden_list { if name.ends_with(suffix) { return true; } } return false; } fn read_toml_from_file serde::Deserialize<'de>>(path: &String) -> Option { let text = match fs::read_to_string(path) { Ok(t) => t, Err(e) => { println!("Error while reading file '{path}': {e}"); return None; } }; match toml::from_str(&text) { Ok(t) => Some(t), Err(e) => { println!("Unable to parse file '{path}':\n{e}"); return None; } } } #[tokio::main] async fn main() { // Parse Command line arguments let cli_args = CliArgs::parse(); // Read configuration file let config: config::EchoIpServiceConfig = match cli_args.config { Some(config_path) => { match read_toml_from_file::(&config_path) { Some(c) => c, None => { println!("Could not read confuration file, exiting."); ::std::process::exit(1); } } }, None => Default::default(), }; // Initalize Tera templates let mut template_base_dir = match cli_args.template_location { Some(template_base_dir) => template_base_dir, None => (&config.template.template_location).to_owned(), }; if !template_base_dir.ends_with("/") { template_base_dir = template_base_dir + "/"; } let template_extra_config = match &cli_args.extra_config { Some(path) => read_toml_from_file(path), None => match &config.template.extra_config { Some(path) => read_toml_from_file(path), None => { println!("Trying to read default template configuration ..."); println!("(If this fails that may be ok, depending on your template)"); read_toml_from_file(&(template_base_dir.clone()+"extra.toml")) }, }, }; let template_glob = template_base_dir.clone()+"*"; println!("Parsing Templates from '{}' ...", &template_glob); let res = Tera::new((template_glob).as_str()); let tera = match res { Ok(t) => t, Err(e) => { println!("Template parsing error(s): {}", e); ::std::process::exit(1); } }; let templating_engine = templating_engine::Engine{ tera: tera, template_config: template_extra_config, }; // Static file directory let static_file_directory = cli_args.static_location.unwrap_or( config.server.static_location.clone().unwrap_or( template_base_dir+"/static" ) ); println!("Static files will be served from: {static_file_directory}"); // Initalize GeoIP Database let asn_db = geoip::MMDBCarrier::new( "GeoIP ASN Database".to_string(), config.geoip.asn_database.clone() ); asn_db.reload_database().ok(); let location_db = geoip::MMDBCarrier::new( "GeoIP Location Database".to_string(), config.geoip.location_database.clone() ); location_db.reload_database().ok(); // Initalize DNS resolver with os defaults println!("Initalizing dns resolver ..."); let mut dns_resolver_selectables = Vec::::new(); println!("Initalizing System resolver ..."); let res = TokioAsyncResolver::tokio_from_system_conf(); //let res = TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()); let dns_resolver = match res { Ok(resolver) => resolver, Err(e) => { println!("Error while setting up dns resolver: {e}"); ::std::process::exit(1); } }; dns_resolver_selectables.push(Selectable { id: "default".to_string(), name: "System".to_string() }); //FIXME: Not release ready,must be configurable and have better error handling. println!("Initalizing Quad9 resolver ..."); let quad9_resolver = TokioAsyncResolver::tokio( trust_dns_resolver::config::ResolverConfig::quad9_tls(), Default::default() ).unwrap(); dns_resolver_selectables.push(Selectable { id: "quad9".to_string(), name: "Quad9".to_string() }); println!("Initalizing Google resolver ..."); let google_resolver = TokioAsyncResolver::tokio( trust_dns_resolver::config::ResolverConfig::google(), Default::default() ).unwrap(); dns_resolver_selectables.push(Selectable { id: "google".to_string(), name: "Google".to_string() }); println!("Initalizing Cloudflare resolver ..."); let cloudflare_resolver = TokioAsyncResolver::tokio( trust_dns_resolver::config::ResolverConfig::cloudflare_tls(), Default::default() ).unwrap(); dns_resolver_selectables.push(Selectable { id: "cloudflare".to_string(), name: "Cloudflare".to_string() }); let mut dns_resolver_map: HashMap = HashMap::new(); for (key, resolver_config) in &config.dns.resolver { println!("Initalizing {} resolver ...", key); let resolver = TokioAsyncResolver::tokio( resolver_config.to_trust_resolver_config(), Default::default() ).unwrap(); dns_resolver_map.insert(key.clone(), resolver); dns_resolver_selectables.push(Selectable { id: key.clone(), name: resolver_config.display_name.clone(), }); } dns_resolver_map.insert("default".to_string(), dns_resolver); dns_resolver_map.insert("quad9".to_string(), quad9_resolver); dns_resolver_map.insert("google".to_string(), google_resolver); dns_resolver_map.insert("cloudflare".to_string(), cloudflare_resolver); let listen_on = config.server.listen_on; let ip_header = config.server.ip_header.clone(); // Initialize shared state let shared_state = Arc::new( ServiceSharedState { templating_engine: templating_engine, //dns_resolver: dns_resolver, dns_resolvers: dns_resolver_map, asn_db: asn_db, location_db: location_db, config: config.clone(), }); let derived_config = DerivedConfiguration { dns_resolver_selectables: dns_resolver_selectables, }; let signal_usr1_handlers_state = shared_state.clone(); task::spawn(async move { println!("Trying to register USR1 signal for reloading geoip databases"); let mut signal_stream = match signal(SignalKind::user_defined1()) { Ok(signal_stream) => signal_stream, Err(e) => { println!("Error while registring signal handler: {e}"); println!("Continuing without ..."); return; } }; loop { if None == signal_stream.recv().await { return; } println!("Received signal USR1, reloading geoip databses!"); signal_usr1_handlers_state.location_db.reload_database().ok(); signal_usr1_handlers_state.asn_db.reload_database().ok(); } }); // Initalize axum server let app = Router::new() .route("/", get(handle_default_route)) .route("/dig/:name", get(handle_dig_route_with_path)) .route("/ip/:address", get(handle_ip_route_with_path)) .route("/dns_resolver/:resolver", get(handle_dns_resolver_route_with_path)) .route("/dns_resolver", get(handle_dns_resolver_route)) .route("/ua", get(user_agent_handler)) .route("/hi", get(hello_world_handler)) .fallback_service( ServeDir::new(static_file_directory) .fallback(not_found_handler.with_state(shared_state.clone())) ) .with_state(shared_state) .layer( ServiceBuilder::new() .layer(ip_header.into_extension()) .layer(ratelimit::build_rate_limiting_state( config.ratelimit.per_minute, config.ratelimit.burst)) .layer(middleware::from_fn(ratelimit::rate_limit_middleware)) .layer(Extension(config)) .layer(Extension(derived_config)) .layer(middleware::from_fn(settings_query_middleware)) ) ; println!("Starting Server on {} ...",listen_on); axum::Server::bind(&listen_on) .serve(app.into_make_service_with_connect_info::()) .await .unwrap(); } async fn settings_query_middleware( Query(query): Query, Extension(config): Extension, Extension(derived_config): Extension, cookie_header: Option>, user_agent_header: Option>, mut req: Request, next: Next ) -> Response { let mut format = query.format; let mut dns_resolver_id = config.dns.default_resolver; if let Some(resolver_id) = query.dns { dns_resolver_id = resolver_id; } else if let Some(cookie_header) = cookie_header { if let Some(resolver_id) = cookie_header.0.get("dns_resolver") { dns_resolver_id = resolver_id.to_string(); } } // Try to guess type from user agent if format.is_none() { if let Some(TypedHeader(user_agent)) = user_agent_header { let ua = user_agent.as_str(); for tua in config.template.text_user_agents { if ua.starts_with(&tua) { format = Some(ResponseFormat::TextPlain); break; } } } } // Add the request settings extension req.extensions_mut().insert(QuerySettings{ format: format.unwrap_or(ResponseFormat::TextHtml), lang: query.lang.unwrap_or("en".to_string()), available_dns_resolvers: derived_config.dns_resolver_selectables, dns_resolver_id: dns_resolver_id, }); next.run(req).await } async fn not_found_handler( State(arc_state): State>, Extension(settings): Extension, ) -> Response { let state = Arc::clone(&arc_state); state.templating_engine.render_view( &settings, &View::NotFound, ).await } async fn hello_world_handler( State(arc_state): State>, Extension(settings): Extension, ) -> Response { let state = Arc::clone(&arc_state); state.templating_engine.render_view( &settings, &View::Message{ title: "Hey There!".to_string(), message: "You,You are an awesome Creature!".to_string() }, ).await } async fn user_agent_handler( TypedHeader(user_agent): TypedHeader, ) -> String { user_agent.to_string() } async fn handle_default_route( Query(search_query): Query, State(arc_state): State>, Extension(settings): Extension, user_agent_header: Option>, SecureClientIp(address): SecureClientIp ) -> Response { let state = Arc::clone(&arc_state); if let Some(search_query) = search_query.query { if search_query.trim() != "" { return handle_search_request( search_query, false, settings, state ).await; } } let result = get_ip_result(&address, &settings.lang, &"default".to_string(), &state).await; let user_agent: Option = match user_agent_header { Some(TypedHeader(user_agent)) => Some(user_agent.to_string()), None => None, }; state.templating_engine.render_view( &settings, &View::Index{ result: result, user_agent: user_agent, } ).await } async fn handle_search_request( search_query: String, this_should_have_been_an_ip: bool, settings: QuerySettings, arc_state: Arc, ) -> Response { let mut search_query = search_query.trim().to_string(); let mut settings = settings; lazy_static!{ static ref ASN_REGEX: Regex = Regex::new(r"^[Aa][Ss][Nn]?\s*(\d{1,7})$").unwrap(); static ref VIA_REGEX: Regex = Regex::new(r"[Vv][Ii][Aa]\s+(\S+)").unwrap(); } //If someone asked for an asn, give an asn answer if let Some(asn_cap) = ASN_REGEX.captures(&search_query) { if let Some(asn) = asn_cap.get(1).map_or(None, |m| m.as_str().parse::().ok()) { // Render a dummy template that can at least link to other pages let state = Arc::clone(&arc_state); return state.templating_engine.render_view( &settings, &View::Asn{asn: asn}, ).await } } if let Some(via_cap) = VIA_REGEX.captures(&search_query) { if let Some(via) = via_cap.get(1).map(|c| c.as_str().to_string()) { let state = Arc::clone(&arc_state); if state.dns_resolvers.contains_key(&via) { settings.dns_resolver_id = via; } } search_query = VIA_REGEX.replace(&search_query,"").trim().to_string(); } // Try to interpret as an IP-Address if let Ok(address) = search_query.parse() { return handle_ip_request(address, settings, arc_state).await; } // Fall back to treating it as a hostname return handle_dig_request( search_query, settings, arc_state, !this_should_have_been_an_ip, ).await } async fn handle_dns_resolver_route( Extension(settings): Extension, State(arc_state): State>, ) -> Response { let state = Arc::clone(&arc_state); state.templating_engine.render_view( &settings, &View::DnsResolverList, ).await } async fn handle_dns_resolver_route_with_path( Extension(settings): Extension, State(arc_state): State>, extract::Path(query): extract::Path, ) -> Response { let state = Arc::clone(&arc_state); if let Some(resolver) = state.config.dns.resolver.get(&query) { state.templating_engine.render_view( &settings, &View::DnsResolver{ config: resolver.clone() }, ).await } else { state.templating_engine.render_view( &settings, &View::NotFound, ).await } } async fn handle_ip_route_with_path( Extension(settings): Extension, State(arc_state): State>, extract::Path(query): extract::Path, ) -> Response { if let Ok(address) = query.parse() { return handle_ip_request(address, settings, arc_state).await } else { return handle_search_request(query, true, settings, arc_state).await; } } async fn handle_ip_request( address: IpAddr, settings: QuerySettings, arc_state: Arc, ) -> Response { let state = Arc::clone(&arc_state); let result = get_ip_result( &address, &settings.lang, &settings.dns_resolver_id, &state).await; state.templating_engine.render_view( &settings, &View::Ip{result: result} ).await } async fn get_ip_result( address: &IpAddr, lang: &String, dns_resolver_name: &String, state: &ServiceSharedState, ) -> IpResult { let ip_info = AddressInfo::new(&address); if !(ip_info.scope == AddressScope::Global || ip_info.scope == AddressScope::Shared) || ip_info.cast != AddressCast::Unicast { if !((ip_info.scope == AddressScope::Private || ip_info.scope == AddressScope::LinkLocal) && state.config.server.allow_private_ip_lookup) { return IpResult { address: *address, hostname: None, asn: None, location: None, ip_info: ip_info, used_dns_resolver: None, } } } // do reverse lookup let mut hostname: Option = None; let mut used_dns_resolver: Option = None; if state.config.dns.allow_reverse_lookup { if let Some(dns_resolver) = &state.dns_resolvers.get(dns_resolver_name) { hostname = simple_dns::reverse_lookup(&dns_resolver, &address).await; used_dns_resolver = Some(dns_resolver_name.clone()); } } // asn lookup let asn_result = state.asn_db.query_asn_for_ip(address); // location lookup let location_result = state.location_db.query_location_for_ip( address, &vec![lang, &"en".to_string()] ); // filter reverse lookup if let Some(name) = &hostname { if match_domain_hidden_list(&name, &state.config.dns.hidden_suffixes) { hostname = None; used_dns_resolver = None; } } IpResult{ address: *address, hostname: hostname, asn: asn_result, location: location_result, ip_info: ip_info, used_dns_resolver: used_dns_resolver, } } async fn handle_dig_route_with_path( Extension(settings): Extension, State(arc_state): State>, extract::Path(name): extract::Path, ) -> Response { return handle_dig_request(name, settings, arc_state, true).await } async fn handle_dig_request( dig_query: String, settings: QuerySettings, arc_state: Arc, do_full_lookup: bool, ) -> Response { let state = Arc::clone(&arc_state); let dig_result = get_dig_result( &dig_query, &settings.dns_resolver_id, &state, do_full_lookup ).await; state.templating_engine.render_view( &settings, &View::Dig{ query: dig_query, result: dig_result} ).await } async fn get_dig_result( dig_query: &String, dns_resolver_name: &String, state: &ServiceSharedState, do_full_lookup: bool, ) -> DigResult { let name = &dig_query.trim().trim_end_matches(".").to_string(); let idna_name = IdnaName::from_string(&name); if let Some(dns_resolver) = &state.dns_resolvers.get(dns_resolver_name) { if match_domain_hidden_list(&name, &state.config.dns.hidden_suffixes) { // Try to hide the fact that we didn't do dns resolution at all // We resolve example.org as basic avoidance of timing sidechannels. // WARNING: this timing sidechannel avoidance is very crude. simple_dns::lookup( &dns_resolver, &("example.org.".to_string()), do_full_lookup).await; DigResult { records: DnsLookupResult{ nxdomain: true , ..Default::default() }, idn: idna_name, partial_lookup: !do_full_lookup, used_dns_resolver: dns_resolver_name.clone(), } } else { DigResult { records: simple_dns::lookup( &dns_resolver, &(idna_name.idn.clone().unwrap_or(name.to_owned())+"."), do_full_lookup).await, idn: idna_name, partial_lookup: !do_full_lookup, used_dns_resolver: dns_resolver_name.clone(), } } } else { return Default::default(); } }