donut_lib/
opts.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{Error, format_err};
6use clap::{Parser, Subcommand};
7use eui48::MacAddress;
8use {fidl_fuchsia_wlan_common as wlan_common, fidl_fuchsia_wlan_policy as wlan_policy};
9
10#[derive(PartialEq, Copy, Clone, Debug, clap::ValueEnum)]
11pub enum RoleArg {
12    Client,
13    Ap,
14}
15
16#[derive(PartialEq, Copy, Clone, Debug, clap::ValueEnum)]
17pub enum ScanTypeArg {
18    Active,
19    Passive,
20}
21
22#[derive(PartialEq, Copy, Clone, Debug, clap::ValueEnum)]
23pub enum SecurityTypeArg {
24    None,
25    Wep,
26    Wpa,
27    Wpa2,
28    Wpa3,
29}
30
31#[derive(PartialEq, Copy, Clone, Debug, clap::ValueEnum)]
32pub enum CredentialTypeArg {
33    None,
34    Psk,
35    Password,
36}
37
38impl From<RoleArg> for wlan_common::WlanMacRole {
39    fn from(arg: RoleArg) -> Self {
40        match arg {
41            RoleArg::Client => wlan_common::WlanMacRole::Client,
42            RoleArg::Ap => wlan_common::WlanMacRole::Ap,
43        }
44    }
45}
46
47impl From<ScanTypeArg> for wlan_common::ScanType {
48    fn from(arg: ScanTypeArg) -> Self {
49        match arg {
50            ScanTypeArg::Active => wlan_common::ScanType::Active,
51            ScanTypeArg::Passive => wlan_common::ScanType::Passive,
52        }
53    }
54}
55
56impl From<SecurityTypeArg> for wlan_policy::SecurityType {
57    fn from(arg: SecurityTypeArg) -> Self {
58        match arg {
59            SecurityTypeArg::r#None => wlan_policy::SecurityType::None,
60            SecurityTypeArg::Wep => wlan_policy::SecurityType::Wep,
61            SecurityTypeArg::Wpa => wlan_policy::SecurityType::Wpa,
62            SecurityTypeArg::Wpa2 => wlan_policy::SecurityType::Wpa2,
63            SecurityTypeArg::Wpa3 => wlan_policy::SecurityType::Wpa3,
64        }
65    }
66}
67
68impl From<PolicyNetworkConfig> for wlan_policy::NetworkConfig {
69    fn from(arg: PolicyNetworkConfig) -> Self {
70        let credential = match arg.credential_type {
71            Some(CredentialTypeArg::r#None) => wlan_policy::Credential::None(wlan_policy::Empty),
72            Some(CredentialTypeArg::Psk) => {
73                wlan_policy::Credential::Psk(parse_psk_string(arg.credential.unwrap()))
74            }
75            Some(CredentialTypeArg::Password) => {
76                wlan_policy::Credential::Password(arg.credential.unwrap().as_bytes().to_vec())
77            }
78            None => {
79                // If credential type is not provided, infer it from the credential value.
80                credential_from_string(arg.credential.unwrap_or_else(|| "".to_string()))
81            }
82        };
83
84        let security_type = security_type_from_args(arg.security_type, &credential);
85
86        let network_id = wlan_policy::NetworkIdentifier {
87            ssid: arg.ssid.as_bytes().to_vec(),
88            type_: security_type,
89        };
90        wlan_policy::NetworkConfig {
91            id: Some(network_id),
92            credential: Some(credential),
93            ..Default::default()
94        }
95    }
96}
97
98/// Parse the hexadecimal characters to bytes if a valid PSK is provided, or panic with an error
99/// message if the format is invalid.
100fn parse_psk_string(credential: String) -> Vec<u8> {
101    let psk_arg = credential.as_bytes().to_vec();
102    hex::decode(psk_arg).expect(
103        "Error: PSK must be 64 hexadecimal characters.\
104        Example: \"123456789ABCDEF123456789ABCDEF123456789ABCDEF123456789ABCDEF1234\"",
105    )
106}
107
108/// Build a WLAN policy FIDL type credential from a string. PSK will be given in hexadecimal.
109/// This panics if the string does not represent a valid credential.
110fn credential_from_string(credential: String) -> wlan_policy::Credential {
111    match credential.len() {
112        0 => wlan_policy::Credential::None(wlan_policy::Empty),
113        0..=63 => wlan_policy::Credential::Password(credential.into_bytes()),
114        64 => wlan_policy::Credential::Psk(parse_psk_string(credential)),
115        65..=usize::MAX => {
116            panic!(
117                "Provided credential is too long. A password must be between 0 and 63 \
118                characters and a PSK must be 64 hexadecimal characters. Provided \
119                credential is {} characters.",
120                credential.len()
121            );
122        }
123        _ => {
124            // This shouldn't happen; all possible lengths should be handled above.
125            panic!("Invalid credential of length {}", credential.len())
126        }
127    }
128}
129
130/// Convert the security type provided as an argument, or use a default type that matches the
131/// provided credential.
132fn security_type_from_args(
133    security_arg: Option<SecurityTypeArg>,
134    credential: &wlan_policy::Credential,
135) -> wlan_policy::SecurityType {
136    if let Some(arg) = security_arg {
137        match arg {
138            SecurityTypeArg::Wep => wlan_policy::SecurityType::Wep,
139            SecurityTypeArg::Wpa => wlan_policy::SecurityType::Wpa,
140            SecurityTypeArg::Wpa2 => wlan_policy::SecurityType::Wpa2,
141            SecurityTypeArg::Wpa3 => wlan_policy::SecurityType::Wpa3,
142            SecurityTypeArg::r#None => wlan_policy::SecurityType::None,
143        }
144    } else {
145        match credential {
146            wlan_policy::Credential::None(_) => wlan_policy::SecurityType::None,
147            _ => wlan_policy::SecurityType::Wpa2,
148        }
149    }
150}
151
152#[derive(clap::Args, Clone, Debug)]
153pub struct PolicyNetworkConfig {
154    #[arg(long)]
155    pub ssid: String,
156    #[arg(long = "security-type", value_enum, ignore_case = true)]
157    pub security_type: Option<SecurityTypeArg>,
158    #[arg(long = "credential-type", value_enum, ignore_case = true)]
159    pub credential_type: Option<CredentialTypeArg>,
160    #[arg(long)]
161    pub credential: Option<String>,
162}
163
164/// Remove args are similar to SaveNetworkArgs but with optional security type and credential
165/// because it can match to saved networks with just SSID.
166/// Examples of valid arguments:
167///     --ssid MyNetwork
168///     --ssid MyNetwork --security-type wpa2
169///     --ssid MyNetwork --credential MyPassword (defaults to credential-type: password)
170///     --ssid MyNetwork --credential-type password --credential MyPassword
171/// Example of invalid arguments:
172///     --ssid MyNetwork --security-type none --credential MyPassword
173///     --ssid MyNetwork --credential-type password
174#[derive(clap::Args, Clone, Debug)]
175pub struct RemoveArgs {
176    #[arg(long)]
177    pub ssid: String,
178    #[arg(long = "security-type", value_enum, ignore_case = true)]
179    pub security_type: Option<SecurityTypeArg>,
180    #[arg(long = "credential-type", value_enum, ignore_case = true)]
181    pub credential_type: Option<CredentialTypeArg>,
182    #[arg(long)]
183    pub credential: Option<String>,
184}
185
186impl RemoveArgs {
187    pub fn parse_security(&self) -> Option<wlan_policy::SecurityType> {
188        self.security_type.map(|s| s.into())
189    }
190
191    /// Determine the credential to use based on security_type, credential_type, and credential. If
192    /// a value is provided without a type, Password will be used by default for open networks.
193    ///
194    /// This includes a bunch of if statements for edge cases such as a credential being provided
195    /// as "" or a credential type that conflicts with the credential value. But it does not check
196    /// all input errors such as invalid PSK length or all conflicting security credential pairs.
197    pub fn try_parse_credential(&self) -> Result<Option<wlan_policy::Credential>, Error> {
198        let credential = if let Some(cred_val) = &self.credential {
199            match self.credential_type {
200                Some(CredentialTypeArg::Password) => {
201                    Some(wlan_policy::Credential::Password(cred_val.as_bytes().to_vec()))
202                }
203                Some(CredentialTypeArg::Psk) => {
204                    // The PSK is given in a 64 character hexadecimal string. Config args are safe to
205                    // unwrap because the tool requires them to be present in the command.
206                    let psk_arg = cred_val.as_bytes().to_vec();
207                    let psk = hex::decode(psk_arg).expect(
208                        "Error: PSK must be 64 hexadecimal characters.\
209                        Example: \"123456789ABCDEF123456789ABCDEF123456789ABCDEF123456789ABCDEF1234\"",
210                    );
211                    Some(wlan_policy::Credential::Psk(psk))
212                }
213                Some(CredentialTypeArg::None) => {
214                    // Credential arg shouldn't be provided if the credential type is None; if it
215                    // is provided return an error rather than throwing away the input.
216                    if cred_val.is_empty() {
217                        Some(wlan_policy::Credential::None(wlan_policy::Empty))
218                    } else {
219                        return Err(format_err!(
220                            "A credential value was provided with a credential \
221                            type of None. No value be provided if there is no credential, or the \
222                            credential type should be PSK or Password."
223                        ));
224                    }
225                }
226                None => {
227                    // If credential value is provided but its type isn't, default to password.
228                    // Except for the edge case of an empty string - assume the user meant
229                    // Credential None since passwords can't be empty.
230                    if cred_val.is_empty() {
231                        Some(wlan_policy::Credential::None(wlan_policy::Empty))
232                    } else {
233                        Some(wlan_policy::Credential::Password(cred_val.as_bytes().to_vec()))
234                    }
235                }
236            }
237        } else {
238            if let Some(credential_type) = self.credential_type {
239                if credential_type == CredentialTypeArg::None {
240                    Some(wlan_policy::Credential::None(wlan_policy::Empty))
241                } else {
242                    return Err(format_err!(
243                        "A credential type was provided with no value. Please \
244                        provide the credential to be used"
245                    ));
246                }
247            } else {
248                // This is the case where no credential type or value was provided. There is no
249                // need to default to Credential::None for open networks since that is the only
250                // type of credential that could be saved for them.
251                None
252            }
253        };
254        Ok(credential)
255    }
256}
257
258#[derive(clap::Args, Clone, Debug)]
259pub struct SaveNetworkArgs {
260    #[arg(long)]
261    pub ssid: String,
262    #[arg(long = "security-type", value_enum, ignore_case = true)]
263    pub security_type: SecurityTypeArg,
264    #[arg(long = "credential-type", value_enum, ignore_case = true)]
265    pub credential_type: Option<CredentialTypeArg>,
266    #[arg(long)]
267    pub credential: String,
268}
269
270#[derive(clap::Args, Clone, Debug)]
271pub struct ConnectArgs {
272    #[arg(long)]
273    pub ssid: String,
274    #[arg(long = "security-type", value_enum, ignore_case = true)]
275    pub security_type: Option<SecurityTypeArg>,
276}
277
278#[derive(Subcommand, Clone, Debug)]
279pub enum PolicyClientCmd {
280    #[command(name = "connect")]
281    Connect(ConnectArgs),
282    #[command(name = "list-saved-networks")]
283    GetSavedNetworks,
284    #[command(name = "listen")]
285    Listen,
286    #[command(name = "remove-network")]
287    RemoveNetwork(RemoveArgs),
288    #[command(name = "save-network")]
289    SaveNetwork(PolicyNetworkConfig),
290    #[command(name = "scan")]
291    ScanForNetworks,
292    #[command(name = "start-client-connections")]
293    StartClientConnections,
294    #[command(name = "stop-client-connections")]
295    StopClientConnections,
296    #[command(name = "dump-config")]
297    DumpConfig,
298    #[command(name = "restore-config")]
299    RestoreConfig { serialized_config: String },
300    #[command(name = "status")]
301    Status,
302}
303
304#[derive(Subcommand, Clone, Debug)]
305pub enum PolicyAccessPointCmd {
306    // TODO(sakuma): Allow users to specify connectivity mode and operating band.
307    #[command(name = "start")]
308    Start(PolicyNetworkConfig),
309    #[command(name = "stop")]
310    Stop(PolicyNetworkConfig),
311    #[command(name = "stop-all")]
312    StopAllAccessPoints,
313    #[command(name = "listen")]
314    Listen,
315    #[command(name = "status")]
316    Status,
317}
318
319#[derive(Subcommand, Clone, Debug)]
320pub enum DeprecatedConfiguratorCmd {
321    #[command(name = "suggest-mac")]
322    SuggestAccessPointMacAddress {
323        #[arg(required = true)]
324        mac: MacAddress,
325    },
326}
327
328#[derive(Parser, Clone, Debug)]
329pub enum Opt {
330    #[command(subcommand, name = "client")]
331    Client(PolicyClientCmd),
332    #[command(subcommand, name = "ap")]
333    AccessPoint(PolicyAccessPointCmd),
334    #[command(subcommand, name = "deprecated")]
335    Deprecated(DeprecatedConfiguratorCmd),
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341    use test_case::test_case;
342
343    /// Tests that a WEP network config will be correctly translated for save and remove network.
344    #[fuchsia::test]
345    fn test_construct_config_wep() {
346        test_construct_config_security(wlan_policy::SecurityType::Wep, SecurityTypeArg::Wep);
347    }
348
349    /// Tests that a WPA network config will be correctly translated for save and remove network.
350    #[fuchsia::test]
351    fn test_construct_config_wpa() {
352        test_construct_config_security(wlan_policy::SecurityType::Wpa, SecurityTypeArg::Wpa);
353    }
354
355    /// Tests that a WPA2 network config will be correctly translated for save and remove network.
356    #[fuchsia::test]
357    fn test_construct_config_wpa2() {
358        test_construct_config_security(wlan_policy::SecurityType::Wpa2, SecurityTypeArg::Wpa2);
359    }
360
361    /// Tests that a WPA3 network config will be correctly translated for save and remove network.
362    #[fuchsia::test]
363    fn test_construct_config_wpa3() {
364        test_construct_config_security(wlan_policy::SecurityType::Wpa3, SecurityTypeArg::Wpa3);
365    }
366
367    /// Tests that a config for an open network will be correctly translated to FIDL values for
368    /// save and remove network when no security type and credential type are omitted.
369    #[fuchsia::test]
370    fn test_construct_config_open() {
371        let open_config = PolicyNetworkConfig {
372            ssid: "some_ssid".to_string(),
373            security_type: None,
374            credential_type: None,
375            credential: Some("".to_string()),
376        };
377        let expected_cfg = wlan_policy::NetworkConfig {
378            id: Some(wlan_policy::NetworkIdentifier {
379                ssid: "some_ssid".as_bytes().to_vec(),
380                type_: wlan_policy::SecurityType::None,
381            }),
382            credential: Some(wlan_policy::Credential::None(wlan_policy::Empty {})),
383            ..Default::default()
384        };
385        let result_cfg = wlan_policy::NetworkConfig::from(open_config);
386        assert_eq!(expected_cfg, result_cfg);
387    }
388
389    /// Tests that a config for an open network will be correctly translated to FIDL values for
390    /// save and remove network when credential type and security type are specified.
391    #[fuchsia::test]
392    fn test_construct_config_open_with_omitted_args() {
393        let open_config = PolicyNetworkConfig {
394            ssid: "some_ssid".to_string(),
395            security_type: Some(SecurityTypeArg::None),
396            credential_type: Some(CredentialTypeArg::None),
397            credential: Some("".to_string()),
398        };
399        let expected_cfg = wlan_policy::NetworkConfig {
400            id: Some(wlan_policy::NetworkIdentifier {
401                ssid: "some_ssid".as_bytes().to_vec(),
402                type_: wlan_policy::SecurityType::None,
403            }),
404            credential: Some(wlan_policy::Credential::None(wlan_policy::Empty {})),
405            ..Default::default()
406        };
407        let result_cfg = wlan_policy::NetworkConfig::from(open_config);
408        assert_eq!(expected_cfg, result_cfg);
409    }
410
411    /// Test the case where a config is saved with SSID and password, but no security type or
412    /// credential type provided. This is a common usage of the tool.
413    #[fuchsia::test]
414    fn test_construct_config_password_provided_no_security() {
415        let password = "mypassword";
416        let ssid = "some_ssid";
417        let arg_config = PolicyNetworkConfig {
418            ssid: ssid.to_string(),
419            security_type: None,
420            credential_type: None,
421            credential: Some(password.to_string()),
422        };
423        let expected_cfg = wlan_policy::NetworkConfig {
424            id: Some(wlan_policy::NetworkIdentifier {
425                ssid: ssid.as_bytes().to_vec(),
426                type_: wlan_policy::SecurityType::Wpa2,
427            }),
428            credential: Some(wlan_policy::Credential::Password(password.as_bytes().to_vec())),
429            ..Default::default()
430        };
431        let result_cfg = wlan_policy::NetworkConfig::from(arg_config);
432        assert_eq!(expected_cfg, result_cfg);
433    }
434
435    /// Test the case where a config is saved with SSID and psk, but no security type or
436    /// credential type provided.
437    #[fuchsia::test]
438    fn test_construct_config_psk_provided_no_security() {
439        let psk = "123456789ABCDEF123456789ABCDEF123456789ABCDEF123456789ABCDEF1234".to_string();
440        let psk_bytes = hex::decode(psk.as_bytes().to_vec()).unwrap();
441        let ssid = "some_ssid";
442        let arg_config = PolicyNetworkConfig {
443            ssid: ssid.to_string(),
444            security_type: None,
445            credential_type: None,
446            credential: Some(psk),
447        };
448        let expected_cfg = wlan_policy::NetworkConfig {
449            id: Some(wlan_policy::NetworkIdentifier {
450                ssid: ssid.as_bytes().to_vec(),
451                type_: wlan_policy::SecurityType::Wpa2,
452            }),
453            credential: Some(wlan_policy::Credential::Psk(psk_bytes)),
454            ..Default::default()
455        };
456        let result_cfg = wlan_policy::NetworkConfig::from(arg_config);
457        assert_eq!(expected_cfg, result_cfg);
458    }
459
460    /// Test that a config with a PSK will be translated correctly, including a transfer from a
461    /// hex string to bytes.
462    #[fuchsia::test]
463    fn test_construct_config_psk() {
464        // Test PSK separately since it has a unique credential
465        const ASCII_ZERO: u8 = 49;
466        let psk =
467            String::from_utf8([ASCII_ZERO; 64].to_vec()).expect("Failed to create PSK test value");
468        let wpa_config = PolicyNetworkConfig {
469            ssid: "some_ssid".to_string(),
470            security_type: Some(SecurityTypeArg::Wpa2),
471            credential_type: Some(CredentialTypeArg::Psk),
472            credential: Some(psk),
473        };
474        let expected_cfg = wlan_policy::NetworkConfig {
475            id: Some(wlan_policy::NetworkIdentifier {
476                ssid: "some_ssid".as_bytes().to_vec(),
477                type_: wlan_policy::SecurityType::Wpa2,
478            }),
479            credential: Some(wlan_policy::Credential::Psk([17; 32].to_vec())),
480            ..Default::default()
481        };
482        let result_cfg = wlan_policy::NetworkConfig::from(wpa_config);
483        assert_eq!(expected_cfg, result_cfg);
484    }
485
486    /// Test that the given variant of security type with a password works when constructing
487    /// network configs as used by save and remove network.
488    fn test_construct_config_security(
489        fidl_type: wlan_policy::SecurityType,
490        tool_type: SecurityTypeArg,
491    ) {
492        let args_config = PolicyNetworkConfig {
493            ssid: "some_ssid".to_string(),
494            security_type: Some(tool_type),
495            credential_type: Some(CredentialTypeArg::Password),
496            credential: Some("some_password_here".to_string()),
497        };
498        let expected_cfg = wlan_policy::NetworkConfig {
499            id: Some(wlan_policy::NetworkIdentifier {
500                ssid: "some_ssid".as_bytes().to_vec(),
501                type_: fidl_type,
502            }),
503            credential: Some(wlan_policy::Credential::Password(
504                "some_password_here".as_bytes().to_vec(),
505            )),
506            ..Default::default()
507        };
508        let result_cfg = wlan_policy::NetworkConfig::from(args_config);
509        assert_eq!(expected_cfg, result_cfg);
510    }
511
512    /// Test cases where the credential and type are not provided and the parsed credential should
513    /// be none.
514    #[test_case(Some(SecurityTypeArg::None))]
515    #[test_case(None)]
516    fn test_try_parse_credentialcredential_not_provided(security_type: Option<SecurityTypeArg>) {
517        let args = RemoveArgs {
518            ssid: "some_ssid".to_string(),
519            security_type,
520            credential_type: None,
521            credential: None,
522        };
523        let expected_credential = None;
524        let result = args.try_parse_credential().expect("error occurred parsing credential");
525        assert_eq!(result, expected_credential);
526    }
527
528    /// Test that if the credential argument is provided as an empty string, and security type
529    /// is none, the credential should be Credential::None
530    #[fuchsia::test]
531    fn test_try_parse_credential_open_network_credential_empty_string() {
532        let args = RemoveArgs {
533            ssid: "some_ssid".to_string(),
534            security_type: Some(SecurityTypeArg::None),
535            credential_type: None,
536            credential: Some("".to_string()),
537        };
538        let expected_credential = Some(wlan_policy::Credential::None(wlan_policy::Empty));
539        let result = args.try_parse_credential().expect("error occurred parsing credential");
540        assert_eq!(result, expected_credential);
541    }
542
543    /// Test that if a password is provided without a type, the credential type is password by
544    /// default. The security type should not have a default value.
545    #[fuchsia::test]
546    fn test_try_parse_credential_with_no_type() {
547        let password = "somepassword";
548        let args = RemoveArgs {
549            ssid: "some_ssid".to_string(),
550            security_type: None,
551            credential_type: None,
552            credential: Some(password.to_string()),
553        };
554        let expected_credential =
555            Some(wlan_policy::Credential::Password(password.as_bytes().to_vec()));
556        let result = args.try_parse_credential().expect("error occurred parsing credential");
557        assert_eq!(result, expected_credential);
558        assert_eq!(args.parse_security(), None);
559    }
560
561    /// Test that if the credential type and value are provided, they are used without error. It
562    /// does not matter what the security type is.
563    #[fuchsia::test]
564    fn test_try_parse_credential_exact_args_provided() {
565        let psk = "123456789ABCDEF123456789ABCDEF123456789ABCDEF123456789ABCDEF1234";
566        let args = RemoveArgs {
567            ssid: "some_ssid".to_string(),
568            security_type: Some(SecurityTypeArg::None),
569            credential_type: Some(CredentialTypeArg::Psk),
570            credential: Some(psk.to_string()),
571        };
572        let expected_credential = Some(wlan_policy::Credential::Psk(hex::decode(psk).unwrap()));
573        let result = args.try_parse_credential().expect("error occurred parsing credential");
574        assert_eq!(result, expected_credential);
575    }
576
577    /// Test that an error occurs when the credential type is provided without a value and the
578    /// type is not None.
579    #[fuchsia::test]
580    fn test_try_parse_credential_password_with_no_value_is_err() {
581        let args = RemoveArgs {
582            ssid: "some_ssid".to_string(),
583            security_type: Some(SecurityTypeArg::Wpa2),
584            credential_type: Some(CredentialTypeArg::Password),
585            credential: None,
586        };
587        args.try_parse_credential().expect_err("an error should occur parsing this credential");
588    }
589}