diff --git a/Cargo.lock b/Cargo.lock index 0487d6a..7f30b4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "admin" +version = "0.1.0" +dependencies = [ + "blake2b_simd", + "getopts", + "thiserror", + "xash3d-protocol", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -17,6 +27,18 @@ dependencies = [ "libc", ] +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "autocfg" version = "1.1.0" @@ -29,6 +51,17 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -62,6 +95,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "core-foundation-sys" version = "0.8.4" diff --git a/Cargo.toml b/Cargo.toml index 67b457f..8985122 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,5 @@ resolver = "2" members = [ "protocol", "master", + "admin", ] diff --git a/admin/Cargo.toml b/admin/Cargo.toml new file mode 100644 index 0000000..3fa291b --- /dev/null +++ b/admin/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "admin" +version = "0.1.0" +license = "GPL-3.0-only" +authors = ["Denis Drakhnia "] +edition = "2021" +rust-version = "1.56" + +[dependencies] +thiserror = "1.0.49" +getopts = "0.2.21" +blake2b_simd = "<0.6" +xash3d-protocol = { path = "../protocol", version = "0.1.0" } diff --git a/admin/src/cli.rs b/admin/src/cli.rs new file mode 100644 index 0000000..65503ca --- /dev/null +++ b/admin/src/cli.rs @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2023 Denis Drakhnia + +use std::process; + +use getopts::Options; +use xash3d_protocol::admin; + +const BIN_NAME: &str = env!("CARGO_BIN_NAME"); +const PKG_NAME: &str = env!("CARGO_PKG_NAME"); +const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const DEFAULT_HOST: &str = "mentality.rip"; +const DEFAULT_PORT: u16 = 27010; + +#[derive(Debug)] +pub struct Cli { + pub address: String, + pub command: String, + pub hash_len: usize, + pub hash_key: String, + pub hash_personal: String, +} + +impl Default for Cli { + fn default() -> Cli { + Cli { + address: format!("{}:{}", DEFAULT_HOST, DEFAULT_PORT), + command: String::new(), + hash_len: admin::HASH_LEN, + hash_key: admin::HASH_KEY.to_owned(), + hash_personal: admin::HASH_PERSONAL.to_owned(), + } + } +} + +fn print_usage(opts: Options) { + let brief = format!("Usage: {} [options]", BIN_NAME); + print!("{}", opts.usage(&brief)); +} + +fn print_version() { + println!("{} v{}", PKG_NAME, PKG_VERSION); +} + +pub fn parse() -> Cli { + let mut cli = Cli::default(); + + let args: Vec<_> = std::env::args().collect(); + let mut opts = Options::new(); + opts.optflag("h", "help", "print usage help"); + opts.optflag("v", "version", "print program version"); + let help = format!("address to connect [default: {}]", cli.address); + opts.optopt("h", "host", &help, "ADDR"); + let help = format!("hash length [default: {}]", cli.hash_len); + opts.optopt("l", "hash-length", &help, "N"); + let help = format!("hash key [default: {}]", cli.hash_key); + opts.optopt("k", "hash-key", &help, "KEY"); + let help = format!("hash personalization [default: {}]", cli.hash_personal); + opts.optopt("p", "hash-personal", &help, "PERSONAL"); + + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(e) => { + eprintln!("{}", e); + process::exit(1); + } + }; + + if matches.opt_present("help") { + print_usage(opts); + process::exit(0); + } + + if matches.opt_present("version") { + print_version(); + process::exit(0); + } + + if let Some(mut s) = matches.opt_str("host") { + if !s.contains(':') { + s.push_str(":27010"); + } + cli.address = s; + } + + match matches.opt_get("hash-length") { + Ok(Some(n)) => cli.hash_len = n, + Ok(None) => {} + Err(_) => { + eprintln!("Invalid key length"); + process::exit(1); + } + } + + if let Some(s) = matches.opt_str("hash-key") { + cli.hash_key = s; + } + + if let Some(s) = matches.opt_str("hash-personal") { + cli.hash_personal = s; + } + + cli.command = matches.free.join(" "); + cli +} diff --git a/admin/src/main.rs b/admin/src/main.rs new file mode 100644 index 0000000..0a6f35a --- /dev/null +++ b/admin/src/main.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2023 Denis Drakhnia + +mod cli; + +use std::io; +use std::net::UdpSocket; + +use blake2b_simd::Params; +use thiserror::Error; +use xash3d_protocol::{admin, master}; + +#[derive(Error, Debug)] +enum Error { + #[error("Unexpected response from master server")] + UnexpectedPacket, + #[error(transparent)] + Protocol(#[from] xash3d_protocol::Error), + #[error(transparent)] + Io(#[from] io::Error), +} + +fn send_command(cli: &cli::Cli) -> Result<(), Error> { + let sock = UdpSocket::bind("0.0.0.0:0")?; + sock.connect(&cli.address)?; + + let mut buf = [0; 512]; + let n = admin::AdminChallenge.encode(&mut buf)?; + sock.send(&buf[..n])?; + + let n = sock.recv(&mut buf)?; + let challenge = match master::Packet::decode(&buf[..n])? { + master::Packet::AdminChallengeResponse(p) => p.challenge, + _ => return Err(Error::UnexpectedPacket), + }; + + println!("Password:"); + let mut password = String::new(); + io::stdin().read_line(&mut password)?; + if password.ends_with('\n') { + password.pop(); + } + + let hash = Params::new() + .hash_length(cli.hash_len) + .key(cli.hash_key.as_bytes()) + .personal(cli.hash_personal.as_bytes()) + .to_state() + .update(password.as_bytes()) + .update(&challenge.to_le_bytes()) + .finalize(); + + let n = admin::AdminCommand::new(hash.as_bytes(), &cli.command).encode(&mut buf)?; + sock.send(&buf[..n])?; + + Ok(()) +} + +fn main() { + let cli = cli::parse(); + + if let Err(e) = send_command(&cli) { + eprintln!("error: {}", e); + std::process::exit(1); + } +} diff --git a/protocol/src/admin.rs b/protocol/src/admin.rs index f2b7a68..a1c162e 100644 --- a/protocol/src/admin.rs +++ b/protocol/src/admin.rs @@ -6,6 +6,8 @@ use crate::types::Str; use crate::Error; pub const HASH_LEN: usize = 64; +pub const HASH_KEY: &str = "Half-Life"; +pub const HASH_PERSONAL: &str = "Freeman"; #[derive(Clone, Debug, PartialEq)] pub struct AdminChallenge;