aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorEllie Huxtable <e@elm.sh>2021-04-20 21:53:07 +0100
committerGitHub <noreply@github.com>2021-04-20 20:53:07 +0000
commita21737e2b7f8d1e426726bdd7536033f299d476a (patch)
treee940afdff9c145d25d9a2895fd44a77d70719a2e /src
parentSwitch to Warp + SQLx, use async, switch to Rust stable (#36) (diff)
downloadatuin-a21737e2b7f8d1e426726bdd7536033f299d476a.zip
Use cargo workspaces (#37)
* Switch to Cargo workspaces Breaking things into "client", "server" and "common" makes managing the codebase much easier! client - anything running on a user's machine for adding history server - handles storing/syncing history and running a HTTP server common - request/response API definitions, common utils, etc * Update dockerfile
Diffstat (limited to 'src')
-rw-r--r--src/api.rs70
-rw-r--r--src/command/history.rs10
-rw-r--r--src/command/import.rs6
-rw-r--r--src/command/login.rs8
-rw-r--r--src/command/mod.rs40
-rw-r--r--src/command/register.rs8
-rw-r--r--src/command/search.rs5
-rw-r--r--src/command/server.rs15
-rw-r--r--src/command/stats.rs8
-rw-r--r--src/command/sync.rs6
-rw-r--r--src/local/api_client.rs95
-rw-r--r--src/local/database.rs272
-rw-r--r--src/local/encryption.rs108
-rw-r--r--src/local/history.rs66
-rw-r--r--src/local/import.rs176
-rw-r--r--src/local/mod.rs6
-rw-r--r--src/local/sync.rs141
-rw-r--r--src/main.rs39
-rw-r--r--src/server/auth.rs222
-rw-r--r--src/server/database.rs202
-rw-r--r--src/server/handlers/history.rs89
-rw-r--r--src/server/handlers/mod.rs6
-rw-r--r--src/server/handlers/user.rs140
-rw-r--r--src/server/mod.rs23
-rw-r--r--src/server/models.rs49
-rw-r--r--src/server/router.rs121
-rw-r--r--src/settings.rs172
-rw-r--r--src/utils.rs24
28 files changed, 60 insertions, 2067 deletions
diff --git a/src/api.rs b/src/api.rs
deleted file mode 100644
index 82ee6604..00000000
--- a/src/api.rs
+++ /dev/null
@@ -1,70 +0,0 @@
-use chrono::Utc;
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct UserResponse {
- pub username: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct RegisterRequest {
- pub email: String,
- pub username: String,
- pub password: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct RegisterResponse {
- pub session: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct LoginRequest {
- pub username: String,
- pub password: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct LoginResponse {
- pub session: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct AddHistoryRequest {
- pub id: String,
- pub timestamp: chrono::DateTime<Utc>,
- pub data: String,
- pub hostname: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct CountResponse {
- pub count: i64,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct SyncHistoryRequest {
- pub sync_ts: chrono::DateTime<chrono::FixedOffset>,
- pub history_ts: chrono::DateTime<chrono::FixedOffset>,
- pub host: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct SyncHistoryResponse {
- pub history: Vec<String>,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct ErrorResponse {
- pub reason: String,
-}
-
-impl ErrorResponse {
- pub fn reply(reason: &str, status: warp::http::StatusCode) -> impl warp::Reply {
- warp::reply::with_status(
- warp::reply::json(&ErrorResponse {
- reason: String::from(reason),
- }),
- status,
- )
- }
-}
diff --git a/src/command/history.rs b/src/command/history.rs
index 627efae4..2b691bac 100644
--- a/src/command/history.rs
+++ b/src/command/history.rs
@@ -4,10 +4,10 @@ use eyre::Result;
use fork::{fork, Fork};
use structopt::StructOpt;
-use crate::local::database::Database;
-use crate::local::history::History;
-use crate::local::sync;
-use crate::settings::Settings;
+use atuin_client::database::Database;
+use atuin_client::history::History;
+use atuin_client::settings::Settings;
+use atuin_client::sync;
#[derive(StructOpt)]
pub enum Cmd {
@@ -79,7 +79,7 @@ impl Cmd {
db.update(&h)?;
- if settings.local.should_sync()? {
+ if settings.should_sync()? {
match fork() {
Ok(Fork::Parent(child)) => {
debug!("launched sync background process with PID {}", child);
diff --git a/src/command/import.rs b/src/command/import.rs
index ae927aaf..56fb30a7 100644
--- a/src/command/import.rs
+++ b/src/command/import.rs
@@ -5,9 +5,9 @@ use directories::UserDirs;
use eyre::{eyre, Result};
use structopt::StructOpt;
-use crate::local::database::Database;
-use crate::local::history::History;
-use crate::local::import::Zsh;
+use atuin_client::database::Database;
+use atuin_client::history::History;
+use atuin_client::import::Zsh;
use indicatif::ProgressBar;
#[derive(StructOpt)]
diff --git a/src/command/login.rs b/src/command/login.rs
index 636ac0d3..eaeb297f 100644
--- a/src/command/login.rs
+++ b/src/command/login.rs
@@ -5,7 +5,7 @@ use std::io::prelude::*;
use eyre::{eyre, Result};
use structopt::StructOpt;
-use crate::settings::Settings;
+use atuin_client::settings::Settings;
#[derive(StructOpt)]
#[structopt(setting(structopt::clap::AppSettings::DeriveDisplayOrder))]
@@ -26,7 +26,7 @@ impl Cmd {
map.insert("username", self.username.clone());
map.insert("password", self.password.clone());
- let url = format!("{}/login", settings.local.sync_address);
+ let url = format!("{}/login", settings.sync_address);
let client = reqwest::blocking::Client::new();
let resp = client.post(url).json(&map).send()?;
@@ -38,11 +38,11 @@ impl Cmd {
let session = resp.json::<HashMap<String, String>>()?;
let session = session["session"].clone();
- let session_path = settings.local.session_path.as_str();
+ let session_path = settings.session_path.as_str();
let mut file = File::create(session_path)?;
file.write_all(session.as_bytes())?;
- let key_path = settings.local.key_path.as_str();
+ let key_path = settings.key_path.as_str();
let mut file = File::create(key_path)?;
file.write_all(&base64::decode(self.key.clone())?)?;
diff --git a/src/command/mod.rs b/src/command/mod.rs
index cd857e9f..6fd52613 100644
--- a/src/command/mod.rs
+++ b/src/command/mod.rs
@@ -1,9 +1,12 @@
+use std::path::PathBuf;
+
use eyre::Result;
use structopt::StructOpt;
-use uuid::Uuid;
-use crate::local::database::Database;
-use crate::settings::Settings;
+use atuin_client::database::Sqlite;
+use atuin_client::settings::Settings as ClientSettings;
+use atuin_common::utils::uuid_v4;
+use atuin_server::settings::Settings as ServerSettings;
mod event;
mod history;
@@ -58,30 +61,33 @@ pub enum AtuinCmd {
Key,
}
-pub fn uuid_v4() -> String {
- Uuid::new_v4().to_simple().to_string()
-}
-
impl AtuinCmd {
- pub async fn run<T: Database + Send>(self, db: &mut T, settings: &Settings) -> Result<()> {
+ pub async fn run(self) -> Result<()> {
+ let client_settings = ClientSettings::new()?;
+ let server_settings = ServerSettings::new()?;
+
+ let db_path = PathBuf::from(client_settings.db_path.as_str());
+
+ let mut db = Sqlite::new(db_path)?;
+
match self {
- Self::History(history) => history.run(settings, db).await,
- Self::Import(import) => import.run(db),
- Self::Server(server) => server.run(settings).await,
- Self::Stats(stats) => stats.run(db, settings),
+ Self::History(history) => history.run(&client_settings, &mut db).await,
+ Self::Import(import) => import.run(&mut db),
+ Self::Server(server) => server.run(&server_settings).await,
+ Self::Stats(stats) => stats.run(&mut db, &client_settings),
Self::Init => init::init(),
- Self::Search { query } => search::run(&query, db),
+ Self::Search { query } => search::run(&query, &mut db),
- Self::Sync { force } => sync::run(settings, force, db).await,
- Self::Login(l) => l.run(settings),
+ Self::Sync { force } => sync::run(&client_settings, force, &mut db).await,
+ Self::Login(l) => l.run(&client_settings),
Self::Register(r) => register::run(
- settings,
+ &client_settings,
r.username.as_str(),
r.email.as_str(),
r.password.as_str(),
),
Self::Key => {
- let key = std::fs::read(settings.local.key_path.as_str())?;
+ let key = std::fs::read(client_settings.key_path.as_str())?;
println!("{}", base64::encode(key));
Ok(())
}
diff --git a/src/command/register.rs b/src/command/register.rs
index 62bbeaeb..1126645a 100644
--- a/src/command/register.rs
+++ b/src/command/register.rs
@@ -5,7 +5,7 @@ use std::io::prelude::*;
use eyre::{eyre, Result};
use structopt::StructOpt;
-use crate::settings::Settings;
+use atuin_client::settings::Settings;
#[derive(StructOpt)]
#[structopt(setting(structopt::clap::AppSettings::DeriveDisplayOrder))]
@@ -26,7 +26,7 @@ pub fn run(settings: &Settings, username: &str, email: &str, password: &str) ->
map.insert("email", email);
map.insert("password", password);
- let url = format!("{}/user/{}", settings.local.sync_address, username);
+ let url = format!("{}/user/{}", settings.sync_address, username);
let resp = reqwest::blocking::get(url)?;
if resp.status().is_success() {
@@ -34,7 +34,7 @@ pub fn run(settings: &Settings, username: &str, email: &str, password: &str) ->
return Ok(());
}
- let url = format!("{}/register", settings.local.sync_address);
+ let url = format!("{}/register", settings.sync_address);
let client = reqwest::blocking::Client::new();
let resp = client.post(url).json(&map).send()?;
@@ -46,7 +46,7 @@ pub fn run(settings: &Settings, username: &str, email: &str, password: &str) ->
let session = resp.json::<HashMap<String, String>>()?;
let session = session["session"].clone();
- let path = settings.local.session_path.as_str();
+ let path = settings.session_path.as_str();
let mut file = File::create(path)?;
file.write_all(session.as_bytes())?;
diff --git a/src/command/search.rs b/src/command/search.rs
index d7b477da..773c112f 100644
--- a/src/command/search.rs
+++ b/src/command/search.rs
@@ -14,9 +14,10 @@ use tui::{
};
use unicode_width::UnicodeWidthStr;
+use atuin_client::database::Database;
+use atuin_client::history::History;
+
use crate::command::event::{Event, Events};
-use crate::local::database::Database;
-use crate::local::history::History;
const VERSION: &str = env!("CARGO_PKG_VERSION");
diff --git a/src/command/server.rs b/src/command/server.rs
index a7835092..2fcf23d6 100644
--- a/src/command/server.rs
+++ b/src/command/server.rs
@@ -1,8 +1,8 @@
use eyre::Result;
use structopt::StructOpt;
-use crate::server;
-use crate::settings::Settings;
+use atuin_server::launch;
+use atuin_server::settings::Settings;
#[derive(StructOpt)]
pub enum Cmd {
@@ -23,13 +23,12 @@ impl Cmd {
pub async fn run(&self, settings: &Settings) -> Result<()> {
match self {
Self::Start { host, port } => {
- let host = host.as_ref().map_or(
- settings.server.host.clone(),
- std::string::ToString::to_string,
- );
- let port = port.map_or(settings.server.port, |p| p);
+ let host = host
+ .as_ref()
+ .map_or(settings.host.clone(), std::string::ToString::to_string);
+ let port = port.map_or(settings.port, |p| p);
- server::launch(settings, host, port).await
+ launch(settings, host, port).await
}
}
}
diff --git a/src/command/stats.rs b/src/command/stats.rs
index 694484bc..0da303d7 100644
--- a/src/command/stats.rs
+++ b/src/command/stats.rs
@@ -8,9 +8,9 @@ use cli_table::{format::Justify, print_stdout, Cell, Style, Table};
use eyre::{eyre, Result};
use structopt::StructOpt;
-use crate::local::database::Database;
-use crate::local::history::History;
-use crate::settings::Settings;
+use atuin_client::database::Database;
+use atuin_client::history::History;
+use atuin_client::settings::Settings;
#[derive(StructOpt)]
pub enum Cmd {
@@ -80,7 +80,7 @@ impl Cmd {
words.join(" ")
};
- let start = match settings.local.dialect.to_lowercase().as_str() {
+ let start = match settings.dialect.to_lowercase().as_str() {
"uk" => parse_date_string(&words, Local::now(), Dialect::Uk)?,
_ => parse_date_string(&words, Local::now(), Dialect::Us)?,
};
diff --git a/src/command/sync.rs b/src/command/sync.rs
index 88217b3c..d70b554f 100644
--- a/src/command/sync.rs
+++ b/src/command/sync.rs
@@ -1,8 +1,8 @@
use eyre::Result;
-use crate::local::database::Database;
-use crate::local::sync;
-use crate::settings::Settings;
+use atuin_client::database::Database;
+use atuin_client::settings::Settings;
+use atuin_client::sync;
pub async fn run(settings: &Settings, force: bool, db: &mut (impl Database + Send)) -> Result<()> {
sync::sync(settings, force, db).await?;
diff --git a/src/local/api_client.rs b/src/local/api_client.rs
deleted file mode 100644
index 1b64a295..00000000
--- a/src/local/api_client.rs
+++ /dev/null
@@ -1,95 +0,0 @@
-use chrono::Utc;
-use eyre::Result;
-use reqwest::header::{HeaderMap, AUTHORIZATION};
-use reqwest::Url;
-use sodiumoxide::crypto::secretbox;
-
-use crate::api::{AddHistoryRequest, CountResponse, SyncHistoryResponse};
-use crate::local::encryption::decrypt;
-use crate::local::history::History;
-use crate::utils::hash_str;
-
-pub struct Client<'a> {
- sync_addr: &'a str,
- token: &'a str,
- key: secretbox::Key,
- client: reqwest::Client,
-}
-
-impl<'a> Client<'a> {
- pub fn new(sync_addr: &'a str, token: &'a str, key: secretbox::Key) -> Self {
- Client {
- sync_addr,
- token,
- key,
- client: reqwest::Client::new(),
- }
- }
-
- pub async fn count(&self) -> Result<i64> {
- let url = format!("{}/sync/count", self.sync_addr);
- let url = Url::parse(url.as_str())?;
- let token = format!("Token {}", self.token);
- let token = token.parse()?;
-
- let mut headers = HeaderMap::new();
- headers.insert(AUTHORIZATION, token);
-
- let resp = self.client.get(url).headers(headers).send().await?;
-
- let count = resp.json::<CountResponse>().await?;
-
- Ok(count.count)
- }
-
- pub async fn get_history(
- &self,
- sync_ts: chrono::DateTime<Utc>,
- history_ts: chrono::DateTime<Utc>,
- host: Option<String>,
- ) -> Result<Vec<History>> {
- let host = match host {
- None => hash_str(&format!("{}:{}", whoami::hostname(), whoami::username())),
- Some(h) => h,
- };
-
- let url = format!(
- "{}/sync/history?sync_ts={}&history_ts={}&host={}",
- self.sync_addr,
- urlencoding::encode(sync_ts.to_rfc3339().as_str()),
- urlencoding::encode(history_ts.to_rfc3339().as_str()),
- host,
- );
-
- let resp = self
- .client
- .get(url)
- .header(AUTHORIZATION, format!("Token {}", self.token))
- .send()
- .await?;
-
- let history = resp.json::<SyncHistoryResponse>().await?;
- let history = history
- .history
- .iter()
- .map(|h| serde_json::from_str(h).expect("invalid base64"))
- .map(|h| decrypt(&h, &self.key).expect("failed to decrypt history! check your key"))
- .collect();
-
- Ok(history)
- }
-
- pub async fn post_history(&self, history: &[AddHistoryRequest]) -> Result<()> {
- let url = format!("{}/history", self.sync_addr);
- let url = Url::parse(url.as_str())?;
-
- self.client
- .post(url)
- .json(history)
- .header(AUTHORIZATION, format!("Token {}", self.token))
- .send()
- .await?;
-
- Ok(())
- }
-}
diff --git a/src/local/database.rs b/src/local/database.rs
deleted file mode 100644
index abc22bb8..00000000
--- a/src/local/database.rs
+++ /dev/null
@@ -1,272 +0,0 @@
-use chrono::prelude::*;
-use chrono::Utc;
-use std::path::Path;
-
-use eyre::Result;
-
-use rusqlite::{params, Connection};
-use rusqlite::{Params, Transaction};
-
-use super::history::History;
-
-pub trait Database {
- fn save(&mut self, h: &History) -> Result<()>;
- fn save_bulk(&mut self, h: &[History]) -> Result<()>;
-
- fn load(&self, id: &str) -> Result<History>;
- fn list(&self) -> Result<Vec<History>>;
- fn range(&self, from: chrono::DateTime<Utc>, to: chrono::DateTime<Utc>)
- -> Result<Vec<History>>;
-
- fn query(&self, query: &str, params: impl Params) -> Result<Vec<History>>;
- fn update(&self, h: &History) -> Result<()>;
- fn history_count(&self) -> Result<i64>;
-
- fn first(&self) -> Result<History>;
- fn last(&self) -> Result<History>;
- fn before(&self, timestamp: chrono::DateTime<Utc>, count: i64) -> Result<Vec<History>>;
-
- fn prefix_search(&self, query: &str) -> Result<Vec<History>>;
-}
-
-// Intended for use on a developer machine and not a sync server.
-// TODO: implement IntoIterator
-pub struct Sqlite {
- conn: Connection,
-}
-
-impl Sqlite {
- pub fn new(path: impl AsRef<Path>) -> Result<Self> {
- let path = path.as_ref();
- debug!("opening sqlite database at {:?}", path);
-
- let create = !path.exists();
- if create {
- if let Some(dir) = path.parent() {
- std::fs::create_dir_all(dir)?;
- }
- }
-
- let conn = Connection::open(path)?;
-
- Self::setup_db(&conn)?;
-
- Ok(Self { conn })
- }
-
- fn setup_db(conn: &Connection) -> Result<()> {
- debug!("running sqlite database setup");
-
- conn.execute(
- "create table if not exists history (
- id text primary key,
- timestamp integer not null,
- duration integer not null,
- exit integer not null,
- command text not null,
- cwd text not null,
- session text not null,
- hostname text not null,
-
- unique(timestamp, cwd, command)
- )",
- [],
- )?;
-
- conn.execute(
- "create table if not exists history_encrypted (
- id text primary key,
- data blob not null
- )",
- [],
- )?;
-
- Ok(())
- }
-
- fn save_raw(tx: &Transaction, h: &History) -> Result<()> {
- tx.execute(
- "insert or ignore into history (
- id,
- timestamp,
- duration,
- exit,
- command,
- cwd,
- session,
- hostname
- ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
- params![
- h.id,
- h.timestamp.timestamp_nanos(),
- h.duration,
- h.exit,
- h.command,
- h.cwd,
- h.session,
- h.hostname
- ],
- )?;
-
- Ok(())
- }
-}
-
-impl Database for Sqlite {
- fn save(&mut self, h: &History) -> Result<()> {
- debug!("saving history to sqlite");
-
- let tx = self.conn.transaction()?;
- Self::save_raw(&tx, h)?;
- tx.commit()?;
-
- Ok(())
- }
-
- fn save_bulk(&mut self, h: &[History]) -> Result<()> {
- debug!("saving history to sqlite");
-
- let tx = self.conn.transaction()?;
- for i in h {
- Self::save_raw(&tx, i)?
- }
- tx.commit()?;
-
- Ok(())
- }
-
- fn load(&self, id: &str) -> Result<History> {
- debug!("loading history item");
-
- let mut stmt = self.conn.prepare(
- "select id, timestamp, duration, exit, command, cwd, session, hostname from history
- where id = ?1",
- )?;
-
- let history = stmt.query_row(params![id], |row| {
- history_from_sqlite_row(Some(id.to_string()), row)
- })?;
-
- Ok(history)
- }
-
- fn update(&self, h: &History) -> Result<()> {
- debug!("updating sqlite history");
-
- self.conn.execute(
- "update history
- set timestamp = ?2, duration = ?3, exit = ?4, command = ?5, cwd = ?6, session = ?7, hostname = ?8
- where id = ?1",
- params![h.id, h.timestamp.timestamp_nanos(), h.duration, h.exit, h.command, h.cwd, h.session, h.hostname],
- )?;
-
- Ok(())
- }
-
- fn list(&self) -> Result<Vec<History>> {
- debug!("listing history");
-
- let mut stmt = self
- .conn
- .prepare("SELECT * FROM history order by timestamp asc")?;
-
- let history_iter = stmt.query_map(params![], |row| history_from_sqlite_row(None, row))?;
-
- Ok(history_iter.filter_map(Result::ok).collect())
- }
-
- fn range(
- &self,
- from: chrono::DateTime<Utc>,
- to: chrono::DateTime<Utc>,
- ) -> Result<Vec<History>> {
- debug!("listing history from {:?} to {:?}", from, to);
-
- let mut stmt = self.conn.prepare(
- "SELECT * FROM history where timestamp >= ?1 and timestamp <= ?2 order by timestamp asc",
- )?;
-
- let history_iter = stmt.query_map(
- params![from.timestamp_nanos(), to.timestamp_nanos()],
- |row| history_from_sqlite_row(None, row),
- )?;
-
- Ok(history_iter.filter_map(Result::ok).collect())
- }
-
- fn first(&self) -> Result<History> {
- let mut stmt = self
- .conn
- .prepare("SELECT * FROM history order by timestamp asc limit 1")?;
-
- let history = stmt.query_row(params![], |row| history_from_sqlite_row(None, row))?;
-
- Ok(history)
- }
-
- fn last(&self) -> Result<History> {
- let mut stmt = self
- .conn
- .prepare("SELECT * FROM history order by timestamp desc limit 1")?;
-
- let history = stmt.query_row(params![], |row| history_from_sqlite_row(None, row))?;
-
- Ok(history)
- }
-
- fn before(&self, timestamp: chrono::DateTime<Utc>, count: i64) -> Result<Vec<History>> {
- let mut stmt = self
- .conn
- .prepare("SELECT * FROM history where timestamp < ? order by timestamp desc limit ?")?;
-
- let history_iter = stmt.query_map(params![timestamp.timestamp_nanos(), count], |row| {
- history_from_sqlite_row(None, row)
- })?;
-
- Ok(history_iter.filter_map(Result::ok).collect())
- }
-
- fn query(&self, query: &str, params: impl Params) -> Result<Vec<History>> {
- let mut stmt = self.conn.prepare(query)?;
-
- let history_iter = stmt.query_map(params, |row| history_from_sqlite_row(None, row))?;
-
- Ok(history_iter.filter_map(Result::ok).collect())
- }
-
- fn prefix_search(&self, query: &str) -> Result<Vec<History>> {
- self.query(
- "select * from history where command like ?1 || '%' order by timestamp asc limit 1000",
- &[query],
- )
- }
-
- fn history_count(&self) -> Result<i64> {
- let res: i64 =
- self.conn
- .query_row_and_then("select count(1) from history;", params![], |row| row.get(0))?;
-
- Ok(res)
- }
-}
-
-fn history_from_sqlite_row(
- id: Option<String>,
- row: &rusqlite::Row,
-) -> Result<History, rusqlite::Error> {
- let id = match id {
- Some(id) => id,
- None => row.get(0)?,
- };
-
- Ok(History {
- id,
- timestamp: Utc.timestamp_nanos(row.get(1)?),
- duration: row.get(2)?,
- exit: row.get(3)?,
- command: row.get(4)?,
- cwd: row.get(5)?,
- session: row.get(6)?,
- hostname: row.get(7)?,
- })
-}
diff --git a/src/local/encryption.rs b/src/local/encryption.rs
deleted file mode 100644
index 3c1699e3..00000000
--- a/src/local/encryption.rs
+++ /dev/null
@@ -1,108 +0,0 @@
-// The general idea is that we NEVER send cleartext history to the server
-// This way the odds of anything private ending up where it should not are
-// very low
-// The server authenticates via the usual username and password. This has
-// nothing to do with the encryption, and is purely authentication! The client
-// generates its own secret key, and encrypts all shell history with libsodium's
-// secretbox. The data is then sent to the server, where it is stored. All
-// clients must share the secret in order to be able to sync, as it is needed
-// to decrypt
-
-use std::fs::File;
-use std::io::prelude::*;
-use std::path::PathBuf;
-
-use eyre::{eyre, Result};
-use sodiumoxide::crypto::secretbox;
-
-use crate::local::history::History;
-use crate::settings::Settings;
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct EncryptedHistory {
- pub ciphertext: Vec<u8>,
- pub nonce: secretbox::Nonce,
-}
-
-// Loads the secret key, will create + save if it doesn't exist
-pub fn load_key(settings: &Settings) -> Result<secretbox::Key> {
- let path = settings.local.key_path.as_str();
-
- if PathBuf::from(path).exists() {
- let bytes = std::fs::read(path)?;
- let key: secretbox::Key = rmp_serde::from_read_ref(&bytes)?;
- Ok(key)
- } else {
- let key = secretbox::gen_key();
- let buf = rmp_serde::to_vec(&key)?;
-
- let mut file = File::create(path)?;
- file.write_all(&buf)?;
-
- Ok(key)
- }
-}
-
-pub fn encrypt(history: &History, key: &secretbox::Key) -> Result<EncryptedHistory> {
- // serialize with msgpack
- let buf = rmp_serde::to_vec(history)?;
-
- let nonce = secretbox::gen_nonce();
-
- let ciphertext = secretbox::seal(&buf, &nonce, key);
-
- Ok(EncryptedHistory { ciphertext, nonce })
-}
-
-pub fn decrypt(encrypted_history: &EncryptedHistory, key: &secretbox::Key) -> Result<History> {
- let plaintext = secretbox::open(&encrypted_history.ciphertext, &encrypted_history.nonce, key)
- .map_err(|_| eyre!("failed to open secretbox - invalid key?"))?;
-
- let history = rmp_serde::from_read_ref(&plaintext)?;
-
- Ok(history)
-}
-
-#[cfg(test)]
-mod test {
- use sodiumoxide::crypto::secretbox;
-
- use crate::local::history::History;
-
- use super::{decrypt, encrypt};
-
- #[test]
- fn test_encrypt_decrypt() {
- let key1 = secretbox::gen_key();
- let key2 = secretbox::gen_key();
-
- let history = History::new(
- chrono::Utc::now(),
- "ls".to_string(),
- "/home/ellie".to_string(),
- 0,
- 1,
- Some("beep boop".to_string()),
- Some("booop".to_string()),
- );
-
- let e1 = encrypt(&history, &key1).unwrap();
- let e2 = encrypt(&history, &key2).unwrap();
-
- assert_ne!(e1.ciphertext, e2.ciphertext);
- assert_ne!(e1.nonce, e2.nonce);
-
- // test decryption works
- // this should pass
- match decrypt(&e1, &key1) {
- Err(e) => assert!(false, "failed to decrypt, got {}", e),
- Ok(h) => assert_eq!(h, history),
- };
-
- // this should err
- match decrypt(&e2, &key1) {
- Ok(_) => assert!(false, "expected an error decrypting with invalid key"),
- Err(_) => {}
- };
- }
-}
diff --git a/src/local/history.rs b/src/local/history.rs
deleted file mode 100644
index 1712f8b9..00000000
--- a/src/local/history.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-use std::env;
-use std::hash::{Hash, Hasher};
-
-use chrono::Utc;
-
-use crate::command::uuid_v4;
-
-// Any new fields MUST be Optional<>!
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct History {
- pub id: String,
- pub timestamp: chrono::DateTime<Utc>,
- pub duration: i64,
- pub exit: i64,
- pub command: String,
- pub cwd: String,
- pub session: String,
- pub hostname: String,
-}
-
-impl History {
- pub fn new(
- timestamp: chrono::DateTime<Utc>,
- command: String,
- cwd: String,
- exit: i64,
- duration: i64,
- session: Option<String>,
- hostname: Option<String>,
- ) -> Self {
- let session = session
- .or_else(|| env::var("ATUIN_SESSION").ok())
- .unwrap_or_else(uuid_v4);
- let hostname =
- hostname.unwrap_or_else(|| format!("{}:{}", whoami::hostname(), whoami::username()));
-
- Self {
- id: uuid_v4(),
- timestamp,
- command,
- cwd,
- exit,
- duration,
- session,
- hostname,
- }
- }
-}
-
-impl PartialEq for History {
- // for the sakes of listing unique history only, we do not care about
- // anything else
- // obviously this does not refer to the *same* item of history, but when
- // we only render the command, it looks the same
- fn eq(&self, other: &Self) -> bool {
- self.command == other.command
- }
-}
-
-impl Eq for History {}
-
-impl Hash for History {
- fn hash<H: Hasher>(&self, state: &mut H) {
- self.command.hash(state);
- }
-}
diff --git a/src/local/import.rs b/src/local/import.rs
deleted file mode 100644
index 3b0b2a69..00000000
--- a/src/local/import.rs
+++ /dev/null
@@ -1,176 +0,0 @@
-// import old shell history!
-// automatically hoover up all that we can find
-
-use std::io::{BufRead, BufReader, Seek, SeekFrom};
-use std::{fs::File, path::Path};
-
-use chrono::prelude::*;
-use chrono::Utc;
-use eyre::{eyre, Result};
-use itertools::Itertools;
-
-use super::history::History;
-
-#[derive(Debug)]
-pub struct Zsh {
- file: BufReader<File>,
-
- pub loc: u64,
- pub counter: i64,
-}
-
-// this could probably be sped up
-fn count_lines(buf: &mut BufReader<File>) -> Result<usize> {
- let lines = buf.lines().count();
- buf.seek(SeekFrom::Start(0))?;
-
- Ok(lines)
-}
-
-impl Zsh {
- pub fn new(path: impl AsRef<Path>) -> Result<Self> {
- let file = File::open(path)?;
- let mut buf = BufReader::new(file);
- let loc = count_lines(&mut buf)?;
-
- Ok(Self {
- file: buf,
- loc: loc as u64,
- counter: 0,
- })
- }
-}
-
-fn parse_extended(line: &str, counter: i64) -> History {
- let line = line.replacen(": ", "", 2);
- let (time, duration) = line.splitn(2, ':').collect_tuple().unwrap();
- let (duration, command) = duration.splitn(2, ';').collect_tuple().unwrap();
-
- let time = time
- .parse::<i64>()
- .unwrap_or_else(|_| chrono::Utc::now().timestamp());
-
- let offset = chrono::Duration::milliseconds(counter);
- let time = Utc.timestamp(time, 0);
- let time = time + offset;
-
- let duration = duration.parse::<i64>().map_or(-1, |t| t * 1_000_000_000);
-
- // use nanos, because why the hell not? we won't display them.
- History::new(
- time,
- command.trim_end().to_string(),
- String::from("unknown"),
- 0, // assume 0, we have no way of knowing :(
- duration,
- None,
- None,
- )
-}
-
-impl Zsh {
- fn read_line(&mut self) -> Option<Result<String>> {
- let mut line = String::new();
-
- match self.file.read_line(&mut line) {
- Ok(0) => None,
- Ok(_) => Some(Ok(line)),
- Err(e) => Some(Err(eyre!("failed to read line: {}", e))), // we can skip past things like invalid utf8
- }
- }
-}
-
-impl Iterator for Zsh {
- type Item = Result<History>;
-
- fn next(&mut self) -> Option<Self::Item> {
- // ZSH extended history records the timestamp + command duration
- // These lines begin with :
- // So, if the line begins with :, parse it. Otherwise it's just
- // the command
- let line = self.read_line()?;
-
- if let Err(e) = line {
- return Some(Err(e)); // :(
- }
-
- let mut line = line.unwrap();
-
- while line.ends_with("\\\n") {
- let next_line = self.read_line()?;
-
- if next_line.is_err() {
- // There's a chance that the last line of a command has invalid
- // characters, the only safe thing to do is break :/
- // usually just invalid utf8 or smth
- // however, we really need to avoid missing history, so it's
- // better to have some items that should have been part of
- // something else, than to miss things. So break.
- break;
- }
-
- line.push_str(next_line.unwrap().as_str());
- }
-
- // We have to handle the case where a line has escaped newlines.
- // Keep reading until we have a non-escaped newline
-
- let extended = line.starts_with(':');
-
- if extended {
- self.counter += 1;
- Some(Ok(parse_extended(line.as_str(), self.counter)))
- } else {
- let time = chrono::Utc::now();
- let offset = chrono::Duration::seconds(self.counter);
- let time = time - offset;
-
- self.counter += 1;
-
- Some(Ok(History::new(
- time,
- line.trim_end().to_string(),
- String::from("unknown"),
- -1,
- -1,
- None,
- None,
- )))
- }
- }
-}
-
-#[cfg(test)]
-mod test {
- use chrono::prelude::*;
- use chrono::Utc;
-
- use super::parse_extended;
-
- #[test]
- fn test_parse_extended_simple() {
- let parsed = parse_extended(": 1613322469:0;cargo install atuin", 0);
-
- assert_eq!(parsed.command, "cargo install atuin");
- assert_eq!(parsed.duration, 0);
- assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0));
-
- let parsed = parse_extended(": 1613322469:10;cargo install atuin;cargo update", 0);
-
- assert_eq!(parsed.command, "cargo install atuin;cargo update");
- assert_eq!(parsed.duration, 10_000_000_000);
- assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0));
-
- let parsed = parse_extended(": 1613322469:10;cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷", 0);
-
- assert_eq!(parsed.command, "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷");
- assert_eq!(parsed.duration, 10_000_000_000);
- assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0));
-
- let parsed = parse_extended(": 1613322469:10;cargo install \\n atuin\n", 0);
-
- assert_eq!(parsed.command, "cargo install \\n atuin");
- assert_eq!(parsed.duration, 10_000_000_000);
- assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0));
- }
-}
diff --git a/src/local/mod.rs b/src/local/mod.rs
deleted file mode 100644
index 9fe31292..00000000
--- a/src/local/mod.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-pub mod api_client;
-pub mod database;
-pub mod encryption;
-pub mod history;
-pub mod import;
-pub mod sync;
diff --git a/src/local/sync.rs b/src/local/sync.rs
deleted file mode 100644
index e0feb759..00000000
--- a/src/local/sync.rs
+++ /dev/null
@@ -1,141 +0,0 @@
-use std::convert::TryInto;
-
-use chrono::prelude::*;
-use eyre::Result;
-
-use crate::local::api_client;
-use crate::local::database::Database;
-use crate::local::encryption::{encrypt, load_key};
-use crate::settings::{Local, Settings, HISTORY_PAGE_SIZE};
-use crate::{api::AddHistoryRequest, utils::hash_str};
-
-// Currently sync is kinda naive, and basically just pages backwards through
-// history. This means newly added stuff shows up properly! We also just use
-// the total count in each database to indicate whether a sync is needed.
-// I think this could be massively improved! If we had a way of easily
-// indicating count per time period (hour, day, week, year, etc) then we can
-// easily pinpoint where we are missing data and what needs downloading. Start
-// with year, then find the week, then the day, then the hour, then download it
-// all! The current naive approach will do for now.
-
-// Check if remote has things we don't, and if so, download them.
-// Returns (num downloaded, total local)
-async fn sync_download(
- force: bool,
- client: &api_client::Client<'_>,
- db: &mut (impl Database + Send),
-) -> Result<(i64, i64)> {
- let remote_count = client.count().await?;
-
- let initial_local = db.history_count()?;
- let mut local_count = initial_local;
-
- let mut last_sync = if force {
- Utc.timestamp_millis(0)
- } else {
- Local::last_sync()?
- };
-
- let mut last_timestamp = Utc.timestamp_millis(0);
-
- let host = if force { Some(String::from("")) } else { None };
-
- while remote_count > local_count {
- let page = client
- .get_history(last_sync, last_timestamp, host.clone())
- .await?;
-
- if page.len() < HISTORY_PAGE_SIZE.try_into().unwrap() {
- break;
- }
-
- db.save_bulk(&page)?;
-
- local_count = db.history_count()?;
-
- let page_last = page
- .last()
- .expect("could not get last element of page")
- .timestamp;
-
- // in the case of a small sync frequency, it's possible for history to
- // be "lost" between syncs. In this case we need to rewind the sync
- // timestamps
- if page_last == last_timestamp {
- last_timestamp = Utc.timestamp_millis(0);
- last_sync = last_sync - chrono::Duration::hours(1);
- } else {
- last_timestamp = page_last;
- }
- }
-
- Ok((local_count - initial_local, local_count))
-}
-
-// Check if we have things remote doesn't, and if so, upload them
-async fn sync_upload(
- settings: &Settings,
- _force: bool,
- client: &api_client::Client<'_>,
- db: &mut (impl Database + Send),
-) -> Result<()> {
- let initial_remote_count = client.count().await?;
- let mut remote_count = initial_remote_count;
-
- let local_count = db.history_count()?;
-
- let key = load_key(settings)?; // encryption key
-
- // first just try the most recent set
-
- let mut cursor = Utc::now();
-
- while local_count > remote_count {
- let last = db.before(cursor, HISTORY_PAGE_SIZE)?;
- let mut buffer = Vec::<AddHistoryRequest>::new();
-
- if last.is_empty() {
- break;
- }
-
- for i in last {
- let data = encrypt(&i, &key)?;
- let data = serde_json::to_string(&data)?;
-
- let add_hist = AddHistoryRequest {
- id: i.id,
- timestamp: i.timestamp,
- data,
- hostname: hash_str(i.hostname.as_str()),
- };
-
- buffer.push(add_hist);
- }
-
- // anything left over outside of the 100 block size
- client.post_history(&buffer).await?;
- cursor = buffer.last().unwrap().timestamp;
-
- remote_count = client.count().await?;
- }
-
- Ok(())
-}
-
-pub async fn sync(settings: &Settings, force: bool, db: &mut (impl Database + Send)) -> Result<()> {
- let client = api_client::Client::new(
- settings.local.sync_address.as_str(),
- settings.local.session_token.as_str(),
- load_key(settings)?,
- );
-
- sync_upload(settings, force, &client, db).await?;
-
- let download = sync_download(force, &client, db).await?;
-
- debug!("sync downloaded {}", download.0);
-
- Local::save_sync_time()?;
-
- Ok(())
-}
diff --git a/src/main.rs b/src/main.rs
index 0045a943..c116d1f3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,29 +1,16 @@
#![warn(clippy::pedantic, clippy::nursery)]
#![allow(clippy::use_self)] // not 100% reliable
-use std::path::PathBuf;
-
-use eyre::{eyre, Result};
+use eyre::Result;
use fern::colors::{Color, ColoredLevelConfig};
-use human_panic::setup_panic;
use structopt::{clap::AppSettings, StructOpt};
#[macro_use]
extern crate log;
-#[macro_use]
-extern crate serde_derive;
-
use command::AtuinCmd;
-use local::database::Sqlite;
-use settings::Settings;
-mod api;
mod command;
-mod local;
-mod server;
-mod settings;
-mod utils;
#[derive(StructOpt)]
#[structopt(
@@ -33,28 +20,13 @@ mod utils;
global_settings(&[AppSettings::ColoredHelp, AppSettings::DeriveDisplayOrder])
)]
struct Atuin {
- #[structopt(long, parse(from_os_str), help = "db file path")]
- db: Option<PathBuf>,
-
#[structopt(subcommand)]
atuin: AtuinCmd,
}
impl Atuin {
- async fn run(self, settings: &Settings) -> Result<()> {
- let db_path = if let Some(db_path) = self.db {
- let path = db_path
- .to_str()
- .ok_or_else(|| eyre!("path {:?} was not valid UTF-8", db_path))?;
- let path = shellexpand::full(path)?;
- PathBuf::from(path.as_ref())
- } else {
- PathBuf::from(settings.local.db_path.as_str())
- };
-
- let mut db = Sqlite::new(db_path)?;
-
- self.atuin.run(&mut db, settings).await
+ async fn run(self) -> Result<()> {
+ self.atuin.run().await
}
}
@@ -78,8 +50,5 @@ async fn main() -> Result<()> {
.chain(std::io::stdout())
.apply()?;
- let settings = Settings::new()?;
- setup_panic!();
-
- Atuin::from_args().run(&settings).await
+ Atuin::from_args().run().await
}
diff --git a/src/server/auth.rs b/src/server/auth.rs
deleted file mode 100644
index 52a73108..00000000
--- a/src/server/auth.rs
+++ /dev/null
@@ -1,222 +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/server/database.rs b/src/server/database.rs
deleted file mode 100644
index 5945baaf..00000000
--- a/src/server/database.rs
+++ /dev/null
@@ -1,202 +0,0 @@
-use async_trait::async_trait;
-
-use eyre::{eyre, Result};
-use sqlx::postgres::PgPoolOptions;
-
-use crate::settings::HISTORY_PAGE_SIZE;
-
-use super::models::{History, NewHistory, NewSession, NewUser, Session, User};
-
-#[async_trait]
-pub trait Database {
- async fn get_session(&self, token: &str) -> Result<Session>;
- async fn get_session_user(&self, token: &str) -> Result<User>;
- async fn add_session(&self, session: &NewSession) -> Result<()>;
-
- async fn get_user(&self, username: String) -> Result<User>;
- async fn get_user_session(&self, u: &User) -> Result<Session>;
- async fn add_user(&self, user: NewUser) -> Result<i64>;
-
- async fn count_history(&self, user: &User) -> Result<i64>;
- async fn list_history(
- &self,
- user: &User,
- created_since: chrono::NaiveDateTime,
- since: chrono::NaiveDateTime,
- host: String,
- ) -> Result<Vec<History>>;
- async fn add_history(&self, history: &[NewHistory]) -> Result<()>;
-}
-
-#[derive(Clone)]
-pub struct Postgres {
- pool: sqlx::Pool<sqlx::postgres::Postgres>,
-}
-
-impl Postgres {
- pub async fn new(uri: &str) -> Result<Self, sqlx::Error> {
- let pool = PgPoolOptions::new()
- .max_connections(100)
- .connect(uri)
- .await?;
-
- Ok(Self { pool })
- }
-}
-
-#[async_trait]
-impl Database for Postgres {
- async fn get_session(&self, token: &str) -> Result<Session> {
- let res: Option<Session> =
- sqlx::query_as::<_, Session>("select * from sessions where token = $1")
- .bind(token)
- .fetch_optional(&self.pool)
- .await?;
-
- if let Some(s) = res {
- Ok(s)
- } else {
- Err(eyre!("could not find session"))
- }
- }
-
- async fn get_user(&self, username: String) -> Result<User> {
- let res: Option<User> =
- sqlx::query_as::<_, User>("select * from users where username = $1")
- .bind(username)
- .fetch_optional(&self.pool)
- .await?;
-
- if let Some(u) = res {
- Ok(u)
- } else {
- Err(eyre!("could not find user"))
- }
- }
-
- async fn get_session_user(&self, token: &str) -> Result<User> {
- let res: Option<User> = sqlx::query_as::<_, User>(
- "select * from users
- inner join sessions
- on users.id = sessions.user_id
- and sessions.token = $1",
- )
- .bind(token)
- .fetch_optional(&self.pool)
- .await?;
-
- if let Some(u) = res {
- Ok(u)
- } else {
- Err(eyre!("could not find user"))
- }
- }
-
- async fn count_history(&self, user: &User) -> Result<i64> {
- let res: (i64,) = sqlx::query_as(
- "select count(1) from history
- where user_id = $1",
- )
- .bind(user.id)
- .fetch_one(&self.pool)
- .await?;
-
- Ok(res.0)
- }
-
- async fn list_history(
- &self,
- user: &User,
- created_since: chrono::NaiveDateTime,
- since: chrono::NaiveDateTime,
- host: String,
- ) -> Result<Vec<History>> {
- let res = sqlx::query_as::<_, History>(
- "select * from history
- where user_id = $1
- and hostname != $2
- and created_at >= $3
- and timestamp >= $4
- order by timestamp asc
- limit $5",
- )
- .bind(user.id)
- .bind(host)
- .bind(created_since)
- .bind(since)
- .bind(HISTORY_PAGE_SIZE)
- .fetch_all(&self.pool)
- .await?;
-
- Ok(res)
- }
-
- async fn add_history(&self, history: &[NewHistory]) -> Result<()> {
- let mut tx = self.pool.begin().await?;
-
- for i in history {
- sqlx::query(
- "insert into history
- (client_id, user_id, hostname, timestamp, data)
- values ($1, $2, $3, $4, $5)
- on conflict do nothing
- ",
- )
- .bind(i.client_id)
- .bind(i.user_id)
- .bind(i.hostname)
- .bind(i.timestamp)
- .bind(i.data)
- .execute(&mut tx)
- .await?;
- }
-
- tx.commit().await?;
-
- Ok(())
- }
-
- async fn add_user(&self, user: NewUser) -> Result<i64> {
- let res: (i64,) = sqlx::query_as(
- "insert into users
- (username, email, password)
- values($1, $2, $3)
- returning id",
- )
- .bind(user.username.as_str())
- .bind(user.email.as_str())
- .bind(user.password)
- .fetch_one(&self.pool)
- .await?;
-
- Ok(res.0)
- }
-
- async fn add_session(&self, session: &NewSession) -> Result<()> {
- sqlx::query(
- "insert into sessions
- (user_id, token)
- values($1, $2)",
- )
- .bind(session.user_id)
- .bind(session.token)
- .execute(&self.pool)
- .await?;
-
- Ok(())
- }
-
- async fn get_user_session(&self, u: &User) -> Result<Session> {
- let res: Option<Session> =
- sqlx::query_as::<_, Session>("select * from sessions where user_id = $1")
- .bind(u.id)
- .fetch_optional(&self.pool)
- .await?;
-
- if let Some(s) = res {
- Ok(s)
- } else {
- Err(eyre!("could not find session"))
- }
- }
-}
diff --git a/src/server/handlers/history.rs b/src/server/handlers/history.rs
deleted file mode 100644
index 4fd6f03f..00000000
--- a/src/server/handlers/history.rs
+++ /dev/null
@@ -1,89 +0,0 @@
-use std::convert::Infallible;
-
-use warp::{http::StatusCode, reply::json};
-
-use crate::api::{
- AddHistoryRequest, CountResponse, ErrorResponse, SyncHistoryRequest, SyncHistoryResponse,
-};
-use crate::server::database::Database;
-use crate::server::models::{NewHistory, User};
-
-pub async fn count(
- user: User,
- db: impl Database + Clone + Send + Sync,
-) -> Result<Box<dyn warp::Reply>, Infallible> {
- db.count_history(&user).await.map_or(
- Ok(Box::new(ErrorResponse::reply(
- "failed to query history count",
- StatusCode::INTERNAL_SERVER_ERROR,
- ))),
- |count| Ok(Box::new(json(&CountResponse { count }))),
- )
-}
-
-pub async fn list(
- req: SyncHistoryRequest,
- user: User,
- db: impl Database + Clone + Send + Sync,
-) -> Result<Box<dyn warp::Reply>, Infallible> {
- let history = db
- .list_history(
- &user,
- req.sync_ts.naive_utc(),
- req.history_ts.naive_utc(),
- req.host,
- )
- .await;
-
- if let Err(e) = history {
- error!("failed to load history: {}", e);
- let resp =
- ErrorResponse::reply("failed to load history", StatusCode::INTERNAL_SERVER_ERROR);
- let resp = Box::new(resp);
- return Ok(resp);
- }
-
- let history: Vec<String> = history
- .unwrap()
- .iter()
- .map(|i| i.data.to_string())
- .collect();
-
- debug!(
- "loaded {} items of history for user {}",
- history.len(),
- user.id
- );
-
- Ok(Box::new(json(&SyncHistoryResponse { history })))
-}
-
-pub async fn add(
- req: Vec<AddHistoryRequest>,
- user: User,
- db: impl Database + Clone + Send + Sync,
-) -> Result<Box<dyn warp::Reply>, Infallible> {
- debug!("request to add {} history items", req.len());
-
- let history: Vec<NewHistory> = req
- .iter()
- .map(|h| NewHistory {
- client_id: h.id.as_str(),
- user_id: user.id,
- hostname: h.hostname.as_str(),
- timestamp: h.timestamp.naive_utc(),
- data: h.data.as_str(),
- })
- .collect();
-
- if let Err(e) = db.add_history(&history).await {
- error!("failed to add history: {}", e);
-
- return Ok(Box::new(ErrorResponse::reply(
- "failed to add history",
- StatusCode::INTERNAL_SERVER_ERROR,
- )));
- };
-
- Ok(Box::new(warp::reply()))
-}
diff --git a/src/server/handlers/mod.rs b/src/server/handlers/mod.rs
deleted file mode 100644
index 3c20538c..00000000
--- a/src/server/handlers/mod.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-pub mod history;
-pub mod user;
-
-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"
-}
diff --git a/src/server/handlers/user.rs b/src/server/handlers/user.rs
deleted file mode 100644
index 782d7dbd..00000000
--- a/src/server/handlers/user.rs
+++ /dev/null
@@ -1,140 +0,0 @@
-use std::convert::Infallible;
-
-use sodiumoxide::crypto::pwhash::argon2id13;
-use uuid::Uuid;
-use warp::http::StatusCode;
-use warp::reply::json;
-
-use crate::api::{
- ErrorResponse, LoginRequest, LoginResponse, RegisterRequest, RegisterResponse, UserResponse,
-};
-use crate::server::database::Database;
-use crate::server::models::{NewSession, NewUser};
-use crate::settings::Settings;
-use crate::utils::hash_secret;
-
-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,
- }
-}
-
-pub async fn get(
- username: String,
- db: impl Database + Clone + Send + Sync,
-) -> Result<Box<dyn warp::Reply>, Infallible> {
- let user = match db.get_user(username).await {
- Ok(user) => user,
- Err(e) => {
- debug!("user not found: {}", e);
- return Ok(Box::new(ErrorResponse::reply(
- "user not found",
- StatusCode::NOT_FOUND,
- )));
- }
- };
-
- Ok(Box::new(warp::reply::json(&UserResponse {
- username: user.username,
- })))
-}
-
-pub async fn register(
- register: RegisterRequest,
- settings: Settings,
- db: impl Database + Clone + Send + Sync,
-) -> Result<Box<dyn warp::Reply>, Infallible> {
- if !settings.server.open_registration {
- return Ok(Box::new(ErrorResponse::reply(
- "this server is not open for registrations",
- StatusCode::BAD_REQUEST,
- )));
- }
-
- let hashed = hash_secret(register.password.as_str());
-
- let new_user = NewUser {
- email: register.email,
- username: register.username,
- password: hashed,
- };
-
- let user_id = match db.add_user(new_user).await {
- Ok(id) => id,
- Err(e) => {
- error!("failed to add user: {}", e);
- return Ok(Box::new(ErrorResponse::reply(
- "failed to add user",
- StatusCode::BAD_REQUEST,
- )));
- }
- };
-
- let token = Uuid::new_v4().to_simple().to_string();
-
- let new_session = NewSession {
- user_id,
- token: token.as_str(),
- };
-
- match db.add_session(&new_session).await {
- Ok(_) => Ok(Box::new(json(&RegisterResponse { session: token }))),
- Err(e) => {
- error!("failed to add session: {}", e);
- Ok(Box::new(ErrorResponse::reply(
- "failed to register user",
- StatusCode::BAD_REQUEST,
- )))
- }
- }
-}
-
-pub async fn login(
- login: LoginRequest,
- db: impl Database + Clone + Send + Sync,
-) -> Result<Box<dyn warp::Reply>, Infallible> {
- let user = match db.get_user(login.username.clone()).await {
- Ok(u) => u,
- Err(e) => {
- error!("failed to get user {}: {}", login.username.clone(), e);
-
- return Ok(Box::new(ErrorResponse::reply(
- "user not found",
- StatusCode::NOT_FOUND,
- )));
- }
- };
-
- let session = match db.get_user_session(&user).await {
- Ok(u) => u,
- Err(e) => {
- error!("failed to get session for {}: {}", login.username, e);
-
- return Ok(Box::new(ErrorResponse::reply(
- "user not found",
- StatusCode::NOT_FOUND,
- )));
- }
- };
-
- let verified = verify_str(user.password.as_str(), login.password.as_str());
-
- if !verified {
- return Ok(Box::new(ErrorResponse::reply(
- "user not found",
- StatusCode::NOT_FOUND,
- )));
- }
-
- Ok(Box::new(warp::reply::json(&LoginResponse {
- session: session.token,
- })))
-}
diff --git a/src/server/mod.rs b/src/server/mod.rs
deleted file mode 100644
index d5e083df..00000000
--- a/src/server/mod.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-use std::net::IpAddr;
-
-use eyre::Result;
-
-use crate::settings::Settings;
-
-pub mod auth;
-pub mod database;
-pub mod handlers;
-pub mod models;
-pub mod router;
-
-pub async fn launch(settings: &Settings, host: String, port: u16) -> Result<()> {
- // routes to run:
- // index, register, add_history, login, get_user, sync_count, sync_list
- let host = host.parse::<IpAddr>()?;
-
- let r = router::router(settings).await?;
-
- warp::serve(r).run((host, port)).await;
-
- Ok(())
-}
diff --git a/src/server/models.rs b/src/server/models.rs
deleted file mode 100644
index fbf1897e..00000000
--- a/src/server/models.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-use chrono::prelude::*;
-
-#[derive(sqlx::FromRow)]
-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,
-}
-
-pub struct NewHistory<'a> {
- pub client_id: &'a str,
- pub user_id: i64,
- pub hostname: &'a str,
- pub timestamp: chrono::NaiveDateTime,
-
- pub data: &'a str,
-}
-
-#[derive(sqlx::FromRow)]
-pub struct User {
- pub id: i64,
- pub username: String,
- pub email: String,
- pub password: String,
-}
-
-#[derive(sqlx::FromRow)]
-pub struct Session {
- pub id: i64,
- pub user_id: i64,
- pub token: String,
-}
-
-pub struct NewUser {
- pub username: String,
- pub email: String,
- pub password: String,
-}
-
-pub struct NewSession<'a> {
- pub user_id: i64,
- pub token: &'a str,
-}
diff --git a/src/server/router.rs b/src/server/router.rs
deleted file mode 100644
index ed317ab2..00000000
--- a/src/server/router.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use std::convert::Infallible;
-
-use eyre::Result;
-use warp::Filter;
-
-use super::handlers;
-use super::{database::Database, database::Postgres};
-use crate::server::models::User;
-use crate::{api::SyncHistoryRequest, settings::Settings};
-
-fn with_settings(
- settings: Settings,
-) -> impl Filter<Extract = (Settings,), Error = Infallible> + Clone {
- warp::any().map(move || settings.clone())
-}
-
-fn with_db(
- db: impl Database + Clone + Send + Sync,
-) -> impl Filter<Extract = (impl Database + Clone,), Error = Infallible> + Clone {
- warp::any().map(move || db.clone())
-}
-
-fn with_user(
- postgres: Postgres,
-) -> impl Filter<Extract = (User,), Error = warp::Rejection> + Clone {
- warp::header::<String>("authorization").and_then(move |header: String| {
- // async closures are still buggy :(
- let postgres = postgres.clone();
-
- async move {
- let header: Vec<&str> = header.split(' ').collect();
-
- let token;
-
- if header.len() == 2 {
- if header[0] != "Token" {
- return Err(warp::reject());
- }
-
- token = header[1];
- } else {
- return Err(warp::reject());
- }
-
- let user = postgres
- .get_session_user(token)
- .await
- .map_err(|_| warp::reject())?;
-
- Ok(user)
- }
- })
-}
-
-pub async fn router(
- settings: &Settings,
-) -> Result<impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone> {
- let postgres = Postgres::new(settings.server.db_uri.as_str()).await?;
- let index = warp::get().and(warp::path::end()).map(handlers::index);
-
- let count = warp::get()
- .and(warp::path("sync"))
- .and(warp::path("count"))
- .and(warp::path::end())
- .and(with_user(postgres.clone()))
- .and(with_db(postgres.clone()))
- .and_then(handlers::history::count);
-
- let sync = warp::get()
- .and(warp::path("sync"))
- .and(warp::path("history"))
- .and(warp::query::<SyncHistoryRequest>())
- .and(warp::path::end())
- .and(with_user(postgres.clone()))
- .and(with_db(postgres.clone()))
- .and_then(handlers::history::list);
-
- let add_history = warp::post()
- .and(warp::path("history"))
- .and(warp::path::end())
- .and(warp::body::json())
- .and(with_user(postgres.clone()))
- .and(with_db(postgres.clone()))
- .and_then(handlers::history::add);
-
- let user = warp::get()
- .and(warp::path("user"))
- .and(warp::path::param::<String>())
- .and(warp::path::end())
- .and(with_db(postgres.clone()))
- .and_then(handlers::user::get);
-
- let register = warp::post()
- .and(warp::path("register"))
- .and(warp::path::end())
- .and(warp::body::json())
- .and(with_settings(settings.clone()))
- .and(with_db(postgres.clone()))
- .and_then(handlers::user::register);
-
- let login = warp::post()
- .and(warp::path("login"))
- .and(warp::path::end())
- .and(warp::body::json())
- .and(with_db(postgres))
- .and_then(handlers::user::login);
-
- let r = warp::any()
- .and(
- index
- .or(count)
- .or(sync)
- .or(add_history)
- .or(user)
- .or(register)
- .or(login),
- )
- .with(warp::filters::log::log("atuin::api"));
-
- Ok(r)
-}
diff --git a/src/settings.rs b/src/settings.rs
deleted file mode 100644
index 5325610e..00000000
--- a/src/settings.rs
+++ /dev/null
@@ -1,172 +0,0 @@
-use std::fs::{create_dir_all, File};
-use std::io::prelude::*;
-use std::path::{Path, PathBuf};
-
-use chrono::prelude::*;
-use chrono::Utc;
-use config::{Config, Environment, File as ConfigFile};
-use directories::ProjectDirs;
-use eyre::{eyre, Result};
-use parse_duration::parse;
-
-pub const HISTORY_PAGE_SIZE: i64 = 100;
-
-#[derive(Clone, Debug, Deserialize)]
-pub struct Local {
- pub dialect: String,
- pub auto_sync: bool,
- pub sync_address: String,
- pub sync_frequency: String,
- pub db_path: String,
- pub key_path: String,
- pub session_path: String,
-
- // This is automatically loaded when settings is created. Do not set in
- // config! Keep secrets and settings apart.
- pub session_token: String,
-}
-
-impl Local {
- pub fn save_sync_time() -> Result<()> {
- let sync_time_path = ProjectDirs::from("com", "elliehuxtable", "atuin")
- .ok_or_else(|| eyre!("could not determine key file location"))?;
- let sync_time_path = sync_time_path.data_dir().join("last_sync_time");
-
- std::fs::write(sync_time_path, Utc::now().to_rfc3339())?;
-
- Ok(())
- }
-
- pub fn last_sync() -> Result<chrono::DateTime<Utc>> {
- let sync_time_path = ProjectDirs::from("com", "elliehuxtable", "atuin");
-
- if sync_time_path.is_none() {
- debug!("failed to load projectdirs, not syncing");
- return Err(eyre!("could not load project dirs"));
- }
-
- let sync_time_path = sync_time_path.unwrap();
- let sync_time_path = sync_time_path.data_dir().join("last_sync_time");
-
- if !sync_time_path.exists() {
- return Ok(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0));
- }
-
- let time = std::fs::read_to_string(sync_time_path)?;
- let time = chrono::DateTime::parse_from_rfc3339(time.as_str())?;
-
- Ok(time.with_timezone(&Utc))
- }
-
- pub fn should_sync(&self) -> Result<bool> {
- if !self.auto_sync {
- return Ok(false);
- }
-
- match parse(self.sync_frequency.as_str()) {
- Ok(d) => {
- let d = chrono::Duration::from_std(d).unwrap();
- Ok(Utc::now() - Local::last_sync()? >= d)
- }
- Err(e) => Err(eyre!("failed to check sync: {}", e)),
- }
- }
-}
-
-#[derive(Clone, Debug, Deserialize)]
-pub struct Server {
- pub host: String,
- pub port: u16,
- pub db_uri: String,
- pub open_registration: bool,
-}
-
-#[derive(Clone, Debug, Deserialize)]
-pub struct Settings {
- pub local: Local,
- pub server: Server,
-}
-
-impl Settings {
- pub fn new() -> Result<Self> {
- let config_dir = ProjectDirs::from("com", "elliehuxtable", "atuin").unwrap();
- let config_dir = config_dir.config_dir();
-
- create_dir_all(config_dir)?;
-
- let config_file = if let Ok(p) = std::env::var("ATUIN_CONFIG") {
- PathBuf::from(p)
- } else {
- let mut config_file = PathBuf::new();
- config_file.push(config_dir);
- config_file.push("config.toml");
- config_file
- };
-
- // create the config file if it does not exist
-
- let mut s = Config::new();
-
- let db_path = ProjectDirs::from("com", "elliehuxtable", "atuin")
- .ok_or_else(|| eyre!("could not determine db file location"))?
- .data_dir()
- .join("history.db");
-
- let key_path = ProjectDirs::from("com", "elliehuxtable", "atuin")
- .ok_or_else(|| eyre!("could not determine key file location"))?
- .data_dir()
- .join("key");
-
- let session_path = ProjectDirs::from("com", "elliehuxtable", "atuin")
- .ok_or_else(|| eyre!("could not determine session file location"))?
- .data_dir()
- .join("session");
-
- s.set_default("local.db_path", db_path.to_str())?;
- s.set_default("local.key_path", key_path.to_str())?;
- s.set_default("local.session_path", session_path.to_str())?;
- s.set_default("local.dialect", "us")?;
- s.set_default("local.auto_sync", true)?;
- s.set_default("local.sync_frequency", "5m")?;
- s.set_default("local.sync_address", "https://api.atuin.sh")?;
-
- s.set_default("server.host", "127.0.0.1")?;
- s.set_default("server.port", 8888)?;
- s.set_default("server.open_registration", false)?;
- s.set_default("server.db_uri", "default_uri")?;
-
- if config_file.exists() {
- s.merge(ConfigFile::with_name(config_file.to_str().unwrap()))?;
- } else {
- let example_config = include_bytes!("../config.toml");
- let mut file = File::create(config_file)?;
- file.write_all(example_config)?;
- }
-
- s.merge(Environment::with_prefix("atuin").separator("_"))?;
-
- // all paths should be expanded
- let db_path = s.get_str("local.db_path")?;
- let db_path = shellexpand::full(db_path.as_str())?;
- s.set("local.db_path", db_path.to_string())?;
-
- let key_path = s.get_str("local.key_path")?;
- let key_path = shellexpand::full(key_path.as_str())?;
- s.set("local.key_path", key_path.to_string())?;
-
- let session_path = s.get_str("local.session_path")?;
- let session_path = shellexpand::full(session_path.as_str())?;
- s.set("local.session_path", session_path.to_string())?;
-
- // Finally, set the auth token
- if Path::new(session_path.to_string().as_str()).exists() {
- let token = std::fs::read_to_string(session_path.to_string())?;
- s.set("local.session_token", token.trim())?;
- } else {
- s.set("local.session_token", "not logged in")?;
- }
-
- s.try_into()
- .map_err(|e| eyre!("failed to deserialize: {}", e))
- }
-}
diff --git a/src/utils.rs b/src/utils.rs
deleted file mode 100644
index b395b148..00000000
--- a/src/utils.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-use crypto::digest::Digest;
-use crypto::sha2::Sha256;
-use sodiumoxide::crypto::pwhash::argon2id13;
-
-pub fn hash_secret(secret: &str) -> String {
- sodiumoxide::init().unwrap();
- let hash = argon2id13::pwhash(
- secret.as_bytes(),
- argon2id13::OPSLIMIT_INTERACTIVE,
- argon2id13::MEMLIMIT_INTERACTIVE,
- )
- .unwrap();
- let texthash = std::str::from_utf8(&hash.0).unwrap().to_string();
-
- // postgres hates null chars. don't do that to postgres
- texthash.trim_end_matches('\u{0}').to_string()
-}
-
-pub fn hash_str(string: &str) -> String {
- let mut hasher = Sha256::new();
- hasher.input_str(string);
-
- hasher.result_str()
-}