51 Commits

Author SHA1 Message Date
8d055682b6 Version bump to 1.5.2 2024-12-14 19:36:59 +01:00
ff8d86ff1d Dependency updates 2024-12-14 19:35:13 +01:00
ce7632d443 Version bump to 1.5.1 2024-10-26 18:23:32 +02:00
cf82db3e87 Update dependencies 2024-10-26 18:19:39 +02:00
fecbe68c7a Cargo update 2024-10-26 18:08:33 +02:00
2e5a2408b4 Add robots meta tag to templates to make it less likely for a crawler to get stuck 2024-08-06 19:36:17 +02:00
3b4e6eba4b Update README, add maintainence mode notice 2024-08-06 19:26:09 +02:00
708fb9c0b3 Improve configurability 2024-08-06 19:17:37 +02:00
0d711648a8 Update idna to 1.0 🥳 2024-08-06 18:40:49 +02:00
1863af50f8 Remove unused configuration option 2024-08-06 18:36:40 +02:00
52d2834e98 Replace lazy_static crate with "new" std::sync::LazyLock 2024-08-06 18:35:00 +02:00
da391003e4 cargo update 2024-08-06 18:12:10 +02:00
7e58423269 Update dependencies 2024-04-21 00:38:30 +02:00
2657aae847 Template fix 2024-04-21 00:26:15 +02:00
13cb85ac5a Added an opt-in to looking up own IP-Address 2024-04-21 00:00:04 +02:00
1a973e09a0 cargo update 2024-04-20 21:47:06 +02:00
f799927f90 Cargo update 2024-03-17 22:02:20 +01:00
8695f0026f lib-humus is now on crates.io 2024-02-11 14:15:13 +01:00
3b552dba8a Downgrade clap to 4.4.18 to support "older" rust versions 2024-02-11 12:38:33 +01:00
1ce60d8291 to_trust_resolver_config() -> to_hickory_resolver_config() 2024-02-11 11:52:33 +01:00
b5097b5a03 cargo update 2024-02-11 11:51:23 +01:00
610842abac Remove unused import 2024-02-11 11:51:08 +01:00
35c71aba64 Use absolute path for icons 2023-12-29 02:51:12 +01:00
d79d949d65 Use the more efficient icon 2023-12-29 02:49:04 +01:00
b3f94b0d90 cargo update 2023-12-29 02:41:33 +01:00
96207f3960 Added a way to display the icon as part of the sitename 2023-12-29 02:37:22 +01:00
cd7a7fbe05 Added a favicon 2023-12-29 02:26:32 +01:00
aaecdb84bb Release 1.2.4 hickory 2023-12-10 18:35:16 +01:00
b08c98376c Update trust_dns to hickory
It doesn't work yet because of:
https://github.com/hickory-dns/hickory-dns/issues/2108
2023-12-10 18:34:20 +01:00
51877fc4c3 1.2.3 quickfix don't encode as numbers (they are nubers internally so that is safe) 2023-12-10 10:43:19 +01:00
396bbdb348 Release 1.2.2 2023-12-10 10:18:31 +01:00
a582c74d18 urlencode queries to external services
and add crt.sh
2023-12-10 10:16:43 +01:00
e8a21ac95f Release 1.2.1 2023-12-09 23:26:12 +01:00
d706e7c614 Update to axum 0.7 2023-12-09 23:21:19 +01:00
0bffa0fd96 Update smaller dependencies 2023-12-09 12:01:00 +01:00
fb0ce1dc0b Update trust_dns to 23.2 2023-12-09 11:54:07 +01:00
a67631fa9b cargo update 2023-12-09 11:17:33 +01:00
636e10f786 Adapted to the new syncronous interface of the HumusEngine 2023-10-30 17:44:33 +01:00
0076db531a cargo update 2023-10-30 17:41:37 +01:00
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
24 changed files with 1496 additions and 1098 deletions

