asic_rs/miners/backends/vnish/
web.rs

1use crate::miners::{
2    api::{APIClient, WebAPIClient},
3    commands::MinerCommand,
4};
5use anyhow::{Result, anyhow};
6use async_trait::async_trait;
7use reqwest::{Client, Method, Response};
8use serde_json::Value;
9use std::{net::IpAddr, sync::RwLock, time::Duration};
10
11/// VNish WebAPI client
12
13#[derive(Debug)]
14pub struct VnishWebAPI {
15    client: Client,
16    pub ip: IpAddr,
17    port: u16,
18    timeout: Duration,
19    bearer_token: RwLock<Option<String>>,
20    password: Option<String>,
21}
22
23#[async_trait]
24impl APIClient for VnishWebAPI {
25    async fn get_api_result(&self, command: &MinerCommand) -> Result<Value> {
26        match command {
27            MinerCommand::WebAPI {
28                command,
29                parameters,
30            } => self
31                .send_command(command, false, parameters.clone(), Method::GET)
32                .await
33                .map_err(|e| anyhow!(e.to_string())),
34            _ => Err(anyhow!("Cannot send non web command to web API")),
35        }
36    }
37}
38
39#[async_trait]
40impl WebAPIClient for VnishWebAPI {
41    /// Send a command to the Vnish miner API
42    async fn send_command(
43        &self,
44        command: &str,
45        _privileged: bool,
46        parameters: Option<Value>,
47        method: Method,
48    ) -> Result<Value> {
49        // Ensure we're authenticated before making the request
50        if let Err(e) = self.ensure_authenticated().await {
51            return Err(anyhow!("Failed to authenticate: {}", e));
52        }
53
54        let url = format!("http://{}:{}/api/v1/{}", self.ip, self.port, command);
55
56        let response = self.execute_request(&url, &method, parameters).await?;
57
58        let status = response.status();
59        if status.is_success() {
60            let json_data = response
61                .json()
62                .await
63                .map_err(|e| VnishError::ParseError(e.to_string()))?;
64            Ok(json_data)
65        } else {
66            Err(VnishError::HttpError(status.as_u16()))?
67        }
68    }
69}
70
71impl VnishWebAPI {
72    /// Create a new Vnish WebAPI client
73    pub fn new(ip: IpAddr, port: u16) -> Self {
74        let client = Client::builder()
75            .timeout(Duration::from_secs(10))
76            .build()
77            .expect("Failed to create HTTP client");
78
79        Self {
80            client,
81            ip,
82            port,
83            timeout: Duration::from_secs(5),
84            bearer_token: RwLock::new(None),
85            password: Some("admin".to_string()), // Default password
86        }
87    }
88
89    /// Ensure authentication token is present, authenticate if needed
90    async fn ensure_authenticated(&self) -> Result<(), VnishError> {
91        if self.bearer_token.read().unwrap().is_none() && self.password.is_some() {
92            if let Some(ref password) = self.password {
93                match self.authenticate(password).await {
94                    Ok(token) => {
95                        *self.bearer_token.write().unwrap() = Some(token);
96                        Ok(())
97                    }
98                    Err(e) => Err(e),
99                }
100            } else {
101                Err(VnishError::AuthenticationFailed)
102            }
103        } else {
104            Ok(())
105        }
106    }
107
108    async fn authenticate(&self, password: &str) -> Result<String, VnishError> {
109        let unlock_payload = serde_json::json!({ "pw": password });
110        let url = format!("http://{}:{}/api/v1/unlock", self.ip, self.port);
111
112        let response = self
113            .client
114            .post(&url)
115            .json(&unlock_payload)
116            .timeout(self.timeout)
117            .send()
118            .await
119            .map_err(|e| VnishError::NetworkError(e.to_string()))?;
120
121        if !response.status().is_success() {
122            return Err(VnishError::AuthenticationFailed);
123        }
124
125        let unlock_response: Value = response
126            .json()
127            .await
128            .map_err(|e| VnishError::ParseError(e.to_string()))?;
129
130        unlock_response
131            .pointer("/token")
132            .and_then(|t| t.as_str())
133            .map(String::from)
134            .ok_or(VnishError::AuthenticationFailed)
135    }
136
137    /// Execute the actual HTTP request
138    async fn execute_request(
139        &self,
140        url: &str,
141        method: &Method,
142        parameters: Option<Value>,
143    ) -> Result<Response, VnishError> {
144        let request_builder = match *method {
145            Method::GET => self.client.get(url),
146            Method::POST => {
147                let mut builder = self.client.post(url);
148                if let Some(params) = parameters {
149                    builder = builder.json(&params);
150                }
151                builder
152            }
153            Method::PATCH => {
154                let mut builder = self.client.patch(url);
155                if let Some(params) = parameters {
156                    builder = builder.json(&params);
157                }
158                builder
159            }
160            _ => return Err(VnishError::UnsupportedMethod(method.to_string())),
161        };
162
163        let mut request_builder = request_builder.timeout(self.timeout);
164
165        // Add authentication headers if provided
166        if let Some(ref token) = *self.bearer_token.read().unwrap() {
167            request_builder = request_builder.header("Authorization", format!("Bearer {token}"));
168        }
169
170        let request = request_builder
171            .build()
172            .map_err(|e| VnishError::RequestError(e.to_string()))?;
173
174        let response = self
175            .client
176            .execute(request)
177            .await
178            .map_err(|e| VnishError::NetworkError(e.to_string()))?;
179
180        Ok(response)
181    }
182}
183
184/// Error types for Vnish WebAPI operations
185#[derive(Debug, Clone)]
186pub enum VnishError {
187    /// Network error (connection issues, DNS resolution, etc.)
188    NetworkError(String),
189    /// HTTP error with status code
190    HttpError(u16),
191    /// JSON parsing error
192    ParseError(String),
193    /// Request building error
194    RequestError(String),
195    /// Timeout error
196    Timeout,
197    /// Unsupported HTTP method
198    UnsupportedMethod(String),
199    /// Maximum retries exceeded
200    MaxRetriesExceeded,
201    /// Authentication failed
202    AuthenticationFailed,
203    /// Unauthorized (401)
204    Unauthorized,
205}
206
207impl std::fmt::Display for VnishError {
208    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209        match self {
210            VnishError::NetworkError(msg) => write!(f, "Network error: {msg}"),
211            VnishError::HttpError(code) => write!(f, "HTTP error: {code}"),
212            VnishError::ParseError(msg) => write!(f, "Parse error: {msg}"),
213            VnishError::RequestError(msg) => write!(f, "Request error: {msg}"),
214            VnishError::Timeout => write!(f, "Request timeout"),
215            VnishError::UnsupportedMethod(method) => write!(f, "Unsupported method: {method}"),
216            VnishError::MaxRetriesExceeded => write!(f, "Maximum retries exceeded"),
217            VnishError::AuthenticationFailed => write!(f, "Authentication failed"),
218            VnishError::Unauthorized => write!(f, "Unauthorized access"),
219        }
220    }
221}
222
223impl std::error::Error for VnishError {}