Refactor, fixes and first template

This commit is contained in:
Raphaël Thériault 2020-10-01 23:33:54 -04:00
parent daef6e9b94
commit 820a82bd60
No known key found for this signature in database
GPG Key ID: D4E92B68275D389F
7 changed files with 219 additions and 195 deletions

View File

@ -10,44 +10,38 @@ use sled::Db;
use tokio::task;
use warp::{http::HeaderValue, Filter, Rejection};
pub fn optional(
db: &'static Db,
config: &'static Config,
) -> impl Filter<Extract = (Option<User>,), Error = Rejection> + Copy + Send + Sync + 'static {
warp::header::value("Authorization")
.and_then(move |header| async move {
match user(header, db, config) {
Ok(u) => Ok(Some(u)),
Err(e) => Err(e),
}
})
.or(warp::any().and_then(|| async move { Result::<_, Rejection>::Ok(None) }))
.unify()
}
pub fn required(
pub fn auth(
db: &'static Db,
config: &'static Config,
) -> impl Filter<Extract = (User,), Error = Rejection> + Copy + Send + Sync + 'static {
warp::header::value("Authorization")
.and_then(move |header| async move { user(header, db, config) })
.or(warp::any().and_then(|| async move {
Result::<HeaderValue, Rejection>::Err(crate::reject::unauthorized(
"Authentication Required",
))
}))
.unify()
.and_then(move |header| user(header, db, config))
}
#[tracing::instrument(level = "debug", skip(db))]
fn user(header: HeaderValue, db: &Db, config: &Config) -> Result<User, Rejection> {
let credentials = Basic::decode(&header).or_400()?;
async fn user(header: HeaderValue, db: &Db, config: &Config) -> Result<User, Rejection> {
let credentials = Basic::decode(&header).or_bad_request("Invalid Credentials")?;
let user = crate::db::user(credentials.username(), db)
.or_500()?
.or_401()?;
if !verify(
&user.password_hash,
credentials.password().as_bytes(),
&config.password,
)
.or_500()?
{
return Err(crate::reject::unauthorized());
.or_unauthorized("Invalid Credentials")?;
let valid = !task::block_in_place(|| {
verify(
&user.password_hash,
credentials.password().as_bytes(),
&config.password,
)
})
.or_500()?;
if !valid {
return Err(crate::reject::unauthorized("Invalid Credentials"));
}
Ok(user)
@ -56,10 +50,8 @@ fn user(header: HeaderValue, db: &Db, config: &Config) -> Result<User, Rejection
#[tracing::instrument(level = "debug", skip(encoded, password))]
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))?,
Some(s) => argon2::verify_encoded_ext(encoded, password, s.as_bytes(), &[])?,
None => argon2::verify_encoded(encoded, password)?,
};
Ok(res)
}

161
src/db.rs
View File

