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