325 lines
8.9 KiB
Rust
325 lines
8.9 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::process::Command as pCommand;
|
|
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: PathBuf) {
|
|
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(),
|
|
}
|
|
|
|
let mut build_dir = dir;
|
|
build_dir.push(&msg.basename);
|
|
|
|
let mut manifest = build_dir.clone();
|
|
manifest.push("Cargo.toml");
|
|
|
|
success!(
|
|
"Received message from <green>{:?}<//>: <magenta>{}<//>",
|
|
address,
|
|
msg.uuid,
|
|
);
|
|
|
|
let git_output = pCommand::new("git")
|
|
.arg("clone")
|
|
.arg(msg.repository.to_string())
|
|
.arg(&build_dir)
|
|
.output()
|
|
.unwrap();
|
|
if !git_output.status.success() {
|
|
warn!(
|
|
"Git clone failed for repository <cyan>{}<//>",
|
|
&msg.repository
|
|
);
|
|
}
|
|
|
|
tokio::spawn(async move {
|
|
let build_output = match msg.command.tag {
|
|
Tag::Release => {
|
|
pCommand::new(msg.command.build_system.to_string())
|
|
.arg(msg.command.subcommand.to_string())
|
|
.arg(msg.command.tag.to_string())
|
|
.arg(format!("--manifest-path={}", &manifest.display()))
|
|
// .arg(features.to_string())
|
|
.output()
|
|
.unwrap()
|
|
}
|
|
Tag::Debug => pCommand::new(msg.command.build_system.to_string())
|
|
.arg(msg.command.subcommand.to_string())
|
|
.arg(format!("--manifest-path={}", &manifest.display()))
|
|
.output()
|
|
.unwrap(),
|
|
};
|
|
|
|
if !build_output.status.success() {
|
|
warn!("Build <magenta>{}<//> failed!", &msg.uuid);
|
|
} else {
|
|
success!("Build <magenta>{}<//> finished!", &msg.uuid);
|
|
}
|
|
});
|
|
|
|
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: PathBuf,
|
|
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 = PathBuf::from(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;
|
|
}
|