diff --git a/Cargo.lock b/Cargo.lock index acb5e29..ed86a6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2679,6 +2679,7 @@ dependencies = [ "sqlx", "tokio", "url", + "urlencoding", "uuid", "validator", ] @@ -2963,6 +2964,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" + [[package]] name = "utf-8" version = "0.7.6" diff --git a/Cargo.toml b/Cargo.toml index 2d80bc3..5f8f3bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ log = "0.4" lazy_static = "1.4" url = "2.2" +urlencoding = "2.1.0" rand = "0.8" uuid = { version="0.8.2", features = ["v4"]} diff --git a/src/api/v1/admin/auth.rs b/src/api/v1/admin/auth.rs index ebbbf4c..0a35821 100644 --- a/src/api/v1/admin/auth.rs +++ b/src/api/v1/admin/auth.rs @@ -25,12 +25,29 @@ use crate::errors::*; use crate::AppData; pub mod routes { + use crate::middleware::auth::GetLoginRoute; + use url::Url; pub struct Auth { pub logout: &'static str, pub login: &'static str, pub register: &'static str, } + impl GetLoginRoute for Auth { + fn get_login_route(&self, src: Option<&str>) -> String { + if let Some(redirect_to) = src { + let mut url = Url::parse("http://x/").unwrap(); + url.set_path(self.login); + url.query_pairs_mut() + .append_pair("redirect_to", redirect_to); + let path = format!("{}/?{}", url.path(), url.query().unwrap()); + path + } else { + self.login.to_string() + } + } + } + impl Auth { pub const fn new() -> Auth { let login = "/api/v1/admin/signin"; diff --git a/src/api/v1/admin/campaigns.rs b/src/api/v1/admin/campaigns.rs index 8d9c9c3..6a3da44 100644 --- a/src/api/v1/admin/campaigns.rs +++ b/src/api/v1/admin/campaigns.rs @@ -51,7 +51,7 @@ pub mod runners { let mut uuid; let now = OffsetDateTime::now_utc(); - payload.difficulties.sort(); + payload.difficulties.sort_unstable(); loop { uuid = get_uuid(); @@ -121,11 +121,45 @@ async fn add( #[cfg(test)] mod tests { - use crate::api::v1::bench::{Bench, Submission}; + use crate::api::v1::bench::Submission; use crate::data::Data; + use crate::middleware::auth::GetLoginRoute; use crate::tests::*; use crate::*; + use actix_web::{http::header, test}; + + #[actix_rt::test] + async fn test_bench_register_works() { + let data = Data::new().await; + let app = get_app!(data).await; + let signin_resp = test::call_service( + &app, + test::TestRequest::get() + .uri(V1_API_ROUTES.benches.register) + .to_request(), + ) + .await; + + assert_eq!(signin_resp.status(), StatusCode::OK); + + let redirect_to = Some("foo"); + + let signin_resp = test::call_service( + &app, + test::TestRequest::get() + .uri(&V1_API_ROUTES.benches.get_login_route(redirect_to)) + .to_request(), + ) + .await; + assert_eq!(signin_resp.status(), StatusCode::FOUND); + let headers = signin_resp.headers(); + assert_eq!( + headers.get(header::LOCATION).unwrap(), + redirect_to.as_ref().unwrap() + ) + } + #[actix_rt::test] async fn test_add_campaign() { const NAME: &str = "testadminuser"; @@ -136,29 +170,6 @@ mod tests { const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2"; const THREADS: i32 = 4; - let benches = vec![ - Bench { - difficulty: 1, - duration: 1.00, - }, - Bench { - difficulty: 2, - duration: 2.00, - }, - Bench { - difficulty: 3, - duration: 3.00, - }, - Bench { - difficulty: 4, - duration: 4.00, - }, - Bench { - difficulty: 5, - duration: 5.00, - }, - ]; - { let data = Data::new().await; delete_user(NAME, &data).await; @@ -181,7 +192,7 @@ mod tests { device_user_provided: DEVICE_USER_PROVIDED.into(), device_software_recognised: DEVICE_SOFTWARE_RECOGNISED.into(), threads: THREADS, - benches: benches.clone(), + benches: BENCHES.clone(), }; let _proof = diff --git a/src/api/v1/admin/mod.rs b/src/api/v1/admin/mod.rs index 8ac162c..7123d0c 100644 --- a/src/api/v1/admin/mod.rs +++ b/src/api/v1/admin/mod.rs @@ -30,8 +30,8 @@ pub fn services(cfg: &mut ServiceConfig) { campaigns::services(cfg); } -pub fn get_admin_check_login() -> crate::CheckLogin { - crate::CheckLogin::new(crate::V1_API_ROUTES.admin.auth.register) +pub fn get_admin_check_login() -> crate::CheckLogin { + crate::CheckLogin::new(crate::V1_API_ROUTES.admin.auth) } pub mod routes { diff --git a/src/api/v1/bench.rs b/src/api/v1/bench.rs index b302231..8a8f6cf 100644 --- a/src/api/v1/bench.rs +++ b/src/api/v1/bench.rs @@ -18,7 +18,7 @@ use std::borrow::Cow; use std::str::FromStr; use actix_identity::Identity; -use actix_web::{web, HttpResponse, Responder}; +use actix_web::{http, web, HttpResponse, Responder}; use futures::future::try_join_all; use serde::{Deserialize, Serialize}; use sqlx::types::time::OffsetDateTime; @@ -29,6 +29,9 @@ use crate::errors::*; use crate::AppData; pub mod routes { + + use crate::middleware::auth::GetLoginRoute; + pub struct Benches { pub submit: &'static str, pub register: &'static str, @@ -36,6 +39,25 @@ pub mod routes { pub scope: &'static str, } + impl GetLoginRoute for Benches { + fn get_login_route(&self, src: Option<&str>) -> String { + if let Some(redirect_to) = src { + // uri::Builder::new().path_and_query( + format!( + "{}?redirect_to={}", + self.register, + urlencoding::encode(redirect_to) + ) + // let mut url: Uri = self.register.parse().unwrap(); + // url.qu + // url.query_pairs_mut() + // .append_pair("redirect_to", redirect_to); + } else { + self.register.to_string() + } + } + } + impl Benches { pub const fn new() -> Benches { let submit = "/api/v1/benches/{campaign_id}/submit"; @@ -50,10 +72,10 @@ pub mod routes { } } pub fn submit_route(&self, campaign_id: &str) -> String { - self.submit.replace("{campaign_id}", &campaign_id) + self.submit.replace("{campaign_id}", campaign_id) } pub fn fetch_routes(&self, campaign_id: &str) -> String { - self.fetch.replace("{campaign_id}", &campaign_id) + self.fetch.replace("{campaign_id}", campaign_id) } } } @@ -99,11 +121,27 @@ pub mod runners { } } +#[derive(Deserialize)] +pub struct Query { + pub redirect_to: Option, +} + #[my_codegen::get(path = "crate::V1_API_ROUTES.benches.register")] -async fn register(data: AppData, id: Identity) -> ServiceResult { +async fn register( + data: AppData, + id: Identity, + path: web::Query, +) -> ServiceResult { let uuid = runners::register_runner(&data).await?; id.remember(uuid.to_string()); - Ok(HttpResponse::Ok()) + let path = path.into_inner(); + if let Some(redirect_to) = path.redirect_to { + Ok(HttpResponse::Found() + .insert_header((http::header::LOCATION, redirect_to)) + .finish()) + } else { + Ok(HttpResponse::Ok().into()) + } } #[derive(Serialize, Deserialize, Clone)] @@ -126,8 +164,8 @@ pub struct SubmissionProof { pub proof: String, } -fn get_check_login() -> crate::CheckLogin { - crate::CheckLogin::new(crate::V1_API_ROUTES.benches.register) +fn get_check_login() -> crate::CheckLogin { + crate::CheckLogin::new(crate::V1_API_ROUTES.benches) } #[my_codegen::post( diff --git a/src/middleware/auth.rs b/src/middleware/auth.rs index 9ea811e..dd70d69 100644 --- a/src/middleware/auth.rs +++ b/src/middleware/auth.rs @@ -16,6 +16,8 @@ */ #![allow(clippy::type_complexity)] +use std::rc::Rc; + use actix_http::body::AnyBody; use actix_identity::Identity; use actix_service::{Service, Transform}; @@ -24,43 +26,50 @@ use actix_web::{http, Error, FromRequest, HttpResponse}; use futures::future::{ok, Either, Ready}; -pub struct CheckLogin { - login: &'static str, +pub trait GetLoginRoute { + fn get_login_route(&self, src: Option<&str>) -> String; } -impl CheckLogin { - pub fn new(login: &'static str) -> Self { +pub struct CheckLogin { + login: Rc, +} + +impl CheckLogin { + pub fn new(login: T) -> Self { + let login = Rc::new(login); Self { login } } } -impl Transform for CheckLogin +impl Transform for CheckLogin where S: Service, Error = Error>, S::Future: 'static, + GT: GetLoginRoute, { type Response = ServiceResponse; type Error = Error; - type Transform = CheckLoginMiddleware; + type Transform = CheckLoginMiddleware; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ok(CheckLoginMiddleware { service, - login: self.login, + login: self.login.clone(), }) } } -pub struct CheckLoginMiddleware { +pub struct CheckLoginMiddleware { service: S, - login: &'static str, + login: Rc, } -impl Service for CheckLoginMiddleware +impl Service for CheckLoginMiddleware where S: Service, Error = Error>, S::Future: 'static, + GT: GetLoginRoute, { type Response = ServiceResponse; type Error = Error; @@ -80,12 +89,134 @@ where let req = ServiceRequest::from_parts(r, pl); Either::Left(self.service.call(req)) } else { - let req = ServiceRequest::from_parts(r, pl); //.ok().unwrap(); + let path = r.uri().path_and_query().map(|path| path.as_str()); + let path = self.login.get_login_route(path); + let req = ServiceRequest::from_parts(r, pl); Either::Right(ok(req.into_response( HttpResponse::Found() - .insert_header((http::header::LOCATION, self.login)) + .insert_header((http::header::LOCATION, path)) .finish(), ))) } } } + +#[cfg(test)] +mod tests { + use url::Url; + + use crate::api::v1::bench::Submission; + use crate::data::Data; + use crate::middleware::auth::GetLoginRoute; + use crate::tests::*; + use crate::*; + + use actix_web::{http::header, test}; + + #[actix_rt::test] + async fn auth_middleware_works() { + fn make_uri(path: &str, queries: &Option>) -> String { + let mut url = Url::parse("http://x/").unwrap(); + let final_path; + url.set_path(path); + + if let Some(queries) = queries { + { + let mut query_pairs = url.query_pairs_mut(); + queries.iter().for_each(|(k, v)| { + query_pairs.append_pair(k, v); + }); + } + + final_path = format!("{}?{}", url.path(), url.query().unwrap()); + } else { + final_path = url.path().to_string(); + } + final_path + } + + const NAME: &str = "testmiddlewareuser"; + const EMAIL: &str = "testuserupda@testmiddlewareuser.com"; + const PASSWORD: &str = "longpassword2"; + const DEVICE_USER_PROVIDED: &str = "foo"; + const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2"; + const THREADS: i32 = 4; + let queries = Some(vec![ + ("foo", "bar"), + ("src", "/x/y/z"), + ("with_q", "/a/b/c/?goo=x"), + ]); + + { + let data = Data::new().await; + delete_user(NAME, &data).await; + } + let (data, _creds, signin_resp) = + register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let survey = get_survey_user(data.clone()).await; + let survey_cookie = get_cookie!(survey); + + let campaign = create_new_campaign(NAME, data.clone(), cookies.clone()).await; + + let bench_submit_route = + V1_API_ROUTES.benches.submit_route(&campaign.campaign_id); + let bench_routes = vec![ + (&bench_submit_route, queries.clone()), + (&bench_submit_route, None), + ]; + + let app = get_app!(data).await; + + // let campaign_routes = vec![ + // (Some(V1_API_ROUTES.camp.submit), queries.clone()), + // (None, None), + // (Some(V1_API_ROUTES.benches.submit), None), + // ]; + + let bench_submit_payload = Submission { + device_user_provided: DEVICE_USER_PROVIDED.into(), + device_software_recognised: DEVICE_SOFTWARE_RECOGNISED.into(), + threads: THREADS, + benches: BENCHES.clone(), + }; + + for (from, query) in bench_routes.iter() { + let route = make_uri(from, query); + let signin_resp = test::call_service( + &app, + post_request!(&bench_submit_payload, &route).to_request(), + ) + .await; + assert_eq!(signin_resp.status(), StatusCode::FOUND); + + let redirect_to = V1_API_ROUTES.benches.get_login_route(Some(&route)); + let headers = signin_resp.headers(); + assert_eq!(headers.get(header::LOCATION).unwrap(), &redirect_to); + + let add_feedback_resp = test::call_service( + &app, + post_request!(&bench_submit_payload, &route) + .cookie(survey_cookie.clone()) + .to_request(), + ) + .await; + assert_eq!(add_feedback_resp.status(), StatusCode::OK); + } + } + + // let signin_resp = test::call_service( + // &app, + // test::TestRequest::get() + // .uri(V1_API_ROUTES.benches.get_login_route(redirect_to).as_ref().unwrap()) + // .to_request(), + // ) + // .await; + // assert_eq!(signin_resp.status(), StatusCode::FOUND); + // let headers = signin_resp.headers(); + // assert_eq!( + // headers.get(header::LOCATION).unwrap(), + // redirect_to.as_ref().unwrap() + // ) + // +} diff --git a/src/static_assets/static_files.rs b/src/static_assets/static_files.rs index e0da09b..bb29064 100644 --- a/src/static_assets/static_files.rs +++ b/src/static_assets/static_files.rs @@ -126,8 +126,8 @@ mod tests { for file in [ assets::LOGO.path, assets::HEADSETS.path, - &*crate::JS, - &*crate::GLUE, + *crate::JS, + *crate::GLUE, ] .iter() { diff --git a/src/tests.rs b/src/tests.rs index 467c683..f2a35d6 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -20,6 +20,7 @@ use std::sync::Arc; use actix_web::cookie::Cookie; use actix_web::test; use actix_web::{dev::ServiceResponse, error::ResponseError, http::StatusCode}; +use lazy_static::lazy_static; use serde::Serialize; use uuid::Uuid; @@ -28,7 +29,7 @@ use crate::api::v1::admin::{ auth::runners::{Login, Register}, campaigns::{AddCapmaign, AddCapmaignResp}, }; -use crate::api::v1::bench::{BenchConfig, Submission, SubmissionProof}; +use crate::api::v1::bench::{Bench, BenchConfig, Submission, SubmissionProof}; use crate::data::Data; use crate::errors::*; use crate::V1_API_ROUTES; @@ -396,3 +397,28 @@ pub async fn submit_bench( // assert_eq!(get_feedback_resp.status(), StatusCode::OK); // test::read_body_json(get_feedback_resp).await //} + +lazy_static! { + pub static ref BENCHES: Vec = vec![ + Bench { + difficulty: 1, + duration: 1.00, + }, + Bench { + difficulty: 2, + duration: 2.00, + }, + Bench { + difficulty: 3, + duration: 3.00, + }, + Bench { + difficulty: 4, + duration: 4.00, + }, + Bench { + difficulty: 5, + duration: 5.00, + }, + ]; +} diff --git a/templates/vendor.ts b/templates/vendor.ts new file mode 100644 index 0000000..8e920cc --- /dev/null +++ b/templates/vendor.ts @@ -0,0 +1,13 @@ +/* + * mCaptcha is a PoW based DoS protection software. + * This is the frontend web component of the mCaptcha system + * Copyright © 2021 Aravinth Manivnanan . + * + * Use of this source code is governed by Apache 2.0 or MIT license. + * You shoud have received a copy of MIT and Apache 2.0 along with + * this program. If not, see for + * MIT or for Apache. + */ +import * as glue from 'mcaptcha-glue' + +glue.init()