mirror of
https://github.com/LowderPlay/cheburcheck.git
synced 2026-09-22 22:37:59 +03:00
initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
target/
|
||||
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/databases
|
||||
Generated
+3808
File diff suppressed because it is too large
Load Diff
+26
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "blocklist-check"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
hickory-resolver = { version = "0.26.0-alpha.1", features = ["tokio", "webpki-roots", "https-ring"] }
|
||||
rocket = "0.5.1"
|
||||
rocket_dyn_templates = { version = "0.2.0", features = ["tera"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4.28"
|
||||
maxminddb = "0.26"
|
||||
ipnet-trie = "0.3.0"
|
||||
ipnet = "2.11.0"
|
||||
csv = "1.4.0"
|
||||
trie-rs = "0.4.2"
|
||||
env_logger = "0.11.8"
|
||||
reqwest = { version = "0.12.24", features = ["stream", "rustls-tls-webpki-roots"] }
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
rocket-cache-response = "0.6.4"
|
||||
|
||||
[build-dependencies]
|
||||
reqwest = { version = "0.12", features = ["blocking", "json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tar = "0.4"
|
||||
flate2 = "1.0"
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
FROM docker.io/rust:1-slim-bookworm AS build
|
||||
|
||||
## cargo package name: customize here or provide via --build-arg
|
||||
ARG pkg=blocklist-check
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN apt update && apt install -y libssl-dev pkg-config
|
||||
|
||||
RUN --mount=type=cache,target=/build/target \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
set -eux; \
|
||||
cargo build --release; \
|
||||
objcopy --compress-debug-sections target/release/$pkg ./main
|
||||
|
||||
################################################################################
|
||||
|
||||
FROM docker.io/debian:bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt update && apt install -y libssl3 ca-certificates
|
||||
|
||||
## copy the main binary
|
||||
COPY --from=build /build/main ./
|
||||
|
||||
COPY --from=build /build/static ./static
|
||||
COPY --from=build /build/templates ./templates
|
||||
|
||||
## ensure the container listens globally on port 8080
|
||||
ENV ROCKET_ADDRESS=0.0.0.0
|
||||
ENV ROCKET_PORT=8080
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ./main
|
||||
@@ -0,0 +1,28 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2023, Ivan Evstratov
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,57 @@
|
||||
#  Cheburcheck.ru
|
||||
|
||||
[Cheburcheck.ru](https://cheburcheck.ru) — open-source сервис для проверки доменов и IP-адресов на наличие в списках блокировок Роскомнадзора и популярных заблокированных CDN-провайдеров.
|
||||
|
||||
---
|
||||
|
||||
## О проекте
|
||||
|
||||
Этот сервис реализован на языке Rust с использованием веб-фреймворка [Rocket.rs](https://rocket.rs/). Основная задача — проверять, заблокирован ли домен или IP-адрес, основываясь на актуальных данных из реестров Роскомнадзора и других источниках.
|
||||
|
||||
---
|
||||
|
||||
## Технологии
|
||||
|
||||
- **Rust** — язык программирования, на котором написан весь бекенд.
|
||||
- **Rocket.rs** — веб-фреймворк для построения HTTP API и серверной логики.
|
||||
- **DoH резолвер Cloudflare** — для разрешения DNS-запросов используется DNS-over-HTTPS через Cloudflare, что позволяет обойти локальные ограничения и получать актуальные данные.
|
||||
- **Tera** — шаблонизатор для генерации HTML страниц, используется для фронтенда.
|
||||
- **lucide** — библиотека иконок, применяемая для визуального оформления интерфейса.
|
||||
|
||||
---
|
||||
|
||||
## Как это работает
|
||||
|
||||
1. Пользователь вводит домен или IP-адрес в форму на сайте.
|
||||
2. Сервер выполняет DNS-запрос через Cloudflare DoH для получения актуальной информации.
|
||||
3. Полученный домен/IP проверяется на наличие в блокировочных списках через префиксные деревья.
|
||||
4. Результат формируется с помощью Tera и отображается пользователю.
|
||||
|
||||
---
|
||||
|
||||
## Списки
|
||||
|
||||
Для проверки используются списки [123jjck/cdn-ip-ranges](https://github.com/123jjck/cdn-ip-ranges) и [antifilter.download](https://antifilter.download/).
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
* `src/` — исходный код приложения
|
||||
* `templates/` — Tera-шаблоны для фронтенда
|
||||
* `static/` — статические файлы
|
||||
* `Cargo.toml` — зависимости и конфигурация Rust-проекта
|
||||
|
||||
---
|
||||
|
||||
## Вклад
|
||||
|
||||
Если хотите помочь с разработкой — открывайте issue или присылайте pull requests.
|
||||
Если хотите помочь финансово:
|
||||
- TON: `UQAACsiwpGryjP-kqp4TJPAWpXytuB6M_puuO0Cg5zNvaSJW`
|
||||
|
||||
---
|
||||
|
||||
## Контакты
|
||||
|
||||
Для вопросов и обсуждений можно написать на [support@cheburcheck.ru](mailto:support@cheburcheck.ru).
|
||||
@@ -0,0 +1,61 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::{File},
|
||||
io::{self, Cursor},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use flate2::read::GzDecoder;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Deserialize;
|
||||
use tar::Archive;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct NpmMetadata {
|
||||
dist: Dist,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Dist {
|
||||
tarball: String,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let out_dir = env::var("OUT_DIR")?;
|
||||
let lucide_js_path = Path::new(&out_dir).join("lucide.js");
|
||||
|
||||
let client = Client::new();
|
||||
|
||||
println!("cargo:rerun-if-env-changed=LUCIDE_VERSION");
|
||||
let metadata_url = format!("https://registry.npmjs.org/lucide/{}", option_env!("LUCIDE_VERSION").unwrap_or("latest"));
|
||||
|
||||
println!("Fetching npm metadata from {}", metadata_url);
|
||||
let metadata: NpmMetadata = client.get(metadata_url).send()?.json()?;
|
||||
|
||||
println!("Downloading tarball from {}", metadata.dist.tarball);
|
||||
let tarball_bytes = client.get(&metadata.dist.tarball).send()?.bytes()?;
|
||||
|
||||
let tar = GzDecoder::new(Cursor::new(tarball_bytes));
|
||||
let mut archive = Archive::new(tar);
|
||||
|
||||
let mut lucide_js_found = false;
|
||||
for entry in archive.entries()? {
|
||||
let mut entry = entry?;
|
||||
let path = entry.path()?;
|
||||
if path == Path::new("package/dist/umd/lucide.min.js") {
|
||||
println!("Found lucide.min.js, extracting to {:?}", lucide_js_path);
|
||||
let mut out_file = File::create(&lucide_js_path)?;
|
||||
io::copy(&mut entry, &mut out_file)?;
|
||||
lucide_js_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !lucide_js_found {
|
||||
panic!("Could not find dist/lucide.min.js inside the tarball");
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use std::{io};
|
||||
use std::io::Error;
|
||||
use std::net::IpAddr;
|
||||
use maxminddb::{geoip2, MaxMindDbError};
|
||||
use maxminddb::geoip2::{city, country, City, Country};
|
||||
use rocket::serde::Serialize;
|
||||
use crate::updater::{fetch_db, Updatable};
|
||||
|
||||
pub struct GeoIp {
|
||||
asn: Option<maxminddb::Reader<Vec<u8>>>,
|
||||
city: Option<maxminddb::Reader<Vec<u8>>>,
|
||||
country: Option<maxminddb::Reader<Vec<u8>>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct IpInfo {
|
||||
asn: String,
|
||||
organisation: String,
|
||||
location: String,
|
||||
}
|
||||
|
||||
impl Default for IpInfo {
|
||||
fn default() -> IpInfo {
|
||||
IpInfo {
|
||||
asn: "-".to_string(),
|
||||
organisation: "-".to_string(),
|
||||
location: "-".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GeoIp {
|
||||
pub fn new() -> Self {
|
||||
GeoIp {
|
||||
asn: None,
|
||||
country: None,
|
||||
city: None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, asn: Vec<u8>, country: Vec<u8>, city: Vec<u8>) -> Result<(), MaxMindDbError> {
|
||||
self.asn = Some(maxminddb::Reader::from_source(asn)?);
|
||||
self.country = Some(maxminddb::Reader::from_source(country)?);
|
||||
self.city = Some(maxminddb::Reader::from_source(city)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn lookup(&self, ip: IpAddr) -> Result<IpInfo, MaxMindDbError> {
|
||||
let asn = if let Some(db) = &self.asn {
|
||||
db.lookup::<geoip2::Asn>(ip)?
|
||||
} else { None };
|
||||
let city = if let Some(db) = &self.city {
|
||||
db.lookup::<City>(ip)?
|
||||
} else { None };
|
||||
let country = if let Some(db) = &self.country {
|
||||
db.lookup::<Country>(ip)?
|
||||
} else { None };
|
||||
|
||||
let location = match (city, country) {
|
||||
(Some(City { city: Some(city::City { names: Some(city), .. }),
|
||||
country: Some(country::Country { names: Some(country), .. }), .. }), _) => {
|
||||
let city = city.get("ru").unwrap_or(&"-");
|
||||
let country = country.get("ru").unwrap_or(&"-");
|
||||
format!("{}, {}", city, country)
|
||||
}
|
||||
(_, Some(Country { country: Some(country::Country { names: Some(country), .. }), .. })) => {
|
||||
country.get("ru").unwrap_or(&"-").to_string()
|
||||
}
|
||||
(_, _) => "-".to_string(),
|
||||
};
|
||||
|
||||
Ok(IpInfo {
|
||||
location,
|
||||
asn: asn.clone().and_then(|asn| asn.autonomous_system_number)
|
||||
.map(|asn| format!("AS{}", asn))
|
||||
.unwrap_or("-".to_string()),
|
||||
organisation: asn.and_then(|asn| asn.autonomous_system_organization)
|
||||
.unwrap_or("-").to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Updatable for GeoIp {
|
||||
type Base = (Vec<u8>, Vec<u8>, Vec<u8>);
|
||||
|
||||
async fn download() -> Result<Self::Base, io::Error> {
|
||||
Ok((fetch_db(Self::get_url("GEO_ASN", "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-ASN.mmdb")).await?,
|
||||
fetch_db(Self::get_url("GEO_COUNTRY", "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb")).await?,
|
||||
fetch_db(Self::get_url("GEO_CITY", "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb")).await?))
|
||||
}
|
||||
|
||||
async fn install(&mut self, (asn, country, city): Self::Base) -> Result<(), Error> {
|
||||
self.update(asn, country, city)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::io;
|
||||
use std::io::{BufRead, Error, Read};
|
||||
use std::net::IpAddr;
|
||||
use std::str::FromStr;
|
||||
use ipnet::IpNet;
|
||||
use ipnet_trie::IpnetTrie;
|
||||
use serde::{de, Deserialize, Deserializer, Serializer};
|
||||
use trie_rs::map::{Trie, TrieBuilder};
|
||||
use crate::updater::{fetch_db, Updatable};
|
||||
|
||||
pub struct CdnList {
|
||||
trie: IpnetTrie<NetworkRecord>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Eq, PartialEq, Hash)]
|
||||
pub struct NetworkRecord {
|
||||
pub provider: String,
|
||||
#[serde(deserialize_with = "deserialize_ip_net")]
|
||||
#[serde(serialize_with = "serialize_ip_net")]
|
||||
pub cidr: IpNet,
|
||||
pub region: Option<String>,
|
||||
}
|
||||
|
||||
fn deserialize_ip_net<'de, D>(deserializer: D) -> Result<IpNet, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
FromStr::from_str(&s).map_err(de::Error::custom)
|
||||
}
|
||||
|
||||
fn serialize_ip_net<S>(ip_net: &IpNet, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&ip_net.to_string())
|
||||
}
|
||||
|
||||
impl CdnList {
|
||||
pub fn new() -> CdnList{
|
||||
CdnList { trie: IpnetTrie::new() }
|
||||
}
|
||||
|
||||
pub fn update<R: Read>(&mut self, list_reader: R) -> Result<(), Error> {
|
||||
let mut trie = IpnetTrie::new();
|
||||
let mut rdr = csv::Reader::from_reader(list_reader);
|
||||
for result in rdr.deserialize() {
|
||||
let record: NetworkRecord = result?;
|
||||
trie.insert(record.cidr, record);
|
||||
}
|
||||
let (v4, v6) = trie.ip_count();
|
||||
info!("ip count: v4={}, v6={}", v4, v6);
|
||||
self.trie = trie;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn v4_count(&self) -> u32 {
|
||||
self.trie.ip_count().0
|
||||
}
|
||||
|
||||
pub fn contains(&self, ip: &IpAddr) -> Option<NetworkRecord> {
|
||||
self.trie.longest_match(&IpNet::from(*ip)).map(|(_, net)| net.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Updatable for CdnList {
|
||||
type Base = VecDeque<u8>;
|
||||
|
||||
async fn download() -> Result<Self::Base, Error> {
|
||||
Ok(VecDeque::from(fetch_db(Self::get_url(
|
||||
"CDN_SOURCE",
|
||||
"https://raw.githubusercontent.com/123jjck/cdn-ip-ranges/refs/heads/main/all/all.csv"
|
||||
)).await?))
|
||||
}
|
||||
|
||||
async fn install(&mut self, base: Self::Base) -> Result<(), Error> {
|
||||
self.update(base)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RuBlacklist {
|
||||
ip_trie: IpnetTrie<()>,
|
||||
domain_trie: Trie<String, String>,
|
||||
pub domain_count: usize,
|
||||
}
|
||||
|
||||
impl RuBlacklist {
|
||||
pub fn new() -> RuBlacklist {
|
||||
RuBlacklist {
|
||||
ip_trie: Default::default(),
|
||||
domain_trie: TrieBuilder::new().build(),
|
||||
domain_count: 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update<R: BufRead>(&mut self, ip_reader: R, domain_reader: R) -> Result<(), Error> {
|
||||
let mut ip_trie = IpnetTrie::new();
|
||||
for net in ip_reader.lines() {
|
||||
let net = net?;
|
||||
let net = IpNet::from_str(&net)
|
||||
.map_err(|e| Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
ip_trie.insert(net, ());
|
||||
}
|
||||
let (v4, v6) = ip_trie.ip_count();
|
||||
info!("ip count: v4={}, v6={}", v4, v6);
|
||||
self.ip_trie = ip_trie;
|
||||
|
||||
let mut domain_trie = TrieBuilder::new();
|
||||
let mut count = 0;
|
||||
for domain in domain_reader.lines() {
|
||||
let domain = domain?;
|
||||
domain_trie.insert(Self::domain_chunks(&domain), domain);
|
||||
count += 1;
|
||||
}
|
||||
info!("domain count: {}", count);
|
||||
self.domain_count = count;
|
||||
self.domain_trie = domain_trie.build();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn v4_count(&self) -> u32 {
|
||||
self.ip_trie.ip_count().0
|
||||
}
|
||||
|
||||
fn domain_chunks(domain: &str) -> Vec<String> {
|
||||
domain.split(".").collect::<Vec<_>>()
|
||||
.into_iter().map(|s| s.to_string())
|
||||
.rev().collect()
|
||||
}
|
||||
|
||||
pub fn contains_ip(&self, ip: &IpAddr) -> Option<IpNet> {
|
||||
self.ip_trie.longest_match(&IpNet::from(*ip)).map(|(ip, _)| ip)
|
||||
}
|
||||
|
||||
pub fn contains_domain(&self, domain: &str) -> Option<String> {
|
||||
self.domain_trie.common_prefix_search(Self::domain_chunks(domain)).next()
|
||||
.map(|(_, b): (Vec<_>, &String)| b).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Updatable for RuBlacklist {
|
||||
type Base = (VecDeque<u8>, VecDeque<u8>);
|
||||
|
||||
async fn download() -> Result<Self::Base, Error> {
|
||||
Ok((VecDeque::from(
|
||||
fetch_db(Self::get_url("RKN_NETS", "https://antifilter.download/list/allyouneed.lst")).await?),
|
||||
VecDeque::from(
|
||||
fetch_db(Self::get_url("RKN_DOMAINS", "https://antifilter.download/list/domains.lst")).await?)))
|
||||
}
|
||||
|
||||
async fn install(&mut self, (nets, domains): Self::Base) -> Result<(), Error> {
|
||||
self.update(nets, domains)
|
||||
}
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
mod target;
|
||||
mod resolver;
|
||||
mod geoip;
|
||||
mod lists;
|
||||
mod updater;
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
extern crate maxminddb;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use chrono::{DateTime, Utc};
|
||||
use rocket::fs::FileServer;
|
||||
use rocket::http::Status;
|
||||
use rocket::{tokio, Request, State};
|
||||
use rocket::response::content::RawJavaScript;
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use serde::Serialize;
|
||||
use log::error;
|
||||
use rocket::tokio::sync::{watch, RwLock};
|
||||
use rocket::tokio::time;
|
||||
use rocket_cache_response::CacheResponse;
|
||||
use crate::geoip::{GeoIp, IpInfo};
|
||||
use crate::lists::{CdnList, NetworkRecord, RuBlacklist};
|
||||
use crate::resolver::Resolver;
|
||||
use crate::target::Target;
|
||||
use crate::updater::update_all;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct GlobalContext {
|
||||
version: &'static str,
|
||||
}
|
||||
|
||||
impl GlobalContext {
|
||||
fn new() -> Self {
|
||||
GlobalContext { version: env!("CARGO_PKG_VERSION") }
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn index(info: &State<watch::Receiver<UpdateInfo>>) -> Template {
|
||||
Template::render("index", context! {
|
||||
global: GlobalContext::new(),
|
||||
info: &*info.borrow(),
|
||||
})
|
||||
}
|
||||
|
||||
#[get("/healthcheck")]
|
||||
fn healthcheck(info: &State<watch::Receiver<UpdateInfo>>) -> (Status, String) {
|
||||
if info.borrow().last_update.is_some() {
|
||||
(Status::Ok, "OK".to_string())
|
||||
} else {
|
||||
(Status::InternalServerError, "LOADING DATABASES".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/check?<target>")]
|
||||
async fn check(target: &str, resolver: &State<Resolver>,
|
||||
geo_ip: &State<Arc<RwLock<GeoIp>>>,
|
||||
cdn: &State<Arc<RwLock<CdnList>>>,
|
||||
ru_blacklist: &State<Arc<RwLock<RuBlacklist>>>) -> Result<Template, Status> {
|
||||
let parsed = Target::from(target);
|
||||
let ips = match parsed.resolve(resolver).await {
|
||||
Ok(ips) => ips,
|
||||
Err(e) if e.kind().is_no_records_found() => {
|
||||
return Ok(Template::render("empty", context! {
|
||||
global: GlobalContext::new(),
|
||||
target,
|
||||
target_type: parsed.readable_type(),
|
||||
}));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return Err(Status::BadRequest);
|
||||
},
|
||||
};
|
||||
let geo_ip = geo_ip.read().await;
|
||||
let geo = match ips.get(0).map(|ip| geo_ip.lookup(ip.clone())) {
|
||||
None => IpInfo::default(),
|
||||
Some(Ok(ip)) => ip,
|
||||
Some(Err(e)) => {
|
||||
error!("{}", e);
|
||||
return Err(Status::BadRequest);
|
||||
},
|
||||
};
|
||||
let mut providers: HashMap<String, HashSet<NetworkRecord>> = HashMap::new();
|
||||
|
||||
let cdn = cdn.read().await;
|
||||
ips.iter()
|
||||
.filter_map(|ip| cdn.contains(ip))
|
||||
.map(|ip| (match &ip.region {
|
||||
None => ip.provider.clone(),
|
||||
Some(region) => format!("{} ({})", ip.provider, region),
|
||||
}, ip.clone()))
|
||||
.for_each(|(k, v)| {
|
||||
providers.entry(k).or_default().insert(v);
|
||||
});
|
||||
|
||||
let ru_blacklist = ru_blacklist.read().await;
|
||||
let domain = match &parsed {
|
||||
Target::Domain(domain) => ru_blacklist.contains_domain(domain),
|
||||
_ => None
|
||||
};
|
||||
|
||||
let blocked_subnets: HashSet<String> = ips.iter()
|
||||
.filter_map(|ip| ru_blacklist.contains_ip(ip))
|
||||
.map(|ip| ip.to_string()).collect();
|
||||
|
||||
Ok(Template::render("result", context! {
|
||||
global: GlobalContext::new(),
|
||||
found: providers.len() > 0 || domain.is_some() || blocked_subnets.len() > 0,
|
||||
cdn_found: providers.len() > 0,
|
||||
domain_found: domain.is_some(),
|
||||
subnet_found: blocked_subnets.len() > 0,
|
||||
providers,
|
||||
domain,
|
||||
blocked_subnets,
|
||||
target,
|
||||
target_type: parsed.readable_type(),
|
||||
ips,
|
||||
geo,
|
||||
}))
|
||||
}
|
||||
|
||||
#[catch(default)]
|
||||
fn default(status: Status, _req: &Request) -> Template {
|
||||
Template::render("error", context! {
|
||||
global: GlobalContext::new(),
|
||||
status: status.code,
|
||||
reason: status.reason_lossy(),
|
||||
})
|
||||
}
|
||||
|
||||
#[rocket::get("/vendor/lucide.js")]
|
||||
fn lucide() -> CacheResponse<RawJavaScript<&'static [u8]>> {
|
||||
CacheResponse::Public {
|
||||
responder: RawJavaScript(include_bytes!(concat!(env!("OUT_DIR"), "/lucide.js"))),
|
||||
max_age: 604800,
|
||||
must_revalidate: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct UpdateInfo {
|
||||
last_update: Option<DateTime<Utc>>,
|
||||
domain_count: String,
|
||||
v4_count: String,
|
||||
}
|
||||
|
||||
fn format_number(number: u64) -> String {
|
||||
number.to_string()
|
||||
.as_bytes()
|
||||
.rchunks(3)
|
||||
.rev()
|
||||
.map(std::str::from_utf8)
|
||||
.collect::<Result<Vec<&str>, _>>()
|
||||
.unwrap()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
#[launch]
|
||||
async fn rocket() -> _ {
|
||||
env_logger::builder().filter_level(log::LevelFilter::Info).init();
|
||||
|
||||
let mut interval = time::interval(Duration::from_secs(std::env::var("DATABASE_INTERVAL_SECONDS")
|
||||
.unwrap_or("21600".to_string()).parse().unwrap()));
|
||||
|
||||
let (tx, rx) = watch::channel(UpdateInfo::default());
|
||||
let cdn_list = Arc::new(RwLock::new(CdnList::new()));
|
||||
let rkn_list = Arc::new(RwLock::new(RuBlacklist::new()));
|
||||
let geo_ip = Arc::new(RwLock::new(GeoIp::new()));
|
||||
|
||||
let geo_ip_clone = geo_ip.clone();
|
||||
let rkn_list_clone = rkn_list.clone();
|
||||
let cdn_list_clone = cdn_list.clone();
|
||||
tokio::spawn(async move {
|
||||
info!("Refreshing DB every {:?}", interval.period());
|
||||
loop {
|
||||
interval.tick().await;
|
||||
update_all(geo_ip_clone.clone(), rkn_list_clone.clone(), cdn_list_clone.clone()).await;
|
||||
let domain_count = rkn_list_clone.read().await.domain_count;
|
||||
let v4_count = rkn_list_clone.read().await.v4_count() + cdn_list_clone.read().await.v4_count();
|
||||
tx.send(UpdateInfo {
|
||||
last_update: Some(Utc::now()),
|
||||
domain_count: format_number(domain_count as u64),
|
||||
v4_count: format_number(v4_count as u64),
|
||||
}).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
rocket::build()
|
||||
.manage(Resolver::new().await)
|
||||
.manage(cdn_list)
|
||||
.manage(rkn_list)
|
||||
.manage(geo_ip)
|
||||
.manage(rx)
|
||||
.mount("/", routes![index, lucide, check, healthcheck])
|
||||
.register("/", catchers![default])
|
||||
.mount("/", FileServer::from(PathBuf::from("static")))
|
||||
.attach(Template::fairing())
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use std::net::IpAddr;
|
||||
use hickory_resolver::config::{LookupIpStrategy, ResolverConfig, ResolverOpts};
|
||||
use hickory_resolver::name_server::{TokioConnectionProvider};
|
||||
use hickory_resolver::proto::ProtoError;
|
||||
|
||||
pub struct Resolver {
|
||||
resolver: hickory_resolver::Resolver<TokioConnectionProvider>,
|
||||
}
|
||||
|
||||
impl Resolver {
|
||||
pub async fn new() -> Resolver {
|
||||
let config = ResolverConfig::quad9_https();
|
||||
let mut opts = ResolverOpts::default();
|
||||
opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
let resolver = hickory_resolver::Resolver::builder_with_config(config, TokioConnectionProvider::default())
|
||||
.with_options(opts)
|
||||
.build();
|
||||
Resolver { resolver }
|
||||
}
|
||||
|
||||
pub async fn lookup_ips(&self, domain: &str) -> Result<Vec<IpAddr>, ProtoError> {
|
||||
Ok(self.resolver.lookup_ip(domain).await?
|
||||
.into_iter().collect())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use hickory_resolver::proto::ProtoError;
|
||||
use crate::resolver::Resolver;
|
||||
|
||||
pub enum Target {
|
||||
Domain(String),
|
||||
Ipv4(Ipv4Addr),
|
||||
Ipv6(Ipv6Addr),
|
||||
}
|
||||
|
||||
impl From<&str> for Target {
|
||||
fn from(input: &str) -> Self {
|
||||
if let Ok(ipv4) = input.parse::<Ipv4Addr>() {
|
||||
return Target::Ipv4(ipv4);
|
||||
}
|
||||
|
||||
if let Ok(ipv6) = input.parse::<Ipv6Addr>() {
|
||||
return Target::Ipv6(ipv6);
|
||||
}
|
||||
Target::Domain(input.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Target {
|
||||
pub fn readable_type(&self) -> &'static str {
|
||||
match self {
|
||||
Target::Domain(_) => "Домен",
|
||||
Target::Ipv4(_) => "IPv4-адрес",
|
||||
Target::Ipv6(_) => "IPv6-адрес"
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve(&self, resolver: &Resolver) -> Result<Vec<IpAddr>, ProtoError> {
|
||||
Ok(match self {
|
||||
Target::Domain(domain) => resolver.lookup_ips(domain).await?,
|
||||
Target::Ipv4(ipv4) => vec![IpAddr::V4(ipv4.clone())],
|
||||
Target::Ipv6(ipv6) => vec![IpAddr::V6(ipv6.clone())],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use std::io;
|
||||
use std::io::Error;
|
||||
use std::sync::Arc;
|
||||
use reqwest::IntoUrl;
|
||||
use rocket::tokio::sync::RwLock;
|
||||
use crate::geoip::GeoIp;
|
||||
use crate::lists::{CdnList, RuBlacklist};
|
||||
|
||||
pub async fn fetch_db<T: IntoUrl>(url: T) -> Result<Vec<u8>, Error> {
|
||||
let response = reqwest::get(url).await
|
||||
.map_err(|e| Error::new(io::ErrorKind::Other, e))?
|
||||
.error_for_status()
|
||||
.map_err(|e| Error::new(io::ErrorKind::Other, e))?;
|
||||
let bytes = response.bytes().await
|
||||
.map_err(|e| Error::new(io::ErrorKind::Other, e))?;
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Updatable {
|
||||
type Base;
|
||||
async fn download() -> Result<Self::Base, Error>;
|
||||
async fn install(&mut self, base: Self::Base) -> Result<(), Error>;
|
||||
fn get_url(key: &'static str, default: &'static str) -> String {
|
||||
std::env::var(key).ok().unwrap_or(default.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_all(geo_ip: Arc<RwLock<GeoIp>>, rkn: Arc<RwLock<RuBlacklist>>, cdn: Arc<RwLock<CdnList>>) {
|
||||
info!("Updating all DBs");
|
||||
match GeoIp::download().await {
|
||||
Ok(base) => {
|
||||
if let Err(e) = geo_ip.write().await.install(base).await {
|
||||
error!("Failed to update GeoIP: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to download GeoIP: {}", e);
|
||||
}
|
||||
}
|
||||
match RuBlacklist::download().await {
|
||||
Ok(base) => {
|
||||
if let Err(e) = rkn.write().await.install(base).await {
|
||||
error!("Failed to update RKN: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to download RKN: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
match CdnList::download().await {
|
||||
Ok(base) => {
|
||||
if let Err(e) = cdn.write().await.install(base).await {
|
||||
error!("Failed to update CDN: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to download CDN: {}", e);
|
||||
}
|
||||
}
|
||||
info!("Updated databases");
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud-alert-icon lucide-cloud-alert">
|
||||
<style>
|
||||
path {
|
||||
stroke: black;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { stroke: white; }
|
||||
}
|
||||
</style>
|
||||
<path d="M12 12v4"/><path d="M12 20h.01"/><path d="M17 18h.5a1 1 0 0 0 0-9h-1.79A7 7 0 1 0 7 17.708"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 501 B |
+433
@@ -0,0 +1,433 @@
|
||||
:root {
|
||||
--bg-color: #0a0a0a; /* neutral-950 */
|
||||
--text-main: #d4d4d4; /* neutral-300 */
|
||||
--text-muted: #737373; /* neutral-500 */
|
||||
--text-bright: #f5f5f5; /* neutral-100 */
|
||||
--border-color: #262626; /* neutral-800 */
|
||||
|
||||
--input-bg: #171717; /* neutral-900 */
|
||||
--input-border: #404040; /* neutral-700 */
|
||||
|
||||
--btn-bg: #f5f5f5; /* neutral-100 */
|
||||
--btn-text: #0a0a0a; /* neutral-950 */
|
||||
|
||||
--red-color: #ef4444; /* red-500 */
|
||||
--red-bg-subtle: rgba(69, 10, 10, 0.2);
|
||||
--red-border: rgba(127, 29, 29, 0.3);
|
||||
|
||||
--green-color: #22c55e; /* green-500 */
|
||||
--green-bg-subtle: rgba(5, 46, 22, 0.2);
|
||||
--green-border: rgba(20, 83, 45, 0.3);
|
||||
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-mono), sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #404040;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header, .footer {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.flex { display: flex; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.gap-2 { gap: 0.5rem; }
|
||||
.gap-4 { gap: 1rem; }
|
||||
.column { flex-direction: column; }
|
||||
|
||||
.hidden { display: none !important; }
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-bright);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 0.25rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1.error-code {
|
||||
font-size: 4rem;
|
||||
line-height: 4rem;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
color: var(--red-color);
|
||||
}
|
||||
|
||||
.text-sm { font-size: 0.875rem; }
|
||||
.text-xs { font-size: 0.75rem; }
|
||||
.text-lg { font-size: 1.5rem; }
|
||||
.uppercase { text-transform: uppercase; }
|
||||
.font-bold { font-weight: 700; }
|
||||
.text-muted { color: var(--text-muted); }
|
||||
.text-bright { color: var(--text-bright); }
|
||||
.break-all { word-break: break-all; }
|
||||
|
||||
.header {
|
||||
margin-bottom: 3rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-bright);
|
||||
letter-spacing: -0.05em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.version-link {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 3rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
text-align: center;
|
||||
color: #525252;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-icon-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 2rem;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-muted);
|
||||
transition: color 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
color: white;
|
||||
padding: 1rem 1rem 1rem 3rem;
|
||||
height: 4rem;
|
||||
font-size: 1.125rem;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: #a3a3a3;
|
||||
background-color: #262626;
|
||||
}
|
||||
|
||||
.search-input:focus + .search-icon-wrapper {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
/* Убрано абсолютное позиционирование */
|
||||
width: 100%; /* Полная ширина на мобильных */
|
||||
height: 4rem; /* Высота совпадает с полем ввода */
|
||||
padding: 0 1.5rem;
|
||||
background-color: var(--btn-bg);
|
||||
color: var(--btn-text);
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center; /* Центрируем текст и иконку */
|
||||
gap: 0.5rem;
|
||||
font-family: inherit;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Статистика под поиском */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-card-header {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* --- 6. Результаты (Blocked/Clean) --- */
|
||||
.result-panel {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
/*background-color: rgba(69, 10, 10, 0.1);*/
|
||||
}
|
||||
|
||||
/* Цветовые схемы для результатов */
|
||||
.blocked-theme {
|
||||
border-color: var(--red-border);
|
||||
background-color: rgba(69, 10, 10, 0.1); /* Очень прозрачный красный */
|
||||
}
|
||||
.blocked-theme h2, .blocked-theme .status-icon, .blocked-theme .subheading { color: var(--red-color); }
|
||||
.blocked-theme .reason-header { border-bottom: 1px solid var(--red-border); color: #f87171; }
|
||||
.blocked-theme .alert-box {
|
||||
border: 1px solid var(--red-border);
|
||||
background: var(--red-bg-subtle);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.clean-theme {
|
||||
border-color: var(--green-border);
|
||||
background-color: rgba(5, 46, 22, 0.1); /* Очень прозрачный зеленый */
|
||||
}
|
||||
.clean-theme h2, .clean-theme .status-icon, .clean-theme .subheading { color: var(--green-color); }
|
||||
.clean-theme .reason-header { border-bottom: 1px solid var(--green-border); color: var(--green-color); }
|
||||
.clean-theme .alert-box {
|
||||
border: 1px solid var(--green-border);
|
||||
background: var(--green-bg-subtle);
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
/* Общие элементы результатов */
|
||||
.result-header {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.icon-box {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid currentColor;
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.target-info {
|
||||
background-color: rgba(23, 23, 23, 0.5);
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.target-info-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.target-value {
|
||||
font-size: 1.25rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Сетка деталей */
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.detail-section h3 {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: white;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.detail-row:last-child { border-bottom: none; }
|
||||
|
||||
.row-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.row-value {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #e5e5e5;
|
||||
text-align: right;
|
||||
}
|
||||
.row-value.alert { color: var(--red-color); }
|
||||
|
||||
.alert-box {
|
||||
padding: 1rem;
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
text-decoration: none;
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: 1px solid #404040;
|
||||
background: transparent;
|
||||
color: #a3a3a3;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.reset-btn:hover {
|
||||
border-color: white;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* --- 7. Demo Controls --- */
|
||||
.demo-controls {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.demo-controls:hover { opacity: 1; }
|
||||
|
||||
.demo-btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.625rem;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
font-family: inherit;
|
||||
}
|
||||
.demo-btn.block { background-color: #450a0a; color: #fee2e2; border-color: #7f1d1d; }
|
||||
.demo-btn.clean { background-color: #052e16; color: #dcfce7; border-color: #14532d; }
|
||||
|
||||
@media (min-width: 640px) {
|
||||
body { padding: 2rem; }
|
||||
|
||||
h1.error-code {
|
||||
font-size: 6rem;
|
||||
line-height: 6rem;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
flex-direction: row;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex-grow: 1;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
width: auto;
|
||||
margin-left: -1px;
|
||||
height: 4rem;
|
||||
justify-content: space-between;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.stats-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
.details-grid { grid-template-columns: 1fr 1fr; }
|
||||
.detail-row {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.reset-btn { width: auto; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
{% block head %}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cheburcheck</title>
|
||||
|
||||
<link rel="stylesheet" href="/main.css"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
{% endblock head %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
{% block header %}
|
||||
<div class="flex justify-between items-center">
|
||||
<a class="logo" href="/">
|
||||
<i data-lucide="cloud-alert" width="32" height="32"></i>
|
||||
<span class="text-lg font-bold uppercase">Cheburcheck</span>
|
||||
</a>
|
||||
<div class="flex gap-4 text-xs text-muted">
|
||||
<a class="version-link text-muted" href="#">
|
||||
<span>v{{ global.version }}</span>
|
||||
<svg width="20" height="20" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock header %}
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
{% block content %}{% endblock content %}
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
{% block footer %}
|
||||
<p class="text-xs uppercase">
|
||||
Сделано в России
|
||||
<i data-lucide="copyright" width="10" height="10"></i> {{ now() | date(format="%Y") }}
|
||||
</p>
|
||||
{% endblock footer %}
|
||||
</footer>
|
||||
|
||||
<script src="/vendor/lucide.js"></script>
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
{% extends 'base' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="result-panel">
|
||||
<div class="result-header">
|
||||
<div class="icon-box status-icon">
|
||||
<i data-lucide="shield-question-mark" width="32" height="32"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Не найдено</h2>
|
||||
<p class="subheading text-sm">Возможно вы неправильно ввели запрос?</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="target-info">
|
||||
<div class="target-info-label">{{ target_type }}:</div>
|
||||
<div class="target-value target-display">{{ target }}</div>
|
||||
</div>
|
||||
|
||||
{# <div class="details-grid">#}
|
||||
{# <div class="detail-section">#}
|
||||
{# <h3 class="section-title">Сетевые данные</h3>#}
|
||||
{# <div class="detail-row">#}
|
||||
{# <span class="row-label">IP-адреса</span>#}
|
||||
{# <div>#}
|
||||
{# {% for ip in ips %}#}
|
||||
{# <p class="row-value">{{ ip }}</p>#}
|
||||
{# {% endfor %}#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# <div class="detail-row">#}
|
||||
{# <span class="row-label">Хостинг / ISP</span>#}
|
||||
{# <span class="row-value">{{ geo.organisation }}</span>#}
|
||||
{# </div>#}
|
||||
{# <div class="detail-row">#}
|
||||
{# <span class="row-label">Location</span>#}
|
||||
{# <span class="row-value">{{ geo.location }}</span>#}
|
||||
{# </div>#}
|
||||
{# <div class="detail-row">#}
|
||||
{# <span class="row-label">ASN</span>#}
|
||||
{# <span class="row-value">{{ geo.asn }}</span>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
|
||||
{# <div class="detail-section">#}
|
||||
{# <h3 class="reason-header">Нахождение в списках</h3>#}
|
||||
{# <div class="detail-row">#}
|
||||
{# <span class="row-label">CDN</span>#}
|
||||
{# {% if found %}#}
|
||||
{# <div>#}
|
||||
{# {% for net in subnets %}#}
|
||||
{# <p class="row-value">{{ net }}</p>#}
|
||||
{# {% endfor %}#}
|
||||
{# </div>#}
|
||||
{# {% else %}#}
|
||||
{# <span class="row-value">Не найден</span>#}
|
||||
{# {% endif %}#}
|
||||
{# </div>#}
|
||||
{# <div class="detail-row">#}
|
||||
{# <span class="row-label">Статус РКН</span>#}
|
||||
{# <span class="row-value alert">ОГРАНИЧЕН</span>#}
|
||||
{# </div>#}
|
||||
{# <div class="detail-row">#}
|
||||
{# <span class="row-label">Орган</span>#}
|
||||
{# <span class="row-value">Генпрокуратура</span>#}
|
||||
{# </div>#}
|
||||
{# <div class="detail-row">#}
|
||||
{# <span class="row-label">Номер решения</span>#}
|
||||
{# <span class="row-value">27-31-2024/Ид4124</span>#}
|
||||
{# </div>#}
|
||||
{# <div class="detail-row">#}
|
||||
{# <span class="row-label">Дата внесения</span>#}
|
||||
{# <span class="row-value">12.11.2024</span>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
</div>
|
||||
|
||||
<a href="/" class="reset-btn">
|
||||
<i data-lucide="rotate-ccw" width="14" height="14"></i>
|
||||
<span>Проверить другой</span>
|
||||
</a>
|
||||
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends 'base' %}
|
||||
|
||||
{% block content %}
|
||||
<i class="error-code" data-lucide="triangle-alert" width="196" height="196"></i>
|
||||
<h1 class="error-code">{{ status }} {{ reason }}</h1>
|
||||
<p class="text-muted text-lg">Что-то пошло не так. Возможно в запросе есть ошибка?</p>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,51 @@
|
||||
{% extends 'base' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="search-container">
|
||||
<h1>Статус Ресурса</h1>
|
||||
<p class="text-muted">Введите домен или IP-адрес для поиска по спискам заблокированных адресов и хостинг-провайдеров.</p>
|
||||
</div>
|
||||
|
||||
<form class="search-form" action="/check">
|
||||
<input
|
||||
type="text"
|
||||
name="target"
|
||||
placeholder="example.com или 1.1.1.1 или 2606:4700:4700::1001"
|
||||
class="search-input"
|
||||
minlength="3"
|
||||
required
|
||||
autofocus
|
||||
>
|
||||
<div class="search-icon-wrapper">
|
||||
<i data-lucide="search" width="20" height="20"></i>
|
||||
</div>
|
||||
<button type="submit" class="search-btn">
|
||||
<span>Проверить</span>
|
||||
<i data-lucide="chevron-right" width="16" height="16"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<i data-lucide="globe" width="16" height="16"></i>
|
||||
<span>Количество доменов</span>
|
||||
</div>
|
||||
<span>{{ info.domain_count }}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<i data-lucide="server" width="16" height="16"></i>
|
||||
<span>Количество IPv4-адресов</span>
|
||||
</div>
|
||||
<span>{{ info.v4_count }}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<i data-lucide="activity" width="16" height="16"></i>
|
||||
<span>Последнее обновление</span>
|
||||
</div>
|
||||
<span><script>document.write(new Date("{{ info.last_update }}").toLocaleString())</script></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,112 @@
|
||||
{% extends 'base' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="result-panel {% if found %}blocked-theme{% else %}clean-theme{% endif %}">
|
||||
<div class="result-header">
|
||||
<div class="icon-box status-icon">
|
||||
{% if found %}
|
||||
<i data-lucide="shield-alert" width="32" height="32"></i>
|
||||
{% else %}
|
||||
<i data-lucide="shield-check" width="32" height="32"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if found %}
|
||||
<div>
|
||||
<h2>Заблокирован</h2>
|
||||
<p class="subheading text-sm">Ресурс был найден в списках блокировок</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<h2>Доступен</h2>
|
||||
<p class="subheading text-sm">Ограничений не обнаружено</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="target-info">
|
||||
<div class="target-info-label">{{ target_type }}:</div>
|
||||
<div class="target-value target-display">{{ target }}</div>
|
||||
</div>
|
||||
|
||||
<div class="details-grid">
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">Сетевые данные</h3>
|
||||
<div class="detail-row">
|
||||
<span class="row-label">IP-адреса</span>
|
||||
<div>
|
||||
{% for ip in ips %}
|
||||
<p class="row-value">{{ ip }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="row-label">Хостинг / ISP</span>
|
||||
<span class="row-value">{{ geo.organisation }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="row-label">Локация</span>
|
||||
<span class="row-value">{{ geo.location }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="row-label">ASN</span>
|
||||
<span class="row-value">{{ geo.asn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h3 class="reason-header">Нахождение в списках</h3>
|
||||
<div class="detail-row">
|
||||
<span class="row-label">CDN</span>
|
||||
{% if cdn_found %}
|
||||
<p class="row-value alert">НАЙДЕН</p>
|
||||
{% else %}
|
||||
<span class="row-value">Не найден</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% for provider, networks in providers %}
|
||||
<div class="detail-row">
|
||||
<span class="row-label">{{ provider }}</span>
|
||||
<div>
|
||||
{% for network in networks %}
|
||||
<p class="row-value">{{ network.cidr }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="detail-row">
|
||||
<span class="row-label">Статус РКН</span>
|
||||
|
||||
{% if domain_found or subnet_found %}
|
||||
<span class="row-value alert">ОГРАНИЧЕН</span>
|
||||
{% else %}
|
||||
<span class="row-value">Не найден</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if domain_found %}
|
||||
<div class="detail-row">
|
||||
<span class="row-label">Домен</span>
|
||||
<span class="row-value">{{ domain }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if subnet_found %}
|
||||
<div class="detail-row">
|
||||
<span class="row-label">Подсети</span>
|
||||
<div>
|
||||
{% for network in blocked_subnets %}
|
||||
<p class="row-value">{{ network }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/" class="reset-btn">
|
||||
<i data-lucide="rotate-ccw" width="14" height="14"></i>
|
||||
<span>Проверить другой</span>
|
||||
</a>
|
||||
|
||||
{% endblock content %}
|
||||
Reference in New Issue
Block a user