1798
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,30 @@
[package]
name = "echoip-slatecave"
version = "1.1.3"
version = "1.5.2"
edition = "2021"
authors = ["Slatian <baschdel@disroot.org>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = { version = "0.6", features = ["macros", "headers"] }
axum-extra = { version = "0.7", features = ["cookie"] }
axum-client-ip = "0.4"
clap = { version = "4", features = ["derive"] }
governor = "0.5"
idna = "0.3"
lazy_static = "1.4.0"
lib-humus = { version="0.2", features=["axum-view+cookie"] }
axum = { version = "0.7", features = ["macros"] }
axum-extra = { version = "0.9", features = ["cookie", "typed-header"] }
axum-client-ip = "0.6"
clap = { version = "4.5", features = ["derive"] }
governor = "0.8"
idna = "1.0"
parking_lot = "0.12"
regex = "1.7"
regex = "1.11"
serde = { version = "1", features = ["derive","rc"] }
tokio = { version = "1", features = ["macros","signal"] }
tera = "1"
toml = "0.7"
tower = "0.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"
toml = "0.8"
tower = "0.5"
tower-http = { version = "0.6", features = ["fs"] }
hickory-proto = "0.24"
hickory-resolver = { version = "0.24", features = ["dns-over-rustls","dns-over-https","dns-over-quic","native-certs"] }
maxminddb = "0.24"
mime = "0.3"
http = "1.2"

View File

@ -1,9 +1,20 @@
# echoip-slatecave
This is a service inspired by ifconfig.co, but built from scratch with a more useful feature set (in rust !!1!).
This is a service inspired by ifconfig.co, but built from scratch with a more useful feature set. Currently live on [echoip.slatecave.net](https://echoip.slatecave.net).
It is Licensed under the AGPL-v3 license.
## Maintainence Mode
This project is in maintanance mode.
This means the following will happen:
* Dependency updates
* Bugfixes
* Small quality of life improvements
But no active feature development by Slatian.
## Building
Simply run `cargo build` after cloning. The binary should be called `target/debug/echoip-slatecave`.
@ -34,18 +45,45 @@ 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
### Geolocation databases
For geolocation to work you need a MaxMind format database, for full functionality you need the GeoLite2-ASN and GeoLite2-City databses. Unfortunately you have to sign up with [MaxMind](https://maxmind.com) to obtain them. Once you have a license key there is a helper script in [contrib/maxmind-download.sh](contrib/maxmind-download.sh) that helps you with keeping the databse updated.
As an alternative to MaxMind there is also [DB-IP who offer their free databases without a login](https://db-ip.com/db/lite.php). You want the City and ASN databases in mmdb format.
**Don't forget to set the atttribution in the template configuration appropriately.**
See the file `templates/extra.toml`.
Since v1.0 echoip-slatecave reloads the databses when it rececieves a `USR1` signal.
## Security Considerations

View File

@ -1,5 +1,7 @@
use serde::{Deserialize,Serialize};
use trust_dns_resolver::config::Protocol;
use hickory_resolver::config::Protocol;
use hickory_resolver::config::ResolverConfig as HickoryResolverConfig;
use hickory_resolver::config::NameServerConfig;
use std::sync::Arc;
use std::collections::HashMap;
@ -43,8 +45,8 @@ pub struct DnsResolverConfig {
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")]
pub trust_nx_responses: bool,
#[serde(default="default_true", alias="trust_nx_responses")]
pub trust_negative_responses: bool,
}
fn zero() -> i32 {
@ -84,16 +86,16 @@ impl Into<Protocol> for DnsProtocol {
}
impl DnsResolverConfig {
pub fn to_trust_resolver_config(
pub fn to_hickory_resolver_config(
&self
) -> trust_dns_resolver::config::ResolverConfig {
let mut resolver = trust_dns_resolver::config::ResolverConfig::new();
) -> HickoryResolverConfig {
let mut resolver = HickoryResolverConfig::new();
for server in &self.servers {
resolver.add_name_server(trust_dns_resolver::config::NameServerConfig{
resolver.add_name_server(NameServerConfig{
socket_addr: *server,
protocol: self.protocol.clone().into(),
tls_dns_name: self.tls_dns_name.clone().map(|s| s.to_string()),
trust_nx_responses: self.trust_nx_responses,
trust_negative_responses: self.trust_negative_responses,
tls_config: None,
bind_addr: self.bind_address,
});

View File

@ -6,7 +6,7 @@ use std::num::NonZeroU32;
mod dns;
pub use crate::config::dns::{DnsConfig, DnsProtocol, DnsResolverConfig};
pub use crate::config::dns::{DnsConfig, DnsResolverConfig};
#[derive(Deserialize, Default, Clone)]
pub struct EchoIpServiceConfig {
@ -23,7 +23,6 @@ pub struct ServerConfig {
pub ip_header: SecureClientIpSource,
pub allow_private_ip_lookup: bool,
pub static_location: Option<String>,
}
@ -53,7 +52,6 @@ impl Default for ServerConfig {
listen_on: "127.0.0.1:3000".parse().unwrap(),
ip_header: SecureClientIpSource::ConnectInfo,
allow_private_ip_lookup: false,
static_location: None,
}
}
}

View File

@ -1,29 +1,28 @@
use axum::{
body::Body,
extract::{
self,
Query,
State,
Extension,
},
headers,
http::Request,
handler::Handler,
http::Request,
middleware::{self, Next},
response::Response,
Router,
routing::get,
TypedHeader,
};
use axum_client_ip::SecureClientIp;
use axum_extra::headers;
use axum_extra::TypedHeader;
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 hickory_resolver::Name;
use hickory_resolver::TokioAsyncResolver;
use tokio::signal::unix::{
signal,
@ -32,18 +31,23 @@ use tokio::signal::unix::{
use tokio::task;
use std::collections::HashMap;
use std::fs;
use std::net::IpAddr;
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::LazyLock;
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,14 +58,17 @@ 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>,
lang: Option<String>,
dns: Option<String>,
dns_self_lookup: Option<bool>,
}
#[derive(Deserialize, Serialize, Clone)]
@ -77,6 +84,7 @@ pub struct IpResult {
location: Option<LocationResult>,
ip_info: AddressInfo,
used_dns_resolver: Option<Arc<str>>,
reverse_dns_disabled_for_privacy: bool,
}
// We need this one to hide the partial lookup field when irelevant
@ -93,7 +101,7 @@ pub struct DigResult {
}
struct ServiceSharedState {
templating_engine: templating_engine::Engine,
templating_engine: TemplatingEngine,
dns_resolvers: HashMap<Arc<str>,TokioAsyncResolver>,
dns_resolver_aliases: HashMap<Arc<str>,Arc<str>>,
asn_db: geoip::MMDBCarrier,
@ -114,7 +122,7 @@ struct CliArgs {
#[arg(short, long)]
config: Option<String>,
#[arg(short, long)]
listen_on: Option<String>,
listen_on: Option<SocketAddr>,
#[arg(short, long)]
template_location: Option<String>,
#[arg(short,long)]
@ -133,23 +141,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 +150,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 +163,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}");
@ -259,9 +230,9 @@ 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(),
resolver_config.to_hickory_resolver_config(),
Default::default()
).unwrap();
);
dns_resolver_map.insert(key.clone(), resolver);
dns_resolver_selectables.push(Selectable {
id: key.clone(),
@ -273,7 +244,7 @@ async fn main() {
}
}
let listen_on = config.server.listen_on;
let listen_on = cli_args.listen_on.unwrap_or(config.server.listen_on);
let ip_header = config.server.ip_header.clone();
// Initialize shared state
@ -344,21 +315,21 @@ async fn main() {
println!("Starting Server on {} ...",listen_on);
axum::Server::bind(&listen_on)
.serve(app.into_make_service_with_connect_info::<std::net::SocketAddr>())
let listener = tokio::net::TcpListener::bind(&listen_on).await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>())
.await
.unwrap();
}
async fn settings_query_middleware<B>(
async fn settings_query_middleware(
Query(query): Query<SettingsQuery>,
Extension(config): Extension<config::EchoIpServiceConfig>,
Extension(derived_config): Extension<DerivedConfiguration>,
cookie_header: Option<TypedHeader<headers::Cookie>>,
user_agent_header: Option<TypedHeader<headers::UserAgent>>,
mut req: Request<B>,
next: Next<B>
mut req: Request<Body>,
next: Next
) -> Response {
let mut format = query.format;
let mut dns_resolver_id = derived_config.default_resolver;
@ -377,7 +348,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,10 +356,11 @@ 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,
dns_disable_self_lookup: !query.dns_self_lookup.unwrap_or(false),
});
next.run(req).await
}
@ -401,8 +373,8 @@ async fn not_found_handler(
state.templating_engine.render_view(
&settings,
&View::NotFound,
).await
View::NotFound,
)
}
async fn hello_world_handler(
@ -413,11 +385,11 @@ 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()
message: "You are an awesome Creature!".to_string()
},
).await
)
}
@ -432,7 +404,7 @@ async fn handle_default_route(
State(arc_state): State<Arc<ServiceSharedState>>,
Extension(settings): Extension<QuerySettings>,
user_agent_header: Option<TypedHeader<headers::UserAgent>>,
SecureClientIp(address): SecureClientIp
SecureClientIp(client_ip): SecureClientIp
) -> Response {
let state = Arc::clone(&arc_state);
@ -443,12 +415,20 @@ async fn handle_default_route(
search_query,
false,
settings,
state
state,
&client_ip
).await;
}
}
let result = get_ip_result(&address, &settings.lang, &"default".into(), &state).await;
let result = get_ip_result(
&client_ip,
&settings.lang,
&settings.dns_resolver_id,
settings.dns_disable_self_lookup,
&client_ip,
&state,
).await;
let user_agent: Option<String> = match user_agent_header {
Some(TypedHeader(user_agent)) => Some(user_agent.to_string()),
@ -457,29 +437,27 @@ async fn handle_default_route(
state.templating_engine.render_view(
&settings,
&View::Index{
View::Index{
result: result,
user_agent: user_agent,
}
).await
)
}
static ASN_REGEX: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"^[Aa][Ss][Nn]?\s*(\d{1,7})$").unwrap() });
static VIA_REGEX: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"[Vv][Ii][Aa]\s+(\S+)").unwrap() });
async fn handle_search_request(
search_query: String,
this_should_have_been_an_ip: bool,
settings: QuerySettings,
arc_state: Arc<ServiceSharedState>,
client_ip: &IpAddr,
) -> 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::<u32>().ok()) {
@ -487,8 +465,8 @@ async fn handle_search_request(
let state = Arc::clone(&arc_state);
return state.templating_engine.render_view(
&settings,
&View::Asn{asn: asn},
).await
View::Asn{asn: asn},
)
}
}
@ -506,7 +484,7 @@ async fn handle_search_request(
// Try to interpret as an IP-Address
if let Ok(address) = search_query.parse() {
return handle_ip_request(address, settings, arc_state).await;
return handle_ip_request(address, settings, arc_state, client_ip).await;
}
// Fall back to treating it as a hostname
@ -526,8 +504,8 @@ async fn handle_dns_resolver_route(
let state = Arc::clone(&arc_state);
state.templating_engine.render_view(
&settings,
&View::DnsResolverList,
).await
View::DnsResolverList,
)
}
@ -540,13 +518,13 @@ async fn handle_dns_resolver_route_with_path(
if let Some(resolver) = state.config.dns.resolver.get(query.as_str()) {
state.templating_engine.render_view(
&settings,
&View::DnsResolver{ config: resolver.clone() },
).await
View::DnsResolver{ config: resolver.clone() },
)
} else {
state.templating_engine.render_view(
&settings,
&View::NotFound,
).await
View::NotFound,
)
}
}
@ -554,11 +532,12 @@ async fn handle_ip_route_with_path(
Extension(settings): Extension<QuerySettings>,
State(arc_state): State<Arc<ServiceSharedState>>,
extract::Path(query): extract::Path<String>,
SecureClientIp(client_ip): SecureClientIp
) -> Response {
if let Ok(address) = query.parse() {
return handle_ip_request(address, settings, arc_state).await
return handle_ip_request(address, settings, arc_state, &client_ip).await
} else {
return handle_search_request(query, true, settings, arc_state).await;
return handle_search_request(query, true, settings, arc_state, &client_ip).await;
}
}
@ -566,6 +545,7 @@ async fn handle_ip_request(
address: IpAddr,
settings: QuerySettings,
arc_state: Arc<ServiceSharedState>,
client_ip: &IpAddr,
) -> Response {
let state = Arc::clone(&arc_state);
@ -573,21 +553,33 @@ async fn handle_ip_request(
&address,
&settings.lang,
&settings.dns_resolver_id,
settings.dns_disable_self_lookup,
client_ip,
&state).await;
state.templating_engine.render_view(
&settings,
&View::Ip{result: result}
).await
View::Ip{result: result}
)
}
async fn get_ip_result(
address: &IpAddr,
lang: &String,
dns_resolver_name: &Arc<str>,
dns_disable_self_lookup: bool,
client_ip: &IpAddr,
state: &ServiceSharedState,
) -> IpResult {
let mut reverse_dns_disabled_for_privacy = false;
if state.config.dns.allow_reverse_lookup {
if address == client_ip && dns_disable_self_lookup {
reverse_dns_disabled_for_privacy = true;
}
}
let ip_info = AddressInfo::new(&address);
if !(ip_info.scope == AddressScope::Global || ip_info.scope == AddressScope::Shared) || ip_info.cast != AddressCast::Unicast {
@ -599,6 +591,7 @@ async fn get_ip_result(
location: None,
ip_info: ip_info,
used_dns_resolver: None,
reverse_dns_disabled_for_privacy: reverse_dns_disabled_for_privacy,
}
}
}
@ -606,7 +599,7 @@ async fn get_ip_result(
// do reverse lookup
let mut hostname: Option<String> = None;
let mut used_dns_resolver: Option<Arc<str>> = None;
if state.config.dns.allow_reverse_lookup {
if state.config.dns.allow_reverse_lookup && !reverse_dns_disabled_for_privacy {
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());
@ -637,6 +630,7 @@ async fn get_ip_result(
location: location_result,
ip_info: ip_info,
used_dns_resolver: used_dns_resolver,
reverse_dns_disabled_for_privacy: reverse_dns_disabled_for_privacy,
}
}
@ -666,8 +660,8 @@ async fn handle_dig_request(
state.templating_engine.render_view(
&settings,
&View::Dig{ query: dig_query, result: dig_result}
).await
View::Dig{ query: dig_query, result: dig_result}
)
}

