aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKunoiSayami <[email protected]>2021-05-10 18:28:02 +0800
committerKunoiSayami <[email protected]>2021-05-10 18:28:02 +0800
commitbaeb888db8a12181f38499560b30c0a84ec9982e (patch)
treee1231e566ba5e25bde522a544d9e7f793dfee80d
parent309a78d246ff175eaf12f6a7d4e9d70845b9edda (diff)
refactor(database): Add repo tablev0.3.0
* feat(core): Add upgrade function from database v1 to v2
-rw-r--r--Cargo.lock67
-rw-r--r--Cargo.toml5
-rw-r--r--src/database.rs46
-rw-r--r--src/datastructures.rs43
-rw-r--r--src/main.rs134
5 files changed, 263 insertions, 32 deletions
diff --git a/Cargo.lock b/Cargo.lock
index f878d36..86bfaea 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 4fbfe80..b818819 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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();
}
}