@ -5,7 +5,6 @@ use rand::{distributions::Alphanumeric, Rng};
use serde::{Deserialize, Serialize};
use sled::Db;
use std::fmt;
use tokio::task;
#[tracing::instrument(level = "debug")]
pub fn connect(config: &DatabaseConfig) -> Result<&'static Db> {
@ -33,17 +32,7 @@ pub enum FiliteInner {
}
#[tracing::instrument(level = "debug", skip(db))]
pub fn filite(id: &str, inc: bool, db: &Db) -> Result<Option<Filite>> {
task::block_in_place(move || {
if inc {
filite_inc(id, db)
} else {
filite_noinc(id, db)
}
})
}
fn filite_inc(id: &str, db: &Db) -> Result<Option<Filite>> {
pub fn get(id: &str, db: &Db) -> Result<Option<Filite>> {
macro_rules! tryy {
($op:expr, $default:expr, $val:ident) => {
match $op {
@ -71,25 +60,14 @@ fn filite_inc(id: &str, db: &Db) -> Result<Option<Filite>> {
filite
}
fn filite_noinc(id: &str, db: &Db) -> Result<Option<Filite>> {
let bytes = match db.get(id)? {
Some(b) => b,
None => return Ok(None),
};
let filite = bincode::deserialize(&bytes)?;
Ok(Some(filite))
}
fn insert_filite(id: &str, filite: Filite, db: &Db) -> Result<Option<Filite>> {
task::block_in_place(move || {
if db.contains_key(id)? {
return Ok(None);
}
if db.contains_key(id)? {
return Ok(None);
}
let bytes = bincode::serialize(&filite)?;
db.insert(id, bytes)?;
Ok(Some(filite))
})
let bytes = bincode::serialize(&filite)?;
db.insert(id, bytes)?;
Ok(Some(filite))
}
#[tracing::instrument(level = "debug", skip(db))]
@ -142,34 +120,33 @@ pub fn insert_text(id: &str, owner: String, data: String, db: &Db) -> Result<Opt
#[tracing::instrument(level = "debug", skip(db))]
pub fn delete_filite(id: &str, user: &User, db: &Db) -> Result<Option<Filite>> {
task::block_in_place(move || match filite_noinc(id, db)? {
Some(f) => {
if user.admin || f.owner == user.id {
db.remove(id)?;
Ok(Some(f))
} else {
Ok(None)
}
}
None => Ok(None),
})
let bytes = match db.get(id)? {
Some(b) => b,
None => return Ok(None),
};
let filite: Filite = bincode::deserialize(&bytes)?;
if user.admin || filite.owner == user.id {
db.remove(id)?;
Ok(Some(filite))
} else {
Ok(None)
}
}
#[tracing::instrument(level = "debug", skip(db))]
pub fn random_id(length: usize, db: &Db) -> Result<String> {
task::block_in_place(move || {
let mut id;
loop {
id = rand::thread_rng()
.sample_iter(Alphanumeric)
.take(length)
.collect();
let mut id;
loop {
id = rand::thread_rng()
.sample_iter(Alphanumeric)
.take(length)
.collect();
if !db.contains_key(&id)? {
break Ok(id);
}
if !db.contains_key(&id)? {
break Ok(id);
}
})
}
}
#[derive(Clone, Deserialize, Serialize)]
@ -196,19 +173,17 @@ struct DbUser {
#[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: DbUser = bincode::deserialize(&bytes)?;
Ok(Some(User {
id: id.to_owned(),
admin: user.admin,
password_hash: user.password_hash,
}))
})
let users = db.open_tree("users")?;
let bytes = match users.get(id)? {
Some(b) => b,
None => return Ok(None),
};
let user: DbUser = bincode::deserialize(&bytes)?;
Ok(Some(User {
id: id.to_owned(),
admin: user.admin,
password_hash: user.password_hash,
}))
}
#[tracing::instrument(level = "debug", skip(password, db))]
@ -219,42 +194,38 @@ pub fn insert_user(
db: &Db,
config: &Config,
) -> Result<Option<User>> {
task::block_in_place(move || {
let users = db.open_tree("users")?;
if users.contains_key(id)? {
return Ok(None);
}
let users = db.open_tree("users")?;
if users.contains_key(id)? {
return Ok(None);
}
let password_hash = crate::auth::hash(password.as_bytes(), &config.password)?;
let user = DbUser {
admin,
password_hash,
};
let password_hash = crate::auth::hash(password.as_bytes(), &config.password)?;
let user = DbUser {
admin,
password_hash,
};
let bytes = bincode::serialize(&user)?;
users.insert(id, bytes)?;
Ok(Some(User {
id: id.to_owned(),
admin: user.admin,
password_hash: user.password_hash,
}))
})
let bytes = bincode::serialize(&user)?;
users.insert(id, bytes)?;
Ok(Some(User {
id: id.to_owned(),
admin: user.admin,
password_hash: user.password_hash,
}))
}
#[tracing::instrument(level = "debug", skip(db))]
pub fn delete_user(id: &str, db: &Db) -> Result<Option<User>> {
task::block_in_place(move || {
let users = db.open_tree("users")?;
match users.remove(id)? {
Some(b) => {
let user: DbUser = bincode::deserialize(&b)?;
Ok(Some(User {
id: id.to_owned(),
admin: user.admin,
password_hash: user.password_hash,
}))
}
None => Ok(None),
let users = db.open_tree("users")?;
match users.remove(id)? {
Some(b) => {
let user: DbUser = bincode::deserialize(&b)?;
Ok(Some(User {
id: id.to_owned(),
admin: user.admin,
password_hash: user.password_hash,
}))
}
})
None => Ok(None),
}
}

View File

@ -7,27 +7,21 @@ use warp::{
#[derive(Debug, Clone)]
enum FiliteRejection {
BadRequest,
Unauthorized,
BadRequest(String),
Unauthorized(String),
NotFound,
Conflict,
InternalServerError,
Custom(String, StatusCode),
}
impl Reject for FiliteRejection {}
impl Reply for FiliteRejection {
fn into_response(self) -> Response {
match self {
Self::BadRequest => {
warp::reply::with_status("Bad Request", StatusCode::BAD_REQUEST).into_response()
Self::BadRequest(reply) => {
warp::reply::with_status(reply, StatusCode::BAD_REQUEST).into_response()
}
Self::Unauthorized => warp::reply::with_status(
warp::reply::with_header(
"Unauthorized",
"WWW-Authenticate",
r#"Basic realm="filite""#,
),
Self::Unauthorized(reply) => warp::reply::with_status(
warp::reply::with_header(reply, "WWW-Authenticate", r#"Basic realm="filite""#),
StatusCode::UNAUTHORIZED,
)
.into_response(),
@ -41,44 +35,28 @@ impl Reply for FiliteRejection {
warp::reply::with_status("Internal Server Error", StatusCode::INTERNAL_SERVER_ERROR)
.into_response()
}
Self::Custom(reply, status) => warp::reply::with_status(reply, status).into_response(),
}
}
}
#[inline]
pub fn unauthorized() -> Rejection {
warp::reject::custom(FiliteRejection::Unauthorized)
pub fn bad_request(reply: impl ToString) -> Rejection {
warp::reject::custom(FiliteRejection::BadRequest(reply.to_string()))
}
#[inline]
pub fn custom<T: ToString>(reply: T, status: StatusCode) -> Rejection {
warp::reject::custom(FiliteRejection::Custom(reply.to_string(), status))
pub fn unauthorized(reply: impl ToString) -> Rejection {
warp::reject::custom(FiliteRejection::Unauthorized(reply.to_string()))
}
pub trait TryExt<T> {
fn or_400(self) -> Result<T, Rejection>;
fn or_401(self) -> Result<T, Rejection>;
fn or_404(self) -> Result<T, Rejection>;
fn or_409(self) -> Result<T, Rejection>;
fn or_500(self) -> Result<T, Rejection>;
fn or_bad_request(self, reply: impl ToString) -> Result<T, Rejection>;
fn or_unauthorized(self, reply: impl ToString) -> Result<T, Rejection>;
}
impl<T, E: Display> TryExt<T> for Result<T, E> {
fn or_400(self) -> Result<T, Rejection> {
self.map_err(|e| {
tracing::info!("{}", e);
warp::reject::custom(FiliteRejection::BadRequest)
})
}
fn or_401(self) -> Result<T, Rejection> {
self.map_err(|e| {
tracing::info!("{}", e);
warp::reject::custom(FiliteRejection::Unauthorized)
})
}
fn or_404(self) -> Result<T, Rejection> {
self.map_err(|e| {
tracing::info!("{}", e);
@ -98,15 +76,22 @@ impl<T, E: Display> TryExt<T> for Result<T, E> {
warp::reject::custom(FiliteRejection::InternalServerError)
})
}
fn or_bad_request(self, reply: impl ToString) -> Result<T, Rejection> {
self.map_err(|e| {
tracing::info!("{}", e);
bad_request(reply)
})
}
fn or_unauthorized(self, reply: impl ToString) -> Result<T, Rejection> {
self.map_err(|e| {
tracing::info!("{}", e);
unauthorized(reply)
})
}
}
impl<T> TryExt<T> for Option<T> {
fn or_400(self) -> Result<T, Rejection> {
self.ok_or_else(|| warp::reject::custom(FiliteRejection::BadRequest))
}
fn or_401(self) -> Result<T, Rejection> {
self.ok_or_else(|| warp::reject::custom(FiliteRejection::Unauthorized))
}
fn or_404(self) -> Result<T, Rejection> {
self.ok_or_else(|| warp::reject::custom(FiliteRejection::NotFound))
}
@ -117,6 +102,13 @@ impl<T> TryExt<T> for Option<T> {
fn or_500(self) -> Result<T, Rejection> {
self.ok_or_else(|| warp::reject::custom(FiliteRejection::InternalServerError))
}
fn or_bad_request(self, reply: impl ToString) -> Result<T, Rejection> {
self.ok_or_else(move || bad_request(reply))
}
fn or_unauthorized(self, reply: impl ToString) -> Result<T, Rejection> {
self.ok_or_else(move || unauthorized(reply))
}
}
#[tracing::instrument(level = "debug")]

View File

@ -3,8 +3,10 @@ use crate::{
db::{Filite, FiliteInner, User},
reject::TryExt,
};
use askama::Template;
use bytes::Bytes;
use sled::Db;
use tokio::task;
use warp::{
http::{StatusCode, Uri},
reply::{Reply, Response},
@ -15,59 +17,90 @@ pub fn handler(
config: &'static Config,
db: &'static Db,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Copy + Send + Sync + 'static {
let filite = warp::path!(String)
let spectre = warp::path!("spectre.css").map(|| {
warp::reply::with_header(
include_str!("../static/spectre.min.css"),
"Content-Type",
"text/css",
)
});
let index = warp::path::end()
.and(crate::auth::auth(db, config))
.and_then(index);
let get = warp::path!(String)
.and(warp::get())
.and_then(move |id| filite(id, db));
.and_then(move |id| get(id, db));
let post_file = warp::path!("f")
.and(warp::post())
.and(crate::auth::required(db, config))
.and(crate::auth::auth(db, config))
.and(warp::body::bytes())
.and(warp::header::optional("Content-Type"))
.and(warp::header::optional("X-ID-Length"))
.and_then(move |user, data, mime, len| post_file(user, data, mime, len, db));
let put_file = warp::path!("f" / String)
.and(warp::put())
.and(crate::auth::required(db, config))
.and(crate::auth::auth(db, config))
.and(warp::body::bytes())
.and(warp::header::optional("Content-Type"))
.and_then(move |id, user, data, mime| put_file(id, user, data, mime, db));
let post_link = warp::path!("l")
.and(warp::post())
.and(crate::auth::required(db, config))
.and(crate::auth::auth(db, config))
.and(crate::util::body())
.and(warp::header::optional("X-ID-Length"))
.and_then(move |user, location, len| post_link(user, location, len, db));
let put_link = warp::path!("l" / String)
.and(warp::put())
.and(crate::auth::required(db, config))
.and(crate::auth::auth(db, config))
.and(crate::util::body())
.and_then(move |id, user, location| put_link(id, user, location, db));
let post_text = warp::path!("t")
.and(warp::post())
.and(crate::auth::required(db, config))
.and(crate::auth::auth(db, config))
.and(crate::util::body())
.and(warp::header::optional("X-ID-Length"))
.and_then(move |user, data, len| post_text(user, data, len, db));
let put_text = warp::path!("t" / String)
.and(warp::put())
.and(crate::auth::required(db, config))
.and(crate::auth::auth(db, config))
.and(crate::util::body())
.and_then(move |id, user, data| put_text(id, user, data, db));
filite
spectre
.or(index)
.or(get)
.or(post_file)
.or(put_file)
.or(post_link)
.or(put_link)
.or(post_text)
.or(put_text)
.recover(crate::reject::handle_rejections)
}
#[tracing::instrument(level = "debug")]
async fn index(user: User) -> Result<impl Reply, Rejection> {
#[derive(Template)]
#[template(path = "index.html")]
struct Template<'a> {
title: &'a str,
user: &'a str,
}
let template = Template {
title: "filite",
user: &user.id,
};
Ok(warp::reply::html(template.render().or_500()?))
}
#[tracing::instrument(level = "debug", skip(db))]
async fn filite(id: String, db: &Db) -> Result<impl Reply, Rejection> {
async fn get(id: String, db: &Db) -> Result<impl Reply, Rejection> {
impl Reply for Filite {
fn into_response(self) -> Response {
match self.inner {
@ -83,7 +116,9 @@ async fn filite(id: String, db: &Db) -> Result<impl Reply, Rejection> {
}
}
let filite = crate::db::filite(&id, true, db).or_500()?.or_404()?;
let filite = task::block_in_place(|| crate::db::get(&id, db))
.or_500()?
.or_404()?;
Ok(filite)
}
@ -95,7 +130,7 @@ async fn post_file(
len: Option<usize>,
db: &Db,
) -> Result<impl Reply, Rejection> {
let id = crate::db::random_id(len.unwrap_or(8), db).or_500()?;
let id = task::block_in_place(|| crate::db::random_id(len.unwrap_or(8), db)).or_500()?;
put_file(id, user, data, mime, db).await
}
@ -107,13 +142,15 @@ async fn put_file(
mime: Option<String>,
db: &Db,
) -> Result<impl Reply, Rejection> {
crate::db::insert_file(
&id,
user.id,
data.to_vec(),
mime.unwrap_or_else(|| "application/octet-stream".to_owned()),
db,
)
task::block_in_place(|| {
crate::db::insert_file(
&id,
user.id,
data.to_vec(),
mime.unwrap_or_else(|| "application/octet-stream".to_owned()),
db,
)
})
.or_500()?
.or_409()?;
Ok(warp::reply::with_status(id, StatusCode::CREATED))
@ -126,13 +163,13 @@ async fn post_link(
len: Option<usize>,
db: &Db,
) -> Result<impl Reply, Rejection> {
let id = crate::db::random_id(len.unwrap_or(8), db).or_500()?;
let id = task::block_in_place(|| crate::db::random_id(len.unwrap_or(8), db)).or_500()?;
put_link(id, user, location, db).await
}
#[tracing::instrument(level = "debug", skip(db))]
async fn put_link(id: String, user: User, location: Uri, db: &Db) -> Result<impl Reply, Rejection> {
crate::db::insert_link(&id, user.id, location.to_string(), db)
task::block_in_place(|| crate::db::insert_link(&id, user.id, location.to_string(), db))
.or_500()?
.or_409()?;
Ok(warp::reply::with_status(id, StatusCode::CREATED))
@ -145,13 +182,13 @@ async fn post_text(
len: Option<usize>,
db: &Db,
) -> Result<impl Reply, Rejection> {
let id = crate::db::random_id(len.unwrap_or(8), db).or_500()?;
let id = task::block_in_place(|| crate::db::random_id(len.unwrap_or(8), db)).or_500()?;
put_text(id, user, data, db).await
}
#[tracing::instrument(level = "debug", skip(db))]
async fn put_text(id: String, user: User, data: String, db: &Db) -> Result<impl Reply, Rejection> {
crate::db::insert_text(&id, user.id, data, db)
task::block_in_place(|| crate::db::insert_text(&id, user.id, data, db))
.or_500()?
.or_409()?;
Ok(warp::reply::with_status(id, StatusCode::CREATED))

View File

@ -1,6 +1,6 @@
use bytes::Bytes;
use std::str::FromStr;
use warp::{http::StatusCode, Filter, Rejection};
use warp::{Filter, Rejection};
pub trait DefaultExt {
fn is_default(&self) -> bool;
@ -18,11 +18,11 @@ where
{
warp::body::bytes().and_then(|b: Bytes| async move {
match std::str::from_utf8(&b) {
Ok(s) => match s.parse() {
Ok(s) => match T::from_str(s) {
Ok(v) => Ok(v),
Err(e) => Err(crate::reject::custom(e, StatusCode::BAD_REQUEST)),
Err(e) => Err(crate::reject::bad_request(e)),
},
Err(e) => Err(crate::reject::custom(e, StatusCode::BAD_REQUEST)),
Err(e) => Err(crate::reject::bad_request(e)),
}
})
}

1
static/spectre.min.css vendored Normal file

File diff suppressed because one or more lines are too long

31
templates/index.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{{ title }}</title>
<link rel="stylesheet" href="spectre.css">
</head>
<body>
<ul class="tab">
<li class="tab-item active">
<a href="#">
File
</a>
</li>
<li class="tab-item">
<a href="#">
Link
</a>
</li>
<li class="tab-item">
<a href="#">
Text
</a>
</li>
<li class="tab-item tab-action">
<span>{{ user }}</span>
</li>
</ul>
</body>
</html>