1mod commands;
2mod hardware;
3mod model;
4mod traits;
5
6use anyhow::Result;
7use futures::future::FutureExt;
8use futures::{Stream, StreamExt, pin_mut, stream};
9use ipnet::IpNet;
10use rand::seq::SliceRandom;
11use reqwest::StatusCode;
12use reqwest::header::HeaderMap;
13use std::collections::HashSet;
14use std::net::IpAddr;
15use std::net::Ipv4Addr;
16use std::str::FromStr;
17use std::time::Duration;
18use tokio::net::TcpStream;
19use tokio::task::JoinSet;
20use tokio::time::timeout;
21
22use super::commands::MinerCommand;
23use super::util::{send_rpc_command, send_web_command};
24use crate::data::device::{MinerFirmware, MinerMake, MinerModel};
25use crate::miners::backends::antminer::AntMiner;
26use crate::miners::backends::avalonminer::AvalonMiner;
27use crate::miners::backends::bitaxe::BitAxe;
28use crate::miners::backends::epic::PowerPlay;
29use crate::miners::backends::marathon::Marathon;
30use crate::miners::backends::traits::{GetMinerData, MinerConstructor};
31use crate::miners::backends::vnish::Vnish;
32use crate::miners::backends::whatsminer::WhatsMiner;
33use crate::miners::factory::traits::VersionSelection;
34use std::net::SocketAddr;
35use traits::{DiscoveryCommands, ModelSelection};
36
37const IDENTIFICATION_TIMEOUT: Duration = Duration::from_secs(10);
38const CONNECTIVITY_TIMEOUT: Duration = Duration::from_secs(1);
39const CONNECTIVITY_RETRIES: u32 = 3;
40
41fn calculate_optimal_concurrency(ip_count: usize) -> usize {
42 match ip_count {
44 0..=100 => 100, 101..=1000 => 250, 1001..=5000 => 500, 5001..=10000 => 750, _ => 1000, }
50}
51
52async fn check_port_open(ip: IpAddr, port: u16, connectivity_timeout: Duration) -> bool {
54 let addr: SocketAddr = (ip, port).into();
55
56 let stream = match timeout(connectivity_timeout, TcpStream::connect(addr)).await {
57 Ok(Ok(stream)) => stream,
58 _ => return false,
59 };
60
61 let _ = stream.set_nodelay(true);
63
64 let _ = stream.set_linger(Some(Duration::from_secs(0)));
66
67 true
68}
69
70async fn get_miner_type_from_command(
71 ip: IpAddr,
72 command: MinerCommand,
73) -> Option<(Option<MinerMake>, Option<MinerFirmware>)> {
74 match command {
75 MinerCommand::RPC {
76 command,
77 parameters: _,
78 } => {
79 let response = send_rpc_command(&ip, command).await?;
80 parse_type_from_socket(response)
81 }
82 MinerCommand::WebAPI {
83 command,
84 parameters: _,
85 } => {
86 let response = send_web_command(&ip, command).await?;
87 parse_type_from_web(response)
88 }
89 _ => None,
90 }
91}
92
93fn parse_type_from_socket(
94 response: serde_json::Value,
95) -> Option<(Option<MinerMake>, Option<MinerFirmware>)> {
96 let json_string = response.to_string().to_uppercase();
97 match () {
98 _ if json_string.contains("BOSMINER") || json_string.contains("BOSER") => {
99 Some((None, Some(MinerFirmware::BraiinsOS)))
100 }
101 _ if json_string.contains("LUXMINER") => Some((None, Some(MinerFirmware::LuxOS))),
102 _ if json_string.contains("MARAFW") || json_string.contains("KAONSU") => {
103 Some((None, Some(MinerFirmware::Marathon)))
104 }
105 _ if json_string.contains("VNISH") => Some((None, Some(MinerFirmware::VNish))),
106 _ if json_string.contains("BITMICRO") || json_string.contains("BTMINER") => {
107 Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
108 }
109 _ if json_string.contains("ANTMINER") => {
110 Some((Some(MinerMake::AntMiner), Some(MinerFirmware::Stock)))
111 }
112 _ if json_string.contains("AVALON") => {
113 Some((Some(MinerMake::AvalonMiner), Some(MinerFirmware::Stock)))
114 }
115 _ => None,
116 }
117}
118
119fn parse_type_from_web(
120 response: (String, HeaderMap, StatusCode),
121) -> Option<(Option<MinerMake>, Option<MinerFirmware>)> {
122 let (resp_text, resp_headers, resp_status) = response;
123 let auth_header = match resp_headers.get("www-authenticate") {
124 Some(header) => header.to_str().unwrap(),
125 None => "",
126 };
127 let algo_header = match resp_headers.get("algorithm") {
128 Some(header) => header.to_str().unwrap(),
129 None => "",
130 };
131 let redirect_header = match resp_headers.get("location") {
132 Some(header) => header.to_str().unwrap(),
133 None => "",
134 };
135 match () {
136 _ if resp_status == 401 && algo_header.contains("MD5") => {
137 Some((None, Some(MinerFirmware::Marathon)))
138 }
139 _ if resp_status == 401 && auth_header.contains("realm=\"antMiner") => {
140 Some((Some(MinerMake::AntMiner), Some(MinerFirmware::Stock)))
141 }
142 _ if resp_text.contains("Braiins OS") => Some((None, Some(MinerFirmware::BraiinsOS))),
143 _ if resp_text.contains("Luxor Firmware") => Some((None, Some(MinerFirmware::LuxOS))),
144 _ if resp_text.contains("AxeOS") => {
145 Some((Some(MinerMake::BitAxe), Some(MinerFirmware::Stock)))
146 }
147 _ if resp_text.contains("Miner Web Dashboard") => Some((None, Some(MinerFirmware::EPic))),
148 _ if resp_text.contains("Avalon") => {
149 Some((Some(MinerMake::AvalonMiner), Some(MinerFirmware::Stock)))
150 }
151 _ if resp_text.contains("AnthillOS") => Some((None, Some(MinerFirmware::VNish))),
152 _ if redirect_header.contains("https://") && resp_status == 307
153 || resp_text.contains("/cgi-bin/luci") =>
154 {
155 Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
156 }
157 _ => None,
158 }
159}
160
161fn select_backend(
162 ip: IpAddr,
163 model: Option<MinerModel>,
164 firmware: Option<MinerFirmware>,
165 version: Option<semver::Version>,
166) -> Option<Box<dyn GetMinerData>> {
167 match (model, firmware) {
168 (Some(MinerModel::WhatsMiner(_)), Some(MinerFirmware::Stock)) => {
169 Some(WhatsMiner::new(ip, model?, version))
170 }
171 (Some(MinerModel::BitAxe(_)), Some(MinerFirmware::Stock)) => {
172 Some(BitAxe::new(ip, model?, version))
173 }
174 (Some(MinerModel::AvalonMiner(_)), Some(MinerFirmware::Stock)) => {
175 Some(AvalonMiner::new(ip, model?, version))
176 }
177 (Some(MinerModel::AntMiner(_)), Some(MinerFirmware::Stock)) => {
178 Some(AntMiner::new(ip, model?, version))
179 }
180 (Some(_), Some(MinerFirmware::VNish)) => Some(Vnish::new(ip, model?, version)),
181 (Some(_), Some(MinerFirmware::EPic)) => Some(PowerPlay::new(ip, model?, version)),
182 (Some(_), Some(MinerFirmware::Marathon)) => Some(Marathon::new(ip, model?, version)),
183 _ => None,
184 }
185}
186
187pub struct MinerFactory {
188 search_makes: Option<Vec<MinerMake>>,
189 search_firmwares: Option<Vec<MinerFirmware>>,
190 ips: Vec<IpAddr>,
191 identification_timeout: Duration,
192 connectivity_timeout: Duration,
193 connectivity_retries: u32,
194 concurrent: Option<usize>,
195 check_port: bool,
196}
197
198impl Default for MinerFactory {
199 fn default() -> Self {
200 Self::new()
201 }
202}
203
204impl MinerFactory {
205 pub async fn scan_miner(&self, ip: IpAddr) -> Result<Option<Box<dyn GetMinerData>>> {
206 if (1..self.connectivity_retries).next().is_some() {
208 if self.check_port && !check_port_open(ip, 80, self.connectivity_timeout).await {
209 return Ok(None);
210 } else {
211 return self.get_miner(ip).await;
212 }
213 }
214 Ok(None)
215 }
216
217 pub async fn get_miner(&self, ip: IpAddr) -> Result<Option<Box<dyn GetMinerData>>> {
218 let search_makes = self.search_makes.clone().unwrap_or(vec![
219 MinerMake::AntMiner,
220 MinerMake::WhatsMiner,
221 MinerMake::AvalonMiner,
222 MinerMake::EPic,
223 MinerMake::Braiins,
224 MinerMake::BitAxe,
225 ]);
226 let search_firmwares = self.search_firmwares.clone().unwrap_or(vec![
227 MinerFirmware::Stock,
228 MinerFirmware::BraiinsOS,
229 MinerFirmware::VNish,
230 MinerFirmware::EPic,
231 MinerFirmware::HiveOS,
232 MinerFirmware::LuxOS,
233 MinerFirmware::Marathon,
234 MinerFirmware::MSKMiner,
235 ]);
236 let mut commands: HashSet<MinerCommand> = HashSet::new();
237
238 for make in search_makes {
239 for command in make.get_discovery_commands() {
240 commands.insert(command);
241 }
242 }
243 for firmware in search_firmwares {
244 for command in firmware.get_discovery_commands() {
245 commands.insert(command);
246 }
247 }
248
249 let mut discovery_tasks = JoinSet::new();
250 for command in commands {
251 let _ = discovery_tasks.spawn(get_miner_type_from_command(ip, command));
252 }
253
254 let timeout = tokio::time::sleep(self.identification_timeout).fuse();
255 let tasks = tokio::spawn(async move {
256 loop {
257 if discovery_tasks.is_empty() {
258 return None;
259 };
260 match discovery_tasks.join_next().await.unwrap_or(Ok(None)) {
261 Ok(Some(result)) => {
262 return Some(result);
263 }
264 _ => continue,
265 };
266 }
267 });
268
269 pin_mut!(timeout, tasks);
270
271 let miner_info = tokio::select!(
272 Ok(miner_info) = &mut tasks => {
273 miner_info
274 },
275 _ = &mut timeout => {
276 None
277 }
278 );
279
280 match miner_info {
281 Some((Some(make), Some(MinerFirmware::Stock))) => {
282 let model = make.get_model(ip).await;
283 let version = make.get_version(ip).await;
284
285 Ok(select_backend(
286 ip,
287 model,
288 Some(MinerFirmware::Stock),
289 version,
290 ))
291 }
292 Some((_, Some(firmware))) => {
293 let model = firmware.get_model(ip).await;
294 let version = firmware.get_version(ip).await;
295
296 if let Some(model) = model {
297 return Ok(select_backend(ip, Some(model), Some(firmware), version));
298 }
299
300 Ok(select_backend(ip, model, Some(firmware), version))
301 }
302 Some((Some(make), firmware)) => {
303 let model = make.get_model(ip).await;
304 let version = make.get_version(ip).await;
305
306 Ok(select_backend(ip, model, firmware, version))
307 }
308 _ => Ok(None),
309 }
310 }
311
312 pub fn new() -> MinerFactory {
313 MinerFactory {
314 search_makes: None,
315 search_firmwares: None,
316 ips: Vec::new(),
317 identification_timeout: IDENTIFICATION_TIMEOUT,
318 connectivity_timeout: CONNECTIVITY_TIMEOUT,
319 connectivity_retries: CONNECTIVITY_RETRIES,
320 concurrent: None,
321 check_port: true, }
323 }
324
325 pub fn with_port_check(mut self, enabled: bool) -> Self {
327 self.check_port = enabled;
328 self
329 }
330
331 pub fn with_concurrent_limit(mut self, limit: usize) -> Self {
333 self.concurrent = Some(limit);
334 self
335 }
336
337 pub fn with_adaptive_concurrency(mut self) -> Self {
338 self.concurrent = Some(calculate_optimal_concurrency(self.ips.len()));
339 self
340 }
341
342 fn update_adaptive_concurrency(&mut self) {
343 if self.concurrent.is_none() {
344 self.concurrent = Some(calculate_optimal_concurrency(self.ips.len()));
345 }
346 }
347
348 pub fn with_identification_timeout(mut self, timeout: Duration) -> Self {
350 self.identification_timeout = timeout;
351 self
352 }
353
354 pub fn with_identification_timeout_secs(mut self, timeout_secs: u64) -> Self {
355 self.identification_timeout = Duration::from_secs(timeout_secs);
356 self
357 }
358
359 pub fn with_connectivity_timeout(mut self, timeout: Duration) -> Self {
360 self.connectivity_timeout = timeout;
361 self
362 }
363
364 pub fn with_connectivity_timeout_secs(mut self, timeout_secs: u64) -> Self {
365 self.connectivity_timeout = Duration::from_secs(timeout_secs);
366 self
367 }
368
369 pub fn with_connectivity_retries(mut self, retries: u32) -> Self {
370 self.connectivity_retries = retries;
371 self
372 }
373
374 pub fn with_search_makes(mut self, search_makes: Vec<MinerMake>) -> Self {
376 self.search_makes = Some(search_makes);
377 self
378 }
379
380 pub fn with_makes(mut self, makes: Vec<MinerMake>) -> Self {
381 self.search_makes = Some(makes);
382 self
383 }
384
385 pub fn add_search_make(mut self, search_make: MinerMake) -> Self {
386 if self.search_makes.is_none() {
387 self.search_makes = Some(vec![search_make]);
388 } else {
389 self.search_makes.as_mut().unwrap().push(search_make);
390 }
391 self
392 }
393
394 pub fn remove_search_make(mut self, search_make: MinerMake) -> Self {
395 if let Some(makes) = self.search_makes.as_mut() {
396 makes.retain(|val| *val != search_make);
397 }
398 self
399 }
400
401 pub fn with_search_firmwares(mut self, search_firmwares: Vec<MinerFirmware>) -> Self {
403 self.search_firmwares = Some(search_firmwares);
404 self
405 }
406
407 pub fn with_firmwares(mut self, firmwares: Vec<MinerFirmware>) -> Self {
408 self.search_firmwares = Some(firmwares);
409 self
410 }
411
412 pub fn add_search_firmware(mut self, search_firmware: MinerFirmware) -> Self {
413 if self.search_firmwares.is_none() {
414 self.search_firmwares = Some(vec![search_firmware]);
415 } else {
416 self.search_firmwares
417 .as_mut()
418 .unwrap()
419 .push(search_firmware);
420 }
421 self
422 }
423
424 pub fn remove_search_firmware(mut self, search_firmware: MinerFirmware) -> Self {
425 if let Some(firmwares) = self.search_firmwares.as_mut() {
426 firmwares.retain(|val| *val != search_firmware);
427 }
428 self
429 }
430
431 pub fn with_subnet(mut self, subnet: &str) -> Result<Self> {
434 let ips = self.hosts_from_subnet(subnet)?;
435 self.ips = ips;
436 self.shuffle_ips();
437 Ok(self)
438 }
439 fn hosts_from_subnet(&self, subnet: &str) -> Result<Vec<IpAddr>> {
440 let network = IpNet::from_str(subnet)?;
441 Ok(network.hosts().collect())
442 }
443
444 fn shuffle_ips(&mut self) {
446 let mut rng = rand::rng();
447 self.ips.shuffle(&mut rng);
448 }
449
450 pub fn with_octets(
453 mut self,
454 octet1: &str,
455 octet2: &str,
456 octet3: &str,
457 octet4: &str,
458 ) -> Result<Self> {
459 let ips = self.hosts_from_octets(octet1, octet2, octet3, octet4)?;
460 self.ips = ips;
461 self.shuffle_ips();
462 self.update_adaptive_concurrency();
463 Ok(self)
464 }
465 fn hosts_from_octets(
466 &self,
467 octet1: &str,
468 octet2: &str,
469 octet3: &str,
470 octet4: &str,
471 ) -> Result<Vec<IpAddr>> {
472 let octet1_range = parse_octet_range(octet1)?;
473 let octet2_range = parse_octet_range(octet2)?;
474 let octet3_range = parse_octet_range(octet3)?;
475 let octet4_range = parse_octet_range(octet4)?;
476
477 Ok(generate_ips_from_ranges(
478 &octet1_range,
479 &octet2_range,
480 &octet3_range,
481 &octet4_range,
482 ))
483 }
484
485 pub fn with_range(self, range_str: &str) -> Result<Self> {
488 let parts: Vec<&str> = range_str.split('.').collect();
489 if parts.len() != 4 {
490 return Err(anyhow::anyhow!(
491 "Invalid IP range format. Expected format: 10.1-199.0.1-199"
492 ));
493 }
494
495 self.with_octets(parts[0], parts[1], parts[2], parts[3])
496 }
497
498 pub fn hosts(&self) -> Vec<IpAddr> {
500 self.ips.clone()
501 }
502
503 pub fn len(&self) -> usize {
505 self.ips.len()
506 }
507
508 pub fn is_empty(&self) -> bool {
510 self.ips.is_empty()
511 }
512
513 pub async fn scan(&self) -> Result<Vec<Box<dyn GetMinerData>>> {
515 if self.ips.is_empty() {
516 return Err(anyhow::anyhow!(
517 "No IPs to scan. Use with_subnet, with_octets, or with_range to set IPs."
518 ));
519 }
520
521 let concurrency = self
522 .concurrent
523 .unwrap_or(calculate_optimal_concurrency(self.ips.len()));
524
525 let miners: Vec<Box<dyn GetMinerData>> = stream::iter(self.ips.iter().copied())
526 .map(|ip| async move { self.scan_miner(ip).await.ok().flatten() })
527 .buffer_unordered(concurrency)
528 .filter_map(|miner_opt| async move { miner_opt })
529 .collect()
530 .await;
531
532 Ok(miners)
533 }
534
535 pub fn scan_stream(&self) -> Result<impl Stream<Item = Box<dyn GetMinerData>>> {
536 if self.ips.is_empty() {
537 return Err(anyhow::anyhow!(
538 "No IPs to scan. Use with_subnet, with_octets, or with_range to set IPs."
539 ));
540 }
541
542 let concurrency = self
543 .concurrent
544 .unwrap_or(calculate_optimal_concurrency(self.ips.len()));
545
546 let stream = stream::iter(
547 self.ips
548 .iter()
549 .copied()
550 .map(move |ip| async move { self.scan_miner(ip).await.ok().flatten() }),
551 )
552 .buffer_unordered(concurrency)
553 .filter_map(|miner_opt| async move { miner_opt });
554
555 Ok(Box::pin(stream))
556 }
557
558 pub fn scan_stream_with_ip(
559 &self,
560 ) -> Result<impl Stream<Item = (IpAddr, Option<Box<dyn GetMinerData>>)>> {
561 if self.ips.is_empty() {
562 return Err(anyhow::anyhow!(
563 "No IPs to scan. Use with_subnet, with_octets, or with_range to set IPs."
564 ));
565 }
566
567 let concurrency = self
568 .concurrent
569 .unwrap_or(calculate_optimal_concurrency(self.ips.len()));
570
571 let stream = stream::iter(
572 self.ips
573 .iter()
574 .copied()
575 .map(move |ip| async move { (ip, self.scan_miner(ip).await.ok().flatten()) }),
576 )
577 .buffer_unordered(concurrency);
578
579 Ok(Box::pin(stream))
580 }
581
582 pub async fn scan_by_octets(
584 self,
585 octet1: &str,
586 octet2: &str,
587 octet3: &str,
588 octet4: &str,
589 ) -> Result<Vec<Box<dyn GetMinerData>>> {
590 self.with_octets(octet1, octet2, octet3, octet4)?
591 .scan()
592 .await
593 }
594
595 pub async fn scan_by_range(self, range_str: &str) -> Result<Vec<Box<dyn GetMinerData>>> {
597 self.with_range(range_str)?.scan().await
598 }
599}
600
601fn parse_octet_range(range_str: &str) -> Result<Vec<u8>> {
603 if range_str.contains('-') {
604 let parts: Vec<&str> = range_str.split('-').collect();
605 if parts.len() != 2 {
606 return Err(anyhow::anyhow!("Invalid range format: {}", range_str));
607 }
608
609 let start: u8 = parts[0].parse()?;
610 let end: u8 = parts[1].parse()?;
611
612 if start > end {
613 return Err(anyhow::anyhow!(
614 "Invalid range: start > end in {}",
615 range_str
616 ));
617 }
618
619 Ok((start..=end).collect())
620 } else {
621 let value: u8 = range_str.parse()?;
623 Ok(vec![value])
624 }
625}
626
627fn generate_ips_from_ranges(
629 octet1_range: &[u8],
630 octet2_range: &[u8],
631 octet3_range: &[u8],
632 octet4_range: &[u8],
633) -> Vec<IpAddr> {
634 let mut ips = Vec::new();
635
636 for &o1 in octet1_range {
637 for &o2 in octet2_range {
638 for &o3 in octet3_range {
639 for &o4 in octet4_range {
640 ips.push(IpAddr::V4(Ipv4Addr::new(o1, o2, o3, o4)));
641 }
642 }
643 }
644 }
645
646 ips
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652
653 #[test]
654 fn test_parse_type_from_socket_whatsminer_2024_09_30() {
655 const RAW_DATA: &str = r#"{"STATUS": [{"STATUS": "S", "Msg": "Device Details"}], "DEVDETAILS": [{"DEVDETAILS": 0, "Name": "SM", "ID": 0, "Driver": "bitmicro", "Kernel": "", "Model": "M30S+_VE40"}, {"DEVDETAILS": 1, "Name": "SM", "ID": 1, "Driver": "bitmicro", "Kernel": "", "Model": "M30S+_VE40"}, {"DEVDETAILS": 2, "Name": "SM", "ID": 2, "Driver": "bitmicro", "Kernel": "", "Model": "M30S+_VE40"}], "id": 1}"#;
656 let parsed_data = serde_json::from_str(RAW_DATA).unwrap();
657 let result = parse_type_from_socket(parsed_data);
658 assert_eq!(
659 result,
660 Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
661 )
662 }
663
664 #[test]
665 fn test_parse_type_from_web_whatsminer_2024_09_30() {
666 let mut headers = HeaderMap::new();
667 headers.insert("location", "https://example.com/".parse().unwrap());
668
669 let response_data = (String::from(""), headers, StatusCode::TEMPORARY_REDIRECT);
670
671 let result = parse_type_from_web(response_data);
672 assert_eq!(
673 result,
674 Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
675 )
676 }
677
678 #[test]
679 fn test_parse_octet_range() {
680 let result = parse_octet_range("10").unwrap();
682 assert_eq!(result, vec![10]);
683
684 let result = parse_octet_range("1-5").unwrap();
686 assert_eq!(result, vec![1, 2, 3, 4, 5]);
687
688 let result = parse_octet_range("200-255").unwrap();
690 assert_eq!(result, (200..=255).collect::<Vec<u8>>());
691
692 let result = parse_octet_range("200-100");
694 assert!(result.is_err());
695
696 let result = parse_octet_range("1-5-10");
698 assert!(result.is_err());
699
700 let result = parse_octet_range("300");
702 assert!(result.is_err());
703 }
704
705 #[test]
706 fn test_generate_ips_from_ranges() {
707 let octet1 = vec![192];
708 let octet2 = vec![168];
709 let octet3 = vec![1];
710 let octet4 = vec![1, 2];
711
712 let ips = generate_ips_from_ranges(&octet1, &octet2, &octet3, &octet4);
713
714 assert_eq!(ips.len(), 2);
715 assert!(ips.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
716 assert!(ips.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))));
717 }
718}