Switch to sled

This commit is contained in:
Raphaël Thériault 2020-09-28 21:53:13 -04:00
parent 066231fbd1
commit 81c9246634
No known key found for this signature in database
GPG Key ID: D4E92B68275D389F
15 changed files with 331 additions and 1138 deletions

2
.gitignore vendored
View File

@ -4,5 +4,3 @@ target/
.idea/
filite.json
.env
*.db

686
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -18,19 +18,17 @@ license = "MIT"
anyhow = "1.0.32"
askama = "0.10.3"
base64 = "0.12.3"
chrono = { version = "0.4.13", features = ["serde"] }
bincode = "1.3.1"
chrono = { version = "0.4.18", features = ["serde"] }
futures = "0.3.5"
rand = "0.7.3"
rust-argon2 = "0.8.2"
serde = { version = "1.0.115", features = ["derive"] }
serde = { version = "1.0.116", features = ["derive"] }
serde_json = "1.0.57"
sqlx = { version = "0.4.0-beta.1", features = ["chrono", "macros", "migrate", "offline", "runtime-tokio", "sqlite"], default-features = false }
structopt = "0.3.16"
sled = "0.34.4"
structopt = "0.3.18"
tokio = { version = "0.2.22", features = ["blocking", "fs", "rt-threaded"] }
tracing = "0.1.19"
tracing-futures = "0.2.4"
tracing-subscriber = "0.2.11"
warp = { version = "0.2.4", features = ["tls"], default-features = false }
[target.'cfg(not(any(target_os = "windows", target_os = "macos")))'.dependencies]
openssl = { version = "*", features = ["vendored"] }
tracing-subscriber = "0.2.12"
warp = { version = "0.2.5", features = ["tls"], default-features = false }

View File

