Migrate to Rocket 0.5.

Rocket 0.5 is a major uprade, rewriting most of Rocket to be async.
Required many changes through the code, especially the database layer.
The new Rocket async database calls require Futures with 'static
lifetimes.

General:
 - Move to stable rust.
 - Most of codebase is now async.
 - Rocket migrations (e.g. Cookies to CookieJar).

Database:
 - Switched to owned data (&str -> String) for inserts because of the
   'static lifetime requirement on Rocket's DB future.
 - All database methods now asynchronous.

Pages:
 - Changed various routes to async.
 - Needed to add clone calls to some places because we need to use
   owned data multiple times (registration).
This commit is contained in:
projectmoon 2020-12-13 13:57:50 +00:00
parent 4024f32621
commit 8c8ed4f6ef
13 changed files with 1086 additions and 534 deletions

1348
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -20,10 +20,16 @@ thiserror = "1.0"
rust-argon2 = "0.8" rust-argon2 = "0.8"
log = "0.4" log = "0.4"
rand = "0.7" rand = "0.7"
rocket = { version= "0.4.6", features = ["private-cookies"] } futures = "0.3"
strum = { version = "0.20", features = ["derive"] } strum = { version = "0.20", features = ["derive"] }
[dependencies.rocket]
git = "https://github.com/SergioBenitez/Rocket"
branch = "master"
features = ["secrets"]
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
version = "0.4.6" git = "https://github.com/SergioBenitez/Rocket"
branch = "master"
default-features = false default-features = false
features = [ "tera_templates", "diesel_sqlite_pool", "serve" ] features = [ "tera_templates", "diesel_sqlite_pool", "serve" ]

View File

@ -7,5 +7,5 @@ write_timeout = 5
log = "normal" log = "normal"
limits = { forms = 32768 } limits = { forms = 32768 }
[development.databases] [global.databases]
tenebrous_db = { url = "./tenebrous.sqlite" } tenebrous_db = { url = "./tenebrous.sqlite" }

View File

@ -1 +1 @@
nightly stable

View File

