From 8b7a27043f03475ad7289652e0bacf0076922401 Mon Sep 17 00:00:00 2001 From: tropicbliss Date: Wed, 1 Sep 2021 23:34:14 +0800 Subject: [PATCH] New sniper that supports multi accounts --- Cargo.toml | 7 +- src/config.rs | 80 +++++------------- src/main.rs | 217 +++++++++++++++++++++++++----------------------- src/requests.rs | 34 +++----- src/sockets.rs | 42 +++++----- 5 files changed, 171 insertions(+), 209 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 78c7734..089c56e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "buckshot" -version = "3.0.3" +version = "4.0.0" authors = ["tropicbliss "] edition = "2018" license = "MIT" @@ -11,7 +11,6 @@ anyhow = "1.0" chrono = "0.4" console = "0.14" dialoguer = "0.8" -indicatif = "0.16" native-tls = "0.2" reqwest = { version = "0.11", features = ["blocking", "json", "multipart"] } serde = { version = "1.0", features = ["derive"] } @@ -20,3 +19,7 @@ structopt = "0.3" tokio = { version = "1.10", features = ["fs", "rt-multi-thread", "macros"] } tokio-native-tls = "0.3" toml = "0.5" + +[profile.release] +lto = true +codegen-units = 1 diff --git a/src/config.rs b/src/config.rs index c2302da..8d824b8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,75 +1,37 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use serde::Deserialize; -use std::fs::{read_to_string, write}; -use std::io::ErrorKind::NotFound; -use std::path::Path; +use std::fs::read_to_string; +use std::path::{Path, PathBuf}; #[derive(Deserialize)] pub struct Config { - pub account: Account, - pub config: Others, -} - -#[derive(Deserialize)] -pub struct Account { - pub email: String, - pub password: String, - pub sq_ans: [String; 3], -} - -#[derive(Deserialize)] -pub struct Others { + pub accounts: Vec, pub offset: i64, pub auto_offset: bool, pub spread: usize, pub microsoft_auth: bool, pub gc_snipe: bool, - pub change_skin: bool, + pub skin: Option, + pub name_queue: Option>, +} + +#[derive(Deserialize)] +pub struct Skin { pub skin_model: String, - pub skin_path: String, - pub name_queue: Vec, + pub skin_path: PathBuf, +} + +#[derive(Deserialize)] +pub struct Account { + pub email: String, + pub password: String, + pub sq_ans: Option<[String; 3]>, } impl Config { pub fn new(config_path: &Path) -> Result { - match read_to_string(&config_path) { - Ok(s) => { - let config: Self = toml::from_str(&s)?; - Ok(config) - } - Err(e) if e.kind() == NotFound => { - write(&config_path, get_default_config().as_bytes())?; - bail!( - "{} not found, creating a new config file", - config_path.display() - ); - } - Err(e) => bail!(e), - } + let s = read_to_string(&config_path)?; + let config: Self = toml::from_str(&s)?; + Ok(config) } } - -fn get_default_config() -> String { - r#"[account] -email = "test@example.com" -password = "123456789" -# Leave the strings in this array empty if your Minecraft account does not have security questions -sq_ans = ["Foo", "Bar", "Baz"] - -[config] -offset = 0 -auto_offset = false -# Spread (delay in milliseconds between each snipe request, not to be confused with offset which is the number of millseconds in which the sniper sends its first request before the name drops) -spread = 0 -microsoft_auth = false -gc_snipe = false -change_skin = false -skin_model = "slim" -skin_path = "example.png" -# Name queueing (allows you to queue up multiple names for sniping) -# Note: This is an optional feature, leave this array empty if you prefer to enter your name manually via an input prompt) -name_queue = [] - -"# - .to_string() -} diff --git a/src/main.rs b/src/main.rs index a2460d2..b22ff70 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,9 @@ mod config; mod requests; mod sockets; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use chrono::{Duration, Utc}; use console::{style, Emoji}; -use indicatif::{ProgressBar, ProgressStyle}; use std::{ io::{stdout, Write}, path::PathBuf, @@ -48,28 +47,34 @@ async fn main() -> Result<()> { static HOURGLASS: Emoji<'_, '_> = Emoji("\u{231b} ", ""); static SPARKLE: Emoji<'_, '_> = Emoji("\u{2728} ", ":-) "); let args = Args::new(); - let config = - config::Config::new(&args.config_path).with_context(|| "Failed to get config options")?; - let task = if !config.config.microsoft_auth { - if config.config.gc_snipe { + let config = config::Config::new(&args.config_path) + .with_context(|| format!("Failed to parse {}", args.config_path.display()))?; + let task = if !config.microsoft_auth { + if config.gc_snipe { writeln!(stdout(), "{}", style("`microsoft_auth` is set to false yet `gc_snipe` is set to true, defaulting to GC sniping instead").red())?; SnipeTask::Giftcode } else { SnipeTask::Mojang } - } else if config.config.gc_snipe { + } else if config.gc_snipe { SnipeTask::Giftcode } else { SnipeTask::Microsoft }; - let requestor = requests::Requests::new(&config.account.email, &config.account.password)?; + if task != SnipeTask::Giftcode && config.accounts.len() != 1 { + bail!( + "`accounts` field is of invalid length as sniper is set to GC sniping in {}", + args.config_path.display() + ); + } let name_list = if let Some(username_to_snipe) = args.username_to_snipe { vec![username_to_snipe] - } else if config.config.name_queue.is_empty() { - vec![cli::get_username_choice().with_context(|| "Failed to get username choice")?] + } else if let Some(x) = config.name_queue { + x } else { - config.config.name_queue + vec![cli::get_username_choice().with_context(|| "Failed to get username choice")?] }; + let requestor = requests::Requests::new()?; for (count, username) in name_list.into_iter().enumerate() { let name = username.trim().to_string(); if !cli::username_filter_predicate(&name) { @@ -86,62 +91,27 @@ async fn main() -> Result<()> { sleep(std::time::Duration::from_secs(20)); } writeln!(stdout(), "{}Initialising...", HOURGLASS)?; - let progress_bar = ProgressBar::new(100); - let progress_bar_style = ProgressStyle::default_bar() - .progress_chars("= ") - .template("{bar:40} {percent}%"); - progress_bar.set_style(progress_bar_style); let droptime = if let Some(x) = requestor .check_name_availability_time(&name) .with_context(|| "Failed to get droptime")? { - progress_bar.inc(25); x } else { - progress_bar.abandon(); continue; }; - let mut bearer_token = authenticate(&config.account.sq_ans, &requestor, &task)?; - progress_bar.inc(25); - if task == SnipeTask::Giftcode && count == 0 { - if let Some(gc) = &args.giftcode { - requestor.redeem_giftcode(&bearer_token, gc)?; - writeln!( - stdout(), - "{}", - style("Successfully redeemed giftcode").green() - )?; - } else { - writeln!( - stdout(), - "{}", - style("Reminder: You should redeem your giftcode before GC sniping").red() - )?; - } - } else { - requestor - .check_name_change_eligibility(&bearer_token) - .with_context(|| "Failed to check name change eligibility")?; - } - progress_bar.inc(25); + let mut bearer_tokens = Vec::new(); let is_gc = task == SnipeTask::Giftcode; let executor = sockets::Executor::new(&name, is_gc); - let offset = if config.config.auto_offset { + let offset = if config.auto_offset { + writeln!(stdout(), "{}Calculating offset...", HOURGLASS)?; executor .auto_offset_calculator() .await .with_context(|| "Failed to calculate offset")? } else { - config.config.offset + config.offset }; - progress_bar.inc(25); - progress_bar.finish_with_message("done"); - writeln!( - stdout(), - "{}Initialisation complete. Your offset is: {} ms", - SPARKLE, - offset - )?; + writeln!(stdout(), "{}Your offset is: {} ms", SPARKLE, offset)?; let formatted_droptime = droptime.format("%F %T"); let duration_in_sec = droptime - Utc::now(); if duration_in_sec < Duration::minutes(1) { @@ -162,7 +132,7 @@ async fn main() -> Result<()> { )?; } let snipe_time = droptime - Duration::milliseconds(offset); - let setup_time = snipe_time - Duration::hours(12); + let setup_time = snipe_time - Duration::hours(23); if Utc::now() < setup_time { let sleep_duration = match (setup_time - Utc::now()).to_std() { Ok(x) => x, @@ -176,66 +146,103 @@ async fn main() -> Result<()> { { continue; } - bearer_token = authenticate(&config.account.sq_ans, &requestor, &task)?; - if task != SnipeTask::Giftcode { - requestor - .check_name_change_eligibility(&bearer_token) - .with_context(|| "Failed to check name change eligibility")?; + bearer_tokens = Vec::new(); + for account in &config.accounts { + if account.email.is_empty() || account.password.is_empty() { + if config.accounts.len() != 1 { + writeln!( + stdout(), + "No email or password provided, moving on to next account..." + )?; + continue; + } + bail!("No email or password provided"); + } + let bearer_token = if task == SnipeTask::Mojang { + let bearer_token = requestor + .authenticate_mojang(&account.email, &account.password) + .with_context(|| "Failed to authenticate Mojang account")?; + if let Some(questions) = requestor + .get_questions(&bearer_token) + .with_context(|| "Failed to get SQ IDs.")? + { + match &account.sq_ans { + Some(x) => { + requestor + .send_answers(&bearer_token, questions, x) + .with_context(|| "Failed to send SQ answers")?; + } + None => { + if config.accounts.len() != 1 { + writeln!( + stdout(), + "SQ answers required, moving on to next account..." + )?; + continue; + } + bail!("SQ answers required"); + } + } + } + bearer_token + } else { + requestor + .authenticate_microsoft(&account.email, &account.password) + .with_context(|| "Failed to authenticate Microsoft account")? + }; + if task == SnipeTask::Giftcode && count == 0 { + if let Some(gc) = &args.giftcode { + requestor.redeem_giftcode(&bearer_token, gc)?; + writeln!( + stdout(), + "{}", + style("Successfully redeemed giftcode").green() + )?; + } else { + writeln!( + stdout(), + "{}", + style("Reminder: You should redeem your giftcode before GC sniping") + .red() + )?; + } + } else { + requestor + .check_name_change_eligibility(&bearer_token) + .with_context(|| "Failed to check name change eligibility")?; + } + bearer_tokens.push(bearer_token); + if config.accounts.len() != 1 { + writeln!(stdout(), "Waiting 20 seconds to prevent rate limiting...")?; + sleep(std::time::Duration::from_secs(20)); + } } } writeln!(stdout(), "{}", style("Successfully signed in").green())?; writeln!(stdout(), "Setup complete")?; - let is_success = executor - .snipe_executor(&bearer_token, config.config.spread, snipe_time) + match executor + .snipe_executor(bearer_tokens, config.spread, snipe_time) .await - .with_context(|| "Failed to execute snipe")?; - if is_success { - writeln!( - stdout(), - "{}", - style(format!("Successfully sniped {}!", name)).green() - )?; - if config.config.change_skin { - requestor - .upload_skin( - &bearer_token, - &config.config.skin_path, - config.config.skin_model.clone(), - ) - .with_context(|| "Failed to upload skin")?; - writeln!(stdout(), "{}", style("Successfully changed skin").green())?; + .with_context(|| "Failed to execute snipe")? + { + Some(bearer) => { + writeln!( + stdout(), + "{}", + style(format!("Successfully sniped {}!", name)).green() + )?; + if let Some(skin) = config.skin { + requestor + .upload_skin(&bearer, skin.skin_path, skin.skin_model) + .with_context(|| "Failed to upload skin")?; + writeln!(stdout(), "{}", style("Successfully changed skin").green())?; + } + break; + } + None => { + writeln!(stdout(), "Failed to snipe {}", name)?; } - } else { - writeln!(stdout(), "Failed to snipe {}", name)?; - } - if is_success { - break; } } Ok(()) } - -fn authenticate( - answers: &[String; 3], - requestor: &requests::Requests, - task: &SnipeTask, -) -> Result { - if task == &SnipeTask::Mojang { - let bearer_token = requestor - .authenticate_mojang() - .with_context(|| "Failed to authenticate Mojang account")?; - if let Some(questions) = requestor - .get_questions(&bearer_token) - .with_context(|| "Failed to get SQ IDs.")? - { - requestor - .send_answers(&bearer_token, questions, answers) - .with_context(|| "Failed to send SQ answers")?; - } - Ok(bearer_token) - } else { - requestor - .authenticate_microsoft() - .with_context(|| "Failed to authenticate Microsoft account") - } -} diff --git a/src/requests.rs b/src/requests.rs index cf68b54..d700534 100644 --- a/src/requests.rs +++ b/src/requests.rs @@ -8,34 +8,28 @@ use reqwest::{ use serde_json::{json, Value}; use std::{ io::{stdout, Write}, + path::PathBuf, time::Duration, }; -pub struct Requests<'a> { +pub struct Requests { client: Client, - email: &'a str, - password: &'a str, } -impl<'a> Requests<'a> { - pub fn new(email: &'a str, password: &'a str) -> Result { - if email.is_empty() || password.is_empty() { - bail!("No email or password provided"); - } +impl Requests { + pub fn new() -> Result { Ok(Self { client: Client::builder() .timeout(Duration::from_secs(5)) .user_agent("Sniper") .build()?, - email, - password, }) } - pub fn authenticate_mojang(&self) -> Result { + pub fn authenticate_mojang(&self, email: &str, password: &str) -> Result { let post_json = json!({ - "username": self.email, - "password": self.password + "username": email, + "password": password }); let res = self .client @@ -61,10 +55,10 @@ impl<'a> Requests<'a> { } } - pub fn authenticate_microsoft(&self) -> Result { + pub fn authenticate_microsoft(&self, email: &str, password: &str) -> Result { let post_json = json!({ - "email": self.email, - "password": self.password + "email": email, + "password": password }); let res = self .client @@ -130,9 +124,6 @@ impl<'a> Requests<'a> { questions: [i64; 3], answers: &[String; 3], ) -> Result<()> { - if answers[0].is_empty() || answers[1].is_empty() || answers[2].is_empty() { - bail!("One or more SQ answers not provided"); - } let post_body = json!([ { "id": questions[0], @@ -224,15 +215,12 @@ impl<'a> Requests<'a> { pub fn upload_skin( &self, bearer_token: &str, - skin_path: &str, + skin_path: PathBuf, skin_model: String, ) -> Result<()> { if !(skin_model.to_lowercase() == "slim" || skin_model.to_lowercase() == "classic") { bail!("Invalid skin model"); } - if skin_path.is_empty() { - bail!("No skin path provided") - } let form = Form::new() .text("variant", skin_model) .file("file", skin_path)?; diff --git a/src/sockets.rs b/src/sockets.rs index a922396..132dccc 100644 --- a/src/sockets.rs +++ b/src/sockets.rs @@ -50,20 +50,12 @@ impl<'a> Executor<'a> { pub async fn snipe_executor( &self, - bearer_token: &str, + bearer_token: Vec, spread_offset: usize, snipe_time: DateTime, - ) -> Result { - let mut is_success = false; + ) -> Result> { let req_count = if self.is_gc { 6 } else { 3 }; let mut spread = 0; - let payload = if self.is_gc { - let post_body = json!({ "profileName": self.name }).to_string(); - format!("POST /minecraft/profile HTTP/1.1\r\nHost: api.minecraftservices.com\r\nConnection: close\r\nAuthorization: Bearer {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}", bearer_token, post_body.len(), post_body).into_bytes() - } else { - format!("PUT /minecraft/profile/name/{} HTTP/1.1\r\nHost: api.minecraftservices.com\r\nConnection: close\r\nAuthorization: Bearer {}\r\n", self.name, bearer_token).into_bytes() - }; - let payload = Arc::new(payload); let addr = "api.minecraftservices.com:443" .to_socket_addrs()? .next() @@ -71,8 +63,18 @@ impl<'a> Executor<'a> { let cx = TlsConnector::builder().build()?; let cx = tokio_native_tls::TlsConnector::from(cx); let cx = Arc::new(cx); - let handle_vec: Vec>> = (0..req_count) - .map(|_| { + let mut handle_vec: Vec>> = + Vec::with_capacity(req_count * bearer_token.len()); + for bearer in bearer_token { + let payload = if self.is_gc { + let post_body = json!({ "profileName": self.name }).to_string(); + format!("POST /minecraft/profile HTTP/1.1\r\nHost: api.minecraftservices.com\r\nConnection: close\r\nAuthorization: Bearer {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}", bearer, post_body.len(), post_body).into_bytes() + } else { + format!("PUT /minecraft/profile/name/{} HTTP/1.1\r\nHost: api.minecraftservices.com\r\nConnection: close\r\nAuthorization: Bearer {}\r\n", self.name, bearer).into_bytes() + }; + let payload = Arc::new(payload); + for _ in 0..req_count { + let bearer = bearer.clone(); let cx = Arc::clone(&cx); let payload = Arc::clone(&payload); let handle = tokio::task::spawn(async move { @@ -106,7 +108,7 @@ impl<'a> Executor<'a> { style("200").green(), style(format!("{}", formatted_res_time)).cyan() )?; - Ok(true) + Ok(Some(bearer)) } status => { writeln!( @@ -116,20 +118,20 @@ impl<'a> Executor<'a> { style(format!("{}", status)).red(), style(format!("{}", formatted_res_time)).cyan() )?; - Ok(false) + Ok(None) } } }); spread += spread_offset as i64; - handle - }) - .collect(); + handle_vec.push(handle); + } + } for handle in handle_vec { let status = handle.await??; - if status { - is_success = true; + if status.is_some() { + return Ok(status); } } - Ok(is_success) + Ok(None) } }