diff options
| author | KunoiSayami <[email protected]> | 2021-05-06 00:03:27 +0800 |
|---|---|---|
| committer | KunoiSayami <[email protected]> | 2021-05-06 00:03:27 +0800 |
| commit | cbc7b3810bfe8fa750e566e7db2166ef8b7fcad1 (patch) | |
| tree | 91be52074dfb0652cbfefa05f5e9ca07001ce4a8 /src | |
init
Diffstat (limited to 'src')
| -rw-r--r-- | src/authentication_page.html | 9 | ||||
| -rw-r--r-- | src/database.rs | 43 | ||||
| -rw-r--r-- | src/datastructures.rs | 75 | ||||
| -rw-r--r-- | src/main.rs | 326 |
4 files changed, 453 insertions, 0 deletions
diff --git a/src/authentication_page.html b/src/authentication_page.html new file mode 100644 index 0000000..73ec85a --- /dev/null +++ b/src/authentication_page.html @@ -0,0 +1,9 @@ +<h2>Authentication Required</h2> +<form method='post' action='{{action}}'> + <input type='hidden' name='redirect' value='{{redirect}}' /> + <table> + <tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr> + <tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr> + <tr><td colspan='2'><input value='Login' type='submit' /></td></tr> + </table> +</form>
\ No newline at end of file diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..1a1dedf --- /dev/null +++ b/src/database.rs @@ -0,0 +1,43 @@ +/* + ** Copyright (C) 2021 KunoiSayami + ** + ** This file is part of cgit-simple-authentication and is released under + ** the AGPL v3 License: https://www.gnu.org/licenses/agpl-3.0.txt + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU Affero General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU Affero General Public License for more details. + ** + ** 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/>. + */ +#[allow(dead_code)] +pub mod v1 { + pub const CREATE_TABLES: &str = r#" + CREATE TABLE "accounts" ( + "user" TEXT NOT NULL, + "password" TEXT NOT NULL, + PRIMARY KEY("user") + ); + + + CREATE TABLE "auth_meta" ( + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + PRIMARY KEY("key") + ); + + INSERT INTO "auth_meta" VALUES ('version', '1'); + "#; + + pub const VERSION: &str = "1"; + +} +pub use v1::VERSION; +pub use v1 as current; diff --git a/src/datastructures.rs b/src/datastructures.rs new file mode 100644 index 0000000..7ee01a9 --- /dev/null +++ b/src/datastructures.rs @@ -0,0 +1,75 @@ +/* + ** Copyright (C) 2021 KunoiSayami + ** + ** This file is part of cgit-simple-authentication and is released under + ** the AGPL v3 License: https://www.gnu.org/licenses/agpl-3.0.txt + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU Affero General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU Affero General Public License for more details. + ** + ** 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/>. + */ + + +use sha2::Digest; +use anyhow::Result; + +#[derive(Debug, Clone, Default)] +pub struct Config { + +} + +impl Config { + pub fn new() -> Self { + Self {} + } +} + + +#[derive(Debug, Clone, Default)] +pub struct FormData { + user: String, + password: String, + hash: String, +} + +impl FormData { + pub fn new() -> Self { + Self { ..Default::default()} + } + + pub fn set_password(&mut self, password: String) { + self.password = password; + self.hash = Default::default(); + } + + pub fn set_user(&mut self, user: String) { + self.user = user + } + + pub fn get_user(&self) -> &String { + &self.user + } + + pub fn get_password_sha256(&self) -> Result<String> { + let mut hasher = sha2::Sha256::new(); + hasher.update(self.password.as_bytes()); + Ok(format!("{:x}", hasher.finalize())) + } + + #[allow(dead_code)] + pub fn get_password_sha256_cache(&mut self) -> Result<String> { + if self.hash.len() == 0 { + self.hash = self.get_password_sha256()?; + } + Ok(self.hash.clone()) + } +}
\ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..81f09e2 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,326 @@ +/* + ** Copyright (C) 2021 KunoiSayami + ** + ** This file is part of cgit-simple-authentication and is released under + ** the AGPL v3 License: https://www.gnu.org/licenses/agpl-3.0.txt + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU Affero General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU Affero General Public License for more details. + ** + ** 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/>. + */ + +mod database; +mod datastructures; + +use std::fs::OpenOptions; +use anyhow::Result; +use std::io::{Write, stdin, Read}; +use std::{process, env}; +use log::LevelFilter; +use syslog::{Formatter3164, Facility, BasicLogger}; +use clap::{Arg, App, SubCommand, ArgMatches}; +use rand::Rng; +use serde::{Serialize}; +use handlebars::Handlebars; +use std::borrow::Cow; +use url::form_urlencoded; +use sqlx::Connection; +use crate::datastructures::{Config, FormData}; +use redis::Commands; + +const COOKIE_LENGTH: usize = 45; + +fn rand_str(len: usize) -> String { + const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ + abcdefghijklmnopqrstuvwxyz\ + 0123456789"; + let mut rng = rand::thread_rng(); + + let password: String = (0..len) + .map(|_| { + let idx = rng.gen_range(0, CHARSET.len()); + CHARSET[idx] as char + }) + .collect(); + + password +} + + +#[derive(Serialize)] +struct Meta<'a> { + action: &'a str, + redirect: &'a str, +} + +// Verify username and password via gogs. +async fn verify_login(cfg: &Config, data: &FormData) -> Result<bool> { + let mut conn = sqlx::SqliteConnection::connect("/tmp/database.db").await?; + let password_sha = data.get_password_sha256()?; + let ret = sqlx::query(r#"SELECT 1 FROM "accounts" WHERE "user" = ? AND "password" = ? "#) + .bind(data.get_user()) + .bind(password_sha) + .fetch_all(&mut conn) + .await?; + Ok(ret.len() > 0) +} + + +// Processing the `authenticate-basic` called by cgit. +fn cmd_authenticate_basic( + matches: &ArgMatches, + cfg: Option<Config>, +) -> Result<()> { + unimplemented!() +} + +// Processing the `authenticate-cookie` called by cgit. +async fn cmd_authenticate_cookie( + matches: &ArgMatches<'_>, + cfg: Option<Config>, +) -> Result<bool> { + let cookies = matches.value_of("http-cookie").unwrap_or(""); + + if cookies.is_empty() { + return Ok(false) + } + + let mut conn = redis::Client::open("redis://127.0.0.1/")?; + + for cookie in cookies.split(';').map(|x| x.trim()) { + let (key, value) = cookie.split_once('=').unwrap(); + if key.eq("cgit_auth") { + if conn.get::<_, i32>(format!("cgit_auth_{}", value)).is_ok() { + return Ok(true) + } + break + } + } + + Ok(false) +} + + +async fn cmd_init(cfg: Option<Config>) -> Result<()> { + + + // TODO: read database location from configure file + let mut conn = sqlx::SqliteConnection::connect("sqlite::memory:").await?; + + 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() { + sqlx::query(database::current::CREATE_TABLES) + .execute(&mut conn) + .await?; + } + + Ok(()) +} + +// Processing the `authenticate-post` called by cgit. +async fn cmd_authenticate_post( + matches: &ArgMatches<'_>, + cfg: Option<Config>, +) -> Result<()> { + // Load configurations. + let cfg = cfg.unwrap_or_default(); + // Read stdin from upstream. + let mut buffer = String::new(); + stdin().read_to_string(&mut buffer)?; + let mut data = datastructures::FormData::new(); + // Parsing user posted form. + let fields = form_urlencoded::parse(buffer.as_bytes()); + for f in fields { + match f.0 { + Cow::Borrowed("username") => { + data.set_user(f.1.to_string()) + } + Cow::Borrowed("password") => { + data.set_password(f.1.to_string()) + } + _ => {} + } + } + + // Authenticated via gogs. + if verify_login(&cfg, &data).await.is_ok() { + //let hash = data.hash(); + //let cgitauth = format!("{}:{}", hash, data.nonce); + //let cgitauth_b64 = base64::encode_block(cgitauth.as_bytes()); + //let path = Path::new(&cfg.cache_dir).join(&hash); + //data.to_file(path, true); + let cookie = rand_str(COOKIE_LENGTH); + + //let redis = redis_async::client::connect(&SocketAddr::from_str("127.0.0.1:5432").unwrap()).await?; + //redis. + let mut conn = redis::Client::open("redis://127.0.0.1/")?; + conn.set_ex::<_, &str, i32>(format!("cgit_auth_{}", cookie), "1", 600)?; + + let is_secure = matches + .value_of("https") + .map_or(false, |x| matches!(x, "yes" | "on" | "1")); + let domain = matches.value_of("http-host").unwrap_or("*"); + let location = matches + .value_of("current-url") + .unwrap_or("/") + .split('?') + .next() + .unwrap(); + let cookie_suffix = if is_secure { "; secure" } else { "" }; + println!("Status: 302 Redirect"); + println!("Cache-Control: no-cache, no-store"); + println!("Location: {}", location); + println!( + "Set-Cookie: cgit_auth={}; Domain={}; Max-Age={}; HttpOnly{}", + // TODO: use cookie ttl instead + //cookie, domain, cfg.cookie_ttl, cookie_suffix + cookie, domain, 600, cookie_suffix + ); + } else { + println!("Status: 403 Forbidden"); + println!("Cache-Control: no-cache, no-store"); + } + println!(); + + + Ok(()) +} + + +// Processing the `body` called by cgit. +async fn cmd_body(matches: &ArgMatches<'_>, _cfg: Option<Config>) { + let source = include_str!("authentication_page.html"); + let handlebars = Handlebars::new(); + let meta = Meta { + action: matches.value_of("login-url").unwrap_or(""), + redirect: matches.value_of("current-url").unwrap_or(""), + }; + handlebars + .render_template_to_write(source, &meta, std::io::stdout()) + .unwrap(); +} + +// Processing the `body` called by cron. +fn cmd_expire(_matches: &ArgMatches, cfg: Option<Config>) { + unimplemented!(); +} + + +async fn async_main(arg_matches: ArgMatches<'_>, cfg: Config) -> Result<i32>{ + match arg_matches.subcommand() { + ("authenticate-cookie", Some(matches)) => { + if cmd_authenticate_cookie(matches, Some(cfg)).await.is_ok() { + return Ok(1) + } + } + ("authenticate-post", Some(matches)) => { + cmd_authenticate_post(matches, Some(cfg)).await.unwrap(); + } + ("body", Some(matches)) => { + cmd_body(matches, Some(cfg)).await; + } + ("expire", Some(matches)) => { + cmd_expire(matches, Some(cfg)); + } + ("init", Some(matches)) => { + cmd_init(Some(cfg)).await?; + } + _ => {} + } + Ok(0) +} + + +fn main() -> Result<()>{ + + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open("/tmp/output")?; + let args = std::env::args().collect::<Vec<String>>().join(" "); + //args.push("\n".into()); + file.write_all(args.as_bytes())?; + file.write_all("\n".as_bytes())?; + //println!(r#"<meta name="test">"#); + + + let formatter = Formatter3164 { + facility: Facility::LOG_USER, + hostname: None, + process: "cgit-gogs-auth-filter".into(), + pid: process::id() as i32, + }; + + let logger = syslog::unix(formatter).expect("could not connect to syslog"); + if let Ok(()) = log::set_boxed_logger(Box::new(BasicLogger::new(logger))) { + log::set_max_level(LevelFilter::Debug); + } + + // Prints each argument on a separate line + for (nth, argument) in env::args().enumerate() { + log::debug!("[{}]={}", nth, argument); + } + + // Sub-arguments for each command, see cgi defines. + let sub_args = &[ + Arg::with_name("http-cookie").required(true), + Arg::with_name("request-method").required(true), + Arg::with_name("query-string").required(true), + Arg::with_name("http-referer").required(true), + Arg::with_name("path-info").required(true), + Arg::with_name("http-host").required(true), + Arg::with_name("https").required(true), + Arg::with_name("repo").required(true), + Arg::with_name("page").required(true), + Arg::with_name("current-url").required(true), + Arg::with_name("login-url").required(true), + ]; + + let matches = App::new("Simple Authentication Filter for cgit") + .version(env!("CARGO_PKG_VERSION")) + .subcommand( + SubCommand::with_name("authenticate-cookie") + .about("Processing authenticated cookie") + .args(sub_args), + ) + .subcommand( + SubCommand::with_name("authenticate-post") + .about("Processing posted username and password") + .args(sub_args), + ) + .subcommand( + SubCommand::with_name("body") + .about("Return the login form") + .args(sub_args), + ) + .subcommand(SubCommand::with_name("init").about("Init sqlite database")) + //.subcommand(SubCommand::with_name("expire").about("Check and clean all expired cookies")) + .get_matches(); + + // Load filter configurations + let cfg = Config::new(); + + let ret = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async_main(matches, cfg))?; + if ret == 1 { + std::process::exit(1); + } + + Ok(()) +}
\ No newline at end of file |
