gourami

[UNMAINTAINED] Activitypub server in Rust
Log | Files | Refs | README | LICENSE

commit cd5095f0f1fc64d9b73aae3974f82a8be23f81f1
parent 827614e9b9f185425dd8030f3e3cee6f153c0777
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sun, 19 Apr 2020 02:05:32 -0500

major cleanup

Diffstat:
MCargo.lock | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 2+-
Mmigrations/2020-04-13-014917_initialize/up.sql | 2++
Msrc/db/schema.rs | 9+++++----
Msrc/db/user.rs | 6++++--
Msrc/lib.rs | 172++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/routes.rs | 48+-----------------------------------------------
Msrc/session.rs | 44+++++++++++++++++++++++++++++---------------
Ctemplates/user.html -> templates.rs | 0
Mtemplates/base.html | 2+-
Atemplates/noteslist.html | 13+++++++++++++
Mtemplates/timeline.html | 16++--------------
Mtemplates/user.html | 9+++++++++
13 files changed, 231 insertions(+), 152 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -290,6 +290,7 @@ dependencies = [ "byteorder", "diesel_derives", "libsqlite3-sys", + "r2d2", ] [[package]] @@ -788,6 +789,15 @@ dependencies = [ ] [[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + +[[package]] name = "log" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1001,6 +1011,30 @@ dependencies = [ ] [[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e136c1904604defe99ce5fd71a28d473fa60a12255d511aa78a9ddf11237aeb" +dependencies = [ + "cfg-if", + "cloudabi", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.8", +] + +[[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1144,6 +1178,17 @@ dependencies = [ ] [[package]] +name = "r2d2" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1497e40855348e4a8a40767d8e55174bce1e445a3ac9254ad44ad468ee0485af" +dependencies = [ + "log 0.4.8", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] name = "rand" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1410,12 +1455,27 @@ dependencies = [ ] [[package]] +name = "scheduled-thread-pool" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0988d7fdf88d5e5fcf5923a0f1e8ab345f3e98ab4bc6bc45a2d5ff7f7458fbf6" +dependencies = [ + "parking_lot", +] + +[[package]] name = "scoped-tls" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" [[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] name = "security-framework" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -10,7 +10,7 @@ askama = "0.8" bcrypt = "0.7" cookie = "0.13" chrono = "*" -diesel = { version = "1.0.0", features = ["sqlite"] } +diesel = { version = "1.4", features = ["sqlite", "r2d2"] } env_logger = "0.7" lazy_static = "1.4.0" log = "0.4" diff --git a/migrations/2020-04-13-014917_initialize/up.sql b/migrations/2020-04-13-014917_initialize/up.sql @@ -5,10 +5,12 @@ CREATE TABLE users ( username VARCHAR(255), password VARCHAR(255), email VARCHAR(255), + bio VARCHAR(1023) default "New here!", created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX users_username_idx ON users (username); +CREATE UNIQUE INDEX users_email_idx ON users (email); CREATE TABLE activities ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, diff --git a/src/db/schema.rs b/src/db/schema.rs @@ -11,9 +11,10 @@ table! { table! { users (id) { id -> Integer, - username -> Text, - password -> Text, - email -> Text, + username -> Varchar, + password -> Varchar, + email -> Varchar, + bio -> Text, created_time -> Timestamp, } } @@ -21,7 +22,7 @@ table! { table! { sessions (id) { id -> Integer, - cookie -> Text, + cookie -> Varchar, user_id -> Integer, created_time -> Timestamp, } diff --git a/src/db/user.rs b/src/db/user.rs @@ -6,14 +6,16 @@ use diesel::prelude::*; use serde::{Deserialize, Serialize}; use bcrypt; -#[derive(Debug, Queryable, Deserialize)] +#[derive(Debug, Clone, Default, Queryable, Deserialize)] pub struct User { pub id: i32, pub username: String, pub email: String, + pub bio: String, pub created_time: String, } +// TODO -- default "anonymous" user impl User { pub fn authenticate( @@ -24,7 +26,7 @@ impl User { use crate::db::schema::users::dsl::*; let (user, hash) = match users .filter(username.eq(user)) - .select(((id, username, email, created_time), password)) + .select(((id, username, email, created_time, bio), password)) .first::<(User, String)>(conn) { Ok((user, hash)) => (user, hash), diff --git a/src/lib.rs b/src/lib.rs @@ -1,6 +1,10 @@ +// auth functions] #[macro_use] extern crate diesel; #[macro_use] extern crate log; +#[macro_use] extern crate lazy_static; + +use std::convert::Infallible; use warp::{Reply, Filter, Rejection}; use warp::http; @@ -18,15 +22,41 @@ use diesel::sqlite::SqliteConnection; use diesel::insert_into; use serde::{Deserialize, Serialize}; use session::{Session}; +use std::sync::{Arc, Mutex}; +use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; + +type SqlitePool = Pool<ConnectionManager<SqliteConnection>>; mod db; mod session; -fn establish_connection() -> SqliteConnection { - let url = ::std::env::var("DATABASE_URL").unwrap(); - let conn = SqliteConnection::establish(&url).unwrap(); - conn +// We use a global shared sqlite connection because it's simple and performance is not +// very important + +fn pooled_sqlite() -> SqlitePool { + let manager = ConnectionManager::<SqliteConnection>::new(std::env::var("DATABASE_URL").unwrap()); + Pool::new(manager).expect("Postgres connection pool could not be created") +} + + +lazy_static! { + static ref POOL: SqlitePool = pooled_sqlite(); } + +// fn POOL.get().unwrap() -> diesel::SqliteConnection { +// return *POOL.get().unwrap(); + + +#[derive(Template)] +#[template(path = "user.html")] +struct UserTemplate<'a>{ + global: Global<'a>, + page: &'a str, + notes: Vec<Note>, + user: &'a User +} + + // TODO split into separate templates. not sure how #[derive(Template)] #[template(path = "timeline.html")] @@ -38,22 +68,22 @@ struct TimelineTemplate<'a>{ struct Global<'a> { title: &'a str, - username: String, + user: User, logged_in: bool, } impl<'a> Global<'a> { - fn from_user(user: Option<User>) -> Self { - match user { - Some(u) => Global { + fn from_session(session: Option<Session>) -> Self { + match session { + Some(s) => Global { logged_in: true, title: "gourami", - username: u.username.clone(), + user: s.user.clone(), }, None => Global { logged_in: false, title: "gourami", - username: String::from("anonymous"), + user: User::default(), } } } @@ -79,10 +109,9 @@ pub fn render_template<T: askama::Template>(t: &T) -> http::Response<hyper::body .unwrap() } -fn delete_note(note_id: i32) -> impl Reply { +fn delete_note(session: Option<Session>, note_id: i32) -> impl Reply { use db::schema::notes::dsl::*; - let conn = establish_connection(); - diesel::delete(notes.filter(id.eq(note_id))).execute(&conn).unwrap(); + diesel::delete(notes.filter(id.eq(note_id))).execute(&POOL.get().unwrap()).unwrap(); warp::redirect::redirect(warp::http::Uri::from_static("/")) } @@ -91,19 +120,17 @@ struct NewNoteRequest { note_input: String, // has to be String } -fn new_note(auth_cookie: Option<String>, req: &NewNoteRequest) -> impl Reply { +fn new_note(session: Option<Session>, req: NewNoteRequest) -> impl Reply { use db::schema::notes::dsl::*; // create activitypub activity object // TODO -- micropub? - if let Some(k) = auth_cookie { - let conn = establish_connection(); - let user = Session::from_key(&conn, &k).user.unwrap(); + if let Some(s) = session { let new_note = NoteInput{ - creator_id: user.id, + creator_id: s.user.id, parent_id: None, content: req.note_input.clone(), // how to avoid clone here? }; - insert_into(notes).values(new_note).execute(&conn).unwrap(); + insert_into(notes).values(new_note).execute(&POOL.get().unwrap()).unwrap(); return warp::redirect::redirect(warp::http::Uri::from_static("/")) } else { return warp::redirect::redirect(warp::http::Uri::from_static("/")) @@ -131,8 +158,9 @@ struct RegisterTemplate<'a>{ global: Global<'a>, } -fn register_page() -> impl Reply { - let global = Global::from_user(None); +fn register_page() -> impl warp::Reply { + let global = Global::from_session(None); + // TODO -- do... something if session is not none render_template(&RegisterTemplate{page: "register", global:global}) } @@ -163,8 +191,7 @@ fn do_register(form: RegisterForm) -> impl Reply{ let hash = bcrypt::hash(&form.password, bcrypt::DEFAULT_COST).unwrap(); let new_user = NewUser {username: &form.username, password: &hash, email: &form.email}; // todo data validation - let conn = establish_connection(); - insert_into(users).values(new_user).execute(&conn).unwrap(); + insert_into(users).values(new_user).execute(&POOL.get().unwrap()).unwrap(); // insert into database do_login(LoginForm{username: form.username, password: form.password}) @@ -187,13 +214,12 @@ struct LoginTemplate<'a>{ fn login_page() -> impl Reply { // dont let you access this page if logged in - let global = Global::from_user(None); + let global = Global::from_session(None); render_template(&LoginTemplate{page: "login", login_failed: false, global:global}) } fn do_login(form: LoginForm) -> impl Reply { - let conn = establish_connection(); - if let Some(cookie) = Session::authenticate(&conn, &form.username, &form.password) { + if let Some(cookie) = Session::authenticate(&POOL.get().unwrap(), &form.username, &form.password) { http::Response::builder() .status(http::StatusCode::FOUND) .header(http::header::LOCATION, "/") @@ -203,26 +229,23 @@ fn do_login(form: LoginForm) -> impl Reply { ) .body(Body::empty()).unwrap() } else { - let global = Global::from_user(None); + let global = Global::from_session(None); render_template(&LoginTemplate{page: "login", login_failed: true, global:global}) // TODO -- better error handling } } -fn timeline(auth_cookie: Option<String>) -> impl Reply { +fn render_timeline(session: Option<Session>) -> impl Reply { // no session -- anonymous - let conn = establish_connection(); - let session = Session::from_key(&conn, &auth_cookie.unwrap()); - let global = Global::from_user(session.user); - //ownership? + let global = Global::from_session(session); use db::schema::notes::dsl::*; let results = notes - .load::<Note>(&conn) + .load::<Note>(&POOL.get().unwrap()) .expect("Error loading posts"); render_template(&TimelineTemplate{ - page: "timeline", - global: global, - notes: results, + page: "timeline", + global: global, + notes: results, }) } @@ -247,54 +270,67 @@ fn inbox() { pub async fn run_server() { env_logger::init(); + // cors filters etc + let session_filter = move || session::create_session_filter().clone(); - let notifications = warp::path("notifications"); - - // How does this interact with tokio? who knows! - let test = warp::path("test").map(|| "Hello world"); + use warp::{path, body::form}; - let register_page = warp::path("register").map(|| register_page()); - let do_register = warp::path("register") - .and(warp::body::form()) - .map(|f: RegisterForm| do_register(f)); + let home = warp::path::end() + .and(session_filter()) + .map(render_timeline); - let login_page = warp::path("login").map(|| login_page()); - let do_login = warp::path("login") - .and(warp::body::form()) - .map(|f: LoginForm| do_login(f)); + // auth functions + let register_page = path("register") + .map(|| register_page()); + + let do_register = path("register") + .and(form()) + .map(do_register); + + let login_page = path("login") + .map(|| login_page()); + + let do_login = path("login") + .and(form()) + .map(do_login); + + // CRUD actions + let create_note = path("create_note") + .and(session_filter()) + .and(form()) + .map(new_note); + + let delete_note = session_filter() + .and(warp::path::param::<i32>()) + .and(warp::path("delete")) + .map(delete_note); - let logout = warp::path("logout").map(|| "Hello from logout"); - // post - // user - // default page -- timeline - let home = warp::path::end() - .and(warp::filters::cookie::optional("EXAUTH")) - .map(|auth_cookie| timeline(auth_cookie)); let static_files = warp::path("static") - .and(warp::fs::dir("./static")); + .and(warp::fs::dir("./static")); // https://github.com/seanmonstar/warp/issues/42 -- how to set up diesel // TODO set content length limit // TODO redirect via redirect in request // TODO secure against xss - let create_note = warp::path("create_note") - .and(warp::filters::cookie::optional("EXAUTH")) - .and(warp::body::form()) - .map(|auth_cookie, note_req: NewNoteRequest| new_note(auth_cookie, &note_req)); - - let delete_note = warp::path::param::<i32>() - .and(warp::path("delete")) - .map(|note_id| delete_note(note_id)); - + // used for api based authentication + // let api_filter = session::create_session_filter(&POOL.get()); + let html_renders = home.or(login_page).or(register_page); + let forms = login_page.or(do_register).or(do_login).or(create_note).or(delete_note); + // let api // catch all for any other paths let not_found = warp::any().map(|| "404 not found"); - let routes = warp::get().and( - home.or(test).or(static_files).or(login_page).or(register_page).or(not_found)) - .or(warp::post().and(create_note.or(delete_note).or(do_login).or(do_register))) + let routes = warp::get().and(html_renders) + .or( + warp::post() + .and(warp::body::content_length_limit(1024 * 32)) + .and(forms)) + .or(static_files) + .or(not_found) .with(warp::log("server")); + warp::serve(routes) .run(([127, 0, 0, 1], 3030)) .await; diff --git a/src/routes.rs b/src/routes.rs @@ -1,47 +1 @@ -use warp; - -def get_routes() { - let notifications = warp::path("notifications"); - - // How does this interact with tokio? who knows! - let test = warp::path("test").map(|| "Hello world"); - - let register = warp::path("register").map(|| "Hello from register"); - let login = warp::path("login").map(|| "Hello from login"); - let logout = warp::path("logout").map(|| "Hello from logout"); - - // post - // user - // default page -- timeline - let home = warp::path::end() - .map(|| render_template(&TimelineTemplate{ - page: "timeline", - logged_in: true, - notes: Note::get_for_user(&SqliteConnection::establish("sample.db").unwrap(), 1), - username: "alex", - title: "gourami"})); - - let static_files = warp::path("static") - .and(warp::fs::dir("./static")); - - // https://github.com/seanmonstar/warp/issues/42 -- how to set up diesel - // TODO set content length limit - // TODO redirect via redirect in request - // TODO secure against xss - let create_note = warp::path("create_note") - .and(warp::body::form()) - .map(|note_req: NewNoteRequest| new_note(&note_req)); - - let delete_note = warp::path::param::<i32>() - .and(warp::path("delete")) - .map(|note_id| delete_note(note_id)); - - // catch all for any other paths - let not_found = warp::any().map(|| "404 not found"); - - let routes = warp::get().and( - home.or(test).or(static_files).or(not_found)) - .or(warp::post().and(create_note.or(delete_note))); - routes -} - +// TODO add routes here diff --git a/src/session.rs b/src/session.rs @@ -10,8 +10,8 @@ use warp::filters::{cookie, BoxedFilter}; pub struct Session { // dbpool maybe - pub id: Option<i32>, - pub user: Option<User>, + pub id: i32, + pub user: User } // TODO -- figure out if database pooling is strictly necessary for security @@ -45,19 +45,33 @@ impl Session { } None } - pub fn from_key(conn: &SqliteConnection, sessionkey: &str) -> Self { - use db::schema::sessions::dsl as s; - use db::schema::users::dsl as u; - let (id, user) = u::users - .inner_join(s::sessions) - .select((s::id, (u::id, u::username, u::email, u::created_time))) - .filter(s::cookie.eq(sessionkey)) - .first::<(i32, User)>(conn) - .ok() - .map(|(i, u)| (Some(i), Some(u))) - .unwrap_or((None, None)); + pub fn from_key(sess: Option<String>) -> Option<Self> { + if let Some(sessionkey) = sess { + use db::schema::sessions::dsl as s; + use db::schema::users::dsl as u; + let result = u::users + .inner_join(s::sessions) + .select((s::id, (u::id, u::username, u::email, u::created_time, u::bio))) // TODO figure out how to not select pw + .filter(s::cookie.eq(sessionkey)) + .first::<(i32, User)>(&POOL.get().unwrap()) + .ok(); + if let Some(r) = result { + Some(Self {id: r.0, user: r.1}) + } + else { + None + } + } + else { + // so we don't have to query db when key isnt present + None + } - debug!("Got: #{:?} {:?}", id, user); - Session { id, user } } } + +pub fn create_session_filter() -> BoxedFilter<(Option<Session>,)> { + cookie::optional("EXAUTH") + .map(move |key: Option<String>| {Session::from_key(key)}) + .boxed() +} diff --git a/templates/user.html b/templates.rs diff --git a/templates/base.html b/templates/base.html @@ -18,7 +18,7 @@ </div> <div class="col-sm text-right"> {% if global.logged_in %} - <a href="/user/{{global.username}}">@{{global.username}}</a> + <a href="/user/{{global.user.username}}">@{{global.user.username}}</a> {% else %} <a href="/register">register</a> | <a href="login">login</a> {% endif %} diff --git a/templates/noteslist.html b/templates/noteslist.html @@ -0,0 +1,13 @@ +{% for note in notes %} +<div class="row"> + <div class="note"> + <a href="/note/{{note.id}}">>{{note.id}}</a> {{note.created_time}} <a href="user/{{note.creator_id}}">@{{note.creator_id}}</a> | {{note.content}} + <form method="post" action="/{{note.id}}/delete" class="inline"> + <input type="hidden" name="extra_submit_param" value="extra_submit_value"> + <button type="submit" name="submit_param" value="submit_value" class="link-button"> + x + </button> + </form> + </div> +</div> +{% endfor %} diff --git a/templates/timeline.html b/templates/timeline.html @@ -7,20 +7,8 @@ <textarea name="note_input" rows=3 placeholder="note"></textarea> <br> <button id="post">create note</button> -{% endif %} </form> -{% for note in notes %} -<div class="row"> - <div class="note"> - <a href="/note/{{note.id}}">>{{note.id}}</a> {{note.created_time}} <a href="user/{{note.creator_id}}">@{{note.creator_id}}</a> | {{note.content}} - <form method="post" action="/{{note.id}}/delete" class="inline"> - <input type="hidden" name="extra_submit_param" value="extra_submit_value"> - <button type="submit" name="submit_param" value="submit_value" class="link-button"> - x - </button> - </form> - </div> -</div> -{% endfor %} +{% endif %} +{% include "noteslist.html" %} </div> {% endblock %} diff --git a/templates/user.html b/templates/user.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block content %} +<div class="container"> + {{ user.username }} {{user.id}} +bio: {{user.bio}} +{% include "noteslist.html" %} +</div> +{% endblock %}