@ -6,7 +6,7 @@ COPY ./Cargo.toml ./Cargo.toml
COPY ./Cargo.lock ./Cargo.lock
RUN apk update \
&& apk add --no-cache make musl-dev perl \
&& apk add --no-cache musl-dev \
&& rm -rf /var/cache/apk/*
RUN cargo build --release

View File

@ -1,5 +0,0 @@
CREATE TABLE users (
id varchar(32) NOT NULL PRIMARY KEY,
password varchar(256) NOT NULL,
role int NOT NULL
);

View File

@ -1,11 +0,0 @@
CREATE TABLE filite (
id varchar(32) NOT NULL PRIMARY KEY,
ty int NOT NULL,
val text NOT NULL,
creator varchar(32) NOT NULL REFERENCES users(id),
created timestamp NOT NULL,
visibility int NOT NULL,
views int NOT NULL
);

View File

@ -1,353 +0,0 @@
{
"db": "SQLite",
"0fa193d147d714b9cbd4efc9f4aef2d0603089dfd0a5a1644db38b2383cb3d75": {
"query": "SELECT id, password, role as \"role: Role\" FROM users WHERE id = $1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "password",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "role: Role",
"ordinal": 2,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
}
},
"2939cc3de5feaa6024500b980f0bc425cf15ca0e08a47397d2ce56a2d8ca3aaf": {
"query": "SELECT id, ty as \"ty: Type\", val, creator, created FROM filite WHERE id = $1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "ty: Type",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "val",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "creator",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "created",
"ordinal": 4,
"type_info": "Datetime"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false
]
}
},
"2bded90766f48d62b0426cf9fe55477232add7cd45d15f613feada68fc11e80e": {
"query": "SELECT id FROM filite WHERE id = $1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
}
},
"5f0027d087a1932b987e697800210f9267b5730e002ef14bf79841ea790c3ab5": {
"query": "SELECT * FROM filite WHERE id = $1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "ty",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "val",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "creator",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "created",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "visibility",
"ordinal": 5,
"type_info": "Int64"
},
{
"name": "views",
"ordinal": 6,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
}
},
"79f48ed4fbf70048313a8ef5b665bc371ed16957e7e7186fb4817029931847ab": {
"query": "SELECT id, password, role FROM users WHERE id = $1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "password",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "role",
"ordinal": 2,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
}
},
"7fd8490787f6136611e1e30fc87177c7f6ec8ce072539efe6963155107458ba4": {
"query": "SELECT id, ty as \"ty: Type\", val, creator, created, visibility as \"visibility: Visibility\" FROM filite WHERE id = $1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "ty: Type",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "val",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "creator",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "created",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "visibility: Visibility",
"ordinal": 5,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false
]
}
},
"843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3": {
"query": "SELECT * FROM users WHERE id = $1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "password",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "role",
"ordinal": 2,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false
]
}
},
"a0cf6aac396dc7c11b86d885fdb5cbc78c40344ad134a83136d19189d5877538": {
"query": "SELECT id, ty as \"ty: Type\", val, creator, created, visibility as \"visibility: Visibility\", views FROM filite WHERE id = $1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "ty: Type",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "val",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "creator",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "created",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "visibility: Visibility",
"ordinal": 5,
"type_info": "Int64"
},
{
"name": "views",
"ordinal": 6,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
}
},
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855": {
"query": "",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
}
},
"e7baf3f2538368ddc02bf7712da5deaf4c19969d69181717033f9b29087ebf8b": {
"query": "SELECT id, ty as \"ty: Type\" FROM filite WHERE id = $1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "ty: Type",
"ordinal": 1,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false
]
}
},
"fac905277f1bd32c350c4fa7d3636ec79fb74c242f2a3bd4b4e52a10fcd0d0ae": {
"query": "UPDATE filite SET views = views + 1 WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
}
}
}

View File

@ -1,21 +1,21 @@
use crate::{
db,
db::models::User,
config::{Config, PasswordConfig},
db::{self, User},
reject::{self, TryExt},
};
use anyhow::Result;
use argon2::Config;
use rand::Rng;
use sqlx::SqlitePool;
use sled::Db;
use tokio::task;
use warp::{Filter, Rejection};
pub fn auth_optional(
pool: &'static SqlitePool,
db: &'static Db,
config: &'static Config,
) -> impl Filter<Extract = (Option<User>,), Error = Rejection> + Copy + Send + Sync + 'static {
warp::header::optional("Authorization").and_then(move |header| async move {
match header {
Some(h) => match user(h, pool).await {
Some(h) => match user(h, db, config).await {
Ok(u) => Ok(Some(u)),
Err(e) => Err(e),
},
@ -25,18 +25,19 @@ pub fn auth_optional(
}
pub fn auth_required(
pool: &'static SqlitePool,
db: &'static Db,
config: &'static Config,
) -> impl Filter<Extract = (User,), Error = Rejection> + Copy + Send + Sync + 'static {
warp::header::header("Authorization").and_then(move |header| user(header, pool))
warp::header::header("Authorization").and_then(move |header| user(header, db, config))
}
#[tracing::instrument(level = "debug")]
async fn user(header: String, pool: &SqlitePool) -> Result<User, Rejection> {
async fn user(header: String, db: &Db, config: &Config) -> Result<User, Rejection> {
if &header[..5] != "Basic" {
return Err(reject::unauthorized());
}
let decoded = base64::decode(&header[6..]).or_401()?;
let decoded = task::block_in_place(move || base64::decode(&header[6..])).or_401()?;
let (user, password) = {
let mut split = None;
@ -51,29 +52,49 @@ async fn user(header: String, pool: &SqlitePool) -> Result<User, Rejection> {
(std::str::from_utf8(u).or_401()?, p)
};
let user = db::user(user, pool).await.or_500()?.or_401()?;
if !verify(user.password.clone(), password.to_owned())
.await
.or_500()?
{
let user = db::user(user, db).or_500()?.or_401()?;
if !verify(&user.password_hash, password, &config.password).or_500()? {
return Err(reject::unauthorized());
}
Ok(user)
}
// TODO: Allow custom configuration
#[tracing::instrument(level = "debug", skip(password))]
async fn hash(password: Vec<u8>) -> Result<String> {
let config = Config::default();
Ok(task::spawn_blocking(move || {
let salt: [u8; 16] = rand::thread_rng().gen();
argon2::hash_encoded(&password, &salt[..], &config)
})
.await??)
fn hash(password: &[u8], config: &PasswordConfig) -> Result<String> {
let mut cfg = argon2::Config::default();
if let Some(hl) = config.hash_length {
cfg.hash_length = hl;
}
if let Some(l) = config.lanes {
cfg.lanes = l;
}
if let Some(mc) = config.memory_cost {
cfg.mem_cost = mc;
}
if let Some(tc) = config.time_cost {
cfg.time_cost = tc;
}
if let Some(s) = config.secret.as_ref().map(|s| s.as_bytes()) {
cfg.secret = s;
}
let hashed = task::block_in_place(move || {
let mut salt = vec![0; config.salt_length.unwrap_or(16)];
rand::thread_rng().fill(&mut salt[..]);
argon2::hash_encoded(password, &salt[..], &cfg)
})?;
Ok(hashed)
}
#[tracing::instrument(level = "debug", skip(encoded, password))]
async fn verify(encoded: String, password: Vec<u8>) -> Result<bool> {
Ok(task::spawn_blocking(move || argon2::verify_encoded(&encoded, &password)).await??)
fn verify(encoded: &str, password: &[u8], config: &PasswordConfig) -> Result<bool> {
let res = match &config.secret {
Some(s) => task::block_in_place(move || {
argon2::verify_encoded_ext(encoded, password, s.as_bytes(), &[])
})?,
None => task::block_in_place(move || argon2::verify_encoded(encoded, password))?,
};
Ok(res)
}

View File

@ -7,45 +7,6 @@ use std::{
path::{Path, PathBuf},
};
#[inline]
fn default_log_level() -> String {
"info,sqlx=warn".to_owned()
}
#[inline]
fn log_level_is_default(level: &str) -> bool {
level.to_lowercase() == default_log_level()
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Config {
pub port: u16,
pub database_url: String,
pub files_dir: PathBuf,
#[serde(skip_serializing_if = "log_level_is_default")]
pub log_level: String,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub pool: PoolConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub tls: Option<TlsConfig>,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub threads: ThreadsConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
port: 80,
database_url: "filite.db".to_owned(),
files_dir: PathBuf::from("files"),
log_level: default_log_level(),
tls: None,
pool: Default::default(),
threads: Default::default(),
}
}
}
pub fn read(path: impl AsRef<Path>) -> Result<&'static Config> {
let file = File::open(path)?;
let config: Config = serde_json::from_reader(BufReader::new(file))?;
@ -59,35 +20,128 @@ pub fn write(path: impl AsRef<Path>) -> Result<()> {
Ok(())
}
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Config {
pub port: u16,
pub database: DatabaseConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub tls: Option<TlsConfig>,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub runtime: RuntimeConfig,
#[serde(skip_serializing_if = "DefaultExt::is_default")]
pub password: PasswordConfig,
#[serde(skip_serializing_if = "Config::log_level_is_default")]
pub log_level: String,
}
impl Config {
#[inline]
fn default_log_level() -> String {
"info".to_owned()
}
#[inline]
fn log_level_is_default(level: &String) -> bool {
level.to_lowercase() == Self::default_log_level()
}
}
impl Default for Config {
#[inline]
fn default() -> Self {
Self {
port: 80,
database: Default::default(),
tls: None,
runtime: Default::default(),
password: Default::default(),
log_level: Self::default_log_level(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct TlsConfig {
pub cert: PathBuf,
pub key: PathBuf,
}
#[derive(Debug, Deserialize, Serialize, Default, PartialEq)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct PoolConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_connections: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_connections: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub connect_timeout: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub idle_timeout: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_lifetime: Option<u64>,
pub struct DatabaseConfig {
pub path: PathBuf,
#[serde(default, skip_serializing_if = "DefaultExt::is_default")]
pub mode: DatabaseMode,
#[serde(
default = "DatabaseConfig::default_cache_capacity",
skip_serializing_if = "DatabaseConfig::cache_capacity_is_default"
)]
pub cache_capacity: u64,
}
#[derive(Debug, Deserialize, Serialize, Default, PartialEq)]
impl DatabaseConfig {
#[inline]
fn default_cache_capacity() -> u64 {
1024 * 1024 * 1024
}
#[inline]
fn cache_capacity_is_default(cc: &u64) -> bool {
*cc == Self::default_cache_capacity()
}
}
impl Default for DatabaseConfig {
#[inline]
fn default() -> Self {
Self {
path: PathBuf::from("filite"),
mode: Default::default(),
cache_capacity: Self::default_cache_capacity(),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DatabaseMode {
Space,
Throughput,
}
impl From<DatabaseMode> for sled::Mode {
#[inline]
fn from(m: DatabaseMode) -> Self {
match m {
DatabaseMode::Space => Self::LowSpace,
DatabaseMode::Throughput => Self::HighThroughput,
}
}
}
impl Default for DatabaseMode {
#[inline]
fn default() -> Self {
Self::Space
}
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct ThreadsConfig {
pub struct RuntimeConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub core_threads: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_threads: Option<usize>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct PasswordConfig {
pub hash_length: Option<u32>,
pub salt_length: Option<usize>,
pub lanes: Option<u32>,
pub memory_cost: Option<u32>,
pub time_cost: Option<u32>,
pub secret: Option<String>,
}

34
src/db.rs Normal file
View File

@ -0,0 +1,34 @@
use crate::config::DatabaseConfig;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use sled::Db;
use tokio::task;
#[tracing::instrument(level = "debug")]
pub fn connect(config: &DatabaseConfig) -> Result<&'static Db> {
let db = sled::Config::default()
.path(&config.path)
.mode(config.mode.into())
.cache_capacity(config.cache_capacity)
.open()?;
Ok(Box::leak(Box::new(db)))
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct User {
pub admin: bool,
pub password_hash: String,
}
#[tracing::instrument(level = "debug", skip(db))]
pub fn user(id: &str, db: &Db) -> Result<Option<User>> {
task::block_in_place(move || {
let users = db.open_tree("users")?;
let bytes = match users.get(id)? {
Some(b) => b,
None => return Ok(None),
};
let user = bincode::deserialize(&bytes)?;
Ok(Some(user))
})
}

View File

@ -1,37 +0,0 @@
pub mod models;
pub mod pool;
use crate::db::models::{Filite, Role, Type, User, Visibility};
use anyhow::Result;
use sqlx::SqlitePool;
#[tracing::instrument(level = "debug")]
pub async fn user(id: &str, pool: &SqlitePool) -> Result<Option<User>> {
Ok(sqlx::query_as!(
User,
r#"SELECT id, password, role as "role: Role" FROM users WHERE id = $1"#,
id
)
.fetch_optional(pool)
.await?)
}
#[tracing::instrument(level = "debug")]
pub async fn filite(id: &str, view: bool, pool: &SqlitePool) -> Result<Option<Filite>> {
if !view
|| sqlx::query!("UPDATE filite SET views = views + 1 WHERE id = $1", id)
.fetch_optional(pool)
.await?
.is_some()
{
Ok(sqlx::query_as!(
Filite,
r#"SELECT id, ty as "ty: Type", val, creator, created, visibility as "visibility: Visibility", views FROM filite WHERE id = $1"#,
id
)
.fetch_optional(pool)
.await?)
} else {
Ok(None)
}
}

View File

@ -1,42 +0,0 @@
use chrono::NaiveDateTime;
pub struct User {
pub id: String,
pub password: String,
pub role: Role,
}
#[derive(sqlx::Type)]
#[repr(i64)]
pub enum Role {
User = 0,
Admin = 255,
}
pub struct Filite {
pub id: String,
pub ty: Type,
pub val: String,
pub creator: String,
pub created: NaiveDateTime,
pub visibility: Visibility,
pub views: i64,
}
#[derive(sqlx::Type)]
#[repr(i64)]
pub enum Type {
Fi = 0,
Li = 1,
Te = 2,
}
#[derive(sqlx::Type)]
#[repr(i64)]
pub enum Visibility {
Public = 0,
Protected = 1,
Private = 2,
}

View File

@ -1,34 +0,0 @@
use crate::config::Config;
use anyhow::Result;
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
use std::time::Duration;
#[tracing::instrument(level = "debug")]
pub async fn build(config: &Config) -> Result<&'static SqlitePool> {
let mut options: SqlitePoolOptions = Default::default();
if let Some(ms) = config.pool.max_connections {
options = options.max_connections(ms);
}
if let Some(ms) = config.pool.min_connections {
options = options.min_connections(ms);
}
if let Some(ct) = config.pool.connect_timeout {
options = options.connect_timeout(Duration::from_millis(ct));
}
if let Some(it) = config.pool.idle_timeout {
options = options.idle_timeout(Duration::from_millis(it));
}
if let Some(ml) = config.pool.max_lifetime {
options = options.max_lifetime(Duration::from_millis(ml));
}
let pool = options
.connect(&format!("sqlite://{}", config.database_url))
.await?;
sqlx::migrate!("./migrations").run(&pool).await?;
Ok(&*Box::leak(Box::new(pool)))
}

View File

@ -51,14 +51,15 @@ fn main() -> Result<(), Error> {
.with_span_events(FmtSpan::CLOSE)
.init();
let mut runtime = runtime::build(&config)?;
let db = db::connect(&config.database)?;
let mut runtime = runtime::build(&config.runtime)?;
runtime.block_on(run(config))?;
Ok(())
}
async fn run(config: &'static Config) -> Result<(), Error> {
let pool = db::pool::build(&config).await?;
Ok(())
}

View File

@ -1,13 +1,12 @@
use crate::config::Config;
use crate::config::RuntimeConfig;
use anyhow::Error;
use tokio::runtime::{Builder, Runtime};
#[tracing::instrument(level = "debug")]
pub fn build(config: &Config) -> Result<Runtime, Error> {
pub fn build(config: &RuntimeConfig) -> Result<Runtime, Error> {
let mut builder = Builder::new();
builder.threaded_scheduler().enable_all();
let config = &config.threads;
if let Some(ct) = config.core_threads {
builder.core_threads(ct);
}