mirror of
https://codeberg.org/slatian/service.echoip-slatecave.git
synced 2025-07-17 14:33:27 +02:00
Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
d5c5d30d32 | |||
38be0c05d0 | |||
e81ce74a2f | |||
7130c0d94a | |||
47fad2007b | |||
ba34caf8fc | |||
caf47522e4 | |||
b98bb67b4c | |||
d902dae35d | |||
2aae2d6626 | |||
4079e24c43 | |||
2b0c4eb3fb | |||
8d055682b6 | |||
ff8d86ff1d | |||
ce7632d443 | |||
cf82db3e87 | |||
fecbe68c7a | |||
2e5a2408b4 | |||
3b4e6eba4b | |||
708fb9c0b3 | |||
0d711648a8 | |||
1863af50f8 | |||
52d2834e98 | |||
da391003e4 | |||
7e58423269 | |||
2657aae847 | |||
13cb85ac5a | |||
1a973e09a0 | |||
f799927f90 | |||
8695f0026f | |||
3b552dba8a | |||
1ce60d8291 | |||
b5097b5a03 | |||
610842abac | |||
35c71aba64 | |||
d79d949d65 | |||
b3f94b0d90 | |||
96207f3960 | |||
cd7a7fbe05 | |||
aaecdb84bb | |||
b08c98376c | |||
51877fc4c3 | |||
396bbdb348 | |||
a582c74d18 | |||
e8a21ac95f | |||
d706e7c614 | |||
0bffa0fd96 | |||
fb0ce1dc0b | |||
a67631fa9b | |||
636e10f786 | |||
0076db531a | |||
64e639b0df | |||
2f9f01e947 | |||
f2e9e36e99 | |||
912a119361 | |||
5adca4fb80 | |||
5ac056ef99 | |||
51aa05fe13 | |||
de179ea7fa | |||
bfa383ddbe | |||
a33473fdc9 | |||
20fb7ee2ff | |||
c5a7597561 | |||
c56cc6edbd | |||
5c74de5685 | |||
223abdd804 |
2459
Cargo.lock
generated
2459
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
39
Cargo.toml
39
Cargo.toml
@ -1,27 +1,32 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "echoip-slatecave"
|
name = "echoip-slatecave"
|
||||||
version = "0.1.0"
|
version = "1.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Slatian <baschdel@disroot.org>"]
|
authors = ["Slatian <baschdel@disroot.org>"]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.6", features = ["macros", "headers"] }
|
lib-humus = { version="0.3", features=["axum-view+cookie"] }
|
||||||
axum-extra = { version = "0.7", features = ["cookie"] }
|
|
||||||
axum-client-ip = "0.4"
|
axum-client-ip = "0.7"
|
||||||
clap = { version = "4", features = ["derive"] }
|
axum-extra = { version = "0.10", features = ["cookie", "typed-header"] }
|
||||||
governor = "0.5"
|
axum = { version = "0.8", features = ["macros"] }
|
||||||
idna = "0.3"
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
lazy_static = "1.4.0"
|
env_logger = "0.11"
|
||||||
|
governor = "0.8"
|
||||||
|
hickory-proto = "0.25"
|
||||||
|
hickory-resolver = { version = "0.25", features = ["tls-ring","https-ring","quic-ring","rustls-platform-verifier","system-config"] }
|
||||||
|
http = "1.2"
|
||||||
|
idna = "1.0"
|
||||||
|
log = "0.4"
|
||||||
|
maxminddb = "0.24"
|
||||||
|
mime = "0.3"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
regex = "1.7"
|
regex = "1.11"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive","rc"] }
|
||||||
tokio = { version = "1", features = ["macros","signal"] }
|
|
||||||
tera = "1"
|
tera = "1"
|
||||||
toml = "0.7"
|
tokio = { version = "1", features = ["macros","signal"] }
|
||||||
tower = "0.4"
|
toml = "0.8"
|
||||||
tower-http = { version = "0.4", features = ["fs"] }
|
tower = "0.5"
|
||||||
trust-dns-proto = "0.22"
|
tower-http = { version = "0.6", features = ["fs"] }
|
||||||
trust-dns-resolver = { version = "0.22", features = ["dns-over-rustls","dns-over-https","dns-over-quic"] }
|
|
||||||
maxminddb = "0.23"
|
|
||||||
|
50
README.md
50
README.md
@ -1,9 +1,20 @@
|
|||||||
# echoip-slatecave
|
# 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.
|
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
|
## Building
|
||||||
|
|
||||||
Simply run `cargo build` after cloning. The binary should be called `target/debug/echoip-slatecave`.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
Since v1.0 echoip-slatecave reloads the databses when it rececieves a `USR1` signal.
|
||||||
|
|
||||||
## Security Considerations
|
## Security Considerations
|
||||||
@ -60,6 +98,10 @@ Most noably you can disable reverse dns lookups, hide domains with given suffixe
|
|||||||
`echoip-slatecave` only exposes an unencrypted http interface to keep the service itself simple.
|
`echoip-slatecave` only exposes an unencrypted http interface to keep the service itself simple.
|
||||||
For a public service you should use a reverse proxy like Caddy, apache2 or nginx and configure the `ip_header` option, see the echoip_config.toml file. Usually the preconfigured `RightmostXForwardedFor` is the correct one, but please doublecheck it matches your servers configuration, it should fail by simply not working, but no guarantees given.
|
For a public service you should use a reverse proxy like Caddy, apache2 or nginx and configure the `ip_header` option, see the echoip_config.toml file. Usually the preconfigured `RightmostXForwardedFor` is the correct one, but please doublecheck it matches your servers configuration, it should fail by simply not working, but no guarantees given.
|
||||||
|
|
||||||
|
Consider hiding the values of the following in your server logs for increased privacy:
|
||||||
|
* The `query` URL query paramter
|
||||||
|
* All paths subpath to `/ip/` and `/dig/`
|
||||||
|
|
||||||
### Denail of Service
|
### Denail of Service
|
||||||
|
|
||||||
`echoip-slatecave` has some simle ratelimiting built in (see the `[ratelimit]` section in the configuration file) this should help you with too frequest automated requests causung high load.
|
`echoip-slatecave` has some simle ratelimiting built in (see the `[ratelimit]` section in the configuration file) this should help you with too frequest automated requests causung high load.
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
use serde::{Deserialize,Serialize};
|
use serde::{Deserialize,Serialize};
|
||||||
use trust_dns_resolver::config::Protocol;
|
use hickory_proto::xfer::Protocol;
|
||||||
|
use hickory_resolver::config::ResolverConfig as HickoryResolverConfig;
|
||||||
|
use hickory_resolver::config::NameServerConfig;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
@ -10,12 +13,12 @@ pub struct DnsConfig {
|
|||||||
pub allow_forward_lookup: bool,
|
pub allow_forward_lookup: bool,
|
||||||
pub allow_reverse_lookup: bool,
|
pub allow_reverse_lookup: bool,
|
||||||
pub hidden_suffixes: Vec<String>,
|
pub hidden_suffixes: Vec<String>,
|
||||||
pub resolver: HashMap<String,DnsResolverConfig>,
|
pub resolver: HashMap<Arc<str>,DnsResolverConfig>,
|
||||||
|
|
||||||
pub enable_system_resolver: bool,
|
pub enable_system_resolver: bool,
|
||||||
pub system_resolver_name: String,
|
pub system_resolver_name: Arc<str>,
|
||||||
pub system_resolver_weight: i32,
|
pub system_resolver_weight: i32,
|
||||||
pub system_resolver_id: String,
|
pub system_resolver_id: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
@ -30,20 +33,21 @@ pub enum DnsProtocol {
|
|||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct DnsResolverConfig {
|
pub struct DnsResolverConfig {
|
||||||
pub display_name: String,
|
pub display_name: Arc<str>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub info_url: Option<String>,
|
pub info_url: Option<Arc<str>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub aliases: Vec<String>,
|
pub aliases: Vec<Arc<str>>,
|
||||||
#[serde(default="zero")]
|
#[serde(default="zero")]
|
||||||
pub weight: i32,
|
pub weight: i32,
|
||||||
pub servers: Vec<SocketAddr>,
|
pub servers: Vec<SocketAddr>,
|
||||||
pub protocol: DnsProtocol,
|
pub protocol: DnsProtocol,
|
||||||
pub tls_dns_name: Option<String>,
|
pub tls_dns_name: Option<Arc<str>>,
|
||||||
|
pub http_endpoint: Option<Arc<str>>,
|
||||||
#[serde(skip_serializing)] //Don't leak our bind address to the outside
|
#[serde(skip_serializing)] //Don't leak our bind address to the outside
|
||||||
pub bind_address: Option<SocketAddr>,
|
pub bind_address: Option<SocketAddr>,
|
||||||
#[serde(default="default_true")]
|
#[serde(default="default_true", alias="trust_nx_responses")]
|
||||||
pub trust_nx_responses: bool,
|
pub trust_negative_responses: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn zero() -> i32 {
|
fn zero() -> i32 {
|
||||||
@ -63,37 +67,37 @@ impl Default for DnsConfig {
|
|||||||
resolver: Default::default(),
|
resolver: Default::default(),
|
||||||
|
|
||||||
enable_system_resolver: true,
|
enable_system_resolver: true,
|
||||||
system_resolver_name: "System".to_string(),
|
system_resolver_name: "System".into(),
|
||||||
system_resolver_weight: 1000,
|
system_resolver_weight: 1000,
|
||||||
system_resolver_id: "system".to_string(),
|
system_resolver_id: "system".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Protocol> for DnsProtocol {
|
impl From<DnsProtocol> for Protocol {
|
||||||
fn into(self) -> Protocol {
|
fn from(value: DnsProtocol) -> Self {
|
||||||
match self {
|
match value {
|
||||||
Self::Udp => Protocol::Udp,
|
DnsProtocol::Udp => Protocol::Udp,
|
||||||
Self::Tcp => Protocol::Tcp,
|
DnsProtocol::Tcp => Protocol::Tcp,
|
||||||
Self::Tls => Protocol::Tls,
|
DnsProtocol::Tls => Protocol::Tls,
|
||||||
Self::Https => Protocol::Https,
|
DnsProtocol::Https => Protocol::Https,
|
||||||
Self::Quic => Protocol::Quic,
|
DnsProtocol::Quic => Protocol::Quic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DnsResolverConfig {
|
impl DnsResolverConfig {
|
||||||
pub fn to_trust_resolver_config(
|
pub fn to_hickory_resolver_config(
|
||||||
&self
|
&self
|
||||||
) -> trust_dns_resolver::config::ResolverConfig {
|
) -> HickoryResolverConfig {
|
||||||
let mut resolver = trust_dns_resolver::config::ResolverConfig::new();
|
let mut resolver = HickoryResolverConfig::new();
|
||||||
for server in &self.servers {
|
for server in &self.servers {
|
||||||
resolver.add_name_server(trust_dns_resolver::config::NameServerConfig{
|
resolver.add_name_server(NameServerConfig{
|
||||||
socket_addr: *server,
|
socket_addr: *server,
|
||||||
protocol: self.protocol.clone().into(),
|
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,
|
http_endpoint: self.http_endpoint.as_deref().map(ToString::to_string),
|
||||||
tls_config: None,
|
trust_negative_responses: self.trust_negative_responses,
|
||||||
bind_addr: self.bind_address,
|
bind_addr: self.bind_address,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use std::num::NonZeroU32;
|
|||||||
|
|
||||||
mod dns;
|
mod dns;
|
||||||
|
|
||||||
pub use crate::config::dns::{DnsConfig, DnsProtocol, DnsResolverConfig};
|
pub use crate::config::dns::{DnsConfig, DnsResolverConfig};
|
||||||
|
|
||||||
#[derive(Deserialize, Default, Clone)]
|
#[derive(Deserialize, Default, Clone)]
|
||||||
pub struct EchoIpServiceConfig {
|
pub struct EchoIpServiceConfig {
|
||||||
@ -23,7 +23,6 @@ pub struct ServerConfig {
|
|||||||
pub ip_header: SecureClientIpSource,
|
pub ip_header: SecureClientIpSource,
|
||||||
|
|
||||||
pub allow_private_ip_lookup: bool,
|
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(),
|
listen_on: "127.0.0.1:3000".parse().unwrap(),
|
||||||
ip_header: SecureClientIpSource::ConnectInfo,
|
ip_header: SecureClientIpSource::ConnectInfo,
|
||||||
allow_private_ip_lookup: false,
|
allow_private_ip_lookup: false,
|
||||||
static_location: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
49
src/geoip.rs
49
src/geoip.rs
@ -3,9 +3,10 @@
|
|||||||
* that provides the results ready for templating.
|
* that provides the results ready for templating.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use maxminddb;
|
use log::{debug,info,warn,error};
|
||||||
use maxminddb::geoip2;
|
use maxminddb::geoip2;
|
||||||
|
|
||||||
|
use maxminddb::MaxMindDBError::AddressNotFoundError;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@ -55,7 +56,7 @@ pub struct MMDBCarrier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait QueryLocation {
|
pub trait QueryLocation {
|
||||||
fn query_location_for_ip(&self, address: &IpAddr, laguages: &Vec<&String>) -> Option<LocationResult>;
|
fn query_location_for_ip(&self, address: &IpAddr, laguages: &[&str]) -> Option<LocationResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait QueryAsn {
|
pub trait QueryAsn {
|
||||||
@ -66,12 +67,12 @@ pub trait QueryAsn {
|
|||||||
|
|
||||||
pub fn extract_localized_name(
|
pub fn extract_localized_name(
|
||||||
names: &Option<BTreeMap<&str, &str>>,
|
names: &Option<BTreeMap<&str, &str>>,
|
||||||
languages: &Vec<&String>)
|
languages: &[&str])
|
||||||
-> Option<String> {
|
-> Option<String> {
|
||||||
match names {
|
match names {
|
||||||
Some(names) => {
|
Some(names) => {
|
||||||
for language in languages {
|
for language in languages {
|
||||||
if let Some(name) = names.get(language.as_str()){
|
if let Some(name) = names.get(language){
|
||||||
return Some(name.to_string())
|
return Some(name.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,7 +82,7 @@ names: &Option<BTreeMap<&str, &str>>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn geoip2_city_to_named_location(item: geoip2::city::City, languages: &Vec<&String>) -> NamedLocation {
|
pub fn geoip2_city_to_named_location(item: geoip2::city::City, languages: &[&str]) -> NamedLocation {
|
||||||
NamedLocation {
|
NamedLocation {
|
||||||
iso_code: None,
|
iso_code: None,
|
||||||
geoname_id: item.geoname_id,
|
geoname_id: item.geoname_id,
|
||||||
@ -89,7 +90,7 @@ pub fn geoip2_city_to_named_location(item: geoip2::city::City, languages: &Vec<&
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn geoip2_continent_to_named_location(item: geoip2::country::Continent, languages: &Vec<&String>) -> NamedLocation {
|
pub fn geoip2_continent_to_named_location(item: geoip2::country::Continent, languages: &[&str]) -> NamedLocation {
|
||||||
NamedLocation {
|
NamedLocation {
|
||||||
iso_code: item.code.map(ToString::to_string),
|
iso_code: item.code.map(ToString::to_string),
|
||||||
geoname_id: item.geoname_id,
|
geoname_id: item.geoname_id,
|
||||||
@ -97,7 +98,7 @@ pub fn geoip2_continent_to_named_location(item: geoip2::country::Continent, lang
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn geoip2_country_to_named_location(item: geoip2::country::Country, languages: &Vec<&String>) -> NamedLocation {
|
pub fn geoip2_country_to_named_location(item: geoip2::country::Country, languages: &[&str]) -> NamedLocation {
|
||||||
NamedLocation {
|
NamedLocation {
|
||||||
iso_code: item.iso_code.map(ToString::to_string),
|
iso_code: item.iso_code.map(ToString::to_string),
|
||||||
geoname_id: item.geoname_id,
|
geoname_id: item.geoname_id,
|
||||||
@ -105,7 +106,7 @@ pub fn geoip2_country_to_named_location(item: geoip2::country::Country, language
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn geoip2_represented_country_to_named_location(item: geoip2::country::RepresentedCountry, languages: &Vec<&String>) -> NamedLocation {
|
pub fn geoip2_represented_country_to_named_location(item: geoip2::country::RepresentedCountry, languages: &[&str]) -> NamedLocation {
|
||||||
NamedLocation {
|
NamedLocation {
|
||||||
iso_code: item.iso_code.map(ToString::to_string),
|
iso_code: item.iso_code.map(ToString::to_string),
|
||||||
geoname_id: item.geoname_id,
|
geoname_id: item.geoname_id,
|
||||||
@ -113,7 +114,7 @@ pub fn geoip2_represented_country_to_named_location(item: geoip2::country::Repre
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn geoip2_subdivision_to_named_location(item: geoip2::city::Subdivision, languages: &Vec<&String>) -> NamedLocation {
|
pub fn geoip2_subdivision_to_named_location(item: geoip2::city::Subdivision, languages: &[&str]) -> NamedLocation {
|
||||||
NamedLocation {
|
NamedLocation {
|
||||||
iso_code: item.iso_code.map(ToString::to_string),
|
iso_code: item.iso_code.map(ToString::to_string),
|
||||||
geoname_id: item.geoname_id,
|
geoname_id: item.geoname_id,
|
||||||
@ -135,9 +136,15 @@ impl QueryAsn for MMDBCarrier {
|
|||||||
name: res.autonomous_system_organization.map(ToString::to_string),
|
name: res.autonomous_system_organization.map(ToString::to_string),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
Err(AddressNotFoundError(_)) => {
|
||||||
|
// Log to the debug channel.
|
||||||
|
// This isn't severe, and shouldn't be logged in production.
|
||||||
|
debug!("ASN not found in database for {address}.");
|
||||||
|
None
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error while looking up ASN for {address}: {e}");
|
error!("Error while looking up ASN for {address}: {e}");
|
||||||
Default::default()
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -147,7 +154,7 @@ impl QueryAsn for MMDBCarrier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl QueryLocation for MMDBCarrier {
|
impl QueryLocation for MMDBCarrier {
|
||||||
fn query_location_for_ip(&self, address: &IpAddr, languages: &Vec<&String>) -> Option<LocationResult> {
|
fn query_location_for_ip(&self, address: &IpAddr, languages: &[&str]) -> Option<LocationResult> {
|
||||||
let mmdb = self.mmdb.read();
|
let mmdb = self.mmdb.read();
|
||||||
match &*mmdb {
|
match &*mmdb {
|
||||||
Some(mmdb) => {
|
Some(mmdb) => {
|
||||||
@ -203,9 +210,15 @@ impl QueryLocation for MMDBCarrier {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
Err(AddressNotFoundError(_)) => {
|
||||||
|
// Log to the debug channel.
|
||||||
|
// This isn't severe, and shouldn't be logged in production.
|
||||||
|
debug!("IP location not found in database for {address}");
|
||||||
|
None
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error while looking up ASN for {address}: {e}");
|
error!("Error while looking up IP location for {address}: {e}");
|
||||||
Default::default()
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -232,7 +245,7 @@ impl MMDBCarrier {
|
|||||||
|
|
||||||
pub fn load_database_from_path(&self, path: &Path) -> Result<(),maxminddb::MaxMindDBError> {
|
pub fn load_database_from_path(&self, path: &Path) -> Result<(),maxminddb::MaxMindDBError> {
|
||||||
let mut mmdb = self.mmdb.write();
|
let mut mmdb = self.mmdb.write();
|
||||||
println!("Loading {} from '{}' ...", &self.name, path.display());
|
info!("Loading {} from '{}' ...", &self.name, path.display());
|
||||||
match maxminddb::Reader::open_readfile(path) {
|
match maxminddb::Reader::open_readfile(path) {
|
||||||
Ok(reader) => {
|
Ok(reader) => {
|
||||||
let wording = if mmdb.is_some() {
|
let wording = if mmdb.is_some() {
|
||||||
@ -241,13 +254,13 @@ impl MMDBCarrier {
|
|||||||
"Loaded new"
|
"Loaded new"
|
||||||
};
|
};
|
||||||
*mmdb = Some(reader);
|
*mmdb = Some(reader);
|
||||||
println!("{} {} with new one.", wording, &self.name);
|
info!("{} {} with new one.", wording, &self.name);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error while reading {}: {}", &self.name, &e);
|
error!("Error while reading {}: {}", &self.name, &e);
|
||||||
if mmdb.is_some() {
|
if mmdb.is_some() {
|
||||||
println!("Not replacing old database.");
|
warn!("Not replacing old database.");
|
||||||
}
|
}
|
||||||
Err(e)
|
Err(e)
|
||||||
},
|
},
|
||||||
|
12
src/idna.rs
12
src/idna.rs
@ -14,7 +14,7 @@ pub enum NameType {
|
|||||||
#[default]
|
#[default]
|
||||||
Ascii,
|
Ascii,
|
||||||
Unicode,
|
Unicode,
|
||||||
IDN,
|
Idn,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note, that the
|
// Note, that the
|
||||||
@ -32,8 +32,8 @@ pub struct IdnaName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl IdnaName {
|
impl IdnaName {
|
||||||
pub fn from_string(s: &String) -> Self {
|
pub fn from_str(s: &str) -> Self {
|
||||||
if s == "" {
|
if s.is_empty() {
|
||||||
return Default::default();
|
return Default::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,17 +41,17 @@ impl IdnaName {
|
|||||||
let unicode: String;
|
let unicode: String;
|
||||||
let decoder_error;
|
let decoder_error;
|
||||||
if s.starts_with("xn--") && s.is_ascii() {
|
if s.starts_with("xn--") && s.is_ascii() {
|
||||||
original_was = NameType::IDN;
|
original_was = NameType::Idn;
|
||||||
let (uc, ures) = idna::domain_to_unicode(s);
|
let (uc, ures) = idna::domain_to_unicode(s);
|
||||||
unicode = uc;
|
unicode = uc;
|
||||||
decoder_error = ures.map_or_else(|e| Some(e.to_string()), |_| None);
|
decoder_error = ures.map_or_else(|e| Some(e.to_string()), |_| None);
|
||||||
} else {
|
} else {
|
||||||
unicode = s.clone();
|
unicode = s.to_owned();
|
||||||
decoder_error = None;
|
decoder_error = None;
|
||||||
};
|
};
|
||||||
let (idn, encoder_error) = match idna::domain_to_ascii(s) {
|
let (idn, encoder_error) = match idna::domain_to_ascii(s) {
|
||||||
Ok(idn) => {
|
Ok(idn) => {
|
||||||
if &idn != s || original_was == NameType::IDN {
|
if idn != s || original_was == NameType::Idn {
|
||||||
(Some(idn), None)
|
(Some(idn), None)
|
||||||
} else {
|
} else {
|
||||||
original_was = NameType::Ascii;
|
original_was = NameType::Ascii;
|
||||||
|
@ -28,6 +28,7 @@ pub enum AddressScope {
|
|||||||
Loopback,
|
Loopback,
|
||||||
Reserved,
|
Reserved,
|
||||||
Documentation,
|
Documentation,
|
||||||
|
Nat64,
|
||||||
#[default]
|
#[default]
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
@ -78,6 +79,9 @@ impl AddressInfo {
|
|||||||
// Test for the documentation address 2001:db8::/32
|
// Test for the documentation address 2001:db8::/32
|
||||||
} else if segments[0]==0x2001 && segments[1]==0x0db8 && segments[2]==0 && segments[3]==0 {
|
} else if segments[0]==0x2001 && segments[1]==0x0db8 && segments[2]==0 && segments[3]==0 {
|
||||||
address_scope = AddressScope::Documentation;
|
address_scope = AddressScope::Documentation;
|
||||||
|
// Test for NAT64 address 64:ff9b::/96
|
||||||
|
} else if segments[0]==0x64 && segments[1]==0xff9b {
|
||||||
|
address_scope = AddressScope::Nat64;
|
||||||
// Test for multicase scope
|
// Test for multicase scope
|
||||||
} else if addr.is_multicast() {
|
} else if addr.is_multicast() {
|
||||||
address_cast = AddressCast::Multicast;
|
address_cast = AddressCast::Multicast;
|
||||||
@ -111,6 +115,7 @@ impl AddressInfo {
|
|||||||
scope: address_scope
|
scope: address_scope
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
443
src/main.rs
443
src/main.rs
@ -1,29 +1,35 @@
|
|||||||
|
|
||||||
|
#![allow(clippy::redundant_field_names)]
|
||||||
|
#![allow(clippy::needless_return)]
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
|
body::Body,
|
||||||
extract::{
|
extract::{
|
||||||
self,
|
self,
|
||||||
Query,
|
Query,
|
||||||
State,
|
State,
|
||||||
Extension,
|
Extension,
|
||||||
},
|
},
|
||||||
headers,
|
|
||||||
http::Request,
|
|
||||||
handler::Handler,
|
handler::Handler,
|
||||||
|
http::Request,
|
||||||
middleware::{self, Next},
|
middleware::{self, Next},
|
||||||
response::Response,
|
response::Response,
|
||||||
Router,
|
Router,
|
||||||
routing::get,
|
routing::get,
|
||||||
TypedHeader,
|
|
||||||
};
|
};
|
||||||
use axum_client_ip::SecureClientIp;
|
use axum_client_ip::SecureClientIp;
|
||||||
|
use axum_extra::headers;
|
||||||
|
use axum_extra::TypedHeader;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use lazy_static::lazy_static;
|
use env_logger::Env;
|
||||||
|
use hickory_resolver::{name_server::TokioConnectionProvider, system_conf::read_system_conf, Name, ResolveError, Resolver};
|
||||||
|
use hickory_resolver::TokioResolver;
|
||||||
|
use log::{info,warn,error};
|
||||||
|
use nat64::resolve_nat64_address;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize,Serialize};
|
use serde::{Deserialize,Serialize};
|
||||||
use tera::Tera;
|
|
||||||
use tower::ServiceBuilder;
|
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use trust_dns_resolver::Name;
|
use tower::ServiceBuilder;
|
||||||
use trust_dns_resolver::TokioAsyncResolver;
|
|
||||||
|
|
||||||
use tokio::signal::unix::{
|
use tokio::signal::unix::{
|
||||||
signal,
|
signal,
|
||||||
@ -32,18 +38,24 @@ use tokio::signal::unix::{
|
|||||||
use tokio::task;
|
use tokio::task;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
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 config;
|
||||||
mod geoip;
|
mod geoip;
|
||||||
|
mod idna;
|
||||||
mod ipinfo;
|
mod ipinfo;
|
||||||
|
mod nat64;
|
||||||
mod ratelimit;
|
mod ratelimit;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod simple_dns;
|
mod simple_dns;
|
||||||
mod templating_engine;
|
mod view;
|
||||||
mod idna;
|
|
||||||
|
|
||||||
use crate::geoip::{
|
use crate::geoip::{
|
||||||
QueryAsn,
|
QueryAsn,
|
||||||
@ -54,14 +66,17 @@ use crate::geoip::{
|
|||||||
use crate::idna::IdnaName;
|
use crate::idna::IdnaName;
|
||||||
use crate::simple_dns::DnsLookupResult;
|
use crate::simple_dns::DnsLookupResult;
|
||||||
use crate::settings::*;
|
use crate::settings::*;
|
||||||
use crate::templating_engine::View;
|
use crate::view::View;
|
||||||
use crate::ipinfo::{AddressCast,AddressInfo,AddressScope};
|
use crate::ipinfo::{AddressInfo,AddressScope};
|
||||||
|
|
||||||
|
type TemplatingEngine = HumusEngine<View,QuerySettings,ResponseFormat>;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct SettingsQuery {
|
pub struct SettingsQuery {
|
||||||
format: Option<ResponseFormat>,
|
format: Option<ResponseFormat>,
|
||||||
lang: Option<String>,
|
lang: Option<String>,
|
||||||
dns: Option<String>,
|
dns: Option<String>,
|
||||||
|
dns_self_lookup: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
@ -69,33 +84,54 @@ pub struct SearchQuery {
|
|||||||
query: Option<String>,
|
query: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Enumerates possible mapping strategies
|
||||||
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
|
#[serde(rename_all="snake_case")]
|
||||||
|
pub enum IpMappingStrategy {
|
||||||
|
/// See: https://en.wikipedia.org/wiki/NAT64
|
||||||
|
Nat64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct IpMapping {
|
||||||
|
strategy: IpMappingStrategy,
|
||||||
|
from_address: IpAddr,
|
||||||
|
to_address: IpAddr,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
pub struct IpResult {
|
pub struct IpResult {
|
||||||
|
/// When the mapping is set the queried for address
|
||||||
|
/// was automtically replaced with the mapped to address.
|
||||||
|
mapping: Option<IpMapping>,
|
||||||
|
/// The address that was queried for or the mapping resulted in.
|
||||||
address: IpAddr,
|
address: IpAddr,
|
||||||
hostname: Option<String>,
|
hostname: Option<String>,
|
||||||
asn: Option<AsnResult>,
|
asn: Option<AsnResult>,
|
||||||
location: Option<LocationResult>,
|
location: Option<LocationResult>,
|
||||||
ip_info: AddressInfo,
|
ip_info: AddressInfo,
|
||||||
used_dns_resolver: Option<String>,
|
used_dns_resolver: Option<Arc<str>>,
|
||||||
|
reverse_dns_disabled_for_privacy: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need this one to hide the partial lookup field when irelevant
|
// We need this one to hide the partial lookup field when irelevant
|
||||||
pub fn not(b: &bool) -> bool { !b }
|
pub fn not(b: &bool) -> bool { !b }
|
||||||
|
|
||||||
#[derive(Serialize, Default, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
pub struct DigResult {
|
pub struct DigResult {
|
||||||
records: simple_dns::DnsLookupResult,
|
records: simple_dns::DnsLookupResult,
|
||||||
#[serde(skip_serializing_if = "IdnaName::was_ascii")]
|
#[serde(skip_serializing_if = "IdnaName::was_ascii")]
|
||||||
idn: IdnaName,
|
idn: IdnaName,
|
||||||
#[serde(skip_serializing_if = "not")]
|
#[serde(skip_serializing_if = "not")]
|
||||||
partial_lookup: bool,
|
partial_lookup: bool,
|
||||||
used_dns_resolver: String,
|
used_dns_resolver: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ServiceSharedState {
|
struct ServiceSharedState {
|
||||||
templating_engine: templating_engine::Engine,
|
templating_engine: TemplatingEngine,
|
||||||
dns_resolvers: HashMap<String,TokioAsyncResolver>,
|
dns_resolvers: HashMap<Arc<str>,TokioResolver>,
|
||||||
dns_resolver_aliases: HashMap<String,String>,
|
dns_resolver_aliases: HashMap<Arc<str>,Arc<str>>,
|
||||||
asn_db: geoip::MMDBCarrier,
|
asn_db: geoip::MMDBCarrier,
|
||||||
location_db: geoip::MMDBCarrier,
|
location_db: geoip::MMDBCarrier,
|
||||||
config: config::EchoIpServiceConfig,
|
config: config::EchoIpServiceConfig,
|
||||||
@ -105,7 +141,7 @@ struct ServiceSharedState {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct DerivedConfiguration {
|
struct DerivedConfiguration {
|
||||||
dns_resolver_selectables: Vec<Selectable>,
|
dns_resolver_selectables: Vec<Selectable>,
|
||||||
default_resolver: String,
|
default_resolver: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@ -114,7 +150,7 @@ struct CliArgs {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
config: Option<String>,
|
config: Option<String>,
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
listen_on: Option<String>,
|
listen_on: Option<SocketAddr>,
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
template_location: Option<String>,
|
template_location: Option<String>,
|
||||||
#[arg(short,long)]
|
#[arg(short,long)]
|
||||||
@ -123,7 +159,7 @@ struct CliArgs {
|
|||||||
static_location: Option<String>,
|
static_location: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_domain_hidden_list(domain: &String, hidden_list: &Vec<String>) -> bool {
|
fn match_domain_hidden_list(domain: &str, hidden_list: &Vec<String>) -> bool {
|
||||||
let name = domain.trim_end_matches(".");
|
let name = domain.trim_end_matches(".");
|
||||||
for suffix in hidden_list {
|
for suffix in hidden_list {
|
||||||
if name.ends_with(suffix) {
|
if name.ends_with(suffix) {
|
||||||
@ -133,25 +169,12 @@ fn match_domain_hidden_list(domain: &String, hidden_list: &Vec<String>) -> bool
|
|||||||
return false;
|
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
|
||||||
|
// Initalize logger:
|
||||||
|
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||||
|
|
||||||
// Parse Command line arguments
|
// Parse Command line arguments
|
||||||
let cli_args = CliArgs::parse();
|
let cli_args = CliArgs::parse();
|
||||||
|
|
||||||
@ -159,9 +182,11 @@ async fn main() {
|
|||||||
let config: config::EchoIpServiceConfig = match cli_args.config {
|
let config: config::EchoIpServiceConfig = match cli_args.config {
|
||||||
Some(config_path) => {
|
Some(config_path) => {
|
||||||
match read_toml_from_file::<config::EchoIpServiceConfig>(&config_path) {
|
match read_toml_from_file::<config::EchoIpServiceConfig>(&config_path) {
|
||||||
Some(c) => c,
|
Ok(c) => c,
|
||||||
None => {
|
Err(e) => {
|
||||||
println!("Could not read confuration file, exiting.");
|
error!("Could not read confuration file!");
|
||||||
|
error!("{e}");
|
||||||
|
error!("Exiting ...");
|
||||||
::std::process::exit(1);
|
::std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,49 +195,27 @@ async fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Initalize Tera templates
|
// Initalize Tera templates
|
||||||
let mut template_base_dir = match cli_args.template_location {
|
let template_loader = TemplateEngineLoader::new(
|
||||||
Some(template_base_dir) => template_base_dir,
|
config.template.template_location.clone(),
|
||||||
None => (&config.template.template_location).to_owned(),
|
config.template.extra_config.clone()
|
||||||
};
|
)
|
||||||
if !template_base_dir.ends_with("/") {
|
.cli_template_location(cli_args.template_location)
|
||||||
template_base_dir = template_base_dir + "/";
|
.cli_extra_config_location(cli_args.extra_config);
|
||||||
}
|
|
||||||
let template_extra_config = match &cli_args.extra_config {
|
|
||||||
Some(path) => read_toml_from_file(path),
|
let templating_engine = match template_loader.load_templates() {
|
||||||
None => match &config.template.extra_config {
|
Ok(t) => t.into(),
|
||||||
Some(path) => read_toml_from_file(path),
|
|
||||||
None => {
|
|
||||||
println!("Trying to read default template configuration ...");
|
|
||||||
println!("(If this fails that may be ok, depending on your template)");
|
|
||||||
read_toml_from_file(&(template_base_dir.clone()+"extra.toml"))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let template_glob = template_base_dir.clone()+"*";
|
|
||||||
println!("Parsing Templates from '{}' ...", &template_glob);
|
|
||||||
let res = Tera::new((template_glob).as_str());
|
|
||||||
let tera = match res {
|
|
||||||
Ok(t) => t,
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Template parsing error(s): {}", e);
|
error!("{e}");
|
||||||
::std::process::exit(1);
|
::std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let templating_engine = templating_engine::Engine{
|
|
||||||
tera: tera,
|
|
||||||
template_config: template_extra_config,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Static file directory
|
// Static file directory
|
||||||
|
|
||||||
let static_file_directory = cli_args.static_location.unwrap_or(
|
let static_file_directory = template_loader.base_dir()+"/static";
|
||||||
config.server.static_location.clone().unwrap_or(
|
|
||||||
template_base_dir+"/static"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
println!("Static files will be served from: {static_file_directory}");
|
info!("Static files will be served from: {static_file_directory}");
|
||||||
|
|
||||||
// Initalize GeoIP Database
|
// Initalize GeoIP Database
|
||||||
|
|
||||||
@ -231,24 +234,27 @@ async fn main() {
|
|||||||
location_db.reload_database().ok();
|
location_db.reload_database().ok();
|
||||||
|
|
||||||
// Initalize DNS resolver with os defaults
|
// Initalize DNS resolver with os defaults
|
||||||
println!("Initalizing dns resolvers ...");
|
info!("Initalizing dns resolvers ...");
|
||||||
|
|
||||||
let mut dns_resolver_selectables = Vec::<Selectable>::new();
|
let mut dns_resolver_selectables = Vec::<Selectable>::new();
|
||||||
let mut dns_resolver_map: HashMap<String,TokioAsyncResolver> = HashMap::new();
|
let mut dns_resolver_map: HashMap<Arc<str>,TokioResolver> = HashMap::new();
|
||||||
let mut dns_resolver_aliases: HashMap<String,String> = HashMap::new();
|
let mut dns_resolver_aliases: HashMap<Arc<str>,Arc<str>> = HashMap::new();
|
||||||
|
|
||||||
if config.dns.enable_system_resolver {
|
if config.dns.enable_system_resolver {
|
||||||
println!("Initalizing System resolver ...");
|
info!("Initalizing System resolver ...");
|
||||||
let res = TokioAsyncResolver::tokio_from_system_conf();
|
match initalize_system_resolver() {
|
||||||
let resolver = match res {
|
Ok(resolver) => {
|
||||||
Ok(resolver) => resolver,
|
info!("System resolver successfully Initalized.");
|
||||||
|
dns_resolver_map.insert(
|
||||||
|
config.dns.system_resolver_id.clone(),
|
||||||
|
resolver
|
||||||
|
);
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error while setting up dns resolver: {e}");
|
error!("Problem setting up system resolver: {e}");
|
||||||
::std::process::exit(1);
|
::std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
dns_resolver_map.insert(config.dns.system_resolver_id.clone(), resolver);
|
|
||||||
dns_resolver_selectables.push(Selectable {
|
dns_resolver_selectables.push(Selectable {
|
||||||
id: config.dns.system_resolver_id.clone(),
|
id: config.dns.system_resolver_id.clone(),
|
||||||
name: config.dns.system_resolver_name.clone(),
|
name: config.dns.system_resolver_name.clone(),
|
||||||
@ -257,11 +263,11 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (key, resolver_config) in &config.dns.resolver {
|
for (key, resolver_config) in &config.dns.resolver {
|
||||||
println!("Initalizing {} resolver ...", key);
|
info!("Initalizing {} resolver ...", key);
|
||||||
let resolver = TokioAsyncResolver::tokio(
|
let resolver = TokioResolver::builder_with_config(
|
||||||
resolver_config.to_trust_resolver_config(),
|
resolver_config.to_hickory_resolver_config(),
|
||||||
Default::default()
|
Default::default()
|
||||||
).unwrap();
|
).build();
|
||||||
dns_resolver_map.insert(key.clone(), resolver);
|
dns_resolver_map.insert(key.clone(), resolver);
|
||||||
dns_resolver_selectables.push(Selectable {
|
dns_resolver_selectables.push(Selectable {
|
||||||
id: key.clone(),
|
id: key.clone(),
|
||||||
@ -273,7 +279,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();
|
let ip_header = config.server.ip_header.clone();
|
||||||
|
|
||||||
// Initialize shared state
|
// Initialize shared state
|
||||||
@ -288,9 +294,9 @@ async fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
dns_resolver_selectables.sort_by(|a,b| b.weight.cmp(&a.weight));
|
dns_resolver_selectables.sort_by(|a,b| b.weight.cmp(&a.weight));
|
||||||
let default_resolver = dns_resolver_selectables.get(0)
|
let default_resolver = dns_resolver_selectables.first()
|
||||||
.map(|s| s.id.clone() )
|
.map(|s| s.id.clone() )
|
||||||
.unwrap_or("none".to_string());
|
.unwrap_or("none".into());
|
||||||
let derived_config = DerivedConfiguration {
|
let derived_config = DerivedConfiguration {
|
||||||
dns_resolver_selectables: dns_resolver_selectables,
|
dns_resolver_selectables: dns_resolver_selectables,
|
||||||
default_resolver: default_resolver,
|
default_resolver: default_resolver,
|
||||||
@ -299,18 +305,18 @@ async fn main() {
|
|||||||
let signal_usr1_handlers_state = shared_state.clone();
|
let signal_usr1_handlers_state = shared_state.clone();
|
||||||
|
|
||||||
task::spawn(async move {
|
task::spawn(async move {
|
||||||
println!("Trying to register USR1 signal for reloading geoip databases");
|
info!("Trying to register USR1 signal for reloading geoip databases");
|
||||||
let mut signal_stream = match signal(SignalKind::user_defined1()) {
|
let mut signal_stream = match signal(SignalKind::user_defined1()) {
|
||||||
Ok(signal_stream) => signal_stream,
|
Ok(signal_stream) => signal_stream,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error while registring signal handler: {e}");
|
error!("Error while registring signal handler: {e}");
|
||||||
println!("Continuing without ...");
|
warn!("Continuing without geoip reaload signal ...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loop {
|
loop {
|
||||||
if None == signal_stream.recv().await { return; }
|
if signal_stream.recv().await.is_none() { return; }
|
||||||
println!("Received signal USR1, reloading geoip databses!");
|
info!("Received signal USR1, reloading geoip databses!");
|
||||||
signal_usr1_handlers_state.location_db.reload_database().ok();
|
signal_usr1_handlers_state.location_db.reload_database().ok();
|
||||||
signal_usr1_handlers_state.asn_db.reload_database().ok();
|
signal_usr1_handlers_state.asn_db.reload_database().ok();
|
||||||
}
|
}
|
||||||
@ -319,9 +325,9 @@ async fn main() {
|
|||||||
// Initalize axum server
|
// Initalize axum server
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(handle_default_route))
|
.route("/", get(handle_default_route))
|
||||||
.route("/dig/:name", get(handle_dig_route_with_path))
|
.route("/dig/{name}", get(handle_dig_route_with_path))
|
||||||
.route("/ip/:address", get(handle_ip_route_with_path))
|
.route("/ip/{address}", get(handle_ip_route_with_path))
|
||||||
.route("/dns_resolver/:resolver", get(handle_dns_resolver_route_with_path))
|
.route("/dns_resolver/{resolver}", get(handle_dns_resolver_route_with_path))
|
||||||
.route("/dns_resolver", get(handle_dns_resolver_route))
|
.route("/dns_resolver", get(handle_dns_resolver_route))
|
||||||
.route("/ua", get(user_agent_handler))
|
.route("/ua", get(user_agent_handler))
|
||||||
.route("/hi", get(hello_world_handler))
|
.route("/hi", get(hello_world_handler))
|
||||||
@ -329,7 +335,7 @@ async fn main() {
|
|||||||
ServeDir::new(static_file_directory)
|
ServeDir::new(static_file_directory)
|
||||||
.fallback(not_found_handler.with_state(shared_state.clone()))
|
.fallback(not_found_handler.with_state(shared_state.clone()))
|
||||||
)
|
)
|
||||||
.with_state(shared_state)
|
.with_state(shared_state.clone())
|
||||||
.layer(
|
.layer(
|
||||||
ServiceBuilder::new()
|
ServiceBuilder::new()
|
||||||
.layer(ip_header.into_extension())
|
.layer(ip_header.into_extension())
|
||||||
@ -338,38 +344,61 @@ async fn main() {
|
|||||||
.layer(middleware::from_fn(ratelimit::rate_limit_middleware))
|
.layer(middleware::from_fn(ratelimit::rate_limit_middleware))
|
||||||
.layer(Extension(config))
|
.layer(Extension(config))
|
||||||
.layer(Extension(derived_config))
|
.layer(Extension(derived_config))
|
||||||
.layer(middleware::from_fn(settings_query_middleware))
|
.layer(middleware::from_fn_with_state(shared_state, settings_query_middleware))
|
||||||
)
|
)
|
||||||
;
|
;
|
||||||
|
|
||||||
println!("Starting Server on {} ...",listen_on);
|
info!("Starting Server on {} ...",listen_on);
|
||||||
|
|
||||||
axum::Server::bind(&listen_on)
|
let listener = tokio::net::TcpListener::bind(&listen_on).await.unwrap();
|
||||||
.serve(app.into_make_service_with_connect_info::<std::net::SocketAddr>())
|
axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn initalize_system_resolver() -> Result<TokioResolver, ResolveError> {
|
||||||
|
let (system_conf, system_options) = read_system_conf()?;
|
||||||
|
let mut builder = Resolver::builder_with_config(
|
||||||
|
system_conf,
|
||||||
|
TokioConnectionProvider::default()
|
||||||
|
);
|
||||||
|
*builder.options_mut() = system_options;
|
||||||
|
|
||||||
|
return Ok(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
async fn settings_query_middleware<B>(
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn settings_query_middleware(
|
||||||
Query(query): Query<SettingsQuery>,
|
Query(query): Query<SettingsQuery>,
|
||||||
|
State(arc_state): State<Arc<ServiceSharedState>>,
|
||||||
Extension(config): Extension<config::EchoIpServiceConfig>,
|
Extension(config): Extension<config::EchoIpServiceConfig>,
|
||||||
Extension(derived_config): Extension<DerivedConfiguration>,
|
Extension(derived_config): Extension<DerivedConfiguration>,
|
||||||
cookie_header: Option<TypedHeader<headers::Cookie>>,
|
cookie_header: Option<TypedHeader<headers::Cookie>>,
|
||||||
user_agent_header: Option<TypedHeader<headers::UserAgent>>,
|
user_agent_header: Option<TypedHeader<headers::UserAgent>>,
|
||||||
mut req: Request<B>,
|
mut req: Request<Body>,
|
||||||
next: Next<B>
|
next: Next,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
let state = Arc::clone(&arc_state);
|
||||||
let mut format = query.format;
|
let mut format = query.format;
|
||||||
let mut dns_resolver_id = derived_config.default_resolver;
|
|
||||||
|
let mut dns_resolver_id = derived_config.default_resolver.clone();
|
||||||
|
let mut test_for_resolver = false;
|
||||||
|
|
||||||
if let Some(resolver_id) = query.dns {
|
if let Some(resolver_id) = query.dns {
|
||||||
dns_resolver_id = resolver_id;
|
dns_resolver_id = resolver_id.into();
|
||||||
|
test_for_resolver = true;
|
||||||
} else if let Some(cookie_header) = cookie_header {
|
} else if let Some(cookie_header) = cookie_header {
|
||||||
if let Some(resolver_id) = cookie_header.0.get("dns_resolver") {
|
if let Some(resolver_id) = cookie_header.0.get("dns_resolver") {
|
||||||
dns_resolver_id = resolver_id.to_string();
|
dns_resolver_id = resolver_id.into();
|
||||||
|
test_for_resolver = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Falls back to the default resolver if an invalid resolver id ws requested.
|
||||||
|
// This may be the case for bookmarked links or old cookies of a resolver was removed.
|
||||||
|
if test_for_resolver && !state.dns_resolvers.contains_key(&dns_resolver_id) {
|
||||||
|
dns_resolver_id = derived_config.default_resolver;
|
||||||
|
}
|
||||||
|
|
||||||
// Try to guess type from user agent
|
// Try to guess type from user agent
|
||||||
if format.is_none() {
|
if format.is_none() {
|
||||||
@ -377,7 +406,7 @@ async fn settings_query_middleware<B>(
|
|||||||
let ua = user_agent.as_str();
|
let ua = user_agent.as_str();
|
||||||
for tua in config.template.text_user_agents {
|
for tua in config.template.text_user_agents {
|
||||||
if ua.starts_with(&tua) {
|
if ua.starts_with(&tua) {
|
||||||
format = Some(ResponseFormat::TextPlain);
|
format = Some(ResponseFormat::Text);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -385,10 +414,11 @@ async fn settings_query_middleware<B>(
|
|||||||
}
|
}
|
||||||
// Add the request settings extension
|
// Add the request settings extension
|
||||||
req.extensions_mut().insert(QuerySettings{
|
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()),
|
lang: query.lang.unwrap_or("en".to_string()),
|
||||||
available_dns_resolvers: derived_config.dns_resolver_selectables,
|
available_dns_resolvers: derived_config.dns_resolver_selectables,
|
||||||
dns_resolver_id: dns_resolver_id,
|
dns_resolver_id: dns_resolver_id,
|
||||||
|
dns_disable_self_lookup: !query.dns_self_lookup.unwrap_or(false),
|
||||||
});
|
});
|
||||||
next.run(req).await
|
next.run(req).await
|
||||||
}
|
}
|
||||||
@ -401,8 +431,8 @@ async fn not_found_handler(
|
|||||||
|
|
||||||
state.templating_engine.render_view(
|
state.templating_engine.render_view(
|
||||||
&settings,
|
&settings,
|
||||||
&View::NotFound,
|
View::NotFound,
|
||||||
).await
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn hello_world_handler(
|
async fn hello_world_handler(
|
||||||
@ -413,11 +443,11 @@ async fn hello_world_handler(
|
|||||||
|
|
||||||
state.templating_engine.render_view(
|
state.templating_engine.render_view(
|
||||||
&settings,
|
&settings,
|
||||||
&View::Message{
|
View::Message{
|
||||||
title: "Hey There!".to_string(),
|
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 +462,7 @@ async fn handle_default_route(
|
|||||||
State(arc_state): State<Arc<ServiceSharedState>>,
|
State(arc_state): State<Arc<ServiceSharedState>>,
|
||||||
Extension(settings): Extension<QuerySettings>,
|
Extension(settings): Extension<QuerySettings>,
|
||||||
user_agent_header: Option<TypedHeader<headers::UserAgent>>,
|
user_agent_header: Option<TypedHeader<headers::UserAgent>>,
|
||||||
SecureClientIp(address): SecureClientIp
|
SecureClientIp(client_ip): SecureClientIp
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
|
||||||
let state = Arc::clone(&arc_state);
|
let state = Arc::clone(&arc_state);
|
||||||
@ -443,61 +473,65 @@ async fn handle_default_route(
|
|||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
settings,
|
settings,
|
||||||
state
|
state,
|
||||||
|
&client_ip
|
||||||
).await;
|
).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = get_ip_result(&address, &settings.lang, &"default".to_string(), &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 {
|
let user_agent: Option<String> = user_agent_header
|
||||||
Some(TypedHeader(user_agent)) => Some(user_agent.to_string()),
|
.map(|TypedHeader(user_agent)| user_agent.to_string());
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
state.templating_engine.render_view(
|
state.templating_engine.render_view(
|
||||||
&settings,
|
&settings,
|
||||||
&View::Index{
|
View::Index{
|
||||||
result: result,
|
result: result,
|
||||||
user_agent: user_agent,
|
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(
|
async fn handle_search_request(
|
||||||
search_query: String,
|
search_query: String,
|
||||||
this_should_have_been_an_ip: bool,
|
this_should_have_been_an_ip: bool,
|
||||||
settings: QuerySettings,
|
settings: QuerySettings,
|
||||||
arc_state: Arc<ServiceSharedState>,
|
arc_state: Arc<ServiceSharedState>,
|
||||||
|
client_ip: &IpAddr,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
|
||||||
let mut search_query = search_query.trim().to_string();
|
let mut search_query = search_query.trim().to_string();
|
||||||
let mut settings = settings;
|
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 someone asked for an asn, give an asn answer
|
||||||
if let Some(asn_cap) = ASN_REGEX.captures(&search_query) {
|
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()) {
|
if let Some(asn) = asn_cap.get(1).and_then(|m| m.as_str().parse::<u32>().ok()) {
|
||||||
// Render a dummy template that can at least link to other pages
|
// Render a dummy template that can at least link to other pages
|
||||||
let state = Arc::clone(&arc_state);
|
let state = Arc::clone(&arc_state);
|
||||||
return state.templating_engine.render_view(
|
return state.templating_engine.render_view(
|
||||||
&settings,
|
&settings,
|
||||||
&View::Asn{asn: asn},
|
View::Asn{asn: asn},
|
||||||
).await
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(via_cap) = VIA_REGEX.captures(&search_query) {
|
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);
|
let state = Arc::clone(&arc_state);
|
||||||
if state.dns_resolvers.contains_key(&via) {
|
if state.dns_resolvers.contains_key(via.as_str()) {
|
||||||
settings.dns_resolver_id = via;
|
settings.dns_resolver_id = via.as_str().into();
|
||||||
} else if let Some(alias) = state.dns_resolver_aliases.get(&via) {
|
} else if let Some(alias) = state.dns_resolver_aliases.get(via.as_str()) {
|
||||||
settings.dns_resolver_id = alias.clone();
|
settings.dns_resolver_id = alias.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -506,7 +540,7 @@ async fn handle_search_request(
|
|||||||
|
|
||||||
// Try to interpret as an IP-Address
|
// Try to interpret as an IP-Address
|
||||||
if let Ok(address) = search_query.parse() {
|
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
|
// Fall back to treating it as a hostname
|
||||||
@ -526,8 +560,8 @@ async fn handle_dns_resolver_route(
|
|||||||
let state = Arc::clone(&arc_state);
|
let state = Arc::clone(&arc_state);
|
||||||
state.templating_engine.render_view(
|
state.templating_engine.render_view(
|
||||||
&settings,
|
&settings,
|
||||||
&View::DnsResolverList,
|
View::DnsResolverList,
|
||||||
).await
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -537,16 +571,16 @@ async fn handle_dns_resolver_route_with_path(
|
|||||||
extract::Path(query): extract::Path<String>,
|
extract::Path(query): extract::Path<String>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let state = Arc::clone(&arc_state);
|
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(
|
state.templating_engine.render_view(
|
||||||
&settings,
|
&settings,
|
||||||
&View::DnsResolver{ config: resolver.clone() },
|
View::DnsResolver{ config: resolver.clone() },
|
||||||
).await
|
)
|
||||||
} else {
|
} else {
|
||||||
state.templating_engine.render_view(
|
state.templating_engine.render_view(
|
||||||
&settings,
|
&settings,
|
||||||
&View::NotFound,
|
View::NotFound,
|
||||||
).await
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,11 +588,12 @@ async fn handle_ip_route_with_path(
|
|||||||
Extension(settings): Extension<QuerySettings>,
|
Extension(settings): Extension<QuerySettings>,
|
||||||
State(arc_state): State<Arc<ServiceSharedState>>,
|
State(arc_state): State<Arc<ServiceSharedState>>,
|
||||||
extract::Path(query): extract::Path<String>,
|
extract::Path(query): extract::Path<String>,
|
||||||
|
SecureClientIp(client_ip): SecureClientIp
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Ok(address) = query.parse() {
|
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 {
|
} 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 +601,7 @@ async fn handle_ip_request(
|
|||||||
address: IpAddr,
|
address: IpAddr,
|
||||||
settings: QuerySettings,
|
settings: QuerySettings,
|
||||||
arc_state: Arc<ServiceSharedState>,
|
arc_state: Arc<ServiceSharedState>,
|
||||||
|
client_ip: &IpAddr,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
|
||||||
let state = Arc::clone(&arc_state);
|
let state = Arc::clone(&arc_state);
|
||||||
@ -573,42 +609,75 @@ async fn handle_ip_request(
|
|||||||
&address,
|
&address,
|
||||||
&settings.lang,
|
&settings.lang,
|
||||||
&settings.dns_resolver_id,
|
&settings.dns_resolver_id,
|
||||||
|
settings.dns_disable_self_lookup,
|
||||||
|
client_ip,
|
||||||
&state).await;
|
&state).await;
|
||||||
|
|
||||||
state.templating_engine.render_view(
|
state.templating_engine.render_view(
|
||||||
&settings,
|
&settings,
|
||||||
&View::Ip{result: result}
|
View::Ip{result: result}
|
||||||
).await
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ip_mapping(address: &IpAddr) -> Option<IpMapping> {
|
||||||
|
if let IpAddr::V6(v6_address) = address {
|
||||||
|
if let Some(nat64_result) = resolve_nat64_address(*v6_address) {
|
||||||
|
return Some(IpMapping {
|
||||||
|
from_address: *address,
|
||||||
|
to_address: IpAddr::V4(nat64_result),
|
||||||
|
strategy: IpMappingStrategy::Nat64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_ip_result(
|
async fn get_ip_result(
|
||||||
address: &IpAddr,
|
address: &IpAddr,
|
||||||
lang: &String,
|
lang: &str,
|
||||||
dns_resolver_name: &String,
|
dns_resolver_name: &Arc<str>,
|
||||||
|
dns_disable_self_lookup: bool,
|
||||||
|
client_ip: &IpAddr,
|
||||||
state: &ServiceSharedState,
|
state: &ServiceSharedState,
|
||||||
) -> IpResult {
|
) -> IpResult {
|
||||||
|
|
||||||
let ip_info = AddressInfo::new(&address);
|
let mapping = get_ip_mapping(address);
|
||||||
|
let original_address = address;
|
||||||
|
let address = &mapping.clone().map_or(*original_address, |m| m.to_address);
|
||||||
|
|
||||||
if !(ip_info.scope == AddressScope::Global || ip_info.scope == AddressScope::Shared) || ip_info.cast != AddressCast::Unicast {
|
let mut reverse_dns_disabled_for_privacy = false;
|
||||||
if !((ip_info.scope == AddressScope::Private || ip_info.scope == AddressScope::LinkLocal) && state.config.server.allow_private_ip_lookup) {
|
|
||||||
return IpResult {
|
if state.config.dns.allow_reverse_lookup &&
|
||||||
address: *address,
|
(address == client_ip || original_address == client_ip) &&
|
||||||
hostname: None,
|
dns_disable_self_lookup
|
||||||
asn: None,
|
{
|
||||||
location: None,
|
reverse_dns_disabled_for_privacy = true;
|
||||||
ip_info: ip_info,
|
}
|
||||||
used_dns_resolver: None,
|
|
||||||
}
|
let ip_info = AddressInfo::new(address);
|
||||||
|
|
||||||
|
// Return dummy result if:
|
||||||
|
//
|
||||||
|
// The address falls into a private range and lookup of private addresses is not allowed.
|
||||||
|
if (!state.config.server.allow_private_ip_lookup) && (ip_info.scope == AddressScope::Private || ip_info.scope == AddressScope::LinkLocal) {
|
||||||
|
return IpResult {
|
||||||
|
mapping: mapping,
|
||||||
|
address: *address,
|
||||||
|
hostname: None,
|
||||||
|
asn: None,
|
||||||
|
location: None,
|
||||||
|
ip_info: ip_info,
|
||||||
|
used_dns_resolver: None,
|
||||||
|
reverse_dns_disabled_for_privacy: reverse_dns_disabled_for_privacy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// do reverse lookup
|
// do reverse lookup
|
||||||
let mut hostname: Option<String> = None;
|
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 state.config.dns.allow_reverse_lookup && !reverse_dns_disabled_for_privacy {
|
||||||
if let Some(dns_resolver) = &state.dns_resolvers.get(dns_resolver_name) {
|
if let Some(dns_resolver) = &state.dns_resolvers.get(dns_resolver_name) {
|
||||||
hostname = simple_dns::reverse_lookup(&dns_resolver, &address).await;
|
hostname = simple_dns::reverse_lookup(dns_resolver, address).await;
|
||||||
used_dns_resolver = Some(dns_resolver_name.clone());
|
used_dns_resolver = Some(dns_resolver_name.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -619,24 +688,26 @@ async fn get_ip_result(
|
|||||||
// location lookup
|
// location lookup
|
||||||
let location_result = state.location_db.query_location_for_ip(
|
let location_result = state.location_db.query_location_for_ip(
|
||||||
address,
|
address,
|
||||||
&vec![lang, &"en".to_string()]
|
&[lang, "en"]
|
||||||
);
|
);
|
||||||
|
|
||||||
// filter reverse lookup
|
// filter reverse lookup
|
||||||
if let Some(name) = &hostname {
|
if let Some(name) = &hostname {
|
||||||
if match_domain_hidden_list(&name, &state.config.dns.hidden_suffixes) {
|
if match_domain_hidden_list(name, &state.config.dns.hidden_suffixes) {
|
||||||
hostname = None;
|
hostname = None;
|
||||||
used_dns_resolver = None;
|
used_dns_resolver = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IpResult{
|
IpResult{
|
||||||
|
mapping: mapping,
|
||||||
address: *address,
|
address: *address,
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
asn: asn_result,
|
asn: asn_result,
|
||||||
location: location_result,
|
location: location_result,
|
||||||
ip_info: ip_info,
|
ip_info: ip_info,
|
||||||
used_dns_resolver: used_dns_resolver,
|
used_dns_resolver: used_dns_resolver,
|
||||||
|
reverse_dns_disabled_for_privacy: reverse_dns_disabled_for_privacy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -666,27 +737,27 @@ async fn handle_dig_request(
|
|||||||
|
|
||||||
state.templating_engine.render_view(
|
state.templating_engine.render_view(
|
||||||
&settings,
|
&settings,
|
||||||
&View::Dig{ query: dig_query, result: dig_result}
|
View::Dig{ query: dig_query, result: dig_result}
|
||||||
).await
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_dig_result(
|
async fn get_dig_result(
|
||||||
dig_query: &String,
|
dig_query: &str,
|
||||||
dns_resolver_name: &String,
|
dns_resolver_name: &Arc<str>,
|
||||||
state: &ServiceSharedState,
|
state: &ServiceSharedState,
|
||||||
do_full_lookup: bool,
|
do_full_lookup: bool,
|
||||||
) -> DigResult {
|
) -> DigResult {
|
||||||
let name = &dig_query.trim().trim_end_matches(".").to_string();
|
let name = &dig_query.trim().trim_end_matches(".").to_string();
|
||||||
let idna_name = IdnaName::from_string(&name);
|
let idna_name = IdnaName::from_str(name);
|
||||||
if let Some(dns_resolver) = &state.dns_resolvers.get(dns_resolver_name) {
|
if let Some(dns_resolver) = state.dns_resolvers.get(dns_resolver_name) {
|
||||||
if let Ok(domain_name) = Name::from_str_relaxed(name.to_owned()+".") {
|
if let Ok(domain_name) = Name::from_str_relaxed(name.to_owned()+".") {
|
||||||
if match_domain_hidden_list(&name, &state.config.dns.hidden_suffixes) {
|
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
|
// Try to hide the fact that we didn't do dns resolution at all
|
||||||
// We resolve example.org as basic avoidance of timing sidechannels.
|
// We resolve example.org as basic avoidance of timing sidechannels.
|
||||||
// WARNING: this timing sidechannel avoidance is very crude.
|
// WARNING: this timing sidechannel avoidance is very crude.
|
||||||
simple_dns::lookup(
|
simple_dns::lookup(
|
||||||
&dns_resolver,
|
dns_resolver,
|
||||||
&Name::from_ascii("example.org.").expect("Static Dummy Name"),
|
&Name::from_ascii("example.org.").expect("Static Dummy Name"),
|
||||||
do_full_lookup).await;
|
do_full_lookup).await;
|
||||||
return DigResult {
|
return DigResult {
|
||||||
@ -698,7 +769,7 @@ async fn get_dig_result(
|
|||||||
} else {
|
} else {
|
||||||
return DigResult {
|
return DigResult {
|
||||||
records: simple_dns::lookup(
|
records: simple_dns::lookup(
|
||||||
&dns_resolver,
|
dns_resolver,
|
||||||
&domain_name,
|
&domain_name,
|
||||||
do_full_lookup).await,
|
do_full_lookup).await,
|
||||||
idn: idna_name,
|
idn: idna_name,
|
||||||
@ -707,21 +778,27 @@ async fn get_dig_result(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Invalid domain name
|
||||||
return DigResult {
|
return DigResult {
|
||||||
records: DnsLookupResult{
|
records: DnsLookupResult{
|
||||||
invalid_name: true,
|
invalid_name: true,
|
||||||
.. Default::default()
|
.. Default::default()
|
||||||
},
|
},
|
||||||
.. Default::default()
|
idn: idna_name,
|
||||||
|
partial_lookup: !do_full_lookup,
|
||||||
|
used_dns_resolver: dns_resolver_name.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Unknown resolver name
|
||||||
return DigResult {
|
return DigResult {
|
||||||
records: DnsLookupResult{
|
records: DnsLookupResult{
|
||||||
unkown_resolver: true,
|
unkown_resolver: true,
|
||||||
.. Default::default()
|
.. Default::default()
|
||||||
},
|
},
|
||||||
.. Default::default()
|
idn: idna_name,
|
||||||
|
partial_lookup: !do_full_lookup,
|
||||||
|
used_dns_resolver: "unkown_resolver".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
src/nat64.rs
Normal file
23
src/nat64.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use std::net::Ipv4Addr;
|
||||||
|
use std::net::Ipv6Addr;
|
||||||
|
|
||||||
|
/// Resolves a NAT64 address if it is in the range of 64:ff9b::/96
|
||||||
|
pub fn resolve_nat64_address(from: Ipv6Addr) -> Option<Ipv4Addr> {
|
||||||
|
if is_nat64_address(&from) {
|
||||||
|
let segments = from.segments();
|
||||||
|
return Some(Ipv4Addr::new(
|
||||||
|
((segments[6] & 0xff00) >> 8) as u8,
|
||||||
|
(segments[6] & 0x00ff) as u8,
|
||||||
|
((segments[7] & 0xff00) >> 8) as u8,
|
||||||
|
(segments[7] & 0x00ff) as u8,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_nat64_address(address: &Ipv6Addr) -> bool {
|
||||||
|
let segments = address.segments();
|
||||||
|
segments[0]==0x64 && segments[1]==0xff9b
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
|||||||
use axum_client_ip::SecureClientIp;
|
use axum_client_ip::SecureClientIp;
|
||||||
use axum::{
|
use axum::{
|
||||||
|
body::Body,
|
||||||
extract::Extension,
|
extract::Extension,
|
||||||
http::{
|
http::{
|
||||||
Request,
|
Request,
|
||||||
@ -17,6 +18,7 @@ use governor::{
|
|||||||
RateLimiter,
|
RateLimiter,
|
||||||
state::keyed::DefaultKeyedStateStore,
|
state::keyed::DefaultKeyedStateStore,
|
||||||
};
|
};
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
@ -40,11 +42,11 @@ pub fn build_rate_limiting_state(
|
|||||||
Extension(arc_limiter)
|
Extension(arc_limiter)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn rate_limit_middleware<B>(
|
pub async fn rate_limit_middleware(
|
||||||
SecureClientIp(address): SecureClientIp,
|
SecureClientIp(address): SecureClientIp,
|
||||||
Extension(arc_limiter): Extension<Arc<SimpleRateLimiter<IpAddr>>>,
|
Extension(arc_limiter): Extension<Arc<SimpleRateLimiter<IpAddr>>>,
|
||||||
req: Request<B>,
|
req: Request<Body>,
|
||||||
next: Next<B>
|
next: Next
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let limiter = Arc::clone(&arc_limiter);
|
let limiter = Arc::clone(&arc_limiter);
|
||||||
|
|
||||||
@ -54,10 +56,10 @@ pub async fn rate_limit_middleware<B>(
|
|||||||
if limiter.check_key(&IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)).is_ok() {
|
if limiter.check_key(&IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)).is_ok() {
|
||||||
let oldlen = limiter.len();
|
let oldlen = limiter.len();
|
||||||
if oldlen > 100 {
|
if oldlen > 100 {
|
||||||
println!("Doing limiter cleanup ...");
|
debug!("Doing limiter cleanup ...");
|
||||||
limiter.retain_recent();
|
limiter.retain_recent();
|
||||||
limiter.shrink_to_fit();
|
limiter.shrink_to_fit();
|
||||||
println!("Old limiter store size: {oldlen} New limiter store size: {}", limiter.len());
|
debug!("Old limiter store size: {oldlen} New limiter store size: {}", limiter.len());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next.run(req).await
|
next.run(req).await
|
||||||
|
@ -1,36 +1,13 @@
|
|||||||
use serde::{Deserialize,Serialize};
|
use serde::{Deserialize,Serialize};
|
||||||
|
|
||||||
|
use lib_humus::HtmlTextJsonFormat;
|
||||||
|
use lib_humus::HumusQuerySettings;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/* Response format */
|
/* Response format */
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Copy)]
|
pub type ResponseFormat = HtmlTextJsonFormat;
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Query and Template Settings */
|
/* Query and Template Settings */
|
||||||
|
|
||||||
@ -39,13 +16,27 @@ pub struct QuerySettings {
|
|||||||
pub format: ResponseFormat,
|
pub format: ResponseFormat,
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
pub available_dns_resolvers: Vec<Selectable>,
|
pub available_dns_resolvers: Vec<Selectable>,
|
||||||
pub dns_resolver_id: String,
|
pub dns_resolver_id: Arc<str>,
|
||||||
|
pub dns_disable_self_lookup: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct Selectable {
|
pub struct Selectable {
|
||||||
pub id: String,
|
pub id: Arc<str>,
|
||||||
pub name: String,
|
pub name: Arc<str>,
|
||||||
pub weight: i32,
|
pub weight: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HumusQuerySettings<ResponseFormat> for QuerySettings {
|
||||||
|
|
||||||
|
fn initalize_template_context(&self, context: &mut tera::Context) {
|
||||||
|
context.insert("language", &self.lang);
|
||||||
|
context.insert("dns_resolvers", &self.available_dns_resolvers);
|
||||||
|
context.insert("dns_resolver_id", &self.dns_resolver_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_format(&self) -> ResponseFormat {
|
||||||
|
self.format.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -1,23 +1,24 @@
|
|||||||
/*
|
|
||||||
* 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;
|
//! This module wraps the hickory_resolver library
|
||||||
use trust_dns_proto::rr::{
|
//! 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,
|
RData,
|
||||||
record_type::RecordType,
|
record_type::RecordType,
|
||||||
};
|
};
|
||||||
use trust_dns_resolver::{
|
use hickory_proto::ProtoErrorKind;
|
||||||
error::ResolveError,
|
use hickory_resolver::{
|
||||||
error::ResolveErrorKind,
|
|
||||||
lookup::Lookup,
|
lookup::Lookup,
|
||||||
Name,
|
Name,
|
||||||
TokioAsyncResolver,
|
ResolveError,
|
||||||
|
ResolveErrorKind,
|
||||||
|
TokioResolver,
|
||||||
};
|
};
|
||||||
|
use log::{warn,error};
|
||||||
|
|
||||||
use tokio::join;
|
use tokio::join;
|
||||||
|
|
||||||
@ -42,6 +43,7 @@ pub struct DnsLookupResult {
|
|||||||
pub dns_error: bool,
|
pub dns_error: bool,
|
||||||
pub nxdomain: bool,
|
pub nxdomain: bool,
|
||||||
pub timeout: bool,
|
pub timeout: bool,
|
||||||
|
pub too_busy: bool,
|
||||||
pub invalid_name: bool,
|
pub invalid_name: bool,
|
||||||
pub unkown_resolver: bool,
|
pub unkown_resolver: bool,
|
||||||
}
|
}
|
||||||
@ -74,13 +76,13 @@ pub struct SrvRecord {
|
|||||||
/* Lookup Functions*/
|
/* Lookup Functions*/
|
||||||
|
|
||||||
pub async fn reverse_lookup(
|
pub async fn reverse_lookup(
|
||||||
resolver: &TokioAsyncResolver,
|
resolver: &TokioResolver,
|
||||||
address: &IpAddr,
|
address: &IpAddr,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let revese_res = resolver.reverse_lookup(*address);
|
let revese_res = resolver.reverse_lookup(*address);
|
||||||
match revese_res.await {
|
match revese_res.await {
|
||||||
Ok(lookup) => {
|
Ok(lookup) => {
|
||||||
for name in lookup {
|
if let Some(name) = lookup.iter().next() {
|
||||||
return Some(name.to_string())
|
return Some(name.to_string())
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@ -88,11 +90,19 @@ pub async fn reverse_lookup(
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
let kind = e.kind();
|
let kind = e.kind();
|
||||||
match kind {
|
match kind {
|
||||||
ResolveErrorKind::NoRecordsFound { .. } => {
|
ResolveErrorKind::Proto(protocol_error) => {
|
||||||
//Ignore, that just happens …
|
match protocol_error.kind() {
|
||||||
|
ProtoErrorKind::NoRecordsFound { .. } => {
|
||||||
|
//Ignore, that just happens …
|
||||||
|
// TODO: Add NSec when adding support for dnssec
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("Reverse lookup on {address} failed with protocol error: {protocol_error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("Reverse lookup on {address} failed: {kind}");
|
error!("Reverse lookup on {address} failed: {kind}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@ -121,9 +131,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){
|
pub fn add_record_to_lookup_result(result: &mut DnsLookupResult, record: &RData){
|
||||||
match record {
|
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::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::CAA(caa) => opush(&mut result.caa, caa.to_string()),
|
||||||
RData::CNAME(cname) => opush(&mut result.cname, cname.to_string()),
|
RData::CNAME(cname) => opush(&mut result.cname, cname.to_string()),
|
||||||
RData::MX(mx) => opush(&mut result.mx, MxRecord{
|
RData::MX(mx) => opush(&mut result.mx, MxRecord{
|
||||||
@ -154,7 +164,9 @@ pub fn add_record_to_lookup_result(result: &mut DnsLookupResult, record: &RData)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => { println!("Tried to add an unkown DNS record to results: {record}"); },
|
_ => {
|
||||||
|
warn!("Tried to add an unkown DNS record to results: {record}");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,9 +189,7 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
|
|||||||
let name = lookup.query().name();
|
let name = lookup.query().name();
|
||||||
for record in lookup.record_iter() {
|
for record in lookup.record_iter() {
|
||||||
if name == record.name() {
|
if name == record.name() {
|
||||||
if let Some(data) = record.data() {
|
add_record_to_lookup_result(dig_result, record.data());
|
||||||
add_record_to_lookup_result(dig_result, data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//TODO: handle additional responses
|
//TODO: handle additional responses
|
||||||
}
|
}
|
||||||
@ -187,27 +197,35 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
match e.kind() {
|
match e.kind() {
|
||||||
ResolveErrorKind::Message(..) |
|
ResolveErrorKind::Message(..) |
|
||||||
ResolveErrorKind::Msg(..) |
|
ResolveErrorKind::Msg(..) => {
|
||||||
ResolveErrorKind::NoConnections |
|
error!("There was an error message while doing a DNS Lookup: {e}");
|
||||||
ResolveErrorKind::Io(..) |
|
|
||||||
ResolveErrorKind::Proto(..) => {
|
|
||||||
dig_result.other_error = true;
|
|
||||||
println!("There was an error while doing a DNS Lookup: {e}");
|
|
||||||
},
|
|
||||||
ResolveErrorKind::Timeout => {
|
|
||||||
dig_result.timeout = true;
|
|
||||||
println!("There was a timeout while doing a DNS Lookup.");
|
|
||||||
},
|
|
||||||
ResolveErrorKind::NoRecordsFound{response_code, ..} => {
|
|
||||||
match response_code {
|
|
||||||
ResponseCode::NXDomain => dig_result.nxdomain = true,
|
|
||||||
ResponseCode::NoError => {},
|
|
||||||
_ => {
|
|
||||||
println!("The DNS Server returned an error while doing a DNS Lookup: {response_code}");
|
|
||||||
dig_result.dns_error = true;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ResolveErrorKind::Proto(protocol_error) => {
|
||||||
|
match protocol_error.kind() {
|
||||||
|
ProtoErrorKind::Busy => {
|
||||||
|
dig_result.too_busy = true;
|
||||||
|
warn!("A resource was too busy for doing a DNS Lookup.");
|
||||||
|
},
|
||||||
|
ProtoErrorKind::Timeout => {
|
||||||
|
dig_result.timeout = true;
|
||||||
|
warn!("There was a timeout while doing a DNS Lookup.");
|
||||||
|
},
|
||||||
|
ProtoErrorKind::NoRecordsFound { response_code, .. } => {
|
||||||
|
match response_code {
|
||||||
|
ResponseCode::NXDomain => dig_result.nxdomain = true,
|
||||||
|
ResponseCode::NoError => {},
|
||||||
|
_ => {
|
||||||
|
error!("The DNS Server returned an error while doing a DNS Lookup: {response_code}");
|
||||||
|
dig_result.dns_error = true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
dig_result.other_error = true;
|
||||||
|
error!("There was an error while doing a DNS Lookup: {protocol_error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
_ => { /*Ignore for now*/ },
|
_ => { /*Ignore for now*/ },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -218,7 +236,7 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
|
|||||||
// If do_full_lookup is false only the A and AAAA (CNAMEs planned for the future)
|
// If do_full_lookup is false only the A and AAAA (CNAMEs planned for the future)
|
||||||
// records will be fetched.
|
// records will be fetched.
|
||||||
pub async fn lookup(
|
pub async fn lookup(
|
||||||
resolver: &TokioAsyncResolver,
|
resolver: &TokioResolver,
|
||||||
name: &Name,
|
name: &Name,
|
||||||
do_full_lookup: bool,
|
do_full_lookup: bool,
|
||||||
) -> DnsLookupResult {
|
) -> DnsLookupResult {
|
||||||
|
@ -1,135 +0,0 @@
|
|||||||
/*
|
|
||||||
* This is the echoip-slatecave templating engine.
|
|
||||||
* It wraps around tera in is specialized for echoip-slatecave.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use axum::{
|
|
||||||
headers::HeaderValue,
|
|
||||||
http::StatusCode,
|
|
||||||
http::header::SET_COOKIE,
|
|
||||||
response::Html,
|
|
||||||
response::IntoResponse,
|
|
||||||
response::Response,
|
|
||||||
response::Json,
|
|
||||||
};
|
|
||||||
use axum_extra::extract::cookie::Cookie;
|
|
||||||
use axum_extra::extract::cookie;
|
|
||||||
use tera::Tera;
|
|
||||||
use toml::Table;
|
|
||||||
|
|
||||||
use crate::DigResult;
|
|
||||||
use crate::IpResult;
|
|
||||||
use crate::config::DnsResolverConfig;
|
|
||||||
use crate::settings::*;
|
|
||||||
|
|
||||||
|
|
||||||
/* The echoip view */
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, Clone)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum View {
|
|
||||||
Asn { asn: u32 },
|
|
||||||
Dig { query: String, result: DigResult },
|
|
||||||
DnsResolver{ config: DnsResolverConfig },
|
|
||||||
DnsResolverList,
|
|
||||||
Index { result: IpResult, user_agent: Option<String> },
|
|
||||||
Ip { result: IpResult },
|
|
||||||
Message{ title: String, message: String },
|
|
||||||
#[serde(rename="404")]
|
|
||||||
NotFound,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View {
|
|
||||||
pub fn template_name(&self) -> String {
|
|
||||||
match self {
|
|
||||||
View::Asn{..} => "asn",
|
|
||||||
View::Dig{..} => "dig",
|
|
||||||
View::DnsResolver{..} => "dns_resolver",
|
|
||||||
View::DnsResolverList => "dns_resolver_list",
|
|
||||||
View::Index{..} => "index",
|
|
||||||
View::Ip{..} => "ip",
|
|
||||||
View::Message{..} => "message",
|
|
||||||
View::NotFound => "404",
|
|
||||||
}.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The engine itself */
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Engine {
|
|
||||||
pub tera: Tera,
|
|
||||||
pub template_config: Option<Table>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Engine {
|
|
||||||
pub async fn render_view(
|
|
||||||
&self,
|
|
||||||
settings: &QuerySettings,
|
|
||||||
view: &View,
|
|
||||||
) -> Response {
|
|
||||||
let mut response = match settings.format {
|
|
||||||
ResponseFormat::TextHtml | ResponseFormat::TextPlain => {
|
|
||||||
let template_name = view.template_name();
|
|
||||||
|
|
||||||
let mut context = tera::Context::new();
|
|
||||||
context.insert("view", &template_name);
|
|
||||||
//intented for shared macros
|
|
||||||
context.insert("format", &settings.format.to_string());
|
|
||||||
context.insert("language", &settings.lang);
|
|
||||||
context.insert("dns_resolvers", &settings.available_dns_resolvers);
|
|
||||||
context.insert("dns_resolver_id", &settings.dns_resolver_id);
|
|
||||||
context.insert("data", &view);
|
|
||||||
context.insert("extra", &self.template_config);
|
|
||||||
|
|
||||||
match self.tera.render(&(template_name+&settings.format.to_file_extension()), &context) {
|
|
||||||
Ok(text) =>
|
|
||||||
match settings.format {
|
|
||||||
ResponseFormat::TextHtml => Html(text).into_response(),
|
|
||||||
_ => text.into_response(),
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("There was an error while rendering template {}: {e:?}", view.template_name());
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("Template error in {}, contact owner or see logs.\n", view.template_name())
|
|
||||||
).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//TODO: Plain Text should have its own matcher
|
|
||||||
ResponseFormat::ApplicationJson => {
|
|
||||||
match view {
|
|
||||||
View::Dig{result, ..} => {
|
|
||||||
Json(result).into_response()
|
|
||||||
},
|
|
||||||
View::Index{result, ..} | View::Ip{result, ..} => {
|
|
||||||
Json(result).into_response()
|
|
||||||
},
|
|
||||||
View::DnsResolverList => {
|
|
||||||
Json(settings.available_dns_resolvers.clone()).into_response()
|
|
||||||
},
|
|
||||||
View::DnsResolver{ config } => {
|
|
||||||
Json(config).into_response()
|
|
||||||
}
|
|
||||||
_ => Json(view).into_response(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match view {
|
|
||||||
View::NotFound => *response.status_mut() = StatusCode::NOT_FOUND,
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
let cookie = Cookie::build("dns_resolver",settings.dns_resolver_id.clone())
|
|
||||||
.path("/")
|
|
||||||
.same_site(cookie::SameSite::Strict)
|
|
||||||
.finish();
|
|
||||||
if let Ok(header_value) = HeaderValue::from_str(&cookie.to_string()) {
|
|
||||||
response.headers_mut().append(
|
|
||||||
SET_COOKIE,
|
|
||||||
header_value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
response
|
|
||||||
}
|
|
||||||
}
|
|
80
src/view.rs
Normal file
80
src/view.rs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
|
||||||
|
use axum::http::status::StatusCode;
|
||||||
|
use axum::Json;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::response::Response;
|
||||||
|
use axum_extra::extract::cookie::Cookie;
|
||||||
|
use axum_extra::extract::cookie;
|
||||||
|
use lib_humus::HumusView;
|
||||||
|
|
||||||
|
use crate::DigResult;
|
||||||
|
use crate::IpResult;
|
||||||
|
use crate::config::DnsResolverConfig;
|
||||||
|
use crate::settings::QuerySettings;
|
||||||
|
use crate::settings::ResponseFormat;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum View {
|
||||||
|
Asn { asn: u32 },
|
||||||
|
Dig { query: String, result: DigResult },
|
||||||
|
DnsResolver{ config: DnsResolverConfig },
|
||||||
|
DnsResolverList,
|
||||||
|
Index { result: IpResult, user_agent: Option<String> },
|
||||||
|
Ip { result: IpResult },
|
||||||
|
Message{ title: String, message: String },
|
||||||
|
#[serde(rename="404")]
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HumusView<QuerySettings, ResponseFormat> for View {
|
||||||
|
fn get_template_name(&self) -> String {
|
||||||
|
match self {
|
||||||
|
View::Asn{..} => "asn",
|
||||||
|
View::Dig{..} => "dig",
|
||||||
|
View::DnsResolver{..} => "dns_resolver",
|
||||||
|
View::DnsResolverList => "dns_resolver_list",
|
||||||
|
View::Index{..} => "index",
|
||||||
|
View::Ip{..} => "ip",
|
||||||
|
View::Message{..} => "message",
|
||||||
|
View::NotFound => "404",
|
||||||
|
}.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_status_code(&self, _: &QuerySettings) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
Self::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
_ => StatusCode::OK,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cookie_header(&self, settings: &QuerySettings) -> Option<String> {
|
||||||
|
Some(
|
||||||
|
Cookie::build(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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,9 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{% block title %}{{ extra[view].title | default(value="…") }}{% endblock %} | {{extra.site_name|default(value="echoip")}}</title>
|
<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 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 -->
|
<!-- Open-Graph -->
|
||||||
{% block metadata %}
|
{% 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 %}" />
|
<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>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<nav>
|
<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 }}">
|
<form class="search" method="GET" action="{{ extra.base_url }}">
|
||||||
<input type="search" name="query" autocomplete="on" maxlength="260"
|
<input type="search" name="query" autocomplete="on" maxlength="260"
|
||||||
title="Search for an IP-Adress, Domain-Name, or ASN."
|
title="Search for an IP-Adress, Domain-Name, or ASN."
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
{% import "helpers.html" as helper %}
|
{% import "helpers.html" as helper %}
|
||||||
{% import "links.html" as links %}
|
{% import "links.html" as links %}
|
||||||
|
|
||||||
|
{% block robots_meta %}<meta name="robots" content="noindex,nofollow">{% endblock %}
|
||||||
|
|
||||||
{% block title %}dig {{ data.query }}{% endblock %}
|
{% block title %}dig {{ data.query }}{% endblock %}
|
||||||
{% block og_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 %}
|
{% 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 %}
|
||||||
|
@ -8,15 +8,26 @@ base_url="http://localhost:3000"
|
|||||||
stylesheet = "/style.css"
|
stylesheet = "/style.css"
|
||||||
|
|
||||||
# URL to and mimetype of your favicon
|
# URL to and mimetype of your favicon
|
||||||
# favicon = ""
|
favicon = "/icon_64.png"
|
||||||
# favicon_mimetype = "image/png"
|
favicon_mimetype = "image/png"
|
||||||
|
# favicon_mimetype = "image/svg+xml"
|
||||||
# favicon_mimetype = "image/jpeg"
|
# 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
|
# URLs to look up v4 and v6 addresses explicitly
|
||||||
# If you have not configured them, comment them out, the button will stay hidden
|
# If you have not configured them, comment them out, the button will stay hidden
|
||||||
v4_url="http://v4.localhost:3000/"
|
v4_url="http://v4.localhost:3000/"
|
||||||
v6_url="http://v6.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]
|
[404]
|
||||||
# configure the 404 page, this is available for other pages too!
|
# configure the 404 page, this is available for other pages too!
|
||||||
# Use the template name as the section name.
|
# Use the template name as the section name.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% macro place_dl(place, label="", iso_code_prefix="") -%}
|
{% macro place_dl(place, label="", iso_code_prefix="") -%}
|
||||||
{%- if place -%}
|
{%- if place -%}
|
||||||
{%- if format=="text/html" %}
|
{%- if format=="html" %}
|
||||||
{% if label %}<dt>{{label}}</dt>{% endif %}
|
{% 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>
|
<dd>{{place.name}} {% if place.iso_code%}({% if iso_code_prefix %}{{iso_code_prefix}}-{% endif %}{{place.iso_code}}){% endif %}</dd>
|
||||||
{% else -%}
|
{% 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>
|
<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 %}
|
{% endmacro dig %}
|
||||||
|
|
||||||
{% macro ip(extra, ip, text=false) %}
|
{% 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 }}"><code>{% if text %}{{ text }}{% else %}{{ ip }}{% endif %}</code></a>
|
<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 %}
|
{% endmacro dig %}
|
||||||
|
|
||||||
{% macro breadcrumb_domain(extra, name) %}
|
{% macro breadcrumb_domain(extra, name) %}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
{% extends "ip.html" %}
|
{% extends "ip.html" %}
|
||||||
{% import "helpers.html" as helper %}
|
{% import "helpers.html" as helper %}
|
||||||
|
|
||||||
|
{% block robots_meta %}{# Allow indexing for landing page #}{% endblock %}
|
||||||
|
|
||||||
{% block title %}Your IP: {{ data.result.address }}{% endblock %}
|
{% block title %}Your IP: {{ data.result.address }}{% endblock %}
|
||||||
{% block og_title %}What is my IP-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 %}
|
{% block h1 %}Your IPv{% if data.result.ip_info.is_v6_address %}6{% else %}4{% endif %}: <code>{{ data.result.address }}</code>{% endblock %}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
{% import "helpers.html" as helper %}
|
{% import "helpers.html" as helper %}
|
||||||
{% import "links.html" as links %}
|
{% import "links.html" as links %}
|
||||||
|
|
||||||
|
{% block robots_meta %}<meta name="robots" content="noindex,nofollow">{% endblock %}
|
||||||
|
|
||||||
{% block title %}{{ data.result.address }}{% endblock %}
|
{% block title %}{{ data.result.address }}{% endblock %}
|
||||||
{% block og_title %}Lookup {{ data.result.address }}{% endblock %}
|
{% block og_title %}Lookup {{ data.result.address }}{% endblock %}
|
||||||
{% block h1 %}Lookup <code>{{ data.result.address }}</code>{% endblock %}
|
{% block h1 %}Lookup <code>{{ data.result.address }}</code>{% endblock %}
|
||||||
@ -10,6 +12,12 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% set r = data.result %}
|
{% set r = data.result %}
|
||||||
|
{% if r.mapping %}
|
||||||
|
<section>
|
||||||
|
<h2>{{ r.mapping.strategy | title }} Mapping</h2>
|
||||||
|
<p>The address <code>{{ r.mapping.from_address }}</code> was automatically translated to <code>{{ r.mapping.to_address }}</code> using {{ r.mapping.strategy | title }}.</p>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
<section>
|
<section>
|
||||||
<h2>Network Information</h2>
|
<h2>Network Information</h2>
|
||||||
<dl>
|
<dl>
|
||||||
@ -18,6 +26,9 @@
|
|||||||
{% if r.hostname %}
|
{% if r.hostname %}
|
||||||
<dt>Hostname</dt>
|
<dt>Hostname</dt>
|
||||||
<dd>{{ helper::dig(extra=extra, name=r.hostname) }}</dd>
|
<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 %}
|
{% endif %}
|
||||||
{% if r.asn %}
|
{% if r.asn %}
|
||||||
<dt><abbr="Autonomous System Number">ASN</abbr></dt>
|
<dt><abbr="Autonomous System Number">ASN</abbr></dt>
|
||||||
@ -31,43 +42,44 @@
|
|||||||
{% if r.location %}
|
{% if r.location %}
|
||||||
<section>
|
<section>
|
||||||
<h2>Geolocation</h2>
|
<h2>Geolocation</h2>
|
||||||
<dl>
|
{% if extra.geo_attribution_html %}
|
||||||
{{ helper::place_dl(place=r.location.continent, label="Continent") }}
|
<dl>
|
||||||
{{ helper::place_dl(place=r.location.country, label="Country") }}
|
{{ helper::place_dl(place=r.location.continent, label="Continent") }}
|
||||||
{% if r.location.country.iso_code | default(value="") != r.location.registered_country.iso_code | default(value="") %}
|
{{ helper::place_dl(place=r.location.country, label="Country") }}
|
||||||
{{ helper::place_dl(place=r.location.registered_country, label="Registered in") }}
|
{% if r.location.country.iso_code | default(value="") != r.location.registered_country.iso_code | default(value="") %}
|
||||||
{% endif %}
|
{{ helper::place_dl(place=r.location.registered_country, label="Registered in") }}
|
||||||
{% if r.location.country.iso_code | default(value="") != r.location.represented_country.iso_code | default(value="")%}
|
{% endif %}
|
||||||
{{ helper::place_dl(place=r.location.represented_country, label="Represents") }}
|
{% if r.location.country.iso_code | default(value="") != r.location.represented_country.iso_code | default(value="")%}
|
||||||
{% endif %}
|
{{ helper::place_dl(place=r.location.represented_country, label="Represents") }}
|
||||||
{% if r.location.subdivisions %}
|
{% endif %}
|
||||||
{% for sd in r.location.subdivisions %}
|
{% if r.location.subdivisions %}
|
||||||
{{ helper::place_dl(place=sd, label="Subdivision", iso_code_prefix=r.location.country.iso_code|default(value="")) }}
|
{% for sd in r.location.subdivisions %}
|
||||||
{% endfor %}
|
{{ helper::place_dl(place=sd, label="Subdivision", iso_code_prefix=r.location.country.iso_code|default(value="")) }}
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
{{ helper::place_dl(place=r.location.city, label="City") }}
|
{% endif %}
|
||||||
{% if r.location.postal_code %}
|
{{ helper::place_dl(place=r.location.city, label="City") }}
|
||||||
<dt>Postal Code</dt>
|
{% if r.location.postal_code %}
|
||||||
<dd>{{r.location.postal_code}}</dd>
|
<dt>Postal Code</dt>
|
||||||
{% endif %}
|
<dd>{{r.location.postal_code}}</dd>
|
||||||
{% if r.location.time_zone %}
|
{% endif %}
|
||||||
<dt>Timezone</dt>
|
{% if r.location.time_zone %}
|
||||||
<dd>{{r.location.time_zone}}</dd>
|
<dt>Timezone</dt>
|
||||||
{% endif %}
|
<dd>{{r.location.time_zone}}</dd>
|
||||||
{% if r.location.accuracy %}
|
{% endif %}
|
||||||
<dt>Accuracy</dt>
|
{% if r.location.accuracy %}
|
||||||
<dd>~{{r.location.accuracy}}km</dd>
|
<dt>Accuracy</dt>
|
||||||
{% endif %}
|
<dd>~{{r.location.accuracy}}km</dd>
|
||||||
{% if r.location.coordinates %}
|
{% endif %}
|
||||||
<dt>Coordinates</dt>
|
{% if r.location.coordinates %}
|
||||||
<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>
|
<dt>Coordinates</dt>
|
||||||
{% endif %}
|
<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>
|
||||||
</dl>
|
{% endif %}
|
||||||
<!--We have to put that there to comply with maxminds licensing-->
|
</dl>
|
||||||
<p><small>
|
<p><small>{{extra.geo_attribution_html | safe}}</small></p>
|
||||||
The GeoIP and ASN information is provided by the GeoLite2 database created by
|
{% else %}
|
||||||
<a target="_blank" href="https://www.maxmind.com">MaxMind</a>.
|
<p><strong style="font-size: 2em">Please configure the <code>geo_attribution_html</code> key in the template extra configuration!</strong></p>
|
||||||
</small></p>
|
<p>The geolocation information will then become visible.</p>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% block extra_content %}{% endblock %}
|
{% block extra_content %}{% endblock %}
|
||||||
|
@ -8,17 +8,31 @@
|
|||||||
{% set r = data.result -%}
|
{% set r = data.result -%}
|
||||||
# {% block title %}Lookup {{ data.result.address }}{% endblock %}
|
# {% block title %}Lookup {{ data.result.address }}{% endblock %}
|
||||||
|
|
||||||
|
{%- if r.mapping %}
|
||||||
|
|
||||||
|
## {{ r.mapping.strategy | title }} Mapping
|
||||||
|
|
||||||
|
The address {{ r.mapping.from_address }} was automatically translated to {{ r.mapping.to_address }} using {{ r.mapping.strategy | title }}.
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
## Network information
|
## Network information
|
||||||
|
|
||||||
* Type of Address: {{ helper::ip_info(ip_info=r.ip_info) }}
|
* Type of Address: {{ helper::ip_info(ip_info=r.ip_info) }}
|
||||||
{% if r.hostname -%}
|
{% if r.hostname -%}
|
||||||
* Hostname: {{ r.hostname }}
|
* Hostname: {{ r.hostname }}
|
||||||
|
{%- elif r.reverse_dns_disabled_for_privacy %}
|
||||||
|
* Hostname: Lookup disabled by default
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{% if r.asn -%}
|
{% if r.asn -%}
|
||||||
* ASN: AS{{ r.asn.asn }}
|
* ASN: AS{{ r.asn.asn }}
|
||||||
* AS Name: {{r.asn.name}}
|
* AS Name: {{r.asn.name}}
|
||||||
{%- endif -%}
|
{%- 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 %}
|
{%- if r.location %}
|
||||||
|
|
||||||
## Geolocation
|
## Geolocation
|
||||||
@ -52,7 +66,7 @@ lat: {{r.location.coordinates.lat}}, lon: {{r.location.coordinates.lon}}
|
|||||||
=> {{ links::map_link(lat=r.location.coordinates.lat, lon=r.location.coordinates.lon)}}
|
=> {{ links::map_link(lat=r.location.coordinates.lat, lon=r.location.coordinates.lon)}}
|
||||||
{%- endif %}
|
{%- 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 -%}
|
{% endif -%}
|
||||||
|
|
||||||
{%- block extra_content %}{% endblock -%}
|
{%- block extra_content %}{% endblock -%}
|
||||||
|
@ -6,22 +6,27 @@
|
|||||||
<li><a target="_blank" href="https://client.rdap.org/?type=ip&object={{ address }}">… 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://client.rdap.org/?type=ip&object={{ address }}">… 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/host/{{ address }}">… on shodan.io <small>(limited queries per day, wants an account)</small></a></li>
|
<li><a target="_blank" href="https://www.shodan.io/host/{{ address }}">… on shodan.io <small>(limited queries 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=ip%3D{{ address }}">… on search.censys.io <small>(10 query 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=ip%3D{{ address }}">… on search.censys.io <small>(10 query per day, wants an account)</small></a></li>
|
||||||
|
<li><a target="_blank" href="https://www.abuseipdb.com/check/{{ address }}">… an AbuseIPDB.com</a></li>
|
||||||
|
<li><a target="_blank" href="https://app.crowdsec.net/cti/{{ address }}">… on CrowdSec.net CTI <small>(10 query's per day, wants an account)</small></a></li>
|
||||||
{% if not address is matching(":") %}
|
{% if not address is matching(":") %}
|
||||||
{# v4 only #}
|
{# v4 only #}
|
||||||
<li><a target="_blank" href="https://www.virustotal.com/gui/ip-address/{{ address }}">… on virustotal.com</a></li>
|
<li><a target="_blank" href="https://www.virustotal.com/gui/ip-address/{{ address }}">… on virustotal.com</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li><a target="_blank" href="https://check.spamhaus.org/results?query={{ address }}">… on spamhaus.org</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endmacro ip_address_links %}
|
{% endmacro ip_address_links %}
|
||||||
|
|
||||||
{% macro domain_name_links(name) %}
|
{% macro domain_name_links(name) %}
|
||||||
<p>Look up <code>{{name}}</code></p>
|
<p>Look up <code>{{name}}</code></p>
|
||||||
<ul class="link-list">
|
<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://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 }}">… on search.censys.io <small>(10 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 }}">… on virustotal.com</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 }}">… on the Mozilla Observatory (http and tls checks)</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 }}">… on the Internet.nl Website test</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 }}">… 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://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>
|
||||||
|
<li><a target="_blank" href="https://check.spamhaus.org/results?query={{ name | urlencode_strict }}">… on spamhaus.org</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endmacro domain_name_links %}
|
{% endmacro domain_name_links %}
|
||||||
|
|
||||||
@ -31,6 +36,8 @@
|
|||||||
<li><a target="_blank" href="https://bgp.he.net/AS{{asn}}">… on Hurricane Electric BGP Toolkit</a></li>
|
<li><a target="_blank" href="https://bgp.he.net/AS{{asn}}">… on Hurricane Electric BGP Toolkit</a></li>
|
||||||
<li><a target="_blank" href="https://radar.qrator.net/as{{asn}}">… on radar.qrator.net (BGP Tool)</a></li>
|
<li><a target="_blank" href="https://radar.qrator.net/as{{asn}}">… on radar.qrator.net (BGP Tool)</a></li>
|
||||||
<li><a target="_blank" href="https://search.censys.io/search?resource=hosts&sort=RELEVANCE&per_page=25&virtual_hosts=EXCLUDE&q=autonomous_system.asn%3D{{asn}}">… on search.censys.io <small>(10 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=autonomous_system.asn%3D{{asn}}">… on search.censys.io <small>(10 query's per day, wants an account)</small></a></li>
|
||||||
|
<li><a target="_blank" href="https://app.crowdsec.net/cti?q=as_num%3A{{ asn }}&page=1">… on CrowdSec.net <small>(30 queries per week, wants an account)</small></a></li>
|
||||||
|
<li><a target="_blank" href="https://check.spamhaus.org/results?query=AS{{ asn }}">… on spamhaus.org</a></li>
|
||||||
<li><a target="_blank" href="https://client.rdap.org/?type=autnum&object={{ asn }}">… 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://client.rdap.org/?type=autnum&object={{ asn }}">… 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://query.wikidata.org/#%23Select%20Wikipedia%20articles%20that%20belong%20to%20a%20given%20asn%0ASELECT%20DISTINCT%20%3Fitem%20%3Fwebsite%20%3FitemLabel%20%3FitemDescription%20%3Flang%20%3Farticle%20WHERE%20%7B%0A%20%20VALUES%20%3Fasn%20%7B%0A%20%20%20%20%22{{ asn }}%22%0A%20%20%7D%0A%20%20%3Fasn%20%5Ewdt%3AP3797%20%3Fitem.%0A%20%20OPTIONAL%20%7B%20%3Fitem%20wdt%3AP856%20%3Fwebsite.%20%7D%0A%20%20OPTIONAL%20%7B%0A%20%20%20%20%3Fitem%20%5Eschema%3Aabout%20%3Farticle.%0A%20%20%20%20%3Farticle%20schema%3AisPartOf%20_%3Ab64.%0A%20%20%20%20_%3Ab64%20wikibase%3AwikiGroup%20%22wikipedia%22.%0A%20%20%20%20%3Farticle%20schema%3AinLanguage%20%3Flang%3B%0A%20%20%20%20%20%20schema%3Aname%20%3Farticlename.%0A%20%20%20%20FILTER(((%3Flang%20%3D%20%22%5BAUTO_LANGUAGE%5D%22)%20%7C%7C%20(%3Flang%20%3D%20%22en%22))%20%7C%7C%20(%3Flang%20%3D%20%22de%22))%0A%20%20%7D%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%0A%20%20%20%20bd%3AserviceParam%20wikibase%3Alanguage%20%22%5BAUTO_LANGUAGE%5D%2Cen%22.%0A%20%20%20%20%3Fitem%20rdfs%3Alabel%20%3FitemLabel%3B%0A%20%20%20%20%20%20schema%3Adescription%20%3FitemDescription.%0A%20%20%7D%0A%7D%0AORDER%20BY%20(UCASE(%3FitemLabel))">… on Wikidata and Wikipedia <small>(Press the run button in the sidebar to get results)</small></a></li>
|
<li><a target="_blank" href="https://query.wikidata.org/#%23Select%20Wikipedia%20articles%20that%20belong%20to%20a%20given%20asn%0ASELECT%20DISTINCT%20%3Fitem%20%3Fwebsite%20%3FitemLabel%20%3FitemDescription%20%3Flang%20%3Farticle%20WHERE%20%7B%0A%20%20VALUES%20%3Fasn%20%7B%0A%20%20%20%20%22{{ asn }}%22%0A%20%20%7D%0A%20%20%3Fasn%20%5Ewdt%3AP3797%20%3Fitem.%0A%20%20OPTIONAL%20%7B%20%3Fitem%20wdt%3AP856%20%3Fwebsite.%20%7D%0A%20%20OPTIONAL%20%7B%0A%20%20%20%20%3Fitem%20%5Eschema%3Aabout%20%3Farticle.%0A%20%20%20%20%3Farticle%20schema%3AisPartOf%20_%3Ab64.%0A%20%20%20%20_%3Ab64%20wikibase%3AwikiGroup%20%22wikipedia%22.%0A%20%20%20%20%3Farticle%20schema%3AinLanguage%20%3Flang%3B%0A%20%20%20%20%20%20schema%3Aname%20%3Farticlename.%0A%20%20%20%20FILTER(((%3Flang%20%3D%20%22%5BAUTO_LANGUAGE%5D%22)%20%7C%7C%20(%3Flang%20%3D%20%22en%22))%20%7C%7C%20(%3Flang%20%3D%20%22de%22))%0A%20%20%7D%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%0A%20%20%20%20bd%3AserviceParam%20wikibase%3Alanguage%20%22%5BAUTO_LANGUAGE%5D%2Cen%22.%0A%20%20%20%20%3Fitem%20rdfs%3Alabel%20%3FitemLabel%3B%0A%20%20%20%20%20%20schema%3Adescription%20%3FitemDescription.%0A%20%20%7D%0A%7D%0AORDER%20BY%20(UCASE(%3FitemLabel))">… on Wikidata and Wikipedia <small>(Press the run button in the sidebar to get results)</small></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
50
templates/static/icon.svg
Normal file
50
templates/static/icon.svg
Normal 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 |
BIN
templates/static/icon_128.png
Normal file
BIN
templates/static/icon_128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
BIN
templates/static/icon_32.png
Normal file
BIN
templates/static/icon_32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 950 B |
BIN
templates/static/icon_64.png
Normal file
BIN
templates/static/icon_64.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
@ -599,3 +599,10 @@ form.search {
|
|||||||
background: var(--button-bg);
|
background: var(--button-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom icon style for sitename*/
|
||||||
|
|
||||||
|
.sitename > img {
|
||||||
|
height: 1.2em;
|
||||||
|
padding: 0 0.3ch;
|
||||||
|
margin-bottom: -.2em;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user