324 lines
7.2 KiB
Rust
324 lines
7.2 KiB
Rust
#[macro_use]
|
|
extern crate rocket;
|
|
|
|
mod check;
|
|
use crate::check::check;
|
|
use services::{get_config, models::*, Config, Software};
|
|
|
|
use std::fs::File;
|
|
|
|
use asn_db2::Database;
|
|
use diesel::prelude::*;
|
|
use fluent_templates::FluentLoader;
|
|
use rocket::{
|
|
fairing::{Fairing, Info, Kind},
|
|
figment::{util::map, value::Value},
|
|
form::{self, Error, Form, Strict},
|
|
http::Header,
|
|
response::content::RawHtml,
|
|
shield::{Referrer, Shield},
|
|
Request, Response, State,
|
|
};
|
|
use rocket_accept_language::{language, AcceptLanguage, LanguageIdentifier};
|
|
use rocket_sync_db_pools::{database, diesel};
|
|
use serde::Serialize;
|
|
use serde_json::json;
|
|
use std::io::BufReader;
|
|
use tera::{Context, Tera};
|
|
use unic_langid::{langid, subtags::Language};
|
|
|
|
const LANGUAGE_DEFAULT: Language = language!("en");
|
|
|
|
const SL: [LanguageIdentifier; 2] = [langid!("en"), langid!("fr")];
|
|
|
|
fluent_templates::static_loader! {
|
|
static LOCALES = {
|
|
locales: "./locales",
|
|
fallback_language: "en",
|
|
customise: |bundle| bundle.set_use_isolating(false),
|
|
};
|
|
}
|
|
|
|
fn gen_context(accept_language: &AcceptLanguage, values: tera::Value) -> Context {
|
|
let mut cont: Context = Context::from_value(json!({
|
|
"ln": &accept_language
|
|
.get_appropriate_language_region(&SL)
|
|
.unwrap_or((LANGUAGE_DEFAULT, None)).0.as_str()
|
|
}))
|
|
.unwrap();
|
|
cont.extend(Context::from_value(values).unwrap());
|
|
cont
|
|
}
|
|
|
|
pub struct HttpHeaders;
|
|
#[rocket::async_trait]
|
|
impl Fairing for HttpHeaders {
|
|
fn info(&self) -> Info {
|
|
Info {
|
|
name: "HTTP Headers",
|
|
kind: Kind::Response,
|
|
}
|
|
}
|
|
async fn on_response<'r>(&self, _: &'r Request<'_>, response: &mut Response<'r>) {
|
|
response.set_header(Header::new(
|
|
"content-security-policy",
|
|
"default-src 'none'; form-action 'self'; base-uri 'none';",
|
|
));
|
|
response.remove_header("server");
|
|
}
|
|
}
|
|
|
|
#[database("main_db")]
|
|
struct DbConn(diesel::SqliteConnection);
|
|
|
|
#[launch]
|
|
fn rocket() -> _ {
|
|
let config = get_config();
|
|
|
|
let mut tera = Tera::new("templates/*.html.tera").unwrap();
|
|
tera.register_function("fluent", FluentLoader::new(&*LOCALES));
|
|
|
|
rocket::custom(rocket::Config::figment().merge((
|
|
"databases",
|
|
map!["main_db" => map!{
|
|
"url" => Into::<Value>::into(config.database.clone()),
|
|
}],
|
|
)))
|
|
.manage(
|
|
Database::from_reader(BufReader::new(
|
|
File::open(&config.ip_to_asn).expect("unable to open ip2asn TSV file"),
|
|
))
|
|
.unwrap(),
|
|
)
|
|
.manage(config)
|
|
.manage(tera)
|
|
.attach(DbConn::fairing())
|
|
.attach(Shield::new().enable(Referrer::NoReferrer))
|
|
.attach(HttpHeaders)
|
|
.mount(
|
|
"/",
|
|
routes![
|
|
list_services,
|
|
list_scans,
|
|
add_service_get,
|
|
add_service_post,
|
|
dl,
|
|
about,
|
|
],
|
|
)
|
|
}
|
|
|
|
#[get("/services.db")]
|
|
fn dl(config: &State<Config>) -> File {
|
|
File::open(&config.database).unwrap()
|
|
}
|
|
|
|
#[derive(Serialize, Debug)]
|
|
struct IpInfo {
|
|
ip: String,
|
|
subnet: String,
|
|
asn: String,
|
|
country: String,
|
|
as_owner: String,
|
|
}
|
|
#[derive(Serialize, Debug)]
|
|
struct TemplateServices {
|
|
url: String,
|
|
software: String,
|
|
server: String,
|
|
ipv6: String,
|
|
ipv4: String,
|
|
availability_ipv6: String,
|
|
availability_ipv4: String,
|
|
ip_info: Vec<IpInfo>,
|
|
}
|
|
#[get("/?<software>")]
|
|
async fn list_services(
|
|
conn: DbConn,
|
|
tera: &State<Tera>,
|
|
ipdb: &State<Database>,
|
|
al: &AcceptLanguage,
|
|
software: Option<Strict<Software>>,
|
|
) -> RawHtml<String> {
|
|
let services = conn
|
|
.run(|c| {
|
|
let mut request = services::schema::services::dsl::services.into_boxed();
|
|
if let Some(s) = software {
|
|
request = request
|
|
.filter(services::schema::services::software.eq(s.to_string().to_lowercase()));
|
|
}
|
|
request
|
|
.limit(300)
|
|
.select(Services::as_select())
|
|
.load(c)
|
|
.unwrap()
|
|
})
|
|
.await;
|
|
|
|
let mut templates: Vec<TemplateServices> = vec![];
|
|
for service in &services {
|
|
let mut ip_info = vec![];
|
|
let mut ip_combined: Vec<&str> = service.address_ipv6.split(',').collect();
|
|
ip_combined.append(&mut service.address_ipv4.split(',').collect());
|
|
ip_combined
|
|
.iter()
|
|
.filter(|ip| !ip.is_empty())
|
|
.for_each(|ip| {
|
|
match ipdb.lookup(ip.parse().unwrap()).unwrap() {
|
|
asn_db2::IpEntry::V6(info) => ip_info.push(IpInfo {
|
|
ip: ip.to_string(),
|
|
subnet: info.subnet.to_string(),
|
|
asn: info.as_number.to_string(),
|
|
country: info.country.to_string(),
|
|
as_owner: info.owner.to_string(),
|
|
}),
|
|
asn_db2::IpEntry::V4(info) => ip_info.push(IpInfo {
|
|
ip: ip.to_string(),
|
|
subnet: info.subnet.to_string(),
|
|
asn: info.as_number.to_string(),
|
|
country: info.country.to_string(),
|
|
as_owner: info.owner.to_string(),
|
|
}),
|
|
};
|
|
});
|
|
|
|
templates.push(TemplateServices {
|
|
url: service.url.to_string(),
|
|
software: service.software.to_string(),
|
|
server: service.server.to_string(),
|
|
ipv6: service.ipv6.to_string(),
|
|
ipv4: service.ipv4.to_string(),
|
|
availability_ipv6: service.availability_ipv6.to_string(),
|
|
availability_ipv4: service.availability_ipv4.to_string(),
|
|
ip_info,
|
|
})
|
|
}
|
|
|
|
RawHtml(
|
|
tera.render(
|
|
"list-services.html.tera",
|
|
&gen_context(
|
|
al,
|
|
json!({
|
|
"services": &templates,
|
|
}),
|
|
),
|
|
)
|
|
.unwrap(),
|
|
)
|
|
}
|
|
|
|
#[get("/list-scans")]
|
|
async fn list_scans(conn: DbConn, tera: &State<Tera>, al: &AcceptLanguage) -> RawHtml<String> {
|
|
RawHtml(
|
|
tera.render(
|
|
"list-scans.html.tera",
|
|
&gen_context(
|
|
al,
|
|
json!({
|
|
"scans": conn.run(|c| services::schema::scans::dsl::scans
|
|
.limit(1000)
|
|
.select(Scans::as_select())
|
|
.load(c)
|
|
.unwrap()).await
|
|
}),
|
|
),
|
|
)
|
|
.unwrap(),
|
|
)
|
|
}
|
|
|
|
#[get("/add-service")]
|
|
fn add_service_get(tera: &State<Tera>, al: &AcceptLanguage) -> RawHtml<String> {
|
|
RawHtml(
|
|
tera.render("add-service.html.tera", &gen_context(al, json!({})))
|
|
.unwrap(),
|
|
)
|
|
}
|
|
|
|
fn check_url<'v>(url: &str) -> form::Result<'v, ()> {
|
|
match reqwest::Url::parse(url) {
|
|
Ok(url) => match (
|
|
url.scheme(),
|
|
url.username(),
|
|
url.password(),
|
|
url.query(),
|
|
url.fragment(),
|
|
url.domain(),
|
|
) {
|
|
("https", "", None, None, None, Some(_)) => Ok(()),
|
|
_ => Err(Error::validation("URL format forbidden").into()),
|
|
},
|
|
_ => Err(Error::validation("Invalid URL"))?,
|
|
}
|
|
}
|
|
|
|
#[derive(FromForm)]
|
|
struct Submission<'r> {
|
|
#[field(validate = check_url())]
|
|
url: &'r str,
|
|
}
|
|
|
|
#[post("/add-service", data = "<submission>")]
|
|
async fn add_service_post(
|
|
conn: DbConn,
|
|
submission: Form<Strict<Submission<'_>>>,
|
|
tera: &State<Tera>,
|
|
al: &AcceptLanguage,
|
|
) -> RawHtml<String> {
|
|
use ::services::schema::services::dsl::*;
|
|
use diesel::associations::HasTable;
|
|
|
|
let service = match check(submission.url, None).await {
|
|
Ok(service) => service,
|
|
Err(err) => {
|
|
return RawHtml(
|
|
tera.render(
|
|
"error.html.tera",
|
|
&gen_context(al, json!({"error_message": &err})),
|
|
)
|
|
.unwrap(),
|
|
);
|
|
}
|
|
};
|
|
|
|
conn.run(|c| {
|
|
diesel::insert_into(services::table())
|
|
.values(Services {
|
|
url: service.url,
|
|
software: service.software,
|
|
server: service.server,
|
|
ipv6: "".to_string(),
|
|
ipv4: "".to_string(),
|
|
availability_ipv6: "".to_string(),
|
|
availability_ipv4: "".to_string(),
|
|
address_ipv6: "".to_string(),
|
|
address_ipv4: "".to_string(),
|
|
})
|
|
.execute(c)
|
|
.unwrap()
|
|
})
|
|
.await;
|
|
|
|
RawHtml(
|
|
tera.render("add-service.html.tera", &gen_context(al, json!({})))
|
|
.unwrap(),
|
|
)
|
|
}
|
|
|
|
#[get("/about")]
|
|
fn about(config: &State<Config>, tera: &State<Tera>, al: &AcceptLanguage) -> RawHtml<String> {
|
|
RawHtml(
|
|
tera.render(
|
|
"about.html.tera",
|
|
&gen_context(
|
|
al,
|
|
json!({
|
|
"source_code": config.source_code,
|
|
}),
|
|
),
|
|
)
|
|
.unwrap(),
|
|
)
|
|
}
|