Skip to main content

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