@ -2,23 +2,27 @@ use crate::models::characters::{Character, NewCharacter, StrippedCharacter};
use crate::models::users::{NewUser, User}; use crate::models::users::{NewUser, User};
use crate::schema::characters; use crate::schema::characters;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::SqliteConnection; use rocket_contrib::databases::diesel;
#[database("tenebrous_db")] #[database("tenebrous_db")]
pub(crate) struct TenebrousDbConn(SqliteConnection); pub(crate) struct TenebrousDbConn(SqliteConnection);
#[rocket::async_trait]
pub(crate) trait Dao { pub(crate) trait Dao {
fn load_user_by_id(&self, id: i32) -> QueryResult<Option<User>>; async fn load_user_by_id(&self, id: i32) -> QueryResult<Option<User>>;
fn load_user(&self, for_username: &str) -> QueryResult<Option<User>>; async fn load_user(&self, for_username: String) -> QueryResult<Option<User>>;
fn insert_user(&self, new_user: &NewUser) -> QueryResult<User>; //async fn insert_user<'a>(&self, new_user: &'a NewUser<'a>) -> QueryResult<User>;
fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>>; async fn insert_user(&self, new_user: NewUser) -> QueryResult<User>;
fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>>; async fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>>;
fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()>; async fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>>;
//async fn insert_character<'a>(&self, new_character: NewCharacter<'a>) -> QueryResult<()>;
async fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()>;
} }
type StrippedCharacterColumns = ( type StrippedCharacterColumns = (
@ -39,55 +43,67 @@ const STRIPPED_CHARACTER_COLUMNS: StrippedCharacterColumns = (
characters::data_version, characters::data_version,
); );
#[rocket::async_trait]
impl Dao for TenebrousDbConn { impl Dao for TenebrousDbConn {
fn load_user_by_id(&self, user_id: i32) -> QueryResult<Option<User>> { async fn load_user_by_id(&self, user_id: i32) -> QueryResult<Option<User>> {
use crate::schema::users::dsl::*; use crate::schema::users::dsl::*;
users.filter(id.eq(user_id)).first(&self.0).optional() self.run(move |conn| users.filter(id.eq(user_id)).first(conn).optional())
.await
} }
fn load_user(&self, for_username: &str) -> QueryResult<Option<User>> { async fn load_user(&self, for_username: String) -> QueryResult<Option<User>> {
use crate::schema::users::dsl::*; use crate::schema::users::dsl::*;
users self.run(move |conn| {
.filter(username.eq(for_username)) users
.first(&self.0) .filter(username.eq(for_username))
.optional() .first(conn)
.optional()
})
.await
} }
fn insert_user(&self, new_user: &NewUser) -> QueryResult<User> { async fn insert_user(&self, new_user: NewUser) -> QueryResult<User> {
use crate::schema::users; self.run(move |conn| {
diesel::insert_into(users).values(&new_user).execute(conn)?;
diesel::insert_into(users::table) use crate::schema::users::dsl::*;
.values(new_user) users.filter(username.eq(new_user.username)).first(conn)
.execute(&self.0)?; })
.await
use crate::schema::users::dsl::*;
Ok(users
.filter(username.eq(new_user.username))
.first(&self.0)?)
} }
fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>> { async fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>> {
use crate::schema::characters::dsl::*;
characters
.filter(user_id.eq(for_user_id))
.select(STRIPPED_CHARACTER_COLUMNS)
.load(&self.0)
}
fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>> {
use crate::schema::characters::dsl::*; use crate::schema::characters::dsl::*;
characters self.run(move |conn| {
.filter(id.eq(character_id)) characters
.first(&self.0) .filter(user_id.eq(for_user_id))
.optional() .select(STRIPPED_CHARACTER_COLUMNS)
.load(conn)
})
.await
} }
fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()> { async fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>> {
diesel::insert_into(characters::table) use crate::schema::characters::dsl::*;
.values(new_character)
.execute(&self.0)?; self.run(move |conn| {
characters
.filter(id.eq(character_id))
.first(conn)
.optional()
})
.await
}
async fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()> {
self.run(|conn| {
diesel::insert_into(characters::table)
.values(new_character)
.execute(conn)
})
.await?;
Ok(()) Ok(())
} }

View File

@ -27,10 +27,6 @@ pub enum Error {
DeserializationError(#[from] prost::DecodeError), DeserializationError(#[from] prost::DecodeError),
} }
#[derive(Error, Debug)]
#[error("internal eror")]
pub struct SensitiveError(Error);
impl Error { impl Error {
fn is_sensitive(&self) -> bool { fn is_sensitive(&self) -> bool {
use Error::*; use Error::*;
@ -41,8 +37,9 @@ impl Error {
} }
} }
impl<'r> Responder<'r> for Error { #[rocket::async_trait]
fn respond_to(self, req: &Request) -> response::Result<'r> { impl<'r> Responder<'r, 'static> for Error {
fn respond_to(self, req: &Request) -> response::Result<'static> {
log::error!("error: {0}", self.to_string()); log::error!("error: {0}", self.to_string());
//Hide sensitive error information //Hide sensitive error information
let message: String = if self.is_sensitive() { let message: String = if self.is_sensitive() {
@ -64,22 +61,3 @@ impl<'r> Responder<'r> for Error {
} }
} }
} }
impl<'r> Responder<'r> for SensitiveError {
fn respond_to(self, req: &Request) -> response::Result<'r> {
log::error!("sensitive error: {0}", self.0.to_string());
let message: String = self.to_string();
let mut context = HashMap::new();
context.insert("message", message);
let resp = Template::render("error", context).respond_to(req)?;
use Error::*;
match self.0 {
NotFound => status::NotFound(resp).respond_to(req),
NotLoggedIn => status::Forbidden(Some(resp)).respond_to(req),
NoPermission => status::Forbidden(Some(resp)).respond_to(req),
_ => status::Custom(Status::InternalServerError, resp).respond_to(req),
}
}
}

View File

@ -1,5 +1,3 @@
#![feature(proc_macro_hygiene, decl_macro, never_type)]
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
@ -24,7 +22,8 @@ pub mod models;
pub mod routes; pub mod routes;
pub mod schema; pub mod schema;
fn main() { #[rocket::main]
async fn main() -> Result<(), rocket::error::Error> {
let root_routes: Vec<rocket::Route> = { let root_routes: Vec<rocket::Route> = {
routes::root::routes() routes::root::routes()
.into_iter() .into_iter()
@ -45,5 +44,6 @@ fn main() {
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/proto")), StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/proto")),
) )
.register(catchers) .register(catchers)
.launch(); .launch()
.await
} }

View File

@ -155,11 +155,11 @@ impl Visibility for StrippedCharacter {
/// names correspond to columns. /// names correspond to columns.
#[derive(Insertable)] #[derive(Insertable)]
#[table_name = "characters"] #[table_name = "characters"]
pub struct NewCharacter<'a> { pub struct NewCharacter {
pub user_id: i32, pub user_id: i32,
pub viewable: bool, pub viewable: bool,
pub character_name: &'a str, pub character_name: String,
pub data_type: CharacterDataType, pub data_type: CharacterDataType,
pub data_version: i32, pub data_version: i32,
pub data: &'a [u8], pub data: Vec<u8>,
} }

