darkwing/server/dtos/entities/
proxy_dto.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
//! Proxy data transfer objects and related functionality
//!
//! This module provides types and implementations for handling proxy
//! configuration data, including conversion between encrypted database records
//! and decrypted runtime representations. It supports various proxy types,
//! optional authentication, and IP change functionality.

use base64::{engine::general_purpose::URL_SAFE, Engine as _};

use crate::{
  database::browser_profile::{Proxy, ProxyType},
  server::{
    error::{AppResult, Error},
    services::database_encryption_services::DynDatabaseEncryption,
  },
};

/// Represents the complete proxy configuration data with decrypted sensitive
/// fields
///
/// This struct contains all the necessary information to configure a proxy
/// connection, including optional authentication credentials and IP change
/// functionality.
#[derive(Debug, Clone)]
pub struct ProxyFullData {
  /// Unique identifier for the proxy configuration
  pub id: u64,
  /// Type of proxy (e.g. HTTP, SOCKS5)
  pub proxy_type: ProxyType,
  /// Hostname or IP address of the proxy server
  pub host: String,
  /// Optional username for proxy authentication
  pub login: Option<String>,
  /// Optional password for proxy authentication
  pub password: Option<String>,
  /// Port number that the proxy server listens on
  pub port: String,
  /// Optional URL endpoint for triggering IP address rotation
  pub change_ip_url: Option<String>,
}

impl ProxyFullData {
  /// Creates a new `ProxyFullData` instance from an encrypted `Proxy` record
  ///
  /// # Arguments
  ///
  /// * `proxy` - The encrypted proxy record from the database
  /// * `database_encryption` - Service for decrypting sensitive proxy data
  ///
  /// # Returns
  ///
  /// Returns `AppResult<Self>` which is Ok if all decryption operations succeed
  ///
  /// # Errors
  ///
  /// Returns an error if:
  /// - Encrypted data cannot be decoded as UTF-8
  /// - Decryption operations fail
  /// - Required fields are missing or invalid
  pub async fn new(
    proxy: Proxy,
    database_encryption: DynDatabaseEncryption,
  ) -> AppResult<Self> {
    let id = proxy.id;

    let login = match proxy.loginCrypt {
      Some(login) => {
        database_encryption
          .decrypt_string(
            proxy.cryptoKeyId as usize,
            String::from_utf8(login).map_err(|_| {
              Error::ConfigForming(
                "Login cannot be decoded as its value is not valid UTF-8"
                  .to_string(),
              )
            })?,
          )
          .await?
      }
      None => proxy.deletedLogin,
    };
    let login = login.trim().to_string();
    let login = if login.is_empty() { None } else { Some(login) };

    let password = match proxy.passwordCrypt {
      Some(password) => {
        database_encryption
          .decrypt_string(
            proxy.cryptoKeyId as usize,
            String::from_utf8(password).map_err(|_| {
              Error::ConfigForming(
                "Password cannot be decoded as its value is not valid UTF-8"
                  .to_string(),
              )
            })?,
          )
          .await?
      }
      None => proxy.password,
    };
    let password = password.trim().to_string();
    let password = if password.is_empty() {
      None
    } else {
      Some(password)
    };

    let port = match proxy.portCrypt {
      Some(port) => {
        database_encryption
          .decrypt_string(
            proxy.cryptoKeyId as usize,
            String::from_utf8(port).map_err(|_| {
              Error::ConfigForming(
                "Port cannot be decoded as its value is not valid UTF-8"
                  .to_string(),
              )
            })?,
          )
          .await?
      }
      None => proxy
        .deletedPort
        .ok_or(Error::ConfigForming("Port is not set".to_string()))?,
    };
    let port = port.trim().to_string();

    let change_ip_url = match proxy.changeIpUrlCrypt {
      Some(change_ip_url) => Some(
        database_encryption
          .decrypt_string(
            proxy.cryptoKeyId as usize,
            String::from_utf8(change_ip_url).map_err(|_| {
              Error::DatabaseDecoding(
                "changeIpUrl".to_string(),
                "value is not valid UTF-8".to_string(),
              )
            })?,
          )
          .await?,
      ),
      None => proxy.changeIpUrl,
    };

    let change_ip_url = change_ip_url.and_then(|url| {
      if url.is_empty() {
        None
      } else {
        Some(url.trim().to_string())
      }
    });

    Ok(Self {
      id,
      proxy_type: proxy.r#type,
      host: proxy.host.trim().to_string(),
      login,
      password,
      port,
      change_ip_url,
    })
  }

  /// Formats the proxy configuration as a standard proxy URL string
  ///
  /// Generates a URL in the format: `protocol://[user[:pass]@]host:port`
  /// where the authentication part is included only if credentials are present
  ///
  /// # Returns
  ///
  /// Returns a String containing the formatted proxy URL
  fn as_url(&self) -> String {
    match (&self.login, &self.password) {
      (Some(login), None) => format!(
        "{}://{}@{}:{}",
        self.proxy_type, login, self.host, self.port
      ),
      (Some(login), Some(password)) if !login.is_empty() => {
        format!(
          "{}://{}:{}@{}:{}",
          self.proxy_type, login, password, self.host, self.port
        )
      }
      (None, Some(password)) => format!(
        "{}://:{}@{}:{}",
        self.proxy_type, password, self.host, self.port
      ),
      _ => format!("{}://{}:{}", self.proxy_type, self.host, self.port),
    }
  }

  /// Formats the proxy configuration as a URL with base64-encoded credentials
  ///
  /// Similar to `as_url()`, but when both login and password are present,
  /// they are combined and base64-encoded in the format:
  /// `protocol://base64_encoded_creds:base64@host:port`
  ///
  /// # Returns
  ///
  /// Returns a String containing the proxy URL with base64-encoded credentials
  /// if applicable, otherwise returns a standard proxy URL
  pub fn as_base64_url(&self) -> String {
    match (&self.login, &self.password) {
      (Some(login), Some(password)) => {
        let encoded_pair = URL_SAFE
          .encode(format!("{}:{}", login, password))
          .replace("=", "");
        format!(
          "{}://{}:base64@{}:{}",
          self.proxy_type, encoded_pair, self.host, self.port
        )
      }
      _ => self.as_url(),
    }
  }

  /// Creates a mock proxy configuration for testing purposes
  ///
  /// # Returns
  ///
  /// Returns a `ProxyFullData` instance configured with localhost HTTP proxy
  /// settings
  pub fn get_mock() -> Self {
    Self {
      id: 0,
      proxy_type: ProxyType::Http,
      host: "127.0.0.1".to_string(),
      port: "8080".to_string(),
      login: None,
      password: None,
      change_ip_url: None,
    }
  }
}