#[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::::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) -> 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, } #[get("/?")] async fn list_services( conn: DbConn, tera: &State, ipdb: &State, al: &AcceptLanguage, software: Option>, ) -> RawHtml { 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 = 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, al: &AcceptLanguage) -> RawHtml { 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, al: &AcceptLanguage) -> RawHtml { 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 = "")] async fn add_service_post( conn: DbConn, submission: Form>>, tera: &State, al: &AcceptLanguage, ) -> RawHtml { 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, tera: &State, al: &AcceptLanguage) -> RawHtml { RawHtml( tera.render( "about.html.tera", &gen_context( al, json!({ "source_code": config.source_code, }), ), ) .unwrap(), ) }