View File

@ -25,24 +25,29 @@ impl User {
} }
} }
async fn attempt_load_user<'a>(db: &'a TenebrousDbConn, id: i32) -> Option<User> {
db.load_user_by_id(id).await.ok().flatten()
}
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for &'a User { impl<'a, 'r> FromRequest<'a, 'r> for &'a User {
type Error = !; type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, !> { async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, ()> {
let user: &Option<User> = request.local_cache(|| { let db = try_outcome!(request.guard::<TenebrousDbConn>().await);
let attempt_load_user = |id| -> Option<User> { let user_id: Option<i32> = request
TenebrousDbConn::from_request(request) .cookies()
.map(|conn| conn.load_user_by_id(id).ok().flatten()) .get_private("user_id")
.succeeded() .and_then(|cookie| cookie.value().parse().ok());
.flatten()
};
request let user: &Option<User> = request
.cookies() .local_cache_async(async {
.get_private("user_id") match user_id {
.and_then(|cookie| cookie.value().parse().ok()) Some(id) => attempt_load_user(&db, id).await,
.and_then(attempt_load_user) _ => None,
}); }
})
.await;
user.as_ref().or_forward(()) user.as_ref().or_forward(())
} }
@ -50,7 +55,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for &'a User {
#[derive(Insertable)] #[derive(Insertable)]
#[table_name = "users"] #[table_name = "users"]
pub struct NewUser<'a> { pub struct NewUser {
pub username: &'a str, pub username: String,
pub password: &'a str, pub password: String,
} }

View File

