aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock109
-rw-r--r--src/authentication.rs598
-rw-r--r--src/main.rs623
-rw-r--r--src/test.rs9
4 files changed, 680 insertions, 659 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 27a7a9f..c980503 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -241,9 +241,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
-version = "1.2.16"
+version = "1.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
+checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
dependencies = [
"shlex",
]
@@ -650,9 +650,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
-version = "0.1.4"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "form_urlencoded"
@@ -758,14 +758,14 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.3.1"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
+checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"libc",
- "wasi 0.13.3+wasi-0.2.2",
- "windows-targets 0.52.6",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
]
[[package]]
@@ -782,9 +782,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "handlebars"
-version = "6.3.1"
+version = "6.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d752747ddabc4c1a70dd28e72f2e3c218a816773e0d7faf67433f1acfa6cba7c"
+checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098"
dependencies = [
"derive_builder",
"log",
@@ -863,14 +863,15 @@ checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
[[package]]
name = "iana-time-zone"
-version = "0.1.61"
+version = "0.1.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
+checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
+ "log",
"wasm-bindgen",
"windows-core",
]
@@ -1071,9 +1072,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
-version = "0.2.4"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e"
+checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260"
dependencies = [
"jiff-static",
"log",
@@ -1084,9 +1085,9 @@ dependencies = [
[[package]]
name = "jiff-static"
-version = "0.2.4"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9"
+checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c"
dependencies = [
"proc-macro2",
"quote",
@@ -1165,9 +1166,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.26"
+version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
dependencies = [
"serde",
]
@@ -1444,9 +1445,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
-version = "2.7.15"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
+checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
dependencies = [
"memchr",
"thiserror 2.0.12",
@@ -1455,9 +1456,9 @@ dependencies = [
[[package]]
name = "pest_derive"
-version = "2.7.15"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e"
+checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5"
dependencies = [
"pest",
"pest_generator",
@@ -1465,9 +1466,9 @@ dependencies = [
[[package]]
name = "pest_generator"
-version = "2.7.15"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b"
+checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841"
dependencies = [
"pest",
"pest_meta",
@@ -1478,9 +1479,9 @@ dependencies = [
[[package]]
name = "pest_meta"
-version = "2.7.15"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea"
+checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0"
dependencies = [
"once_cell",
"pest",
@@ -1569,6 +1570,12 @@ dependencies = [
]
[[package]]
+name = "r-efi"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
+[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1625,14 +1632,14 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
- "getrandom 0.3.1",
+ "getrandom 0.3.2",
]
[[package]]
name = "redis"
-version = "0.29.1"
+version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8034fb926579ff49d3fe58d288d5dcb580bf11e9bccd33224b45adebf0fd0c23"
+checksum = "b110459d6e323b7cda23980c46c77157601199c9da6241552b284cd565a7a133"
dependencies = [
"arc-swap",
"bytes",
@@ -1736,9 +1743,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
-version = "1.0.2"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825"
+checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
dependencies = [
"bitflags",
"errno",
@@ -1749,9 +1756,9 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.23.23"
+version = "0.23.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
+checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c"
dependencies = [
"once_cell",
"ring",
@@ -1778,9 +1785,9 @@ checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
[[package]]
name = "rustls-webpki"
-version = "0.102.8"
+version = "0.103.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
+checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f"
dependencies = [
"ring",
"rustls-pki-types",
@@ -2235,12 +2242,12 @@ dependencies = [
[[package]]
name = "tempfile"
-version = "3.19.0"
+version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600"
+checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
dependencies = [
"fastrand",
- "getrandom 0.3.1",
+ "getrandom 0.3.2",
"once_cell",
"rustix",
"windows-sys 0.59.0",
@@ -2544,7 +2551,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
- "getrandom 0.3.1",
+ "getrandom 0.3.2",
]
[[package]]
@@ -2577,9 +2584,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
-version = "0.13.3+wasi-0.2.2"
+version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
@@ -2659,9 +2666,9 @@ dependencies = [
[[package]]
name = "whoami"
-version = "1.5.2"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d"
+checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
dependencies = [
"redox_syscall",
"wasite",
@@ -2700,9 +2707,9 @@ dependencies = [
[[package]]
name = "windows-link"
-version = "0.1.0"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
+checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-sys"
@@ -2863,9 +2870,9 @@ dependencies = [
[[package]]
name = "wit-bindgen-rt"
-version = "0.33.0"
+version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
@@ -2908,18 +2915,18 @@ dependencies = [
[[package]]
name = "zerocopy"
-version = "0.8.23"
+version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
+checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.23"
+version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
+checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
dependencies = [
"proc-macro2",
"quote",
diff --git a/src/authentication.rs b/src/authentication.rs
new file mode 100644
index 0000000..4d343c0
--- /dev/null
+++ b/src/authentication.rs
@@ -0,0 +1,598 @@
+#[cfg(feature = "pam")]
+use crate::datastructures::AuthorizerType;
+use crate::datastructures::{Config, Cookie, FormData, WrapConfigure};
+use anyhow::Result;
+use clap::ArgMatches;
+use handlebars::Handlebars;
+use itertools::Itertools as _;
+use redis::AsyncCommands;
+use serde::Serialize;
+use sqlx::sqlite::SqliteConnectOptions;
+use sqlx::{ConnectOptions, Connection, SqliteConnection};
+use std::env;
+use std::io::{BufRead, Write};
+use std::str::FromStr;
+use tempfile::TempDir;
+use tokio_stream::StreamExt as _;
+
+pub(crate) struct IOModule<R, W> {
+ reader: R,
+ writer: W,
+}
+
+impl<R, W> IOModule<R, W> {
+ pub fn new(reader: R, writer: W) -> Self {
+ Self { reader, writer }
+ }
+}
+
+impl<R: BufRead, W: Write> IOModule<R, W> {
+ // Processing the `authenticate-post` called by cgit.
+ pub(crate) async fn cmd_authenticate_post(
+ &mut self,
+ matches: &ArgMatches,
+ cfg: Config,
+ ) -> Result<()> {
+ // Read stdin from upstream.
+ let mut buffer = String::new();
+ self.reader.read_to_string(&mut buffer)?;
+
+ //log::debug!("{}", buffer);
+ let data = FormData::from(buffer);
+
+ let cfg = WrapConfigure::from(cfg);
+ log::trace!("Method is {}", cfg.get_authorizer().method());
+ let ret = verify_login(&cfg, &data).await;
+
+ if let Err(ref e) = ret {
+ eprintln!("{e:?}");
+ #[cfg(test)]
+ eprintln!(
+ "If database locked error occurs frequently, \
+ please use environment DISK_WAIT_TIME to specify longer time."
+ );
+ log::error!("{e:?}")
+ }
+
+ if ret.unwrap_or(false) {
+ let redis_conn = redis::Client::open("redis://127.0.0.1/")?;
+ let cookie = Cookie::generate(data.get_user());
+ let mut conn = redis_conn.get_multiplexed_async_connection().await?;
+
+ conn.set_ex::<_, _, String>(
+ format!("cgit_auth_{}", cookie.get_key()),
+ cookie.get_body(),
+ cfg.get_config().cookie_ttl,
+ )
+ .await?;
+
+ let cookie_value = cookie.to_string();
+
+ let is_secure = matches
+ .get_one::<String>("https")
+ .map(|s| s.as_str())
+ .is_some_and(|x| matches!(x, "yes" | "on" | "1"));
+ let domain = matches
+ .get_one::<String>("http-host")
+ .map(|s| s.as_str())
+ .unwrap_or("*");
+ let location = matches
+ .get_one::<String>("http-referer")
+ .map(|s| s.as_str())
+ .unwrap_or("/");
+ let cookie_suffix = if is_secure { "; secure" } else { "" };
+ writeln!(&mut self.writer, "Status: 302 Found")?;
+ writeln!(&mut self.writer, "Cache-Control: no-cache, no-store")?;
+ writeln!(&mut self.writer, "Location: {location}")?;
+ writeln!(
+ &mut self.writer,
+ "Set-Cookie: cgit_auth={cookie_value}; Domain={domain}; Max-Age={}; HttpOnly{cookie_suffix}",
+ cfg.get_config().cookie_ttl * 10,
+ )?;
+ } else {
+ writeln!(&mut self.writer, "Status: 403 Forbidden")?;
+ writeln!(&mut self.writer, "Cache-Control: no-cache, no-store")?;
+ }
+
+ writeln!(&mut self.writer)?;
+ Ok(())
+ }
+}
+
+// Processing the `authenticate-cookie` called by cgit.
+pub(crate) async fn cmd_authenticate_cookie(matches: &ArgMatches, cfg: Config) -> Result<bool> {
+ let cookies = matches
+ .get_one::<String>("http-cookie")
+ .map(|s| s.as_str())
+ .unwrap_or("");
+ let repo = matches
+ .get_one::<String>("repo")
+ .map(|s| s.as_str())
+ .unwrap_or("");
+ /*let current_url = matches.value_of("current-url").unwrap_or("");*/
+
+ let mut bypass = false;
+
+ if cfg.bypass_root /*&& current_url.eq("/")*/ && repo.is_empty() {
+ bypass = true;
+ }
+
+ if bypass || (!repo.is_empty() && !cfg.check_repo_protect(repo)) {
+ return Ok(true);
+ }
+
+ if cookies.is_empty() {
+ return Ok(false);
+ }
+
+ let redis_conn = redis::Client::open("redis://127.0.0.1/")?;
+ let mut conn = redis_conn.get_multiplexed_async_connection().await?;
+
+ let redis_key = format!("cgit_repo_{repo}");
+ if !repo.is_empty() && !conn.exists(&redis_key).await? {
+ let sql_conn = SqliteConnectOptions::from_str(cfg.get_database_location())?
+ .read_only(true)
+ .immutable(true)
+ .disable_statement_logging()
+ .connect()
+ .await;
+ if let Err(ref e) = sql_conn {
+ log::error!(
+ "Got error while open sqlite connection: {e:?}\nDatabase location: {}",
+ cfg.get_database_location()
+ );
+ }
+ let mut sql_conn = sql_conn?;
+ if let Some((users,)) =
+ sqlx::query_as::<_, (String,)>(r#"SELECT "users" FROM "repos" WHERE "repo" = ? "#)
+ .bind(repo)
+ .fetch_optional(&mut sql_conn)
+ .await?
+ {
+ let users = users.split_whitespace().collect::<Vec<&str>>();
+ let _: () = conn.sadd(&redis_key, users).await?;
+ }
+ }
+
+ if let Ok(Some(cookie)) = Cookie::load_from_request(cookies) {
+ log::debug!("Cookie is {cookie:?}");
+ if let Ok(r) = conn
+ .get::<_, String>(format!("cgit_auth_{}", cookie.get_key()))
+ .await
+ {
+ conn.expire::<_, bool>(
+ format!("cgit_auth_{}", cookie.get_key()),
+ cfg.cookie_ttl as i64,
+ )
+ .await?;
+ if cookie.eq_body(r.as_str()) {
+ if repo.is_empty() {
+ return Ok(true);
+ }
+ if conn
+ .sismember::<_, _, i32>(&redis_key, cookie.get_user())
+ .await?
+ == 1
+ {
+ return Ok(true);
+ }
+ }
+ }
+ log::debug!("{cookie:?}");
+ }
+
+ Ok(false)
+}
+
+pub(crate) async fn cmd_init(cfg: Config) -> Result<()> {
+ let loc = std::path::Path::new(cfg.get_database_location());
+ let exists = loc.exists();
+ if !exists {
+ std::fs::File::create(loc)?;
+ }
+
+ let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?;
+
+ if exists {
+ let rows = sqlx::query(r#"SELECT name FROM sqlite_master WHERE type='table' AND name=?"#)
+ .bind("auth_meta")
+ .fetch_all(&mut conn)
+ .await?;
+
+ if !rows.is_empty() {
+ return Ok(());
+ }
+ }
+
+ sqlx::query(crate::database::current::CREATE_TABLES)
+ .execute(&mut conn)
+ .await?;
+ println!("Initialize the database successfully");
+
+ drop(conn);
+
+ cfg.write_database_commit_timestamp().await?;
+ Ok(())
+}
+
+#[cfg(not(feature = "pam"))]
+pub(crate) async fn verify_login(cfg: &WrapConfigure, data: &FormData) -> Result<bool> {
+ cfg.hook().await?;
+ data.authorize(cfg.get_authorizer()).await
+}
+
+#[cfg(feature = "pam")]
+pub(crate) async fn verify_login(cfg: &WrapConfigure, data: &FormData) -> Result<bool> {
+ if let AuthorizerType::Password = cfg.get_authorizer().method() {
+ cfg.hook().await?;
+ }
+ data.authorize(cfg.get_authorizer()).await
+}
+
+#[derive(Serialize)]
+pub struct Meta<'a> {
+ action: &'a str,
+ redirect: &'a str,
+ version: &'a str,
+}
+
+// Processing the `body` called by cgit.
+pub(crate) async fn cmd_body(matches: &ArgMatches, _cfg: Config) {
+ let source = include_str!("authentication_page.html");
+ let handlebars = Handlebars::new();
+ let meta = Meta {
+ action: matches
+ .get_one::<String>("login-url")
+ .map(|s| s.as_str())
+ .unwrap_or_default(),
+ redirect: matches
+ .get_one::<String>("current-url")
+ .map(|s| s.as_str())
+ .unwrap_or_default(),
+ version: env!("CARGO_PKG_VERSION"),
+ };
+ handlebars
+ .render_template_to_write(source, &meta, std::io::stdout())
+ .unwrap();
+}
+
+pub(crate) async fn cmd_add_user(matches: &ArgMatches, cfg: Config) -> Result<()> {
+ let re = regex::Regex::new(r"^\w+$").unwrap();
+ let user = matches
+ .get_one::<String>("user")
+ .map(|s| s.as_str())
+ .unwrap_or_default();
+ let passwd = matches
+ .get_one::<String>("password")
+ .map(|s| s.to_string())
+ .unwrap_or_default();
+ if user.is_empty() || passwd.is_empty() {
+ 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 = SqliteConnection::connect(cfg.get_database_location()).await?;
+
+ let items = sqlx::query(r#"SELECT 1 FROM "accounts" WHERE "user" = ? "#)
+ .bind(user)
+ .fetch_all(&mut conn)
+ .await?;
+
+ if !items.is_empty() {
+ return Err(anyhow::Error::msg("User already exists!"));
+ }
+
+ let uid = uuid::Uuid::new_v4().to_string();
+
+ sqlx::query(r#"INSERT INTO "accounts" VALUES (?, ?, ?) "#)
+ .bind(user)
+ .bind(FormData::gen_string_argon2_hash(&passwd)?)
+ .bind(&uid)
+ .execute(&mut conn)
+ .await?;
+
+ println!("Insert {user} ({uid}) to database");
+
+ drop(conn);
+
+ cfg.write_database_commit_timestamp().await?;
+ Ok(())
+}
+
+pub(crate) async fn cmd_list_user(cfg: Config) -> Result<()> {
+ let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?;
+
+ let (count,) = sqlx::query_as::<_, (i32,)>(r#"SELECT COUNT(*) FROM "accounts""#)
+ .fetch_one(&mut conn)
+ .await?;
+
+ if count > 0 {
+ let mut iter =
+ sqlx::query_as::<_, (String,)>(r#"SELECT "user" FROM "accounts""#).fetch(&mut conn);
+
+ println!(
+ "There is {count} user{} in database",
+ if count > 1 { "s" } else { "" }
+ );
+ while let Some(Ok((row,))) = iter.next().await {
+ println!("{row}")
+ }
+ } else {
+ println!("There is not user exists.")
+ }
+
+ Ok(())
+}
+
+pub(crate) async fn cmd_delete_user(matches: &ArgMatches, cfg: Config) -> Result<()> {
+ let user = matches
+ .get_one::<String>("user")
+ .map(|s| s.as_str())
+ .unwrap_or("");
+ if user.is_empty() {
+ return Err(anyhow::Error::msg("Please input a valid username"));
+ }
+
+ let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?;
+
+ let items = sqlx::query_as::<_, (i32,)>(r#"SELECT 1 FROM "accounts" WHERE "user" = ?"#)
+ .bind(user)
+ .fetch_all(&mut conn)
+ .await?;
+
+ if items.is_empty() {
+ return Err(anyhow::Error::msg(format!("User {user} not found")));
+ }
+
+ sqlx::query(r#"DELETE FROM "accounts" WHERE "user" = ?"#)
+ .bind(user)
+ .execute(&mut conn)
+ .await?;
+
+ println!("Delete {user} from database");
+
+ cfg.write_database_commit_timestamp().await?;
+ Ok(())
+}
+
+pub(crate) async fn cmd_reset_database(matches: &ArgMatches, cfg: Config) -> Result<()> {
+ if !matches.contains_id("confirm") {
+ return Err(anyhow::anyhow!(
+ "Please add --confirm argument to process reset",
+ ));
+ }
+
+ let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?;
+
+ sqlx::query(crate::database::current::DROP_TABLES)
+ .execute(&mut conn)
+ .await?;
+
+ sqlx::query(crate::database::current::CREATE_TABLES)
+ .execute(&mut conn)
+ .await?;
+
+ println!("Reset database successfully");
+
+ cfg.write_database_commit_timestamp().await?;
+ Ok(())
+}
+
+pub(crate) async fn cmd_upgrade_database(cfg: Config) -> Result<()> {
+ let tmp_dir = TempDir::new()?;
+
+ let v2_path = tmp_dir.path().join("v2.db");
+ let v3_path = tmp_dir.path().join("v3.db");
+
+ drop(std::fs::File::create(&v3_path).expect("Create v3 database failure"));
+
+ std::fs::copy(cfg.get_database_location(), &v2_path)
+ .expect("Copy v2 database to tempdir failure");
+
+ let mut origin_conn = SqliteConnectOptions::from_str(v2_path.as_path().to_str().unwrap())?
+ .read_only(true)
+ .immutable(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(crate::database::previous::VERSION) {
+ let mut conn = SqliteConnection::connect(v3_path.as_path().to_str().unwrap()).await?;
+
+ sqlx::query(crate::database::current::CREATE_TABLES)
+ .execute(&mut conn)
+ .await?;
+
+ let mut iter = sqlx::query_as::<_, (String, String, String)>(r#"SELECT * FROM "accounts""#)
+ .fetch(&mut origin_conn);
+
+ while let Some(Ok((user, passwd, uid))) = iter.next().await {
+ 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(&v3_path, cfg.get_database_location())
+ .expect("Copy back to database location failure");
+ println!("Upgrade database successful");
+ } else {
+ eprintln!(
+ "Got database version {v} but {} required",
+ crate::database::previous::VERSION
+ )
+ }
+ drop(origin_conn);
+ tmp_dir.close()?;
+
+ cfg.write_database_commit_timestamp().await?;
+ Ok(())
+}
+
+pub(crate) async fn cmd_repo_user_control(
+ matches: &ArgMatches,
+ cfg: Config,
+ is_delete: bool,
+) -> Result<()> {
+ let repo = matches
+ .get_one::<String>("repo")
+ .map(|s| s.as_str())
+ .unwrap_or("");
+ let user = matches
+ .get_one::<String>("user")
+ .map(|s| s.as_str())
+ .unwrap_or_default();
+
+ let clear_all = is_delete && matches.contains_id("clear-all");
+
+ if repo.is_empty()
+ || (is_delete && !clear_all && user.is_empty())
+ || (!is_delete && user.is_empty())
+ {
+ return Err(anyhow::Error::msg("Invalid repository or username"));
+ }
+
+ let redis_client = redis::Client::open("redis://127.0.0.1/")?;
+ let mut redis_conn = redis_client.get_multiplexed_async_connection().await?;
+
+ let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?;
+
+ if sqlx::query(r#"SELECT "users" FROM "repos" WHERE "repo" = ?"#)
+ .bind(repo)
+ .fetch_optional(&mut conn)
+ .await?
+ .is_none()
+ {
+ if is_delete {
+ println!("Row is empty.");
+ return Ok(());
+ }
+ sqlx::query(r#"INSERT INTO "repos" VALUES (?, '')"#)
+ .bind(repo)
+ .execute(&mut conn)
+ .await?;
+ }
+
+ let (users,) =
+ sqlx::query_as::<_, (String,)>(r#"SELECT "users" FROM "repos" WHERE "repo" = ?"#)
+ .bind(repo)
+ .fetch_optional(&mut conn)
+ .await?
+ .unwrap();
+ let mut users = users.split_whitespace().collect::<Vec<&str>>();
+
+ if let Some(index) = users.clone().into_iter().position(|x| x.eq(user)) {
+ if is_delete {
+ if clear_all {
+ users.clear();
+ } else {
+ users.remove(index);
+ }
+ } else {
+ return Err(anyhow::Error::msg("User already in repository ACL"));
+ }
+ }
+
+ if !is_delete {
+ users.push(user);
+ }
+
+ sqlx::query(r#"UPDATE "repos" SET "users" = ? WHERE "repo" = ?"#)
+ .bind(users.join(" "))
+ .bind(repo)
+ .execute(&mut conn)
+ .await?;
+
+ let redis_key = format!("cgit_repo_{repo}");
+ if redis_conn.exists::<_, i32>(&redis_key).await? == 0 {
+ redis_conn.sadd::<_, _, i32>(&redis_key, users).await?;
+ } else if is_delete {
+ if clear_all {
+ redis_conn.del::<_, i32>(&redis_key).await?;
+ } else {
+ redis_conn.srem::<_, _, i32>(&redis_key, user).await?;
+ }
+ } else {
+ redis_conn.sadd::<_, _, i32>(&redis_key, user).await?;
+ }
+
+ if !clear_all {
+ println!(
+ "{} user {user} {} repository {repo} ACL successful",
+ if is_delete { "Delete" } else { "Add" },
+ if is_delete { "from" } else { "to" },
+ );
+ } else {
+ println!("Clear all users from repository {repo} ACL");
+ }
+
+ Ok(())
+}
+
+pub(crate) async fn cmd_list_repos_acl(arg_matches: &ArgMatches, cfg: Config) -> Result<()> {
+ let repo = arg_matches
+ .get_one::<String>("repo")
+ .map(|s| s.as_str())
+ .unwrap_or("");
+
+ let mut conn = SqliteConnectOptions::from_str(cfg.get_database_location())?
+ .read_only(true)
+ .immutable(true)
+ .connect()
+ .await?;
+
+ if repo.is_empty() {
+ let (length,) = sqlx::query_as::<_, (i32,)>(r#"SELECT COUNT(*) FROM "repos""#)
+ .fetch_optional(&mut conn)
+ .await?
+ .unwrap_or((0,));
+
+ println!(
+ "There is total {length} {} in database",
+ if length == 1 {
+ "repository"
+ } else {
+ "repositories"
+ },
+ );
+
+ let mut iter =
+ sqlx::query_as::<_, (String, String)>(r#"SELECT * FROM "repos""#).fetch(&mut conn);
+ while let Some(Ok((repo, users))) = iter.next().await {
+ println!("{repo}: {}", users.split_whitespace().join(","))
+ }
+ } else {
+ let ret =
+ sqlx::query_as::<_, (String, String)>(r#"SELECT * FROM "repos" WHERE "repo" = ?"#)
+ .bind(repo)
+ .fetch_optional(&mut conn)
+ .await?;
+ if let Some((repo, users)) = ret {
+ println!("{repo}: {}", users.split_whitespace().join(","))
+ } else {
+ println!("Repository {repo} not register in database")
+ }
+ }
+
+ Ok(())
+}
diff --git a/src/main.rs b/src/main.rs
index f392792..318a94c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15,599 +15,19 @@
** along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+mod authentication;
mod database;
mod datastructures;
#[cfg(test)]
mod test;
-#[cfg(feature = "pam")]
-use crate::datastructures::AuthorizerType;
-use crate::datastructures::{Config, Cookie, FormData, TestSuite, WrapConfigure};
+use crate::datastructures::{Config, TestSuite};
use anyhow::Result;
use clap::{Arg, ArgMatches, Command};
-use handlebars::Handlebars;
-use itertools::Itertools as _;
use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder;
-use redis::AsyncCommands;
-use serde::Serialize;
-use sqlx::sqlite::SqliteConnectOptions;
-use sqlx::{ConnectOptions, Connection, SqliteConnection};
use std::env;
-use std::io::{BufRead, Write};
-use std::str::FromStr;
-use tempfile::TempDir;
-use tokio_stream::StreamExt as _;
-
-struct IOModule<R, W> {
- reader: R,
- writer: W,
-}
-
-impl<R: BufRead, W: Write> IOModule<R, W> {
- // Processing the `authenticate-post` called by cgit.
- async fn cmd_authenticate_post(&mut self, matches: &ArgMatches, cfg: Config) -> Result<()> {
- // Read stdin from upstream.
- let mut buffer = String::new();
- self.reader.read_to_string(&mut buffer)?;
-
- //log::debug!("{}", buffer);
- let data = datastructures::FormData::from(buffer);
-
- let cfg = WrapConfigure::from(cfg);
- log::trace!("Method is {}", cfg.get_authorizer().method());
- let ret = verify_login(&cfg, &data).await;
-
- if let Err(ref e) = ret {
- eprintln!("{e:?}");
- #[cfg(test)]
- eprintln!(
- "If database locked error occurs frequently, \
- please use environment DISK_WAIT_TIME to specify longer time."
- );
- log::error!("{e:?}")
- }
-
- if ret.unwrap_or(false) {
- let redis_conn = redis::Client::open("redis://127.0.0.1/")?;
- let cookie = Cookie::generate(data.get_user());
- let mut conn = redis_conn.get_multiplexed_async_connection().await?;
-
- conn.set_ex::<_, _, String>(
- format!("cgit_auth_{}", cookie.get_key()),
- cookie.get_body(),
- cfg.get_config().cookie_ttl,
- )
- .await?;
-
- let cookie_value = cookie.to_string();
-
- let is_secure = matches
- .get_one::<String>("https")
- .map(|s| s.as_str())
- .is_some_and(|x| matches!(x, "yes" | "on" | "1"));
- let domain = matches
- .get_one::<String>("http-host")
- .map(|s| s.as_str())
- .unwrap_or("*");
- let location = matches
- .get_one::<String>("http-referer")
- .map(|s| s.as_str())
- .unwrap_or("/");
- let cookie_suffix = if is_secure { "; secure" } else { "" };
- writeln!(&mut self.writer, "Status: 302 Found")?;
- writeln!(&mut self.writer, "Cache-Control: no-cache, no-store")?;
- writeln!(&mut self.writer, "Location: {}", location)?;
- writeln!(
- &mut self.writer,
- "Set-Cookie: cgit_auth={cookie_value}; Domain={domain}; Max-Age={}; HttpOnly{cookie_suffix}",
- cfg.get_config().cookie_ttl * 10,
- )?;
- } else {
- writeln!(&mut self.writer, "Status: 403 Forbidden")?;
- writeln!(&mut self.writer, "Cache-Control: no-cache, no-store")?;
- }
-
- writeln!(&mut self.writer)?;
- Ok(())
- }
-}
-
-// Processing the `authenticate-cookie` called by cgit.
-async fn cmd_authenticate_cookie(matches: &ArgMatches, cfg: Config) -> Result<bool> {
- let cookies = matches
- .get_one::<String>("http-cookie")
- .map(|s| s.as_str())
- .unwrap_or("");
- let repo = matches
- .get_one::<String>("repo")
- .map(|s| s.as_str())
- .unwrap_or("");
- /*let current_url = matches.value_of("current-url").unwrap_or("");*/
-
- let mut bypass = false;
-
- if cfg.bypass_root /*&& current_url.eq("/")*/ && repo.is_empty() {
- bypass = true;
- }
-
- if bypass || (!repo.is_empty() && !cfg.check_repo_protect(repo)) {
- return Ok(true);
- }
-
- if cookies.is_empty() {
- return Ok(false);
- }
-
- let redis_conn = redis::Client::open("redis://127.0.0.1/")?;
- let mut conn = redis_conn.get_multiplexed_async_connection().await?;
-
- let redis_key = format!("cgit_repo_{repo}");
- if !repo.is_empty() && !conn.exists(&redis_key).await? {
- let sql_conn = SqliteConnectOptions::from_str(cfg.get_database_location())?
- .read_only(true)
- .immutable(true)
- .disable_statement_logging()
- .connect()
- .await;
- if let Err(ref e) = sql_conn {
- log::error!(
- "Got error while open sqlite connection: {e:?}\nDatabase location: {}",
- cfg.get_database_location()
- );
- }
- let mut sql_conn = sql_conn?;
- if let Some((users,)) =
- sqlx::query_as::<_, (String,)>(r#"SELECT "users" FROM "repos" WHERE "repo" = ? "#)
- .bind(repo)
- .fetch_optional(&mut sql_conn)
- .await?
- {
- let users = users.split_whitespace().collect::<Vec<&str>>();
- let _: () = conn.sadd(&redis_key, users).await?;
- }
- }
-
- if let Ok(Some(cookie)) = Cookie::load_from_request(cookies) {
- log::debug!("Cookie is {cookie:?}");
- if let Ok(r) = conn
- .get::<_, String>(format!("cgit_auth_{}", cookie.get_key()))
- .await
- {
- conn.expire::<_, bool>(
- format!("cgit_auth_{}", cookie.get_key()),
- cfg.cookie_ttl as i64,
- )
- .await?;
- if cookie.eq_body(r.as_str()) {
- if repo.is_empty() {
- return Ok(true);
- }
- if conn
- .sismember::<_, _, i32>(&redis_key, cookie.get_user())
- .await?
- == 1
- {
- return Ok(true);
- }
- }
- }
- log::debug!("{cookie:?}");
- }
-
- Ok(false)
-}
-
-async fn cmd_init(cfg: Config) -> Result<()> {
- let loc = std::path::Path::new(cfg.get_database_location());
- let exists = loc.exists();
- if !exists {
- std::fs::File::create(loc)?;
- }
-
- let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?;
-
- if exists {
- let rows = sqlx::query(r#"SELECT name FROM sqlite_master WHERE type='table' AND name=?"#)
- .bind("auth_meta")
- .fetch_all(&mut conn)
- .await?;
-
- if !rows.is_empty() {
- return Ok(());
- }
- }
-
- sqlx::query(database::current::CREATE_TABLES)
- .execute(&mut conn)
- .await?;
- println!("Initialize the database successfully");
-
- drop(conn);
-
- cfg.write_database_commit_timestamp().await?;
- Ok(())
-}
-
-#[cfg(not(feature = "pam"))]
-async fn verify_login(cfg: &WrapConfigure, data: &FormData) -> Result<bool> {
- cfg.hook().await?;
- data.authorize(cfg.get_authorizer()).await
-}
-
-#[cfg(feature = "pam")]
-async fn verify_login(cfg: &WrapConfigure, data: &FormData) -> Result<bool> {
- if let AuthorizerType::Password = cfg.get_authorizer().method() {
- cfg.hook().await?;
- }
- data.authorize(cfg.get_authorizer()).await
-}
-
-#[derive(Serialize)]
-pub struct Meta<'a> {
- action: &'a str,
- redirect: &'a str,
- version: &'a str,
-}
-
-// Processing the `body` called by cgit.
-async fn cmd_body(matches: &ArgMatches, _cfg: Config) {
- let source = include_str!("authentication_page.html");
- let handlebars = Handlebars::new();
- let meta = Meta {
- action: matches
- .get_one::<String>("login-url")
- .map(|s| s.as_str())
- .unwrap_or_default(),
- redirect: matches
- .get_one::<String>("current-url")
- .map(|s| s.as_str())
- .unwrap_or_default(),
- version: env!("CARGO_PKG_VERSION"),
- };
- handlebars
- .render_template_to_write(source, &meta, std::io::stdout())
- .unwrap();
-}
-
-async fn cmd_add_user(matches: &ArgMatches, cfg: Config) -> Result<()> {
- let re = regex::Regex::new(r"^\w+$").unwrap();
- let user = matches
- .get_one::<String>("user")
- .map(|s| s.as_str())
- .unwrap_or_default();
- let passwd = matches
- .get_one::<String>("password")
- .map(|s| s.to_string())
- .unwrap_or_default();
- if user.is_empty() || passwd.is_empty() {
- 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 = SqliteConnection::connect(cfg.get_database_location()).await?;
-
- let items = sqlx::query(r#"SELECT 1 FROM "accounts" WHERE "user" = ? "#)
- .bind(user)
- .fetch_all(&mut conn)
- .await?;
-
- if !items.is_empty() {
- return Err(anyhow::Error::msg("User already exists!"));
- }
-
- let uid = uuid::Uuid::new_v4().to_string();
-
- sqlx::query(r#"INSERT INTO "accounts" VALUES (?, ?, ?) "#)
- .bind(user)
- .bind(FormData::gen_string_argon2_hash(&passwd)?)
- .bind(&uid)
- .execute(&mut conn)
- .await?;
-
- println!("Insert {user} ({uid}) to database");
-
- drop(conn);
-
- cfg.write_database_commit_timestamp().await?;
- Ok(())
-}
-
-async fn cmd_list_user(cfg: Config) -> Result<()> {
- let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?;
-
- let (count,) = sqlx::query_as::<_, (i32,)>(r#"SELECT COUNT(*) FROM "accounts""#)
- .fetch_one(&mut conn)
- .await?;
-
- if count > 0 {
- let mut iter =
- sqlx::query_as::<_, (String,)>(r#"SELECT "user" FROM "accounts""#).fetch(&mut conn);
-
- println!(
- "There is {count} user{} in database",
- if count > 1 { "s" } else { "" }
- );
- while let Some(Ok((row,))) = iter.next().await {
- println!("{row}")
- }
- } else {
- println!("There is not user exists.")
- }
-
- Ok(())
-}
-
-async fn cmd_delete_user(matches: &ArgMatches, cfg: Config) -> Result<()> {
- let user = matches
- .get_one::<String>("user")
- .map(|s| s.as_str())
- .unwrap_or("");
- if user.is_empty() {
- return Err(anyhow::Error::msg("Please input a valid username"));
- }
-
- let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?;
-
- let items = sqlx::query_as::<_, (i32,)>(r#"SELECT 1 FROM "accounts" WHERE "user" = ?"#)
- .bind(user)
- .fetch_all(&mut conn)
- .await?;
-
- if items.is_empty() {
- return Err(anyhow::Error::msg(format!("User {user} not found")));
- }
-
- sqlx::query(r#"DELETE FROM "accounts" WHERE "user" = ?"#)
- .bind(user)
- .execute(&mut conn)
- .await?;
-
- println!("Delete {user} from database");
-
- cfg.write_database_commit_timestamp().await?;
- Ok(())
-}
-
-async fn cmd_reset_database(matches: &ArgMatches, cfg: Config) -> Result<()> {
- if !matches.contains_id("confirm") {
- return Err(anyhow::Error::msg(
- "Please add --confirm argument to process reset",
- ));
- }
-
- let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?;
-
- sqlx::query(database::current::DROP_TABLES)
- .execute(&mut conn)
- .await?;
-
- sqlx::query(database::current::CREATE_TABLES)
- .execute(&mut conn)
- .await?;
-
- println!("Reset database successfully");
-
- cfg.write_database_commit_timestamp().await?;
- Ok(())
-}
-
-async fn cmd_upgrade_database(cfg: Config) -> Result<()> {
- let tmp_dir = TempDir::new()?;
-
- let v2_path = tmp_dir.path().join("v2.db");
- let v3_path = tmp_dir.path().join("v3.db");
-
- drop(std::fs::File::create(&v3_path).expect("Create v3 database failure"));
-
- std::fs::copy(cfg.get_database_location(), &v2_path)
- .expect("Copy v2 database to tempdir failure");
-
- let mut origin_conn = SqliteConnectOptions::from_str(v2_path.as_path().to_str().unwrap())?
- .read_only(true)
- .immutable(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(v3_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, String)>(r#"SELECT * FROM "accounts""#)
- .fetch(&mut origin_conn);
-
- while let Some(Ok((user, passwd, uid))) = iter.next().await {
- 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(&v3_path, cfg.get_database_location())
- .expect("Copy back to database location failure");
- println!("Upgrade database successful");
- } else {
- eprintln!(
- "Got database version {v} but {} required",
- database::previous::VERSION
- )
- }
- drop(origin_conn);
- tmp_dir.close()?;
-
- cfg.write_database_commit_timestamp().await?;
- Ok(())
-}
-
-async fn cmd_repo_user_control(matches: &ArgMatches, cfg: Config, is_delete: bool) -> Result<()> {
- let repo = matches
- .get_one::<String>("repo")
- .map(|s| s.as_str())
- .unwrap_or("");
- let user = matches
- .get_one::<String>("user")
- .map(|s| s.as_str())
- .unwrap_or_default();
-
- let clear_all = is_delete && matches.contains_id("clear-all");
-
- if repo.is_empty()
- || (is_delete && !clear_all && user.is_empty())
- || (!is_delete && user.is_empty())
- {
- return Err(anyhow::Error::msg("Invalid repository or username"));
- }
-
- let redis_client = redis::Client::open("redis://127.0.0.1/")?;
- let mut redis_conn = redis_client.get_multiplexed_async_connection().await?;
-
- let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?;
-
- if sqlx::query(r#"SELECT "users" FROM "repos" WHERE "repo" = ?"#)
- .bind(repo)
- .fetch_optional(&mut conn)
- .await?
- .is_none()
- {
- if is_delete {
- println!("Row is empty.");
- return Ok(());
- }
- sqlx::query(r#"INSERT INTO "repos" VALUES (?, ?)"#)
- .bind(repo)
- .bind("")
- .execute(&mut conn)
- .await?;
- }
-
- let (users,) =
- sqlx::query_as::<_, (String,)>(r#"SELECT "users" FROM "repos" WHERE "repo" = ?"#)
- .bind(repo)
- .fetch_optional(&mut conn)
- .await?
- .unwrap();
- let mut users = users.split_whitespace().collect::<Vec<&str>>();
-
- if let Some(index) = users.clone().into_iter().position(|x| x.eq(user)) {
- if is_delete {
- if clear_all {
- users.clear();
- } else {
- users.remove(index);
- }
- } else {
- return Err(anyhow::Error::msg("User already in repository ACL"));
- }
- }
-
- if !is_delete {
- users.push(user);
- }
-
- sqlx::query(r#"UPDATE "repos" SET "users" = ? WHERE "repo" = ?"#)
- .bind(users.join(" "))
- .bind(repo)
- .execute(&mut conn)
- .await?;
-
- let redis_key = format!("cgit_repo_{repo}");
- if redis_conn.exists::<_, i32>(&redis_key).await? == 0 {
- redis_conn.sadd::<_, _, i32>(&redis_key, users).await?;
- } else if is_delete {
- if clear_all {
- redis_conn.del::<_, i32>(&redis_key).await?;
- } else {
- redis_conn.srem::<_, _, i32>(&redis_key, user).await?;
- }
- } else {
- redis_conn.sadd::<_, _, i32>(&redis_key, user).await?;
- }
-
- if !clear_all {
- println!(
- "{} user {user} {} repository {repo} ACL successful",
- if is_delete { "Delete" } else { "Add" },
- if is_delete { "from" } else { "to" },
- );
- } else {
- println!("Clear all users from repository {repo} ACL");
- }
-
- Ok(())
-}
-
-async fn cmd_list_repos_acl(arg_matches: &ArgMatches, cfg: Config) -> Result<()> {
- let repo = arg_matches
- .get_one::<String>("repo")
- .map(|s| s.as_str())
- .unwrap_or("");
-
- let mut conn = SqliteConnectOptions::from_str(cfg.get_database_location())?
- .read_only(true)
- .immutable(true)
- .connect()
- .await?;
-
- if repo.is_empty() {
- let (length,) = sqlx::query_as::<_, (i32,)>(r#"SELECT COUNT(*) FROM "repos""#)
- .fetch_optional(&mut conn)
- .await?
- .unwrap_or((0,));
-
- println!(
- "There is total {length} {} in database",
- if length == 1 {
- "repository"
- } else {
- "repositories"
- },
- );
-
- let mut iter =
- sqlx::query_as::<_, (String, String)>(r#"SELECT * FROM "repos""#).fetch(&mut conn);
- while let Some(Ok((repo, users))) = iter.next().await {
- println!("{repo}: {}", users.split_whitespace().join(","))
- }
- } else {
- let ret =
- sqlx::query_as::<_, (String, String)>(r#"SELECT * FROM "repos" WHERE "repo" = ?"#)
- .bind(repo)
- .fetch_optional(&mut conn)
- .await?;
- if let Some((repo, users)) = ret {
- println!("{repo}: {}", users.split_whitespace().join(","))
- } else {
- println!("Repository {repo} not register in database")
- }
- }
-
- Ok(())
-}
async fn async_main(arg_matches: ArgMatches) -> Result<i32> {
let cfg = if env::args().any(|x| x.eq("--test")) {
@@ -617,7 +37,7 @@ async fn async_main(arg_matches: ArgMatches) -> Result<i32> {
};
match arg_matches.subcommand() {
Some(("authenticate-cookie", matches)) => {
- if let Ok(should_pass) = cmd_authenticate_cookie(matches, cfg).await {
+ if let Ok(should_pass) = authentication::cmd_authenticate_cookie(matches, cfg).await {
if should_pass {
return Ok(1);
}
@@ -628,46 +48,45 @@ async fn async_main(arg_matches: ArgMatches) -> Result<i32> {
let input = stdin.lock();
let output = std::io::stdout();
- let mut module = IOModule {
- reader: input,
- writer: output,
- };
+ let mut module = authentication::IOModule::new(input, output);
module.cmd_authenticate_post(matches, cfg).await?;
}
Some(("body", matches)) => {
- cmd_body(matches, cfg).await;
+ authentication::cmd_body(matches, cfg).await;
}
Some(("user", matches)) => match matches.subcommand() {
Some(("add", matches)) => {
- cmd_add_user(matches, cfg).await?;
+ authentication::cmd_add_user(matches, cfg).await?;
}
Some(("del", matches)) => {
- cmd_delete_user(matches, cfg).await?;
+ authentication::cmd_delete_user(matches, cfg).await?;
}
Some(("list", _matches)) => {
- cmd_list_user(cfg).await?;
+ authentication::cmd_list_user(cfg).await?;
}
_ => {}
},
Some(("database", matches)) => match matches.subcommand() {
Some(("init", _matches)) => {
- cmd_init(cfg).await?;
+ authentication::cmd_init(cfg).await?;
}
Some(("upgrade", _matches)) => {
- cmd_upgrade_database(cfg).await?;
+ authentication::cmd_upgrade_database(cfg).await?;
}
Some(("reset", matches)) => {
- cmd_reset_database(matches, cfg).await?;
+ authentication::cmd_reset_database(matches, cfg).await?;
}
_ => {}
},
Some(("repo", matches)) => match matches.subcommand() {
- Some(("add", matches)) => cmd_repo_user_control(matches, cfg, false).await?,
+ Some(("add", matches)) => {
+ authentication::cmd_repo_user_control(matches, cfg, false).await?
+ }
Some(("del", matches)) => {
- cmd_repo_user_control(matches, cfg, true).await?;
+ authentication::cmd_repo_user_control(matches, cfg, true).await?;
}
Some(("list", matches)) => {
- cmd_list_repos_acl(matches, cfg).await?;
+ authentication::cmd_list_repos_acl(matches, cfg).await?;
}
_ => {}
},
@@ -814,20 +233,18 @@ fn main() -> Result<()> {
let logfile = match logfile {
Ok(f) => f,
Err(e) => {
- return Err(anyhow::Error::msg(format!(
+ return Err(anyhow::anyhow!(
"Got error while append to {logfile_path}: {e:?}",
- )));
+ ));
}
};
let config = log4rs::Config::builder()
.appender(Appender::builder().build("logfile", Box::new(logfile)))
- .logger(
+ .loggers([
log4rs::config::Logger::builder().build("handlebars::render", log::LevelFilter::Warn),
- )
- .logger(
log4rs::config::Logger::builder().build("handlebars::context", log::LevelFilter::Warn),
- )
+ ])
.build(
Root::builder()
.appender("logfile")
diff --git a/src/test.rs b/src/test.rs
index 3d54d71..e6e1bbc 100644
--- a/src/test.rs
+++ b/src/test.rs
@@ -18,7 +18,9 @@
#[cfg(test)]
mod core {
use crate::{
- IOModule, cmd_add_user, cmd_authenticate_cookie, cmd_init, cmd_repo_user_control,
+ authentication::{
+ IOModule, cmd_add_user, cmd_authenticate_cookie, cmd_init, cmd_repo_user_control,
+ },
datastructures::{Config, TestSuite, rand_str},
get_arg_matches,
};
@@ -99,10 +101,7 @@ mod core {
"/?p=login",
]));
let mut output = Vec::new();
- let mut module = IOModule {
- reader: &correct_input[..],
- writer: &mut output,
- };
+ let mut module = IOModule::new(&correct_input[..], &mut output);
let cfg = Config::generate_test_config();