mirror of
https://codeberg.org/slatian/service.echoip-slatecave.git
synced 2025-07-17 14:33:27 +02:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
64e639b0df | |||
2f9f01e947 | |||
f2e9e36e99 | |||
912a119361 | |||
5adca4fb80 | |||
5ac056ef99 | |||
51aa05fe13 | |||
de179ea7fa | |||
bfa383ddbe | |||
a33473fdc9 | |||
20fb7ee2ff | |||
c5a7597561 | |||
c56cc6edbd | |||
5c74de5685 | |||
223abdd804 |
669
Cargo.lock
generated
669
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,14 @@
|
||||
[package]
|
||||
name = "echoip-slatecave"
|
||||
version = "0.1.0"
|
||||
version = "1.2.0"
|
||||
edition = "2021"
|
||||
authors = ["Slatian <baschdel@disroot.org>"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
lib-humus = { version="0.1", features=["axum-view+cookie"], git="https://codeberg.org/slatian/lib-humus.git" }
|
||||
|
||||
axum = { version = "0.6", features = ["macros", "headers"] }
|
||||
axum-extra = { version = "0.7", features = ["cookie"] }
|
||||
axum-client-ip = "0.4"
|
||||
@ -16,7 +18,7 @@ idna = "0.3"
|
||||
lazy_static = "1.4.0"
|
||||
parking_lot = "0.12"
|
||||
regex = "1.7"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde = { version = "1", features = ["derive","rc"] }
|
||||
tokio = { version = "1", features = ["macros","signal"] }
|
||||
tera = "1"
|
||||
toml = "0.7"
|
||||
@ -25,3 +27,4 @@ tower-http = { version = "0.4", features = ["fs"] }
|
||||
trust-dns-proto = "0.22"
|
||||
trust-dns-resolver = { version = "0.22", features = ["dns-over-rustls","dns-over-https","dns-over-quic"] }
|
||||
maxminddb = "0.23"
|
||||
mime = "0.3"
|
||||
|
25
README.md
25
README.md
@ -34,12 +34,33 @@ A less sane, but better for testing version can be found in [echoip_test.toml](e
|
||||
|
||||
Templates in the templates folder exist for every rich page that `echoip-slatecave` supports.
|
||||
|
||||
The code that rendeers them can be found in [src/templating_engine.rs](src/templating_engine.rs).
|
||||
|
||||
There is a configuration file for templates which by default is the `extra.toml` file in the template directory. Its content is exposed to the templates in the `extra` struct.
|
||||
|
||||
The default templates should make use of everything exposed to the templating part, the `data.result` or `data` object is usually what you get when you ask for the json version.
|
||||
|
||||
In addition to that the following fields are accessible from inside the template:
|
||||
|
||||
`view`
|
||||
: The views name (the basename of the template file, i.e. `404` or `ip`)
|
||||
|
||||
`format`
|
||||
: The format name (`html`, `text`, `json`)
|
||||
|
||||
`mimetype`
|
||||
: The resulting mimetype (i.e. `text/plain; charset=utf-8`)
|
||||
|
||||
`http_status`
|
||||
: The numeric HTTP Status-Code at the time of rendering the template.
|
||||
|
||||
`language`
|
||||
: The language requested by the browser.
|
||||
|
||||
`dns_resolvers`
|
||||
: A list of [Selectable](src/settings.rs) structs representing the available DNS-Resolvers.
|
||||
|
||||
`dns_resolver_id`
|
||||
: The id of the currently selected DNS-Resolver
|
||||
|
||||
The templates are covered by the AGPL as well, please share them with your users if you modified them.
|
||||
|
||||
### GeoLite2 database
|
||||
|
@ -1,6 +1,7 @@
|
||||
use serde::{Deserialize,Serialize};
|
||||
use trust_dns_resolver::config::Protocol;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
@ -10,12 +11,12 @@ pub struct DnsConfig {
|
||||
pub allow_forward_lookup: bool,
|
||||
pub allow_reverse_lookup: bool,
|
||||
pub hidden_suffixes: Vec<String>,
|
||||
pub resolver: HashMap<String,DnsResolverConfig>,
|
||||
pub resolver: HashMap<Arc<str>,DnsResolverConfig>,
|
||||
|
||||
pub enable_system_resolver: bool,
|
||||
pub system_resolver_name: String,
|
||||
pub system_resolver_name: Arc<str>,
|
||||
pub system_resolver_weight: i32,
|
||||
pub system_resolver_id: String,
|
||||
pub system_resolver_id: Arc<str>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
@ -30,16 +31,16 @@ pub enum DnsProtocol {
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct DnsResolverConfig {
|
||||
pub display_name: String,
|
||||
pub display_name: Arc<str>,
|
||||
#[serde(default)]
|
||||
pub info_url: Option<String>,
|
||||
pub info_url: Option<Arc<str>>,
|
||||
#[serde(default)]
|
||||
pub aliases: Vec<String>,
|
||||
pub aliases: Vec<Arc<str>>,
|
||||
#[serde(default="zero")]
|
||||
pub weight: i32,
|
||||
pub servers: Vec<SocketAddr>,
|
||||
pub protocol: DnsProtocol,
|
||||
pub tls_dns_name: Option<String>,
|
||||
pub tls_dns_name: Option<Arc<str>>,
|
||||
#[serde(skip_serializing)] //Don't leak our bind address to the outside
|
||||
pub bind_address: Option<SocketAddr>,
|
||||
#[serde(default="default_true")]
|
||||
@ -63,9 +64,9 @@ impl Default for DnsConfig {
|
||||
resolver: Default::default(),
|
||||
|
||||
enable_system_resolver: true,
|
||||
system_resolver_name: "System".to_string(),
|
||||
system_resolver_name: "System".into(),
|
||||
system_resolver_weight: 1000,
|
||||
system_resolver_id: "system".to_string(),
|
||||
system_resolver_id: "system".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,7 +92,7 @@ impl DnsResolverConfig {
|
||||
resolver.add_name_server(trust_dns_resolver::config::NameServerConfig{
|
||||
socket_addr: *server,
|
||||
protocol: self.protocol.clone().into(),
|
||||
tls_dns_name: self.tls_dns_name.clone(),
|
||||
tls_dns_name: self.tls_dns_name.clone().map(|s| s.to_string()),
|
||||
trust_nx_responses: self.trust_nx_responses,
|
||||
tls_config: None,
|
||||
bind_addr: self.bind_address,
|
||||
|
161
src/main.rs
161
src/main.rs
@ -19,7 +19,6 @@ 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::Name;
|
||||
@ -32,18 +31,21 @@ use tokio::signal::unix::{
|
||||
use tokio::task;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use lib_humus::TemplateEngineLoader;
|
||||
use lib_humus::read_toml_from_file;
|
||||
use lib_humus::HumusEngine;
|
||||
|
||||
mod config;
|
||||
mod geoip;
|
||||
mod idna;
|
||||
mod ipinfo;
|
||||
mod ratelimit;
|
||||
mod settings;
|
||||
mod simple_dns;
|
||||
mod templating_engine;
|
||||
mod idna;
|
||||
mod view;
|
||||
|
||||
use crate::geoip::{
|
||||
QueryAsn,
|
||||
@ -54,9 +56,11 @@ use crate::geoip::{
|
||||
use crate::idna::IdnaName;
|
||||
use crate::simple_dns::DnsLookupResult;
|
||||
use crate::settings::*;
|
||||
use crate::templating_engine::View;
|
||||
use crate::view::View;
|
||||
use crate::ipinfo::{AddressCast,AddressInfo,AddressScope};
|
||||
|
||||
type TemplatingEngine = HumusEngine<View,QuerySettings,ResponseFormat>;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct SettingsQuery {
|
||||
format: Option<ResponseFormat>,
|
||||
@ -76,26 +80,26 @@ pub struct IpResult {
|
||||
asn: Option<AsnResult>,
|
||||
location: Option<LocationResult>,
|
||||
ip_info: AddressInfo,
|
||||
used_dns_resolver: Option<String>,
|
||||
used_dns_resolver: Option<Arc<str>>,
|
||||
}
|
||||
|
||||
// We need this one to hide the partial lookup field when irelevant
|
||||
pub fn not(b: &bool) -> bool { !b }
|
||||
|
||||
#[derive(Serialize, Default, Clone)]
|
||||
#[derive(Serialize, 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,
|
||||
used_dns_resolver: Arc<str>,
|
||||
}
|
||||
|
||||
struct ServiceSharedState {
|
||||
templating_engine: templating_engine::Engine,
|
||||
dns_resolvers: HashMap<String,TokioAsyncResolver>,
|
||||
dns_resolver_aliases: HashMap<String,String>,
|
||||
templating_engine: TemplatingEngine,
|
||||
dns_resolvers: HashMap<Arc<str>,TokioAsyncResolver>,
|
||||
dns_resolver_aliases: HashMap<Arc<str>,Arc<str>>,
|
||||
asn_db: geoip::MMDBCarrier,
|
||||
location_db: geoip::MMDBCarrier,
|
||||
config: config::EchoIpServiceConfig,
|
||||
@ -105,7 +109,7 @@ struct ServiceSharedState {
|
||||
#[derive(Clone)]
|
||||
struct DerivedConfiguration {
|
||||
dns_resolver_selectables: Vec<Selectable>,
|
||||
default_resolver: String,
|
||||
default_resolver: Arc<str>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
@ -133,23 +137,6 @@ fn match_domain_hidden_list(domain: &String, hidden_list: &Vec<String>) -> bool
|
||||
return false;
|
||||
}
|
||||
|
||||
fn read_toml_from_file<T: for<'de> serde::Deserialize<'de>>(path: &String) -> Option<T> {
|
||||
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
|
||||
@ -159,9 +146,11 @@ async fn main() {
|
||||
let config: config::EchoIpServiceConfig = match cli_args.config {
|
||||
Some(config_path) => {
|
||||
match read_toml_from_file::<config::EchoIpServiceConfig>(&config_path) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
println!("Could not read confuration file, exiting.");
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
println!("Could not read confuration file!");
|
||||
println!("{e}");
|
||||
println!("Exiting ...");
|
||||
::std::process::exit(1);
|
||||
}
|
||||
}
|
||||
@ -170,47 +159,25 @@ async fn main() {
|
||||
};
|
||||
|
||||
// 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,
|
||||
let template_loader = TemplateEngineLoader::new(
|
||||
config.template.template_location.clone(),
|
||||
config.template.extra_config.clone()
|
||||
)
|
||||
.cli_template_location(cli_args.template_location)
|
||||
.cli_extra_config_location(cli_args.extra_config);
|
||||
|
||||
|
||||
let templating_engine = match template_loader.load_templates() {
|
||||
Ok(t) => t.into(),
|
||||
Err(e) => {
|
||||
println!("Template parsing error(s): {}", e);
|
||||
println!("{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"
|
||||
)
|
||||
);
|
||||
let static_file_directory = template_loader.base_dir()+"/static";
|
||||
|
||||
println!("Static files will be served from: {static_file_directory}");
|
||||
|
||||
@ -234,8 +201,8 @@ async fn main() {
|
||||
println!("Initalizing dns resolvers ...");
|
||||
|
||||
let mut dns_resolver_selectables = Vec::<Selectable>::new();
|
||||
let mut dns_resolver_map: HashMap<String,TokioAsyncResolver> = HashMap::new();
|
||||
let mut dns_resolver_aliases: HashMap<String,String> = HashMap::new();
|
||||
let mut dns_resolver_map: HashMap<Arc<str>,TokioAsyncResolver> = HashMap::new();
|
||||
let mut dns_resolver_aliases: HashMap<Arc<str>,Arc<str>> = HashMap::new();
|
||||
|
||||
if config.dns.enable_system_resolver {
|
||||
println!("Initalizing System resolver ...");
|
||||
@ -290,7 +257,7 @@ async fn main() {
|
||||
dns_resolver_selectables.sort_by(|a,b| b.weight.cmp(&a.weight));
|
||||
let default_resolver = dns_resolver_selectables.get(0)
|
||||
.map(|s| s.id.clone() )
|
||||
.unwrap_or("none".to_string());
|
||||
.unwrap_or("none".into());
|
||||
let derived_config = DerivedConfiguration {
|
||||
dns_resolver_selectables: dns_resolver_selectables,
|
||||
default_resolver: default_resolver,
|
||||
@ -364,10 +331,10 @@ async fn settings_query_middleware<B>(
|
||||
let mut dns_resolver_id = derived_config.default_resolver;
|
||||
|
||||
if let Some(resolver_id) = query.dns {
|
||||
dns_resolver_id = resolver_id;
|
||||
dns_resolver_id = resolver_id.into();
|
||||
} 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();
|
||||
dns_resolver_id = resolver_id.into();
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,7 +344,7 @@ async fn settings_query_middleware<B>(
|
||||
let ua = user_agent.as_str();
|
||||
for tua in config.template.text_user_agents {
|
||||
if ua.starts_with(&tua) {
|
||||
format = Some(ResponseFormat::TextPlain);
|
||||
format = Some(ResponseFormat::Text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -385,7 +352,7 @@ async fn settings_query_middleware<B>(
|
||||
}
|
||||
// Add the request settings extension
|
||||
req.extensions_mut().insert(QuerySettings{
|
||||
format: format.unwrap_or(ResponseFormat::TextHtml),
|
||||
format: format.unwrap_or(ResponseFormat::Html),
|
||||
lang: query.lang.unwrap_or("en".to_string()),
|
||||
available_dns_resolvers: derived_config.dns_resolver_selectables,
|
||||
dns_resolver_id: dns_resolver_id,
|
||||
@ -401,7 +368,7 @@ async fn not_found_handler(
|
||||
|
||||
state.templating_engine.render_view(
|
||||
&settings,
|
||||
&View::NotFound,
|
||||
View::NotFound,
|
||||
).await
|
||||
}
|
||||
|
||||
@ -413,7 +380,7 @@ async fn hello_world_handler(
|
||||
|
||||
state.templating_engine.render_view(
|
||||
&settings,
|
||||
&View::Message{
|
||||
View::Message{
|
||||
title: "Hey There!".to_string(),
|
||||
message: "You,You are an awesome Creature!".to_string()
|
||||
},
|
||||
@ -448,7 +415,7 @@ async fn handle_default_route(
|
||||
}
|
||||
}
|
||||
|
||||
let result = get_ip_result(&address, &settings.lang, &"default".to_string(), &state).await;
|
||||
let result = get_ip_result(&address, &settings.lang, &"default".into(), &state).await;
|
||||
|
||||
let user_agent: Option<String> = match user_agent_header {
|
||||
Some(TypedHeader(user_agent)) => Some(user_agent.to_string()),
|
||||
@ -457,7 +424,7 @@ async fn handle_default_route(
|
||||
|
||||
state.templating_engine.render_view(
|
||||
&settings,
|
||||
&View::Index{
|
||||
View::Index{
|
||||
result: result,
|
||||
user_agent: user_agent,
|
||||
}
|
||||
@ -487,17 +454,17 @@ async fn handle_search_request(
|
||||
let state = Arc::clone(&arc_state);
|
||||
return state.templating_engine.render_view(
|
||||
&settings,
|
||||
&View::Asn{asn: asn},
|
||||
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()) {
|
||||
if let Some(via) = via_cap.get(1) {
|
||||
let state = Arc::clone(&arc_state);
|
||||
if state.dns_resolvers.contains_key(&via) {
|
||||
settings.dns_resolver_id = via;
|
||||
} else if let Some(alias) = state.dns_resolver_aliases.get(&via) {
|
||||
if state.dns_resolvers.contains_key(via.as_str()) {
|
||||
settings.dns_resolver_id = via.as_str().into();
|
||||
} else if let Some(alias) = state.dns_resolver_aliases.get(via.as_str()) {
|
||||
settings.dns_resolver_id = alias.clone();
|
||||
}
|
||||
}
|
||||
@ -526,7 +493,7 @@ async fn handle_dns_resolver_route(
|
||||
let state = Arc::clone(&arc_state);
|
||||
state.templating_engine.render_view(
|
||||
&settings,
|
||||
&View::DnsResolverList,
|
||||
View::DnsResolverList,
|
||||
).await
|
||||
}
|
||||
|
||||
@ -537,15 +504,15 @@ async fn handle_dns_resolver_route_with_path(
|
||||
extract::Path(query): extract::Path<String>,
|
||||
) -> Response {
|
||||
let state = Arc::clone(&arc_state);
|
||||
if let Some(resolver) = state.config.dns.resolver.get(&query) {
|
||||
if let Some(resolver) = state.config.dns.resolver.get(query.as_str()) {
|
||||
state.templating_engine.render_view(
|
||||
&settings,
|
||||
&View::DnsResolver{ config: resolver.clone() },
|
||||
View::DnsResolver{ config: resolver.clone() },
|
||||
).await
|
||||
} else {
|
||||
state.templating_engine.render_view(
|
||||
&settings,
|
||||
&View::NotFound,
|
||||
View::NotFound,
|
||||
).await
|
||||
}
|
||||
}
|
||||
@ -577,14 +544,14 @@ async fn handle_ip_request(
|
||||
|
||||
state.templating_engine.render_view(
|
||||
&settings,
|
||||
&View::Ip{result: result}
|
||||
View::Ip{result: result}
|
||||
).await
|
||||
}
|
||||
|
||||
async fn get_ip_result(
|
||||
address: &IpAddr,
|
||||
lang: &String,
|
||||
dns_resolver_name: &String,
|
||||
dns_resolver_name: &Arc<str>,
|
||||
state: &ServiceSharedState,
|
||||
) -> IpResult {
|
||||
|
||||
@ -605,7 +572,7 @@ async fn get_ip_result(
|
||||
|
||||
// do reverse lookup
|
||||
let mut hostname: Option<String> = None;
|
||||
let mut used_dns_resolver: Option<String> = None;
|
||||
let mut used_dns_resolver: Option<Arc<str>> = 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;
|
||||
@ -666,20 +633,20 @@ async fn handle_dig_request(
|
||||
|
||||
state.templating_engine.render_view(
|
||||
&settings,
|
||||
&View::Dig{ query: dig_query, result: dig_result}
|
||||
View::Dig{ query: dig_query, result: dig_result}
|
||||
).await
|
||||
|
||||
}
|
||||
|
||||
async fn get_dig_result(
|
||||
dig_query: &String,
|
||||
dns_resolver_name: &String,
|
||||
dns_resolver_name: &Arc<str>,
|
||||
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 let Some(dns_resolver) = state.dns_resolvers.get(dns_resolver_name) {
|
||||
if let Ok(domain_name) = Name::from_str_relaxed(name.to_owned()+".") {
|
||||
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
|
||||
@ -707,21 +674,27 @@ async fn get_dig_result(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Invalid domain name
|
||||
return DigResult {
|
||||
records: DnsLookupResult{
|
||||
invalid_name: true,
|
||||
.. Default::default()
|
||||
},
|
||||
.. Default::default()
|
||||
idn: idna_name,
|
||||
partial_lookup: !do_full_lookup,
|
||||
used_dns_resolver: dns_resolver_name.clone(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unknown resolver name
|
||||
return DigResult {
|
||||
records: DnsLookupResult{
|
||||
unkown_resolver: true,
|
||||
.. Default::default()
|
||||
},
|
||||
.. Default::default()
|
||||
idn: idna_name,
|
||||
partial_lookup: !do_full_lookup,
|
||||
used_dns_resolver: "unkown_resolver".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +1,13 @@
|
||||
use serde::{Deserialize,Serialize};
|
||||
|
||||
use lib_humus::HtmlTextJsonFormat;
|
||||
use lib_humus::HumusQuerySettings;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/* Response format */
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Copy)]
|
||||
pub enum ResponseFormat {
|
||||
#[serde(rename="text/plain", alias="text")]
|
||||
TextPlain,
|
||||
#[serde(rename="text/html", alias="html")]
|
||||
TextHtml,
|
||||
#[serde(rename="application/json", alias="json")]
|
||||
ApplicationJson,
|
||||
}
|
||||
|
||||
impl ToString for ResponseFormat {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
ResponseFormat::TextPlain => "text/plain",
|
||||
ResponseFormat::TextHtml => "text/html",
|
||||
ResponseFormat::ApplicationJson => "application/json",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseFormat {
|
||||
pub fn to_file_extension(&self) -> String {
|
||||
match self {
|
||||
ResponseFormat::TextPlain => ".txt",
|
||||
ResponseFormat::TextHtml => ".html",
|
||||
ResponseFormat::ApplicationJson => ".json",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
pub type ResponseFormat = HtmlTextJsonFormat;
|
||||
|
||||
/* Query and Template Settings */
|
||||
|
||||
@ -39,13 +16,26 @@ pub struct QuerySettings {
|
||||
pub format: ResponseFormat,
|
||||
pub lang: String,
|
||||
pub available_dns_resolvers: Vec<Selectable>,
|
||||
pub dns_resolver_id: String,
|
||||
pub dns_resolver_id: Arc<str>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct Selectable {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub id: Arc<str>,
|
||||
pub name: Arc<str>,
|
||||
pub weight: i32,
|
||||
}
|
||||
|
||||
impl HumusQuerySettings<ResponseFormat> for QuerySettings {
|
||||
|
||||
fn initalize_template_context(&self, context: &mut tera::Context) {
|
||||
context.insert("language", &self.lang);
|
||||
context.insert("dns_resolvers", &self.available_dns_resolvers);
|
||||
context.insert("dns_resolver_id", &self.dns_resolver_id);
|
||||
}
|
||||
|
||||
fn get_format(&self) -> ResponseFormat {
|
||||
self.format.clone()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,135 +0,0 @@
|
||||
/*
|
||||
* This is the echoip-slatecave templating engine.
|
||||
* It wraps around tera in is specialized for echoip-slatecave.
|
||||
*/
|
||||
|
||||
use axum::{
|
||||
headers::HeaderValue,
|
||||
http::StatusCode,
|
||||
http::header::SET_COOKIE,
|
||||
response::Html,
|
||||
response::IntoResponse,
|
||||
response::Response,
|
||||
response::Json,
|
||||
};
|
||||
use axum_extra::extract::cookie::Cookie;
|
||||
use axum_extra::extract::cookie;
|
||||
use tera::Tera;
|
||||
use toml::Table;
|
||||
|
||||
use crate::DigResult;
|
||||
use crate::IpResult;
|
||||
use crate::config::DnsResolverConfig;
|
||||
use crate::settings::*;
|
||||
|
||||
|
||||
/* The echoip view */
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum View {
|
||||
Asn { asn: u32 },
|
||||
Dig { query: String, result: DigResult },
|
||||
DnsResolver{ config: DnsResolverConfig },
|
||||
DnsResolverList,
|
||||
Index { result: IpResult, user_agent: Option<String> },
|
||||
Ip { result: IpResult },
|
||||
Message{ title: String, message: String },
|
||||
#[serde(rename="404")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn template_name(&self) -> String {
|
||||
match self {
|
||||
View::Asn{..} => "asn",
|
||||
View::Dig{..} => "dig",
|
||||
View::DnsResolver{..} => "dns_resolver",
|
||||
View::DnsResolverList => "dns_resolver_list",
|
||||
View::Index{..} => "index",
|
||||
View::Ip{..} => "ip",
|
||||
View::Message{..} => "message",
|
||||
View::NotFound => "404",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/* The engine itself */
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Engine {
|
||||
pub tera: Tera,
|
||||
pub template_config: Option<Table>,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
pub async fn render_view(
|
||||
&self,
|
||||
settings: &QuerySettings,
|
||||
view: &View,
|
||||
) -> Response {
|
||||
let mut response = match settings.format {
|
||||
ResponseFormat::TextHtml | ResponseFormat::TextPlain => {
|
||||
let template_name = view.template_name();
|
||||
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("view", &template_name);
|
||||
//intented for shared macros
|
||||
context.insert("format", &settings.format.to_string());
|
||||
context.insert("language", &settings.lang);
|
||||
context.insert("dns_resolvers", &settings.available_dns_resolvers);
|
||||
context.insert("dns_resolver_id", &settings.dns_resolver_id);
|
||||
context.insert("data", &view);
|
||||
context.insert("extra", &self.template_config);
|
||||
|
||||
match self.tera.render(&(template_name+&settings.format.to_file_extension()), &context) {
|
||||
Ok(text) =>
|
||||
match settings.format {
|
||||
ResponseFormat::TextHtml => Html(text).into_response(),
|
||||
_ => text.into_response(),
|
||||
}
|
||||
Err(e) => {
|
||||
println!("There was an error while rendering template {}: {e:?}", view.template_name());
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Template error in {}, contact owner or see logs.\n", view.template_name())
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO: Plain Text should have its own matcher
|
||||
ResponseFormat::ApplicationJson => {
|
||||
match view {
|
||||
View::Dig{result, ..} => {
|
||||
Json(result).into_response()
|
||||
},
|
||||
View::Index{result, ..} | View::Ip{result, ..} => {
|
||||
Json(result).into_response()
|
||||
},
|
||||
View::DnsResolverList => {
|
||||
Json(settings.available_dns_resolvers.clone()).into_response()
|
||||
},
|
||||
View::DnsResolver{ config } => {
|
||||
Json(config).into_response()
|
||||
}
|
||||
_ => Json(view).into_response(),
|
||||
}
|
||||
}
|
||||
};
|
||||
match view {
|
||||
View::NotFound => *response.status_mut() = StatusCode::NOT_FOUND,
|
||||
_ => {},
|
||||
}
|
||||
let cookie = Cookie::build("dns_resolver",settings.dns_resolver_id.clone())
|
||||
.path("/")
|
||||
.same_site(cookie::SameSite::Strict)
|
||||
.finish();
|
||||
if let Ok(header_value) = HeaderValue::from_str(&cookie.to_string()) {
|
||||
response.headers_mut().append(
|
||||
SET_COOKIE,
|
||||
header_value,
|
||||
);
|
||||
}
|
||||
response
|
||||
}
|
||||
}
|
80
src/view.rs
Normal file
80
src/view.rs
Normal file
@ -0,0 +1,80 @@
|
||||
|
||||
use axum::http::status::StatusCode;
|
||||
use axum::Json;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::response::Response;
|
||||
use axum_extra::extract::cookie::Cookie;
|
||||
use axum_extra::extract::cookie;
|
||||
use lib_humus::HumusView;
|
||||
|
||||
use crate::DigResult;
|
||||
use crate::IpResult;
|
||||
use crate::config::DnsResolverConfig;
|
||||
use crate::settings::QuerySettings;
|
||||
use crate::settings::ResponseFormat;
|
||||
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum View {
|
||||
Asn { asn: u32 },
|
||||
Dig { query: String, result: DigResult },
|
||||
DnsResolver{ config: DnsResolverConfig },
|
||||
DnsResolverList,
|
||||
Index { result: IpResult, user_agent: Option<String> },
|
||||
Ip { result: IpResult },
|
||||
Message{ title: String, message: String },
|
||||
#[serde(rename="404")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl HumusView<QuerySettings, ResponseFormat> for View {
|
||||
fn get_template_name(&self) -> String {
|
||||
match self {
|
||||
View::Asn{..} => "asn",
|
||||
View::Dig{..} => "dig",
|
||||
View::DnsResolver{..} => "dns_resolver",
|
||||
View::DnsResolverList => "dns_resolver_list",
|
||||
View::Index{..} => "index",
|
||||
View::Ip{..} => "ip",
|
||||
View::Message{..} => "message",
|
||||
View::NotFound => "404",
|
||||
}.to_string()
|
||||
}
|
||||
|
||||
fn get_status_code(&self, _: &QuerySettings) -> StatusCode {
|
||||
match self {
|
||||
Self::NotFound => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::OK,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cookie_header(&self, settings: &QuerySettings) -> Option<String> {
|
||||
Some(
|
||||
Cookie::build("dns_resolver",settings.dns_resolver_id.to_string())
|
||||
.path("/")
|
||||
.same_site(cookie::SameSite::Strict)
|
||||
.finish()
|
||||
.to_string()
|
||||
)
|
||||
}
|
||||
|
||||
fn get_api_response(self, settings: &QuerySettings) -> Response {
|
||||
match self {
|
||||
Self::Dig{result, ..} => {
|
||||
Json(result).into_response()
|
||||
},
|
||||
Self::Index{result, ..} | Self::Ip{result, ..} => {
|
||||
Json(result).into_response()
|
||||
},
|
||||
Self::DnsResolverList => {
|
||||
Json(settings.available_dns_resolvers.clone()).into_response()
|
||||
},
|
||||
Self::DnsResolver{ config } => {
|
||||
Json(config).into_response()
|
||||
}
|
||||
_ => Json(self).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% macro place_dl(place, label="", iso_code_prefix="") -%}
|
||||
{%- if place -%}
|
||||
{%- if format=="text/html" %}
|
||||
{%- if format=="html" %}
|
||||
{% if label %}<dt>{{label}}</dt>{% endif %}
|
||||
<dd>{{place.name}} {% if place.iso_code%}({% if iso_code_prefix %}{{iso_code_prefix}}-{% endif %}{{place.iso_code}}){% endif %}</dd>
|
||||
{% else -%}
|
||||
|
Reference in New Issue
Block a user