Move to binary crate only, move to 'anyhow' for error handling.

This commit is contained in:
Anthony J. Martinez 2021-04-18 11:09:44 +02:00
parent 20accb8d66
commit 38e1337e08
11 changed files with 106 additions and 141 deletions

35
Cargo.lock generated
View File

@ -18,6 +18,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
[[package]]
name = "argon2"
version = "0.1.4"
@ -32,6 +38,7 @@ dependencies = [
name = "arse"
version = "0.4.0-alpha.1"
dependencies = [
"anyhow",
"argon2",
"clap",
"data-encoding",
@ -43,7 +50,6 @@ dependencies = [
"routerify",
"serde",
"simplelog",
"snafu",
"tempfile",
"tera",
"tokio",
@ -233,12 +239,6 @@ dependencies = [
"generic-array 0.14.4",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "fake-simd"
version = "0.1.2"
@ -964,27 +964,6 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
[[package]]
name = "snafu"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7"
dependencies = [
"doc-comment",
"snafu-derive",
]
[[package]]
name = "snafu-derive"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "socket2"
version = "0.4.0"

View File

@ -14,6 +14,7 @@ categories = ["command-line-utilities", "web-programming::http-server"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.40"
argon2 = "0.1"
clap = "2.33.3"
data-encoding = "2.3.2"
@ -25,7 +26,6 @@ rand = "0.8"
routerify = "2.0.0"
serde = { version = "1", features = ["derive"] }
simplelog = "0.10.0"
snafu = { version = "0.6.10", default-features = false, features = ["std"] }
tera = "1.7.0"
tokio = { version = "1", features = ["full"] }
toml = "0.5"

View File

@ -8,18 +8,12 @@ and flexible base for serving sites using:
* [routerify](https://crates.io/crates/routerify) to serve the site
* [simplecss](https://simplecss.org) for default styling
## Binary
## Usage
* Run an existing site given the path to its config TOML: `arse run config.toml`
* Create and run a new site from user input: `arse new`
* Logging verbosity can be increased with `-v` or `-vv`, the default level is `INFO`.
## Library
`arse` can be used as a library to extend functionality as the user sees fit.
Documentation can be found [here](https://docs.rs/arse/).
## Path to 1.0
- [x] Dynamic route handling

View File

@ -8,20 +8,23 @@ http://opensource.org/licenses/MIT>, at your option. This file may not be
copied, modified, or distributed except according to those terms.
*/
use std::usize;
use log::{debug, error};
use super::{Result, Error};
use super::{anyhow, Result};
/**
TODO Document
*/
pub fn generate_secret(len: usize) -> Result<String> {
pub(crate) fn generate_secret(len: usize) -> Result<String> {
use rand::{distributions::Alphanumeric, thread_rng, Rng};
let min = 32;
const MIN: usize = 32;
if len < min {
error!("Attempting to use password < 32ch.");
Err(Error::WeakSecret { min })
if len < MIN {
let msg = format!("Attempting to create secret with < {}ch", &MIN);
error!("{}", &msg);
Err(anyhow!("{}", msg))
} else {
let pass: String = thread_rng()
.sample_iter(&Alphanumeric)
@ -36,7 +39,7 @@ pub fn generate_secret(len: usize) -> Result<String> {
/**
TODO Document
*/
pub fn generate_argon2_phc(secret: &str) -> Result<String> {
pub(crate) fn generate_argon2_phc(secret: &str) -> Result<String> {
use argon2::{
password_hash::{PasswordHasher, SaltString},
Argon2,
@ -50,19 +53,21 @@ pub fn generate_argon2_phc(secret: &str) -> Result<String> {
match argon2.hash_password_simple(secret, salt.as_ref()) {
Ok(phc) => {
debug!("Created Argon2 PHC string");
let msg = "Created Argon2 PHC string";
debug!("{}", msg);
argon2_phc = Ok(phc.to_string());
}
Err(_) => {
error!("Failed to create Argon2 PHC");
argon2_phc = Err(Error::HasherError);
let msg = "Failed to create Argon2 PHC";
error!("{}", &msg);
argon2_phc = Err(anyhow!("{}", msg));
}
}
argon2_phc
}
pub use data_encoding::BASE32_NOPAD;
pub(crate) use data_encoding::BASE32_NOPAD;
#[cfg(test)]
mod tests {

View File

@ -15,8 +15,10 @@ use std::path::{Path, PathBuf};
use glob::glob;
use log::{debug, trace};
use super::{Context, Result};
#[cfg(target_family = "unix")]
pub fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<(), Box<dyn std::error::Error>> {
pub(crate) fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<()> {
debug!("Writing protected file: {}", &dest.as_ref().display());
use std::os::unix::fs::OpenOptionsExt;
let mut options = OpenOptions::new();
@ -25,25 +27,32 @@ pub fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<(), Box<
options.mode(0o600);
trace!("Opening '{}' to write", &dest.as_ref().display());
let mut ro_file = options.open(dest)?;
ro_file.write_all(content.as_bytes())?;
let mut ro_file = options.open(&dest)
.with_context(|| format!("failed to open '{}' for writing", &dest.as_ref().display()))?;
ro_file.write_all(content.as_bytes())
.with_context(|| format!("failure writing '{}'", &dest.as_ref().display()))?;
if !content.ends_with('\n') {
ro_file.write_all(b"\n")?;
ro_file.write_all(b"\n")
.with_context(|| format!("failure writing '{}'", &dest.as_ref().display()))?;
}
trace!("Content written to destination");
Ok(())
}
#[cfg(target_family = "windows")]
pub fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<(), Box<dyn std::error::Error>> {
pub fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<()> {
debug!("Writing protected file: {}", &dest.as_ref().display());
trace!("Opening '{}' to write", &dest.as_ref().display());
let mut ro_file = File::create(dest)?;
ro_file.write_all(content.as_bytes())?;
let metadata = secret_file.metadata()?;
let mut ro_file = File::create(&dest)
.with_context(|| format!("failed to open '{}' for writing", &dest.as_ref().display()))?;
ro_file.write_all(content.as_bytes()).
with_context(|| format!("failure writing '{}'", &dest.as_ref().display()))?;
let metadata = secret_file.metadata()
.with_context(|| format!("failure retrieving metadata on '{}'", &dest.as_ref().display()))?;
let mut perms = metadata.permissions();
if !content.ends_with('\n') {
ro_file.write_all(b"\n")?;
ro_file.write_all(b"\n")
.with_context("failure writing '{}'", &dest.as_ref().display())?;
}
trace!("Content written to destination");
trace!("Setting read-only on destination file");
@ -51,12 +60,13 @@ pub fn str_to_ro_file<P: AsRef<Path>>(content: &str, dest: P) -> Result<(), Box<
Ok(())
}
pub fn path_matches(pat: &str) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
pub fn path_matches(pat: &str) -> Result<Vec<PathBuf>> {
debug!("Building topic content vector from {}", &pat);
let mut path_vec: Vec<PathBuf> = Vec::new();
trace!("Globbing {}", &pat);
let entries = glob(pat)?;
let entries = glob(pat)
.context("failure globbing paths")?;
for entry in entries.filter_map(Result::ok) {
trace!("Adding '{}' to topic content vector", &entry.display());

View File

@ -19,6 +19,7 @@ use serde::{Serialize, Deserialize};
use super::auth;
use super::common;
use super::{anyhow, Context, Result};
fn args() -> App<'static, 'static> {
App::new("A Rust Site Engine")
@ -44,8 +45,8 @@ fn args() -> App<'static, 'static> {
/// TODO Document this public function
/// And Include an Example of its Use
pub fn load() -> Result<AppConfig, Box<dyn std::error::Error>> {
let config: Result<AppConfig, Box<dyn std::error::Error>>;
pub(crate) fn load() -> Result<AppConfig> {
let config: Result<AppConfig>;
let matches = args().get_matches();
// Create a Config with ISO timestamps
@ -55,9 +56,9 @@ pub fn load() -> Result<AppConfig, Box<dyn std::error::Error>> {
// After this block locking is configured at the specified level
match matches.occurrences_of("verbosity") {
0 => SimpleLogger::init(log::LevelFilter::Info, log_config)?,
1 => SimpleLogger::init(log::LevelFilter::Debug, log_config)?,
_ => SimpleLogger::init(log::LevelFilter::Trace, log_config)?,
0 => SimpleLogger::init(log::LevelFilter::Info, log_config).context("failed to initialize logger at level - INFO")?,
1 => SimpleLogger::init(log::LevelFilter::Debug, log_config).context("failed to initialize logger at level - DEBUG")?,
_ => SimpleLogger::init(log::LevelFilter::Trace, log_config).context("failed to initialize logger at level - TRACE")?,
}
info!("Logging started");
@ -70,18 +71,18 @@ pub fn load() -> Result<AppConfig, Box<dyn std::error::Error>> {
trace!("Application called with `new` subcommand - creating config from user input");
let reader = std::io::stdin();
let mut reader = reader.lock();
let current_path = std::env::current_dir()?;
let current_path = std::env::current_dir().context("failed to get current working directory")?;
config = AppConfig::generate(current_path, &mut reader);
} else {
let msg = "Unable to load configuration".to_owned();
error!("{}", &msg);
config = Err(From::from(msg));
config = Err(anyhow!("{}", msg));
}
config
}
fn runner_config(m: ArgMatches) -> Result<AppConfig, Box<dyn std::error::Error>> {
fn runner_config(m: ArgMatches) -> Result<AppConfig> {
if let Some(run) = m.subcommand_matches("run") {
let value = run.value_of("config").unwrap();
let config = AppConfig::from_path(value)?;
@ -89,14 +90,15 @@ fn runner_config(m: ArgMatches) -> Result<AppConfig, Box<dyn std::error::Error>>
} else {
let msg = "Failed to read arguments for 'run' subcommand".to_owned();
error!("{}", &msg);
Err(From::from(msg))
Err(anyhow!("{}", msg))
}
}
fn get_input<R: BufRead>(prompt: &str, reader: &mut R) -> Result<String, Box<dyn std::error::Error>> {
fn get_input<R: BufRead>(prompt: &str, reader: &mut R) -> Result<String> {
let mut buf = String::new();
println!("{}", prompt);
reader.read_line(&mut buf)?;
reader.read_line(&mut buf)
.context("failed reading input from user")?;
let buf = String::from(buf
.trim_start_matches(char::is_whitespace)
.trim_end_matches(char::is_whitespace));
@ -117,14 +119,14 @@ fn csv_to_vec(csv: &str) -> Vec<String> {
/// TODO Document
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct Site {
pub(crate) struct Site {
pub name: String,
pub author: String,
pub topics: Vec<String>,
}
impl Site {
pub fn new_from_input<R: BufRead>(reader: &mut R) -> Result<Site, Box<dyn std::error::Error>> {
pub(crate) fn new_from_input<R: BufRead>(reader: &mut R) -> Result<Site> {
let name = get_input("Please enter a name for the site: ", reader)?;
let author = get_input("Please enter the site author's name: ", reader)?;
let topics = get_input("Please enter comma-separated site topics: ", reader)?;
@ -138,14 +140,14 @@ impl Site {
/// TODO Document
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct Credentials {
pub(crate) struct Credentials {
pub user: String,
pub password: String,
pub token: String,
}
impl Credentials {
pub fn new_from_input<P: AsRef<Path>, R: BufRead>(dir: P, reader: &mut R) -> Result<Credentials, Box<dyn std::error::Error>> {
pub(crate) fn new_from_input<P: AsRef<Path>, R: BufRead>(dir: P, reader: &mut R) -> Result<Credentials> {
let user = get_input("Please enter an username for the site admin: ", reader)?;
const PASSWORD_LEN: usize = 32;
let password = auth::generate_secret(PASSWORD_LEN)?;
@ -170,13 +172,13 @@ impl Credentials {
/// TODO Document
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct DocPaths {
pub(crate) struct DocPaths {
pub templates: String,
pub webroot: String,
}
impl DocPaths {
pub fn new<P: AsRef<Path>>(dir: P) -> Result<DocPaths, Box<dyn std::error::Error>> {
pub(crate) fn new<P: AsRef<Path>>(dir: P) -> DocPaths {
debug!("Creating site DocPaths");
let dir = dir.as_ref().display();
let templates = format!("{}/site/templates", dir);
@ -184,32 +186,34 @@ impl DocPaths {
let docpaths = DocPaths { templates, webroot };
trace!("Site DocPaths: {:?}", docpaths);
Ok(docpaths)
docpaths
}
}
/// TODO Document
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct AppConfig {
pub(crate) struct AppConfig {
pub site: Site,
pub creds: Credentials,
pub docpaths: DocPaths,
}
impl AppConfig {
pub fn from_path<T: AsRef<Path>>(config: T) -> Result<AppConfig, Box<dyn std::error::Error>> {
pub(crate) fn from_path<T: AsRef<Path>>(config: T) -> Result<AppConfig> {
debug!("Loading site configuration from {}", &config.as_ref().display());
let config = std::fs::read_to_string(config)?;
let config_string = std::fs::read_to_string(&config)
.with_context(|| format!("failed reading '{}' to string", &config.as_ref().display()))?;
trace!("Parsing configuration TOML");
let app_config: AppConfig = toml::from_str(&config)?;
let app_config: AppConfig = toml::from_str(&config_string)
.context("failed to parse TOML")?;
Ok(app_config)
}
pub fn generate<P: AsRef<Path>, R: BufRead>(dir: P, reader: &mut R) -> Result<AppConfig, Box<dyn std::error::Error>> {
pub(crate) fn generate<P: AsRef<Path>, R: BufRead>(dir: P, reader: &mut R) -> Result<AppConfig> {
debug!("Generating new site configuration");
let docpaths = DocPaths::new(&dir)?;
let docpaths = DocPaths::new(&dir);
let site = Site::new_from_input(reader)?;
let creds = Credentials::new_from_input(&dir, reader)?;
@ -219,13 +223,15 @@ impl AppConfig {
docpaths,
};
config.create_paths()?;
config.write(&dir)?;
config.create_paths()
.context("failed while creating site paths")?;
config.write(&dir)
.context("failed to write site config to disk")?;
Ok(config)
}
fn create_paths(&self) -> Result<(), Box<dyn std::error::Error>> {
fn create_paths(&self) -> Result<()> {
debug!("Creating site filesystem tree");
create_dir_all(&self.docpaths.templates)?;
create_dir_all(format!("{}/static/ext", &self.docpaths.webroot))?;
@ -241,9 +247,9 @@ impl AppConfig {
Ok(())
}
fn write<P: AsRef<Path>>(&self, dir: P) -> Result<(), Box<dyn std::error::Error>> {
fn write<P: AsRef<Path>>(&self, dir: P) -> Result<()> {
debug!("Writing site configuration to disk");
let config = toml::to_string_pretty(&self)?;
let config = toml::to_string_pretty(&self).context("failure creating TOML")?;
let conf_path = &dir.as_ref().join("config.toml");
common::str_to_ro_file(&config, &conf_path)?;
Ok(())

View File

@ -1,21 +0,0 @@
/*
A Rust Site Engine
Copyright 2020-2021 Anthony Martinez
Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
http://opensource.org/licenses/MIT>, at your option. This file may not be
copied, modified, or distributed except according to those terms.
*/
use snafu::Snafu;
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("Attempted to generate a secret with less than {} characters", min))]
WeakSecret { min: usize },
#[snafu(display("Failed to create Argon2 PHC"))]
HasherError,
}

View File

@ -1,19 +0,0 @@
/*
A Rust Site Engine
Copyright 2020-2021 Anthony Martinez
Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
http://opensource.org/licenses/MIT>, at your option. This file may not be
copied, modified, or distributed except according to those terms.
*/
pub mod auth;
pub mod config;
pub mod common;
pub mod render;
pub mod routes;
pub mod errors;
pub type Error = crate::errors::Error;
pub type Result<T, E = Error> = std::result::Result<T, E>;

View File

@ -11,13 +11,19 @@ copied, modified, or distributed except according to those terms.
use std::net::SocketAddr;
use std::sync::Arc;
use arse::*;
use anyhow::{anyhow, Context, Result};
use log::{info, error};
use hyper::Server;
use routerify::RouterService;
mod auth;
mod config;
mod common;
mod render;
mod routes;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn main() -> Result<()> {
let config = config::load()?;
let app = Arc::new(config);
info!("Configuration loaded");

View File

@ -13,15 +13,17 @@ use std::sync::Arc;
use super::config::AppConfig;
use super::common;
use super::{Context, Result};
use log::{debug, trace};
use pulldown_cmark::{Parser, html};
use tera::{Tera, Context};
use tera::Tera;
use tera::Context as TemplateContext;
mod default;
#[derive(Debug)]
pub struct Engine {
pub(crate) struct Engine {
pub app: Arc<AppConfig>,
pub topic_slug: String,
pub template: String,
@ -29,7 +31,7 @@ pub struct Engine {
}
impl Engine {
pub fn new(a: Arc<AppConfig>, ts: &str, tmpl: &str, inst: Tera) -> Engine {
pub(crate) fn new(a: Arc<AppConfig>, ts: &str, tmpl: &str, inst: Tera) -> Engine {
trace!("Loading rendering engine");
Engine {
app: a,
@ -40,27 +42,29 @@ impl Engine {
}
}
pub(crate) fn load_default_template() -> Result<Tera, Box<dyn std::error::Error>> {
pub(crate) fn load_default_template() -> Result<Tera> {
trace!("Loading default rendering template");
let mut tera = Tera::default();
tera.add_raw_template("default.tmpl", default::TEMPLATE)?;
tera.add_raw_template("default.tmpl", default::TEMPLATE)
.context("failure adding default template")?;
Ok(tera)
}
pub(crate) fn render_topic(engine: Engine) -> Result<String, Box<dyn std::error::Error>> {
pub(crate) fn render_topic(engine: Engine) -> Result<String> {
debug!("Rendering topic: '{}'", &engine.topic_slug);
let site = &engine.app.site;
let topic_data = load_topic(&engine)?;
let mut context = Context::new();
let mut context = TemplateContext::new();
context.insert("site", site);
context.insert("posts", &topic_data);
let output = engine.instance.render(&engine.template, &context)?;
let output = engine.instance.render(&engine.template, &context)
.with_context(|| format!("failed rendering topic: {}", &engine.topic_slug))?;
trace!("Rendered content for topic: {}\n{}", &engine.topic_slug, output);
Ok(output)
}
fn load_topic(engine: &Engine) -> Result<Vec<String>, Box<dyn std::error::Error>> {
fn load_topic(engine: &Engine) -> Result<Vec<String>> {
trace!("Loading topic content for '{}'", &engine.topic_slug);
let topic_path = Path::new(&engine.app.docpaths.webroot).join(&engine.topic_slug).join("posts");
let pat = format!("{}/*.md", topic_path.display());
@ -68,12 +72,13 @@ fn load_topic(engine: &Engine) -> Result<Vec<String>, Box<dyn std::error::Error>
read_to_html(paths)
}
fn read_to_html(paths: Vec<PathBuf>) -> Result<Vec<String>, Box<dyn std::error::Error>> {
fn read_to_html(paths: Vec<PathBuf>) -> Result<Vec<String>> {
debug!("Rendering Markdown to HTML");
let mut contents: Vec<String> = Vec::new();
for path in paths {
trace!("Rendering {} to HTML", &path.display());
let buf = std::fs::read_to_string(path)?;
let buf = std::fs::read_to_string(&path)
.with_context(|| format!("failure reading '{}' to string", &path.display()))?;
let parser = Parser::new(&buf);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);

View File

@ -22,7 +22,7 @@ use super::config::AppConfig;
use super::render;
pub fn router(app: Arc<AppConfig>) -> Router<Body, Infallible> {
pub(crate) fn router(app: Arc<AppConfig>) -> Router<Body, Infallible> {
debug!("Building site router");
Router::builder()
.data(app)