From 0ca420d1ff87750c74caa9f1b2ab2bfa82999806 Mon Sep 17 00:00:00 2001 From: realaravinth Date: Thu, 19 May 2022 17:35:22 +0530 Subject: [PATCH] feat: impl get and submit DNS challenge --- src/main.rs | 2 +- src/pages/auth/add.rs | 208 ++++++++++++++++++++++++++++++++++ src/pages/auth/mod.rs | 33 ++++++ src/pages/mod.rs | 4 +- src/verify.rs | 32 ++---- templates/pages/auth/add.html | 18 +++ 6 files changed, 274 insertions(+), 23 deletions(-) create mode 100644 src/pages/auth/add.rs create mode 100644 src/pages/auth/mod.rs create mode 100644 templates/pages/auth/add.html diff --git a/src/main.rs b/src/main.rs index 950cb1d..18716bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,7 +64,7 @@ async fn main() { pretty_env_logger::init(); lazy_static::initialize(&pages::TEMPLATES); - let ctx = WebCtx::new( Ctx::new(settings.clone()).await); + let ctx = WebCtx::new(Ctx::new(settings.clone()).await); let db = WebDB::new(sqlite::get_data(Some(settings.clone())).await); let federate = WebFederate::new(get_federate(Some(settings.clone())).await); diff --git a/src/pages/auth/add.rs b/src/pages/auth/add.rs new file mode 100644 index 0000000..173b276 --- /dev/null +++ b/src/pages/auth/add.rs @@ -0,0 +1,208 @@ +/* + * ForgeFlux StarChart - A federated software forge spider + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_web::http::{self, header::ContentType}; +use actix_web::{HttpResponse, Responder}; +use actix_web_codegen_const_routes::{get, post}; +use log::info; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use tera::Context; +use url::Url; + +use db_core::prelude::*; + +use crate::errors::ServiceResult; +use crate::pages::errors::*; +use crate::settings::Settings; +use crate::verify::TXTChallenge; +use crate::*; + +pub use crate::pages::*; + +pub const TITLE: &str = "Setup spidering"; +pub const AUTH_ADD: TemplateFile = TemplateFile::new("auth_add", "pages/auth/add.html"); + +pub struct AddChallenge { + ctx: RefCell, +} + +impl CtxError for AddChallenge { + fn with_error(&self, e: &ReadableError) -> String { + self.ctx.borrow_mut().insert(ERROR_KEY, e); + self.render() + } +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct AddChallengePayload { + pub hostname: String, +} + +impl AddChallenge { + fn new(settings: &Settings, payload: Option<&AddChallengePayload>) -> Self { + let ctx = RefCell::new(ctx(settings)); + ctx.borrow_mut().insert(TITLE_KEY, TITLE); + if let Some(payload) = payload { + ctx.borrow_mut().insert(PAYLOAD_KEY, payload); + } + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES.render(AUTH_ADD.name, &self.ctx.borrow()).unwrap() + } + + pub fn page(s: &Settings) -> String { + let p = Self::new(s, None); + p.render() + } +} + +#[get(path = "PAGES.auth.add")] +pub async fn get_add(ctx: WebCtx) -> impl Responder { + let login = AddChallenge::page(&ctx.settings); + let html = ContentType::html(); + HttpResponse::Ok().content_type(html).body(login) +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(get_add); + cfg.service(add_submit); +} + +#[post(path = "PAGES.auth.add")] +pub async fn add_submit( + payload: web::Form, + ctx: WebCtx, + db: WebDB, +) -> PageResult { + async fn _add_submit( + payload: &AddChallengePayload, + ctx: &ArcCtx, + db: &BoxDB, + ) -> ServiceResult { + let url_hostname = Url::parse(&payload.hostname).unwrap(); + let hostname = get_hostname(&url_hostname); + let key = TXTChallenge::get_challenge_txt_key(&ctx, &hostname); + if db.dns_challenge_exists(&key).await? { + let value = db.get_dns_challenge_solution(&key).await?; + Ok(TXTChallenge { key, value }) + } else { + let challenge = TXTChallenge::new(ctx, &hostname); + db.create_dns_challenge(&challenge.key, &challenge.value) + .await?; + Ok(challenge) + } + } + + _add_submit(&payload, &ctx, &db) + .await + .map_err(|e| PageError::new(AddChallenge::new(&ctx.settings, Some(&payload)), e))?; + + Ok(HttpResponse::Found() + .insert_header((http::header::LOCATION, PAGES.auth.add)) + .finish()) +} + +#[cfg(test)] +mod tests { + use actix_web::http::StatusCode; + use actix_web::test; + use url::Url; + + use super::AddChallenge; + use super::AddChallengePayload; + use super::TXTChallenge; + use crate::errors::*; + use crate::pages::errors::*; + use crate::settings::Settings; + + use db_core::prelude::*; + + #[cfg(test)] + mod isolated { + use crate::errors::ServiceError; + use crate::pages::auth::add::{AddChallenge, AddChallengePayload, ReadableError}; + use crate::pages::errors::*; + use crate::settings::Settings; + + #[test] + fn add_page_works() { + let settings = Settings::new().unwrap(); + AddChallenge::page(&settings); + let payload = AddChallengePayload { + hostname: "https://example.com".into(), + }; + let page = AddChallenge::new(&settings, Some(&payload)); + page.with_error(&ReadableError::new(&ServiceError::ClosedForRegistration)); + page.render(); + } + } + + #[actix_rt::test] + async fn add_routes_work() { + use crate::tests::*; + use crate::*; + const BASE_DOMAIN: &str = "add_routes_work.example.org"; + + let (db, ctx, federate, _tmpdir) = sqlx_sqlite::get_ctx().await; + let app = get_app!(ctx, db, federate).await; + + let payload = AddChallengePayload { + hostname: format!("https://{BASE_DOMAIN}"), + }; + + println!("{}", payload.hostname); + + let hostname = get_hostname(&Url::parse(&payload.hostname).unwrap()); + let key = TXTChallenge::get_challenge_txt_key(&ctx, &hostname); + + db.delete_dns_challenge(&key).await.unwrap(); + assert!(!db.dns_challenge_exists(&key).await.unwrap()); + + let resp = test::call_service( + &app, + post_request!(&payload, PAGES.auth.add, FORM).to_request(), + ) + .await; + if resp.status() != StatusCode::FOUND { + let resp_err: ErrorToResponse = test::read_body_json(resp).await; + panic!("{}", resp_err.error); + } + assert_eq!(resp.status(), StatusCode::FOUND); + + assert!(db.dns_challenge_exists(&key).await.unwrap()); + + let challenge = db.get_dns_challenge_solution(&key).await.unwrap(); + + // replay config + let resp = test::call_service( + &app, + post_request!(&payload, PAGES.auth.add, FORM).to_request(), + ) + .await; + + assert_eq!(resp.status(), StatusCode::FOUND); + + assert!(db.dns_challenge_exists(&key).await.unwrap()); + assert_eq!( + challenge, + db.get_dns_challenge_solution(&key).await.unwrap() + ); + } +} diff --git a/src/pages/auth/mod.rs b/src/pages/auth/mod.rs new file mode 100644 index 0000000..f0ea025 --- /dev/null +++ b/src/pages/auth/mod.rs @@ -0,0 +1,33 @@ +/* + * ForgeFlux StarChart - A federated software forge spider + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +pub mod add; +pub use add::AUTH_ADD; + +pub use super::{ctx, TemplateFile, ERROR_KEY, PAGES, PAYLOAD_KEY, TITLE_KEY}; + +pub const AUTH_CHALLENGE: TemplateFile = + TemplateFile::new("auth_challenge", "pages/auth/challenge.html"); + +pub fn register_templates(t: &mut tera::Tera) { + AUTH_ADD.register(t).expect(AUTH_ADD.name); + AUTH_CHALLENGE.register(t).expect(AUTH_ADD.name); +} + +pub fn services(cfg: &mut actix_web::web::ServiceConfig) { + add::services(cfg); +} diff --git a/src/pages/mod.rs b/src/pages/mod.rs index f550516..f87e36f 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -135,7 +135,9 @@ mod tests { let mut tera = Tera::default(); let mut tera2 = Tera::default(); for t in [ - BASE, FOOTER, PUB_NAV, + BASE, + FOOTER, + PUB_NAV, auth::AUTH_CHALLENGE, auth::AUTH_ADD, // auth::AUTH_BASE, diff --git a/src/verify.rs b/src/verify.rs index d5838b9..21a9a60 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -15,19 +15,19 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +use serde::{Deserialize, Serialize}; use trust_dns_resolver::{ config::{ResolverConfig, ResolverOpts}, AsyncResolver, }; -use lazy_static::lazy_static; use crate::utils::get_random; use crate::ArcCtx; +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct TXTChallenge { - key: String, - _base_hostname: String, - value: String, + pub key: String, + pub value: String, } const KEY_LEN: usize = 30; @@ -43,16 +43,10 @@ impl TXTChallenge { format!("{}.{}", Self::get_challenge_txt_key_prefix(ctx), hostname) } - - - pub async fn new(ctx: &ArcCtx, hostname: &str) -> Self { + pub fn new(ctx: &ArcCtx, hostname: &str) -> Self { let key = Self::get_challenge_txt_key(ctx, hostname); let value = get_random(VALUES_LEN); - Self { - key, - value, - _base_hostname: hostname.to_string(), - } + Self { key, value } } pub async fn verify_txt(&self) -> Result> { @@ -65,28 +59,24 @@ impl TXTChallenge { } #[cfg(test)] -mod tests { +pub mod tests { use super::*; use crate::tests::sqlx_sqlite; + pub const BASE_DOMAIN: &str = "forge.forgeflux.org"; + pub const VALUE: &str = "ifthisvalueisretrievedbyforgefluxstarchartthenthetestshouldpass"; #[actix_rt::test] async fn verify_txt_works() { // please note that this DNS record is in prod - const BASE_DOMAIN: &str = "forge.forgeflux.org"; - const VALUE: &str = "ifthisvalueisretrievedbyforgefluxstarchartthenthetestshouldpass"; let (_db, ctx, _federate, _tmp_dir) = sqlx_sqlite::get_ctx().await; - let key = TXTChallenge::get_challenge_txt_key(&ctx, BASE_DOMAIN); + let key = TXTChallenge::get_challenge_txt_key(&ctx, BASE_DOMAIN); let mut txt_challenge = TXTChallenge { value: VALUE.to_string(), - _base_hostname: BASE_DOMAIN.to_string(), key: key.clone(), }; - assert_eq!( - TXTChallenge::get_challenge_txt_key(&ctx, BASE_DOMAIN), - key, - ); + assert_eq!(TXTChallenge::get_challenge_txt_key(&ctx, BASE_DOMAIN), key,); assert!( txt_challenge.verify_txt().await.unwrap(), diff --git a/templates/pages/auth/add.html b/templates/pages/auth/add.html new file mode 100644 index 0000000..bfa8b37 --- /dev/null +++ b/templates/pages/auth/add.html @@ -0,0 +1,18 @@ +{% extends 'base' %} +{% block title %} {{ title }} {% endblock %} +{% block nav %} {% include "pub_nav" %} {% endblock %} + +{% block main %} +
+

Add forge instance for spidering

+

Please not that only forge administratior or parties with access the forge's DNS server can register for spidering

+
+ {% include "error_comp" %} + + + + +
+
+{% endblock %}