26 Commits

Author SHA1 Message Date
64e639b0df Release 1.2.0 2023-10-30 01:56:18 +01:00
2f9f01e947 Better template documentation 2023-10-30 01:54:44 +01:00
f2e9e36e99 codename mycelium became lib-humus 2023-10-30 01:54:35 +01:00
912a119361 Made template loading logic reuseable 2023-10-29 20:52:32 +01:00
5adca4fb80 WIP moved templating engine to mycelium 2023-10-29 18:56:44 +01:00
5ac056ef99 Made sure the get_status and update_response callbacks are used correctly 2023-10-29 18:20:57 +01:00
51aa05fe13 Added query settings to the generaliued templating mechanism 2023-10-29 18:10:57 +01:00
de179ea7fa Proper MimeType handling with mycelium 2023-10-29 16:51:43 +01:00
bfa383ddbe Fixed template data piping 2023-10-29 15:36:16 +01:00
a33473fdc9 Moved to a more genral implementation for the response format. 2023-10-29 15:23:47 +01:00
20fb7ee2ff First step to detaching the templating from the logic. 2023-10-29 13:50:22 +01:00
c5a7597561 cargo update 2023-10-29 10:55:40 +01:00
c56cc6edbd Version bump to 1.1.3 2023-10-08 09:27:38 +02:00
5c74de5685 Switch a lot of Strings to Arc<str> 2023-10-08 09:12:06 +02:00
223abdd804 cargo update
This one only removed dependencies 🥳
2023-10-08 08:09:44 +02:00
639d4579e9 Fixed the url pointing to the google dns information^W marketing 2023-08-08 23:15:43 +02:00
4b3a8d5e08 Fixed dns resolver information templates 2023-08-08 23:06:55 +02:00
53da9023da cargo update 2023-08-07 21:59:25 +02:00
4876fb7ea0 Updated README 2023-08-07 21:58:42 +02:00
2aa6baaa57 Removed dns search functionality completely
I don't see it working and not being annoying
2023-08-07 21:52:31 +02:00
daa68bbd5d Reenabled search prevention fqdn dot 2023-08-07 21:42:56 +02:00
231e46a688 Disabled domain search as it caused problems 2023-08-07 21:40:22 +02:00
2fe1b69174 Beeter communication of dns errors 2023-08-07 21:09:14 +02:00
2e1f6a77ac Slightly better handling of invalid domain names 2023-08-07 20:05:25 +02:00
1fe59d24d5 Discard additional responses instead of misattributing them 2023-08-07 19:03:18 +02:00
51d7954d71 Make the sitename render as inline-block to prevent accidental clicking 2023-08-06 13:23:41 +02:00
17 changed files with 653 additions and 712 deletions

