feat: bootstrap config, auth and upload report route

This commit is contained in:
Aravinth Manivannan 2022-08-17 17:50:38 +05:30
parent d2a486a7ce
commit 04ef2cb455
Signed by: realaravinth
GPG key ID: AD9F0F08E855ED88
13 changed files with 1091 additions and 0 deletions

30
Dockerfile Normal file
View file

@ -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" ]

46
Makefile Normal file
View file

@ -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}'

32
build.rs Normal file
View file

@ -0,0 +1,32 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
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);
}

38
config/default.toml Normal file
View file

@ -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"

17
src/api/mod.rs Normal file
View file

@ -0,0 +1,17 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
pub mod v1;

133
src/api/v1/meta.rs Normal file
View file

@ -0,0 +1,133 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
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));
// }
}

76
src/api/v1/mod.rs Normal file
View file

@ -0,0 +1,76 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
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<ServiceRequest, (Error, ServiceRequest)> {
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(),
}
}
}
}

128
src/api/v1/report.rs Normal file
View file

@ -0,0 +1,128 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
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<Vec<String>>,
logs: Vec<String>,
files: Vec<String>,
#[serde(rename(deserialize = "logErrors", serialize = "logErrors"))]
log_errors: Option<String>,
#[serde(rename(deserialize = "fileErrors", serialize = "fileErrors"))]
file_errors: Option<String>,
report_url: Option<String>,
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<Report>,
) -> Result<impl Responder, Error> {
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);
}
}
}

89
src/ctx.rs Normal file
View file

@ -0,0 +1,89 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
//! 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<Ctx>;

189
src/errors.rs Normal file
View file

@ -0,0 +1,189 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
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<CredsError> 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<DBError> 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<ParseError> for ServiceError {
#[cfg(not(tarpaulin_include))]
fn from(_: ParseError) -> ServiceError {
ServiceError::NotAUrl
}
}
#[cfg(not(tarpaulin_include))]
impl From<RecvError> for ServiceError {
#[cfg(not(tarpaulin_include))]
fn from(e: RecvError) -> Self {
log::error!("{:?}", e);
ServiceError::InternalServerError
}
}
#[cfg(not(tarpaulin_include))]
pub type ServiceResult<V> = std::result::Result<V, ServiceError>;

118
src/main.rs Normal file
View file

@ -0,0 +1,118 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
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<ArcCtx>;
#[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()
})
}

21
src/routes.rs Normal file
View file

@ -0,0 +1,21 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
use actix_web::web;
pub fn services(cfg: &mut web::ServiceConfig) {
crate::api::v1::services(cfg);
}

174
src/settings.rs Normal file
View file

@ -0,0 +1,174 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
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<String>,
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<Self, ConfigError> {
// 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<Creds>,
}
#[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<Self, ConfigError> {
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::<Self>() {
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::<String>("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));
}
}