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::avalonminer::AvalonMiner;
26use crate::miners::backends::btminer::BTMiner;
27use crate::miners::backends::epic::PowerPlay;
28use crate::miners::backends::espminer::ESPMiner;
29use crate::miners::backends::traits::GetMinerData;
30use crate::miners::backends::vnish::Vnish;
31use crate::miners::factory::traits::VersionSelection;
32use std::net::SocketAddr;
33use traits::{DiscoveryCommands, ModelSelection};
34
35const IDENTIFICATION_TIMEOUT: Duration = Duration::from_secs(10);
36const CONNECTIVITY_TIMEOUT: Duration = Duration::from_secs(1);
37const CONNECTIVITY_RETRIES: u32 = 3;
38
39fn calculate_optimal_concurrency(ip_count: usize) -> usize {
40 match ip_count {
42 0..=100 => 100, 101..=1000 => 250, 1001..=5000 => 500, 5001..=10000 => 750, _ => 1000, }
48}
49
50async fn check_port_open(ip: IpAddr, port: u16, connectivity_timeout: Duration) -> bool {
52 let addr: SocketAddr = (ip, port).into();
53
54 let stream = match timeout(connectivity_timeout, TcpStream::connect(addr)).await {
55 Ok(Ok(stream)) => stream,
56 _ => return false,
57 };
58
59 let _ = stream.set_nodelay(true);
61
62 let _ = stream.set_linger(Some(Duration::from_secs(0)));
64
65 true
66}
67
68async fn get_miner_type_from_command(
69 ip: IpAddr,
70 command: MinerCommand,
71) -> Option<(Option<MinerMake>, Option<MinerFirmware>)> {
72 match command {
73 MinerCommand::RPC {
74 command,
75 parameters: _,
76 } => {
77 let response = send_rpc_command(&ip, command).await?;
78 parse_type_from_socket(response)
79 }
80 MinerCommand::WebAPI {
81 command,
82 parameters: _,
83 } => {
84 let response = send_web_command(&ip, command).await?;
85 parse_type_from_web(response)
86 }
87 _ => None,
88 }
89}
90
91fn parse_type_from_socket(
92 response: serde_json::Value,
93) -> Option<(Option<MinerMake>, Option<MinerFirmware>)> {
94 let json_string = response.to_string().to_uppercase();
95
96 match () {
97 _ if json_string.contains("BOSMINER") || json_string.contains("BOSER") => {
98 Some((None, Some(MinerFirmware::BraiinsOS)))
99 }
100 _ if json_string.contains("LUXMINER") => Some((None, Some(MinerFirmware::LuxOS))),
101 _ if json_string.contains("BITMICRO") || json_string.contains("BTMINER") => {
102 Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
103 }
104 _ if json_string.contains("ANTMINER") && !json_string.contains("DEVDETAILS") => {
105 Some((Some(MinerMake::AntMiner), Some(MinerFirmware::Stock)))
106 }
107 _ if json_string.contains("AVALON") => {
108 Some((Some(MinerMake::AvalonMiner), Some(MinerFirmware::Stock)))
109 }
110 _ if json_string.contains("VNISH") => {
111 Some((Some(MinerMake::AntMiner), Some(MinerFirmware::VNish)))
112 }
113 _ => None,
114 }
115}
116
117fn parse_type_from_web(
118 response: (String, HeaderMap, StatusCode),
119) -> Option<(Option<MinerMake>, Option<MinerFirmware>)> {
120 let (resp_text, resp_headers, resp_status) = response;
121 let auth_header = match resp_headers.get("www-authenticate") {
122 Some(header) => header.to_str().unwrap(),
123 None => "",
124 };
125 let redirect_header = match resp_headers.get("location") {
126 Some(header) => header.to_str().unwrap(),
127 None => "",
128 };
129
130 match () {
131 _ if resp_status == 401 && auth_header.contains("realm=\"antMiner") => {
132 Some((Some(MinerMake::AntMiner), Some(MinerFirmware::Stock)))
133 }
134 _ if resp_text.contains("Braiins OS") => Some((None, Some(MinerFirmware::BraiinsOS))),
135 _ if resp_text.contains("Luxor Firmware") => Some((None, Some(MinerFirmware::LuxOS))),
136 _ if resp_text.contains("AxeOS") => {
137 Some((Some(MinerMake::BitAxe), Some(MinerFirmware::Stock)))
138 }
139 _ if resp_text.contains("Miner Web Dashboard") => Some((None, Some(MinerFirmware::EPic))),
140 _ if resp_text.contains("Avalon") => {
141 Some((Some(MinerMake::AvalonMiner), Some(MinerFirmware::Stock)))
142 }
143 _ if resp_text.contains("AnthillOS") => {
144 Some((Some(MinerMake::AntMiner), Some(MinerFirmware::VNish)))
145 }
146 _ if redirect_header.contains("https://") && resp_status == 307
147 || resp_text.contains("/cgi-bin/luci") =>
148 {
149 Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
150 }
151 _ => None,
152 }
153}
154
155fn select_backend(
156 ip: IpAddr,
157 make: Option<MinerMake>,
158 model: Option<MinerModel>,
159 firmware: Option<MinerFirmware>,
160 version: Option<semver::Version>,
161) -> Option<Box<dyn GetMinerData>> {
162 match (make, firmware) {
163 (Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)) => {
164 Some(BTMiner::new(ip, model?, firmware?, version?))
165 }
166 (Some(MinerMake::BitAxe), Some(MinerFirmware::Stock)) => {
167 Some(ESPMiner::new(ip, model?, firmware?, version?))
168 }
169 (Some(MinerMake::AvalonMiner), Some(MinerFirmware::Stock)) => {
170 Some(AvalonMiner::new(ip, model?, firmware?))
171 }
172 (Some(_), Some(MinerFirmware::VNish)) => Some(Box::new(Vnish::new(ip, make?, model?))),
173 (Some(_), Some(MinerFirmware::EPic)) => Some(Box::new(PowerPlay::new(ip, make?, model?))),
174 _ => None,
175 }
176}
177
178pub struct MinerFactory {
179 search_makes: Option<Vec<MinerMake>>,
180 search_firmwares: Option<Vec<MinerFirmware>>,
181 ips: Vec<IpAddr>,
182 identification_timeout: Duration,
183 connectivity_timeout: Duration,
184 connectivity_retries: u32,
185 concurrent: Option<usize>,
186 check_port: bool,
187}
188
189impl Default for MinerFactory {
190 fn default() -> Self {
191 Self::new()
192 }
193}
194
195impl MinerFactory {
196 pub async fn scan_miner(&self, ip: IpAddr) -> Result<Option<Box<dyn GetMinerData>>> {
197 if (1..self.connectivity_retries).next().is_some() {
199 if self.check_port && !check_port_open(ip, 80, self.connectivity_timeout).await {
200 return Ok(None);
201 } else {
202 return self.get_miner(ip).await;
203 }
204 }
205 Ok(None)
206 }
207
208 pub async fn get_miner(&self, ip: IpAddr) -> Result<Option<Box<dyn GetMinerData>>> {
209 let search_makes = self.search_makes.clone().unwrap_or(vec![
210 MinerMake::AntMiner,
211 MinerMake::WhatsMiner,
212 MinerMake::AvalonMiner,
213 MinerMake::EPic,
214 MinerMake::Braiins,
215 MinerMake::BitAxe,
216 ]);
217 let search_firmwares = self.search_firmwares.clone().unwrap_or(vec![
218 MinerFirmware::Stock,
219 MinerFirmware::BraiinsOS,
220 MinerFirmware::VNish,
221 MinerFirmware::EPic,
222 MinerFirmware::HiveOS,
223 MinerFirmware::LuxOS,
224 MinerFirmware::Marathon,
225 MinerFirmware::MSKMiner,
226 ]);
227 let mut commands: HashSet<MinerCommand> = HashSet::new();
228
229 for make in search_makes {
230 for command in make.get_discovery_commands() {
231 commands.insert(command);
232 }
233 }
234 for firmware in search_firmwares {
235 for command in firmware.get_discovery_commands() {
236 commands.insert(command);
237 }
238 }
239
240 let mut discovery_tasks = JoinSet::new();
241 for command in commands {
242 let _ = discovery_tasks.spawn(get_miner_type_from_command(ip, command));
243 }
244
245 let timeout = tokio::time::sleep(self.identification_timeout).fuse();
246 let tasks = tokio::spawn(async move {
247 loop {
248 if discovery_tasks.is_empty() {
249 return None;
250 };
251 match discovery_tasks.join_next().await.unwrap_or(Ok(None)) {
252 Ok(Some(result)) => {
253 return Some(result);
254 }
255 _ => continue,
256 };
257 }
258 });
259
260 pin_mut!(timeout, tasks);
261
262 let miner_info = tokio::select!(
263 Ok(miner_info) = &mut tasks => {
264 miner_info
265 },
266 _ = &mut timeout => {
267 None
268 }
269 );
270
271 match miner_info {
272 Some((Some(make), Some(MinerFirmware::Stock))) => {
273 let model = make.get_model(ip).await;
274 let version = make.get_version(ip).await;
275
276 Ok(select_backend(
277 ip,
278 Some(make),
279 model,
280 Some(MinerFirmware::Stock),
281 version,
282 ))
283 }
284 Some((make, Some(firmware))) => {
285 let model = firmware.get_model(ip).await;
286 let version = firmware.get_version(ip).await;
287 if let Some(model) = model {
288 let make = match model {
289 MinerModel::AntMiner(_) => MinerMake::AntMiner,
290 MinerModel::WhatsMiner(_) => MinerMake::WhatsMiner,
291 MinerModel::Braiins(_) => MinerMake::Braiins,
292 MinerModel::Bitaxe(_) => MinerMake::BitAxe,
293 MinerModel::EPic(_) => MinerMake::EPic,
294 MinerModel::Avalon(_) => MinerMake::AvalonMiner,
295 };
296 return Ok(select_backend(
297 ip,
298 Some(make),
299 Some(model),
300 Some(firmware),
301 version,
302 ));
303 }
304
305 Ok(select_backend(ip, make, model, Some(firmware), version))
306 }
307 Some((Some(make), firmware)) => {
308 let model = make.get_model(ip).await;
309 let version = make.get_version(ip).await;
310
311 Ok(select_backend(ip, Some(make), model, firmware, version))
312 }
313 _ => Ok(None),
314 }
315 }
316
317 pub fn new() -> MinerFactory {
318 MinerFactory {
319 search_makes: None,
320 search_firmwares: None,
321 ips: Vec::new(),
322 identification_timeout: IDENTIFICATION_TIMEOUT,
323 connectivity_timeout: CONNECTIVITY_TIMEOUT,
324 connectivity_retries: CONNECTIVITY_RETRIES,
325 concurrent: None,
326 check_port: true, }
328 }
329
330 pub fn with_port_check(mut self, enabled: bool) -> Self {
332 self.check_port = enabled;
333 self
334 }
335
336 pub fn with_concurrent_limit(mut self, limit: usize) -> Self {
338 self.concurrent = Some(limit);
339 self
340 }
341
342 pub fn with_adaptive_concurrency(mut self) -> Self {
343 self.concurrent = Some(calculate_optimal_concurrency(self.ips.len()));
344 self
345 }
346
347 fn update_adaptive_concurrency(&mut self) {
348 if self.concurrent.is_none() {
349 self.concurrent = Some(calculate_optimal_concurrency(self.ips.len()));
350 }
351 }
352
353 pub fn with_identification_timeout(mut self, timeout: Duration) -> Self {
355 self.identification_timeout = timeout;
356 self
357 }
358
359 pub fn with_identification_timeout_secs(mut self, timeout_secs: u64) -> Self {
360 self.identification_timeout = Duration::from_secs(timeout_secs);
361 self
362 }
363
364 pub fn with_connectivity_timeout(mut self, timeout: Duration) -> Self {
365 self.connectivity_timeout = timeout;
366 self
367 }
368
369 pub fn with_connectivity_timeout_secs(mut self, timeout_secs: u64) -> Self {
370 self.connectivity_timeout = Duration::from_secs(timeout_secs);
371 self
372 }
373
374 pub fn with_connectivity_retries(mut self, retries: u32) -> Self {
375 self.connectivity_retries = retries;
376 self
377 }
378
379 pub fn with_search_makes(mut self, search_makes: Vec<MinerMake>) -> Self {
381 self.search_makes = Some(search_makes);
382 self
383 }
384
385 pub fn with_makes(mut self, makes: Vec<MinerMake>) -> Self {
386 self.search_makes = Some(makes);
387 self
388 }
389
390 pub fn add_search_make(mut self, search_make: MinerMake) -> Self {
391 if self.search_makes.is_none() {
392 self.search_makes = Some(vec![search_make]);
393 } else {
394 self.search_makes.as_mut().unwrap().push(search_make);
395 }
396 self
397 }
398
399 pub fn remove_search_make(mut self, search_make: MinerMake) -> Self {
400 if let Some(makes) = self.search_makes.as_mut() {
401 makes.retain(|val| *val != search_make);
402 }
403 self
404 }
405
406 pub fn with_search_firmwares(mut self, search_firmwares: Vec<MinerFirmware>) -> Self {
408 self.search_firmwares = Some(search_firmwares);
409 self
410 }
411
412 pub fn with_firmwares(mut self, firmwares: Vec<MinerFirmware>) -> Self {
413 self.search_firmwares = Some(firmwares);
414 self
415 }
416
417 pub fn add_search_firmware(mut self, search_firmware: MinerFirmware) -> Self {
418 if self.search_firmwares.is_none() {
419 self.search_firmwares = Some(vec![search_firmware]);
420 } else {
421 self.search_firmwares
422 .as_mut()
423 .unwrap()
424 .push(search_firmware);
425 }
426 self
427 }
428
429 pub fn remove_search_firmware(mut self, search_firmware: MinerFirmware) -> Self {
430 if let Some(firmwares) = self.search_firmwares.as_mut() {
431 firmwares.retain(|val| *val != search_firmware);
432 }
433 self
434 }
435
436 pub fn with_subnet(mut self, subnet: &str) -> Result<Self> {
439 let ips = self.hosts_from_subnet(subnet)?;
440 self.ips = ips;
441 self.shuffle_ips();
442 Ok(self)
443 }
444 fn hosts_from_subnet(&self, subnet: &str) -> Result<Vec<IpAddr>> {
445 let network = IpNet::from_str(subnet)?;
446 Ok(network.hosts().collect())
447 }
448
449 fn shuffle_ips(&mut self) {
451 let mut rng = rand::rng();
452 self.ips.shuffle(&mut rng);
453 }
454
455 pub fn with_octets(
458 mut self,
459 octet1: &str,
460 octet2: &str,
461 octet3: &str,
462 octet4: &str,
463 ) -> Result<Self> {
464 let ips = self.hosts_from_octets(octet1, octet2, octet3, octet4)?;
465 self.ips = ips;
466 self.shuffle_ips();
467 self.update_adaptive_concurrency();
468 Ok(self)
469 }
470 fn hosts_from_octets(
471 &self,
472 octet1: &str,
473 octet2: &str,
474 octet3: &str,
475 octet4: &str,
476 ) -> Result<Vec<IpAddr>> {
477 let octet1_range = parse_octet_range(octet1)?;
478 let octet2_range = parse_octet_range(octet2)?;
479 let octet3_range = parse_octet_range(octet3)?;
480 let octet4_range = parse_octet_range(octet4)?;
481
482 Ok(generate_ips_from_ranges(
483 &octet1_range,
484 &octet2_range,
485 &octet3_range,
486 &octet4_range,
487 ))
488 }
489
490 pub fn with_range(self, range_str: &str) -> Result<Self> {
493 let parts: Vec<&str> = range_str.split('.').collect();
494 if parts.len() != 4 {
495 return Err(anyhow::anyhow!(
496 "Invalid IP range format. Expected format: 10.1-199.0.1-199"
497 ));
498 }
499
500 self.with_octets(parts[0], parts[1], parts[2], parts[3])
501 }
502
503 pub fn hosts(&self) -> Vec<IpAddr> {
505 self.ips.clone()
506 }
507
508 pub fn len(&self) -> usize {
510 self.ips.len()
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}