aboutsummaryrefslogtreecommitdiffstats
path: root/src/remote
diff options
context:
space:
mode:
Diffstat (limited to 'src/remote')
-rw-r--r--src/remote/auth.rs220
-rw-r--r--src/remote/database.rs22
-rw-r--r--src/remote/mod.rs5
-rw-r--r--src/remote/models.rs60
-rw-r--r--src/remote/server.rs61
-rw-r--r--src/remote/views.rs185
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 }),
- }
-}