677
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -72,7 +72,7 @@ This configuration option will not be exposed over the webinterface.
By default echoip-slatecave uses the system configuration for dns like most other programs.
In case this is undesired one can difable it by setting `enable_system_resolver` to false.
In case this is undesired one can disable it by setting `enable_system_resolver` to false.
```toml
[dns]
@ -90,10 +90,6 @@ In case you want to use the system resolver and customize it.
`system_resolver_weight`
: Equivalent to the `weight` of a custom resolver, default: 1000
### `search`
This is for a work in progress feature that allows confiuring search domains for all custom dns resolvers.
### Custom resolvers
It is possible to confgure custom resolvers in plce of or in addition to the default system resolver.

View File

@ -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
@ -67,9 +88,9 @@ The default configuration is pretty liberal so that the average human probably w
## TODO
* [ ] Investigate why search isn't working for global TLDs
* [ ] Add a way to configure just the dns server addresses and derive the port from the protocol.
* [ ] Add an about page for the system resolver
* [ ] Expose DNS responses from the additional on the web interface
## License

View File

@ -28,9 +28,6 @@ allow_reverse_lookup = true
# that end with one of these suffixes don't exist
hidden_suffixes = [".com"]
# doesn't really work 🙁
search = ["org","net"]
[geoip]
# Path to geoip databses
# Currently only the mmdb format is supported
@ -116,7 +113,7 @@ tls_dns_name = "cloudflare-dns.com"
[dns.resolver.google]
display_name = "Google"
info_url = "https://www.cloudflare.com/dns/"
info_url = "https://developers.google.com/speed/public-dns/docs/using"
aliases = ["goo","8888"]
weight = 440

View File

@ -1,7 +1,7 @@
use serde::{Deserialize,Serialize};
use trust_dns_resolver::config::Protocol;
use trust_dns_resolver::Name;
use std::sync::Arc;
use std::collections::HashMap;
use std::net::SocketAddr;
@ -11,13 +11,12 @@ pub struct DnsConfig {
pub allow_forward_lookup: bool,
pub allow_reverse_lookup: bool,
pub hidden_suffixes: Vec<String>,
pub search: 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)]
@ -32,18 +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>,
#[serde(default)]
pub search: Vec<String>,
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")]
@ -65,12 +62,11 @@ impl Default for DnsConfig {
allow_reverse_lookup: false,
hidden_suffixes: Vec::new(),
resolver: Default::default(),
search: Vec::new(),
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(),
}
}
}
@ -89,30 +85,23 @@ impl Into<Protocol> for DnsProtocol {
impl DnsResolverConfig {
pub fn to_trust_resolver_config(
&self,
additional_search: &Vec<String>,
&self
) -> trust_dns_resolver::config::ResolverConfig {
let mut resolver = trust_dns_resolver::config::ResolverConfig::new();
for server in &self.servers {
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,
});
}
for search in &self.search {
if let Ok(name) = Name::from_str_relaxed(search) {
resolver.add_search(name);
}
}
for search in additional_search {
if let Ok(name) = Name::from_str_relaxed(search) {
resolver.add_search(name);
}
}
// Not configuring domain search here because searching
// on the resolver level is a bad idea unless we are
// taling about the system resolver which we
// can't tell what to do (which is good!)
return resolver;
}
}

View File

@ -19,9 +19,9 @@ 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;
use trust_dns_resolver::TokioAsyncResolver;
use tokio::signal::unix::{
@ -31,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,
@ -53,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>,
@ -75,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,
@ -104,7 +109,7 @@ struct ServiceSharedState {
#[derive(Clone)]
struct DerivedConfiguration {
dns_resolver_selectables: Vec<Selectable>,
default_resolver: String,
default_resolver: Arc<str>,
}
#[derive(Parser)]
@ -132,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
@ -158,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);
}
}
@ -169,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}");
@ -233,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 ...");
@ -258,7 +226,7 @@ async fn main() {
for (key, resolver_config) in &config.dns.resolver {
println!("Initalizing {} resolver ...", key);
let resolver = TokioAsyncResolver::tokio(
resolver_config.to_trust_resolver_config(&config.dns.search),
resolver_config.to_trust_resolver_config(),
Default::default()
).unwrap();
dns_resolver_map.insert(key.clone(), resolver);
@ -289,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,
@ -363,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();
}
}
@ -376,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;
}
}
@ -384,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,
@ -400,7 +368,7 @@ async fn not_found_handler(
state.templating_engine.render_view(
&settings,
&View::NotFound,
View::NotFound,
).await
}
@ -412,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()
},
@ -447,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()),
@ -456,7 +424,7 @@ async fn handle_default_route(
state.templating_engine.render_view(
&settings,
&View::Index{
View::Index{
result: result,
user_agent: user_agent,
}
@ -486,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();
}
}
@ -525,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
}
@ -536,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
}
}
@ -576,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 {
@ -604,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;
@ -665,46 +633,68 @@ 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 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(
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
// We resolve example.org as basic avoidance of timing sidechannels.
// WARNING: this timing sidechannel avoidance is very crude.
simple_dns::lookup(
&dns_resolver,
&Name::from_ascii("example.org.").expect("Static Dummy Name"),
do_full_lookup).await;
return DigResult {
records: DnsLookupResult{ nxdomain: true , ..Default::default() },
idn: idna_name,
partial_lookup: !do_full_lookup,
used_dns_resolver: dns_resolver_name.clone(),
}
} else {
return DigResult {
records: 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(),
&domain_name,
do_full_lookup).await,
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,
// Invalid domain name
return DigResult {
records: DnsLookupResult{
invalid_name: true,
.. Default::default()
},
idn: idna_name,
partial_lookup: !do_full_lookup,
used_dns_resolver: dns_resolver_name.clone(),
}
}
} else {
return Default::default();
// Unknown resolver name
return DigResult {
records: DnsLookupResult{
unkown_resolver: true,
.. Default::default()
},
idn: idna_name,
partial_lookup: !do_full_lookup,
used_dns_resolver: "unkown_resolver".into(),
}
}
}

View File

@ -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()
}
}

View File

@ -15,6 +15,7 @@ use trust_dns_resolver::{
error::ResolveError,
error::ResolveErrorKind,
lookup::Lookup,
Name,
TokioAsyncResolver,
};
@ -37,10 +38,12 @@ pub struct DnsLookupResult {
pub txt: Option<Vec<String>>,
pub srv: Option<Vec<SrvRecord>>,
pub caa: Option<Vec<String>>,
pub other_error: bool,
pub dns_error: bool,
pub nxdomain: bool,
pub timeout: bool,
pub other_error: bool,
pub dns_error: bool,
pub nxdomain: bool,
pub timeout: bool,
pub invalid_name: bool,
pub unkown_resolver: bool,
}
#[derive(serde::Deserialize, serde::Serialize, Clone, PartialEq)]
@ -171,8 +174,14 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
RecordType::TXT => set_default_if_none(&mut dig_result.txt),
_ => { /* This should not happen */ },
};
for record in lookup.iter() {
add_record_to_lookup_result(dig_result, record);
let name = lookup.query().name();
for record in lookup.record_iter() {
if name == record.name() {
if let Some(data) = record.data() {
add_record_to_lookup_result(dig_result, data);
}
}
//TODO: handle additional responses
}
},
Err(e) => {
@ -210,7 +219,7 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
// records will be fetched.
pub async fn lookup(
resolver: &TokioAsyncResolver,
name: &String,
name: &Name,
do_full_lookup: bool,
) -> DnsLookupResult {
let (
@ -219,10 +228,10 @@ pub async fn lookup(
cname_lookup_res,
aname_lookup_res
) = join!(
resolver.lookup(name, RecordType::A),
resolver.lookup(name, RecordType::AAAA),
resolver.lookup(name, RecordType::CNAME),
resolver.lookup(name, RecordType::ANAME),
resolver.lookup(name.clone(), RecordType::A),
resolver.lookup(name.clone(), RecordType::AAAA),
resolver.lookup(name.clone(), RecordType::CNAME),
resolver.lookup(name.clone(), RecordType::ANAME),
);
// initlize an empty lookup result
@ -243,12 +252,12 @@ pub async fn lookup(
srv_lookup_res,
txt_lookup_res
) = join!(
resolver.lookup(name, RecordType::MX),
resolver.lookup(name, RecordType::NS),
resolver.lookup(name, RecordType::SOA),
resolver.lookup(name, RecordType::CAA),
resolver.lookup(name, RecordType::SRV),
resolver.lookup(name, RecordType::TXT),
resolver.lookup(name.clone(), RecordType::MX),
resolver.lookup(name.clone(), RecordType::NS),
resolver.lookup(name.clone(), RecordType::SOA),
resolver.lookup(name.clone(), RecordType::CAA),
resolver.lookup(name.clone(), RecordType::SRV),
resolver.lookup(name.clone(), RecordType::TXT),
);
integrate_lookup_result(&mut dig_result, mx_lookup_res);

View File

@ -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
View 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(),
}
}
}

View File

@ -26,8 +26,16 @@
<section>
<h2>DNS Records</h2>
{% if r.nxdomain %}
<p class="error box">Our DNS-Server claims that this domain doesn't exist, there shouldn't be any results.</p>
{% set show_nonpresent = true %}
{% if r.unkown_resolver %}
<p class="error box">The resolver you chose is not one of the available ones, if you can reproduce this error by just using the UI <a href="https://codeberg.org/slatian/service.echoip-slatecave/issues/new">please report it</a>.</p>
{% set show_nonpresent = false %}
{% elif r.invalid_name %}
<p class="error box">This domain name does not conform to <a href="https://www.rfc-editor.org/info/std3">the dns specification (std3)</a> rules and was therefore not resolved.</p>
{% set show_nonpresent = false %}
{% elif r.nxdomain %}
<p class="error box">The DNS-Server claims that this domain doesn't exist, there shouldn't be any results.</p>
{% set show_nonpresent = false %}
{% elif r.timeout %}
<p class="error box">There was at least one timeout error while resolving this domain, the results below are incomplete.</p>
{% elif r.other_error %}
@ -66,7 +74,7 @@
<li>{{ helper::ip(extra=extra, ip=address) }}</li>
{% endfor %}
</ul>
{% elif not r.nxdomain %}
{% elif show_nonpresent %}
<p>No <code>A</code> (IPv4) Records.</p>
{% endif %}
@ -77,7 +85,7 @@
<li>{{ helper::ip(extra=extra, ip=address) }}</li>
{% endfor %}
</ul>
{% elif not r.nxdomain %}
{% elif show_nonpresent %}
<p>No <code>AAAA</code> (IPv6) Records.</p>
{% endif %}
@ -90,7 +98,7 @@
<li>{{ helper::dig(extra=extra, name=mx.exchange, fqdn=true, prefix=mx.preference) }}</li>
{% endfor %}
</ul>
{% elif not r.nxdomain %}
{% elif show_nonpresent %}
<p id="mx">No <code>MX</code> (Mail Exchange) records.</p>
{% endif %}
@ -116,7 +124,7 @@
</dl></li>
{% endfor %}
</ul>
{% elif not r.nxdomain %}
{% elif show_nonpresent %}
<p id="soa">No <code>SOA</code> records.</p>
{% endif %}
@ -129,7 +137,7 @@
<li>{{ helper::dig(extra=extra, name=ns) }}</li>
{% endfor %}
</ul>
{% elif not r.nxdomain %}
{% elif show_nonpresent %}
<p id="ns">No <code>NS</code> (Name Server) records.</p>
{% endif %}
@ -141,7 +149,7 @@
<li><code>{{caa}}</code></li>
{% endfor %}
</ul>
{% elif not r.nxdomain %}
{% elif show_nonpresent %}
<p id="caa">No <code>CAA</code> (<a target="_blank" href="https://de.wikipedia.org/wiki/DNS_Certification_Authority_Authorization">Certification Authority Authorization</a>) records.</p>
{% endif %}
@ -152,7 +160,7 @@
<li><code>{{txt}}</code></li>
{% endfor %}
</ul>
{% elif not r.nxdomain %}
{% elif show_nonpresent %}
<p id="txt">No <code>TXT</code> records.</p>
{% endif %}
@ -172,7 +180,7 @@
</dl></li>
{% endfor %}
</ul>
{% elif not r.nxdomain %}
{% elif show_nonpresent %}
<p id="srv">No <code>SRV</code> records.</p>
<p><code>SRV</code> or Service records usually live on their own subdomains like {{ helper::dig(extra=extra, name="_xmpp-client._tcp."~data.query) }}.
{% endif %}

View File

@ -2,7 +2,8 @@
{% block path %}dig/{{ data.query | urlencode_strict }}{% endblock %}
{% block content -%}
{% set r = data.result.records %}
{%- block content -%}
# dig {{data.query}} via {{ data.result.used_dns_resolver }}
{% if data.result.idn -%}
@ -25,7 +26,16 @@ Your IDN would decode to
{% set r = data.result.records -%}
## DNS Records
{% if r.nxdomain %}
{% if r.unkown_resolver %}
{%- set show_nonpresent = false %}
The resolver you chose is not one of the available ones.
=> {{ extra.base_url }}/dns_resolver
{% elif r.invalid_name %}
{%- set show_nonpresent = false %}
This domain name does not conform to the dns specification (std3) rules and was therefore not resolved.
=> https://www.rfc-editor.org/info/std3
{% elif r.nxdomain %}
{%- set show_nonpresent = false %}
Our DNS-Server claims that this domain doesn't exist, there shouldn't be any results.
{%- elif r.timeout -%}
There was at least one timeout error while resolving this domain, the results below are incomplete.
@ -61,7 +71,7 @@ A (IPv4) records:
{% for address in r.a -%}
* {{ address }}
{% endfor %}
{%- elif not r.nxdomain %}
{%- elif show_nonpresent %}
No A (IPv4) Records.
{% endif -%}
@ -70,7 +80,7 @@ AAAA (IPv6) records:
{% for address in r.aaaa -%}
* {{ address }}
{% endfor %}
{%- elif not r.nxdomain %}
{%- elif show_nonpresent %}
No AAAA (IPv6) Records.
{% endif -%}
@ -81,7 +91,7 @@ MX (Mail Exchange) records:
{% for mx in r.mx | sort(attribute="preference") | reverse -%}
* {{ mx.preference }} {{ mx.exchange }}
{% endfor %}
{%- elif not r.nxdomain %}
{%- elif show_nonpresent %}
No MX (Mail Exchange) records.
{% endif %}
@ -96,7 +106,7 @@ SOA (Source Of Authority) records:
* expire: {{soa.expire / 3600 | round(precision=2)}}h
* minimum: {{soa.minimum / 60 | round(precision=2)}}m TTL
{% endfor %}
{%- elif not r.nxdomain %}
{%- elif show_nonpresent %}
No SOA (Source Of Authority) records.
{% endif %}
@ -105,7 +115,7 @@ NS (Name Server) records:
{% for ns in r.ns -%}
* {{ns}}
{% endfor %}
{%- elif not r.nxdomain %}
{%- elif show_nonpresent %}
No NS (Name Server) records.
{% endif %}
@ -114,7 +124,7 @@ CAA (Certification Authority Authorization) records:
{% for caa in r.caa -%}
* {{caa}}
{% endfor %}
{%- elif not r.nxdomain %}
{%- elif show_nonpresent %}
No CAA (Certification Authority Authorization) records.
{% endif %}
@ -123,7 +133,7 @@ TXT records:
{% for txt in r.txt -%}
* {{txt}}
{% endfor %}
{%- elif not r.nxdomain %}
{%- elif show_nonpresent %}
No TXT records.
{% endif %}
@ -135,7 +145,7 @@ SRV records:
* Port: {{srv.port}}
* Target: {{srv.target}}
{% endfor %}
{%- elif not r.nxdomain %}
{%- elif show_nonpresent %}
No SRV records.
SRV or Service records usually live on their own subdomains like {{ "_xmpp-client._tcp."~data.query }}.

View File

@ -31,12 +31,6 @@
<dd>{{ helper::dig(extra=extra, name=c.tls_dns_name) }}</dd>
{%- endif %}
{%- if c.search | length > 0 %}
<dt>Search</dt>
{%- for s in c.search %}
<dd>{{s}}</dd>
{%- endfor %}
{%- endif %}
</dl>
{%- if c.info_url %}
<p class="button-paragraph"><a href="{{c.info_url}}">More about the {{c.display_name}} DNS Server <small>(external link)</small></a></p>

View File

@ -16,14 +16,6 @@ Protocol: {{ c.protocol }}
{%-if c.tls_dns_name %}
DNS Name: {{ c.tls_dns_name }}
{%- endif %}
{%- if c.search | length == 1 %}
Search: {{ c.search | first }}
{%- elif c.search | length > 1 %}
Search:
{%- for s in c.search %}
* {{s}}
{%- endfor %}
{%- endif %}
{%- if c.aliases | length == 1 %}
Alias: {{ c.aliases | first }}
{%- elif c.aliases | length > 1 %}

View File

@ -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 -%}

View File

@ -380,6 +380,8 @@ a:visited {
color: var(--page-link-visited);
}
a.sitename { display: inline-block; }
h1, a.sitename {
margin: var(--heading-mg);
padding: var(--heading-pad);