1use anyhow::{bail, Context, Result};
2use cloudpub_common::constants::DEFAULT_HEARTBEAT_TIMEOUT_SECS;
3use cloudpub_common::{DOMAIN, PORT};
4use serde::{Deserialize, Serialize};
5use std::fmt::{Debug, Display, Formatter};
6use std::str::FromStr;
7
8use crate::options::ConfigOption;
9pub use cloudpub_common::config::{MaskedString, TransportConfig};
10use lazy_static::lazy_static;
11#[cfg(not(target_os = "android"))]
12use machineid_rs::{Encryption, HWIDComponent, IdBuilder};
13use std::collections::HashMap;
14use std::fs::{self, create_dir_all, File};
15use std::io::Write;
16use std::path::PathBuf;
17use tracing::debug;
18use url::Url;
19use uuid::Uuid;
20
21#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
22pub enum Platform {
23 #[default]
24 X64,
25 X32,
26}
27
28#[derive(Clone)]
29pub struct EnvConfig {
30 pub home_1c: PathBuf,
31 #[cfg(target_os = "windows")]
32 pub redist: String,
33 pub httpd: String,
34 pub httpd_dir: String,
35}
36
37impl Default for EnvConfig {
38 fn default() -> Self {
39 #[cfg(target_pointer_width = "64")]
40 return ENV_CONFIG.get(&Platform::X64).unwrap().clone();
41 #[cfg(target_pointer_width = "32")]
42 return ENV_CONFIG.get(&Platform::X32).unwrap().clone();
43 }
44}
45
46lazy_static! {
47 pub static ref ENV_CONFIG: HashMap<Platform, EnvConfig> = {
48 let mut m = HashMap::new();
49 #[cfg(target_os = "windows")]
50 m.insert(
51 Platform::X64,
52 EnvConfig {
53 home_1c: PathBuf::from("C:\\Program Files\\1cv8\\"),
54 redist: "vc_redist.x64.exe".to_string(),
55 httpd: "httpd-2.4.61-240703-win64-VS17.zip".to_string(),
56 httpd_dir: "httpd-x64".to_string(),
57 },
58 );
59 #[cfg(target_os = "windows")]
60 m.insert(
61 Platform::X32,
62 EnvConfig {
63 home_1c: PathBuf::from("C:\\Program Files (x86)\\1cv8"),
64 redist: "vc_redist.x86.exe".to_string(),
65 httpd: "httpd-2.4.61-240703-win32-vs17.zip".to_string(),
66 httpd_dir: "httpd-x32".to_string(),
67 },
68 );
69 #[cfg(target_os = "linux")]
70 m.insert(
71 Platform::X64,
72 EnvConfig {
73 home_1c: PathBuf::from("/opt/1C"),
74 httpd: "httpd-2.4.63-linux.zip".to_string(),
75 httpd_dir: "httpd-x64".to_string(),
76 },
77 );
78 #[cfg(target_os = "macos")]
79 m.insert(
80 Platform::X64,
81 EnvConfig {
82 home_1c: PathBuf::from("/Applications/1C"),
83 httpd: "httpd-2.4.62-macos.zip".to_string(),
84 httpd_dir: "httpd-x64".to_string(),
85 },
86 );
87 #[cfg(target_os = "android")]
88 m.insert(
89 Platform::X64,
90 EnvConfig {
91 home_1c: PathBuf::from("/data/local/tmp"),
92 httpd: String::new(),
93 httpd_dir: String::new(),
94 },
95 );
96 m
97 };
98}
99
100impl Display for Platform {
101 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
102 match self {
103 Platform::X64 => write!(f, "x64"),
104 Platform::X32 => write!(f, "x32"),
105 }
106 }
107}
108
109impl std::str::FromStr for Platform {
110 type Err = anyhow::Error;
111
112 fn from_str(s: &str) -> Result<Self> {
113 match s {
114 "x64" => Ok(Platform::X64),
115 "x32" => Ok(Platform::X32),
116 _ => bail!("Invalid platform: {}", s),
117 }
118 }
119}
120
121#[derive(Debug, PartialEq, Eq, Clone, Default)]
122pub struct ClientOpts {
123 pub gui: bool,
124 pub credentials: Option<(String, String)>,
125 pub transient: bool,
126 pub secondary: bool,
127 pub is_service: bool,
128}
129
130#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
131pub struct ClientConfig {
132 #[serde(skip)]
134 config_path: PathBuf,
135 #[serde(skip)]
136 pub readonly: bool,
137 pub agent_id: String,
138 pub hwid: Option<String>,
139 pub server: Url,
140 pub token: Option<MaskedString>,
141 pub heartbeat_timeout: u64,
142 pub one_c_home: Option<String>,
143 pub one_c_platform: Option<Platform>,
144 pub one_c_publish_dir: Option<String>,
145 pub minecraft_server: Option<String>,
146 pub minecraft_java_opts: Option<String>,
147 pub minimize_to_tray_on_close: Option<bool>,
148 pub minimize_to_tray_on_start: Option<bool>,
149 pub transport: TransportConfig,
150}
151
152impl ClientConfig {
153 pub fn get_config_path(&self) -> &PathBuf {
154 &self.config_path
155 }
156
157 pub fn from_file(path: &PathBuf, readonly: bool) -> Result<Self> {
158 if !path.exists() && !readonly {
159 let default_config = Self::default();
160 debug!("Creating default config at {:?}", default_config);
161 let s = toml::to_string_pretty(&default_config)
162 .context("Failed to serialize the default config")?;
163 fs::write(path, s).context("Failed to write the default config")?;
164 }
165
166 let s: String = fs::read_to_string(path)
167 .with_context(|| format!("Failed to read the config {:?}", path))?;
168 let mut cfg: Self = toml::from_str(&s).with_context(|| {
169 "Configuration is invalid. Please refer to the configuration specification."
170 })?;
171
172 cfg.config_path = path.clone();
173 cfg.readonly = readonly;
174 Ok(cfg)
175 }
176
177 pub fn get_config_dir(user_dir: bool) -> Result<PathBuf> {
178 let dir = if user_dir {
179 let mut dir = dirs::config_dir().context("Can't get config_dir")?;
180 dir.push("cloudpub");
181 dir
182 } else if cfg!(target_family = "unix") {
183 PathBuf::from("./etc/cloudpub")
184 } else {
185 PathBuf::from("C:\\Windows\\system32\\config\\systemprofile\\AppData\\Local\\cloudpub")
186 };
187 if !dir.exists() {
188 create_dir_all(&dir).context("Can't create config dir")?;
189 }
190 debug!("Config dir: {:?}", dir);
191 Ok(dir)
192 }
193
194 pub fn save(&self) -> Result<()> {
195 if self.readonly {
196 debug!("Skipping saving the config in readonly mode");
197 } else {
198 let s = toml::to_string_pretty(self).context("Failed to serialize the config")?;
199 let mut f = File::create(&self.config_path)?;
200 f.write_all(s.as_bytes())?;
201 f.sync_all()?;
202 }
203 Ok(())
204 }
205
206 pub fn load(cfg_name: &str, user_dir: bool, readonly: bool) -> Result<Self> {
207 let mut config_path = Self::get_config_dir(user_dir)?;
208 config_path.push(cfg_name);
209
210 Self::from_file(&config_path, readonly)
211 }
212
213 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
214 let option = ConfigOption::from_str(key)?;
215 self.set_option(option, value)
216 }
217
218 pub fn get(&self, key: &str) -> Result<String> {
219 let option = ConfigOption::from_str(key)?;
220 self.get_option(option)
221 }
222
223 pub fn get_all_options(&self) -> Vec<(String, String)> {
224 self.get_all_config_options()
225 }
226
227 pub fn validate(&self) -> Result<()> {
228 if self.token.is_none() {
229 bail!("{}", crate::t!("error-auth-missing"));
230 }
231 TransportConfig::validate(&self.transport, false)?;
232 Ok(())
233 }
234
235 #[cfg(not(target_os = "android"))]
236 pub fn get_hwid(&self) -> String {
237 self.hwid
238 .as_ref()
239 .unwrap_or(
240 &IdBuilder::new(Encryption::SHA256)
241 .add_component(HWIDComponent::OSName)
242 .add_component(HWIDComponent::SystemID)
243 .add_component(HWIDComponent::MachineName)
244 .add_component(HWIDComponent::CPUID)
245 .build("cloudpub")
246 .unwrap_or(self.agent_id.clone()),
247 )
248 .to_string()
249 }
250
251 #[cfg(target_os = "android")]
252 pub fn get_hwid(&self) -> String {
253 self.hwid.as_ref().unwrap_or(&self.agent_id).to_string()
254 }
255}
256
257impl Default for ClientConfig {
258 fn default() -> Self {
259 Self {
260 agent_id: Uuid::new_v4().to_string(),
261 hwid: None,
262 config_path: PathBuf::new(),
263 server: format!("https://{}:{}", DOMAIN, PORT).parse().unwrap(),
264 token: None,
265 heartbeat_timeout: DEFAULT_HEARTBEAT_TIMEOUT_SECS,
266 one_c_home: None,
267 one_c_platform: None,
268 one_c_publish_dir: None,
269 minecraft_server: None,
270 minecraft_java_opts: None,
271 minimize_to_tray_on_close: Some(true),
272 minimize_to_tray_on_start: Some(false),
273 transport: TransportConfig::default(),
274 readonly: false,
275 }
276 }
277}