mirror of
https://github.com/LowderPlay/cheburcheck.git
synced 2026-09-22 22:37:59 +03:00
feat!: svelte rewrite (#68)
* feat!: svelte rewrite * feat: remove mock * fix: formatting and optimisation * feat: db optimizations * feat: dockerfile and version * ci: change dockerfile syntax * fix: remove domain trie
This commit is contained in:
+2
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "website"
|
||||
version = "0.4.3"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -11,6 +11,7 @@ rocket-client-addr = "0.5.4"
|
||||
serde = { workspace = true }
|
||||
reports = { path = "../reports" }
|
||||
querying = { path = "../querying" }
|
||||
frontend = { path = "../frontend" }
|
||||
env_logger = "0.11.10"
|
||||
rocket-cache-response = "0.6.4"
|
||||
log = { workspace = true }
|
||||
|
||||
@@ -1,79 +1,6 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
io::{self, Cursor},
|
||||
path::Path,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
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>> {
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
|
||||
|
||||
println!("cargo:rerun-if-env-changed=LUCIDE_VERSION");
|
||||
install_npm("lucide", option_env!("LUCIDE_VERSION"), HashMap::from([
|
||||
(PathBuf::from("package/dist/umd/lucide.min.js"), out_dir.join("lucide.js")),
|
||||
]))?;
|
||||
|
||||
println!("cargo:rerun-if-env-changed=CHARTJS_VERSION");
|
||||
install_npm("chart.js", option_env!("CHARTJS_VERSION"), HashMap::from([
|
||||
(PathBuf::from("package/dist/chart.umd.min.js"), out_dir.join("chart.js")),
|
||||
]))?;
|
||||
|
||||
println!("cargo:rerun-if-env-changed=CHARTDATALABELS_VERSION");
|
||||
install_npm("chartjs-plugin-datalabels", option_env!("CHARTDATALABELS_VERSION"), HashMap::from([
|
||||
(PathBuf::from("package/dist/chartjs-plugin-datalabels.min.js"), out_dir.join("chartjs-plugin-datalabels.js")),
|
||||
]))?;
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_npm(package: &str, version: Option<&str>, mut entries: HashMap<PathBuf, PathBuf>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = Client::new();
|
||||
let metadata_url = format!(
|
||||
"https://registry.npmjs.org/{package}/{}",
|
||||
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);
|
||||
|
||||
for entry in archive.entries()? {
|
||||
let mut entry = entry?;
|
||||
let path = PathBuf::from(entry.path()?);
|
||||
if let Some(dest) = entries.remove(&path) {
|
||||
println!("Found {:?}, extracting to {:?}", path.file_name(), dest);
|
||||
let mut out_file = File::create(&dest)?;
|
||||
io::copy(&mut entry, &mut out_file)?;
|
||||
}
|
||||
}
|
||||
|
||||
if !entries.is_empty() {
|
||||
panic!("Not all entries found! {:?}", entries);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
ALTER TABLE
|
||||
queries
|
||||
ALTER COLUMN
|
||||
id
|
||||
SET
|
||||
DEFAULT uuidv7();
|
||||
|
||||
CREATE INDEX IF NOT EXISTS reports_reporter_date_id_desc_idx ON reports (reporter, date DESC, id DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS report_row_domain_idx ON report_row (domain);
|
||||
|
||||
DROP MATERIALIZED VIEW IF EXISTS whitelist;
|
||||
|
||||
CREATE MATERIALIZED VIEW whitelist AS WITH report_domains AS (
|
||||
SELECT
|
||||
DISTINCT domain
|
||||
FROM
|
||||
report_row
|
||||
)
|
||||
SELECT
|
||||
latest.domain,
|
||||
d.rank,
|
||||
MAX(
|
||||
CASE
|
||||
WHEN latest.evidence = 'ok' THEN latest.date
|
||||
END
|
||||
) AS last_ok
|
||||
FROM
|
||||
report_domains rd
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT
|
||||
rd.domain,
|
||||
rr.evidence,
|
||||
r.date
|
||||
FROM
|
||||
reports r
|
||||
JOIN report_row rr ON rr.report_id = r.id
|
||||
AND rr.domain = rd.domain
|
||||
WHERE
|
||||
r.reporter = 1
|
||||
ORDER BY
|
||||
r.date DESC,
|
||||
r.id DESC
|
||||
LIMIT
|
||||
5
|
||||
) latest
|
||||
LEFT JOIN domains d ON d.domain = latest.domain
|
||||
GROUP BY
|
||||
latest.domain,
|
||||
d.rank
|
||||
HAVING
|
||||
COUNT(*) FILTER (
|
||||
WHERE
|
||||
latest.evidence = 'ok'
|
||||
) >= COUNT(*) / 2.0
|
||||
ORDER BY
|
||||
d.rank;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS whitelist_domain_idx ON whitelist (domain);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS whitelist_rank_idx ON whitelist (rank);
|
||||
@@ -1,10 +1,10 @@
|
||||
use reports::AgencyReport;
|
||||
use rocket::State;
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::json::serde_json::json;
|
||||
use rocket::serde::json::{Json, Value};
|
||||
use rocket::serde::msgpack::MsgPack;
|
||||
use rocket_client_addr::ClientRealAddr;
|
||||
use rocket::State;
|
||||
use sqlx::Acquire;
|
||||
use sqlx::postgres::PgPool;
|
||||
|
||||
@@ -20,7 +20,10 @@ pub async fn upload_report(
|
||||
agency: Agency,
|
||||
pool: &State<PgPool>,
|
||||
) -> Result<Json<Value>, (Status, String)> {
|
||||
let mut db = pool.acquire().await.map_err(|e| (Status::InternalServerError, e.to_string()))?;
|
||||
let mut db = pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| (Status::InternalServerError, e.to_string()))?;
|
||||
let mut tx = db
|
||||
.begin()
|
||||
.await
|
||||
|
||||
+56
-4
@@ -1,4 +1,4 @@
|
||||
use crate::db::{check_whitelist, save_query, WhitelistedEntry};
|
||||
use crate::db::{WhitelistedEntry, check_whitelist, save_query};
|
||||
use governor::clock::DefaultClock;
|
||||
use governor::state::keyed::DefaultKeyedStateStore;
|
||||
use governor::{Quota, RateLimiter};
|
||||
@@ -8,20 +8,21 @@ use querying::geoip::IpInfo;
|
||||
use querying::lists::NetworkRecord;
|
||||
use querying::target::Target;
|
||||
use querying::{Check, CheckError, CheckVerdict, Checker};
|
||||
use rocket::State;
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::tokio::sync::RwLock;
|
||||
use rocket::State;
|
||||
use rocket_client_addr::ClientRealAddr;
|
||||
use serde::Serialize;
|
||||
use sqlx::postgres::PgPool;
|
||||
use sqlx::types::Uuid;
|
||||
use sqlx::types::chrono::{DateTime, Utc};
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::num::NonZeroU32;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type ApiRateLimiter =
|
||||
RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock>;
|
||||
pub type ApiRateLimiter = RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock>;
|
||||
|
||||
pub fn build_rate_limiter(per_minute: u32) -> ApiRateLimiter {
|
||||
RateLimiter::keyed(Quota::per_minute(
|
||||
@@ -45,6 +46,8 @@ pub struct ApiCheckResponse {
|
||||
pub asn_info: Option<AsnInfo>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub whitelist: Option<WhitelistedEntry>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub subnet_size: Option<String>,
|
||||
}
|
||||
|
||||
fn build_response(
|
||||
@@ -79,6 +82,7 @@ fn build_response(
|
||||
geo: check.geo,
|
||||
asn_info: check.asn_info,
|
||||
whitelist,
|
||||
subnet_size: target.subnet_size(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,3 +135,51 @@ pub async fn check(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/healthcheck")]
|
||||
pub async fn healthcheck(checker: &State<Arc<RwLock<Checker>>>) -> (Status, String) {
|
||||
if checker.read().await.last_update().is_some() {
|
||||
(Status::Ok, "OK".to_string())
|
||||
} else {
|
||||
(Status::InternalServerError, "LOADING DATABASES".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/feedback/<uuid>/<works>")]
|
||||
pub async fn feedback(
|
||||
uuid: &str,
|
||||
works: bool,
|
||||
pool: &State<PgPool>,
|
||||
addr: &ClientRealAddr,
|
||||
) -> Result<(), Status> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO human_reports (id, source_ip, works) VALUES ($1, $2, $3)",
|
||||
Uuid::try_parse(uuid).map_err(|_| Status::BadRequest)?,
|
||||
addr.ip.to_string(),
|
||||
works
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|_| Status::InternalServerError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ApiStatusResponse {
|
||||
domain_count: usize,
|
||||
v4_count: usize,
|
||||
last_update: Option<DateTime<Utc>>,
|
||||
version: &'static str,
|
||||
}
|
||||
|
||||
#[get("/status")]
|
||||
pub async fn get_system_status(checker: &State<Arc<RwLock<Checker>>>) -> Json<ApiStatusResponse> {
|
||||
let checker_ref = checker.read().await;
|
||||
Json(ApiStatusResponse {
|
||||
domain_count: checker_ref.total_domains().await,
|
||||
v4_count: checker_ref.total_v4s().await,
|
||||
last_update: checker_ref.last_update(),
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
})
|
||||
}
|
||||
|
||||
+20
-15
@@ -2,14 +2,14 @@ use crate::agency::Agency;
|
||||
use querying::target::Target;
|
||||
use querying::{Check, CheckVerdict, Checker};
|
||||
use rocket::http::Status;
|
||||
use rocket::outcome::{try_outcome, IntoOutcome};
|
||||
use rocket::outcome::{IntoOutcome, try_outcome};
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
use rocket::tokio::sync::RwLockReadGuard;
|
||||
use rocket::{Request, State};
|
||||
use rocket_client_addr::ClientRealAddr;
|
||||
use serde::Serialize;
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use sqlx::types::Uuid;
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
|
||||
pub async fn save_query(
|
||||
db: &mut sqlx::PgConnection,
|
||||
@@ -40,12 +40,14 @@ pub async fn save_query(
|
||||
|
||||
let (resolved_ips, cdn_networks) = match target {
|
||||
Target::Asn(_) => (vec![], vec![]),
|
||||
_ => (check
|
||||
.ips
|
||||
.iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
cdn_networks)
|
||||
_ => (
|
||||
check
|
||||
.ips
|
||||
.iter()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
cdn_networks,
|
||||
),
|
||||
};
|
||||
|
||||
let id = sqlx::query_scalar(
|
||||
@@ -93,11 +95,12 @@ impl<'r> FromRequest<'r> for Agency {
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let pool = request.guard::<&State<sqlx::PgPool>>().await.unwrap();
|
||||
let mut db = try_outcome!(pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| Some(e))
|
||||
.or_forward(Status::InternalServerError));
|
||||
let mut db = try_outcome!(
|
||||
pool.acquire()
|
||||
.await
|
||||
.map_err(|e| Some(e))
|
||||
.or_forward(Status::InternalServerError)
|
||||
);
|
||||
let token = request.headers().get_one("Authorization");
|
||||
|
||||
let token = try_outcome!(
|
||||
@@ -166,7 +169,6 @@ pub async fn collect_histogram(
|
||||
limit: i32,
|
||||
filter: bool,
|
||||
) -> Result<Vec<WhitelistHistogramBin>, sqlx::Error> {
|
||||
|
||||
sqlx::query_as!(
|
||||
WhitelistHistogramBin,
|
||||
"WITH bins AS (
|
||||
@@ -181,7 +183,10 @@ FROM bins b
|
||||
LEFT JOIN whitelist w
|
||||
ON FLOOR(w.rank / $2) = b.bin
|
||||
GROUP BY b.bin
|
||||
ORDER BY b.bin;", bins, limit / bins, filter
|
||||
ORDER BY b.bin;",
|
||||
bins,
|
||||
limit / bins,
|
||||
filter
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
|
||||
+83
-245
@@ -5,205 +5,21 @@ mod api;
|
||||
mod db;
|
||||
mod whitelist;
|
||||
|
||||
use crate::db::{check_whitelist, save_query, WhitelistedEntry};
|
||||
use log::{error, info, warn, LevelFilter};
|
||||
use querying::target::Target;
|
||||
use querying::{Check, CheckError, CheckVerdict, Checker};
|
||||
use env_logger::Env;
|
||||
use log::{LevelFilter, error, info};
|
||||
use querying::Checker;
|
||||
use rocket::fairing::AdHoc;
|
||||
use rocket::fs::FileServer;
|
||||
use rocket::http::Status;
|
||||
use rocket::response::content::RawJavaScript;
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::response::content::RawHtml;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::tokio::sync::RwLock;
|
||||
use rocket::tokio::time;
|
||||
use rocket::{fairing, tokio, Build, Request, Rocket, State};
|
||||
use rocket_cache_response::CacheResponse;
|
||||
use rocket_client_addr::ClientRealAddr;
|
||||
use rocket_dyn_templates::{context, Metadata, Template};
|
||||
use rocket::{Build, Request, Rocket, fairing, tokio};
|
||||
use serde::Serialize;
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use env_logger::Env;
|
||||
use rocket::serde::json::Json;
|
||||
use sqlx::types::Uuid;
|
||||
use sqlx::postgres::PgPool;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct GlobalContext {
|
||||
version: &'static str,
|
||||
}
|
||||
|
||||
impl GlobalContext {
|
||||
fn new() -> Self {
|
||||
GlobalContext {
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index(checker: &State<Arc<RwLock<Checker>>>) -> Template {
|
||||
let checker_ref = checker.read().await;
|
||||
Template::render(
|
||||
"index",
|
||||
context! {
|
||||
global: GlobalContext::new(),
|
||||
domain_count: format_number(checker_ref.total_domains().await),
|
||||
v4_count: format_number(checker_ref.total_v4s().await),
|
||||
last_update: checker_ref.last_update(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/kb/<page>")]
|
||||
fn page(metadata: Metadata, page: &str) -> Option<Template> {
|
||||
let page = format!("pages/{}", page);
|
||||
if !metadata.contains_template(&page) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Template::render(
|
||||
page,
|
||||
context! {
|
||||
global: GlobalContext::new(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/healthcheck")]
|
||||
async fn healthcheck(checker: &State<Arc<RwLock<Checker>>>) -> (Status, String) {
|
||||
if checker.read().await.last_update().is_some() {
|
||||
(Status::Ok, "OK".to_string())
|
||||
} else {
|
||||
(Status::InternalServerError, "LOADING DATABASES".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/feedback/<uuid>/<works>")]
|
||||
async fn feedback(uuid: &str, works: bool, pool: &State<PgPool>, addr: &ClientRealAddr) -> Result<(), Status> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO human_reports (id, source_ip, works) VALUES ($1, $2, $3)",
|
||||
Uuid::try_parse(uuid).map_err(|_| Status::BadRequest)?,
|
||||
addr.ip.to_string(),
|
||||
works
|
||||
).execute(&**pool).await.map_err(|_| Status::InternalServerError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/check?<target>")]
|
||||
async fn check(
|
||||
target: &str,
|
||||
checker: &State<Arc<RwLock<Checker>>>,
|
||||
addr: &ClientRealAddr,
|
||||
pool: &State<PgPool>,
|
||||
) -> Result<Template, Status> {
|
||||
let target = Target::from(target.trim());
|
||||
let check = checker.read().await.check(target.clone()).await;
|
||||
|
||||
let mut db = pool.acquire().await.map_err(|_| Status::InternalServerError)?;
|
||||
|
||||
let id: Option<String> = if let Ok(check) = &check {
|
||||
match save_query(&mut *db, &target, check, addr, checker.read().await).await {
|
||||
Ok(id) => Some(id.to_string()),
|
||||
Err(e) => {
|
||||
warn!("Failed to save check: {:?}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let whitelist: Option<WhitelistedEntry> = if let Target::Domain(domain) = &target {
|
||||
check_whitelist(domain, &mut *db)
|
||||
.await
|
||||
.map_err(|_| Status::InternalServerError)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match check {
|
||||
Err(CheckError::NotFound) => Ok(Template::render(
|
||||
"empty",
|
||||
context! {
|
||||
global: GlobalContext::new(),
|
||||
target: target.to_query(),
|
||||
target_type: target.readable_type(),
|
||||
},
|
||||
)),
|
||||
Ok(Check {
|
||||
verdict: CheckVerdict::Clear,
|
||||
geo,
|
||||
ips,
|
||||
rkn_subnets,
|
||||
asn_info,
|
||||
}) => Ok(Template::render(
|
||||
"result",
|
||||
context! {
|
||||
id,
|
||||
global: GlobalContext::new(),
|
||||
found: false,
|
||||
target: target.to_query(),
|
||||
target_type: target.readable_type(),
|
||||
blocked_subnets: rkn_subnets.iter()
|
||||
.map(|n| n.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
whitelist,
|
||||
ips,
|
||||
geo,
|
||||
subnet_size: target.subnet_size(),
|
||||
asn_info,
|
||||
},
|
||||
)),
|
||||
Ok(Check {
|
||||
verdict:
|
||||
CheckVerdict::Blocked {
|
||||
rkn_domain,
|
||||
cdn_provider_subnets,
|
||||
},
|
||||
geo,
|
||||
rkn_subnets,
|
||||
ips,
|
||||
asn_info,
|
||||
}) => Ok(Template::render(
|
||||
"result",
|
||||
context! {
|
||||
id,
|
||||
global: GlobalContext::new(),
|
||||
found: true,
|
||||
domain: rkn_domain,
|
||||
providers: cdn_provider_subnets,
|
||||
blocked_subnets: rkn_subnets.iter()
|
||||
.map(|n| n.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
target: target.to_query(),
|
||||
target_type: target.readable_type(),
|
||||
whitelist,
|
||||
ips,
|
||||
geo,
|
||||
subnet_size: target.subnet_size(),
|
||||
asn_info,
|
||||
},
|
||||
)),
|
||||
Err(e) => {
|
||||
error!("check failed {:?}", e);
|
||||
Err(Status::InternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[catch(default)]
|
||||
fn default(status: Status, _req: &Request) -> Template {
|
||||
Template::render(
|
||||
"error",
|
||||
context! {
|
||||
global: GlobalContext::new(),
|
||||
status: status.code,
|
||||
reason: status.reason_lossy(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct JsonError {
|
||||
@@ -213,44 +29,57 @@ struct JsonError {
|
||||
|
||||
#[catch(default)]
|
||||
fn api_error(status: Status, _: &Request) -> Json<JsonError> {
|
||||
Json(JsonError { code: status.code, info: status.reason_lossy().to_string() })
|
||||
Json(JsonError {
|
||||
code: status.code,
|
||||
info: status.reason_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[rocket::get("/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,
|
||||
}
|
||||
}
|
||||
#[rocket::get("/chart.js")]
|
||||
fn chartjs() -> CacheResponse<RawJavaScript<&'static [u8]>> {
|
||||
CacheResponse::Public {
|
||||
responder: RawJavaScript(include_bytes!(concat!(env!("OUT_DIR"), "/chart.js"))),
|
||||
max_age: 604800,
|
||||
must_revalidate: false,
|
||||
}
|
||||
}
|
||||
#[rocket::get("/chartjs-plugin-datalabels.js")]
|
||||
fn chartjs_datalabels() -> CacheResponse<RawJavaScript<&'static [u8]>> {
|
||||
CacheResponse::Public {
|
||||
responder: RawJavaScript(include_bytes!(concat!(env!("OUT_DIR"), "/chartjs-plugin-datalabels.js"))),
|
||||
max_age: 604800,
|
||||
must_revalidate: false,
|
||||
}
|
||||
#[get("/")]
|
||||
fn frontend_index() -> Option<RawHtml<&'static str>> {
|
||||
frontend::DIST
|
||||
.get_file("index.html")
|
||||
.and_then(frontend::File::contents_utf8)
|
||||
.map(RawHtml)
|
||||
}
|
||||
|
||||
fn format_number(number: usize) -> String {
|
||||
number
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.rchunks(3)
|
||||
.rev()
|
||||
.map(std::str::from_utf8)
|
||||
.collect::<Result<Vec<&str>, _>>()
|
||||
.unwrap()
|
||||
.join(" ")
|
||||
#[get("/<path..>", rank = 20)]
|
||||
fn frontend_asset(path: PathBuf) -> Option<(ContentType, &'static [u8])> {
|
||||
if is_backend_path(&path) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(file) = embedded_file(&path) {
|
||||
return Some((content_type(&path), file.contents()));
|
||||
}
|
||||
|
||||
if path.extension().is_none() {
|
||||
let index = frontend::DIST.get_file("index.html")?;
|
||||
return Some((ContentType::HTML, index.contents()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn embedded_file(path: &PathBuf) -> Option<&'static frontend::File<'static>> {
|
||||
let path = path.to_string_lossy().replace('\\', "/");
|
||||
frontend::DIST.get_file(path)
|
||||
}
|
||||
|
||||
fn is_backend_path(path: &PathBuf) -> bool {
|
||||
matches!(
|
||||
path.components()
|
||||
.next()
|
||||
.and_then(|component| component.as_os_str().to_str()),
|
||||
Some("api" | "agency" | "whitelist")
|
||||
)
|
||||
}
|
||||
|
||||
fn content_type(path: &PathBuf) -> ContentType {
|
||||
path.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.and_then(ContentType::from_extension)
|
||||
.unwrap_or(ContentType::Binary)
|
||||
}
|
||||
|
||||
async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
|
||||
@@ -293,8 +122,8 @@ async fn rocket() -> _ {
|
||||
info!("Downloaded, updating...");
|
||||
checker_clone.read().await.update_all(bases).await;
|
||||
info!("Updated databases");
|
||||
},
|
||||
Err(e) => log::error!("Failed to download all DBs"),
|
||||
}
|
||||
Err(_) => log::error!("Failed to download all DBs"),
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -306,14 +135,18 @@ async fn rocket() -> _ {
|
||||
let api_limiter = std::sync::Arc::new(api::build_rate_limiter(rate_limit_rpm));
|
||||
|
||||
let pool = sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(std::env::var("DATABASE_MAX_CONNECTIONS")
|
||||
.unwrap_or("100".to_string())
|
||||
.parse()
|
||||
.unwrap())
|
||||
.min_connections(std::env::var("DATABASE_MIN_CONNECTIONS")
|
||||
.unwrap_or("10".to_string())
|
||||
.parse()
|
||||
.unwrap())
|
||||
.max_connections(
|
||||
std::env::var("DATABASE_MAX_CONNECTIONS")
|
||||
.unwrap_or("100".to_string())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.min_connections(
|
||||
std::env::var("DATABASE_MIN_CONNECTIONS")
|
||||
.unwrap_or("10".to_string())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.acquire_timeout(Duration::from_secs(5))
|
||||
.idle_timeout(Duration::from_secs(60))
|
||||
.connect(&dotenvy::var("DATABASE_URL").expect("DATABASE_URL must be set"))
|
||||
@@ -325,14 +158,19 @@ async fn rocket() -> _ {
|
||||
.manage(pool)
|
||||
.manage(api_limiter)
|
||||
.attach(AdHoc::try_on_ignite("SQLx Migrations", run_migrations))
|
||||
.mount("/", routes![index, check, healthcheck, page, feedback])
|
||||
.mount("/vendor", routes![lucide, chartjs, chartjs_datalabels])
|
||||
.mount("/api/v1", routes![api::check])
|
||||
.register("/api", catchers![api_error])
|
||||
.mount("/", routes![api::feedback]) // DEPRECATED: backwards compatibility
|
||||
.mount(
|
||||
"/api/v1",
|
||||
routes![
|
||||
api::check,
|
||||
api::healthcheck,
|
||||
api::feedback,
|
||||
api::get_system_status,
|
||||
whitelist::histogram
|
||||
],
|
||||
)
|
||||
.mount("/agency", routes![agency::upload_report])
|
||||
.mount("/whitelist", routes![whitelist::histogram, whitelist::export_csv])
|
||||
.register("/agency", catchers![api_error])
|
||||
.register("/", catchers![default])
|
||||
.mount("/", FileServer::from(PathBuf::from("static")))
|
||||
.attach(Template::fairing())
|
||||
.mount("/whitelist", routes![whitelist::export_csv])
|
||||
.mount("/", routes![frontend_index, frontend_asset])
|
||||
.register("/", catchers![api_error])
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::db::{WhitelistHistogramBin, collect_histogram};
|
||||
use rocket::State;
|
||||
use rocket::futures::StreamExt;
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::request::FromParam;
|
||||
use rocket_cache_response::CacheResponse;
|
||||
use rocket::State;
|
||||
use std::io;
|
||||
use rocket::serde::json::Json;
|
||||
use crate::db::{collect_histogram, WhitelistHistogramBin};
|
||||
use rocket_cache_response::CacheResponse;
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::io;
|
||||
|
||||
enum ExportType {
|
||||
Full,
|
||||
@@ -39,7 +39,10 @@ pub async fn export_csv(
|
||||
}
|
||||
};
|
||||
|
||||
let mut db = pool.acquire().await.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
let mut db = pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
|
||||
let mut stream = db
|
||||
.copy_out_raw(query)
|
||||
@@ -61,9 +64,19 @@ pub async fn export_csv(
|
||||
}
|
||||
|
||||
#[get("/histogram?<filter>&<limit>")]
|
||||
pub async fn histogram(pool: &State<PgPool>, filter: Option<bool>, limit: Option<i32>) -> Result<Json<Vec<WhitelistHistogramBin>>, Status> {
|
||||
pub async fn histogram(
|
||||
pool: &State<PgPool>,
|
||||
filter: Option<bool>,
|
||||
limit: Option<i32>,
|
||||
) -> Result<Json<Vec<WhitelistHistogramBin>>, Status> {
|
||||
let limit = limit.unwrap_or(100_000).clamp(0, 1_000_000);
|
||||
let mut db = pool.acquire().await.map_err(|_| Status::InternalServerError)?;
|
||||
Ok(Json(collect_histogram(&mut *db, 50, limit, filter.is_some()).await
|
||||
.map_err(|_| Status::InternalServerError)?))
|
||||
let mut db = pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|_| Status::InternalServerError)?;
|
||||
Ok(Json(
|
||||
collect_histogram(&mut *db, 50, limit, filter.is_some())
|
||||
.await
|
||||
.map_err(|_| Status::InternalServerError)?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
{% block head %}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="stylesheet" href="/main.css"/>
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
|
||||
{% block metadata %}
|
||||
<title>Cheburcheck</title>
|
||||
<meta name="description" content="Проверка доменов и IP-адресов на наличие в реестре блокировок Роскомнадзора и заблокированных CDN.">
|
||||
<meta name="keywords" content="проверка домена, заблокирован ли сайт, Роскомнадзор, чебурнет, cdn, блокировки">
|
||||
<meta property="og:title" content="Cheburcheck">
|
||||
<meta property="og:description" content="Проверка доменов и IP-адресов на наличие в реестре блокировок Роскомнадзора и заблокированных CDN.">
|
||||
<meta property="og:url" content="https://cheburcheck.ru">
|
||||
<meta property="og:image" content="https://cheburcheck.ru/og-cover.png">
|
||||
{% endblock metadata %}
|
||||
|
||||
<meta property="og:site_name" content="Cheburcheck">
|
||||
<meta property="og:type" content="website">
|
||||
<meta name="author" content="Cheburcheck">
|
||||
|
||||
{% 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 href="/kb/faq" class="text-muted">FAQ</a>
|
||||
<a class="version-link text-muted" href="https://github.com/LowderPlay/cheburcheck">
|
||||
<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">
|
||||
<a href="mailto:support@cheburcheck.ru">support@cheburcheck.ru</a>
|
||||
-
|
||||
Сделано в России
|
||||
<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>
|
||||
@@ -1,82 +0,0 @@
|
||||
{% extends 'base' %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'search-form' %}
|
||||
|
||||
<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>
|
||||
|
||||
{% endblock content %}
|
||||
@@ -1,27 +0,0 @@
|
||||
{% 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">
|
||||
{% if status == 500 %}
|
||||
Внутренняя ошибка сервера. Пожалуйста, попробуйте позже.
|
||||
{% elif status == 502 %}
|
||||
Сервер временно недоступен. Попробуйте обновить страницу.
|
||||
{% elif status == 503 %}
|
||||
Сервис временно недоступен. Ведутся технические работы.
|
||||
{% elif status == 504 %}
|
||||
Превышено время ожидания ответа от сервера. Попробуйте позже.
|
||||
{% elif status == 400 %}
|
||||
Неверный запрос. Проверьте правильность введенных данных.
|
||||
{% elif status == 404 %}
|
||||
Страница не найдена. Проверьте правильность адреса.
|
||||
{% elif status == 422 %}
|
||||
Невозможно обработать запрос. Проверьте формат данных.
|
||||
{% elif status == 429 %}
|
||||
Слишком много запросов. Пожалуйста, подождите немного.
|
||||
{% else %}
|
||||
Что-то пошло не так. Возможно в запросе есть ошибка?
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endblock content %}
|
||||
@@ -1,137 +0,0 @@
|
||||
{% macro head() %}
|
||||
<script src="/vendor/chart.js"></script>
|
||||
<script src="/vendor/chartjs-plugin-datalabels.js"></script>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro histogram(id="histogram",
|
||||
endpoint="/whitelist/histogram",
|
||||
data_label="Количество ресурсов",
|
||||
x_label="Ранг домена",
|
||||
y_label="Количество ресурсов") %}
|
||||
<canvas id="{{ id }}"></canvas>
|
||||
|
||||
<style>
|
||||
#{{ id }} {
|
||||
max-height: 480px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(async function () {
|
||||
function formatNumber(num) {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
const TEXT_LIGHT = '#d4d4d4';
|
||||
const RED_ACCENT = '#ef4444';
|
||||
const AXIS_COLOR = '#404040';
|
||||
const GRID_COLOR = 'rgba(64, 64, 64, 0.2)';
|
||||
|
||||
const ctx = document.getElementById('{{ id }}').getContext('2d');
|
||||
const response = await fetch("{{ endpoint | safe }}");
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
const labels = data.map(item =>
|
||||
`${formatNumber(Math.round(item.bin_min_rank))}-${formatNumber(Math.round(item.bin_max_rank))}`
|
||||
);
|
||||
const counts = data.map(item => item.count);
|
||||
|
||||
Chart.register(ChartDataLabels);
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: '{{ data_label }}',
|
||||
data: counts,
|
||||
backgroundColor: RED_ACCENT,
|
||||
borderColor: RED_ACCENT,
|
||||
borderWidth: 1,
|
||||
borderRadius: 6,
|
||||
hoverBackgroundColor: `${RED_ACCENT}cc`,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
datalabels: {
|
||||
anchor: 'end',
|
||||
align: 'top',
|
||||
offset: 8,
|
||||
color: TEXT_LIGHT,
|
||||
font: {
|
||||
weight: 'bold',
|
||||
size: 10
|
||||
},
|
||||
formatter: (value) => value > 0 ? formatNumber(value) : '',
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += formatNumber(context.parsed.y);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
},
|
||||
backgroundColor: 'rgba(30, 30, 30, 0.9)',
|
||||
titleColor: TEXT_LIGHT,
|
||||
bodyColor: TEXT_LIGHT,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '{{ x_label }}',
|
||||
color: TEXT_LIGHT
|
||||
},
|
||||
ticks: {
|
||||
color: TEXT_LIGHT,
|
||||
maxRotation: 45,
|
||||
minRotation: 45
|
||||
},
|
||||
grid: {
|
||||
color: GRID_COLOR,
|
||||
drawBorder: false
|
||||
},
|
||||
border: {
|
||||
color: AXIS_COLOR
|
||||
}
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: '{{ y_label }}',
|
||||
color: TEXT_LIGHT
|
||||
},
|
||||
ticks: {
|
||||
color: TEXT_LIGHT,
|
||||
callback: () => ''
|
||||
},
|
||||
grid: {
|
||||
color: GRID_COLOR,
|
||||
drawBorder: false
|
||||
},
|
||||
border: {
|
||||
color: AXIS_COLOR
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endmacro metadata %}
|
||||
@@ -1,34 +0,0 @@
|
||||
{% extends 'base' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="search-container">
|
||||
<h1>Статус Ресурса</h1>
|
||||
<p class="text-muted">Введите домен или IP-адрес для поиска по спискам заблокированных адресов и хостинг-провайдеров.</p>
|
||||
</div>
|
||||
|
||||
{% include 'search-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>{{ 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>{{ 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("{{ last_update }}").toLocaleString())</script></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -1,18 +0,0 @@
|
||||
{% extends 'base' %}
|
||||
{% import 'typography' as typography %}
|
||||
|
||||
{% macro metadata(title, description, image="https://cheburcheck.ru/og-cover.png", keywords, url="/") %}
|
||||
<title>{{ title }} - Cheburcheck</title>
|
||||
<meta name="description" content="{{ description }}">
|
||||
<meta name="keywords" content="проверка домена, заблокирован ли сайт, Роскомнадзор, чебурнет, cdn, блокировки, {{ keywords }}">
|
||||
<meta property="og:title" content="{{ title }}">
|
||||
<meta property="og:description" content="{{ description }}">
|
||||
<meta property="og:url" content="https://cheburcheck.ru{{ url }}">
|
||||
<meta property="og:image" content="{{ image }}">
|
||||
{% endmacro metadata %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-container">
|
||||
{% block page_text %}{% endblock %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -1,92 +0,0 @@
|
||||
{% extends 'page' %}
|
||||
{% import 'page' as page %}
|
||||
|
||||
{% block metadata %}
|
||||
{{ page::metadata(title="FAQ", description="Ответы на частые вопросы", keywords="faq", url="/kb/faq") }}
|
||||
{% endblock metadata %}
|
||||
|
||||
{% block page_text %}
|
||||
<h1>Ответы на частые вопросы</h1>
|
||||
<p>
|
||||
На этой странице собраны ответы на часто задаваемые вопросы.
|
||||
Если вас интересует что-то ещё — пишите нам на почту <a href="mailto:support@cheburcheck.ru">support@cheburcheck.ru</a>.
|
||||
</p>
|
||||
|
||||
{{ typography::heading(title="Что это за сайт?") }}
|
||||
<p>
|
||||
Этот сайт — инструмент, который позволяет проверить, находится ли сайт, домен,
|
||||
IP-адрес в <a href="#{{ "Откуда вы берете эти списки?" | slugify }}">списках</a>, блокируемых Роскомнадзором на территории России.
|
||||
</p>
|
||||
|
||||
{{ typography::heading(title="Откуда вы берете эти списки?") }}
|
||||
<p>
|
||||
Для поиска сайтов мы используем списки IP-подсетей популярных CDN-провайдеров,
|
||||
которые могут блокироваться Роскомнадзором, а также домены из самого реестра Роскомнадзора.
|
||||
</p>
|
||||
<p>
|
||||
Подсети, которые мы используем, публикуют сами провайдеры, либо собираются из принадлежащих им автономных сетей.
|
||||
Мы используем <a href="https://github.com/123jjck/cdn-ip-ranges">этот репозиторий</a>, в котором автоматически
|
||||
агрегируются эти подсети.
|
||||
</p>
|
||||
<p>
|
||||
Данные из Реестра Роскомнадзора подгружаются из сервисов <a href="https://antifilter.download/">antifilter.download</a>
|
||||
и <a href="https://antifilter.network/">antifilter.network</a>.
|
||||
</p>
|
||||
|
||||
{{ typography::heading(title="Что такое «белый список»?") }}
|
||||
<p>
|
||||
РКН формирует свой белый список доменов, которые доступны,
|
||||
даже если их адреса находятся в списке заблокированных подсетей.
|
||||
</p>
|
||||
<a href="/kb/whitelist">Подробнее...</a>
|
||||
|
||||
{{ typography::heading(title='Мне написало, что сайт «Заблокирован», но у меня все работает!') }}
|
||||
<p>
|
||||
Мы не можем с точностью утверждать, заблокирован сайт или нет.
|
||||
Блокировки Роскомнадзора, зачастую, непредсказуемые, непубличные и незаконные.
|
||||
Например, блокировка CDN-сервисов, таких, как
|
||||
<a href="https://blog.cloudflare.com/russian-internet-users-are-unable-to-access-the-open-internet/">Cloudflare</a>,
|
||||
никак не комментировалась официально и не публиковались в Реестре. Аналогичная ситуация с
|
||||
<a href="https://ria.ru/20241219/putin-1990168675.html">YouTube</a>, информация о
|
||||
блокировке которого, не добавлена в Реестр — для таких случаев мы создали
|
||||
<a href="https://github.com/LowderPlay/cheburcheck/blob/master/querying/dist-domains.txt">свой список</a> подобных сайтов.
|
||||
Однако, список не исчерпывающий, поэтому
|
||||
<a href="https://github.com/LowderPlay/cheburcheck/pulls">принимаются правки</a>.
|
||||
</p>
|
||||
<p>
|
||||
Сайты помечаются как "заблокированные", в случае, если они были найдены хотя бы в одном списке.
|
||||
</p>
|
||||
|
||||
{{ typography::heading(title='Как часто обновляются списки?') }}
|
||||
<p>
|
||||
Мы обновляем списки из источников раз в 6 часов.
|
||||
</p>
|
||||
<p>
|
||||
Сами источники обновляют их с разной периодичностью:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="https://github.com/123jjck/cdn-ip-ranges">CDN</a> — раз в 12 часов.</li>
|
||||
<li><a href="/kb/whitelist">Белые списки</a> — топ-100,000 раз в 6 часов; топ-1,000,000 раз в сутки.</li>
|
||||
<li><a href="https://antifilter.download/">Домены из Реестра РКН</a> — раз в полчаса.</li>
|
||||
<li><a href="https://antifilter.network/">Подсети из Реестра РКН</a> — раз в час.</li>
|
||||
</ul>
|
||||
|
||||
{{ typography::heading(title='Как я могу помочь в развитии сайта?') }}
|
||||
<p>
|
||||
Самый простой способ помочь — рассказать о сайте знакомым.
|
||||
</p>
|
||||
<p>
|
||||
Если у вас есть навыки разработки, то мы будем рады принять ваши pull-requests
|
||||
на <a href="https://github.com/LowderPlay/cheburcheck">GitHub</a>.
|
||||
</p>
|
||||
<p>
|
||||
Если вы хотите помочь финансово:
|
||||
</p>
|
||||
<ul>
|
||||
<li>TON: <code>UQAACsiwpGryjP-kqp4TJPAWpXytuB6M_puuO0Cg5zNvaSJW</code></li>
|
||||
</ul>
|
||||
<p>
|
||||
Большое спасибо! ❤️
|
||||
</p>
|
||||
{% endblock page_text %}
|
||||
@@ -1,111 +0,0 @@
|
||||
{% extends 'page' %}
|
||||
{% import 'page' as page %}
|
||||
|
||||
{% block metadata %}
|
||||
{{ page::metadata(title="Test", description="Проверка", keywords="test", url="/kb/test") }}
|
||||
{% endblock metadata %}
|
||||
|
||||
{% block page_text %}
|
||||
<img class="fullwidth" src="https://placehold.co/600x400">
|
||||
<h1>Hello, world!</h1>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Aenean at scelerisque purus. Integer eu justo vel mi consectetur sagittis.
|
||||
Proin aliquam, nulla sit amet ornare interdum, leo orci posuere nulla, convallis consequat nibh ex id felis.
|
||||
Nam dapibus enim mauris, bibendum rutrum orci blandit at.
|
||||
Curabitur sed dui sit amet felis consequat cursus quis at magna.
|
||||
Suspendisse at neque lobortis, pulvinar quam at, posuere lorem.
|
||||
Etiam sed elit vehicula leo tincidunt rhoncus et ut metus.
|
||||
Quisque egestas diam non sem pellentesque bibendum.
|
||||
Fusce posuere pretium nisl sit amet pharetra.
|
||||
Maecenas urna odio, dignissim non efficitur vel, varius eu nulla.
|
||||
Sed convallis id lorem sed suscipit. In hac habitasse platea dictumst.
|
||||
Nulla fermentum arcu urna, vitae gravida leo accumsan ut. Phasellus accumsan hendrerit erat,
|
||||
nec imperdiet odio ullamcorper sed.
|
||||
</p>
|
||||
|
||||
{{ typography::heading(title="Test1") }}
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Aenean at scelerisque purus. Integer eu justo vel mi consectetur sagittis.
|
||||
Proin aliquam, nulla sit amet ornare interdum, leo orci posuere nulla, convallis consequat nibh ex id felis.
|
||||
Nam dapibus enim mauris, bibendum rutrum orci blandit at.
|
||||
Curabitur sed dui sit amet felis consequat cursus quis at magna.
|
||||
Suspendisse at neque lobortis, pulvinar quam at, posuere lorem.
|
||||
Etiam sed elit vehicula leo tincidunt rhoncus et ut metus.
|
||||
Quisque egestas diam non sem pellentesque bibendum.
|
||||
Fusce posuere pretium nisl sit amet pharetra.
|
||||
Maecenas urna odio, dignissim non efficitur vel, varius eu nulla.
|
||||
Sed convallis id lorem sed suscipit. In hac habitasse platea dictumst.
|
||||
Nulla fermentum arcu urna, vitae gravida leo accumsan ut. Phasellus accumsan hendrerit erat,
|
||||
nec imperdiet odio ullamcorper sed.
|
||||
</p>
|
||||
|
||||
<img src="https://placehold.co/600x400">
|
||||
<i class="caption">Centered image</i>
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Aenean at scelerisque purus. Integer eu justo vel mi consectetur sagittis.
|
||||
Proin aliquam, nulla sit amet ornare interdum, leo orci posuere nulla, convallis consequat nibh ex id felis.
|
||||
Nam dapibus enim mauris, bibendum rutrum orci blandit at.
|
||||
Curabitur sed dui sit amet felis consequat cursus quis at magna.
|
||||
Suspendisse at neque lobortis, pulvinar quam at, posuere lorem.
|
||||
Etiam sed elit vehicula leo tincidunt rhoncus et ut metus.
|
||||
Quisque egestas diam non sem pellentesque bibendum.
|
||||
Fusce posuere pretium nisl sit amet pharetra.
|
||||
Maecenas urna odio, dignissim non efficitur vel, varius eu nulla.
|
||||
Sed convallis id lorem sed suscipit. In hac habitasse platea dictumst.
|
||||
Nulla fermentum arcu urna, vitae gravida leo accumsan ut. Phasellus accumsan hendrerit erat,
|
||||
nec imperdiet odio ullamcorper sed.
|
||||
</p>
|
||||
{{ typography::heading(title="Test2") }}
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Aenean at scelerisque purus. Integer eu justo vel mi consectetur sagittis.
|
||||
Proin aliquam, nulla sit amet ornare interdum, leo orci posuere nulla, convallis consequat nibh ex id felis.
|
||||
Nam dapibus enim mauris, bibendum rutrum orci blandit at.
|
||||
Curabitur sed dui sit amet felis consequat cursus quis at magna.
|
||||
Suspendisse at neque lobortis, pulvinar quam at, posuere lorem.
|
||||
Etiam sed elit vehicula leo tincidunt rhoncus et ut metus.
|
||||
Quisque egestas diam non sem pellentesque bibendum.
|
||||
Fusce posuere pretium nisl sit amet pharetra.
|
||||
Maecenas urna odio, dignissim non efficitur vel, varius eu nulla.
|
||||
Sed convallis id lorem sed suscipit. In hac habitasse platea dictumst.
|
||||
Nulla fermentum arcu urna, vitae gravida leo accumsan ut. Phasellus accumsan hendrerit erat,
|
||||
nec imperdiet odio ullamcorper sed.
|
||||
</p>
|
||||
{{ typography::heading(title="Test3") }}
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Aenean at scelerisque purus. Integer eu justo vel mi consectetur sagittis.
|
||||
Proin aliquam, nulla sit amet ornare interdum, leo orci posuere nulla, convallis consequat nibh ex id felis.
|
||||
Nam dapibus enim mauris, bibendum rutrum orci blandit at.
|
||||
Curabitur sed dui sit amet felis consequat cursus quis at magna.
|
||||
Suspendisse at neque lobortis, pulvinar quam at, posuere lorem.
|
||||
Etiam sed elit vehicula leo tincidunt rhoncus et ut metus.
|
||||
Quisque egestas diam non sem pellentesque bibendum.
|
||||
Fusce posuere pretium nisl sit amet pharetra.
|
||||
Maecenas urna odio, dignissim non efficitur vel, varius eu nulla.
|
||||
Sed convallis id lorem sed suscipit. In hac habitasse platea dictumst.
|
||||
Nulla fermentum arcu urna, vitae gravida leo accumsan ut. Phasellus accumsan hendrerit erat,
|
||||
nec imperdiet odio ullamcorper sed.
|
||||
</p>
|
||||
{{ typography::heading(title="Test4") }}
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Aenean at scelerisque purus. Integer eu justo vel mi consectetur sagittis.
|
||||
Proin aliquam, nulla sit amet ornare interdum, leo orci posuere nulla, convallis consequat nibh ex id felis.
|
||||
Nam dapibus enim mauris, bibendum rutrum orci blandit at.
|
||||
Curabitur sed dui sit amet felis consequat cursus quis at magna.
|
||||
Suspendisse at neque lobortis, pulvinar quam at, posuere lorem.
|
||||
Etiam sed elit vehicula leo tincidunt rhoncus et ut metus.
|
||||
Quisque egestas diam non sem pellentesque bibendum.
|
||||
Fusce posuere pretium nisl sit amet pharetra.
|
||||
Maecenas urna odio, dignissim non efficitur vel, varius eu nulla.
|
||||
Sed convallis id lorem sed suscipit. In hac habitasse platea dictumst.
|
||||
Nulla fermentum arcu urna, vitae gravida leo accumsan ut. Phasellus accumsan hendrerit erat,
|
||||
nec imperdiet odio ullamcorper sed.
|
||||
</p>
|
||||
|
||||
{% endblock page_text %}
|
||||
@@ -1,99 +0,0 @@
|
||||
{% extends 'page' %}
|
||||
{% import 'page' as page %}
|
||||
{% import 'histogram' as histogram %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
{{ histogram::head() }}
|
||||
{% endblock head %}
|
||||
|
||||
{% block metadata %}
|
||||
{{ page::metadata(title="Белые списки", description="Анализ данных из белых списков блокировок", keywords="белые списки, whitelist", url="/kb/whitelist") }}
|
||||
{% endblock metadata %}
|
||||
|
||||
{% block page_text %}
|
||||
<h1>Белые списки доменов</h1>
|
||||
<p class="warning-note">
|
||||
<i data-lucide="triangle-alert" width="32" height="32"></i>
|
||||
<span>
|
||||
Не путать с <a href="https://habr.com/ru/news/1000784/">белыми списками <i>мобильного</i> интернета</a>!
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Российские операторы связи начали применять новый тип блокировок CDN,
|
||||
при котором загрузка контента обрывается после передачи примерно 16–20 килобайт данных,
|
||||
из‑за чего большинство сайтов на Cloudflare и других зарубежных платформах становятся
|
||||
практически неработоспособными для пользователей в России.
|
||||
Одновременно сохраняются и развиваются белые списки с популярными и «социально значимыми» доменами,
|
||||
которые старательно выводятся из‑под подобных ограничений.
|
||||
</p>
|
||||
<p>
|
||||
Суть схемы в том, что соединение технически устанавливается, первые килобайты HTML,
|
||||
CSS или скриптов передаются, но затем трафик режется или соединение принудительно сбрасывается на уровне
|
||||
российских операторов, обычно после примерно 16 килобайт (10–14 пакетов в зависимости от протокола).
|
||||
Этого объёма достаточно, чтобы страница начала загружаться и создавалась иллюзия доступности,
|
||||
но ключевые части сайта (основной HTML, JS‑бандлы, стили, API‑запросы) не проходят, и сервис фактически
|
||||
перестаёт работать.
|
||||
</p>
|
||||
<pre class="code">$ curl -k https://cheburcheck.ru/100MB.bin -o/dev/null -r 0-65536 --resolve cheburcheck.ru:443:5.78.7.195 --max-time 5
|
||||
% Total % Received % Xferd Average Speed Time Time Time Current
|
||||
Dload Upload Total Spent Left Speed
|
||||
24 65537 24 16101 0 0 3220 0 0:00:20 0:00:04 0:00:16 0
|
||||
curl: (28) Operation timed out after 5000 milliseconds with 16101 out of 65537 bytes received</pre>
|
||||
<p>
|
||||
Определение домена и выбор, «разрешить или задушить» запрос, в текущей схеме делается на связке DPI‑фильтров.
|
||||
По умолчанию, все HTTP(s) запросы на адреса из подсетей хостеров блокируются. Однако, запросы
|
||||
к некоторым доменам из тех же подсетей проходят нормально — они обнаруживаются по открытому расширению
|
||||
TLS SNI, либо по заголовку Host (в случае с plain http).
|
||||
</p>
|
||||
<p>
|
||||
Проверить нахождение домена в белом списке можно с помощью простого HTTPS-запроса с подменой SNI на адрес из
|
||||
заблокированных диапазонов:
|
||||
</p>
|
||||
<pre class="code">$ curl -k https://ok.ru/100MB.bin -o/dev/null -r 0-65536 --resolve ok.ru:443:5.78.7.195
|
||||
% Total % Received % Xferd Average Speed Time Time Time Current
|
||||
Dload Upload Total Spent Left Speed
|
||||
100 65537 100 65537 0 0 55226 0 0:00:01 0:00:01 --:--:-- 55258</pre>
|
||||
<p>
|
||||
Также, стоит отметить, что домены белого списка добавляются с маской по поддоменам:
|
||||
</p>
|
||||
<pre class="code">$ curl -k https://cheburcheck.ok.ru/100MB.bin -o/dev/null -r 0-65536 --resolve cheburcheck.ok.ru:443:
|
||||
5.78.7.195
|
||||
% Total % Received % Xferd Average Speed Time Time Time Current
|
||||
Dload Upload Total Spent Left Speed
|
||||
100 65537 100 65537 0 0 53192 0 0:00:01 0:00:01 --:--:-- 53195</pre>
|
||||
|
||||
{{ typography::heading(title="Автоматическое сканирование") }}
|
||||
<p>
|
||||
Используя методы, указанные выше, мы разработали инструмент для автоматического сканирования и анализа
|
||||
блокировок
|
||||
<b><a href="https://github.com/LowderPlay/cheburcheck/tree/master/reporter">Cheburcheck Reporter</a></b>.
|
||||
Он позволяет сканировать большое количество доменов
|
||||
за короткий срок за счет параллелизации запросов.
|
||||
</p>
|
||||
<p>
|
||||
Исходя из анализа 1,000,000 доменов из рейтинга
|
||||
<a href="https://tranco-list.eu/list/2NPQ9">Tranco list от 26 ноября 2025</a>,
|
||||
в белом списке содержится около 1000 доменов.
|
||||
</p>
|
||||
<i>
|
||||
* — Мы не включаем в это число домены из зоны .co.uk, так как по какой-то причине, они все находятся в белом списке.
|
||||
</i>
|
||||
|
||||
<i class="caption">Гистограмма количества доменов относительно их положения в рейтинге (топ-30k, без .co.uk)</i>
|
||||
{{ histogram::histogram(endpoint="/whitelist/histogram?filter&limit=30000") }}
|
||||
<br>
|
||||
<i class="caption">Гистограмма количества доменов относительно их положения в рейтинге (топ-1kk, включая .co.uk)</i>
|
||||
{{ histogram::histogram(id="filtered", endpoint="/whitelist/histogram?limit=1000000") }}
|
||||
|
||||
{{ typography::heading(title="Скачать списки") }}
|
||||
<p>
|
||||
Мы публикуем результаты наших сканирований в виде CSV-файлов:
|
||||
</p>
|
||||
<div>
|
||||
<i data-lucide="file-symlink" width="16" height="16"></i> <a href="/whitelist/full.csv">Полный список (CSV)</a>
|
||||
</div>
|
||||
<div>
|
||||
<i data-lucide="file-symlink" width="16" height="16"></i> <a href="/whitelist/domains.csv">Только домены (CSV)</a>
|
||||
</div>
|
||||
{% endblock page_text %}
|
||||
@@ -1,315 +0,0 @@
|
||||
{% extends 'base' %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'search-form' %}
|
||||
|
||||
{% set warning = whitelist and not domain %}
|
||||
|
||||
<div class="result-panel {% if warning %}whitelist-theme{% elif found %}blocked-theme{% else %}clean-theme{% endif %}">
|
||||
<div class="result-header">
|
||||
<div class="icon-box status-icon">
|
||||
{% if whitelist %}
|
||||
<i data-lucide="shield-alert" width="32" height="32"></i>
|
||||
{% elif blocked %}
|
||||
<i data-lucide="shield-x" width="32" height="32"></i>
|
||||
{% else %}
|
||||
<i data-lucide="shield-check" width="32" height="32"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if warning %}
|
||||
<div>
|
||||
<h2>Белый список</h2>
|
||||
<p class="subheading text-sm">
|
||||
Ресурс находится в белом списке
|
||||
</p>
|
||||
</div>
|
||||
{% elif 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>
|
||||
|
||||
{% if asn_info %}
|
||||
<div class="asn-summary">
|
||||
<div class="asn-stat">
|
||||
<span class="asn-stat-label">Всего подсетей:</span>
|
||||
<span class="asn-stat-value">{{ asn_info.prefixes | length }}</span>
|
||||
</div>
|
||||
{% if asn_info.blocked_prefixes | length > 0 %}
|
||||
<div class="asn-stat">
|
||||
<span class="asn-stat-label">Заблокировано:</span>
|
||||
<span class="asn-stat-value alert">{{ asn_info.blocked_prefixes | length }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="details-grid">
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">Сетевые данные</h3>
|
||||
{% if not asn_info %}
|
||||
<div class="detail-row">
|
||||
<span class="row-label">IP-адреса</span>
|
||||
<div class="ip-list-container">
|
||||
{% if ips | length == 1 %}
|
||||
<span class="ip-item">{{ ips | first }}</span>
|
||||
{% if subnet_size %}
|
||||
<span class="ip-count">({{ subnet_size }} всего)</span>
|
||||
{% endif %}
|
||||
{% elif ips | length <= 5 %}
|
||||
{% for ip in ips %}
|
||||
<span class="ip-item">{{ ip }}</span>
|
||||
{% endfor %}
|
||||
{% if subnet_size %}
|
||||
<span class="ip-count">({{ subnet_size }} всего)</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="ip-summary">
|
||||
<span class="ip-range">{{ ips | first }} — {{ ips | last }}</span>
|
||||
{% if subnet_size %}
|
||||
<span class="ip-count">({{ subnet_size }} всего, проверено {{ ips | length }})</span>
|
||||
{% else %}
|
||||
<span class="ip-count">({{ ips | length }} адресов)</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<details class="ip-list-details">
|
||||
<summary class="ip-list-toggle">
|
||||
Показать все
|
||||
</summary>
|
||||
<div class="ip-list-expanded">
|
||||
{% for ip in ips %}
|
||||
<span class="ip-item">{{ ip }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="detail-row">
|
||||
<span class="row-label">Хостинг / ISP</span>
|
||||
<span class="row-value">{% if geo.organisation %}{{ geo.organisation }}{% else %}-{% endif %}</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">
|
||||
{% if geo.asn %}
|
||||
<a href="/check?target={{ geo.asn }}" class="asn-link">{{ geo.asn }}</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if asn_info %}
|
||||
<div class="detail-row">
|
||||
<span class="row-label">Подсети ASN</span>
|
||||
<div class="prefix-list-container">
|
||||
{% set ipv4_list = [] %}
|
||||
{% set ipv6_list = [] %}
|
||||
{% for prefix in asn_info.prefixes %}
|
||||
{% if ":" in prefix %}
|
||||
{% set_global ipv6_list = ipv6_list | concat(with=prefix) %}
|
||||
{% else %}
|
||||
{% set_global ipv4_list = ipv4_list | concat(with=prefix) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% set all_prefixes = ipv4_list | concat(with=ipv6_list) %}
|
||||
{% if all_prefixes | length <= 10 %}
|
||||
<div>
|
||||
{% for prefix in all_prefixes %}
|
||||
<p class="row-value">{{ prefix }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
{% for prefix in all_prefixes | slice(end=10) %}
|
||||
<p class="row-value">{{ prefix }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<details class="prefix-list-details">
|
||||
<summary class="prefix-list-toggle">
|
||||
Показать все ({{ all_prefixes | length }})
|
||||
</summary>
|
||||
<div class="prefix-list-expanded">
|
||||
{% for prefix in all_prefixes %}
|
||||
<p class="row-value">{{ prefix }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h3 class="reason-header">Нахождение в списках</h3>
|
||||
<div class="detail-row">
|
||||
<span class="row-label">CDN</span>
|
||||
{% if providers %}
|
||||
<p class="row-value alert">НАЙДЕН</p>
|
||||
{% else %}
|
||||
<span class="row-value">Не найден</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if providers %}
|
||||
{% for provider, networks in providers %}
|
||||
<div class="detail-row">
|
||||
<span class="row-label">{{ provider }}</span>
|
||||
<div class="cdn-networks-container">
|
||||
{% if networks | length <= 5 %}
|
||||
{% for network in networks %}
|
||||
<p class="row-value">{{ network.cidr }}</p>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div>
|
||||
{% for network in networks | slice(end=5) %}
|
||||
<p class="row-value">{{ network.cidr }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<details class="cdn-networks-details">
|
||||
<summary class="cdn-networks-toggle">
|
||||
Показать все ({{ networks | length }})
|
||||
</summary>
|
||||
<div class="cdn-networks-expanded">
|
||||
{% for network in networks %}
|
||||
<p class="row-value">{{ network.cidr }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if whitelist %}
|
||||
<div class="detail-row">
|
||||
<a href="/kb/whitelist" class="row-label">Белый список (?)</a>
|
||||
<span class="row-value success">
|
||||
НАЙДЕН - <span class="hint"
|
||||
title="Дата последнего сканирования, когда данный домен был найден в белом списке">
|
||||
<script>
|
||||
document.write(new Date("{{ whitelist.last_ok }}").toLocaleDateString());
|
||||
</script>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="detail-row">
|
||||
<span class="row-label">Реестр РКН</span>
|
||||
|
||||
{% if domain %}
|
||||
<span class="row-value alert">ОГРАНИЧЕН</span>
|
||||
{% elif blocked_subnets %}
|
||||
<span class="row-value alert hint"
|
||||
title="Адреса пересекаются с подсетями заблокированных доменов (не гарантирует блокировку)">
|
||||
IP-АДРЕСА
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="row-value">Не найден</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if domain %}
|
||||
<div class="detail-row">
|
||||
<span class="row-label">Заблокированный домен</span>
|
||||
<span class="row-value">{{ domain }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if blocked_subnets and not asn_info %}
|
||||
<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 %}
|
||||
|
||||
{% if asn_info and asn_info.blocked_prefixes | length > 0 %}
|
||||
<div class="detail-row">
|
||||
<span class="row-label">Заблокированные подсети ASN</span>
|
||||
<div class="blocked-prefix-list-container">
|
||||
{% if asn_info.blocked_prefixes | length <= 10 %}
|
||||
<div>
|
||||
{% for prefix in asn_info.blocked_prefixes %}
|
||||
<p class="row-value alert">{{ prefix }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
{% for prefix in asn_info.blocked_prefixes | slice(end=10) %}
|
||||
<p class="row-value alert">{{ prefix }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<details class="blocked-prefix-list-details">
|
||||
<summary class="blocked-prefix-list-toggle">
|
||||
Показать все ({{ asn_info.blocked_prefixes | length }})
|
||||
</summary>
|
||||
<div class="blocked-prefix-list-expanded">
|
||||
{% for prefix in asn_info.blocked_prefixes %}
|
||||
<p class="row-value alert">{{ prefix }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-feedback-section">
|
||||
<p class="feedback-prompt">У вас работает этот ресурс?</p>
|
||||
<div class="feedback-buttons">
|
||||
<button class="feedback-btn feedback-works" onclick="sendFeedback(true)">
|
||||
<i data-lucide="thumbs-up" width="16" height="16"></i>
|
||||
Работает
|
||||
</button>
|
||||
<button class="feedback-btn feedback-not-works" onclick="sendFeedback(false)">
|
||||
<i data-lucide="thumbs-down" width="16" height="16"></i>
|
||||
Не работает
|
||||
</button>
|
||||
</div>
|
||||
<div class="feedback-status hidden">
|
||||
<i data-lucide="thumbs-up" width="16" height="16"></i>
|
||||
<span>Спасибо за ваш отзыв!</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function sendFeedback(works) {
|
||||
document.querySelector('.feedback-buttons').classList.add('hidden');
|
||||
document.querySelector('.feedback-prompt').classList.add('hidden');
|
||||
document.querySelector('.feedback-status').classList.remove('hidden');
|
||||
|
||||
fetch(`/feedback/{{ id }}/${works}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
@@ -1,18 +0,0 @@
|
||||
<form class="search-form" action="/check">
|
||||
<input
|
||||
type="text"
|
||||
name="target"
|
||||
placeholder="example.com, 1.1.1.1, 1.0.0.0/24, AS13335"
|
||||
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>
|
||||
@@ -1,5 +0,0 @@
|
||||
{% macro heading(title) %}
|
||||
<h2 id="{{ title | slugify }}">
|
||||
<a href="#{{ title | slugify }}">{{ title }}</a>
|
||||
</h2>
|
||||
{% endmacro title %}
|
||||
Reference in New Issue
Block a user