From baeb888db8a12181f38499560b30c0a84ec9982e Mon Sep 17 00:00:00 2001
From: KunoiSayami <46131041+KunoiSayami@users.noreply.github.com>
Date: Mon, 10 May 2021 18:28:02 +0800
Subject: refactor(database): Add repo table * feat(core): Add upgrade function
from database v1 to v2
---
src/database.rs | 46 ++++++++++++++++-
src/datastructures.rs | 43 +++++++++++++---
src/main.rs | 134 ++++++++++++++++++++++++++++++++++++++++++--------
3 files changed, 193 insertions(+), 30 deletions(-)
(limited to 'src')
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 .
*/
+#[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 {
- 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 {
+ // 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::>();
+ 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 {
match arg_matches.subcommand() {
("authenticate-cookie", Some(matches)) => {
@@ -321,6 +408,9 @@ async fn async_main(arg_matches: ArgMatches<'_>, cfg: Config) -> Result {
("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>) -> 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();
}
}
--
cgit v1.3.1