forge/forge-server/src/main.rs

287 lines
7.7 KiB
Rust

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 HostConfig {
ip: String,
port: u16,
build_directory: Option<String>,
}
#[derive(Serialize, Deserialize, Clone)]
struct AuthConfig {
authenticate: bool,
password: Option<String>,
}
#[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<Vec<String>>,
tag: Tag,
}
#[derive(Serialize, Deserialize)]
struct Message {
authentication: Option<String>,
uuid: Uuid,
pre_exec: Option<String>,
profile: bool,
command: Command,
repository: Url,
basename: String,
}
async fn build(msg: Message, address: SocketAddr, dir: String) {
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 <green>{:?}<//>: <magenta>{}<//>\n<bright-white>cd {}\ngit clone {}\ncd {}\n{} {} {} {}\n<//>",
address,
msg.uuid.format_hyphenated(),
dir,
msg.repository,
msg.basename,
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,
dir: String,
salt: SaltString,
argon2: Argon2<'_>,
) {
let (reader, 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) => {
let directory = dir.clone();
if json
.authenticate(pass.clone(), salt.clone(), argon2.clone())
.await
{
tokio::spawn(async move {
build(json, address, directory).await;
});
}
}
None => {
error!("No password configured!");
}
}
} else {
let directory = dir.clone();
tokio::spawn(async move {
build(json, address, directory).await;
});
}
}
}
#[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: <green>{}<//>", 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 dir = config.host.build_directory.clone().unwrap();
let m_salt = salt.clone();
let m_argon2 = argon2.clone();
tokio::spawn(async move {
process_socket(socket, addr, auth, dir, 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 mut default_build_dir = dirs::home_dir().unwrap();
default_build_dir.push("src");
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,
build_directory: Some(default_build_dir.to_str().unwrap().to_string()),
},
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;
}