@ -1,7 +1,7 @@
use crate::db::{Dao, TenebrousDbConn}; use crate::db::{Dao, TenebrousDbConn};
use crate::models::users::{self, NewUser, User}; use crate::models::users::{self, NewUser, User};
use log::error; use log::error;
use rocket::http::{Cookie, Cookies}; use rocket::http::{Cookie, CookieJar};
use rocket::request::{FlashMessage, Form}; use rocket::request::{FlashMessage, Form};
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
@ -30,11 +30,11 @@ struct Registration {
password: String, password: String,
} }
fn add_login_cookie(cookies: &mut Cookies, user: &User) { fn add_login_cookie(cookies: &CookieJar<'_>, user: &User) {
cookies.add_private(Cookie::new("user_id", user.id.to_string())); cookies.add_private(Cookie::new("user_id", user.id.to_string()));
} }
fn remove_login_cookie(cookies: &mut Cookies) { fn remove_login_cookie(cookies: &CookieJar<'_>) {
cookies.remove_private(Cookie::named("user_id")); cookies.remove_private(Cookie::named("user_id"));
} }
@ -47,13 +47,14 @@ fn registration_error_redirect<S: AsRef<str>>(message: S) -> Flash<Redirect> {
} }
#[post("/login", data = "<login>")] #[post("/login", data = "<login>")]
fn login( async fn login(
mut cookies: Cookies, cookies: &CookieJar<'_>,
login: Form<Login>, login: Form<Login>,
conn: TenebrousDbConn, conn: TenebrousDbConn,
) -> Result<Redirect, Flash<Redirect>> { ) -> Result<Redirect, Flash<Redirect>> {
let user = conn let user = conn
.load_user(&login.username) .load_user(login.username.clone())
.await
.map_err(|e| { .map_err(|e| {
error!("login - error loading user user: {}", e); error!("login - error loading user user: {}", e);
login_error_redirect("Internal error.") login_error_redirect("Internal error.")
@ -61,7 +62,7 @@ fn login(
.ok_or_else(|| login_error_redirect("Invalid username or password."))?; .ok_or_else(|| login_error_redirect("Invalid username or password."))?;
if user.verify_password(&login.password) { if user.verify_password(&login.password) {
add_login_cookie(&mut cookies, &user); add_login_cookie(cookies, &user);
Ok(Redirect::to(uri!(super::root::index))) Ok(Redirect::to(uri!(super::root::index)))
} else { } else {
Err(login_error_redirect("Invalid username or password.")) Err(login_error_redirect("Invalid username or password."))
@ -69,8 +70,8 @@ fn login(
} }
#[post("/logout")] #[post("/logout")]
fn logout(mut cookies: Cookies) -> Flash<Redirect> { fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
remove_login_cookie(&mut cookies); remove_login_cookie(cookies);
Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.") Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.")
} }
@ -100,15 +101,18 @@ fn register_page(flash: Option<FlashMessage>) -> Template {
} }
#[post("/register", data = "<registration>")] #[post("/register", data = "<registration>")]
fn register( async fn register(
mut cookies: Cookies, mut cookies: &CookieJar<'_>,
registration: Form<Registration>, registration: Form<Registration>,
conn: TenebrousDbConn, conn: TenebrousDbConn,
) -> Result<Redirect, Flash<Redirect>> { ) -> Result<Redirect, Flash<Redirect>> {
let existing_user = conn.load_user(&registration.username).map_err(|e| { let existing_user = conn
error!("registration - error loading existing user: {}", e); .load_user(registration.username.clone())
registration_error_redirect("There was an error attempting to register.") .await
})?; .map_err(|e| {
error!("registration - error loading existing user: {}", e);
registration_error_redirect("There was an error attempting to register.")
})?;
if existing_user.is_some() { if existing_user.is_some() {
return Err(registration_error_redirect(format!( return Err(registration_error_redirect(format!(
@ -123,11 +127,11 @@ fn register(
})?; })?;
let user = NewUser { let user = NewUser {
username: &registration.username, username: registration.username.clone(),
password: &hashed_pw, password: hashed_pw,
}; };
let user = conn.insert_user(&user).map_err(|e| { let user = conn.insert_user(user).await.map_err(|e| {
error!("registration - could not insert user: {}", e); error!("registration - could not insert user: {}", e);
registration_error_redirect("There was an error completing registration.") registration_error_redirect("There was an error completing registration.")
})?; })?;

View File

@ -45,16 +45,17 @@ fn view_character_template(user: &User, character: Character) -> Result<Template
} }
#[get("/<username>/<character_id>")] #[get("/<username>/<character_id>")]
fn view_character( async fn view_character(
character_id: i32, character_id: i32,
username: String, username: String,
conn: TenebrousDbConn, conn: TenebrousDbConn,
logged_in_user: Option<&User>, logged_in_user: Option<&User>,
) -> Result<Template, Error> { ) -> Result<Template, Error> {
let user = &conn.load_user(&username)?.ok_or(Error::NotFound)?; let user = &conn.load_user(username).await?.ok_or(Error::NotFound)?;
let character = conn let character = conn
.load_character(character_id)? .load_character(character_id)
.await?
.and_then(|c| c.as_visible_for(logged_in_user)) .and_then(|c| c.as_visible_for(logged_in_user))
.ok_or(Error::NotFound)?; .ok_or(Error::NotFound)?;
@ -63,15 +64,19 @@ fn view_character(
} }
#[get("/<owner>/<character_id>/edit")] #[get("/<owner>/<character_id>/edit")]
fn edit_character( async fn edit_character(
character_id: i32, character_id: i32,
owner: String, owner: String,
logged_in_user: Option<&User>, logged_in_user: Option<&User>,
conn: TenebrousDbConn, conn: TenebrousDbConn,
) -> Result<Template, Error> { ) -> Result<Template, Error> {
let owner = conn.load_user(&owner)?.ok_or(Error::NotFound)?; let owner = conn.load_user(owner).await?.ok_or(Error::NotFound)?;
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?; let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
let character = conn.load_character(character_id)?.ok_or(Error::NotFound)?; let character = conn
.load_character(character_id)
.await?
.ok_or(Error::NotFound)?;
if logged_in_user != &owner { if logged_in_user != &owner {
return Err(Error::NoPermission); return Err(Error::NoPermission);

View File

@ -5,7 +5,6 @@ use crate::models::{
convert::ValidationError, convert::ValidationError,
users::User, users::User,
}; };
use prost::bytes::BytesMut;
use rocket::{request::Form, response::Redirect}; use rocket::{request::Form, response::Redirect};
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
@ -57,24 +56,24 @@ impl NewCharacterContext {
/// Create and insert a new character into the database. The form is /// Create and insert a new character into the database. The form is
/// assumed to be successfully validated. /// assumed to be successfully validated.
fn create_new_character( async fn create_new_character(
form: &Form<NewCharacterForm>, form: &Form<NewCharacterForm>,
user_id: i32, user_id: i32,
conn: TenebrousDbConn, conn: TenebrousDbConn,
) -> Result<(), Error> { ) -> Result<(), Error> {
let system: CharacterDataType = *form.system.as_ref().unwrap(); let system: CharacterDataType = *form.system.as_ref().unwrap();
let sheet: BytesMut = system.create_data()?; let sheet: Vec<u8> = system.create_data()?.to_vec();
let insert = NewCharacter { let insert = NewCharacter {
user_id: user_id, user_id: user_id,
viewable: true, viewable: true,
character_name: &form.name, character_name: form.name.clone(),
data_type: system, data_type: system,
data_version: 1, data_version: 1,
data: &sheet, data: sheet,
}; };
conn.insert_character(insert)?; conn.insert_character(insert).await?;
Ok(()) Ok(())
} }
@ -95,7 +94,7 @@ pub(super) fn new_character_page(_logged_in_user: &User) -> Result<Template, Err
} }
#[post("/new", data = "<form>")] #[post("/new", data = "<form>")]
pub(super) fn new_character_submit( pub(super) async fn new_character_submit(
form: Form<NewCharacterForm>, form: Form<NewCharacterForm>,
logged_in_user: &User, logged_in_user: &User,
conn: TenebrousDbConn, conn: TenebrousDbConn,
@ -104,7 +103,7 @@ pub(super) fn new_character_submit(
return Err(render_error(&form, e.to_string().clone())); return Err(render_error(&form, e.to_string().clone()));
} }
match create_new_character(&form, logged_in_user.id, conn) { match create_new_character(&form, logged_in_user.id, conn).await {
Ok(_) => Ok(crate::routes::common::redirect_to_index()), Ok(_) => Ok(crate::routes::common::redirect_to_index()),
Err(e) => Err(render_error(&form, e.to_string().clone())), Err(e) => Err(render_error(&form, e.to_string().clone())),
} }

View File

@ -18,9 +18,10 @@ pub struct UserHomeContext<'a> {
} }
#[get("/")] #[get("/")]
fn user_index(user: &User, conn: TenebrousDbConn) -> Result<Template, Error> { async fn user_index(user: &User, conn: TenebrousDbConn) -> Result<Template, Error> {
let characters: Vec<StrippedCharacter> = conn let characters: Vec<StrippedCharacter> = conn
.load_character_list(user.id)? .load_character_list(user.id)
.await?
.into_iter() .into_iter()
.map(|c| c.as_visible_for(Some(user))) .map(|c| c.as_visible_for(Some(user)))
.filter_map(|c| c) .filter_map(|c| c)