use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; use paris::{error, info, success, warn}; use serde::{Deserialize, Serialize}; use serde_json::Result; use std::{fmt, fs, io, net::SocketAddr, path::PathBuf, process::exit}; use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::net::{TcpListener, TcpStream}; use url::Url; use uuid::Uuid; use uuid_simd::UuidExt; #[derive(Serialize, Deserialize)] struct Status { status: bool, } #[derive(Serialize, Deserialize)] struct HostConfig { ip: String, port: u16, } #[derive(Serialize, Deserialize, Clone)] struct AuthConfig { authenticate: bool, password: Option, } #[derive(Serialize, Deserialize)] struct Config { host: HostConfig, auth: AuthConfig, } #[derive(Serialize, Deserialize, Debug)] enum BuildSystem { Cargo, Make, Custom(String), } impl fmt::Display for BuildSystem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { BuildSystem::Cargo => write!(f, "cargo"), BuildSystem::Make => write!(f, "make"), BuildSystem::Custom(s) => write!(f, "{}", s), } } } #[derive(Serialize, Deserialize, Debug)] enum Subcommand { Run, Build, Install, Custom(String), } impl fmt::Display for Subcommand { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Subcommand::Run => write!(f, "run"), Subcommand::Build => write!(f, "build"), Subcommand::Install => write!(f, "install"), Subcommand::Custom(s) => write!(f, "{}", s), } } } #[derive(Serialize, Deserialize)] enum Tag { Release, Debug, } impl fmt::Display for Tag { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Tag::Release => write!(f, "--release"), Tag::Debug => write!(f, "--debug"), } } } #[derive(Serialize, Deserialize)] struct Command { build_system: BuildSystem, subcommand: Subcommand, features: Option>, tag: Tag, } #[derive(Serialize, Deserialize)] struct Message { authentication: Option, uuid: Uuid, pre_exec: Option, profile: bool, command: Command, repository: Url, basename: String, } async fn build(msg: Message, address: SocketAddr) { let mut features = String::new(); match msg.command.features { Some(f) => { features.push_str("--features '"); for i in f { features.push_str(&i); features.push_str(" "); } features.push_str("'"); } None => features = "".to_string(), } success!( "Received message from {:?}: {}\ngit clone {}\n{} {} {} {}", address, msg.uuid.format_hyphenated(), msg.repository, msg.command.build_system, msg.command.subcommand, msg.command.tag, features ); match msg.pre_exec { Some(e) => info!("Running pre-exec: {}", e), None => {} } } impl Message { async fn authenticate(&self, passwd: String, salt: SaltString, argon2: Argon2<'_>) -> bool { match &self.authentication { Some(auth) => { let password_hash = argon2 .hash_password(auth.as_bytes(), &salt) .unwrap() .to_string(); let parsed_hash = PasswordHash::new(&password_hash).unwrap(); match Argon2::default().verify_password(passwd.as_bytes(), &parsed_hash) { Ok(s) => { return true; } Err(e) => { error!("{}", e); return false; } } } None => { error!("No authentication provided!"); return false; } } } } async fn process_socket( mut socket: TcpStream, address: SocketAddr, auth: AuthConfig, salt: SaltString, argon2: Argon2<'_>, ) { let (reader, mut writer) = socket.split(); let mut reader = BufReader::new(reader); let mut line = String::new(); loop { let bytes_read = reader.read_line(&mut line).await.unwrap(); if bytes_read == 0 { break; } let json: Message = match serde_json::from_str(&line) { Ok(v) => v, Err(e) => { error!("{}", e); return; } }; if auth.authenticate { match &auth.password { Some(pass) => { if json .authenticate(pass.clone(), salt.clone(), argon2.clone()) .await { build(json, address).await; let status = Status { status: true }; let j_status = serde_json::to_string(&status).unwrap(); info!("{}", j_status); writer.write_all(j_status.as_bytes()).await.unwrap(); } } None => { error!("No password configured!"); } } } else { build(json, address).await; let status = Status { status: true }; let j_status = serde_json::to_string(&status).unwrap(); info!("{}", j_status); writer.write_all(j_status.as_bytes()).await.unwrap(); } } } #[tokio::main] async fn main() -> io::Result<()> { let config = configure().await; let addr = format!("{}:{}", config.host.ip, config.host.port); let listener = TcpListener::bind(&addr).await?; info!("Listening on: {}", addr); let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); loop { match listener.accept().await { Ok((socket, addr)) => { let auth = config.auth.clone(); let m_salt = salt.clone(); let m_argon2 = argon2.clone(); tokio::spawn(async move { process_socket(socket, addr, auth, m_salt, m_argon2).await; }); } Err(e) => error!("couldn't get client: {:?}", e), } } } async fn configure() -> Config { let mut default_config = dirs::config_dir().unwrap(); default_config.push("forge"); default_config.push("config.toml"); let config_contents = match fs::read_to_string(&default_config) { Ok(f) => f, Err(e) => { warn!("Unable to read from config file: {}", e); let config = Config { host: HostConfig { ip: "127.0.0.1".to_string(), port: 9134, }, auth: AuthConfig { authenticate: false, password: None, }, }; let toml = toml::to_string(&config).unwrap(); default_config.pop(); fs::create_dir_all(&default_config).unwrap(); default_config.push("config.toml"); fs::write(&default_config, toml).unwrap(); info!("Created new config file at {}", &default_config.display()); exit(0); } }; let config: Config = match toml::from_str(&config_contents) { Ok(s) => s, Err(e) => { error!("Couldn't parse config file! {}", e); exit(1); } }; return config; }