diff options
author | KunoiSayami <[email protected]> | 2021-05-10 18:28:02 +0800 |
---|---|---|
committer | KunoiSayami <[email protected]> | 2021-05-10 18:28:02 +0800 |
commit | baeb888db8a12181f38499560b30c0a84ec9982e (patch) | |
tree | e1231e566ba5e25bde522a544d9e7f793dfee80d | |
parent | 309a78d246ff175eaf12f6a7d4e9d70845b9edda (diff) |
refactor(database): Add repo tablev0.3.0
* feat(core): Add upgrade function from database v1 to v2
-rw-r--r-- | Cargo.lock | 67 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | src/database.rs | 46 | ||||
-rw-r--r-- | src/datastructures.rs | 43 | ||||
-rw-r--r-- | src/main.rs | 134 |
5 files changed, 263 insertions, 32 deletions
@@ -377,7 +377,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cgit-simple-authentication" -version = "0.2.1" +version = "0.3.0" dependencies = [ "anyhow", "argon2", @@ -391,14 +391,17 @@ dependencies = [ "rand 0.7.3", "rand_core 0.6.2", "redis", + "regex", "serde", "serde_derive", "serde_json", "sqlx", + "tempdir", "tokio 1.5.0", "tokio-stream", "toml", "url", + "uuid", ] [[package]] @@ -650,6 +653,12 @@ dependencies = [ ] [[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] name = "fuchsia-zircon" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1472,6 +1481,19 @@ checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" [[package]] name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" @@ -1517,6 +1539,21 @@ dependencies = [ [[package]] name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" @@ -1552,6 +1589,15 @@ dependencies = [ ] [[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] name = "redis" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1918,6 +1964,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] name = "tempfile" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2189,6 +2245,15 @@ dependencies = [ ] [[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.2", +] + +[[package]] name = "value-bag" version = "1.0.0-alpha.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1,6 +1,6 @@ [package] name = "cgit-simple-authentication" -version = "0.2.1" +version = "0.3.0" authors = ["KunoiSayami <[email protected]>"] edition = "2018" @@ -24,6 +24,9 @@ log4rs = "1" tokio-stream = "0.1" argon2 = "0.2" rand_core = "0.6" +uuid = { version = "0.8", features = ["v4"] } +tempdir = "0.3" +regex = "1" [target.aarch64-unknown-linux-musl.dependencies] openssl = { version = "0.10", features = ["vendored"] }
\ No newline at end of file diff --git a/src/database.rs b/src/database.rs index a9af02b..eceec98 100644 --- a/src/database.rs +++ b/src/database.rs @@ -17,6 +17,7 @@ ** You should have received a copy of the GNU Affero General Public License ** along with this program. If not, see <https://www.gnu.org/licenses/>. */ +#[deprecated(since = "0.3.0", note = "Please use v2 instead")] #[allow(dead_code)] pub mod v1 { pub const CREATE_TABLES: &str = r#" @@ -46,5 +47,46 @@ pub mod v1 { pub const VERSION: &str = "1"; } -pub use v1 as current; -pub use v1::VERSION; + +#[allow(dead_code)] +pub mod v2 { + pub const CREATE_TABLES: &str = r#" + CREATE TABLE "accounts" ( + "user" TEXT NOT NULL, + "password" TEXT NOT NULL, + "uid" TEXT NOT NULL, + PRIMARY KEY("user") + ); + + CREATE TABLE "auth_meta" ( + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + PRIMARY KEY("key") + ); + + CREATE TABLE "repo" ( + "uid" TEXT NOT NULL, + "repos" TEXT NOT NULL, + "expire" INTEGER, + PRIMARY KEY("uid") + ); + + INSERT INTO "auth_meta" VALUES ('version', '2'); + "#; + + pub const DROP_TABLES: &str = r#" + + DROP TABLE "accounts"; + + DROP TABLE "repo"; + + DROP TABLE "auth_meta"; + "#; + + pub const VERSION: &str = "2"; +} + +#[allow(deprecated)] +pub use v1 as previous; +pub use v2 as current; +pub use v2::VERSION; diff --git a/src/datastructures.rs b/src/datastructures.rs index 319446d..0498f8f 100644 --- a/src/datastructures.rs +++ b/src/datastructures.rs @@ -21,7 +21,7 @@ use anyhow::Result; use std::borrow::Cow; use std::fs::read_to_string; -use std::path::Path; +use std::path::{Path, PathBuf}; use url::form_urlencoded; use std::fmt::Formatter; use rand::Rng; @@ -35,9 +35,10 @@ use rand_core::OsRng; const DEFAULT_CONFIG_LOCATION: &str = "/etc/cgitrc"; const DEFAULT_COOKIE_TTL: u64 = 1200; const DEFAULT_DATABASE_LOCATION: &str = "/etc/cgit/auth.db"; -//pub const CACHE_DIR: &str = "/var/cache/cgit"; +pub const CACHE_DIR: &str = "/var/cache/cgit"; pub type RandIntType = u32; //pub const MINIMUM_SECRET_LENGTH: usize = 8; +const COOKIE_LENGTH: usize = 32; pub fn get_current_timestamp() -> u64 { let start = std::time::SystemTime::now(); @@ -142,6 +143,14 @@ impl Config { "" } }*/ + + pub fn get_copied_database_location(&self) -> PathBuf { + std::path::Path::new(CACHE_DIR).join( + std::path::Path::new(self.get_database_location()) + .file_name() + .unwrap(), + ) + } } #[derive(Debug, Clone, Default)] @@ -240,18 +249,21 @@ struct IvFile { timestamp: u64, } +#[derive(Debug)] pub struct Cookie { timestamp: u64, randint: RandIntType, - body: String, + user: String, + reversed: String, } impl Cookie { - fn new(randint: RandIntType, body: &String) -> Self { + fn new(randint: RandIntType, user: &str) -> Self { Self { timestamp: get_current_timestamp(), randint, - body: body.clone() + user: user.to_string(), + reversed: rand_str(COOKIE_LENGTH), } } @@ -269,12 +281,15 @@ impl Cookie { let (key, value) = value.split_once(';').unwrap(); + let (user, reversed) = value.split_once(";").unwrap_or(("", "")); + let (timestamp, randint) = key.split_once("_").unwrap_or(("0", "")); cookie_self = Some(Self{ timestamp: timestamp.parse()?, randint: randint.parse()?, - body: value.to_string(), + user: user.trim().to_string(), + reversed: reversed.trim().to_string(), }); break } @@ -283,18 +298,30 @@ impl Cookie { } pub fn eq_body(&self, s: &str) -> bool { - self.body.eq(s) + self.get_body().eq(s) } pub fn get_key(&self) -> String { format!("{}_{}", self.timestamp, self.randint) } + + pub fn get_user(&self) -> &str { + self.user.as_str() + } + + pub fn get_body(&self) -> String { + format!("{}; {}", self.user, self.reversed) + } + + pub fn generate(user: &str) -> Self { + Self::new(rand_int(), user) + } } impl std::fmt::Display for Cookie { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let s = format!("{}_{}; {}", self.timestamp, self.randint, self.body); + let s = format!("{}_{}; {}; {}", self.timestamp, self.randint, self.user, self.reversed); write!(f, "{}", base64::encode(s)) } } diff --git a/src/main.rs b/src/main.rs index 76c7fa3..6cd95ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ mod database; mod datastructures; -use crate::datastructures::{Config, FormData, get_current_timestamp, Cookie, rand_int, rand_str}; +use crate::datastructures::{Config, FormData, Cookie}; use anyhow::Result; use clap::{App, Arg, ArgMatches, SubCommand}; use handlebars::Handlebars; @@ -39,8 +39,9 @@ use argon2::{ password_hash::{PasswordHash}, }; use std::str::FromStr; +use tempdir::TempDir; +use sqlx::sqlite::SqliteConnectOptions; -const COOKIE_LENGTH: usize = 45; // Processing the `authenticate-cookie` called by cgit. @@ -70,6 +71,7 @@ async fn cmd_authenticate_cookie(matches: &ArgMatches<'_>, cfg: Config) -> Resul return Ok(true); } } + log::debug!("{:?}", cookie); } Ok(false) @@ -99,15 +101,34 @@ async fn cmd_init(cfg: Config) -> Result<()> { Ok(()) } -async fn verify_login(cfg: &Config, data: &FormData) -> Result<bool> { - let mut conn = sqlx::sqlite::SqliteConnectOptions::from_str(cfg.get_database_location())? - .read_only(true) +async fn verify_login(cfg: &Config, data: &FormData, redis_conn: redis::Client) -> Result<bool> { + // TODO: use timestamp to mark file diff + // or copy in init process + std::fs::copy(cfg.get_database_location(), cfg.get_copied_database_location())?; + + let mut rd = redis_conn.get_async_connection().await?; + + let mut conn = sqlx::sqlite::SqliteConnectOptions::from_str(cfg.get_copied_database_location().to_str().unwrap())? + .journal_mode(sqlx::sqlite::SqliteJournalMode::Off) .log_statements(log::LevelFilter::Trace) .connect().await?; - let (passwd_hash,) = sqlx::query_as::<_, (String, )>(r#"SELECT "password" FROM "accounts" WHERE "user" = ?"#) + + let (passwd_hash, uid) = sqlx::query_as::<_, (String, String)>(r#"SELECT "password", "uid" FROM "accounts" WHERE "user" = ?"#) .bind(data.get_user()) .fetch_one(&mut conn) .await?; + + let key = format!("cgit_repo_{}", data.get_user()); + if !rd.exists(&key).await? { + if let Some((repos, )) = sqlx::query_as::<_, (String, )>(r#"SELECT "repos" FROM "repo" WHERE "uid" = ? "#) + .bind(uid) + .fetch_optional(&mut conn) + .await? { + let iter = repos.split_whitespace().collect::<Vec<&str>>(); + rd.sadd(&key, iter).await?; + } + } + let parsed_hash = PasswordHash::new(passwd_hash.as_str()).unwrap(); Ok(data.verify_password(&parsed_hash)) } @@ -118,30 +139,30 @@ async fn cmd_authenticate_post(matches: &ArgMatches<'_>, cfg: Config) -> Result< let mut buffer = String::new(); // TODO: override it that can test function from cargo test stdin().read_to_string(&mut buffer)?; - log::debug!("{}", buffer); + //log::debug!("{}", buffer); let data = datastructures::FormData::from(buffer); // Parsing user posted form. - let ret = verify_login(&cfg, &data).await; + let redis_conn = redis::Client::open("redis://127.0.0.1/")?; + + let ret = verify_login(&cfg, &data, redis_conn.clone()).await; if let Err(ref e) = ret { log::error!("{:?}", e) } if ret.unwrap_or(false) { - let key = format!("{}_{}", get_current_timestamp(), rand_int()); - let value = rand_str(COOKIE_LENGTH); - - let redis_conn = redis::Client::open("redis://127.0.0.1/")?; + let cookie = Cookie::generate(data.get_user()); let mut conn = redis_conn.get_async_connection().await?; + conn.set_ex::<_, _, String>( - format!("cgit_auth_{}", key), - &value, + format!("cgit_auth_{}", cookie.get_key()), + cookie.get_body(), cfg.cookie_ttl as usize, ) .await?; - let cookie_value = base64::encode(format!("{};{}", key, value)); + let cookie_value = cookie.to_string(); let is_secure = matches .value_of("https") @@ -187,16 +208,21 @@ async fn cmd_body(matches: &ArgMatches<'_>, _cfg: Config) { } async fn cmd_add_user(matches: &ArgMatches<'_>, cfg: Config) -> Result<()> { + let re = regex::Regex::new(r"^\w+$").unwrap(); let user = matches.value_of("user").unwrap_or(""); let passwd = matches.value_of("password").unwrap_or("").to_string(); if user.is_empty() || passwd.is_empty() { - return Err(anyhow::Error::msg("Invalid user or password")); + return Err(anyhow::Error::msg("Invalid user or password length")); } if user.len() >= 20 { return Err(anyhow::Error::msg("Username length should less than 21")) } + if !re.is_match(user) { + return Err(anyhow::Error::msg("Username must pass regex check\"^\\w+$\"")) + } + let mut conn = sqlx::SqliteConnection::connect(cfg.get_database_location()).await?; let items = sqlx::query(r#"SELECT 1 FROM "accounts" WHERE "user" = ? "#) @@ -208,12 +234,15 @@ async fn cmd_add_user(matches: &ArgMatches<'_>, cfg: Config) -> Result<()> { return Err(anyhow::Error::msg("User already exists!")); } - sqlx::query(r#"INSERT INTO "accounts" ("user", "password") VALUES (?, ?) "#) + let uid = uuid::Uuid::new_v4().to_hyphenated().to_string(); + + sqlx::query(r#"INSERT INTO "accounts" VALUES (?, ?, ?) "#) .bind(user) .bind(FormData::get_string_argon2_hash(&passwd)?) + .bind(&uid) .execute(&mut conn) .await?; - println!("Insert {} to database", user); + println!("Insert {} ({}) to database", user, uid); Ok(()) } @@ -290,6 +319,64 @@ async fn cmd_reset_database(matches: &ArgMatches<'_>, cfg: Config) -> Result<()> Ok(()) } +async fn cmd_upgrade_database(cfg: Config) -> Result<()> { + + let tmp_dir = TempDir::new("rolling")?; + + let v1_path = tmp_dir.path().join("v1.db"); + let v2_path = tmp_dir.path().join("v2.db"); + + drop(std::fs::File::create(&v2_path).expect("Create v2 database failure")); + + std::fs::copy(cfg.get_database_location(), &v1_path) + .expect("Copy v1 database to tempdir failure"); + + let mut origin_conn = SqliteConnectOptions::from_str(v1_path.as_path().to_str().unwrap())? + .read_only(true) + .connect() + .await?; + + let (v,) = sqlx::query_as::<_, (String,)>(r#"SELECT "value" FROM "auth_meta" WHERE "key" = 'version' "#) + .fetch_optional(&mut origin_conn) + .await? + .unwrap(); + + #[allow(deprecated)] + if v.eq(database::previous::VERSION) { + + let mut conn = SqliteConnection::connect(v2_path.as_path().to_str().unwrap()).await?; + + sqlx::query(database::current::CREATE_TABLES) + .execute(&mut conn) + .await?; + + let mut iter = sqlx::query_as::<_, (String, String)>(r#"SELECT * FROM "accounts""#) + .fetch(&mut origin_conn); + + while let Some(Ok((user, passwd))) = iter.next().await { + let uid = uuid::Uuid::new_v4().to_hyphenated().to_string(); + sqlx::query(r#"INSERT INTO "accounts" VALUES (?, ?, ?)"#) + .bind(user.as_str()) + .bind(passwd) + .bind(uid.as_str()) + .execute(&mut conn) + .await?; + log::debug!("Process user: {} ({})", user, uid); + } + drop(conn); + + std::fs::copy(&v2_path, cfg.get_database_location()) + .expect("Copy back to database location failure"); + println!("Upgrade database successful"); + } else { + eprintln!("Got database version {} but {} required", v, database::previous::VERSION) + } + drop(origin_conn); + tmp_dir.close()?; + + Ok(()) +} + async fn async_main(arg_matches: ArgMatches<'_>, cfg: Config) -> Result<i32> { match arg_matches.subcommand() { ("authenticate-cookie", Some(matches)) => { @@ -321,6 +408,9 @@ async fn async_main(arg_matches: ArgMatches<'_>, cfg: Config) -> Result<i32> { ("reset", Some(matches)) => { cmd_reset_database(matches, cfg).await?; } + ("upgrade", Some(_matches)) => { + cmd_upgrade_database(cfg).await?; + } _ => {} } Ok(0) @@ -377,6 +467,10 @@ fn process_arguments(arguments: Option<Vec<&str>>) -> Result<()> { SubCommand::with_name("reset") .about("Reset database") .arg(Arg::with_name("confirm").long("confirm")) + ) + .subcommand( + SubCommand::with_name("upgrade") + .about("Upgrade database from v1(v0.1.x - v0.2.x) to v2(^v0.3.x)") ); let matches = if let Some(args) = arguments { @@ -406,7 +500,7 @@ fn main() -> Result<()> { .encoder(Box::new(PatternEncoder::new( "{d(%Y-%m-%d %H:%M:%S)}- {h({l})} - {m}{n}", ))) - .build(option_env!("RUST_LOG_FILE").unwrap_or("/tmp/auth.log"))?; + .build(env::var("RUST_LOG_FILE").unwrap_or("/tmp/auth.log".to_string()))?; let config = log4rs::Config::builder() .appender(Appender::builder().build("logfile", Box::new(logfile))) @@ -439,7 +533,6 @@ fn main() -> Result<()> { } mod test { - use crate::process_arguments; #[test] fn test_argon2() { @@ -472,6 +565,7 @@ mod test { #[cfg(unix)] #[allow(dead_code)] fn test_auth_post() { + use crate::process_arguments; process_arguments(Some(vec!["cgit-simple-authentication", "authenticate-post", "", "POST", "p=login", "https://git.example.com/?p=login", "/", "git.example.com", "", "", "login", "/?p=login", "/?p=login"])).unwrap(); } } |