View File

@ -1,5 +1,6 @@
use axum_client_ip::SecureClientIp;
use axum::{
body::Body,
extract::Extension,
http::{
Request,
@ -40,11 +41,11 @@ pub fn build_rate_limiting_state(
Extension(arc_limiter)
}
pub async fn rate_limit_middleware<B>(
pub async fn rate_limit_middleware(
SecureClientIp(address): SecureClientIp,
Extension(arc_limiter): Extension<Arc<SimpleRateLimiter<IpAddr>>>,
req: Request<B>,
next: Next<B>
req: Request<Body>,
next: Next
) -> Response {
let limiter = Arc::clone(&arc_limiter);

View File

@ -1,38 +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 */
@ -42,6 +17,7 @@ pub struct QuerySettings {
pub lang: String,
pub available_dns_resolvers: Vec<Selectable>,
pub dns_resolver_id: Arc<str>,
pub dns_disable_self_lookup: bool,
}
#[derive(Deserialize, Serialize, Clone)]
@ -51,3 +27,16 @@ pub struct Selectable {
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

@ -1,17 +1,16 @@
/*
* 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_proto::op::response_code::ResponseCode;
use trust_dns_proto::rr::{
//! This module wraps the hickory_resolver library
//! to generate results thaat are ready for serializing
//! or templating.
//! It does not aim to be reusable for any other purpose,
//! the hickory_resolver library already does that.
use hickory_proto::op::response_code::ResponseCode;
use hickory_proto::rr::{
RData,
record_type::RecordType,
};
use trust_dns_resolver::{
use hickory_resolver::{
error::ResolveError,
error::ResolveErrorKind,
lookup::Lookup,
@ -121,9 +120,9 @@ pub fn set_default_if_none<T>(opt_vec: &mut Option<Vec<T>>) {
pub fn add_record_to_lookup_result(result: &mut DnsLookupResult, record: &RData){
match record {
RData::AAAA(address) => opush(&mut result.aaaa, std::net::IpAddr::V6(*address)),
RData::AAAA(aaaa) => opush(&mut result.aaaa, std::net::IpAddr::V6(aaaa.0)),
RData::ANAME(aname) => opush(&mut result.aname, aname.to_string()),
RData::A(address) => opush(&mut result.a, std::net::IpAddr::V4(*address)),
RData::A(a) => opush(&mut result.a, std::net::IpAddr::V4(a.0)),
RData::CAA(caa) => opush(&mut result.caa, caa.to_string()),
RData::CNAME(cname) => opush(&mut result.cname, cname.to_string()),
RData::MX(mx) => opush(&mut result.mx, MxRecord{

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.to_string())
.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(Cookie::new("dns_resolver",settings.dns_resolver_id.to_string()))
.path("/")
.same_site(cookie::SameSite::Strict)
.build()
.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

@ -5,6 +5,9 @@
<meta charset="utf-8">
<title>{% block title %}{{ extra[view].title | default(value="…") }}{% endblock %} | {{extra.site_name|default(value="echoip")}}</title>
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta name="color-scheme" content="echoip-slatecave <https://codeberg.org/slatian/service.echoip-slatecave>">
{% block robots_meta %}
{% endblock robots_meta %}
<!-- Open-Graph -->
{% block metadata %}
<meta name="description" property="og:description" content="{% block description %}{{ extra[view].description | default(value="One of the best echoip services") | escape_xml }}{% endblock %}" />
@ -24,7 +27,11 @@
<body>
<header>
<nav>
<a href="{{ extra.base_url }}" class="sitename">{{extra.site_name|default(value="echoip")}}</a>
<a href="{{ extra.base_url }}" class="sitename">
{%- if extra.display_icon -%}
<img src="{{extra.display_icon}}" alt="">
{%- endif -%}
{{extra.site_name|default(value="echoip")}}</a>
<form class="search" method="GET" action="{{ extra.base_url }}">
<input type="search" name="query" autocomplete="on" maxlength="260"
title="Search for an IP-Adress, Domain-Name, or ASN."

View File

@ -2,6 +2,8 @@
{% import "helpers.html" as helper %}
{% import "links.html" as links %}
{% block robots_meta %}<meta name="robots" content="noindex,nofollow">{% endblock %}
{% block title %}dig {{ data.query }}{% endblock %}
{% block og_title %}dig {{ data.query }}{% endblock %}
{% block h1 %}dig <code>{{ helper::breadcrumb_domain(extra=extra, name=data.query) }}</code> <small>via <a href="{{extra.base_url}}/dns_resolver/{{data.result.used_dns_resolver}}">{{data.result.used_dns_resolver}}</a></small>{% endblock %}

View File

@ -8,15 +8,26 @@ base_url="http://localhost:3000"
stylesheet = "/style.css"
# URL to and mimetype of your favicon
# favicon = ""
# favicon_mimetype = "image/png"
favicon = "/icon_64.png"
favicon_mimetype = "image/png"
# favicon_mimetype = "image/svg+xml"
# favicon_mimetype = "image/jpeg"
# Icon to display next to the title
display_icon = "/icon_64.png"
# URLs to look up v4 and v6 addresses explicitly
# If you have not configured them, comment them out, the button will stay hidden
v4_url="http://v4.localhost:3000/"
v6_url="http://v6.localhost:3000/"
# Geolocation Attribution for MaxMind
#geo_attribution_html="The Geolocation and ASN information is provided by the GeoLite2 database created by <a href='https://www.maxmind.com/'>MaxMind</a>."
# Geolocation Attribution for DB-IP
#geo_attribution_html="The Geolocation and ASN information is provided by <a href='https://db-ip.com/'>DB-IP</a>."
[404]
# configure the 404 page, this is available for other pages too!
# Use the template name as the section name.

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 -%}
@ -17,8 +17,8 @@
<a href="{{ self::dig_link(extra=extra, name=name) }}">{% if prefix %}{{ prefix }} {% endif %}{% if fqdn or name=="." %}{{ name }}{% else %}{{ name | trim_end_matches(pat=".") }}{% endif %}</a>
{% endmacro dig %}
{% macro ip(extra, ip, text=false) %}
<a href="{{ extra.base_url }}/ip/{{ ip | urlencode_strict | replace(from="%2e", to=".") | replace(from="%3a", to=":") | safe }}"><code>{% if text %}{{ text }}{% else %}{{ ip }}{% endif %}</code></a>
{% macro ip(extra, ip, text=false, with_self_lookup=false) %}
<a href="{{ extra.base_url }}/ip/{{ ip | urlencode_strict | replace(from="%2e", to=".") | replace(from="%3a", to=":") | safe }}{% if with_self_lookup %}?dns_self_lookup=true{% endif %}"><code>{% if text %}{{ text }}{% else %}{{ ip }}{% endif %}</code></a>
{% endmacro dig %}
{% macro breadcrumb_domain(extra, name) %}

View File

@ -1,6 +1,8 @@
{% extends "ip.html" %}
{% import "helpers.html" as helper %}
{% block robots_meta %}{# Allow indexing for landing page #}{% endblock %}
{% block title %}Your IP: {{ data.result.address }}{% endblock %}
{% block og_title %}What is my IP-Address?{% endblock %}
{% block h1 %}Your IPv{% if data.result.ip_info.is_v6_address %}6{% else %}4{% endif %}: <code>{{ data.result.address }}</code>{% endblock %}

View File

@ -2,6 +2,8 @@
{% import "helpers.html" as helper %}
{% import "links.html" as links %}
{% block robots_meta %}<meta name="robots" content="noindex,nofollow">{% endblock %}
{% block title %}{{ data.result.address }}{% endblock %}
{% block og_title %}Lookup {{ data.result.address }}{% endblock %}
{% block h1 %}Lookup <code>{{ data.result.address }}</code>{% endblock %}
@ -18,6 +20,9 @@
{% if r.hostname %}
<dt>Hostname</dt>
<dd>{{ helper::dig(extra=extra, name=r.hostname) }}</dd>
{% elif r.reverse_dns_disabled_for_privacy %}
<dt>Hostname</dt>
<dd>Lookup disabled by default: {{ helper::ip(ip=r.address, extra=extra, text="enable", with_self_lookup=true)}}</dd>
{% endif %}
{% if r.asn %}
<dt><abbr="Autonomous System Number">ASN</abbr></dt>
@ -31,43 +36,44 @@
{% if r.location %}
<section>
<h2>Geolocation</h2>
<dl>
{{ helper::place_dl(place=r.location.continent, label="Continent") }}
{{ helper::place_dl(place=r.location.country, label="Country") }}
{% if r.location.country.iso_code | default(value="") != r.location.registered_country.iso_code | default(value="") %}
{{ helper::place_dl(place=r.location.registered_country, label="Registered in") }}
{% endif %}
{% if r.location.country.iso_code | default(value="") != r.location.represented_country.iso_code | default(value="")%}
{{ helper::place_dl(place=r.location.represented_country, label="Represents") }}
{% endif %}
{% if r.location.subdivisions %}
{% for sd in r.location.subdivisions %}
{{ helper::place_dl(place=sd, label="Subdivision", iso_code_prefix=r.location.country.iso_code|default(value="")) }}
{% endfor %}
{% endif %}
{{ helper::place_dl(place=r.location.city, label="City") }}
{% if r.location.postal_code %}
<dt>Postal Code</dt>
<dd>{{r.location.postal_code}}</dd>
{% endif %}
{% if r.location.time_zone %}
<dt>Timezone</dt>
<dd>{{r.location.time_zone}}</dd>
{% endif %}
{% if r.location.accuracy %}
<dt>Accuracy</dt>
<dd>~{{r.location.accuracy}}km</dd>
{% endif %}
{% if r.location.coordinates %}
<dt>Coordinates</dt>
<dd><a target="_blank" href="{{ links::map_link(lat=r.location.coordinates.lat, lon=r.location.coordinates.lon)}}">lat: {{r.location.coordinates.lat}}, lon: {{r.location.coordinates.lon}}</a></dd>
{% endif %}
</dl>
<!--We have to put that there to comply with maxminds licensing-->
<p><small>
The GeoIP and ASN information is provided by the GeoLite2 database created by
<a target="_blank" href="https://www.maxmind.com">MaxMind</a>.
</small></p>
{% if extra.geo_attribution_html %}
<dl>
{{ helper::place_dl(place=r.location.continent, label="Continent") }}
{{ helper::place_dl(place=r.location.country, label="Country") }}
{% if r.location.country.iso_code | default(value="") != r.location.registered_country.iso_code | default(value="") %}
{{ helper::place_dl(place=r.location.registered_country, label="Registered in") }}
{% endif %}
{% if r.location.country.iso_code | default(value="") != r.location.represented_country.iso_code | default(value="")%}
{{ helper::place_dl(place=r.location.represented_country, label="Represents") }}
{% endif %}
{% if r.location.subdivisions %}
{% for sd in r.location.subdivisions %}
{{ helper::place_dl(place=sd, label="Subdivision", iso_code_prefix=r.location.country.iso_code|default(value="")) }}
{% endfor %}
{% endif %}
{{ helper::place_dl(place=r.location.city, label="City") }}
{% if r.location.postal_code %}
<dt>Postal Code</dt>
<dd>{{r.location.postal_code}}</dd>
{% endif %}
{% if r.location.time_zone %}
<dt>Timezone</dt>
<dd>{{r.location.time_zone}}</dd>
{% endif %}
{% if r.location.accuracy %}
<dt>Accuracy</dt>
<dd>~{{r.location.accuracy}}km</dd>
{% endif %}
{% if r.location.coordinates %}
<dt>Coordinates</dt>
<dd><a target="_blank" href="{{ links::map_link(lat=r.location.coordinates.lat, lon=r.location.coordinates.lon)}}">lat: {{r.location.coordinates.lat}}, lon: {{r.location.coordinates.lon}}</a></dd>
{% endif %}
</dl>
<p><small>{{extra.geo_attribution_html | safe}}</small></p>
{% else %}
<p><strong style="font-size: 2em">Please configure the <code>geo_attribution_html</code> key in the template extra configuration!</strong></p>
<p>The geolocation information will then become visible.</p>
{% endif %}
</section>
{% endif %}
{% block extra_content %}{% endblock %}

View File

@ -13,12 +13,19 @@
* Type of Address: {{ helper::ip_info(ip_info=r.ip_info) }}
{% if r.hostname -%}
* Hostname: {{ r.hostname }}
{%- elif r.reverse_dns_disabled_for_privacy %}
* Hostname: Lookup disabled by default
{%- endif %}
{% if r.asn -%}
* ASN: AS{{ r.asn.asn }}
* AS Name: {{r.asn.name}}
{%- endif -%}
{%- if r.reverse_dns_disabled_for_privacy %}
=> /ip/{{ data.result.address }}?dns_self_lookup=true Do a reverse DNS lookup
{% endif %}
{%- if r.location %}
## Geolocation
@ -52,7 +59,7 @@ lat: {{r.location.coordinates.lat}}, lon: {{r.location.coordinates.lon}}
=> {{ links::map_link(lat=r.location.coordinates.lat, lon=r.location.coordinates.lon)}}
{%- endif %}
The GeoIP and ASN information is provided by the GeoLite2 database created by MaxMind.
{{ extra.geo_attribution_html | default(value="Please configure the geo_attribution_html key in the template extra configuration.") | striptags }}
{% endif -%}
{%- block extra_content %}{% endblock -%}

View File

@ -16,12 +16,13 @@
{% macro domain_name_links(name) %}
<p>Look up <code>{{name}}</code></p>
<ul class="link-list">
<li><a target="_blank" href="https://www.shodan.io/domain/{{ name }}">… on shodan.io <small>(limited query's per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://search.censys.io/search?resource=hosts&sort=RELEVANCE&per_page=25&virtual_hosts=EXCLUDE&q={{ name }}">… on search.censys.io <small>(10 query's per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://www.virustotal.com/gui/domain/{{ name }}">… on virustotal.com</a></li>
<li><a target="_blank" href="https://observatory.mozilla.org/analyze/{{ name }}">… on the Mozilla Observatory (http and tls checks)</a></li>
<li><a target="_blank" href="https://internet.nl/site/{{ name }}">… on the Internet.nl Website test</a></li>
<li><a target="_blank" href="https://client.rdap.org/?type=domain&object={{ name }}">… on client.rdap.org <small>(a modern whois, make sure to allow xhr to 3rd parties)</small></a></li>
<li><a target="_blank" href="https://www.shodan.io/domain/{{ name | urlencode_strict }}">… on shodan.io <small>(limited query's per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://search.censys.io/search?resource=hosts&sort=RELEVANCE&per_page=25&virtual_hosts=EXCLUDE&q={{ name | urlencode_strict }}">… on search.censys.io <small>(10 query's per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://www.virustotal.com/gui/domain/{{ name | urlencode_strict }}">… on virustotal.com</a></li>
<li><a target="_blank" href="https://observatory.mozilla.org/analyze/{{ name | urlencode_strict }}">… on the Mozilla Observatory (http and tls checks)</a></li>
<li><a target="_blank" href="https://internet.nl/site/{{ name | urlencode_strict }}">… on the Internet.nl Website test</a></li>
<li><a target="_blank" href="https://client.rdap.org/?type=domain&object={{ name | urlencode_strict }}">… on client.rdap.org <small>(a modern whois, make sure to allow xhr to 3rd parties)</small></a></li>
<li><a target="_blank" href="https://crt.sh/?Identity={{ name | urlencode_strict }}&match==">… on crt.sh <small>(Certificate Transparancy Monitor)</small></a></li>
</ul>
{% endmacro domain_name_links %}

50
templates/static/icon.svg Normal file
View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 48 48"
version="1.1"
id="svg1"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<linearGradient
id="linearGradient8">
<stop
style="stop-color:#fb9a00;stop-opacity:1;"
offset="0"
id="stop8" />
<stop
style="stop-color:#884f00;stop-opacity:1;"
offset="0.49966338"
id="stop10" />
<stop
style="stop-color:#be8700;stop-opacity:1;"
offset="1"
id="stop9" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient8"
id="linearGradient9"
x1="10.202637"
y1="35.241699"
x2="39.21582"
y2="12.833984"
gradientUnits="userSpaceOnUse" />
</defs>
<g
id="layer1">
<path
id="path2"
style="fill:url(#linearGradient9);fill-opacity:1;stroke-width:3.15427;stroke-linejoin:round;paint-order:stroke markers fill"
d="m 2,7 v 33.767595 l 1.586,0.0021 L 8.299716,45.41681 12.826,40.767584 H 46 V 7 Z" />
<path
id="rect1"
style="fill:#111111;stroke-width:3;stroke-linejoin:round;paint-order:stroke markers fill"
d="M 3 8 L 3 40 L 4.0019531 40 L 4 40.001953 L 8.2792969 44.205078 L 12.412109 40 L 45 40 L 45 8 L 3 8 z M 35.671875 11.712891 L 39.357422 11.712891 L 39.357422 36.287109 L 35.671875 36.287109 L 35.671875 17.033203 L 31.494141 21.363281 L 28.839844 18.804688 C 31.107109 16.462871 35.671875 11.712891 35.671875 11.712891 z M 8.6425781 21.542969 L 12.328125 21.542969 L 12.328125 25.228516 L 8.6425781 25.228516 L 8.6425781 21.542969 z M 20.927734 21.542969 L 24.615234 21.542969 L 24.615234 25.228516 L 20.927734 25.228516 L 20.927734 21.542969 z M 8.6425781 32.599609 L 12.328125 32.599609 L 12.328125 36.287109 L 8.6425781 36.287109 L 8.6425781 32.599609 z M 20.927734 32.599609 L 24.615234 32.599609 L 24.615234 36.287109 L 20.927734 36.287109 L 20.927734 32.599609 z " />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -599,3 +599,10 @@ form.search {
background: var(--button-bg);
}
/* Custom icon style for sitename*/
.sitename > img {
height: 1.2em;
padding: 0 0.3ch;
margin-bottom: -.2em;
}