mirror of
https://git.verdigado.com/NB-Public/simple-wkd.git
synced 2024-12-06 14:52:41 +01:00
Move rust code in backend folder
This commit is contained in:
parent
7dc3109c72
commit
8d0d13458d
11 changed files with 9 additions and 8 deletions
2893
backend/Cargo.lock
generated
Normal file
2893
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
24
backend/Cargo.toml
Normal file
24
backend/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "simple-wkd"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-files = "0.6.2"
|
||||
actix-web = "4.3.1"
|
||||
chrono = "0.4.24"
|
||||
flexi_logger = "0.25.3"
|
||||
lettre = "0.10.4"
|
||||
log = "0.4.17"
|
||||
once_cell = "1.17.1"
|
||||
rand = "0.8.5"
|
||||
sequoia-net = "0.27.0"
|
||||
sequoia-openpgp = "1.14.0"
|
||||
serde = { version = "1.0.160", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
thiserror = "1.0.40"
|
||||
tokio = { version = "1.27.0", features = ["time"] }
|
||||
toml = "0.7.3"
|
||||
url = { version = "2.3.1", features = ["serde"] }
|
5
backend/src/.gitignore
vendored
Normal file
5
backend/src/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/target
|
||||
/data
|
||||
/logs
|
||||
/config.toml
|
||||
/assets/webpage
|
155
backend/src/confirmation.rs
Normal file
155
backend/src/confirmation.rs
Normal file
|
@ -0,0 +1,155 @@
|
|||
use chrono::Utc;
|
||||
use lettre::message::header::ContentType;
|
||||
use log::{debug, error, trace, warn};
|
||||
|
||||
use crate::errors::Error;
|
||||
use crate::management::{delete_key, Action, Pending};
|
||||
use crate::pending_path;
|
||||
use crate::settings::{MAILER, ROOT_FOLDER, SETTINGS};
|
||||
use crate::utils::{get_email_from_cert, get_filename, parse_pem};
|
||||
|
||||
use lettre::{Message, Transport};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn confirm_action(token: &str) -> Result<(Action, String), Error> {
|
||||
trace!("Handling token {}", token);
|
||||
let pending_path = pending_path!().join(token);
|
||||
let content = if pending_path.is_file() {
|
||||
match fs::read_to_string(&pending_path) {
|
||||
Ok(content) => content,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"Token {} was requested, but can't be read to string!",
|
||||
token
|
||||
);
|
||||
return Err(Error::Inaccessible);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trace!("Requested token {} isn't a file", token);
|
||||
return Err(Error::MissingPending);
|
||||
};
|
||||
let key = match serde_json::from_str::<Pending>(&content) {
|
||||
Ok(key) => key,
|
||||
Err(_) => {
|
||||
warn!("Error while deserializing token {}!", token);
|
||||
return Err(Error::DeserializeData);
|
||||
}
|
||||
};
|
||||
if Utc::now().timestamp() - key.timestamp() > SETTINGS.max_age {
|
||||
match fs::remove_file(&pending_path) {
|
||||
Ok(_) => {
|
||||
debug!(
|
||||
"Deleted stale token {}",
|
||||
get_filename(&pending_path).unwrap()
|
||||
);
|
||||
Err(Error::MissingPending)
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Stale token {} can't be deleted!", token);
|
||||
Err(Error::Inaccessible)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let address = match key.action() {
|
||||
Action::Add => {
|
||||
let cert = parse_pem(key.data())?;
|
||||
let email = get_email_from_cert(&cert)?;
|
||||
let domain = match email.split('@').last() {
|
||||
Some(domain) => domain.to_string(),
|
||||
None => {
|
||||
warn!("Error while parsing email's domain in token {}", token);
|
||||
return Err(Error::ParseEmail);
|
||||
}
|
||||
};
|
||||
match sequoia_net::wkd::insert(ROOT_FOLDER, domain, SETTINGS.variant, &cert) {
|
||||
Ok(_) => email,
|
||||
Err(_) => {
|
||||
warn!("Unable to create a wkd entry for token {}", token);
|
||||
return Err(Error::AddingKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::Delete => match delete_key(key.data()) {
|
||||
Ok(_) => key.data().to_owned(),
|
||||
Err(error) => {
|
||||
warn!("Unable to delete key for user {}", key.data());
|
||||
return Err(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
debug!("Token {} was confirmed", token);
|
||||
match fs::remove_file(&pending_path) {
|
||||
Ok(_) => {
|
||||
trace!(
|
||||
"Deleted confirmed token {}",
|
||||
pending_path.file_name().unwrap().to_str().unwrap()
|
||||
);
|
||||
Ok((*key.action(), address))
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Unable to delete confirmed token {}", token);
|
||||
Err(Error::Inaccessible)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_confirmation_email(address: &str, action: &Action, token: &str) -> Result<(), Error> {
|
||||
debug!("Sending email to {}", address);
|
||||
let template = fs::read_to_string(Path::new("assets").join("mail-template.html")).unwrap();
|
||||
let mut url = SETTINGS
|
||||
.external_url
|
||||
.join("api/")
|
||||
.unwrap()
|
||||
.join("confirm")
|
||||
.unwrap();
|
||||
url.set_query(Some(&format!("token={}", token)));
|
||||
let email = Message::builder()
|
||||
.from(match SETTINGS.mail_settings.mail_from.parse() {
|
||||
Ok(mailbox) => mailbox,
|
||||
Err(_) => {
|
||||
error!("Unable to parse the email in the settings!");
|
||||
panic!("Unable to parse the email in the settings!")
|
||||
}
|
||||
})
|
||||
.to(match address.parse() {
|
||||
Ok(mailbox) => mailbox,
|
||||
Err(_) => {
|
||||
warn!("Error while parsing destination email for token {}", token);
|
||||
return Err(Error::ParseEmail);
|
||||
}
|
||||
})
|
||||
.subject(
|
||||
SETTINGS
|
||||
.mail_settings
|
||||
.mail_subject
|
||||
.replace("%a", &action.to_string().to_lowercase()),
|
||||
)
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(
|
||||
template
|
||||
.replace("{{%u}}", url.as_ref())
|
||||
.replace("{{%a}}", &action.to_string().to_lowercase()),
|
||||
);
|
||||
|
||||
let message = match email {
|
||||
Ok(message) => message,
|
||||
Err(_) => {
|
||||
warn!("Unable to build email for token {}", token);
|
||||
return Err(Error::MailGeneration);
|
||||
}
|
||||
};
|
||||
|
||||
match MAILER.send(&message) {
|
||||
Ok(_) => {
|
||||
debug!("successfully sent email to {}", address);
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Unable to send email to {}", address);
|
||||
Err(Error::SendMail)
|
||||
}
|
||||
}
|
||||
}
|
57
backend/src/errors.rs
Normal file
57
backend/src/errors.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use actix_web::{http::StatusCode, HttpResponseBuilder, ResponseError};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::utils::return_outcome;
|
||||
|
||||
#[derive(Error, Debug, Clone, Copy)]
|
||||
pub enum Error {
|
||||
#[error("(0x01) Cert is invalid")]
|
||||
InvalidCert,
|
||||
#[error("(0x02) Error while parsing cert")]
|
||||
ParseCert,
|
||||
#[error("(0x03) Error while parsing an E-Mail address")]
|
||||
ParseEmail,
|
||||
#[error("(0x04) There is no pending request associated to this token")]
|
||||
MissingPending,
|
||||
#[error("(0x05) Requested key does not exist")]
|
||||
MissingKey,
|
||||
#[error("(0x06) No E-Mail found in the certificate")]
|
||||
MissingMail,
|
||||
#[error("(0x07) Error while sending the E-Mail")]
|
||||
SendMail,
|
||||
#[error("(0x08) rror while serializing data")]
|
||||
SerializeData,
|
||||
#[error("(0x09) Error while deserializing data")]
|
||||
DeserializeData,
|
||||
#[error("(0x0A) The file is inaccessible")]
|
||||
Inaccessible,
|
||||
#[error("(0x0B) Error while adding a key to the wkd")]
|
||||
AddingKey,
|
||||
#[error("(0x0C) Error while generating the wkd path")]
|
||||
PathGeneration,
|
||||
#[error("(0x0D) Error while generating the email")]
|
||||
MailGeneration,
|
||||
#[error("(0x0E) Wrong email domain")]
|
||||
WrongDomain,
|
||||
#[error("(0x0F) The requested file does not exist")]
|
||||
MissingFile,
|
||||
}
|
||||
|
||||
impl ResponseError for Error {
|
||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||
match self {
|
||||
Self::MissingPending => StatusCode::from_u16(404).unwrap(),
|
||||
Self::MissingKey => StatusCode::from_u16(404).unwrap(),
|
||||
Self::MissingFile => StatusCode::from_u16(404).unwrap(),
|
||||
Self::WrongDomain => StatusCode::from_u16(401).unwrap(),
|
||||
_ => StatusCode::from_u16(500).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
|
||||
match return_outcome(Err(&self.to_string())) {
|
||||
Ok(httpbuilder) => httpbuilder,
|
||||
Err(_) => HttpResponseBuilder::new(self.status_code()).body(self.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
143
backend/src/main.rs
Normal file
143
backend/src/main.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
mod confirmation;
|
||||
mod errors;
|
||||
mod management;
|
||||
mod settings;
|
||||
mod utils;
|
||||
|
||||
use crate::confirmation::{confirm_action, send_confirmation_email};
|
||||
use crate::errors::Error;
|
||||
use crate::management::{clean_stale, store_pending_addition, store_pending_deletion, Action};
|
||||
use crate::settings::{ROOT_FOLDER, SETTINGS};
|
||||
use crate::utils::{
|
||||
gen_random_token, get_email_from_cert, is_email_allowed, parse_pem, return_outcome,
|
||||
};
|
||||
|
||||
use actix_files::Files;
|
||||
use actix_web::http::header::ContentType;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{
|
||||
get, post, web, App, HttpRequest, HttpResponse, HttpResponseBuilder, HttpServer, Result,
|
||||
};
|
||||
use log::{debug, error, info};
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use tokio::{task, time};
|
||||
use utils::init_logger;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Key {
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Token {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Email {
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
if let Ok(value) = env::var("RUST_LOG") {
|
||||
env::set_var("RUST_LOG", format!("simple_wkd={}", value));
|
||||
}
|
||||
if init_logger().is_err() {
|
||||
error!("Could not set up logger!");
|
||||
panic!("Could not set up logger!")
|
||||
};
|
||||
fs::create_dir_all(pending_path!())?;
|
||||
task::spawn(async {
|
||||
let mut metronome = time::interval(time::Duration::from_secs(SETTINGS.cleanup_interval));
|
||||
loop {
|
||||
metronome.tick().await;
|
||||
info!("Running cleanup...");
|
||||
clean_stale(SETTINGS.max_age);
|
||||
info!("Cleanup completed!");
|
||||
}
|
||||
});
|
||||
info!(
|
||||
"Running server on http://localhost:{} (External URL: {})",
|
||||
SETTINGS.port, SETTINGS.external_url
|
||||
);
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.service(submit)
|
||||
.service(confirm)
|
||||
.service(delete)
|
||||
.service(
|
||||
Files::new("/.well-known", Path::new(&ROOT_FOLDER).join(".well-known"))
|
||||
.use_hidden_files(),
|
||||
)
|
||||
.route("/{filename:.*}", web::get().to(index))
|
||||
})
|
||||
.bind(("127.0.0.1", SETTINGS.port))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn index(req: HttpRequest) -> Result<HttpResponse, Error> {
|
||||
let path = webpage_path!().join(req.match_info().query("filename"));
|
||||
for file in &["", "index.html"] {
|
||||
let path = if file.is_empty() {
|
||||
path.to_owned()
|
||||
} else {
|
||||
path.join(file)
|
||||
};
|
||||
if path.is_file() {
|
||||
let template = match fs::read_to_string(&path) {
|
||||
Ok(template) => template,
|
||||
Err(_) => {
|
||||
debug!("File {} is inaccessible", path.display());
|
||||
return Err(Error::Inaccessible);
|
||||
}
|
||||
};
|
||||
let page = template.replace("((%u))", SETTINGS.external_url.as_ref());
|
||||
return Ok(HttpResponseBuilder::new(StatusCode::OK)
|
||||
.insert_header(ContentType::html())
|
||||
.body(page));
|
||||
}
|
||||
}
|
||||
debug!("File {} does not exist", path.display());
|
||||
Err(Error::MissingFile)
|
||||
}
|
||||
|
||||
#[post("/api/submit")]
|
||||
async fn submit(pem: web::Form<Key>) -> Result<HttpResponse, Error> {
|
||||
let cert = parse_pem(&pem.key)?;
|
||||
let email = get_email_from_cert(&cert)?;
|
||||
is_email_allowed(&email)?;
|
||||
let token = gen_random_token();
|
||||
store_pending_addition(pem.key.clone(), &email, &token)?;
|
||||
send_confirmation_email(&email, &Action::Add, &token)?;
|
||||
info!("User {} submitted a key!", &email);
|
||||
return_outcome(Ok("You submitted your key successfully!"))
|
||||
}
|
||||
|
||||
#[get("/api/confirm")]
|
||||
async fn confirm(token: web::Query<Token>) -> Result<HttpResponse, Error> {
|
||||
let (action, email) = confirm_action(&token.token)?;
|
||||
match action {
|
||||
Action::Add => {
|
||||
info!("Key for user {} was added successfully!", email);
|
||||
return_outcome(Ok("Your key was added successfully!"))
|
||||
}
|
||||
Action::Delete => {
|
||||
info!("Key for user {} was deleted successfully!", email);
|
||||
return_outcome(Ok("Your key was deleted successfully!"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/delete")]
|
||||
async fn delete(email: web::Query<Email>) -> Result<HttpResponse, Error> {
|
||||
let token = gen_random_token();
|
||||
store_pending_deletion(email.email.clone(), &token)?;
|
||||
send_confirmation_email(&email.email, &Action::Delete, &token)?;
|
||||
info!("User {} requested the deletion of his key!", email.email);
|
||||
return_outcome(Ok("You requested the deletion of your key successfully!"))
|
||||
}
|
140
backend/src/management.rs
Normal file
140
backend/src/management.rs
Normal file
|
@ -0,0 +1,140 @@
|
|||
use crate::pending_path;
|
||||
use crate::settings::ROOT_FOLDER;
|
||||
use crate::utils::{get_user_file_path, key_exists};
|
||||
use crate::{errors::Error, utils::get_filename};
|
||||
|
||||
use chrono::Utc;
|
||||
use log::{debug, error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Display, fs, path::Path};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub enum Action {
|
||||
Add,
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl Display for Action {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Pending {
|
||||
action: Action,
|
||||
data: String,
|
||||
timestamp: i64,
|
||||
}
|
||||
impl Pending {
|
||||
pub fn build_add(pem: String) -> Self {
|
||||
let timestamp = Utc::now().timestamp();
|
||||
Self {
|
||||
action: Action::Add,
|
||||
data: pem,
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
pub fn build_delete(email: String) -> Self {
|
||||
let timestamp = Utc::now().timestamp();
|
||||
Self {
|
||||
action: Action::Delete,
|
||||
data: email,
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
pub const fn action(&self) -> &Action {
|
||||
&self.action
|
||||
}
|
||||
pub fn data(&self) -> &str {
|
||||
&self.data
|
||||
}
|
||||
pub const fn timestamp(&self) -> i64 {
|
||||
self.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
fn store_pending(pending: &Pending, token: &str) -> Result<(), Error> {
|
||||
let serialized = match serde_json::to_string(pending) {
|
||||
Ok(serialized) => serialized,
|
||||
Err(_) => return Err(Error::SerializeData),
|
||||
};
|
||||
match fs::write(pending_path!().join(token), serialized) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(Error::Inaccessible),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store_pending_addition(pem: String, email: &str, token: &str) -> Result<(), Error> {
|
||||
let pending = Pending::build_add(pem);
|
||||
store_pending(&pending, token)?;
|
||||
debug!("Stored submission from {} with token {}", email, token);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn store_pending_deletion(email: String, token: &str) -> Result<(), Error> {
|
||||
if let Err(error) = key_exists(&email) {
|
||||
match error {
|
||||
Error::PathGeneration => debug!("Error while generating path for user {}", email),
|
||||
Error::MissingKey => debug!("There is no key for user {}", email),
|
||||
_ => error!("An unexpected error occoured!"),
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
let pending = Pending::build_delete(email.clone());
|
||||
store_pending(&pending, token)?;
|
||||
debug!(
|
||||
"Stored deletion request from {} with token {}",
|
||||
email, token
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clean_stale(max_age: i64) {
|
||||
for path in fs::read_dir(pending_path!()).unwrap().flatten() {
|
||||
let file_path = path.path();
|
||||
if file_path.is_file() {
|
||||
let content = match fs::read_to_string(&file_path) {
|
||||
Ok(content) => content,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"Could not read contents of token {} to string",
|
||||
get_filename(&file_path).unwrap()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let key = match serde_json::from_str::<Pending>(&content) {
|
||||
Ok(key) => key,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"Could not deserialize token {}",
|
||||
get_filename(&file_path).unwrap()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let now = Utc::now().timestamp();
|
||||
if now - key.timestamp() > max_age {
|
||||
if fs::remove_file(&file_path).is_err() {
|
||||
{
|
||||
warn!(
|
||||
"Could not delete stale token {}",
|
||||
get_filename(&file_path).unwrap()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
}
|
||||
debug!("Deleted stale token {}", get_filename(&file_path).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_key(email: &str) -> Result<(), Error> {
|
||||
let path = Path::new(&ROOT_FOLDER).join(get_user_file_path(email)?);
|
||||
match fs::remove_file(path) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(Error::Inaccessible),
|
||||
}
|
||||
}
|
96
backend/src/settings.rs
Normal file
96
backend/src/settings.rs
Normal file
|
@ -0,0 +1,96 @@
|
|||
use lettre::{transport::smtp::authentication::Credentials, SmtpTransport};
|
||||
use log::{debug, error, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use sequoia_net::wkd::Variant;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Settings {
|
||||
#[serde(with = "VariantDef")]
|
||||
pub variant: Variant,
|
||||
pub max_age: i64,
|
||||
pub cleanup_interval: u64,
|
||||
pub allowed_domains: Vec<String>,
|
||||
pub port: u16,
|
||||
pub external_url: Url,
|
||||
pub mail_settings: MailSettings,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MailSettings {
|
||||
pub smtp_host: String,
|
||||
pub smtp_username: String,
|
||||
pub smtp_password: String,
|
||||
pub smtp_port: u16,
|
||||
pub smtp_tls: SMTPEncryption,
|
||||
pub mail_from: String,
|
||||
pub mail_subject: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(remote = "Variant")]
|
||||
pub enum VariantDef {
|
||||
Advanced,
|
||||
Direct,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum SMTPEncryption {
|
||||
Tls,
|
||||
Starttls,
|
||||
}
|
||||
|
||||
fn get_settings() -> Settings {
|
||||
debug!("Reading settings...");
|
||||
let content = match fs::read_to_string("config.toml") {
|
||||
Ok(content) => content,
|
||||
Err(_) => {
|
||||
error!("Unable to access settings file!");
|
||||
panic!("Unable to access settings file!")
|
||||
}
|
||||
};
|
||||
let settings = match toml::from_str(&content) {
|
||||
Ok(settings) => settings,
|
||||
Err(_) => {
|
||||
error!("Unable to parse settings from file!");
|
||||
panic!("Unable to parse settings from file!")
|
||||
}
|
||||
};
|
||||
debug!("Successfully read setting!");
|
||||
settings
|
||||
}
|
||||
|
||||
fn get_mailer() -> SmtpTransport {
|
||||
debug!("Setting up SMTP...");
|
||||
let creds = Credentials::new(
|
||||
SETTINGS.mail_settings.smtp_username.to_owned(),
|
||||
SETTINGS.mail_settings.smtp_password.to_owned(),
|
||||
);
|
||||
let builder = match &SETTINGS.mail_settings.smtp_tls {
|
||||
SMTPEncryption::Tls => SmtpTransport::relay(&SETTINGS.mail_settings.smtp_host),
|
||||
SMTPEncryption::Starttls => {
|
||||
SmtpTransport::starttls_relay(&SETTINGS.mail_settings.smtp_host)
|
||||
}
|
||||
};
|
||||
let mailer = match builder {
|
||||
Ok(builder) => builder,
|
||||
Err(_) => {
|
||||
error!("Unable to set up smtp");
|
||||
panic!("Unable to set up smtp")
|
||||
}
|
||||
}
|
||||
.credentials(creds)
|
||||
.port(SETTINGS.mail_settings.smtp_port)
|
||||
.build();
|
||||
if mailer.test_connection().is_err() {
|
||||
warn!("Connection test to smtp host failed!");
|
||||
}
|
||||
debug!("SMTP setup successful!");
|
||||
mailer
|
||||
}
|
||||
|
||||
pub const ROOT_FOLDER: &str = "data";
|
||||
pub static SETTINGS: Lazy<Settings> = Lazy::new(get_settings);
|
||||
pub static MAILER: Lazy<SmtpTransport> = Lazy::new(get_mailer);
|
164
backend/src/utils.rs
Normal file
164
backend/src/utils.rs
Normal file
|
@ -0,0 +1,164 @@
|
|||
use crate::settings::SETTINGS;
|
||||
use crate::{errors::Error, settings::ROOT_FOLDER};
|
||||
|
||||
use actix_web::{
|
||||
http::{header::ContentType, StatusCode},
|
||||
HttpResponse, HttpResponseBuilder,
|
||||
};
|
||||
use flexi_logger::{style, DeferredNow, FileSpec, FlexiLoggerError, Logger, LoggerHandle, Record};
|
||||
use log::debug;
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use sequoia_net::wkd::Url;
|
||||
use sequoia_openpgp::{parse::Parse, policy::StandardPolicy, Cert};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! pending_path {
|
||||
() => {
|
||||
Path::new(&ROOT_FOLDER).join("pending")
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! webpage_path {
|
||||
() => {
|
||||
Path::new("assets").join("webpage")
|
||||
};
|
||||
}
|
||||
|
||||
pub fn is_email_allowed(email: &str) -> Result<(), Error> {
|
||||
let allowed = match email.split('@').last() {
|
||||
Some(domain) => SETTINGS.allowed_domains.contains(&domain.to_string()),
|
||||
None => return Err(Error::ParseEmail),
|
||||
};
|
||||
if !allowed {
|
||||
return Err(Error::WrongDomain);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_pem(pemfile: &str) -> Result<Cert, Error> {
|
||||
let cert = match sequoia_openpgp::Cert::from_bytes(pemfile.as_bytes()) {
|
||||
Ok(cert) => cert,
|
||||
Err(_) => return Err(Error::ParseCert),
|
||||
};
|
||||
let policy = StandardPolicy::new();
|
||||
if cert.with_policy(&policy, None).is_err() {
|
||||
return Err(Error::InvalidCert);
|
||||
};
|
||||
Ok(cert)
|
||||
}
|
||||
|
||||
pub fn gen_random_token() -> String {
|
||||
let mut rng = thread_rng();
|
||||
(0..10).map(|_| rng.sample(Alphanumeric) as char).collect()
|
||||
}
|
||||
|
||||
pub fn get_email_from_cert(cert: &Cert) -> Result<String, Error> {
|
||||
let policy = StandardPolicy::new();
|
||||
let validcert = match cert.with_policy(&policy, None) {
|
||||
Ok(validcert) => validcert,
|
||||
Err(_) => return Err(Error::InvalidCert),
|
||||
};
|
||||
let userid_opt = match validcert.primary_userid() {
|
||||
Ok(userid_opt) => userid_opt,
|
||||
Err(_) => return Err(Error::ParseCert),
|
||||
};
|
||||
let email_opt = match userid_opt.email() {
|
||||
Ok(email_opt) => email_opt,
|
||||
Err(_) => return Err(Error::ParseCert),
|
||||
};
|
||||
match email_opt {
|
||||
Some(email) => Ok(email),
|
||||
None => Err(Error::MissingMail),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_user_file_path(email: &str) -> Result<PathBuf, Error> {
|
||||
let wkd_url = match Url::from(email) {
|
||||
Ok(wkd_url) => wkd_url,
|
||||
Err(_) => return Err(Error::PathGeneration),
|
||||
};
|
||||
match wkd_url.to_file_path(SETTINGS.variant) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(_) => Err(Error::PathGeneration),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_exists(email: &str) -> Result<bool, Error> {
|
||||
let path = get_user_file_path(email)?;
|
||||
if !Path::new(&ROOT_FOLDER).join(path).is_file() {
|
||||
return Err(Error::MissingKey);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn get_filename(path: &Path) -> Option<&str> {
|
||||
path.file_name()?.to_str()
|
||||
}
|
||||
|
||||
pub fn custom_color_format(
|
||||
w: &mut dyn std::io::Write,
|
||||
now: &mut DeferredNow,
|
||||
record: &Record,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let level = record.level();
|
||||
write!(
|
||||
w,
|
||||
"[{}] [{}] {}: {}",
|
||||
style(level).paint(now.format("%Y-%m-%d %H:%M:%S").to_string()),
|
||||
style(level).paint(record.module_path().unwrap_or("<unnamed>")),
|
||||
style(level).paint(record.level().to_string()),
|
||||
style(level).paint(&record.args().to_string())
|
||||
)
|
||||
}
|
||||
|
||||
pub fn custom_monochrome_format(
|
||||
w: &mut dyn std::io::Write,
|
||||
now: &mut DeferredNow,
|
||||
record: &Record,
|
||||
) -> Result<(), std::io::Error> {
|
||||
write!(
|
||||
w,
|
||||
"[{}] [{}] {}: {}",
|
||||
now.format("%Y-%m-%d %H:%M:%S"),
|
||||
record.module_path().unwrap_or("<unnamed>"),
|
||||
record.level(),
|
||||
record.args()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn init_logger() -> Result<LoggerHandle, FlexiLoggerError> {
|
||||
Logger::try_with_env_or_str("simple_wkd=debug")?
|
||||
.log_to_file(FileSpec::default().directory("logs"))
|
||||
.duplicate_to_stdout(flexi_logger::Duplicate::All)
|
||||
.format_for_files(custom_monochrome_format)
|
||||
.adaptive_format_for_stdout(flexi_logger::AdaptiveFormat::Custom(
|
||||
custom_monochrome_format,
|
||||
custom_color_format,
|
||||
))
|
||||
.set_palette("b1;3;2;4;6".to_string())
|
||||
.start()
|
||||
}
|
||||
|
||||
pub fn return_outcome(data: Result<&str, &str>) -> Result<HttpResponse, Error> {
|
||||
let path = webpage_path!().join("status").join("index.html");
|
||||
let template = match fs::read_to_string(&path) {
|
||||
Ok(template) => template,
|
||||
Err(_) => {
|
||||
debug!("file {} is inaccessible", path.display());
|
||||
return Err(Error::Inaccessible);
|
||||
}
|
||||
};
|
||||
let (page, message) = match data {
|
||||
Ok(message) => (template.replace("((%s))", "Success!"), message),
|
||||
Err(message) => (template.replace("((%s))", "Failure!"), message),
|
||||
};
|
||||
let page = page.replace("((%m))", message);
|
||||
return Ok(HttpResponseBuilder::new(StatusCode::OK)
|
||||
.insert_header(ContentType::html())
|
||||
.body(page));
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue