services/src/main.rs

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(),
)
}