pyrinas_cli/
lib.rs

1pub mod certs;
2pub mod config;
3pub mod device;
4pub mod git;
5pub mod ota;
6
7use clap::Parser;
8use pyrinas_shared::{ota::OTAPackageVersion, OtaLink};
9use serde::{Deserialize, Serialize};
10use std::{net::TcpStream, num};
11
12// Error handling
13use thiserror::Error;
14
15// Websocket
16use tungstenite::{http, http::Request, protocol::WebSocket, stream::MaybeTlsStream};
17
18#[derive(Debug, Error)]
19pub enum Error {
20    #[error("{source}")]
21    Error {
22        #[from]
23        source: config::Error,
24    },
25
26    #[error("http error: {source}")]
27    HttpError {
28        #[from]
29        source: http::Error,
30    },
31
32    #[error("websocket handshake error {source}")]
33    WebsocketError {
34        #[from]
35        source: tungstenite::Error,
36    },
37
38    #[error("semver error: {source}")]
39    SemVerError {
40        #[from]
41        source: semver::Error,
42    },
43
44    #[error("parse error: {source}")]
45    ParseError {
46        #[from]
47        source: num::ParseIntError,
48    },
49
50    #[error("err: {0}")]
51    CustomError(String),
52
53    #[error("ota error: {source}")]
54    OtaError {
55        #[from]
56        source: ota::Error,
57    },
58
59    #[error("{source}")]
60    CertsError {
61        #[from]
62        source: certs::Error,
63    },
64}
65
66/// Various commands related to the OTA process
67#[derive(Parser, Debug)]
68#[clap(version)]
69pub struct OtaCmd {
70    #[clap(subcommand)]
71    pub subcmd: OtaSubCommand,
72}
73
74/// Commands related to certs
75#[derive(Parser, Debug)]
76#[clap(version)]
77pub struct CertCmd {
78    #[clap(subcommand)]
79    pub subcmd: CertSubcommand,
80}
81
82#[derive(Parser, Debug)]
83#[clap(version)]
84pub enum CertSubcommand {
85    /// Generate CA cert
86    Ca,
87    /// Generate server cert
88    Server,
89    /// Generate device cert
90    Device(CertDevice),
91}
92
93/// Remove a OTA package from the sever
94#[derive(Parser, Debug)]
95#[clap(version)]
96pub struct CertDevice {
97    /// ID of the device (usually IMEI). Obtains from device if not provided.
98    id: Option<String>,
99    /// Automatic provision
100    #[clap(long, short)]
101    provision: bool,
102    /// Serial port
103    #[clap(long, default_value = certs::DEFAULT_MAC_PORT )]
104    port: String,
105    /// Security tag for provisioning
106    #[clap(long, short)]
107    tag: Option<u32>,
108}
109
110#[derive(Parser, Debug)]
111#[clap(version)]
112pub enum OtaSubCommand {
113    /// Add OTA package
114    Add(OtaAdd),
115    /// Associate command
116    Link(OtaLink),
117    /// Unlink from Device/Group
118    Unlink(OtaLink),
119    /// Remove OTA package
120    Remove(OtaRemove),
121    /// List groups
122    ListGroups,
123    /// List images
124    ListImages,
125}
126
127/// Add a OTA package from the sever
128#[derive(Parser, Debug)]
129#[clap(version)]
130pub struct OtaAdd {
131    /// Force updating in dirty repository
132    #[clap(long, short)]
133    pub force: bool,
134    /// Option to autmoatically associate with device.
135    /// Device group also set to device id.
136    #[clap(long, short)]
137    pub device_id: Option<String>,
138    ///  Optional version flag
139    #[clap(long, default_value = pyrinas_shared::DEFAULT_OTA_VERSION)]
140    pub ota_version: u8,
141}
142
143/// Remove a OTA package from the sever
144#[derive(Parser, Debug)]
145#[clap(version)]
146pub struct OtaRemove {
147    /// Image id to be directed to
148    pub image_id: String,
149}
150
151#[derive(Debug, Serialize, Deserialize, Default)]
152pub struct CertConfig {
153    /// Domain certs are being generated for
154    pub domain: String,
155    /// Organization entry for cert gen
156    pub organization: String,
157    /// Country entry for cert gen
158    pub country: String,
159    /// PFX password
160    pub pfx_pass: String,
161}
162
163#[derive(Debug, Serialize, Deserialize, Default)]
164pub struct CertEntry {
165    /// tag number
166    pub tag: u32,
167    /// ca cert
168    pub ca_cert: Option<String>,
169    /// private key
170    pub private_key: Option<String>,
171    /// pub key
172    pub pub_key: Option<String>,
173}
174
175/// Config that can be installed locally
176#[derive(Debug, Serialize, Deserialize, Default)]
177pub struct Config {
178    /// URL of the Pyrinas server to connect to.
179    /// For example: pyrinas-admin.yourdomain.com
180    pub url: String,
181    /// Determines secure connection or not
182    pub secure: bool,
183    /// Authentication key. This is the same key set in
184    /// the Pyrinas config.toml
185    pub authkey: String,
186    /// Server cert configuration
187    pub cert: CertConfig,
188    /// Alternative Certs to program to provisioned devices
189    pub alts: Option<Vec<CertEntry>>,
190}
191
192/// Configuration related commands
193#[derive(Parser, Debug, Serialize, Deserialize)]
194#[clap(version)]
195pub struct ConfigCmd {
196    #[clap(subcommand)]
197    pub subcmd: ConfigSubCommand,
198}
199
200#[derive(Parser, Debug, Serialize, Deserialize)]
201#[clap(version)]
202pub enum ConfigSubCommand {
203    Show(Show),
204    Init,
205}
206
207/// Show current configuration
208#[derive(Parser, Debug, Serialize, Deserialize)]
209#[clap(version)]
210pub struct Show {}
211
212// Struct that gets serialized for OTA support
213#[derive(Debug, Serialize, Deserialize, Clone)]
214pub struct OTAManifest {
215    pub version: OTAPackageVersion,
216    pub file: String,
217    pub force: bool,
218}
219
220// pub fn get_git_describe() -> Result<String, Error> {
221//     // Expected output 0.2.1-19-g09db6ef-dirty
222
223//     // Get git describe output
224//     let out = Command::new("git")
225//         .args(&["describe", "--dirty", "--always", "--long"])
226//         .output()?;
227
228//     let err = std::str::from_utf8(&out.stderr)?;
229//     let out = std::str::from_utf8(&out.stdout)?;
230
231//     // Return error if not blank
232//     if err != "" {
233//         return Err(anyhow!("Git error. Err: {}", err));
234//     }
235
236//     // Convert it to String
237//     Ok(out.to_string())
238// }
239
240pub fn get_socket(config: &Config) -> Result<WebSocket<MaybeTlsStream<TcpStream>>, Error> {
241    if !config.secure {
242        println!("WARNING! Not using secure web socket connection!");
243    }
244
245    // String of full URL
246    let full_uri = format!(
247        "ws{}://{}/socket",
248        match config.secure {
249            true => "s",
250            false => "",
251        },
252        config.url
253    );
254
255    // Set up handshake request
256    let req = Request::builder()
257        .uri(full_uri)
258        .header("ApiKey", config.authkey.clone())
259        .body(())?;
260
261    // Connect to TCP based WS socket
262    // TODO: confirm URL is parsed correctly into tungstenite
263    let (socket, _response) = tungstenite::connect(req)?;
264
265    // Return this guy
266    Ok(socket)
267}
268
269#[cfg(test)]
270mod tests {
271
272    use super::*;
273    use std::sync::Once;
274
275    static INIT: Once = Once::new();
276
277    /// Setup function that is only run once, even if called multiple times.
278    fn setup() {
279        INIT.call_once(|| env_logger::init());
280    }
281
282    #[test]
283    fn get_ota_package_version_success_with_dirty() {
284        // Log setup
285        setup();
286
287        let ver = "0.2.1-19-g09db6ef-dirty";
288
289        let res = git::get_ota_package_version(ver);
290
291        // Make sure it processed ok
292        assert!(res.is_ok());
293
294        let (package_ver, dirty) = res.unwrap();
295
296        // Confirm it's dirty
297        assert!(dirty);
298
299        // confirm the version is correct
300        assert_eq!(
301            package_ver,
302            OTAPackageVersion {
303                major: 0,
304                minor: 2,
305                patch: 1,
306                commit: 19,
307                hash: [
308                    'g' as u8, '0' as u8, '9' as u8, 'd' as u8, 'b' as u8, '6' as u8, 'e' as u8,
309                    'f' as u8
310                ]
311            }
312        )
313    }
314
315    #[test]
316    fn get_ota_package_version_success_clean() {
317        // Log setup
318        setup();
319
320        let ver = "0.2.1-19-g09db6ef";
321
322        let res = git::get_ota_package_version(ver);
323
324        // Make sure it processed ok
325        assert!(res.is_ok());
326
327        let (package_ver, dirty) = res.unwrap();
328
329        // Confirm it's dirty
330        assert!(!dirty);
331
332        // confirm the version is correct
333        assert_eq!(
334            package_ver,
335            OTAPackageVersion {
336                major: 0,
337                minor: 2,
338                patch: 1,
339                commit: 19,
340                hash: [
341                    'g' as u8, '0' as u8, '9' as u8, 'd' as u8, 'b' as u8, '6' as u8, 'e' as u8,
342                    'f' as u8
343                ]
344            }
345        )
346    }
347
348    #[test]
349    fn get_ota_package_version_failure_dirty() {
350        // Log setup
351        setup();
352
353        let ver = "0.2.1-g09db6ef-dirty";
354
355        let res = git::get_ota_package_version(ver);
356
357        // Make sure it processed ok
358        assert!(res.is_err());
359    }
360
361    // TODO: address this
362    #[allow(dead_code)]
363    fn get_git_describe_success() {
364        // Log setup
365        setup();
366
367        let res = git::get_git_describe();
368
369        // Make sure it processed ok
370        assert!(res.is_ok());
371
372        log::info!("res: {}", res.unwrap());
373    }
374}