diff options
| author | Ellie Huxtable <e@elm.sh> | 2021-04-20 17:07:11 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-04-20 16:07:11 +0000 |
| commit | 34888827f8a06de835cbe5833a06914f28cce514 (patch) | |
| tree | 8b56f20e50065cd2c222d5e8e067ec55cf1947a1 /src/remote | |
| parent | Optimise docker (#34) (diff) | |
| download | atuin-34888827f8a06de835cbe5833a06914f28cce514.zip | |
Switch to Warp + SQLx, use async, switch to Rust stable (#36)
* Switch to warp + sql, use async and stable rust
* Update CI to use stable
Diffstat (limited to 'src/remote')
| -rw-r--r-- | src/remote/auth.rs | 220 | ||||
| -rw-r--r-- | src/remote/database.rs | 22 | ||||
| -rw-r--r-- | src/remote/mod.rs | 5 | ||||
| -rw-r--r-- | src/remote/models.rs | 60 | ||||
| -rw-r--r-- | src/remote/server.rs | 61 | ||||
| -rw-r--r-- | src/remote/views.rs | 185 |
6 files changed, 0 insertions, 553 deletions
diff --git a/src/remote/auth.rs b/src/remote/auth.rs deleted file mode 100644 index cf61b077..00000000 --- a/src/remote/auth.rs +++ /dev/null @@ -1,220 +0,0 @@ -use self::diesel::prelude::*; -use eyre::Result; -use rocket::http::Status; -use rocket::request::{self, FromRequest, Outcome, Request}; -use rocket::State; -use rocket_contrib::databases::diesel; -use sodiumoxide::crypto::pwhash::argon2id13; - -use rocket_contrib::json::Json; -use uuid::Uuid; - -use super::models::{NewSession, NewUser, Session, User}; -use super::views::ApiResponse; - -use crate::api::{LoginRequest, RegisterRequest}; -use crate::schema::{sessions, users}; -use crate::settings::Settings; -use crate::utils::hash_secret; - -use super::database::AtuinDbConn; - -#[derive(Debug)] -pub enum KeyError { - Missing, - Invalid, -} - -pub fn verify_str(secret: &str, verify: &str) -> bool { - sodiumoxide::init().unwrap(); - - let mut padded = [0_u8; 128]; - secret.as_bytes().iter().enumerate().for_each(|(i, val)| { - padded[i] = *val; - }); - - match argon2id13::HashedPassword::from_slice(&padded) { - Some(hp) => argon2id13::pwhash_verify(&hp, verify.as_bytes()), - None => false, - } -} - -impl<'a, 'r> FromRequest<'a, 'r> for User { - type Error = KeyError; - - fn from_request(request: &'a Request<'r>) -> request::Outcome<User, Self::Error> { - let session: Vec<_> = request.headers().get("authorization").collect(); - - if session.is_empty() { - return Outcome::Failure((Status::BadRequest, KeyError::Missing)); - } else if session.len() > 1 { - return Outcome::Failure((Status::BadRequest, KeyError::Invalid)); - } - - let session: Vec<_> = session[0].split(' ').collect(); - - if session.len() != 2 { - return Outcome::Failure((Status::BadRequest, KeyError::Invalid)); - } - - if session[0] != "Token" { - return Outcome::Failure((Status::BadRequest, KeyError::Invalid)); - } - - let session = session[1]; - - let db = request - .guard::<AtuinDbConn>() - .succeeded() - .expect("failed to load database"); - - let session = sessions::table - .filter(sessions::token.eq(session)) - .first::<Session>(&*db); - - if session.is_err() { - return Outcome::Failure((Status::Unauthorized, KeyError::Invalid)); - } - - let session = session.unwrap(); - - let user = users::table.find(session.user_id).first(&*db); - - match user { - Ok(user) => Outcome::Success(user), - Err(_) => Outcome::Failure((Status::Unauthorized, KeyError::Invalid)), - } - } -} - -#[get("/user/<user>")] -#[allow(clippy::clippy::needless_pass_by_value)] -pub fn get_user(user: String, conn: AtuinDbConn) -> ApiResponse { - use crate::schema::users::dsl::{username, users}; - - let user: Result<String, diesel::result::Error> = users - .select(username) - .filter(username.eq(user)) - .first(&*conn); - - if user.is_err() { - return ApiResponse { - json: json!({ - "message": "could not find user", - }), - status: Status::NotFound, - }; - } - - let user = user.unwrap(); - - ApiResponse { - json: json!({ "username": user.as_str() }), - status: Status::Ok, - } -} - -#[post("/register", data = "<register>")] -#[allow(clippy::clippy::needless_pass_by_value)] -pub fn register( - conn: AtuinDbConn, - register: Json<RegisterRequest>, - settings: State<Settings>, -) -> ApiResponse { - if !settings.server.open_registration { - return ApiResponse { - status: Status::BadRequest, - json: json!({ - "message": "registrations are not open" - }), - }; - } - - let hashed = hash_secret(register.password.as_str()); - - let new_user = NewUser { - email: register.email.as_str(), - username: register.username.as_str(), - password: hashed.as_str(), - }; - - let user = diesel::insert_into(users::table) - .values(&new_user) - .get_result(&*conn); - - if user.is_err() { - return ApiResponse { - status: Status::BadRequest, - json: json!({ - "message": "failed to create user - username or email in use?", - }), - }; - } - - let user: User = user.unwrap(); - let token = Uuid::new_v4().to_simple().to_string(); - - let new_session = NewSession { - user_id: user.id, - token: token.as_str(), - }; - - match diesel::insert_into(sessions::table) - .values(&new_session) - .execute(&*conn) - { - Ok(_) => ApiResponse { - status: Status::Ok, - json: json!({"message": "user created!", "session": token}), - }, - Err(_) => ApiResponse { - status: Status::BadRequest, - json: json!({ "message": "failed to create user"}), - }, - } -} - -#[post("/login", data = "<login>")] -#[allow(clippy::clippy::needless_pass_by_value)] -pub fn login(conn: AtuinDbConn, login: Json<LoginRequest>) -> ApiResponse { - let user = users::table - .filter(users::username.eq(login.username.as_str())) - .first(&*conn); - - if user.is_err() { - return ApiResponse { - status: Status::NotFound, - json: json!({"message": "user not found"}), - }; - } - - let user: User = user.unwrap(); - - let session = sessions::table - .filter(sessions::user_id.eq(user.id)) - .first(&*conn); - - // a session should exist... - if session.is_err() { - return ApiResponse { - status: Status::InternalServerError, - json: json!({"message": "something went wrong"}), - }; - } - - let verified = verify_str(user.password.as_str(), login.password.as_str()); - - if !verified { - return ApiResponse { - status: Status::NotFound, - json: json!({"message": "user not found"}), - }; - } - - let session: Session = session.unwrap(); - - ApiResponse { - status: Status::Ok, - json: json!({"session": session.token}), - } -} diff --git a/src/remote/database.rs b/src/remote/database.rs deleted file mode 100644 index 03973ca1..00000000 --- a/src/remote/database.rs +++ /dev/null @@ -1,22 +0,0 @@ -use diesel::pg::PgConnection; -use diesel::prelude::*; -use eyre::{eyre, Result}; - -use crate::settings::Settings; - -#[database("atuin")] -pub struct AtuinDbConn(diesel::PgConnection); - -// TODO: connection pooling -pub fn establish_connection(settings: &Settings) -> Result<PgConnection> { - if settings.server.db_uri == "default_uri" { - Err(eyre!( - "Please configure your database! Set db_uri in config.toml" - )) - } else { - let database_url = &settings.server.db_uri; - let conn = PgConnection::establish(database_url)?; - - Ok(conn) - } -} diff --git a/src/remote/mod.rs b/src/remote/mod.rs deleted file mode 100644 index 7147b88e..00000000 --- a/src/remote/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod auth; -pub mod database; -pub mod models; -pub mod server; -pub mod views; diff --git a/src/remote/models.rs b/src/remote/models.rs deleted file mode 100644 index 7f6f7766..00000000 --- a/src/remote/models.rs +++ /dev/null @@ -1,60 +0,0 @@ -use chrono::prelude::*; - -use crate::schema::{history, sessions, users}; - -#[derive(Deserialize, Serialize, Identifiable, Queryable, Associations)] -#[table_name = "history"] -#[belongs_to(User)] -pub struct History { - pub id: i64, - pub client_id: String, // a client generated ID - pub user_id: i64, - pub hostname: String, - pub timestamp: NaiveDateTime, - - pub data: String, - - pub created_at: NaiveDateTime, -} - -#[derive(Identifiable, Queryable, Associations)] -pub struct User { - pub id: i64, - pub username: String, - pub email: String, - pub password: String, -} - -#[derive(Queryable, Identifiable, Associations)] -#[belongs_to(User)] -pub struct Session { - pub id: i64, - pub user_id: i64, - pub token: String, -} - -#[derive(Insertable)] -#[table_name = "history"] -pub struct NewHistory<'a> { - pub client_id: &'a str, - pub user_id: i64, - pub hostname: String, - pub timestamp: chrono::NaiveDateTime, - - pub data: &'a str, -} - -#[derive(Insertable)] -#[table_name = "users"] -pub struct NewUser<'a> { - pub username: &'a str, - pub email: &'a str, - pub password: &'a str, -} - -#[derive(Insertable)] -#[table_name = "sessions"] -pub struct NewSession<'a> { - pub user_id: i64, - pub token: &'a str, -} diff --git a/src/remote/server.rs b/src/remote/server.rs deleted file mode 100644 index ee481ca4..00000000 --- a/src/remote/server.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::collections::HashMap; - -use crate::remote::database::establish_connection; -use crate::settings::Settings; - -use super::database::AtuinDbConn; - -use eyre::Result; -use rocket::config::{Config, Environment, LoggingLevel, Value}; - -// a bunch of these imports are generated by macros, it's easier to wildcard -#[allow(clippy::clippy::wildcard_imports)] -use super::views::*; - -#[allow(clippy::clippy::wildcard_imports)] -use super::auth::*; - -embed_migrations!("migrations"); - -pub fn launch(settings: &Settings, host: String, port: u16) -> Result<()> { - let settings: Settings = settings.clone(); // clone so rocket can manage it - - let mut database_config = HashMap::new(); - let mut databases = HashMap::new(); - - database_config.insert("url", Value::from(settings.server.db_uri.clone())); - databases.insert("atuin", Value::from(database_config)); - - let connection = establish_connection(&settings)?; - - embedded_migrations::run(&connection).expect("failed to run migrations"); - - let config = Config::build(Environment::Production) - .address(host) - .log_level(LoggingLevel::Normal) - .port(port) - .extra("databases", databases) - .finalize() - .unwrap(); - - let app = rocket::custom(config); - - app.mount( - "/", - routes![ - index, - register, - add_history, - login, - get_user, - sync_count, - sync_list - ], - ) - .manage(settings) - .attach(AtuinDbConn::fairing()) - .register(catchers![internal_error, bad_request]) - .launch(); - - Ok(()) -} diff --git a/src/remote/views.rs b/src/remote/views.rs deleted file mode 100644 index 08dff13e..00000000 --- a/src/remote/views.rs +++ /dev/null @@ -1,185 +0,0 @@ -use chrono::Utc; -use rocket::http::uri::Uri; -use rocket::http::RawStr; -use rocket::http::{ContentType, Status}; -use rocket::request::FromFormValue; -use rocket::request::Request; -use rocket::response; -use rocket::response::{Responder, Response}; -use rocket_contrib::databases::diesel; -use rocket_contrib::json::{Json, JsonValue}; - -use self::diesel::prelude::*; - -use crate::api::AddHistoryRequest; -use crate::schema::history; -use crate::settings::HISTORY_PAGE_SIZE; - -use super::database::AtuinDbConn; -use super::models::{History, NewHistory, User}; - -#[derive(Debug)] -pub struct ApiResponse { - pub json: JsonValue, - pub status: Status, -} - -impl<'r> Responder<'r> for ApiResponse { - fn respond_to(self, req: &Request) -> response::Result<'r> { - Response::build_from(self.json.respond_to(req).unwrap()) - .status(self.status) - .header(ContentType::JSON) - .ok() - } -} - -#[get("/")] -pub const fn index() -> &'static str { - "\"Through the fathomless deeps of space swims the star turtle Great A\u{2019}Tuin, bearing on its back the four giant elephants who carry on their shoulders the mass of the Discworld.\"\n\t-- Sir Terry Pratchett" -} - -#[catch(500)] -pub fn internal_error(_req: &Request) -> ApiResponse { - ApiResponse { - status: Status::InternalServerError, - json: json!({"status": "error", "message": "an internal server error has occured"}), - } -} - -#[catch(400)] -pub fn bad_request(_req: &Request) -> ApiResponse { - ApiResponse { - status: Status::InternalServerError, - json: json!({"status": "error", "message": "bad request. don't do that."}), - } -} - -#[post("/history", data = "<add_history>")] -#[allow( - clippy::clippy::cast_sign_loss, - clippy::cast_possible_truncation, - clippy::clippy::needless_pass_by_value -)] -pub fn add_history( - conn: AtuinDbConn, - user: User, - add_history: Json<Vec<AddHistoryRequest>>, -) -> ApiResponse { - let new_history: Vec<NewHistory> = add_history - .iter() - .map(|h| NewHistory { - client_id: h.id.as_str(), - hostname: h.hostname.to_string(), - user_id: user.id, - timestamp: h.timestamp.naive_utc(), - data: h.data.as_str(), - }) - .collect(); - - match diesel::insert_into(history::table) - .values(&new_history) - .on_conflict_do_nothing() - .execute(&*conn) - { - Ok(_) => ApiResponse { - status: Status::Ok, - json: json!({"status": "ok", "message": "history added"}), - }, - Err(_) => ApiResponse { - status: Status::BadRequest, - json: json!({"status": "error", "message": "failed to add history"}), - }, - } -} - -#[get("/sync/count")] -#[allow(clippy::wildcard_imports, clippy::needless_pass_by_value)] -pub fn sync_count(conn: AtuinDbConn, user: User) -> ApiResponse { - use crate::schema::history::dsl::*; - - // we need to return the number of history items we have for this user - // in the future I'd like to use something like a merkel tree to calculate - // which day specifically needs syncing - let count = history - .filter(user_id.eq(user.id)) - .count() - .first::<i64>(&*conn); - - if count.is_err() { - error!("failed to count: {}", count.err().unwrap()); - - return ApiResponse { - json: json!({"message": "internal server error"}), - status: Status::InternalServerError, - }; - } - - ApiResponse { - status: Status::Ok, - json: json!({"count": count.ok()}), - } -} - -pub struct UtcDateTime(chrono::DateTime<Utc>); - -impl<'v> FromFormValue<'v> for UtcDateTime { - type Error = &'v RawStr; - - fn from_form_value(form_value: &'v RawStr) -> Result<UtcDateTime, &'v RawStr> { - let time = Uri::percent_decode(form_value.as_bytes()).map_err(|_| form_value)?; - let time = time.to_string(); - - match chrono::DateTime::parse_from_rfc3339(time.as_str()) { - Ok(t) => Ok(UtcDateTime(t.with_timezone(&Utc))), - Err(e) => { - error!("failed to parse time {}, got: {}", time, e); - Err(form_value) - } - } - } -} - -// Request a list of all history items added to the DB after a given timestamp. -// Provide the current hostname, so that we don't send the client data that -// originated from them -#[get("/sync/history?<sync_ts>&<history_ts>&<host>")] -#[allow(clippy::wildcard_imports, clippy::needless_pass_by_value)] -pub fn sync_list( - conn: AtuinDbConn, - user: User, - sync_ts: UtcDateTime, - history_ts: UtcDateTime, - host: String, -) -> ApiResponse { - use crate::schema::history::dsl::*; - - // we need to return the number of history items we have for this user - // in the future I'd like to use something like a merkel tree to calculate - // which day specifically needs syncing - // TODO: Allow for configuring the page size, both from params, and setting - // the max in config. 100 is fine for now. - let h = history - .filter(user_id.eq(user.id)) - .filter(hostname.ne(host)) - .filter(created_at.ge(sync_ts.0.naive_utc())) - .filter(timestamp.ge(history_ts.0.naive_utc())) - .order(timestamp.asc()) - .limit(HISTORY_PAGE_SIZE) - .load::<History>(&*conn); - - if let Err(e) = h { - error!("failed to load history: {}", e); - - return ApiResponse { - json: json!({"message": "internal server error"}), - status: Status::InternalServerError, - }; - } - - let user_data: Vec<String> = h.unwrap().iter().map(|i| i.data.to_string()).collect(); - - ApiResponse { - status: Status::Ok, - json: json!({ "history": user_data }), - } -} |
