From 04ef2cb455fcda77957ac54b759038cf836c34d7 Mon Sep 17 00:00:00 2001 From: realaravinth Date: Wed, 17 Aug 2022 17:50:38 +0530 Subject: [PATCH] feat: bootstrap config, auth and upload report route --- Dockerfile | 30 +++++++ Makefile | 46 +++++++++++ build.rs | 32 ++++++++ config/default.toml | 38 +++++++++ src/api/mod.rs | 17 ++++ src/api/v1/meta.rs | 133 ++++++++++++++++++++++++++++++ src/api/v1/mod.rs | 76 +++++++++++++++++ src/api/v1/report.rs | 128 +++++++++++++++++++++++++++++ src/ctx.rs | 89 ++++++++++++++++++++ src/errors.rs | 189 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 118 +++++++++++++++++++++++++++ src/routes.rs | 21 +++++ src/settings.rs | 174 +++++++++++++++++++++++++++++++++++++++ 13 files changed, 1091 insertions(+) create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 build.rs create mode 100644 config/default.toml create mode 100644 src/api/mod.rs create mode 100644 src/api/v1/meta.rs create mode 100644 src/api/v1/mod.rs create mode 100644 src/api/v1/report.rs create mode 100644 src/ctx.rs create mode 100644 src/errors.rs create mode 100644 src/main.rs create mode 100644 src/routes.rs create mode 100644 src/settings.rs diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..491c729 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM rust:latest as planner +RUN cargo install cargo-chef +WORKDIR /src +COPY . /src/ +RUN cargo chef prepare --recipe-path recipe.json + +FROM rust:latest as cacher +WORKDIR /src/ +RUN cargo install cargo-chef +COPY --from=planner /src/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json + +FROM rust:latest as rust +WORKDIR /src +COPY . . +COPY --from=cacher /src/target target +#COPY --from=frontend /src/static/cache/bundle/ /src/static/cache/bundle/ +RUN cargo --version +#RUN make cache-bust +RUN cargo build --release + +FROM debian:bullseye as rwhool +LABEL org.opencontainers.image.source https://github.com/realaravinth/rwhool +RUN useradd -ms /bin/bash -u 1001 rwhool +WORKDIR /home/rwhool +COPY --from=rust /src/target/release/rwhool /usr/local/bin/ +#COPY --from=rust /src/config/default.toml /etc/rwhool/config.toml +RUN mkdir /var/lib/rwhool && chown rwhool:rwhool /var/lib/rwhool +USER rwhool +CMD [ "/usr/local/bin/rwhool" ] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b155cf1 --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +default: ## Build app in debug mode + cargo build + +check: ## Check for syntax errors on all workspaces + cargo check --workspace --tests --all-features + +clean: ## Delete build artifacts + @cargo clean + +coverage: migrate ## Generate code coverage report in HTML format + $(call cache_bust) + cargo tarpaulin -t 1200 --out Html + +doc: ## Generate documentation + #yarn doc + cargo doc --no-deps --workspace --all-features + +docker: ## Build Docker image + docker build -t realaravinth/rageshake_webhook:master -t realaravinth/rageshake_webhook:latest . + +docker-publish: docker ## Build and publish Docker image + docker push realaravinth/rageshake_webhook:master + docker push realaravinth/rageshake_webhook:latest + +env: ## Setup development environtment + cargo fetch + +lint: ## Lint codebase + cargo fmt -v --all -- --emit files + cargo clippy --workspace --tests --all-features + +release: ## Build app with release optimizations + cargo build --release + +run: ## Run app in debug mode + cargo run + + +test: ## Run all available tests + cargo test --no-fail-fast + +xml-test-coverage: ## Generate code coverage report in XML format + cargo tarpaulin -t 1200 --out Xml + +help: ## Prints help for targets with comments + @cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..904577c --- /dev/null +++ b/build.rs @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 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 std::process::Command; + +use sqlx::types::time::OffsetDateTime; + +fn main() { + // note: add error checking yourself. + let output = Command::new("git") + .args(&["rev-parse", "HEAD"]) + .output() + .unwrap(); + let git_hash = String::from_utf8(output.stdout).unwrap(); + println!("cargo:rustc-env=GIT_HASH={}", git_hash); + + let now = OffsetDateTime::now_utc().format("%y-%m-%d"); + println!("cargo:rustc-env=COMPILED_DATE={}", &now); +} diff --git a/config/default.toml b/config/default.toml new file mode 100644 index 0000000..0c7b7be --- /dev/null +++ b/config/default.toml @@ -0,0 +1,38 @@ +debug = true +source_code = "https://git.batsense.net/mystiq/rageshake-webhook" +creds = [ + { username = "rwhook", password = "foobar" } +] + +[server] +# Please set a unique value, your mCaptcha instance's security depends on this being +# unique +cookie_secret = "Zae0OOxf^bOJ#zN^&k7VozgW&QAx%n02TQFXpRMG4cCU0xMzgu3dna@tQ9dvc&TlE6p*n#kXUdLZJCQsuODIV%r$@o4%770ePQB7m#dpV!optk01NpY0@615w5e2Br4d" +# The port at which you want authentication to listen to +# takes a number, choose from 1000-10000 if you dont know what you are doing +port = 7000 +#IP address. Enter 0.0.0.0 to listen on all available addresses +ip= "0.0.0.0" +# enter your hostname, eg: example.com +domain = "localhost" +# Set true if you have setup TLS with a reverse proxy like Nginx. +# Does HTTPS redirect and sends additional headers that can only be used if +# HTTPS available to improve security +proxy_has_tls = false +#url_prefix = "" + +#[database] +## This section deals with the database location and how to access it +## Please note that at the moment, we have support for only postgresqa. +## Example, if you are Batman, your config would be: +## hostname = "batcave.org" +## port = "5432" +## username = "batman" +## password = "somereallycomplicatedBatmanpassword" +#hostname = "localhost" +#port = "5432" +#username = "postgres" +#password = "password" +#name = "postgres" +#pool = 4 +#database_type="postgres" # "postgres", "maria" diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..5aa8f74 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,17 @@ +/* + * 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 v1; diff --git a/src/api/v1/meta.rs b/src/api/v1/meta.rs new file mode 100644 index 0000000..96e6cd6 --- /dev/null +++ b/src/api/v1/meta.rs @@ -0,0 +1,133 @@ +/* + * 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::{web, HttpResponse, Responder}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::AppCtx; +use crate::{GIT_COMMIT_HASH, VERSION}; + +#[derive(Clone, Debug, Deserialize, Builder, Serialize)] +pub struct BuildDetails { + pub version: &'static str, + pub git_commit_hash: &'static str, + pub source_code: String, +} + +pub mod routes { + pub struct Meta { + pub build_details: &'static str, + pub health: &'static str, + } + + impl Meta { + pub const fn new() -> Self { + Self { + build_details: "/api/v1/meta/build", + health: "/api/v1/meta/health", + } + } + } +} + +/// emits build details of the bninary +#[actix_web_codegen_const_routes::get(path = "crate::API_V1_ROUTES.meta.build_details")] +async fn build_details(ctx: AppCtx) -> impl Responder { + let build = BuildDetails { + version: VERSION, + git_commit_hash: GIT_COMMIT_HASH, + source_code: ctx.source_code.clone(), + }; + HttpResponse::Ok().json(build) +} + +#[derive(Clone, Debug, Deserialize, Builder, Serialize)] +/// Health check return datatype +pub struct Health { + db: bool, +} + +/// checks all components of the system +#[actix_web_codegen_const_routes::get(path = "crate::API_V1_ROUTES.meta.health")] +async fn health() -> impl Responder { + // let mut resp_builder = HealthBuilder::default(); + + // resp_builder.db(data.db.ping().await); + + HttpResponse::Ok() //.json(resp_builder.build().unwrap()) +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(build_details); + cfg.service(health); +} + +#[cfg(test)] +pub mod tests { + use actix_web::{http::StatusCode, test, App}; + + use crate::api::v1::services; + use crate::*; + + #[actix_rt::test] + async fn build_details_works() { + let settings = Settings::new().unwrap(); + let ctx = AppCtx::new(crate::ctx::Ctx::new(&settings).await); + let app = test::init_service(App::new().app_data(ctx.clone()).configure(services)).await; + + let resp = test::call_service( + &app, + test::TestRequest::get() + .uri(API_V1_ROUTES.meta.build_details) + .to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + } + + // #[actix_rt::test] + // async fn health_works_pg() { + // let data = crate::tests::pg::get_data().await; + // health_works(data).await; + // } + // + // #[actix_rt::test] + // async fn health_works_maria() { + // let data = crate::tests::maria::get_data().await; + // health_works(data).await; + // } + // + // pub async fn health_works(data: ArcCtx) { + // println!("{}", API_V1_ROUTES.meta.health); + // let data = &data; + // let app = get_app!(data).await; + // + // let resp = test::call_service( + // &app, + // test::TestRequest::get() + // .uri(API_V1_ROUTES.meta.health) + // .to_request(), + // ) + // .await; + // assert_eq!(resp.status(), StatusCode::OK); + // + // let health_resp: Health = test::read_body_json(resp).await; + // assert!(health_resp.db); + // assert_eq!(health_resp.redis, Some(true)); + // } +} diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs new file mode 100644 index 0000000..1c4a455 --- /dev/null +++ b/src/api/v1/mod.rs @@ -0,0 +1,76 @@ +/* + * 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::dev::ServiceRequest; +use actix_web::web; +use actix_web::Error; +use actix_web::HttpMessage; +use actix_web_httpauth::extractors::basic::BasicAuth; + +pub mod meta; +pub mod report; + +use crate::errors::*; +use crate::AppCtx; +use crate::SETTINGS; + +pub const API_V1_ROUTES: routes::Routes = routes::Routes::new(); + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SignedInUser(String); + +pub async fn httpauth( + req: ServiceRequest, + credentials: BasicAuth, +) -> Result { + let _ctx: &AppCtx = req.app_data().unwrap(); + let username = credentials.user_id(); + let password = credentials.password().unwrap(); + if SETTINGS.authenticate(username, password) { + { + let mut ext = req.extensions_mut(); + ext.insert(SignedInUser(username.to_string())); + } + Ok(req) + } else { + let e = Error::from(ServiceError::Unauthorized); + Err((e, req)) + } +} + +pub fn services(cfg: &mut web::ServiceConfig) { + report::services(cfg); + meta::services(cfg); +} + +pub mod routes { + use crate::api::v1::meta::routes::Meta; + use crate::api::v1::report::routes::Report; + + pub struct Routes { + pub report: Report, + pub meta: Meta, + } + + impl Routes { + pub const fn new() -> Self { + Self { + report: Report::new(), + meta: Meta::new(), + } + } + } +} diff --git a/src/api/v1/report.rs b/src/api/v1/report.rs new file mode 100644 index 0000000..bd2c3c8 --- /dev/null +++ b/src/api/v1/report.rs @@ -0,0 +1,128 @@ +/* + * 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::HttpMessage; +use actix_web::{web, Error, HttpRequest, HttpResponse, Responder}; +use actix_web_httpauth::middleware::HttpAuthentication; +use serde::{Deserialize, Serialize}; + +use super::httpauth; +use super::SignedInUser; +use super::API_V1_ROUTES; +use crate::AppCtx; + +pub mod routes { + use super::*; + #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)] + pub struct Report { + pub upload: &'static str, + } + impl Report { + pub const fn new() -> Self { + Self { + upload: "/api/v1/report/upload", + } + } + } +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(upload); +} + +#[derive(Serialize, PartialEq, Eq, Deserialize, Debug, Default)] +struct Client { + #[serde(rename(deserialize = "User-Agent", serialize = "User-Agent"))] + user_agent: String, + version: String, + user_id: String, +} + +#[derive(Serialize, PartialEq, Eq, Deserialize, Debug, Default)] +struct Report { + user_text: String, + app: String, + data: Client, + labels: Option>, + logs: Vec, + files: Vec, + #[serde(rename(deserialize = "logErrors", serialize = "logErrors"))] + log_errors: Option, + #[serde(rename(deserialize = "fileErrors", serialize = "fileErrors"))] + file_errors: Option, + report_url: Option, + listing_url: String, +} + +#[actix_web_codegen_const_routes::post( + path = "API_V1_ROUTES.report.upload", + wrap = "HttpAuthentication::basic(httpauth)" +)] +async fn upload( + req: HttpRequest, + ctx: AppCtx, + payload: web::Json, +) -> Result { + Ok(HttpResponse::Ok()) +} + +#[cfg(test)] +pub mod tests { + use actix_web::{ + http::{header, StatusCode}, + test, App, + }; + + use super::*; + use crate::*; + + #[actix_rt::test] + async fn index_works() { + // const USERNAME: &str = "index_works"; + // const PASSWORD: &str = "23k4j;123k4j1;l23kj4"; + let settings = Settings::new().unwrap(); + let creds = settings.creds.get(0).unwrap().clone(); + let auth = format!( + "Basic {}", + base64::encode(format!("{}:{}", creds.username, creds.password)) + ); + + // let settings = Settings::new().unwrap(); + let ctx = AppCtx::new(crate::ctx::Ctx::new(&settings).await); + let app = test::init_service( + App::new() + .app_data(ctx.clone()) + .configure(crate::routes::services), + ) + .await; + + let def_payload = Report::default(); + + let upload_resp = test::call_service( + &app, + test::TestRequest::post() + .append_header((header::AUTHORIZATION, auth)) + .uri(API_V1_ROUTES.report.upload) + .set_json(&def_payload) + .to_request(), + ) + .await; + if upload_resp.status() != StatusCode::OK { + let resp_err: crate::errors::ErrorToResponse = test::read_body_json(upload_resp).await; + panic!("{:?}", resp_err.error); + } + } +} diff --git a/src/ctx.rs b/src/ctx.rs new file mode 100644 index 0000000..c2ee996 --- /dev/null +++ b/src/ctx.rs @@ -0,0 +1,89 @@ +/* + * 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 . + */ +//! App data: database connections, etc. +use std::sync::Arc; +use std::thread; + +use argon2_creds::{Config, ConfigBuilder, PasswordPolicy}; + +//use crate::errors::ServiceResult; +use crate::settings::Settings; +/// App data +pub struct Ctx { + // /// database ops defined by db crates + // pub db: BoxDB, + /// credential management configuration + pub creds: Config, + /// app settings + pub settings: Settings, + pub source_code: String, +} + +impl Ctx { + pub fn get_creds() -> Config { + ConfigBuilder::default() + .username_case_mapped(true) + .profanity(true) + .blacklist(true) + .password_policy(PasswordPolicy::default()) + .build() + .unwrap() + } + + #[cfg(not(tarpaulin_include))] + /// create new instance of app data + pub async fn new(s: &Settings) -> ArcCtx { + let creds = Self::get_creds(); + let c = creds.clone(); + + #[allow(unused_variables)] + let init = thread::spawn(move || { + log::info!("Initializing credential manager"); + c.init(); + log::info!("Initialized credential manager"); + }); + + //let db = match s.database.database_type { + // crate::settings::DBType::Maria => db::maria::get_data(Some(s.clone())).await, + // crate::settings::DBType::Postgres => db::pg::get_data(Some(s.clone())).await, + //}; + + #[cfg(not(debug_assertions))] + init.join().unwrap(); + + let source_code = { + let mut url = s.source_code.clone(); + if !url.ends_with('/') { + url.push('/'); + } + let mut base = url::Url::parse(&url).unwrap(); + base = base.join("tree/").unwrap(); + base = base.join(crate::GIT_COMMIT_HASH).unwrap(); + base.into() + }; + + let data = Ctx { + creds, + // db, + settings: s.clone(), + source_code, + }; + + Arc::new(data) + } +} + +pub type ArcCtx = Arc; diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..d3f0954 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,189 @@ +/* + * 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 std::convert::From; + +use argon2_creds::errors::CredsError; +//use db_core::errors::DBError; +use actix_web::http; +use actix_web::http::StatusCode; +use actix_web::HttpResponse; +use actix_web::HttpResponseBuilder; +use actix_web::ResponseError; +use derive_more::{Display, Error}; +use serde::{Deserialize, Serialize}; +use tokio::sync::oneshot::error::RecvError; +use url::ParseError; + +//#[derive(Debug, Display, Error)] +//pub struct DBErrorWrapper(DBError); +// +//impl std::cmp::PartialEq for DBErrorWrapper { +// fn eq(&self, other: &Self) -> bool { +// format!("{}", self.0) == format!("{}", other.0) +// } +//} +// +#[derive(Debug, Display, PartialEq, Eq, Error)] +#[cfg(not(tarpaulin_include))] +#[allow(dead_code)] +pub enum ServiceError { + #[display(fmt = "unauthorized")] + Unauthorized, + + #[display(fmt = "internal server error")] + InternalServerError, + + #[display( + fmt = "This server is is closed for registration. Contact admin if this is unexpecter" + )] + ClosedForRegistration, + + #[display(fmt = "The value you entered for email is not an email")] //405j + NotAnEmail, + #[display(fmt = "The value you entered for URL is not a URL")] //405j + NotAUrl, + + #[display(fmt = "Wrong password")] + WrongPassword, + + /// when the value passed contains profainity + #[display(fmt = "Can't allow profanity in usernames")] + ProfainityError, + /// when the value passed contains blacklisted words + /// see [blacklist](https://github.com/shuttlecraft/The-Big-Username-Blacklist) + #[display(fmt = "Username contains blacklisted words")] + BlacklistError, + /// when the value passed contains characters not present + /// in [UsernameCaseMapped](https://tools.ietf.org/html/rfc8265#page-7) + /// profile + #[display(fmt = "username_case_mapped violation")] + UsernameCaseMappedError, + + #[display(fmt = "Passsword too short")] + PasswordTooShort, + #[display(fmt = "Username too long")] + PasswordTooLong, + #[display(fmt = "Passwords don't match")] + PasswordsDontMatch, + + /// when the a username is already taken + #[display(fmt = "Username not available")] + UsernameTaken, + + /// email is already taken + #[display(fmt = "Email not available")] + EmailTaken, + // #[display(fmt = "{}", _0)] + // DBError(DBErrorWrapper), +} + +#[derive(Serialize, Deserialize)] +#[cfg(not(tarpaulin_include))] +pub struct ErrorToResponse { + pub error: String, +} + +#[cfg(not(tarpaulin_include))] +impl ResponseError for ServiceError { + #[cfg(not(tarpaulin_include))] + fn error_response(&self) -> HttpResponse { + HttpResponseBuilder::new(self.status_code()) + .append_header(( + http::header::CONTENT_TYPE, + "application/json; charset=UTF-8", + )) + .body( + serde_json::to_string(&ErrorToResponse { + error: self.to_string(), + }) + .unwrap(), + ) + } + + #[cfg(not(tarpaulin_include))] + fn status_code(&self) -> StatusCode { + match self { + ServiceError::ClosedForRegistration => StatusCode::FORBIDDEN, + ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, + ServiceError::NotAUrl => StatusCode::BAD_REQUEST, + ServiceError::NotAnEmail => StatusCode::BAD_REQUEST, + ServiceError::WrongPassword => StatusCode::UNAUTHORIZED, + ServiceError::Unauthorized => StatusCode::UNAUTHORIZED, + + ServiceError::ProfainityError => StatusCode::BAD_REQUEST, + ServiceError::BlacklistError => StatusCode::BAD_REQUEST, + ServiceError::UsernameCaseMappedError => StatusCode::BAD_REQUEST, + + ServiceError::PasswordTooShort => StatusCode::BAD_REQUEST, + ServiceError::PasswordTooLong => StatusCode::BAD_REQUEST, + ServiceError::PasswordsDontMatch => StatusCode::BAD_REQUEST, + + ServiceError::UsernameTaken => StatusCode::BAD_REQUEST, + ServiceError::EmailTaken => StatusCode::BAD_REQUEST, + // ServiceError::DBError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(e: CredsError) -> ServiceError { + match e { + CredsError::UsernameCaseMappedError => ServiceError::UsernameCaseMappedError, + CredsError::ProfainityError => ServiceError::ProfainityError, + CredsError::BlacklistError => ServiceError::BlacklistError, + CredsError::NotAnEmail => ServiceError::NotAnEmail, + CredsError::Argon2Error(_) => ServiceError::InternalServerError, + CredsError::PasswordTooLong => ServiceError::PasswordTooLong, + CredsError::PasswordTooShort => ServiceError::PasswordTooShort, + } + } +} + +//impl From for ServiceError { +// #[cfg(not(tarpaulin_include))] +// fn from(e: DBError) -> ServiceError { +// println!("from conversin: {}", e); +// match e { +// DBError::UsernameTaken => ServiceError::UsernameTaken, +// DBError::SecretTaken => ServiceError::InternalServerError, +// DBError::EmailTaken => ServiceError::EmailTaken, +// DBError::AccountNotFound => ServiceError::AccountNotFound, +// _ => ServiceError::DBError(DBErrorWrapper(e)), +// } +// } +//} + +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(_: ParseError) -> ServiceError { + ServiceError::NotAUrl + } +} + +#[cfg(not(tarpaulin_include))] +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(e: RecvError) -> Self { + log::error!("{:?}", e); + ServiceError::InternalServerError + } +} + +#[cfg(not(tarpaulin_include))] +pub type ServiceResult = std::result::Result; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..72dac82 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,118 @@ +/* + * 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 std::env; + +use actix_web::http::StatusCode; +use actix_web::web::JsonConfig; +use actix_web::{error::InternalError, middleware, App, HttpServer}; +use log::info; + +use lazy_static::lazy_static; + +mod api; +mod ctx; +//mod db; +//mod docs; +#[cfg(not(tarpaulin_include))] +mod errors; +//#[macro_use] +//mod pages; +//#[macro_use] +mod routes; +mod settings; +//mod static_assets; +//#[cfg(test)] +//#[macro_use] +//mod tests; +// +pub use crate::ctx::Ctx; +//pub use crate::static_assets::static_files::assets::*; +pub use api::v1::API_V1_ROUTES; +//pub use docs::DOCS; +//pub use pages::routes::ROUTES as PAGES; +pub use settings::Settings; +//use static_assets::FileMap; + +lazy_static! { + pub static ref SETTINGS: Settings= Settings::new().unwrap(); +// pub static ref S: String = env::var("S").unwrap(); +// pub static ref FILES: FileMap = FileMap::new(); +// pub static ref JS: &'static str = +// FILES.get("./static/cache/bundle/bundle.js").unwrap(); +// pub static ref CSS: &'static str = +// FILES.get("./static/cache/bundle/css/main.css").unwrap(); +// pub static ref MOBILE_CSS: &'static str = +// FILES.get("./static/cache/bundle/css/mobile.css").unwrap(); +} + +pub const COMPILED_DATE: &str = env!("COMPILED_DATE"); +pub const GIT_COMMIT_HASH: &str = env!("GIT_HASH"); +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const PKG_NAME: &str = env!("CARGO_PKG_NAME"); +pub const PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); +pub const PKG_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE"); + +pub const CACHE_AGE: u32 = 604800; + +use ctx::ArcCtx; +pub type AppCtx = actix_web::web::Data; + +#[actix_web::main] +#[cfg(not(tarpaulin_include))] +async fn main() -> std::io::Result<()> { + env::set_var("RUST_LOG", "info"); + + pretty_env_logger::init(); + + info!( + "{}: {}.\nFor more information, see: {}\nBuild info:\nVersion: {} commit: {}", + PKG_NAME, PKG_DESCRIPTION, PKG_HOMEPAGE, VERSION, GIT_COMMIT_HASH + ); + + let settings = Settings::new().unwrap(); + let ctx = Ctx::new(&settings).await; + let ctx = actix_web::web::Data::new(ctx); + + let ip = settings.server.get_ip(); + println!("Starting server on: http://{ip}"); + + HttpServer::new(move || { + App::new() + .wrap(middleware::Logger::default()) + .wrap( + middleware::DefaultHeaders::new().add(("Permissions-Policy", "interest-cohort=()")), + ) + .wrap(middleware::Compress::default()) + .app_data(ctx.clone()) + .wrap(middleware::NormalizePath::new( + middleware::TrailingSlash::Trim, + )) + .app_data(get_json_err()) + .configure(routes::services) + }) + .bind(ip)? + .run() + .await +} + +#[cfg(not(tarpaulin_include))] +pub fn get_json_err() -> JsonConfig { + JsonConfig::default().error_handler(|err, _| { + //debug!("JSON deserialization error: {:?}", &err); + InternalError::new(err, StatusCode::BAD_REQUEST).into() + }) +} diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..7920a1d --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,21 @@ +/* + * 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::web; + +pub fn services(cfg: &mut web::ServiceConfig) { + crate::api::v1::services(cfg); +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..8875eb9 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,174 @@ +/* + * 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 std::env; +use std::path::Path; + +use config::{Config, ConfigError, Environment, File}; +use log::warn; +use serde::Deserialize; +use url::Url; + +#[derive(Debug, Clone, Deserialize)] +pub struct Server { + pub port: u32, + pub domain: String, + pub cookie_secret: String, + pub ip: String, + pub url_prefix: Option, + pub proxy_has_tls: bool, +} + +impl Server { + #[cfg(not(tarpaulin_include))] + pub fn get_ip(&self) -> String { + format!("{}:{}", self.ip, self.port) + } +} + +//#[derive(Deserialize, Serialize, Display, PartialEq, Clone, Debug)] +//#[serde(rename_all = "lowercase")] +//pub enum DBType { +// #[display(fmt = "postgres")] +// Postgres, +// #[display(fmt = "maria")] +// Maria, +//} +// +//impl DBType { +// fn from_url(url: &Url) -> Result { +// match url.scheme() { +// "mysql" => Ok(Self::Maria), +// "postgres" => Ok(Self::Postgres), +// _ => Err(ConfigError::Message("Unknown database type".into())), +// } +// } +//} +// +//#[derive(Debug, Clone, Deserialize)] +//pub struct Database { +// pub url: String, +// pub pool: u32, +// pub database_type: DBType, +//} + +#[derive(Debug, Clone, Deserialize)] +pub struct Creds { + pub username: String, + pub password: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Settings { + pub debug: bool, + // pub database: Database, + pub server: Server, + pub source_code: String, + pub creds: Vec, +} + +#[cfg(not(tarpaulin_include))] +impl Settings { + pub fn authenticate(&self, username: &str, password: &str) -> bool { + self.creds + .iter() + .any(|c| c.username == username && c.password == password) + } + + pub fn new() -> Result { + let mut s = Config::new(); + + const CURRENT_DIR: &str = "./config/default.toml"; + const ETC: &str = "/etc/rwook/config.toml"; + + if let Ok(path) = env::var("RWHOOK_CONFIG") { + s.merge(File::with_name(&path))?; + } else if Path::new(CURRENT_DIR).exists() { + // merging default config from file + s.merge(File::with_name(CURRENT_DIR))?; + } else if Path::new(ETC).exists() { + s.merge(File::with_name(ETC))?; + } else { + log::warn!("configuration file not found"); + } + + s.merge(Environment::with_prefix("RWHOOK").separator("_"))?; + + check_url(&s); + + match env::var("PORT") { + Ok(val) => { + s.set("server.port", val).unwrap(); + } + Err(e) => warn!("couldn't interpret PORT: {}", e), + } + + // match env::var("DATABASE_URL") { + // Ok(val) => { + // let url = Url::parse(&val).expect("couldn't parse Database URL"); + // s.set("database.url", url.to_string()).unwrap(); + // let database_type = DBType::from_url(&url).unwrap(); + // s.set("database.database_type", database_type.to_string()) + // .unwrap(); + // } + // Err(e) => { + // set_database_url(&mut s); + // } + // } + + // setting default values + // #[cfg(test)] + // s.set("database.pool", 2.to_string()) + // .expect("Couldn't set database pool count"); + + match s.try_into::() { + Ok(val) => { + Ok(val) + }, + Err(e) => Err(ConfigError::Message(format!("\n\nError: {}. If it says missing fields, then please refer to https://github.com/mCaptcha/mcaptcha#configuration to learn more about how mcaptcha reads configuration\n\n", e))), + } + } +} + +#[cfg(not(tarpaulin_include))] +fn check_url(s: &Config) { + let url = s + .get::("source_code") + .expect("Couldn't access source_code"); + + Url::parse(&url).expect("Please enter a URL for source_code in settings"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn creds_works() { + let settings = Settings::new().unwrap(); + let mut creds = settings.creds.get(0).unwrap().clone(); + + assert!(settings.authenticate(&creds.username, &creds.password)); + + creds.username = "noexist".into(); + assert!(!settings.authenticate(&creds.username, &creds.password)); + + let mut creds = settings.creds.get(0).unwrap().clone(); + + creds.password = "noexist".into(); + assert!(!settings.authenticate(&creds.username, &creds.password)); + } +}