Add configuration file, make HTTP/HTTPS agnostic

main
Elnu 2 years ago
parent 98dc007fa4
commit 4bfcf6631e

1
.gitignore vendored

@ -1,3 +1,4 @@
/target
*.db
nginx.conf
*.yaml

29
Cargo.lock generated

@ -1694,18 +1694,18 @@ checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1"
[[package]]
name = "serde"
version = "1.0.138"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47"
checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.138"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c"
checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da"
dependencies = [
"proc-macro2",
"quote",
@ -1735,6 +1735,19 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826f989c0f374733af6c286f4822f293bc738def07e2782dc1cbb899960a504a"
dependencies = [
"indexmap",
"itoa 1.0.2",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "servo_arc"
version = "0.1.1"
@ -1801,6 +1814,7 @@ dependencies = [
"actix-web",
"chrono",
"clap",
"derive_more",
"md5",
"reqwest",
"rusqlite",
@ -1808,6 +1822,7 @@ dependencies = [
"scraper",
"serde",
"serde_json",
"serde_yaml",
"validator",
]
@ -2058,6 +2073,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unsafe-libyaml"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dc1c637311091be28e5d462c07db78081e5828da80ba22605c81c4ad6f7f813"
[[package]]
name = "url"
version = "2.2.2"

@ -28,3 +28,5 @@ reqwest = "0.11.11"
scraper = "0.13.0"
sanitize_html = "0.7.0"
clap = { version = "3.2.14", features = ["derive"] }
serde_yaml = "0.9.2"
derive_more = "0.99.17"

@ -12,34 +12,47 @@ ElnuDev <elnu@elnu.com>
A Rust-based comment server using SQLite and an intuitive REST API.
USAGE:
soudan [OPTIONS] <SITES>...
soudan [OPTIONS] [CONFIG]
ARGS:
<SITES>... Set sites where comments will be posted
<CONFIG> Set configuration file [default: soudan.yaml]
OPTIONS:
-e, --email-required Require email for comment submissions
-h, --help Print help information
-n, --name-required Require name for comment submissions
-p, --port <PORT> Set port where HTTP requests will be received [default: 8080]
-t, --testing Run in testing mode, with in-memory database(s) and permissive CORS
policy
-V, --version Print version information
-h, --help Print help information
-p, --port <PORT> Set port where HTTP requests will be received [default: 8080]
-t, --testing Run in testing mode, with in-memory database(s) and permissive CORS policy
-V, --version Print version information
```
The sites must be prefixed with either `http://` or `https://`. Each site may only use one protocol, so the HTTP and HTTPS versions of each site are considered separate sites. You should redirect HTTPS to HTTP.
Soudan uses a YAML configuration file for most configuration options. By default, this configuration file is `soudan.yaml`, but you can override this by passing in the configuration file path to Soudan as an argument.
For example, to run Soudan on example.com and example.org on port 8081, use the following command. Each site will be stored completely separately, with its own database, `example.com.db` and `example.org.db`, respectively.
For example, to run Soudan on port 8081 with `test.yaml` as the configuration file, use the following command.
```SH
soudan -p 8081 https://example.com https://example.org
soudan -p 8081 test.yaml
```
In addition, you can add the `-t`/`--testing` flag to run Soudan in testing mode. In this mode, Soudan stores all comments in a fresh in-memory database, and **all comments will be lost once Soudan is closed.** In addition, a permissive CORS policy is used to make testing easier.
Finally, you can also add the `-n`/`--name-required` and/or `-e`/`--email-required` flags to prevent anonymous comments. Keep in mind that the JavaScript in [soudan.js](demo/soudan.js) assumes that these flags are not set, so you would need to manually add the `required` flag to the name and email `<input>` fields respectively.
### Configuration file
### Moderation
Heres an example configuration file:
```YAML
"localhost:3000":
file: databases/test.db
nameRequired: true
emailRequired: true
"localhost:5000": {}
```
Here, we have two sites, one hosted locally at port 3000 and one hosted locally at port 5000.
For the first site, we specify that we want the SQLite database file to be stored in `databases/test.db`, and we want the name and email fields of each comment to be required to prevent anonymous comments. (Keep in mind that the JavaScript in [soudan.js](demo/soudan.js) assumes that these flags are not set, so you would need to manually add the `required` flag to the name and email `<input>` fields respectively.)
For the second site, we can leave all the configuration fields as their defaults by giving an empty YAML object `{}`. If the database file path isnt provided, it will default to the domain plus the `.db` extension, so in this case it will be `localhost:3000.db` in the current directory. Name and email are not required by default.
## Moderation
Soudan does not have any spam filtering or moderation built in. However, going into the database manually to browse and remove comments is very easy. If you need an SQLite database browser, I'd recommend [DB Browser for SQLite](https://sqlitebrowser.org/).

@ -1,17 +1,41 @@
use crate::Comment;
use rusqlite::{params, Connection, Result};
use serde::Deserialize;
use std::fs;
use derive_more::From;
use std::path::PathBuf;
pub struct Database {
conn: Connection,
pub settings: DatabaseSettings,
}
#[derive(Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct DatabaseSettings {
pub name_required: bool,
pub email_required: bool,
pub file: Option<String>,
}
#[derive(From, Debug)]
pub enum DatabaseCreationError {
RusqliteError(rusqlite::Error),
IoError(std::io::Error),
}
impl Database {
pub fn new(testing: bool, name: &str) -> Result<Self> {
let name = name.replace("http://", "").replace("https://", "");
pub fn new(testing: bool, name: &str, settings: DatabaseSettings) -> Result<Self, DatabaseCreationError> {
let conn = if testing {
Connection::open_in_memory()
} else {
Connection::open(format!("{name}.db"))
let path = PathBuf::from(match &settings.file {
Some(path) => path.clone(),
None => format!("{name}.db"),
});
fs::create_dir_all(path.parent().unwrap())?;
Connection::open(path)
}?;
conn.execute(
"CREATE TABLE IF NOT EXISTS comment (
@ -25,7 +49,7 @@ impl Database {
)",
params![],
)?;
Ok(Self { conn })
Ok(Self { conn, settings })
}
pub fn get_comments(&self, content_id: &str) -> Result<Vec<Comment>> {

@ -2,10 +2,10 @@ mod comment;
pub use comment::*;
mod database;
pub use database::Database;
pub use database::*;
mod error;
pub use error::Error;
pub use error::*;
use actix_cors::Cors;
use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer};
@ -13,13 +13,13 @@ use clap::Parser;
use sanitize_html::{errors::SanitizeError, rules::predefined::DEFAULT, sanitize_str};
use scraper::{Html, Selector};
use serde::Deserialize;
use std::fs::File;
use std::sync::Mutex;
use std::{collections::HashMap, sync::MutexGuard};
use validator::Validate;
struct AppState {
databases: HashMap<String, Mutex<Database>>,
arguments: Arguments,
}
impl AppState {
@ -38,10 +38,14 @@ impl AppState {
}
}
fn trim_protocol(url: &str) -> String {
url.replace("http://", "").replace("https://", "")
}
fn get_request_origin(request: &HttpRequest) -> Option<String> {
match request.head().headers().get("Origin") {
Some(origin) => match origin.to_str() {
Ok(origin) => Some(origin.to_owned()),
Ok(origin) => Some(trim_protocol(origin)),
Err(_) => None,
},
None => None,
@ -51,6 +55,8 @@ fn get_request_origin(request: &HttpRequest) -> Option<String> {
#[derive(Default, Parser)]
#[clap(author, version, about)]
struct Arguments {
#[clap(default_value = "soudan.yaml", help = "Set configuration file")]
config: String,
#[clap(
short,
long,
@ -58,22 +64,12 @@ struct Arguments {
help = "Set port where HTTP requests will be received"
)]
port: u16,
#[clap(
required = true,
min_values = 1,
help = "Set sites where comments will be posted"
)]
sites: Vec<String>,
#[clap(
short,
long,
help = "Run in testing mode, with in-memory database(s) and permissive CORS policy"
)]
testing: bool,
#[clap(short, long, help = "Require name for comment submissions")]
name_required: bool,
#[clap(short, long, help = "Require email for comment submissions")]
email_required: bool,
}
async fn _get_comments(
@ -149,13 +145,7 @@ async fn _post_comment(
};
if comment.validate().is_err() {
return Err(Error::InvalidFields);
}
if comment.author.is_none() && data.arguments.name_required {
return Err(Error::NameRequired);
}
if comment.email.is_none() && data.arguments.email_required {
return Err(Error::EmailRequired);
}
}
let origin = match get_request_origin(&request) {
Some(origin) => origin,
None => return Err(Error::InvalidOrigin),
@ -166,7 +156,7 @@ async fn _post_comment(
// https://github.com/rust-lang/rust/issues/48594
'outer: loop {
for site_root in data.databases.keys() {
if site_root.starts_with(&origin) && url.starts_with(site_root) {
if site_root.eq(&origin) && trim_protocol(&url).starts_with(site_root) {
break 'outer;
}
}
@ -190,6 +180,12 @@ async fn _post_comment(
Ok(database) => database,
Err(err) => return Err(err),
};
if comment.author.is_none() && database.settings.name_required {
return Err(Error::NameRequired);
}
if comment.email.is_none() && database.settings.email_required {
return Err(Error::EmailRequired);
}
if let Some(parent) = comment.parent {
'outer2: loop {
match database.get_comments(&comment.content_id) {
@ -270,20 +266,21 @@ async fn get_page_data(url: &str) -> Result<Option<PageData>, reqwest::Error> {
}
#[actix_web::main]
async fn main() -> Result<(), std::io::Error> {
async fn main() -> std::io::Result<()> {
let arguments = Arguments::parse();
let database_settings: HashMap<String, DatabaseSettings> = match serde_yaml::from_reader(File::open(arguments.config)?) {
Ok(settings) => settings,
Err(_) => return Err(std::io::Error::new(std::io::ErrorKind::Other, "invalid config file")),
};
let mut databases = HashMap::new();
for domain in arguments.sites.iter() {
for (site, settings) in database_settings.iter() {
databases.insert(
domain.to_owned(),
Mutex::new(Database::new(arguments.testing, domain).unwrap()),
site.to_owned(),
Mutex::new(Database::new(arguments.testing, site, settings.clone()).unwrap()),
);
}
let port = arguments.port;
let state = web::Data::new(AppState {
databases,
arguments,
});
let state = web::Data::new(AppState { databases });
HttpServer::new(move || {
App::new()
.service(get_comments)

Loading…
Cancel
Save