Added a templating helper

This commit is contained in:
Slatian 2023-02-12 17:35:32 +01:00
parent b21aa5192f
commit febcb8b02e
4 changed files with 237 additions and 128 deletions

View File

@ -3,6 +3,9 @@ use axum::{
extract::State, extract::State,
http::StatusCode, http::StatusCode,
response::Html, response::Html,
response::IntoResponse,
response::Response,
response::Json,
Router, Router,
routing::get, routing::get,
}; };
@ -11,13 +14,15 @@ use trust_dns_resolver::{
TokioAsyncResolver, TokioAsyncResolver,
config::ResolverOpts, config::ResolverOpts,
config::ResolverConfig, config::ResolverConfig,
error::*,
}; };
use std::net::{IpAddr, Ipv4Addr}; use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc; use std::sync::Arc;
#[derive(serde::Deserialize, serde::Serialize)] mod simple_dns;
#[derive(serde::Deserialize, serde::Serialize, Clone, Copy)]
enum ResponseFormat { enum ResponseFormat {
#[serde(rename="text/plain", alias="text")] #[serde(rename="text/plain", alias="text")]
TextPlain, TextPlain,
@ -30,10 +35,10 @@ enum ResponseFormat {
impl ToString for ResponseFormat { impl ToString for ResponseFormat {
fn to_string(&self) -> String { fn to_string(&self) -> String {
match self { match self {
ResponseFormat::TextPlain => "text/plain".to_string(), ResponseFormat::TextPlain => "text/plain",
ResponseFormat::TextHtml => "text/html".to_string(), ResponseFormat::TextHtml => "text/html",
ResponseFormat::ApplicationJson => "application/json".to_string(), ResponseFormat::ApplicationJson => "application/json",
} }.to_string()
} }
} }
@ -46,27 +51,47 @@ struct IpQuery {
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
struct DigQuery { struct DigQuery {
name: String, name: String,
format: Option<ResponseFormat> format: Option<ResponseFormat>,
} }
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
struct MxRecord { struct IpResult {
preference: u16, hostname: Option<String>,
exchange: String,
} }
#[derive(serde::Deserialize, serde::Serialize)] struct TemplatingEngine {
struct DigResult { tera: Tera,
a: Vec<IpAddr>,
aaaa: Vec<IpAddr>,
mx: Vec<MxRecord>,
} }
struct ServiceSharedState { struct ServiceSharedState {
tera: Tera, templating_engine: TemplatingEngine,
dns_resolver: TokioAsyncResolver, dns_resolver: TokioAsyncResolver,
} }
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
enum EchoipView {
Dig { query: DigQuery, result: simple_dns::DnsLookupResult },
Index { query: IpQuery, result: IpResult },
Ip { query: IpQuery, result: IpResult },
Message(String),
#[serde(rename="404")]
NotFound,
}
impl EchoipView {
fn template_name(&self) -> String {
match self {
EchoipView::Dig{..} => "dig",
EchoipView::Index{..} => "index",
EchoipView::Ip{..} => "ip",
EchoipView::Message(..) => "message",
EchoipView::NotFound => "404",
}.to_string()
}
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// Initalize Tera templates // Initalize Tera templates
@ -94,7 +119,9 @@ async fn main() {
// Initialize shared state // Initialize shared state
let shared_state = Arc::new(ServiceSharedState{ let shared_state = Arc::new(ServiceSharedState{
tera: tera, templating_engine: TemplatingEngine{
tera: tera,
},
dns_resolver: dns_resolver, dns_resolver: dns_resolver,
}); });
@ -114,33 +141,53 @@ async fn main() {
.unwrap(); .unwrap();
} }
async fn hello_world_handler() -> &'static str { async fn hello_world_handler(
"Hello, there, you, awesome creature!" State(arc_state): State<Arc<ServiceSharedState>>,
) -> Response {
let state = Arc::clone(&arc_state);
state.templating_engine.render_view(
ResponseFormat::TextPlain,
EchoipView::Message("Hello! There, You, Awesome Creature!".to_string())
).await
} }
async fn simple_reverse_dns_lookup( impl TemplatingEngine {
resolver: &TokioAsyncResolver, async fn render_view(
address: &IpAddr, &self,
) -> Option<String> { format: ResponseFormat,
let revese_res = resolver.reverse_lookup(*address); view: EchoipView,
match revese_res.await { ) -> Response {
Ok(lookup) => { match format {
for name in lookup { ResponseFormat::TextHtml => {
return Some(name.to_string()) let template_name = view.template_name();
}
None let mut context = tera::Context::new();
} context.insert("view", &template_name);
Err(e) => { //intented for shared macros
let kind = e.kind(); context.insert("format", &format.to_string());
match kind { context.insert("data", &view);
ResolveErrorKind::NoRecordsFound { .. } => {
//Ignore, that just happens … match self.tera.render(&(template_name+".html"), &context) {
} Ok(html) => Html(html).into_response(),
_ => { Err(e) => {
println!("Reverse lookup on {address} failed: {kind}"); println!("There was an error while rendering index.html: {e:?}");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
//TODO: Plain Text should have its own matcher
ResponseFormat::ApplicationJson | ResponseFormat::TextPlain => {
match view {
EchoipView::Dig{result, ..} => {
Json(result).into_response()
},
EchoipView::Index{result, ..} | EchoipView::Ip{result, ..} => {
Json(result).into_response()
},
_ => Json(view).into_response(),
} }
} }
None
} }
} }
} }
@ -148,96 +195,40 @@ async fn simple_reverse_dns_lookup(
async fn handle_default_route( async fn handle_default_route(
Query(ip_query): Query<IpQuery>, Query(ip_query): Query<IpQuery>,
State(arc_state): State<Arc<ServiceSharedState>>, State(arc_state): State<Arc<ServiceSharedState>>,
) -> Result<Html<String>,StatusCode> { ) -> Response {
let address = ip_query.ip.unwrap_or(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))); let address = ip_query.ip.unwrap_or(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)));
let format = ip_query.format.unwrap_or(ResponseFormat::TextHtml); let format = ip_query.format.unwrap_or(ResponseFormat::TextHtml);
let format_string = format.to_string();
let state = Arc::clone(&arc_state); let state = Arc::clone(&arc_state);
// do reverse lookup // do reverse lookup
let hostname = simple_reverse_dns_lookup(&state.dns_resolver, &address); let hostname = simple_dns::reverse_lookup(&state.dns_resolver, &address);
let mut context = tera::Context::new(); let result = IpResult{
context.insert("ip", &address); hostname: hostname.await,
context.insert("format", &format_string); };
context.insert("hostname", &hostname.await);
match state.tera.render("index.html", &context) { state.templating_engine.render_view(
Ok(html) => Ok(Html(html)), format,
Err(e) => { EchoipView::Index{query: ip_query, result: result}
println!("There was an error while rendering index.html: {e}"); ).await
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
} }
async fn handle_dig_route( async fn handle_dig_route(
Query(dig_query): Query<DigQuery>, Query(dig_query): Query<DigQuery>,
State(arc_state): State<Arc<ServiceSharedState>>, State(arc_state): State<Arc<ServiceSharedState>>,
) -> Result<Html<String>,StatusCode> { ) -> Response {
let state = Arc::clone(&arc_state); let state = Arc::clone(&arc_state);
let name = dig_query.name; let name = &dig_query.name;
let format = dig_query.format.unwrap_or(ResponseFormat::TextHtml);
let ipv4_lookup_res = state.dns_resolver.ipv4_lookup(&name); let dig_result = simple_dns::lookup(&state.dns_resolver, name, true).await;
let ipv6_lookup_res = state.dns_resolver.ipv6_lookup(&name);
let mx_lookup_res = state.dns_resolver.mx_lookup(&name);
let mut dig_result = DigResult{ state.templating_engine.render_view(
a: Vec::new(), format,
aaaa: Vec::new(), EchoipView::Dig{ query: dig_query, result: dig_result}
mx: Vec::new(), ).await
};
match ipv4_lookup_res.await {
Ok(lookup) => {
for address in lookup {
dig_result.a.push(std::net::IpAddr::V4(address));
}
}
Err(e) => {
println!("There was an error while looking A up {name}: {e}");
}
}
match ipv6_lookup_res.await {
Ok(lookup) => {
for address in lookup {
dig_result.aaaa.push(std::net::IpAddr::V6(address));
}
}
Err(e) => {
println!("There was an error while looking AAAA up {name}: {e}");
}
}
match mx_lookup_res.await {
Ok(lookup) => {
for mx in lookup {
dig_result.mx.push(MxRecord{
preference: mx.preference(),
exchange: mx.exchange().to_string(),
});
}
}
Err(e) => {
println!("There was an error while looking MX up {name}: {e}");
}
}
let mut context = tera::Context::new();
context.insert("dig_query", &name);
context.insert("dig_result", &dig_result);
match state.tera.render("dig.html", &context) {
Ok(html) => Ok(Html(html)),
Err(e) => {
println!("There was an error while rendering index.html: {e}");
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
} }

118
src/simple_dns.rs Normal file
View File

@ -0,0 +1,118 @@
/*
* This module wraps the trust_dns_resolver library
* to generate results thaat are ready for serializing
* or templating.
* It does not aim to be reusable for any other purpose,
* the trust_dns_resolver library already does that.
*/
use trust_dns_resolver::{
TokioAsyncResolver,
error::*,
};
use std::net::IpAddr;
/* Data Structures */
#[derive(serde::Deserialize, serde::Serialize)]
#[derive(Default)]
pub struct DnsLookupResult {
a: Vec<IpAddr>,
aaaa: Vec<IpAddr>,
mx: Vec<MxRecord>,
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct MxRecord {
preference: u16,
exchange: String,
}
/* Lookup Functions*/
pub async fn reverse_lookup(
resolver: &TokioAsyncResolver,
address: &IpAddr,
) -> Option<String> {
let revese_res = resolver.reverse_lookup(*address);
match revese_res.await {
Ok(lookup) => {
for name in lookup {
return Some(name.to_string())
}
None
}
Err(e) => {
let kind = e.kind();
match kind {
ResolveErrorKind::NoRecordsFound { .. } => {
//Ignore, that just happens …
}
_ => {
println!("Reverse lookup on {address} failed: {kind}");
}
}
None
}
}
}
// This function takes a resolver, a domain name and returns a DnsLookupResult.
// If do_full_lookup is false only the A and AAAA (CNAMEs planned for the future)
// records will be fetched.
pub async fn lookup(
resolver: &TokioAsyncResolver,
name: &String,
do_full_lookup: bool,
) -> DnsLookupResult {
let ipv4_lookup_res = resolver.ipv4_lookup(name);
let ipv6_lookup_res = resolver.ipv6_lookup(name);
// initlize an empty lookup result
let mut dig_result: DnsLookupResult = Default::default();
match ipv4_lookup_res.await {
Ok(lookup) => {
for address in lookup {
dig_result.a.push(std::net::IpAddr::V4(address));
}
}
Err(e) => {
println!("There was an error while looking A up {name}: {e}");
}
}
match ipv6_lookup_res.await {
Ok(lookup) => {
for address in lookup {
dig_result.aaaa.push(std::net::IpAddr::V6(address));
}
}
Err(e) => {
println!("There was an error while looking AAAA up {name}: {e}");
}
}
if do_full_lookup {
let mx_lookup_res = resolver.mx_lookup(name);
match mx_lookup_res.await {
Ok(lookup) => {
for mx in lookup {
dig_result.mx.push(MxRecord{
preference: mx.preference(),
exchange: mx.exchange().to_string(),
});
}
}
Err(e) => {
println!("There was an error while looking MX up {name}: {e}");
}
}
}
return dig_result
}

View File

@ -1,39 +1,39 @@
<!DOCTYPE html> <!DOCTYPE html>
{% set r = data.result %}
{% set q = data.query %}
<html> <html>
<head> <head>
<title>Dig: {{ dig_query }}</title> <title>Dig: {{ q.name }}</title>
</head> </head>
<body> <body>
<h1>Lookup for: {{ dig_query }}</h1> <h1>Lookup for: {{ q.name }}</h1>
{% if dig_result.a %} {% if r.a %}
<p>A (IPv4) records:</p> <p>A (IPv4) records:</p>
<ul> <ul>
{% for address in dig_result.a%} {% for address in r.a%}
<li><code>{{address}}</code></li> <li><code>{{address}}</code></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if dig_result.aaaa %} {% if r.aaaa %}
<p>AAAA (IPv6) records:</p> <p>AAAA (IPv6) records:</p>
<ul> <ul>
{% for address in dig_result.aaaa%} {% for address in r.aaaa%}
<li><code>{{address}}</code></li> <li><code>{{address}}</code></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if dig_result.mx %} {% if r.mx %}
<p>MX (Mail Exchange) records:</p> <p>MX (Mail Exchange) records:</p>
<ul> <ul>
{% for mx in dig_result.mx%} {% for mx in r.mx%}
<li>{{mx.preference}} <code><a href="/dig?name={{mx.exchange}}">{{mx.exchange}}</a></code></li> <li>{{mx.preference}} <code><a href="/dig?name={{mx.exchange}}">{{mx.exchange}}</a></code></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
</body> </body>
</html> </html>

View File

@ -1,13 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Your IP: {{ ip }}</title> <title>Your IP: {{ data.query.ip }}</title>
</head> </head>
<body> <body>
<h1>Your IP-Address is: {{ ip }}</h1> <h1>Your IP-Address is: {{ data.query.ip }}</h1>
<p>Your requested format was: <b>{{format}}</b></p> <p>Your requested format was: <b>{{format}}</b></p>
{% if hostname %} {% if hostname %}
<p>Hostname: <b>{{hostname}}</b></p> <p>Hostname: <b>{{data.result.hostname}}</b></p>
{% endif %} {% endif %}
</body> </body>
</html> </html>