mirror of
https://codeberg.org/slatian/service.echoip-slatecave.git
synced 2025-07-17 14:33:27 +02:00
Compare commits
104 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 | |||
639d4579e9 | |||
4b3a8d5e08 | |||
53da9023da | |||
4876fb7ea0 | |||
2aa6baaa57 | |||
daa68bbd5d | |||
231e46a688 | |||
2fe1b69174 | |||
2e1f6a77ac | |||
1fe59d24d5 | |||
51d7954d71 | |||
2fa9de5cf7 | |||
1013c5365a | |||
cc6bbba3e4 | |||
97a3d18e9c | |||
ae95539c7b | |||
cf806ad8f5 | |||
fef954f6c1 | |||
e7eba57cb2 | |||
a334eb428a | |||
5c7d880733 | |||
55897585ff | |||
d88b15ba02 | |||
fdb23312df | |||
727d9a77cd | |||
cc6a025f89 | |||
104a072fd6 | |||
cd8c0455dc | |||
f173eba2ec | |||
cd8efdcb44 | |||
455ee751c4 | |||
c9d0c44985 | |||
f9753ccbfc | |||
a9512d7d4d | |||
6a57780490 | |||
51f27be997 | |||
554c788488 | |||
6d7e5ac18f |
2689
Cargo.lock
generated
2689
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
39
Cargo.toml
39
Cargo.toml
@ -1,25 +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-client-ip = "0.4"
|
|
||||||
clap = { version = "4", features = ["derive"] }
|
axum-client-ip = "0.7"
|
||||||
governor = "0.5"
|
axum-extra = { version = "0.10", features = ["cookie", "typed-header"] }
|
||||||
idna = "0.3"
|
axum = { version = "0.8", features = ["macros"] }
|
||||||
lazy_static = "1.4.0"
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
regex = "1.7"
|
env_logger = "0.11"
|
||||||
serde = { version = "1", features = ["derive"] }
|
governor = "0.8"
|
||||||
tokio = { version = "1", features = ["full"] }
|
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"
|
||||||
|
regex = "1.11"
|
||||||
|
serde = { version = "1", features = ["derive","rc"] }
|
||||||
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 = "0.22"
|
|
||||||
maxminddb = "0.23"
|
|
||||||
|
209
Configuration.md
Normal file
209
Configuration.md
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
# Configuring echoip-slatecave
|
||||||
|
|
||||||
|
The configuration of echoip-slatecave consists of one [toml](https://toml.io) file for configuring the service itself and one for configuring the template. This document covers on the service configuration.
|
||||||
|
|
||||||
|
By default the service tried to load `echoip.toml` from the current pwd as its configurtion, a custom path can be specified using the `-c <configfile>` option.
|
||||||
|
|
||||||
|
The configuration file consists of multiple sections of options grouped by feature.
|
||||||
|
|
||||||
|
* [server](#server) - Contains global options for the service
|
||||||
|
* [dns](#dns)
|
||||||
|
* [geoip](#geoip)
|
||||||
|
* [template](#template) - templating and rendering
|
||||||
|
* [ratelimit](#ratelimit)
|
||||||
|
|
||||||
|
The default configuration tries to be working out of the box while providing as little security footguns as possible. Have a look at the `server` and `dns` sections that is where most security related options are.
|
||||||
|
|
||||||
|
## [server]
|
||||||
|
|
||||||
|
### `listen_on`
|
||||||
|
|
||||||
|
Configures the bind address and port the service will listen on.
|
||||||
|
|
||||||
|
It uses the format `<ip-address>:<port>`, for the ip-address use `127.0.0.1` for only allowing local connections or `0.0.0.0` for allowing IPv4 connections from anywhere, `[::]` for allowing IPv6 connections from anywhere. For the port pick a free TCP port on your machine, default is `3000`.
|
||||||
|
|
||||||
|
This option can be overridden by the `-l <ip-address>:<port>` option on the commandline.
|
||||||
|
|
||||||
|
### `ip_header`
|
||||||
|
|
||||||
|
Configures which http header that contains the real client IP-Address or tells the service to use the IP-Address used to connect to it when in use without a proxy server.
|
||||||
|
|
||||||
|
Possible values are best covered by the [documentation for the underlying datatype](https://docs.rs/axum-client-ip/latest/axum_client_ip/enum.SecureClientIpSource.html).
|
||||||
|
|
||||||
|
When using a reverse Proxy `RightmostXForwardedFor` is usually what you want.
|
||||||
|
|
||||||
|
When using without a reverse Proxy set to `ConnectInfo`.
|
||||||
|
|
||||||
|
Please keep in mind that the ratelimit depends on the IP-Address being non-spoofable which is only given if the setting here matches the one of your proxy.
|
||||||
|
|
||||||
|
### `allow_private_ip_lookup`
|
||||||
|
|
||||||
|
Defaults to `false`, set to `true` to allow looking up IP-Addresses that fall into the private IP-Range. Enabling is not recommended when the server is publically accessible.
|
||||||
|
|
||||||
|
### `static_location`
|
||||||
|
|
||||||
|
When specified allows overriding the location where echoip-slatecave serve static files from (the default is the `static` directory under the [template_location](#template_location) )
|
||||||
|
|
||||||
|
## [dns]
|
||||||
|
|
||||||
|
### `allow_forward_lookup`
|
||||||
|
|
||||||
|
When set to `true`, allows resolving Domain names over the webinterface for every configured dns resolver.
|
||||||
|
|
||||||
|
### `allow_reverse_lookup`
|
||||||
|
|
||||||
|
When set to `true`, allows looking up domain names for IP-Addresses using reverse dns lookups for every configured resolver.
|
||||||
|
|
||||||
|
### `hidden_suffixes`
|
||||||
|
|
||||||
|
Configure it with a list of suffixes of domin names you don't want to leak out to the web interface.
|
||||||
|
|
||||||
|
If an entry matches, echoip-slatecave will try its best to pretend that resolving the name resulted in it not existing.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```toml
|
||||||
|
[dns]
|
||||||
|
hidden_suffixes = [".local",".box",".internal"]
|
||||||
|
```
|
||||||
|
|
||||||
|
This configuration option will not be exposed over the webinterface.
|
||||||
|
|
||||||
|
### System Resolver
|
||||||
|
|
||||||
|
By default echoip-slatecave uses the system configuration for dns like most other programs.
|
||||||
|
|
||||||
|
In case this is undesired one can disable it by setting `enable_system_resolver` to false.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dns]
|
||||||
|
enable_system_resolver = false
|
||||||
|
```
|
||||||
|
|
||||||
|
In case you want to use the system resolver and customize it.
|
||||||
|
|
||||||
|
`system_resolver_name`
|
||||||
|
: Equivalent to the `display_name` of a custom resolver, default: "System"
|
||||||
|
|
||||||
|
`system_resolver_id`
|
||||||
|
: Equivalent to the `key` of a custom resolver, default "system"
|
||||||
|
|
||||||
|
`system_resolver_weight`
|
||||||
|
: Equivalent to the `weight` of a custom resolver, default: 1000
|
||||||
|
|
||||||
|
### Custom resolvers
|
||||||
|
|
||||||
|
It is possible to confgure custom resolvers in plce of or in addition to the default system resolver.
|
||||||
|
|
||||||
|
To do this create a new section in the configuration file called `[dns.resolver.<key>]` where `<key>` is an url-friendly name for the resolver.
|
||||||
|
|
||||||
|
In this section one can fonfigure functional and cosmetic aspects of the resolver.
|
||||||
|
|
||||||
|
`display_name`
|
||||||
|
: The name that will be used for this resolver in the UI, it should be short and descriptive. Naming a main feature helps with keeping track of which Server is which when you have multiple servers with similar names. (required)
|
||||||
|
|
||||||
|
`info_url`
|
||||||
|
: A url pinting to the page containing service information and a technical overview of the dns server. (optional)
|
||||||
|
|
||||||
|
`aliases`
|
||||||
|
: A list of short strings that can be used to quickly typing in the desired server, inteded for power users.
|
||||||
|
|
||||||
|
`weight`
|
||||||
|
: An integer that helps with ranking multiple resolvers, oneswith higher weights will be displayed further up the lists of available options, the one with the highest weight will become the default resolver.
|
||||||
|
|
||||||
|
`servers`
|
||||||
|
: A list of socket addresses, that is `<ip-address>:<port>` pointing to the available dns servers. Leaving the port out is not yet supported.
|
||||||
|
|
||||||
|
`protocol`
|
||||||
|
: Which protocol to use for accessing the dns server. While `udp` works for almost all servers using `tls` is recommended when available.
|
||||||
|
|
||||||
|
`tls_dns_name`
|
||||||
|
: When using `tls`, `https` or `quic` this name helps the server knowing want ([SNI](https://en.wikipedia.org/wiki/Server_Name_Indication)). Usually this is the domain name of the dns server.
|
||||||
|
|
||||||
|
Available protocols:
|
||||||
|
|
||||||
|
| Protocol | Default Port |
|
||||||
|
|----------|--------------|
|
||||||
|
| udp | 53 |
|
||||||
|
| tcp | 53 |
|
||||||
|
| tls | 853 |
|
||||||
|
| https | 443 |
|
||||||
|
| quic | 443 |
|
||||||
|
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dns.resolver.digitalcourage]
|
||||||
|
display_name = "Digitalcourage"
|
||||||
|
info_url = "https://digitalcourage.de/support/zensurfreier-dns-server"
|
||||||
|
aliases = ["dc","dc3","digitalcourage3"]
|
||||||
|
weight = 990
|
||||||
|
|
||||||
|
servers = ["5.9.164.112:853","[2a01:4f8:251:554::2]:853"]
|
||||||
|
protocol = "tls"
|
||||||
|
tls_dns_name = "dns3.digitalcourage.de"
|
||||||
|
```
|
||||||
|
|
||||||
|
## [geoip]
|
||||||
|
|
||||||
|
These options configure paths to maxmind (or compatible) databses. The Official databases are available after signing up on [maxmind.com](https://maxmind.com). (In case someone knows a similar source of IP to geolocation mapping under a less propritetary license please contact me.)
|
||||||
|
|
||||||
|
To get the full functionalityyou need the ASN and City databases in mmdb format.
|
||||||
|
|
||||||
|
The `asn_database` and `location_database` fields are for their filepaths.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```toml
|
||||||
|
[geoip]
|
||||||
|
asn_database = "mmdb/GeoLite2-ASN.mmdb"
|
||||||
|
location_database = "mmdb/GeoLite2-City.mmdb"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: When echoip-slatecave rececieves a `SIGUSR1` posix signal it will attempt to reload the mmdb files. This is useful for keeping the databses up to date without having to restart the service.
|
||||||
|
|
||||||
|
## [template]
|
||||||
|
|
||||||
|
### `template_location`
|
||||||
|
|
||||||
|
This option contains the path to the direcotry containing the templates. It is overridden by the `-t <template-location>` command line option.
|
||||||
|
|
||||||
|
It can contain a glob pattern, bit make sure to configure the [`static_location`](#static_location) if it does.
|
||||||
|
|
||||||
|
### `extra_config`
|
||||||
|
|
||||||
|
This points to the toml file containing the configuration for the template itself, its content depends on what the template expects. This option is overridden by the `-e <extra-config>` command line option.
|
||||||
|
|
||||||
|
### `text_user_agents`
|
||||||
|
|
||||||
|
A list of Prefixes of UserAgents that should be served plain text by default.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```toml
|
||||||
|
[template]
|
||||||
|
# Give curl the plain text version by default
|
||||||
|
text_user_agents = ["curl/"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## [ratelimit]
|
||||||
|
|
||||||
|
Configure a Quota for the Rate limiter.
|
||||||
|
|
||||||
|
Note: The ratelimiter depends on the [ip_header](#ip_header) setting to be coorect, otherwise spoofing is possible enabling Denail of Service type attacks.
|
||||||
|
|
||||||
|
`per_minute`
|
||||||
|
: Integer of how many requests are allowed (and regnerate) per minute.
|
||||||
|
|
||||||
|
`burst`
|
||||||
|
: Integer of how many requests are additionally allowed.
|
||||||
|
|
||||||
|
Note: The ratelimit is implemented using the [governor](https://docs.rs/governor/0.6.0/governor/) crate.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[ratelimit]
|
||||||
|
per_minute = 20
|
||||||
|
burst = 15
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows up to 20+15=35 rquests without running into any limit. For every 3 seconds passing one additional request is granted (up to the limit of 35), which amounts to 60/3 = 20 requests per minute.
|
66
README.md
66
README.md
@ -1,16 +1,25 @@
|
|||||||
# 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`.
|
||||||
|
|
||||||
To make a release build (the one you want to have on your server) run `cargo build --relese`, the binary will end up in `target/release/echoip-slatecave`.
|
To make a release build (the one you want to have on your server) run `cargo build --release`, the binary will end up in `target/release/echoip-slatecave`.
|
||||||
|
|
||||||
NOTE: As of 2023-02-18 You need at least version 1.65 of the rust compiler. Consider using rustup.
|
|
||||||
|
|
||||||
## Usage and configuration
|
## Usage and configuration
|
||||||
|
|
||||||
@ -36,14 +45,47 @@ 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.
|
||||||
|
|
||||||
|
### Geolocation databases
|
||||||
|
|
||||||
|
For geolocation to work you need a MaxMind format database, for full functionality you need the GeoLite2-ASN and GeoLite2-City databses. Unfortunately you have to sign up with [MaxMind](https://maxmind.com) to obtain them. Once you have a license key there is a helper script in [contrib/maxmind-download.sh](contrib/maxmind-download.sh) that helps you with keeping the databse updated.
|
||||||
|
|
||||||
|
As an alternative to MaxMind there is also [DB-IP who offer their free databases without a login](https://db-ip.com/db/lite.php). You want the City and ASN databases in mmdb format.
|
||||||
|
|
||||||
|
**Don't forget to set the atttribution in the template configuration appropriately.**
|
||||||
|
|
||||||
|
See the file `templates/extra.toml`.
|
||||||
|
|
||||||
|
Since v1.0 echoip-slatecave reloads the databses when it rececieves a `USR1` signal.
|
||||||
|
|
||||||
## Security Considerations
|
## Security Considerations
|
||||||
|
|
||||||
### Information disclosure
|
### Information disclosure
|
||||||
@ -56,10 +98,20 @@ 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.
|
||||||
The default configuration is pretty liberal so that the average human probably won't notice the rate limit, but a misbehavin bot will be limited to one request every 3 seconds after 15 requests.
|
The default configuration is pretty liberal so that the average human probably won't notice the rate limit, but a misbehavingig bot will be limited to one request every 3 seconds after 15 requests.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
* [ ] Add a way to configure just the dns server addresses and derive the port from the protocol.
|
||||||
|
* [ ] Add an about page for the system resolver
|
||||||
|
* [ ] Expose DNS responses from the additional on the web interface
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
174
contrib/maxmind-download.sh
Normal file
174
contrib/maxmind-download.sh
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Settings variables go here
|
||||||
|
GEOIP_LICENSE_KEY="$GEOIP_LICENSE_KEY"
|
||||||
|
PRODUCTS=()
|
||||||
|
DESTINATION_DIRECTORY="."
|
||||||
|
DOWNLOAD_LOCATION=""
|
||||||
|
COLOR_OUTPUT=""
|
||||||
|
[ -t 1 ] && COLOR_OUTPUT="y"
|
||||||
|
|
||||||
|
msg() {
|
||||||
|
COLOR=""
|
||||||
|
COLOR_RST=""
|
||||||
|
if [ -n "$COLOR_OUTPUT" ] ; then
|
||||||
|
COLOR_RST="\e[00m"
|
||||||
|
case "$2" in
|
||||||
|
error|init_error) COLOR="\e[01;31m" ;;
|
||||||
|
success) COLOR="\e[32m" ;;
|
||||||
|
*) ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
printf "$COLOR%s$COLOR_RST\n" "$1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: maxmind-download.sh [OPTIONS]...
|
||||||
|
|
||||||
|
OPTIONS
|
||||||
|
--license-key <key>
|
||||||
|
Set the licencse key that is unfortunately
|
||||||
|
needed for a successful download.
|
||||||
|
--product <id>
|
||||||
|
Which product to download
|
||||||
|
maxmind calls this the EditionID
|
||||||
|
--GeoLite2-mmdb-all
|
||||||
|
Selects all the GeoLite2 Products in mmdb
|
||||||
|
format, hoefully saves some headaces.
|
||||||
|
Will download:
|
||||||
|
* GeoLite2-ASN
|
||||||
|
* GeoLite2-City
|
||||||
|
* GeoLite2-Country
|
||||||
|
--to <destination>
|
||||||
|
Directory to place the downloded files in.
|
||||||
|
Filename will be <product>.(mmdb|csv)
|
||||||
|
--download-to <destination>
|
||||||
|
Directory to download to.
|
||||||
|
If specified, the files in the --to directory
|
||||||
|
will only be replaced if the download was successful.
|
||||||
|
--color
|
||||||
|
--no-color Explicitly enable or disable colored output.
|
||||||
|
--help Show this help message
|
||||||
|
|
||||||
|
ENVOIRNMENT
|
||||||
|
GEOIP_LICENSE_KEY can be used to set the licencse key.
|
||||||
|
|
||||||
|
EXIT CODES
|
||||||
|
1 Invalid paramters or filesystem envoirnment
|
||||||
|
2 Download failed
|
||||||
|
3 Expected file not found in download
|
||||||
|
4 Failed to extract download
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ "$#" -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--license-key) GEOIP_LICENSE_KEY="$2"; shift 2;;
|
||||||
|
--product) PRODUCTS+=("$2"); shift 2;;
|
||||||
|
--GeoLite2-mmdb-all)
|
||||||
|
PRODUCTS+=("GeoLite2-ASN")
|
||||||
|
PRODUCTS+=("GeoLite2-City")
|
||||||
|
PRODUCTS+=("GeoLite2-Country")
|
||||||
|
shift 1;;
|
||||||
|
--to) DESTINATION_DIRECTORY="$2"; shift 2;;
|
||||||
|
--download-to) DOWNLOAD_LOCATION="$2"; shift 2;;
|
||||||
|
--color) COLOR_OUTPUT="y"; shift 1;;
|
||||||
|
--no-color) COLOR_OUTPUT=""; shift 1;;
|
||||||
|
|
||||||
|
--help) show_help; exit 0;;
|
||||||
|
*) printf "Unknown option: %s\n" "$1"; exit 1;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$GEOIP_LICENSE_KEY" ] ; then
|
||||||
|
msg "No License Key specified, the download won't work this way." init_error
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "$DOWNLOAD_LOCATION" ] || DOWNLOAD_LOCATION="$DESTINATION_DIRECTORY"
|
||||||
|
|
||||||
|
if [ -d "$DESTINATION_DIRECTORY" ] || mkdir -p "$DESTINATION_DIRECTORY" ; then
|
||||||
|
true
|
||||||
|
else
|
||||||
|
msg "Destination is not a directory and can't be created!" init_error
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$DOWNLOAD_LOCATION" ] || mkdir -p "$DOWNLOAD_LOCATION" ; then
|
||||||
|
true
|
||||||
|
else
|
||||||
|
msg "Dowload location is not a directory and can't be created!" init_error
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#PRODUCTS[@]}" -eq "0" ] ; then
|
||||||
|
msg "No products specified, nothing to do." init_error
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
get_product_file_ext() {
|
||||||
|
if printf "%s" "$1" | grep -q 'CSV$' ; then
|
||||||
|
echo csv
|
||||||
|
else
|
||||||
|
echo mmdb
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# PrductID,DOWNLOAD_LOCATION
|
||||||
|
download_maxmind_db() {
|
||||||
|
msg "Downloading Database $1 …" progess
|
||||||
|
# the path to download to
|
||||||
|
dl="$2/$1.tar.gz"
|
||||||
|
curl -fsSL -m 40 "https://download.maxmind.com/app/geoip_download?edition_id=$1&license_key=$GEOIP_LICENSE_KEY&suffix=tar.gz" > "$dl"
|
||||||
|
if [ "_$?" != "_0" ] ; then
|
||||||
|
msg "Databse download of $1 failed!" error
|
||||||
|
rm "$dl"
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
EXT="$(get_product_file_ext "$1")"
|
||||||
|
FILE_TO_EXTRACT="$(tar -tzf "$dl" | grep "/$1\.$EXT$")"
|
||||||
|
if [ -z "$FILE_TO_EXTRACT" ] ; then
|
||||||
|
msg "No .$EXT file found in the downloaded data!" error
|
||||||
|
rm "$dl"
|
||||||
|
return 3
|
||||||
|
fi
|
||||||
|
msg "Extracting $FILE_TO_EXTRACT from downloaded archive …" progess
|
||||||
|
if tar -C "$2" --strip-components=1 -xzf "$dl" "$FILE_TO_EXTRACT" ; then
|
||||||
|
msg "File extracted successfully." success
|
||||||
|
rm "$dl"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
msg "File extraction failed!" error
|
||||||
|
rm "$dl"
|
||||||
|
return 4
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
EXIT_CODE=""
|
||||||
|
MSG_OUTPUT_TO_LOG="y"
|
||||||
|
for product in "${PRODUCTS[@]}" ; do
|
||||||
|
download_maxmind_db "$product" "$DOWNLOAD_LOCATION"
|
||||||
|
RETCODE="$?"
|
||||||
|
|
||||||
|
if [ "_$RETCODE" = "_0" ] ; then
|
||||||
|
filename="$product.$(get_product_file_ext "$product")"
|
||||||
|
if [ "_$DOWNLOAD_LOCATION" != "_$DESTINATION_DIRECTORY" ] ; then
|
||||||
|
msg "Moving destination file …" progess
|
||||||
|
if [ -e "$DESTINATION_DIRECTORY/$filename" ] ; then
|
||||||
|
[ -e "$DESTINATION_DIRECTORY/$filename.bak" ] && rm "$DESTINATION_DIRECTORY/$filename.bak"
|
||||||
|
mv "$DESTINATION_DIRECTORY/$filename" "$DESTINATION_DIRECTORY/$filename.bak"
|
||||||
|
fi
|
||||||
|
if mv "$DOWNLOAD_LOCATION/$filename" "$DESTINATION_DIRECTORY/$filename" ; then
|
||||||
|
msg "File $filename installed successfully." success
|
||||||
|
else
|
||||||
|
msg "Failed to install $filename!" error
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
[ -n "$EXIT_CODE" ] && [ "$EXIT_CODE" -lt "$RETCODE" ] || export EXIT_CODE="$RETCODE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
exit $EXIT_CODE
|
@ -5,7 +5,7 @@
|
|||||||
listen_on = "127.0.0.1:3000"
|
listen_on = "127.0.0.1:3000"
|
||||||
|
|
||||||
# What header your reverse proxy sets that contains the real ip-address
|
# What header your reverse proxy sets that contains the real ip-address
|
||||||
# Possible Values: Every Vaiation of SecureClientIpSource in the axum_client_ip package
|
# Possible Values: Every Variation of SecureClientIpSource in the axum_client_ip package
|
||||||
# https://docs.rs/axum-client-ip/latest/axum_client_ip/enum.SecureClientIpSource.html
|
# https://docs.rs/axum-client-ip/latest/axum_client_ip/enum.SecureClientIpSource.html
|
||||||
#ip_header = "RightmostXForwardedFor"
|
#ip_header = "RightmostXForwardedFor"
|
||||||
# When you don't want to use a proxy server:
|
# When you don't want to use a proxy server:
|
||||||
@ -35,7 +35,7 @@ hidden_suffixes = [".com"]
|
|||||||
asn_database = "mmdb/GeoLite2-ASN.mmdb"
|
asn_database = "mmdb/GeoLite2-ASN.mmdb"
|
||||||
location_database = "mmdb/GeoLite2-City.mmdb"
|
location_database = "mmdb/GeoLite2-City.mmdb"
|
||||||
|
|
||||||
# If anyone knows a free (as in freedom) groip database
|
# If anyone knows a free (as in freedom) geoip database
|
||||||
# please open an issue so I can integrate it
|
# please open an issue so I can integrate it
|
||||||
# https://codeberg.org/slatian/service.echoip-slatecave
|
# https://codeberg.org/slatian/service.echoip-slatecave
|
||||||
|
|
||||||
@ -61,3 +61,61 @@ burst = 15
|
|||||||
|
|
||||||
#Note: The ratelimit is implemented using the governor crate
|
#Note: The ratelimit is implemented using the governor crate
|
||||||
|
|
||||||
|
[dns.resolver.digitalcourage]
|
||||||
|
display_name = "Digitalcourage"
|
||||||
|
info_url = "https://digitalcourage.de/support/zensurfreier-dns-server"
|
||||||
|
aliases = ["dc","dc3","digitalcourage3"]
|
||||||
|
weight = 990
|
||||||
|
|
||||||
|
servers = ["5.9.164.112:853","[2a01:4f8:251:554::2]:853"]
|
||||||
|
protocol = "tls"
|
||||||
|
tls_dns_name = "dns3.digitalcourage.de"
|
||||||
|
|
||||||
|
[dns.resolver.quad9]
|
||||||
|
display_name = "Quad9"
|
||||||
|
info_url = "https://www.quad9.net/service/service-addresses-and-features/"
|
||||||
|
aliases = ["q9","9999"]
|
||||||
|
weight = 980
|
||||||
|
|
||||||
|
servers = ["9.9.9.9:853", "149.112.112.112:853", "[2620:fe::fe]:853", "[2620:fe::9]:853"]
|
||||||
|
protocol = "tls"
|
||||||
|
tls_dns_name = "dns.quad9.net"
|
||||||
|
|
||||||
|
[dns.resolver.quad9_ecs]
|
||||||
|
display_name = "Quad9 with ecs"
|
||||||
|
info_url = "https://www.quad9.net/service/service-addresses-and-features/"
|
||||||
|
aliases = ["q9ecs","9999ecs","ecs"]
|
||||||
|
weight = 970
|
||||||
|
|
||||||
|
servers = ["9.9.9.11:853", "149.112.112.11:853", "[2620:fe::fe:11]:853", "[2620:fe::11]:853"]
|
||||||
|
protocol = "tls"
|
||||||
|
tls_dns_name = "dns11.quad9.net"
|
||||||
|
|
||||||
|
[dns.resolver.quad9_unvalidated]
|
||||||
|
display_name = "Quad9 unvalidated"
|
||||||
|
info_url = "https://www.quad9.net/service/service-addresses-and-features/"
|
||||||
|
aliases = ["q9u","9999u"]
|
||||||
|
weight = 960
|
||||||
|
|
||||||
|
servers = ["9.9.9.10:853", "149.112.112.10:853", "[2620:fe::fe:10]:853", "[2620:fe::10]:853"]
|
||||||
|
protocol = "tls"
|
||||||
|
tls_dns_name = "dns10.quad9.net"
|
||||||
|
|
||||||
|
[dns.resolver.cloudflare]
|
||||||
|
display_name = "Cloudflare"
|
||||||
|
info_url = "https://www.cloudflare.com/dns/"
|
||||||
|
aliases = ["cf","1111"]
|
||||||
|
weight = 450
|
||||||
|
|
||||||
|
servers = ["1.1.1.1:853", "1.0.0.1:853", "[2606:4700:4700::1111]:853", "[2606:4700:4700::1001]:853"]
|
||||||
|
protocol = "tls"
|
||||||
|
tls_dns_name = "cloudflare-dns.com"
|
||||||
|
|
||||||
|
[dns.resolver.google]
|
||||||
|
display_name = "Google"
|
||||||
|
info_url = "https://developers.google.com/speed/public-dns/docs/using"
|
||||||
|
aliases = ["goo","8888"]
|
||||||
|
weight = 440
|
||||||
|
|
||||||
|
servers = ["8.8.8.8:53", "8.4.4.8:53", "[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53"]
|
||||||
|
protocol = "udp"
|
||||||
|
111
src/config/dns.rs
Normal file
111
src/config/dns.rs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
use serde::{Deserialize,Serialize};
|
||||||
|
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::net::SocketAddr;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct DnsConfig {
|
||||||
|
pub allow_forward_lookup: bool,
|
||||||
|
pub allow_reverse_lookup: bool,
|
||||||
|
pub hidden_suffixes: Vec<String>,
|
||||||
|
pub resolver: HashMap<Arc<str>,DnsResolverConfig>,
|
||||||
|
|
||||||
|
pub enable_system_resolver: bool,
|
||||||
|
pub system_resolver_name: Arc<str>,
|
||||||
|
pub system_resolver_weight: i32,
|
||||||
|
pub system_resolver_id: Arc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
|
#[serde(rename_all="lowercase")]
|
||||||
|
pub enum DnsProtocol {
|
||||||
|
Udp,
|
||||||
|
Tcp,
|
||||||
|
Tls,
|
||||||
|
Https,
|
||||||
|
Quic,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
|
pub struct DnsResolverConfig {
|
||||||
|
pub display_name: Arc<str>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub info_url: Option<Arc<str>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub aliases: Vec<Arc<str>>,
|
||||||
|
#[serde(default="zero")]
|
||||||
|
pub weight: i32,
|
||||||
|
pub servers: Vec<SocketAddr>,
|
||||||
|
pub protocol: DnsProtocol,
|
||||||
|
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
|
||||||
|
pub bind_address: Option<SocketAddr>,
|
||||||
|
#[serde(default="default_true", alias="trust_nx_responses")]
|
||||||
|
pub trust_negative_responses: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn zero() -> i32 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_true() -> bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DnsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
DnsConfig {
|
||||||
|
allow_forward_lookup: true,
|
||||||
|
allow_reverse_lookup: false,
|
||||||
|
hidden_suffixes: Vec::new(),
|
||||||
|
resolver: Default::default(),
|
||||||
|
|
||||||
|
enable_system_resolver: true,
|
||||||
|
system_resolver_name: "System".into(),
|
||||||
|
system_resolver_weight: 1000,
|
||||||
|
system_resolver_id: "system".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DnsProtocol> for Protocol {
|
||||||
|
fn from(value: DnsProtocol) -> Self {
|
||||||
|
match value {
|
||||||
|
DnsProtocol::Udp => Protocol::Udp,
|
||||||
|
DnsProtocol::Tcp => Protocol::Tcp,
|
||||||
|
DnsProtocol::Tls => Protocol::Tls,
|
||||||
|
DnsProtocol::Https => Protocol::Https,
|
||||||
|
DnsProtocol::Quic => Protocol::Quic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DnsResolverConfig {
|
||||||
|
pub fn to_hickory_resolver_config(
|
||||||
|
&self
|
||||||
|
) -> HickoryResolverConfig {
|
||||||
|
let mut resolver = HickoryResolverConfig::new();
|
||||||
|
for server in &self.servers {
|
||||||
|
resolver.add_name_server(NameServerConfig{
|
||||||
|
socket_addr: *server,
|
||||||
|
protocol: self.protocol.clone().into(),
|
||||||
|
tls_dns_name: self.tls_dns_name.clone().map(|s| s.to_string()),
|
||||||
|
http_endpoint: self.http_endpoint.as_deref().map(ToString::to_string),
|
||||||
|
trust_negative_responses: self.trust_negative_responses,
|
||||||
|
bind_addr: self.bind_address,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Not configuring domain search here because searching
|
||||||
|
// on the resolver level is a bad idea unless we are
|
||||||
|
// taling about the system resolver which we
|
||||||
|
// can't tell what to do (which is good!)
|
||||||
|
return resolver;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,14 @@
|
|||||||
use axum_client_ip::SecureClientIpSource;
|
use axum_client_ip::SecureClientIpSource;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Default, Clone)]
|
mod dns;
|
||||||
|
|
||||||
|
pub use crate::config::dns::{DnsConfig, DnsResolverConfig};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default, Clone)]
|
||||||
pub struct EchoIpServiceConfig {
|
pub struct EchoIpServiceConfig {
|
||||||
pub server: ServerConfig,
|
pub server: ServerConfig,
|
||||||
pub dns: DnsConfig,
|
pub dns: DnsConfig,
|
||||||
@ -11,59 +17,41 @@ pub struct EchoIpServiceConfig {
|
|||||||
pub ratelimit: RatelimitConfig,
|
pub ratelimit: RatelimitConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
pub listen_on: SocketAddr,
|
pub listen_on: SocketAddr,
|
||||||
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>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone)]
|
|
||||||
pub struct DnsConfig {
|
|
||||||
pub allow_forward_lookup: bool,
|
|
||||||
pub allow_reverse_lookup: bool,
|
|
||||||
pub hidden_suffixes: Vec<String>,
|
|
||||||
//Future Idea: allow custom resolver
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
pub struct GeoIpConfig {
|
pub struct GeoIpConfig {
|
||||||
pub asn_database: Option<String>,
|
pub asn_database: Option<String>,
|
||||||
pub location_database: Option<String>,
|
pub location_database: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
pub struct TemplateConfig {
|
pub struct TemplateConfig {
|
||||||
pub template_location: String,
|
pub template_location: String,
|
||||||
pub extra_config: Option<String>,
|
pub extra_config: Option<String>,
|
||||||
pub text_user_agents: Vec<String>,
|
pub text_user_agents: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
pub struct RatelimitConfig {
|
pub struct RatelimitConfig {
|
||||||
pub per_minute: NonZeroU32,
|
pub per_minute: NonZeroU32,
|
||||||
pub burst: NonZeroU32,
|
pub burst: NonZeroU32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Default for ServerConfig {
|
impl Default for ServerConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
ServerConfig {
|
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DnsConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
DnsConfig {
|
|
||||||
allow_forward_lookup: true,
|
|
||||||
allow_reverse_lookup: false,
|
|
||||||
hidden_suffixes: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,3 +83,4 @@ impl Default for RatelimitConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
84
src/geoip.rs
84
src/geoip.rs
@ -3,9 +3,12 @@
|
|||||||
* 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 std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@ -47,12 +50,13 @@ pub struct AsnResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct MMDBCarrier {
|
pub struct MMDBCarrier {
|
||||||
pub mmdb: Option<maxminddb::Reader<Vec<u8>>>,
|
pub mmdb: RwLock<Option<maxminddb::Reader<Vec<u8>>>>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@ -63,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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,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,
|
||||||
@ -86,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,
|
||||||
@ -94,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,
|
||||||
@ -102,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,
|
||||||
@ -110,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,
|
||||||
@ -122,7 +126,8 @@ pub fn geoip2_subdivision_to_named_location(item: geoip2::city::Subdivision, lan
|
|||||||
|
|
||||||
impl QueryAsn for MMDBCarrier {
|
impl QueryAsn for MMDBCarrier {
|
||||||
fn query_asn_for_ip(&self, address: &IpAddr) -> Option<AsnResult> {
|
fn query_asn_for_ip(&self, address: &IpAddr) -> Option<AsnResult> {
|
||||||
match &self.mmdb {
|
let mmdb = self.mmdb.read();
|
||||||
|
match &*mmdb {
|
||||||
Some(mmdb) => {
|
Some(mmdb) => {
|
||||||
match mmdb.lookup::<geoip2::Asn>(*address) {
|
match mmdb.lookup::<geoip2::Asn>(*address) {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
@ -131,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -143,8 +154,9 @@ 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> {
|
||||||
match &self.mmdb {
|
let mmdb = self.mmdb.read();
|
||||||
|
match &*mmdb {
|
||||||
Some(mmdb) => {
|
Some(mmdb) => {
|
||||||
match mmdb.lookup::<geoip2::City>(*address) {
|
match mmdb.lookup::<geoip2::City>(*address) {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
@ -198,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -210,23 +228,39 @@ impl QueryLocation for MMDBCarrier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MMDBCarrier {
|
impl MMDBCarrier {
|
||||||
pub fn load_database_from_path(&mut self, path: &Path) -> Result<(),maxminddb::MaxMindDBError> {
|
pub fn new(name: String, path: Option<String>) -> MMDBCarrier {
|
||||||
println!("Loading {} from '{}' ...", &self.name, path.display());
|
MMDBCarrier {
|
||||||
|
mmdb: RwLock::new(None),
|
||||||
|
name: name,
|
||||||
|
path: path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reload_database(&self) -> Result<(),maxminddb::MaxMindDBError> {
|
||||||
|
match &self.path {
|
||||||
|
Some(path) => self.load_database_from_path(Path::new(&path)),
|
||||||
|
None => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_database_from_path(&self, path: &Path) -> Result<(),maxminddb::MaxMindDBError> {
|
||||||
|
let mut mmdb = self.mmdb.write();
|
||||||
|
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 self.mmdb.is_some() {
|
let wording = if mmdb.is_some() {
|
||||||
"Replaced old"
|
"Replaced old"
|
||||||
} else {
|
} else {
|
||||||
"Loaded new"
|
"Loaded new"
|
||||||
};
|
};
|
||||||
self.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 self.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
687
src/main.rs
687
src/main.rs
@ -1,44 +1,61 @@
|
|||||||
|
|
||||||
|
#![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 tera::Tera;
|
use serde::{Deserialize,Serialize};
|
||||||
use tower::ServiceBuilder;
|
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use trust_dns_resolver::{
|
use tower::ServiceBuilder;
|
||||||
TokioAsyncResolver,
|
|
||||||
// config::ResolverOpts,
|
|
||||||
// config::ResolverConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
use std::fs;
|
use tokio::signal::unix::{
|
||||||
|
signal,
|
||||||
|
SignalKind,
|
||||||
|
};
|
||||||
|
use tokio::task;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::path::Path;
|
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 ipinfo;
|
|
||||||
mod ratelimit;
|
|
||||||
mod simple_dns;
|
|
||||||
mod templating_engine;
|
|
||||||
mod idna;
|
mod idna;
|
||||||
|
mod ipinfo;
|
||||||
|
mod nat64;
|
||||||
|
mod ratelimit;
|
||||||
|
mod settings;
|
||||||
|
mod simple_dns;
|
||||||
|
mod view;
|
||||||
|
|
||||||
use crate::geoip::{
|
use crate::geoip::{
|
||||||
QueryAsn,
|
QueryAsn,
|
||||||
@ -47,54 +64,84 @@ use crate::geoip::{
|
|||||||
LocationResult,
|
LocationResult,
|
||||||
};
|
};
|
||||||
use crate::idna::IdnaName;
|
use crate::idna::IdnaName;
|
||||||
|
use crate::simple_dns::DnsLookupResult;
|
||||||
|
use crate::settings::*;
|
||||||
|
use crate::view::View;
|
||||||
|
use crate::ipinfo::{AddressInfo,AddressScope};
|
||||||
|
|
||||||
use crate::templating_engine::{
|
type TemplatingEngine = HumusEngine<View,QuerySettings,ResponseFormat>;
|
||||||
View,
|
|
||||||
ResponseFormat,
|
|
||||||
TemplateSettings,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::ipinfo::{AddressCast,AddressInfo,AddressScope};
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Clone)]
|
|
||||||
pub struct SettingsQuery {
|
pub struct SettingsQuery {
|
||||||
format: Option<ResponseFormat>,
|
format: Option<ResponseFormat>,
|
||||||
lang: Option<String>,
|
lang: Option<String>,
|
||||||
|
dns: Option<String>,
|
||||||
|
dns_self_lookup: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct SearchQuery {
|
pub struct SearchQuery {
|
||||||
query: Option<String>,
|
query: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Clone)]
|
|
||||||
|
/// 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)]
|
||||||
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<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(serde::Deserialize, serde::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: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct ServiceSharedState {
|
struct ServiceSharedState {
|
||||||
templating_engine: templating_engine::Engine,
|
templating_engine: TemplatingEngine,
|
||||||
dns_resolver: TokioAsyncResolver,
|
dns_resolvers: HashMap<Arc<str>,TokioResolver>,
|
||||||
asn_db: geoip::MMDBCarrier,
|
dns_resolver_aliases: HashMap<Arc<str>,Arc<str>>,
|
||||||
location_db: geoip::MMDBCarrier,
|
asn_db: geoip::MMDBCarrier,
|
||||||
config: config::EchoIpServiceConfig,
|
location_db: geoip::MMDBCarrier,
|
||||||
|
config: config::EchoIpServiceConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stores configuration that is derived from the original configuration
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct DerivedConfiguration {
|
||||||
|
dns_resolver_selectables: Vec<Selectable>,
|
||||||
|
default_resolver: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@ -103,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)]
|
||||||
@ -112,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) {
|
||||||
@ -122,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();
|
||||||
|
|
||||||
@ -148,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,109 +195,147 @@ 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
|
||||||
|
|
||||||
let mut asn_db = geoip::MMDBCarrier {
|
let asn_db = geoip::MMDBCarrier::new(
|
||||||
mmdb: None,
|
"GeoIP ASN Database".to_string(),
|
||||||
name: "GeoIP ASN Database".to_string(),
|
config.geoip.asn_database.clone()
|
||||||
};
|
);
|
||||||
match &config.geoip.asn_database {
|
|
||||||
Some(path) => { asn_db.load_database_from_path(Path::new(&path)).ok(); },
|
|
||||||
None => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut location_db = geoip::MMDBCarrier {
|
asn_db.reload_database().ok();
|
||||||
mmdb: None,
|
|
||||||
name: "GeoIP Location Database".to_string(),
|
let location_db = geoip::MMDBCarrier::new(
|
||||||
};
|
"GeoIP Location Database".to_string(),
|
||||||
match &config.geoip.location_database {
|
config.geoip.location_database.clone()
|
||||||
Some(path) => { location_db.load_database_from_path(Path::new(&path)).ok(); },
|
);
|
||||||
None => {},
|
|
||||||
}
|
location_db.reload_database().ok();
|
||||||
|
|
||||||
// Initalize DNS resolver with os defaults
|
// Initalize DNS resolver with os defaults
|
||||||
println!("Initalizing dns resolver ...");
|
info!("Initalizing dns resolvers ...");
|
||||||
|
|
||||||
println!("Using System configuration ...");
|
let mut dns_resolver_selectables = Vec::<Selectable>::new();
|
||||||
let res = TokioAsyncResolver::tokio_from_system_conf();
|
let mut dns_resolver_map: HashMap<Arc<str>,TokioResolver> = HashMap::new();
|
||||||
//let res = TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());
|
let mut dns_resolver_aliases: HashMap<Arc<str>,Arc<str>> = HashMap::new();
|
||||||
let dns_resolver = match res {
|
|
||||||
Ok(resolver) => resolver,
|
if config.dns.enable_system_resolver {
|
||||||
Err(e) => {
|
info!("Initalizing System resolver ...");
|
||||||
println!("Error while setting up dns resolver: {e}");
|
match initalize_system_resolver() {
|
||||||
::std::process::exit(1);
|
Ok(resolver) => {
|
||||||
|
info!("System resolver successfully Initalized.");
|
||||||
|
dns_resolver_map.insert(
|
||||||
|
config.dns.system_resolver_id.clone(),
|
||||||
|
resolver
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Problem setting up system resolver: {e}");
|
||||||
|
::std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
dns_resolver_selectables.push(Selectable {
|
||||||
|
id: config.dns.system_resolver_id.clone(),
|
||||||
|
name: config.dns.system_resolver_name.clone(),
|
||||||
|
weight: config.dns.system_resolver_weight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, resolver_config) in &config.dns.resolver {
|
||||||
|
info!("Initalizing {} resolver ...", key);
|
||||||
|
let resolver = TokioResolver::builder_with_config(
|
||||||
|
resolver_config.to_hickory_resolver_config(),
|
||||||
|
Default::default()
|
||||||
|
).build();
|
||||||
|
dns_resolver_map.insert(key.clone(), resolver);
|
||||||
|
dns_resolver_selectables.push(Selectable {
|
||||||
|
id: key.clone(),
|
||||||
|
name: resolver_config.display_name.clone(),
|
||||||
|
weight: resolver_config.weight,
|
||||||
|
});
|
||||||
|
for alias in &resolver_config.aliases {
|
||||||
|
dns_resolver_aliases.insert(alias.clone(),key.clone());
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
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
|
||||||
let shared_state = Arc::new(
|
let shared_state = Arc::new(
|
||||||
ServiceSharedState {
|
ServiceSharedState {
|
||||||
templating_engine: templating_engine,
|
templating_engine: templating_engine,
|
||||||
dns_resolver: dns_resolver,
|
dns_resolvers: dns_resolver_map,
|
||||||
|
dns_resolver_aliases: dns_resolver_aliases,
|
||||||
asn_db: asn_db,
|
asn_db: asn_db,
|
||||||
location_db: location_db,
|
location_db: location_db,
|
||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dns_resolver_selectables.sort_by(|a,b| b.weight.cmp(&a.weight));
|
||||||
|
let default_resolver = dns_resolver_selectables.first()
|
||||||
|
.map(|s| s.id.clone() )
|
||||||
|
.unwrap_or("none".into());
|
||||||
|
let derived_config = DerivedConfiguration {
|
||||||
|
dns_resolver_selectables: dns_resolver_selectables,
|
||||||
|
default_resolver: default_resolver,
|
||||||
|
};
|
||||||
|
|
||||||
|
let signal_usr1_handlers_state = shared_state.clone();
|
||||||
|
|
||||||
|
task::spawn(async move {
|
||||||
|
info!("Trying to register USR1 signal for reloading geoip databases");
|
||||||
|
let mut signal_stream = match signal(SignalKind::user_defined1()) {
|
||||||
|
Ok(signal_stream) => signal_stream,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error while registring signal handler: {e}");
|
||||||
|
warn!("Continuing without geoip reaload signal ...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
if signal_stream.recv().await.is_none() { return; }
|
||||||
|
info!("Received signal USR1, reloading geoip databses!");
|
||||||
|
signal_usr1_handlers_state.location_db.reload_database().ok();
|
||||||
|
signal_usr1_handlers_state.asn_db.reload_database().ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 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", 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))
|
||||||
.fallback_service(
|
.fallback_service(
|
||||||
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())
|
||||||
@ -269,72 +343,111 @@ async fn main() {
|
|||||||
config.ratelimit.per_minute, config.ratelimit.burst))
|
config.ratelimit.per_minute, config.ratelimit.burst))
|
||||||
.layer(middleware::from_fn(ratelimit::rate_limit_middleware))
|
.layer(middleware::from_fn(ratelimit::rate_limit_middleware))
|
||||||
.layer(Extension(config))
|
.layer(Extension(config))
|
||||||
.layer(middleware::from_fn(format_and_language_middleware))
|
.layer(Extension(derived_config))
|
||||||
|
.layer(middleware::from_fn_with_state(shared_state, settings_query_middleware))
|
||||||
)
|
)
|
||||||
;
|
;
|
||||||
|
|
||||||
println!("Starting Server ...");
|
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 format_and_language_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>,
|
||||||
|
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.clone();
|
||||||
|
let mut test_for_resolver = false;
|
||||||
|
|
||||||
|
if let Some(resolver_id) = query.dns {
|
||||||
|
dns_resolver_id = resolver_id.into();
|
||||||
|
test_for_resolver = true;
|
||||||
|
} else if let Some(cookie_header) = cookie_header {
|
||||||
|
if let Some(resolver_id) = cookie_header.0.get("dns_resolver") {
|
||||||
|
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() {
|
||||||
if let Some(TypedHeader(user_agent)) = user_agent_header {
|
if let Some(TypedHeader(user_agent)) = user_agent_header {
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Add the request settings extension
|
// Add the request settings extension
|
||||||
req.extensions_mut().insert(TemplateSettings{
|
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,
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn not_found_handler(
|
async fn not_found_handler(
|
||||||
State(arc_state): State<Arc<ServiceSharedState>>,
|
State(arc_state): State<Arc<ServiceSharedState>>,
|
||||||
Extension(settings): Extension<TemplateSettings>,
|
Extension(settings): Extension<QuerySettings>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
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::NotFound,
|
View::NotFound,
|
||||||
).await
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn hello_world_handler(
|
async fn hello_world_handler(
|
||||||
State(arc_state): State<Arc<ServiceSharedState>>,
|
State(arc_state): State<Arc<ServiceSharedState>>,
|
||||||
Extension(settings): Extension<TemplateSettings>,
|
Extension(settings): Extension<QuerySettings>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
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::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
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -347,127 +460,227 @@ async fn user_agent_handler(
|
|||||||
async fn handle_default_route(
|
async fn handle_default_route(
|
||||||
Query(search_query): Query<SearchQuery>,
|
Query(search_query): Query<SearchQuery>,
|
||||||
State(arc_state): State<Arc<ServiceSharedState>>,
|
State(arc_state): State<Arc<ServiceSharedState>>,
|
||||||
Extension(settings): Extension<TemplateSettings>,
|
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);
|
||||||
|
|
||||||
if let Some(search_query) = search_query.query {
|
if let Some(search_query) = search_query.query {
|
||||||
if search_query.trim() != "" {
|
if search_query.trim() != "" {
|
||||||
return handle_search_request(search_query, false, settings, state).await;
|
return handle_search_request(
|
||||||
|
search_query,
|
||||||
|
false,
|
||||||
|
settings,
|
||||||
|
state,
|
||||||
|
&client_ip
|
||||||
|
).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = get_ip_result(&address, &settings.lang, &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: TemplateSettings,
|
settings: QuerySettings,
|
||||||
arc_state: Arc<ServiceSharedState>,
|
arc_state: Arc<ServiceSharedState>,
|
||||||
|
client_ip: &IpAddr,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
|
||||||
let search_query = search_query.trim();
|
let mut search_query = search_query.trim().to_string();
|
||||||
|
let mut settings = settings;
|
||||||
lazy_static!{
|
|
||||||
static ref ASN_REGEX: Regex = Regex::new(r"^[Aa][Ss][Nn]?\s*(\d{1,7})$").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
//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) = via_cap.get(1) {
|
||||||
|
let state = Arc::clone(&arc_state);
|
||||||
|
if state.dns_resolvers.contains_key(via.as_str()) {
|
||||||
|
settings.dns_resolver_id = via.as_str().into();
|
||||||
|
} else if let Some(alias) = state.dns_resolver_aliases.get(via.as_str()) {
|
||||||
|
settings.dns_resolver_id = alias.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
search_query = VIA_REGEX.replace(&search_query,"").trim().to_string();
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
return handle_dig_request(
|
return handle_dig_request(
|
||||||
search_query.to_string(), settings, arc_state,
|
search_query,
|
||||||
|
settings,
|
||||||
|
arc_state,
|
||||||
!this_should_have_been_an_ip,
|
!this_should_have_been_an_ip,
|
||||||
).await
|
).await
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_ip_route_with_path(
|
async fn handle_dns_resolver_route(
|
||||||
Extension(settings): Extension<TemplateSettings>,
|
Extension(settings): Extension<QuerySettings>,
|
||||||
|
State(arc_state): State<Arc<ServiceSharedState>>,
|
||||||
|
) -> Response {
|
||||||
|
let state = Arc::clone(&arc_state);
|
||||||
|
state.templating_engine.render_view(
|
||||||
|
&settings,
|
||||||
|
View::DnsResolverList,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn handle_dns_resolver_route_with_path(
|
||||||
|
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>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Ok(address) = query.parse() {
|
let state = Arc::clone(&arc_state);
|
||||||
return handle_ip_request(address, settings, arc_state).await
|
if let Some(resolver) = state.config.dns.resolver.get(query.as_str()) {
|
||||||
|
state.templating_engine.render_view(
|
||||||
|
&settings,
|
||||||
|
View::DnsResolver{ config: resolver.clone() },
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return handle_search_request(query, true, settings, arc_state).await;
|
state.templating_engine.render_view(
|
||||||
|
&settings,
|
||||||
|
View::NotFound,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_ip_route_with_path(
|
||||||
|
Extension(settings): Extension<QuerySettings>,
|
||||||
|
State(arc_state): State<Arc<ServiceSharedState>>,
|
||||||
|
extract::Path(query): extract::Path<String>,
|
||||||
|
SecureClientIp(client_ip): SecureClientIp
|
||||||
|
) -> Response {
|
||||||
|
if let Ok(address) = query.parse() {
|
||||||
|
return handle_ip_request(address, settings, arc_state, &client_ip).await
|
||||||
|
} else {
|
||||||
|
return handle_search_request(query, true, settings, arc_state, &client_ip).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_ip_request(
|
async fn handle_ip_request(
|
||||||
address: IpAddr,
|
address: IpAddr,
|
||||||
settings: TemplateSettings,
|
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);
|
||||||
let result = get_ip_result(&address, &settings.lang, &state).await;
|
let result = get_ip_result(
|
||||||
|
&address,
|
||||||
|
&settings.lang,
|
||||||
|
&settings.dns_resolver_id,
|
||||||
|
settings.dns_disable_self_lookup,
|
||||||
|
client_ip,
|
||||||
|
&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: &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,
|
}
|
||||||
}
|
|
||||||
|
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 hostname = if state.config.dns.allow_reverse_lookup {
|
let mut hostname: Option<String> = None;
|
||||||
simple_dns::reverse_lookup(&state.dns_resolver, &address).await
|
let mut used_dns_resolver: Option<Arc<str>> = None;
|
||||||
} else {
|
if state.config.dns.allow_reverse_lookup && !reverse_dns_disabled_for_privacy {
|
||||||
None
|
if let Some(dns_resolver) = &state.dns_resolvers.get(dns_resolver_name) {
|
||||||
};
|
hostname = simple_dns::reverse_lookup(dns_resolver, address).await;
|
||||||
|
used_dns_resolver = Some(dns_resolver_name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// asn lookup
|
// asn lookup
|
||||||
let asn_result = state.asn_db.query_asn_for_ip(address);
|
let asn_result = state.asn_db.query_asn_for_ip(address);
|
||||||
@ -475,32 +688,31 @@ 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
|
||||||
let final_hostname = match hostname {
|
if let Some(name) = &hostname {
|
||||||
Some(name) => {
|
if match_domain_hidden_list(name, &state.config.dns.hidden_suffixes) {
|
||||||
if match_domain_hidden_list(&name, &state.config.dns.hidden_suffixes) {
|
hostname = None;
|
||||||
None
|
used_dns_resolver = None;
|
||||||
} else {
|
}
|
||||||
Some(name.to_owned())
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
IpResult{
|
IpResult{
|
||||||
|
mapping: mapping,
|
||||||
address: *address,
|
address: *address,
|
||||||
hostname: final_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,
|
||||||
|
reverse_dns_disabled_for_privacy: reverse_dns_disabled_for_privacy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_dig_route_with_path(
|
async fn handle_dig_route_with_path(
|
||||||
Extension(settings): Extension<TemplateSettings>,
|
Extension(settings): Extension<QuerySettings>,
|
||||||
State(arc_state): State<Arc<ServiceSharedState>>,
|
State(arc_state): State<Arc<ServiceSharedState>>,
|
||||||
extract::Path(name): extract::Path<String>,
|
extract::Path(name): extract::Path<String>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
@ -509,39 +721,84 @@ async fn handle_dig_route_with_path(
|
|||||||
|
|
||||||
async fn handle_dig_request(
|
async fn handle_dig_request(
|
||||||
dig_query: String,
|
dig_query: String,
|
||||||
settings: TemplateSettings,
|
settings: QuerySettings,
|
||||||
arc_state: Arc<ServiceSharedState>,
|
arc_state: Arc<ServiceSharedState>,
|
||||||
do_full_lookup: bool,
|
do_full_lookup: bool,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
|
||||||
let state = Arc::clone(&arc_state);
|
let state = Arc::clone(&arc_state);
|
||||||
|
|
||||||
let dig_result = get_dig_result(&dig_query, &state, do_full_lookup).await;
|
let dig_result = get_dig_result(
|
||||||
|
&dig_query,
|
||||||
|
&settings.dns_resolver_id,
|
||||||
|
&state,
|
||||||
|
do_full_lookup
|
||||||
|
).await;
|
||||||
|
|
||||||
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,
|
||||||
state: &ServiceSharedState,
|
dns_resolver_name: &Arc<str>,
|
||||||
do_full_lookup: bool,
|
state: &ServiceSharedState,
|
||||||
|
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();
|
||||||
if match_domain_hidden_list(&name, &state.config.dns.hidden_suffixes) {
|
let idna_name = IdnaName::from_str(name);
|
||||||
Default::default()
|
if let Some(dns_resolver) = state.dns_resolvers.get(dns_resolver_name) {
|
||||||
|
if let Ok(domain_name) = Name::from_str_relaxed(name.to_owned()+".") {
|
||||||
|
if match_domain_hidden_list(name, &state.config.dns.hidden_suffixes) {
|
||||||
|
// Try to hide the fact that we didn't do dns resolution at all
|
||||||
|
// We resolve example.org as basic avoidance of timing sidechannels.
|
||||||
|
// WARNING: this timing sidechannel avoidance is very crude.
|
||||||
|
simple_dns::lookup(
|
||||||
|
dns_resolver,
|
||||||
|
&Name::from_ascii("example.org.").expect("Static Dummy Name"),
|
||||||
|
do_full_lookup).await;
|
||||||
|
return DigResult {
|
||||||
|
records: DnsLookupResult{ nxdomain: true , ..Default::default() },
|
||||||
|
idn: idna_name,
|
||||||
|
partial_lookup: !do_full_lookup,
|
||||||
|
used_dns_resolver: dns_resolver_name.clone(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return DigResult {
|
||||||
|
records: simple_dns::lookup(
|
||||||
|
dns_resolver,
|
||||||
|
&domain_name,
|
||||||
|
do_full_lookup).await,
|
||||||
|
idn: idna_name,
|
||||||
|
partial_lookup: !do_full_lookup,
|
||||||
|
used_dns_resolver: dns_resolver_name.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Invalid domain name
|
||||||
|
return DigResult {
|
||||||
|
records: DnsLookupResult{
|
||||||
|
invalid_name: true,
|
||||||
|
.. Default::default()
|
||||||
|
},
|
||||||
|
idn: idna_name,
|
||||||
|
partial_lookup: !do_full_lookup,
|
||||||
|
used_dns_resolver: dns_resolver_name.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let idna_name = IdnaName::from_string(&name);
|
// Unknown resolver name
|
||||||
DigResult {
|
return DigResult {
|
||||||
records: simple_dns::lookup(
|
records: DnsLookupResult{
|
||||||
&state.dns_resolver,
|
unkown_resolver: true,
|
||||||
&(idna_name.idn.clone().unwrap_or(name.to_owned())+"."),
|
.. Default::default()
|
||||||
do_full_lookup).await,
|
},
|
||||||
idn: idna_name,
|
idn: idna_name,
|
||||||
partial_lookup: !do_full_lookup,
|
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
|
||||||
|
42
src/settings.rs
Normal file
42
src/settings.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use serde::{Deserialize,Serialize};
|
||||||
|
|
||||||
|
use lib_humus::HtmlTextJsonFormat;
|
||||||
|
use lib_humus::HumusQuerySettings;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/* Response format */
|
||||||
|
|
||||||
|
pub type ResponseFormat = HtmlTextJsonFormat;
|
||||||
|
|
||||||
|
/* Query and Template Settings */
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
|
pub struct QuerySettings {
|
||||||
|
pub format: ResponseFormat,
|
||||||
|
pub lang: String,
|
||||||
|
pub available_dns_resolvers: Vec<Selectable>,
|
||||||
|
pub dns_resolver_id: Arc<str>,
|
||||||
|
pub dns_disable_self_lookup: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
|
pub struct Selectable {
|
||||||
|
pub id: Arc<str>,
|
||||||
|
pub name: Arc<str>,
|
||||||
|
pub weight: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HumusQuerySettings<ResponseFormat> for QuerySettings {
|
||||||
|
|
||||||
|
fn initalize_template_context(&self, context: &mut tera::Context) {
|
||||||
|
context.insert("language", &self.lang);
|
||||||
|
context.insert("dns_resolvers", &self.available_dns_resolvers);
|
||||||
|
context.insert("dns_resolver_id", &self.dns_resolver_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_format(&self) -> ResponseFormat {
|
||||||
|
self.format.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,22 +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,
|
||||||
TokioAsyncResolver,
|
Name,
|
||||||
|
ResolveError,
|
||||||
|
ResolveErrorKind,
|
||||||
|
TokioResolver,
|
||||||
};
|
};
|
||||||
|
use log::{warn,error};
|
||||||
|
|
||||||
use tokio::join;
|
use tokio::join;
|
||||||
|
|
||||||
@ -27,20 +29,23 @@ use std::net::IpAddr;
|
|||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Default, Clone)]
|
#[derive(serde::Deserialize, serde::Serialize, Default, Clone)]
|
||||||
pub struct DnsLookupResult {
|
pub struct DnsLookupResult {
|
||||||
a: Option<Vec<IpAddr>>,
|
pub a: Option<Vec<IpAddr>>,
|
||||||
aaaa: Option<Vec<IpAddr>>,
|
pub aaaa: Option<Vec<IpAddr>>,
|
||||||
aname: Option<Vec<String>>,
|
pub aname: Option<Vec<String>>,
|
||||||
cname: Option<Vec<String>>,
|
pub cname: Option<Vec<String>>,
|
||||||
mx: Option<Vec<MxRecord>>,
|
pub mx: Option<Vec<MxRecord>>,
|
||||||
ns: Option<Vec<String>>,
|
pub ns: Option<Vec<String>>,
|
||||||
soa: Option<Vec<SoaRecord>>,
|
pub soa: Option<Vec<SoaRecord>>,
|
||||||
txt: Option<Vec<String>>,
|
pub txt: Option<Vec<String>>,
|
||||||
srv: Option<Vec<SrvRecord>>,
|
pub srv: Option<Vec<SrvRecord>>,
|
||||||
caa: Option<Vec<String>>,
|
pub caa: Option<Vec<String>>,
|
||||||
other_error: bool,
|
pub other_error: bool,
|
||||||
dns_error: bool,
|
pub dns_error: bool,
|
||||||
nxdomain: bool,
|
pub nxdomain: bool,
|
||||||
timeout: bool,
|
pub timeout: bool,
|
||||||
|
pub too_busy: bool,
|
||||||
|
pub invalid_name: bool,
|
||||||
|
pub unkown_resolver: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
#[derive(serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||||
@ -71,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
|
||||||
@ -85,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
|
||||||
@ -118,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{
|
||||||
@ -151,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}");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,34 +186,46 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
|
|||||||
RecordType::TXT => set_default_if_none(&mut dig_result.txt),
|
RecordType::TXT => set_default_if_none(&mut dig_result.txt),
|
||||||
_ => { /* This should not happen */ },
|
_ => { /* This should not happen */ },
|
||||||
};
|
};
|
||||||
for record in lookup.iter() {
|
let name = lookup.query().name();
|
||||||
add_record_to_lookup_result(dig_result, record);
|
for record in lookup.record_iter() {
|
||||||
|
if name == record.name() {
|
||||||
|
add_record_to_lookup_result(dig_result, record.data());
|
||||||
|
}
|
||||||
|
//TODO: handle additional responses
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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*/ },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,8 +236,8 @@ 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: &String,
|
name: &Name,
|
||||||
do_full_lookup: bool,
|
do_full_lookup: bool,
|
||||||
) -> DnsLookupResult {
|
) -> DnsLookupResult {
|
||||||
let (
|
let (
|
||||||
@ -219,10 +246,10 @@ pub async fn lookup(
|
|||||||
cname_lookup_res,
|
cname_lookup_res,
|
||||||
aname_lookup_res
|
aname_lookup_res
|
||||||
) = join!(
|
) = join!(
|
||||||
resolver.lookup(name, RecordType::A),
|
resolver.lookup(name.clone(), RecordType::A),
|
||||||
resolver.lookup(name, RecordType::AAAA),
|
resolver.lookup(name.clone(), RecordType::AAAA),
|
||||||
resolver.lookup(name, RecordType::CNAME),
|
resolver.lookup(name.clone(), RecordType::CNAME),
|
||||||
resolver.lookup(name, RecordType::ANAME),
|
resolver.lookup(name.clone(), RecordType::ANAME),
|
||||||
);
|
);
|
||||||
|
|
||||||
// initlize an empty lookup result
|
// initlize an empty lookup result
|
||||||
@ -243,12 +270,12 @@ pub async fn lookup(
|
|||||||
srv_lookup_res,
|
srv_lookup_res,
|
||||||
txt_lookup_res
|
txt_lookup_res
|
||||||
) = join!(
|
) = join!(
|
||||||
resolver.lookup(name, RecordType::MX),
|
resolver.lookup(name.clone(), RecordType::MX),
|
||||||
resolver.lookup(name, RecordType::NS),
|
resolver.lookup(name.clone(), RecordType::NS),
|
||||||
resolver.lookup(name, RecordType::SOA),
|
resolver.lookup(name.clone(), RecordType::SOA),
|
||||||
resolver.lookup(name, RecordType::CAA),
|
resolver.lookup(name.clone(), RecordType::CAA),
|
||||||
resolver.lookup(name, RecordType::SRV),
|
resolver.lookup(name.clone(), RecordType::SRV),
|
||||||
resolver.lookup(name, RecordType::TXT),
|
resolver.lookup(name.clone(), RecordType::TXT),
|
||||||
);
|
);
|
||||||
|
|
||||||
integrate_lookup_result(&mut dig_result, mx_lookup_res);
|
integrate_lookup_result(&mut dig_result, mx_lookup_res);
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
/*
|
|
||||||
* This is the echoip-slatecave templating engine.
|
|
||||||
* It wraps around tera in is specialized for echoip-slatecave.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use axum::{
|
|
||||||
http::StatusCode,
|
|
||||||
response::Html,
|
|
||||||
response::IntoResponse,
|
|
||||||
response::Response,
|
|
||||||
response::Json,
|
|
||||||
};
|
|
||||||
use tera::Tera;
|
|
||||||
use toml::Table;
|
|
||||||
|
|
||||||
use crate::DigResult;
|
|
||||||
use crate::IpResult;
|
|
||||||
|
|
||||||
/* Response format */
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Clone, Copy)]
|
|
||||||
pub enum ResponseFormat {
|
|
||||||
#[serde(rename="text/plain", alias="text")]
|
|
||||||
TextPlain,
|
|
||||||
#[serde(rename="text/html", alias="html")]
|
|
||||||
TextHtml,
|
|
||||||
#[serde(rename="application/json", alias="json")]
|
|
||||||
ApplicationJson,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToString for ResponseFormat {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
match self {
|
|
||||||
ResponseFormat::TextPlain => "text/plain",
|
|
||||||
ResponseFormat::TextHtml => "text/html",
|
|
||||||
ResponseFormat::ApplicationJson => "application/json",
|
|
||||||
}.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseFormat {
|
|
||||||
fn to_file_extension(&self) -> String {
|
|
||||||
match self {
|
|
||||||
ResponseFormat::TextPlain => ".txt",
|
|
||||||
ResponseFormat::TextHtml => ".html",
|
|
||||||
ResponseFormat::ApplicationJson => ".json",
|
|
||||||
}.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Template Settings */
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Clone)]
|
|
||||||
pub struct TemplateSettings {
|
|
||||||
pub format: ResponseFormat,
|
|
||||||
pub lang: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The echoip view */
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Clone)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum View {
|
|
||||||
Asn { asn: u32 },
|
|
||||||
Dig { query: String, result: DigResult },
|
|
||||||
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::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: &TemplateSettings,
|
|
||||||
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("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 index.html: {e:?}");
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
"Template error, contact owner or see logs.\n"
|
|
||||||
).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()
|
|
||||||
},
|
|
||||||
_ => Json(view).into_response(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match view {
|
|
||||||
View::NotFound => *response.status_mut() = StatusCode::NOT_FOUND,
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@
|
|||||||
<li><a href="{{ extra.base_url}}">The homepage</a></li>
|
<li><a href="{{ extra.base_url}}">The homepage</a></li>
|
||||||
<li>The <code>/ip/</code> or <code>/dig/</code> endpoints.</li>
|
<li>The <code>/ip/</code> or <code>/dig/</code> endpoints.</li>
|
||||||
<li><a href="{{ extra.base_url }}/ua">The <code>/ua</code> endpoint wich just displays your user agent.</a></li>
|
<li><a href="{{ extra.base_url }}/ua">The <code>/ua</code> endpoint wich just displays your user agent.</a></li>
|
||||||
|
<li><a href="{{ extra.base_url }}/dns_resolver">The list of configured dns resolvers.</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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,12 +27,23 @@
|
|||||||
<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."
|
||||||
placeholder="1.2.3.4, 2001::1:2:3:4, example.org, AS1234"
|
placeholder="1.2.3.4, 2001::1:2:3:4, example.org, AS1234"
|
||||||
value="{% if view == "dig" %}{{ data.query }}{% elif view == "ip" %}{{ data.result.address }}{% elif view == "asn"%}AS{{ data.asn }}{% endif %}"/>
|
value="{% if view == "dig" %}{{ data.query }}{% elif view == "ip" %}{{ data.result.address }}{% elif view == "asn"%}AS{{ data.asn }}{% endif %}"/>
|
||||||
|
{% if dns_resolvers | length > 1 %}
|
||||||
|
<select name="dns" title="DNS Resolver">
|
||||||
|
{% for r in dns_resolvers %}
|
||||||
|
<option value="{{ r.id }}" {% if r.id == dns_resolver_id %}selected{%endif%}>{{ r.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% endif %}
|
||||||
<input type="submit" value="Query"/>
|
<input type="submit" value="Query"/>
|
||||||
</form>
|
</form>
|
||||||
</nav>
|
</nav>
|
||||||
@ -43,7 +57,7 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<p>You can find the <a href="https://codeberg.org/slatian/service.echoip-slatecave">echoip-slatecave sourcecode on Codeberg</a>. If you found a bug or have an idea, feature, etc. please get in touch (I also accept E-Mails!).</p>
|
<p>You can find the <a href="https://codeberg.org/slatian/service.echoip-slatecave">echoip-slatecave sourcecode on Codeberg</a>. If you found a bug or have an idea, feature, etc. please get in touch (I also accept E-Mails!).</p>
|
||||||
<!-- If you made your own template, link to it here -->
|
<!-- If you made your own template, link to it here -->
|
||||||
<p>This service works in its current form because nobody is abusing it, please keep that in mind. If you want to do frequent automated requests please host your own instace. Thank you for being awesome!</p>
|
<p>This service works in its current form because nobody is abusing it, please keep that in mind. If you want to do frequent automated requests please host your own instance. Thank you for being awesome!</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -10,7 +10,7 @@ You can get the json version:
|
|||||||
{%- if view == "404" %}
|
{%- if view == "404" %}
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
You can query this service using the folowing endpoints …
|
You can query this service using the following endpoints …
|
||||||
=> {{ extra.base_url }}/ip/<address> … to query for an ip-address
|
=> {{ extra.base_url }}/ip/<address> … to query for an ip-address
|
||||||
=> {{ extra.base_url }}/dig/<name> … to query for a domain-name
|
=> {{ extra.base_url }}/dig/<name> … to query for a domain-name
|
||||||
=> {{ extra.base_url }}/ua … to get your user agent
|
=> {{ extra.base_url }}/ua … to get your user agent
|
||||||
@ -21,5 +21,5 @@ You can find the echoip-slatecave sourcecode on Codeberg.
|
|||||||
=> https://codeberg.org/slatian/service.echoip-slatecave
|
=> https://codeberg.org/slatian/service.echoip-slatecave
|
||||||
If you found a bug or have an idea, feature, etc. please get in touch (I also accept E-Mails!).
|
If you found a bug or have an idea, feature, etc. please get in touch (I also accept E-Mails!).
|
||||||
|
|
||||||
This service works in its current form because nobody is abusing it, please keep that in mind. If you want to do frequent automated requests please host your own instace. Thank you for being awesome!
|
This service works in its current form because nobody is abusing it, please keep that in mind. If you want to do frequent automated requests please host your own instance. Thank you for being awesome!
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
{% 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>{% 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 og_path %}/dig/{{ data.query | urlencode_strict }}{% endblock %}
|
{% block og_path %}/dig/{{ data.query | urlencode_strict }}{% endblock %}
|
||||||
|
|
||||||
@ -13,7 +15,7 @@
|
|||||||
{% set idn = data.result.idn %}
|
{% set idn = data.result.idn %}
|
||||||
<section>
|
<section>
|
||||||
<h2>Internationalized Domain Names</h2>
|
<h2>Internationalized Domain Names</h2>
|
||||||
<p>Because of some limitations the DNS has, Unicode caracters need a special encoding.</p>
|
<p>Because of some limitations the DNS has, Unicode characters need a special encoding.</p>
|
||||||
{% if idn.original_was == "unicode" %}
|
{% if idn.original_was == "unicode" %}
|
||||||
<p>Your Unicode query has been encoded as the <i>IDN</i> <code>{{ idn.idn }}</code> to generate the results below.</p>
|
<p>Your Unicode query has been encoded as the <i>IDN</i> <code>{{ idn.idn }}</code> to generate the results below.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -26,8 +28,16 @@
|
|||||||
<section>
|
<section>
|
||||||
<h2>DNS Records</h2>
|
<h2>DNS Records</h2>
|
||||||
|
|
||||||
{% if r.nxdomain %}
|
{% set show_nonpresent = true %}
|
||||||
<p class="error box">Our DNS-Server claims that this domain doesn't exist, you shouldn't see any results below.</p>
|
{% if r.unkown_resolver %}
|
||||||
|
<p class="error box">The resolver you chose is not one of the available ones, if you can reproduce this error by just using the UI <a href="https://codeberg.org/slatian/service.echoip-slatecave/issues/new">please report it</a>.</p>
|
||||||
|
{% set show_nonpresent = false %}
|
||||||
|
{% elif r.invalid_name %}
|
||||||
|
<p class="error box">This domain name does not conform to <a href="https://www.rfc-editor.org/info/std3">the dns specification (std3)</a> rules and was therefore not resolved.</p>
|
||||||
|
{% set show_nonpresent = false %}
|
||||||
|
{% elif r.nxdomain %}
|
||||||
|
<p class="error box">The DNS-Server claims that this domain doesn't exist, there shouldn't be any results.</p>
|
||||||
|
{% set show_nonpresent = false %}
|
||||||
{% elif r.timeout %}
|
{% elif r.timeout %}
|
||||||
<p class="error box">There was at least one timeout error while resolving this domain, the results below are incomplete.</p>
|
<p class="error box">There was at least one timeout error while resolving this domain, the results below are incomplete.</p>
|
||||||
{% elif r.other_error %}
|
{% elif r.other_error %}
|
||||||
@ -39,11 +49,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if r.cname %}
|
{% if r.cname %}
|
||||||
<p>This domain has a cname set, this means its contents are full replaced by the linked record.</p>
|
<p>This domain has a <code>CNAME</code> set, this means its contents are full replaced by the linked record.</p>
|
||||||
|
|
||||||
<p class="button-paragraph">{{ helper::dig(extra=extra, name=r.cname[0]) }}</p>
|
<p class="button-paragraph">{{ helper::dig(extra=extra, name=r.cname[0]) }}</p>
|
||||||
|
|
||||||
</p>Usually you get the <code>A</code> and <code>AAAA</code> records for the linked record to avoid uneccessary requests. If anything else resolves, that is a violation of the DNS specification.</p>
|
</p>Usually you get the <code>A</code> and <code>AAAA</code> records for the linked record to avoid unnecessary requests. If anything else resolves, that is a violation of the DNS specification.</p>
|
||||||
|
|
||||||
{% if r.cname | length > 1 %}
|
{% if r.cname | length > 1 %}
|
||||||
<p>This domain resolves to multiple <code>CNAME</code>s, this is not allowed by the DNS specification!</p>
|
<p>This domain resolves to multiple <code>CNAME</code>s, this is not allowed by the DNS specification!</p>
|
||||||
@ -66,7 +76,7 @@
|
|||||||
<li>{{ helper::ip(extra=extra, ip=address) }}</li>
|
<li>{{ helper::ip(extra=extra, ip=address) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% elif show_nonpresent %}
|
||||||
<p>No <code>A</code> (IPv4) Records.</p>
|
<p>No <code>A</code> (IPv4) Records.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -77,7 +87,7 @@
|
|||||||
<li>{{ helper::ip(extra=extra, ip=address) }}</li>
|
<li>{{ helper::ip(extra=extra, ip=address) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% elif show_nonpresent %}
|
||||||
<p>No <code>AAAA</code> (IPv6) Records.</p>
|
<p>No <code>AAAA</code> (IPv6) Records.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -90,7 +100,7 @@
|
|||||||
<li>{{ helper::dig(extra=extra, name=mx.exchange, fqdn=true, prefix=mx.preference) }}</li>
|
<li>{{ helper::dig(extra=extra, name=mx.exchange, fqdn=true, prefix=mx.preference) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% elif show_nonpresent %}
|
||||||
<p id="mx">No <code>MX</code> (Mail Exchange) records.</p>
|
<p id="mx">No <code>MX</code> (Mail Exchange) records.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -116,7 +126,7 @@
|
|||||||
</dl></li>
|
</dl></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% elif show_nonpresent %}
|
||||||
<p id="soa">No <code>SOA</code> records.</p>
|
<p id="soa">No <code>SOA</code> records.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -129,7 +139,7 @@
|
|||||||
<li>{{ helper::dig(extra=extra, name=ns) }}</li>
|
<li>{{ helper::dig(extra=extra, name=ns) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% elif show_nonpresent %}
|
||||||
<p id="ns">No <code>NS</code> (Name Server) records.</p>
|
<p id="ns">No <code>NS</code> (Name Server) records.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -141,7 +151,7 @@
|
|||||||
<li><code>{{caa}}</code></li>
|
<li><code>{{caa}}</code></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% elif show_nonpresent %}
|
||||||
<p id="caa">No <code>CAA</code> (<a target="_blank" href="https://de.wikipedia.org/wiki/DNS_Certification_Authority_Authorization">Certification Authority Authorization</a>) records.</p>
|
<p id="caa">No <code>CAA</code> (<a target="_blank" href="https://de.wikipedia.org/wiki/DNS_Certification_Authority_Authorization">Certification Authority Authorization</a>) records.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -152,7 +162,7 @@
|
|||||||
<li><code>{{txt}}</code></li>
|
<li><code>{{txt}}</code></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% elif show_nonpresent %}
|
||||||
<p id="txt">No <code>TXT</code> records.</p>
|
<p id="txt">No <code>TXT</code> records.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -172,7 +182,7 @@
|
|||||||
</dl></li>
|
</dl></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% elif show_nonpresent %}
|
||||||
<p id="srv">No <code>SRV</code> records.</p>
|
<p id="srv">No <code>SRV</code> records.</p>
|
||||||
<p><code>SRV</code> or Service records usually live on their own subdomains like {{ helper::dig(extra=extra, name="_xmpp-client._tcp."~data.query) }}.
|
<p><code>SRV</code> or Service records usually live on their own subdomains like {{ helper::dig(extra=extra, name="_xmpp-client._tcp."~data.query) }}.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -191,15 +201,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Programatic Lookup</h2>
|
<h2>Programmatic Lookup</h2>
|
||||||
<p>If you want to look up this information in another program the short answer is <b>don't, look up the names using your local DNS!</b></p>
|
<p>If you want to look up this information in another program the short answer is <b>don't, look up the names using your local DNS!</b></p>
|
||||||
<p>On most systems on the commandline you have commands like <code>host</code> and <code>dig</code> even when not present you can probably use <code>ping</code> as a workaround as it resolves the name and gives you the IP-Address it is pinging.</p>
|
<p>On most systems on the commandline you have commands like <code>host</code> and <code>dig</code> even when not present you can probably use <code>ping</code> as a workaround as it resolves the name and gives you the IP-Address it is pinging.</p>
|
||||||
<h3>Why queryting this service is still useful</h3>
|
<h3>Why querying this service is still useful</h3>
|
||||||
<p>This service most probably doesn't share its cache with your local resolver, this way you have a way to see if your DNS-change had the effect it should have.</p>
|
<p>This service most probably doesn't share its cache with your local resolver, this way you have a way to see if your DNS-change had the effect it should have.</p>
|
||||||
<p>It may also be useful for debugging other dns problems or to get around a local resolver that is lying to you because your ISP is a <i>something</i>.</p>
|
<p>It may also be useful for debugging other DNS problems or to get around a local resolver that is lying to you because your ISP is a <i>something</i>.</p>
|
||||||
<h3>How?</h3>
|
<h3>How?</h3>
|
||||||
<p>You can choose between the <code>html</code>, <code>text</code> and <code>json</code> format.</p>
|
<p>You can choose between the <code>html</code>, <code>text</code> and <code>json</code> format.</p>
|
||||||
<p>An example of an url could be: <code>{{ extra.base_url }}/dig/example.org?format=json</code></p>
|
<p>An example of an URL could be: <code>{{ extra.base_url }}/dig/example.org?format=json</code></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
{% block path %}dig/{{ data.query | urlencode_strict }}{% endblock %}
|
{% block path %}dig/{{ data.query | urlencode_strict }}{% endblock %}
|
||||||
|
|
||||||
{% block content -%}
|
{% set r = data.result.records %}
|
||||||
# dig {{data.query}}
|
{%- block content -%}
|
||||||
|
# dig {{data.query}} via {{ data.result.used_dns_resolver }}
|
||||||
|
|
||||||
{% if data.result.idn -%}
|
{% if data.result.idn -%}
|
||||||
{%- set idn = data.result.idn -%}
|
{%- set idn = data.result.idn -%}
|
||||||
## Internationalized Domain Names
|
## Internationalized Domain Names
|
||||||
|
|
||||||
Because of some limitations the DNS has, Unicode caracters need a special encoding.
|
Because of some limitations the DNS has, Unicode characters need a special encoding.
|
||||||
{%- if idn.original_was == "unicode" -%}
|
{%- if idn.original_was == "unicode" -%}
|
||||||
Your Unicode query has been encoded as an IDN to generate the results below.
|
Your Unicode query has been encoded as an IDN to generate the results below.
|
||||||
```
|
```
|
||||||
@ -25,8 +26,17 @@ Your IDN would decode to
|
|||||||
|
|
||||||
{% set r = data.result.records -%}
|
{% set r = data.result.records -%}
|
||||||
## DNS Records
|
## DNS Records
|
||||||
{% if r.nxdomain %}
|
{% if r.unkown_resolver %}
|
||||||
Our DNS-Server claims that this domain doesn't exist, you shouldn't see any results below.
|
{%- set show_nonpresent = false %}
|
||||||
|
The resolver you chose is not one of the available ones.
|
||||||
|
=> {{ extra.base_url }}/dns_resolver
|
||||||
|
{% elif r.invalid_name %}
|
||||||
|
{%- set show_nonpresent = false %}
|
||||||
|
This domain name does not conform to the dns specification (std3) rules and was therefore not resolved.
|
||||||
|
=> https://www.rfc-editor.org/info/std3
|
||||||
|
{% elif r.nxdomain %}
|
||||||
|
{%- set show_nonpresent = false %}
|
||||||
|
Our DNS-Server claims that this domain doesn't exist, there shouldn't be any results.
|
||||||
{%- elif r.timeout -%}
|
{%- elif r.timeout -%}
|
||||||
There was at least one timeout error while resolving this domain, the results below are incomplete.
|
There was at least one timeout error while resolving this domain, the results below are incomplete.
|
||||||
{%- elif r.other_error -%}
|
{%- elif r.other_error -%}
|
||||||
@ -43,7 +53,7 @@ This domain has a cname set, this means its contents are full replaced by the li
|
|||||||
|
|
||||||
{{ r.cname[0] }}
|
{{ r.cname[0] }}
|
||||||
|
|
||||||
Usually you get the A and AAAA records for the linked record to avoid uneccessary requests. If anything else resolves, that is a violation of the DNS specification.
|
Usually you get the A and AAAA records for the linked record to avoid unnecessary requests. If anything else resolves, that is a violation of the DNS specification.
|
||||||
{% if r.cname | length > 1 -%}
|
{% if r.cname | length > 1 -%}
|
||||||
This domain resolves to multiple CNAMEs, this is not allowed by the DNS specification!
|
This domain resolves to multiple CNAMEs, this is not allowed by the DNS specification!
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
@ -61,7 +71,7 @@ A (IPv4) records:
|
|||||||
{% for address in r.a -%}
|
{% for address in r.a -%}
|
||||||
* {{ address }}
|
* {{ address }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- else %}
|
{%- elif show_nonpresent %}
|
||||||
No A (IPv4) Records.
|
No A (IPv4) Records.
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
|
||||||
@ -70,7 +80,7 @@ AAAA (IPv6) records:
|
|||||||
{% for address in r.aaaa -%}
|
{% for address in r.aaaa -%}
|
||||||
* {{ address }}
|
* {{ address }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- else %}
|
{%- elif show_nonpresent %}
|
||||||
No AAAA (IPv6) Records.
|
No AAAA (IPv6) Records.
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
|
||||||
@ -81,7 +91,7 @@ MX (Mail Exchange) records:
|
|||||||
{% for mx in r.mx | sort(attribute="preference") | reverse -%}
|
{% for mx in r.mx | sort(attribute="preference") | reverse -%}
|
||||||
* {{ mx.preference }} {{ mx.exchange }}
|
* {{ mx.preference }} {{ mx.exchange }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- else %}
|
{%- elif show_nonpresent %}
|
||||||
No MX (Mail Exchange) records.
|
No MX (Mail Exchange) records.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -96,7 +106,7 @@ SOA (Source Of Authority) records:
|
|||||||
* expire: {{soa.expire / 3600 | round(precision=2)}}h
|
* expire: {{soa.expire / 3600 | round(precision=2)}}h
|
||||||
* minimum: {{soa.minimum / 60 | round(precision=2)}}m TTL
|
* minimum: {{soa.minimum / 60 | round(precision=2)}}m TTL
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- else %}
|
{%- elif show_nonpresent %}
|
||||||
No SOA (Source Of Authority) records.
|
No SOA (Source Of Authority) records.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -105,7 +115,7 @@ NS (Name Server) records:
|
|||||||
{% for ns in r.ns -%}
|
{% for ns in r.ns -%}
|
||||||
* {{ns}}
|
* {{ns}}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- else %}
|
{%- elif show_nonpresent %}
|
||||||
No NS (Name Server) records.
|
No NS (Name Server) records.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -114,7 +124,7 @@ CAA (Certification Authority Authorization) records:
|
|||||||
{% for caa in r.caa -%}
|
{% for caa in r.caa -%}
|
||||||
* {{caa}}
|
* {{caa}}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- else %}
|
{%- elif show_nonpresent %}
|
||||||
No CAA (Certification Authority Authorization) records.
|
No CAA (Certification Authority Authorization) records.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -123,7 +133,7 @@ TXT records:
|
|||||||
{% for txt in r.txt -%}
|
{% for txt in r.txt -%}
|
||||||
* {{txt}}
|
* {{txt}}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- else %}
|
{%- elif show_nonpresent %}
|
||||||
No TXT records.
|
No TXT records.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -135,7 +145,7 @@ SRV records:
|
|||||||
* Port: {{srv.port}}
|
* Port: {{srv.port}}
|
||||||
* Target: {{srv.target}}
|
* Target: {{srv.target}}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- else %}
|
{%- elif show_nonpresent %}
|
||||||
No SRV records.
|
No SRV records.
|
||||||
|
|
||||||
SRV or Service records usually live on their own subdomains like {{ "_xmpp-client._tcp."~data.query }}.
|
SRV or Service records usually live on their own subdomains like {{ "_xmpp-client._tcp."~data.query }}.
|
||||||
|
52
templates/dns_resolver.html
Normal file
52
templates/dns_resolver.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import "helpers.html" as helper %}
|
||||||
|
|
||||||
|
{% block title %}{{ data.config.display_name }}{% endblock %}
|
||||||
|
{% block h1 %}DNS Resolver: {{ data.config.display_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{%- set c = data.config %}
|
||||||
|
<section>
|
||||||
|
<h2>Connection Configuration</h2>
|
||||||
|
<dl>
|
||||||
|
<dt>Address{% if c.servers | length > 1 %}es{% endif %}</dt>
|
||||||
|
{% if c.servers | length > 0 %}
|
||||||
|
{%- for a in c.servers %}
|
||||||
|
{% if a is matching("^\[") %}
|
||||||
|
{% set ip = a | split(pat="]") | first | trim_start_matches(pat="[") %}
|
||||||
|
{% else %}
|
||||||
|
{% set ip = a | split(pat=":") | first %}
|
||||||
|
{% endif %}
|
||||||
|
<dd>{{ helper::ip(extra=extra, ip=ip, text=a) }}</dd>
|
||||||
|
{%- endfor %}
|
||||||
|
{%- else %}
|
||||||
|
<dd>None Configured</dd>
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
<dt>Protocol</dt>
|
||||||
|
<dd>{{ c.protocol }}</dd>
|
||||||
|
|
||||||
|
{%- if c.tls_dns_name %}
|
||||||
|
<dt>DNS Name</dt>
|
||||||
|
<dd>{{ helper::dig(extra=extra, name=c.tls_dns_name) }}</dd>
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
</dl>
|
||||||
|
{%- if c.info_url %}
|
||||||
|
<p class="button-paragraph"><a href="{{c.info_url}}">More about the {{c.display_name}} DNS Server <small>(external link)</small></a></p>
|
||||||
|
{%- endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{%- if c.aliases | length > 0 %}
|
||||||
|
<section>
|
||||||
|
<h2>Aliases</h2>
|
||||||
|
<ul>
|
||||||
|
{%- for a in c.aliases %}
|
||||||
|
<li>{{a}}</li>
|
||||||
|
{%- endfor %}
|
||||||
|
</ul>
|
||||||
|
<p class="box hint">You can use this DNS server by typing <code>via {{c.aliases | first }}</code> {% if c.aliases | length > 1 %}(or any other alias){% endif %} in the searchfield.</p>
|
||||||
|
</section>
|
||||||
|
<p><a href="{{extra.base_url}}/dns_resolver">Back to DNS Resolver list</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
30
templates/dns_resolver.txt
Normal file
30
templates/dns_resolver.txt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# DNS Resolver: {{ data.config.display_name }}
|
||||||
|
|
||||||
|
{%- set c = data.config %}
|
||||||
|
{% if c.servers | length == 1 %}
|
||||||
|
Address: {{ c.servers | first }}
|
||||||
|
{% elif c.servers | length > 1 %}
|
||||||
|
Addresses:
|
||||||
|
{%- for a in c.servers %}
|
||||||
|
* {{a}}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- else %}
|
||||||
|
Address: None Configured
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
Protocol: {{ c.protocol }}
|
||||||
|
{%-if c.tls_dns_name %}
|
||||||
|
DNS Name: {{ c.tls_dns_name }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- if c.aliases | length == 1 %}
|
||||||
|
Alias: {{ c.aliases | first }}
|
||||||
|
{%- elif c.aliases | length > 1 %}
|
||||||
|
Aliases:
|
||||||
|
{%- for a in c.aliases %}
|
||||||
|
* {{a}}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- if c.info_url %}
|
||||||
|
|
||||||
|
=> {{ c.info_url }}
|
||||||
|
{%- endif %}
|
16
templates/dns_resolver_list.html
Normal file
16
templates/dns_resolver_list.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}List of DNS Servers{% endblock %}
|
||||||
|
{% block h1 %}List of DNS Servers{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section>
|
||||||
|
<p>This is a list of DNS resolvers that are configured with this echoip service.</p>
|
||||||
|
<ul class="link-list">
|
||||||
|
{% for c in dns_resolvers %}
|
||||||
|
<li><a href="{{ extra.base_url }}/dns_resolver/{{c.id}}">{{c.name}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p>The reasons for them being here range from them being popular, privacy friendly, interesting, don't take the a server listed here as endorsement.</p>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
4
templates/dns_resolver_list.txt
Normal file
4
templates/dns_resolver_list.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Here is a list of supported dns resolvers
|
||||||
|
{% for r in dns_resolvers %}
|
||||||
|
=> ./{{ r.id }} {{r.name}}
|
||||||
|
{%- endfor %}
|
@ -1,23 +1,35 @@
|
|||||||
site_name="echoip-slatecave"
|
site_name="echoip-slatecave"
|
||||||
base_url="http://localhost:3000"
|
base_url="http://localhost:3000"
|
||||||
|
|
||||||
# Url to the deafult opengraph preview image
|
# Url to the default opengraph preview image
|
||||||
# og_image=""
|
# og_image=""
|
||||||
|
|
||||||
# Url to your stylesheet
|
# URL to your stylesheet
|
||||||
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"
|
||||||
|
|
||||||
# Urls to look up v4 and v6 addresses explicitly
|
# Icon to display next to the title
|
||||||
|
display_icon = "/icon_64.png"
|
||||||
|
|
||||||
|
# URLs to look up v4 and v6 addresses explicitly
|
||||||
# If you have not configured them, comment them out, the button will stay hidden
|
# 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.
|
||||||
title = "Nothing here"
|
title = "Nothing here"
|
||||||
description = "Page not found"
|
description = "Page not found"
|
||||||
|
@ -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) %}
|
{% 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>{{ ip }}</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) %}
|
||||||
@ -31,7 +31,7 @@
|
|||||||
{%- set_global i = i+1 -%}
|
{%- set_global i = i+1 -%}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% endmacro breadcrumb_domain %}
|
{%- endmacro breadcrumb_domain %}
|
||||||
|
|
||||||
{% macro ip_info(ip_info) -%}
|
{% macro ip_info(ip_info) -%}
|
||||||
{{ip_info.scope | title}} {{ip_info.cast | title}} IPv{% if ip_info.is_v6_address %}6{% else %}4{% endif %}
|
{{ip_info.scope | title}} {{ip_info.cast | title}} IPv{% if ip_info.is_v6_address %}6{% else %}4{% endif %}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
{% 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 %}
|
||||||
|
|
||||||
{% block description %}
|
{% block description %}
|
||||||
Look up Your and others public IP-Adresses. - {{ data.result.address }}
|
Look up Your and others public IP-Addresses. - {{ data.result.address }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block og_path %}/{% endblock %}
|
{% block og_path %}/{% endblock %}
|
||||||
@ -27,11 +29,13 @@
|
|||||||
<section>
|
<section>
|
||||||
<h2>Your User Agent</h2>
|
<h2>Your User Agent</h2>
|
||||||
<p>The program you were using to download this page <a href="https://en.wikipedia.org/wiki/User_agent">identified itself</a> as <code>{{ data.user_agent }}</code></p>
|
<p>The program you were using to download this page <a href="https://en.wikipedia.org/wiki/User_agent">identified itself</a> as <code>{{ data.user_agent }}</code></p>
|
||||||
<p>While this doesn't have to do anything with your public IP-Adress this might be useful information.</p>
|
<p>While this doesn't have to do anything with your public IP-Address this might be useful information.</p>
|
||||||
<p>You can use the <code><a href="{{ extra.base_url}}/ua">/ua</a></code> endpoint to fetch this information with any client.</p>
|
<p>You can use the <code><a href="{{ extra.base_url}}/ua">/ua</a></code> endpoint to fetch this information with any client.</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2>Did you know?</h2>
|
<h2>Did you know?</h2>
|
||||||
<p>If you share this site and the Link gets a preview. The IP-Address after the dash is the one of the machine that generated that preview.</p>
|
<p>If you share this site and the Link gets a preview. The IP-Address after the dash is the one of the machine that generated that preview.</p>
|
||||||
|
<p>This service exports a <a href="{{ extra.base_url }}/dns_resolver">list of dns resolvers it supports</a>, with configuration.</p>
|
||||||
|
<p>Every query that can output html can also output json or plain text using the <code>?format=json</code> or <code>?format=text</code> url parameters?</p>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% extends "ip.txt" %}
|
{% extends "ip.txt" %}
|
||||||
|
|
||||||
{% block title -%}
|
{% block title -%}
|
||||||
Your IP-Adress is: {{ data.result.address }}
|
Your IP-Address is: {{ data.result.address }}
|
||||||
{%- endblock %}
|
{%- 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="Registred 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>Accuaracy</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 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 GeopIP 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 %}
|
||||||
@ -80,11 +92,11 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Programatic Lookup</h2>
|
<h2>Programmatic Lookup</h2>
|
||||||
<p>If you want to look up this IP-Address Information in another program that is okay as long as you are civil about it … (ratelimit)</p>
|
<p>If you want to look up this IP-Address Information in another program that is okay as long as you are civil about it … (ratelimit)</p>
|
||||||
<h3>How?</h3>
|
<h3>How?</h3>
|
||||||
<p>You can choose between the <code>html</code>, <code>text</code> and <code>json</code> format.</p>
|
<p>You can choose between the <code>html</code>, <code>text</code> and <code>json</code> format.</p>
|
||||||
<p>An example of an url could be: <code>{{ extra.base_url }}/ip/1.2.3.4?format=json</code></p>
|
<p>An example of an URL could be: <code>{{ extra.base_url }}/ip/1.2.3.4?format=json</code></p>
|
||||||
<p>To look up your IP-Address as json: <code>{{ extra.base_url }}/?format=json</code></p>
|
<p>To look up your IP-Address as json: <code>{{ extra.base_url }}/?format=json</code></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -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
|
||||||
@ -26,7 +40,7 @@
|
|||||||
{{ helper::place_dl(place=r.location.continent, label="Continent") -}}
|
{{ helper::place_dl(place=r.location.continent, label="Continent") -}}
|
||||||
{{ helper::place_dl(place=r.location.country, label="Country") -}}
|
{{ helper::place_dl(place=r.location.country, label="Country") -}}
|
||||||
{%- if r.location.country.iso_code | default(value="") != r.location.registered_country.iso_code | default(value="") -%}
|
{%- if r.location.country.iso_code | default(value="") != r.location.registered_country.iso_code | default(value="") -%}
|
||||||
{{- helper::place_dl(place=r.location.registered_country, label="Registred in") -}}
|
{{- helper::place_dl(place=r.location.registered_country, label="Registered in") -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if r.location.country.iso_code | default(value="") != r.location.represented_country.iso_code | default(value="") -%}
|
{%- if r.location.country.iso_code | default(value="") != r.location.represented_country.iso_code | default(value="") -%}
|
||||||
{{- helper::place_dl(place=r.location.represented_country, label="Represents") -}}
|
{{- helper::place_dl(place=r.location.represented_country, label="Represents") -}}
|
||||||
@ -44,7 +58,7 @@
|
|||||||
* Timezone: {{r.location.time_zone}}
|
* Timezone: {{r.location.time_zone}}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{%- if r.location.accuracy -%}
|
{%- if r.location.accuracy -%}
|
||||||
* Accuaracy: ~{{r.location.accuracy}}km
|
* Accuracy: ~{{r.location.accuracy}}km
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{%- if r.location.coordinates %}
|
{%- if r.location.coordinates %}
|
||||||
### Coordinates
|
### Coordinates
|
||||||
@ -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 GeopIP 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 -%}
|
||||||
|
@ -3,21 +3,30 @@
|
|||||||
<ul class="link-list">
|
<ul class="link-list">
|
||||||
<li><a target="_blank" href="https://apps.db.ripe.net/db-web-ui/query?bflag=true&dflag=false&rflag=true&searchtext={{ address }}&source=RIPE">… in the RIPE Database</a></li>
|
<li><a target="_blank" href="https://apps.db.ripe.net/db-web-ui/query?bflag=true&dflag=false&rflag=true&searchtext={{ address }}&source=RIPE">… in the RIPE Database</a></li>
|
||||||
<li><a target="_blank" href="https://apps.db.ripe.net/db-web-ui/query?bflag=true&dflag=false&rflag=true&searchtext={{ address }}&source=GRS">… in the RIPE Global Resources Service</a></li>
|
<li><a target="_blank" href="https://apps.db.ripe.net/db-web-ui/query?bflag=true&dflag=false&rflag=true&searchtext={{ address }}&source=GRS">… in the RIPE Global Resources Service</a></li>
|
||||||
<li><a target="_blank" href="https://www.shodan.io/host/{{ address }}">… on shodan.io <small>(limited querys per day, wants an account)</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://search.censys.io/search?resource=hosts&sort=RELEVANCE&per_page=25&virtual_hosts=EXCLUDE&q=ip%3D{{ address }}">… on search.censys.io <small>(10 querys 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://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 querys 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 querys 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 | urlencode_strict }}">… on the Mozilla Observatory (http and tls checks)</a></li>
|
||||||
|
<li><a target="_blank" href="https://internet.nl/site/{{ name | urlencode_strict }}">… on the Internet.nl Website test</a></li>
|
||||||
|
<li><a target="_blank" href="https://client.rdap.org/?type=domain&object={{ name | urlencode_strict }}">… on client.rdap.org <small>(a modern whois, make sure to allow xhr to 3rd parties)</small></a></li>
|
||||||
|
<li><a target="_blank" href="https://crt.sh/?Identity={{ name | urlencode_strict }}&match==">… on crt.sh <small>(Certificate Transparancy Monitor)</small></a></li>
|
||||||
|
<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 %}
|
||||||
|
|
||||||
@ -26,8 +35,11 @@
|
|||||||
<ul class="link-list">
|
<ul class="link-list">
|
||||||
<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 querys 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://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 buttonin the sidebar to get results)</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://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>
|
||||||
{% endmacro asn_links %}
|
{% endmacro asn_links %}
|
||||||
|
|
||||||
|
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 |
@ -1,2 +1,3 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow: /*?
|
Disallow: /ip/
|
||||||
|
Disallow: /dig/
|
@ -380,6 +380,8 @@ a:visited {
|
|||||||
color: var(--page-link-visited);
|
color: var(--page-link-visited);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.sitename { display: inline-block; }
|
||||||
|
|
||||||
h1, a.sitename {
|
h1, a.sitename {
|
||||||
margin: var(--heading-mg);
|
margin: var(--heading-mg);
|
||||||
padding: var(--heading-pad);
|
padding: var(--heading-pad);
|
||||||
@ -571,7 +573,7 @@ tr:hover, th {
|
|||||||
|
|
||||||
/* Search form styling */
|
/* Search form styling */
|
||||||
|
|
||||||
form.search > input {
|
form.search > input, form.search > select {
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@ -597,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