blob: a99c0dedc8a8b55510ba5255140d8534dfc0ee86 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::opts::*;
use crate::printer::Printer;
use anyhow::{format_err, Context as _, Error};
use fidl::endpoints::{Proxy, ServiceMarker};
use fidl_fuchsia_net::{Ipv4Address, Ipv6Address, MacAddress};
use fidl_fuchsia_overnet::{Peer, ServiceConsumerMarker, ServiceConsumerProxy};
use fidl_fuchsia_overnet_protocol::NodeId;
use fidl_fuchsia_router_config::{
Credentials, FilterAction, FilterRule, FlowSelector, Id, L2tp, LanProperties, PortRange, Pppoe,
Pptp, Protocol, RouterAdminMarker, RouterAdminProxy, RouterStateMarker, RouterStateProxy,
SecurityFeatures, WanConnection, WanProperties,
};
use fuchsia_async::{self as fasync};
use fuchsia_component::client::connect_to_service;
use fuchsia_syslog as syslog;
use fuchsia_zircon::{self as zx};
use itertools::Itertools;
use std::convert::TryInto;
use std::io::Write;
use std::net::IpAddr;
use std::str;
/// Creates a `router_config::PortRange` from a port range string.
fn make_port_range(input_range: &str) -> Result<PortRange, Error> {
let port_range = input_range
.split('-')
.map(|s| s.parse::<u16>())
.filter_map(Result::ok)
.collect::<Vec<u16>>();
if port_range.len() != 2 {
return Err(format_err!(
"Invalid port range! Range is: 1 to 65535, with format: <from>-<to>"
));
}
if port_range[0] > port_range[1] {
return Err(format_err!(
"End of range must be less than or equal to start: {}-{}",
port_range[0],
port_range[1]
));
}
Ok(PortRange { from: port_range[0], to: port_range[1] })
}
/// Takes a string of port ranges as input, and produces a vector of `PortRange`'s as its output.
/// The format of `input_ranges` is: "<from>-<to>,<from>-<to>,...".
fn to_port_ranges(input_ranges: &str) -> Result<Vec<PortRange>, Error> {
let (oks, fails): (Vec<_>, Vec<_>) =
input_ranges.split(',').map(|range| make_port_range(range)).partition(Result::is_ok);
if !fails.is_empty() {
let errors: Vec<_> = fails.into_iter().map(Result::unwrap_err).collect();
return Err(format_err!("Invalid port range: {:?}", errors));
}
Ok(oks.into_iter().map(Result::unwrap).collect::<Vec<PortRange>>())
}
pub fn connect() -> Result<(RouterAdminProxy, RouterStateProxy), Error> {
let router_admin = connect_to_service::<RouterAdminMarker>()
.context("failed to connect to network manager admin interface")?;
let router_state = connect_to_service::<RouterStateMarker>()
.context("failed to connect to network manager interface")?;
Ok((router_admin, router_state))
}
fn connect_overnet_node(
svc: &ServiceConsumerProxy,
name: &str,
node: &mut NodeId,
) -> Result<fasync::Channel, Error> {
let (ch0, ch1) = zx::Channel::create()?;
svc.connect_to_service(node, name, ch0)?;
fasync::Channel::from_channel(ch1).map_err(|e| e.into())
}
fn supports_network_manager(peer: &Peer) -> bool {
match peer.description.services {
None => false,
Some(ref services) => [RouterAdminMarker::NAME, RouterStateMarker::NAME]
.iter()
.map(|svc| services.contains(&svc.to_string()))
.all(|v| v),
}
}
pub async fn connect_overnet() -> Result<(RouterAdminProxy, RouterStateProxy), Error> {
let svc = connect_to_service::<ServiceConsumerMarker>()?;
syslog::fx_log_info!("looking for overnet peers...");
loop {
for mut peer in svc.list_peers().await? {
if !supports_network_manager(&peer) {
continue;
}
match (
connect_overnet_node(&svc, &RouterAdminMarker::NAME, &mut peer.id),
connect_overnet_node(&svc, &RouterStateMarker::NAME, &mut peer.id),
) {
(Err(_), _) | (_, Err(_)) => {
continue;
}
(Ok(router_admin_channel), Ok(router_state_channel)) => {
syslog::fx_log_info!("connected to peer {:?}", peer.id.id);
return Ok((
RouterAdminProxy::from_channel(router_admin_channel),
RouterStateProxy::from_channel(router_state_channel),
));
}
}
}
}
}
pub fn make_cmd(command: &str) -> Result<Command, Error> {
let mut args = command.split_whitespace().collect::<Vec<&str>>();
// `from_*` methods silently drop the first element of the vector, since that is /usually/
// the program name.
args.insert(0, "cli_test");
let opts = Opt::from_iter_safe(args.into_iter())?;
Ok(opts.cmd)
}
pub async fn run_cmd<T: Write>(
cmd: Command,
router_admin: RouterAdminProxy,
router_state: RouterStateProxy,
printer: &mut Printer<T>,
) -> Result<(), Error> {
match cmd {
Command::ADD(cmd) => do_add(cmd, router_admin, printer).await,
Command::SHOW(cmd) => do_show(cmd, router_state, printer).await,
Command::SET(cmd) => do_set(cmd, router_admin, printer).await,
Command::REMOVE(cmd) => do_remove(cmd, router_admin, printer).await,
}
}
/// do_add function handles subcommands for adding WAN and LAN interfaces
async fn do_add<T: Write>(
cmd: Add,
router_admin: RouterAdminProxy,
printer: &mut Printer<T>,
) -> Result<(), Error> {
match cmd {
Add::Wan { name, ports, vlan } => {
let response = match vlan {
None => router_admin
.create_wan(&name, 0, &ports)
.await
.context("error getting response")?,
Some(vlan) => router_admin
.create_wan(&name, vlan, &ports)
.await
.context("error getting response")?,
};
printer.println(format!("Response: {:?}", response));
Ok(())
}
Add::Lan { name, vlan, ports } => {
let response = match vlan {
None => router_admin
.create_lan(&name, 0, &ports)
.await
.context("error getting response")?,
Some(vlan) => router_admin
.create_lan(&name, vlan, &ports)
.await
.context("error getting response")?,
};
printer.println(format!("Response: {:?}", response));
Ok(())
}
}
}
/// do_remove function handles subcommands for removing LAN and WAN interfaces
async fn do_remove<T: Write>(
cmd: Remove,
router_admin: RouterAdminProxy,
printer: &mut Printer<T>,
) -> Result<(), Error> {
match cmd {
Remove::Wan { mut wan_id } => {
let response =
router_admin.remove_wan(&mut wan_id).await.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
Remove::Lan { mut lan_id } => {
let response =
router_admin.remove_lan(&mut lan_id).await.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
}
}
/// do_show function handles subcommands for getting interfaces, ports and configuration
async fn do_show<T: Write>(
cmd: Show,
router_state: RouterStateProxy,
printer: &mut Printer<T>,
) -> Result<(), Error> {
match cmd {
Show::Wan { mut wan_id } => {
let response =
router_state.get_wan(&mut wan_id).await.context("error getting response")?;
printer.println(format!("{:?}", response));
Ok(())
}
Show::Lan { mut lan_id } => {
let response =
router_state.get_lan(&mut lan_id).await.context("error getting response")?;
printer.println(format!("{:?}", response));
Ok(())
}
Show::WanConfig { mut wan_id } => {
let response = router_state
.get_wan_properties(&mut wan_id)
.await
.context("error getting response")?;
printer.println(format!("{:?} Response: {:?}", wan_id, response));
Ok(())
}
Show::Port { port } => {
let response = router_state.get_port(port).await.context("error getting response")?;
printer.println(format!("{:?} Response: {:?}", port, response));
Ok(())
}
Show::LanConfig { mut lan_id } => {
let response = router_state
.get_lan_properties(&mut lan_id)
.await
.context("error getting response")?;
printer.println(format!("{:?} Response: {:?}", lan_id, response));
Ok(())
}
Show::DnsConfig {} => {
let response =
router_state.get_dns_resolver().await.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
Show::DhcpConfig { lan_id } => {
printer.println(format!("{:?}", lan_id));
Ok(())
}
Show::ForwardState { lan_id } => {
printer.println(format!("{:?}", lan_id));
Ok(())
}
Show::FilterState {} => {
let response = router_state.get_filters().await.context("error getting response")?;
printer.println(format!("{} filters installed", response.len()));
printer.println(format!("{:?}", response));
Ok(())
}
Show::Wans {} => {
let response = router_state
.get_wans()
.await
.context("error getting response")?
.into_iter()
.map(|mut l| {
l.port_ids.as_mut().map(|x| x.sort());
l
})
.sorted_by_key(|a| a.element.map_or(0, |id| u128::from_ne_bytes(id.uuid)))
.collect_vec();
printer.println(format!("Response: {:?}", response));
Ok(())
}
Show::Lans {} => {
let response = router_state
.get_lans()
.await
.context("error getting response")?
.into_iter()
.map(|mut l| {
l.port_ids.as_mut().map(|x| x.sort());
l
})
.sorted_by_key(|a| a.element.map_or(0, |id| u128::from_ne_bytes(id.uuid)))
.collect_vec();
printer.println(format!("Response: {:?}", response));
Ok(())
}
Show::WanPorts { mut wan_id } => {
let mut response =
router_state.get_wan_ports(&mut wan_id).await.context("error getting response")?;
response.0.sort();
printer.println(format!("Response: {:?}", response));
Ok(())
}
Show::LanPorts { mut lan_id } => {
let mut response =
router_state.get_lan_ports(&mut lan_id).await.context("error getting response")?;
response.0.sort();
printer.println(format!("Response: {:?}", response));
Ok(())
}
// TODO: Implement pretty_print
Show::Ports {} => {
let mut response = router_state.get_ports().await.context("error getting response")?;
response.sort_by_key(|a| a.path.clone());
for port in response {
printer.println(format!("{:?}", port));
}
Ok(())
}
Show::Routes {} => {
let response = router_state.get_routes().await.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
Show::Security {} => {
let response =
router_state.get_security_features().await.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
}
}
/// do_set function handles several subcommands that are used for setting wan and lan properties
async fn do_set<T: Write>(
cmd: Set,
router_admin: RouterAdminProxy,
printer: &mut Printer<T>,
) -> Result<(), Error> {
// wan_properties contains all the wan related configs that are sent to the server
let mut wan_properties = WanProperties {
connection_type: None,
connection_parameters: None,
address_method: None,
address_v4: None,
gateway_v4: None,
connection_v6_mode: None,
address_v6: None,
gateway_v6: None,
hostname: None,
clone_mac: None,
mtu: None,
enable: None,
metric: None,
};
// lan_properties contains all the lan related configs that are sent to the server
let mut lan_properties = LanProperties {
address_v4: None,
enable_dhcp_server: None,
dhcp_config: None,
address_v6: None,
enable_dns_forwarder: None,
enable: None,
};
// This match statement populates the specified wan_properties field based on the subcommand
// and calls the appropriate FIDL interface with the requested configuration changes. It then
// receives the response and prints it for the CLI user
match cmd {
// TODO: Do the validation for "state" in opts.rs using enums
// This subcommand handles state change request for a specific the WAN interface
Set::WanState { mut wan_id, state } => {
// Populating the "wan_properties.enable" field based on the user input
wan_properties.enable = match state.as_ref() {
"up" => Some(true),
"down" => Some(false),
_ => return Err(format_err!("Please provide a valid state: \"up\" or \"down\"")),
};
// Sending the request to the FIDL server and receiving the response
// This can eventually be its own function with a "pretty print" feature
printer.println(format!("Sending: {:?}", wan_properties));
let response = router_admin
.set_wan_properties(&mut wan_id, wan_properties)
.await
.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
// TODO: Do the validation for "connection" in opts.rs using enums
// This subcommand sets the connection type for a specific WAN interface
Set::WanConnection { mut wan_id, connection, username, password, server, metric, mtu } => {
wan_properties.metric = metric;
wan_properties.mtu = mtu;
match connection {
Some(m) => {
// This match statement populates the appropriate fields for wan_properties
// based on the requested connection type
match m.as_ref() {
// Following handles the cases for direct connection configuration
"direct" => {
wan_properties.connection_type = Some(WanConnection::Direct);
}
// Handling pppoe connection configuration
"pppoe" => {
if username.is_none() {
return Err(format_err!(
"No username provided for PPPOE connection"
));
}
if password.is_none() {
return Err(format_err!(
"No password provided for PPPOE connection"
));
}
wan_properties.connection_type = Some(WanConnection::PpPoE);
let pppoe_config = Pppoe {
credentials: Some(Credentials { user: username, password }),
};
// Applying the configurations to the wan_properties data structure
wan_properties.connection_parameters =
Some(fidl_fuchsia_router_config::ConnectionParameters::Pppoe(
pppoe_config,
));
}
// Handling pptp connection configuration
"pptp" => {
if username.is_none() {
return Err(format_err!(
"No username provided for PPTP connection"
));
}
if password.is_none() {
return Err(format_err!(
"No password provided for PPTP connection"
));
}
if server.is_none() {
return Err(format_err!(
"No server IP provided for PPTP connection"
));
}
// Configuring connection type
wan_properties.connection_type = Some(WanConnection::Pptp);
// Wrapping the server ip address in the Ipv4Address structure
let server_ipv4 = Ipv4Address { addr: server.unwrap().octets() };
// Wrapping username and password in "Credentials" structure and
// constructing the Pptp structure
let pptp_config = Pptp {
credentials: Some(Credentials { user: username, password }),
server: Some(fidl_fuchsia_net::IpAddress::Ipv4(server_ipv4)),
};
wan_properties.connection_parameters = Some(
fidl_fuchsia_router_config::ConnectionParameters::Pptp(pptp_config),
);
}
// handling l2tp connection configuration
"l2tp" => {
if username.is_none() {
return Err(format_err!(
"No username provided for L2TP connection"
));
}
if password.is_none() {
return Err(format_err!(
"No password provided for L2TP connection"
));
}
if server.is_none() {
return Err(format_err!(
"No server IP provided for L2TP connection"
));
}
wan_properties.connection_type = Some(WanConnection::L2Tp);
let server_ipv4 = Ipv4Address { addr: server.unwrap().octets() };
let l2tp_config = L2tp {
credentials: Some(Credentials { user: username, password }),
server: Some(fidl_fuchsia_net::IpAddress::Ipv4(server_ipv4)),
};
wan_properties.connection_parameters = Some(
fidl_fuchsia_router_config::ConnectionParameters::L2tp(l2tp_config),
);
}
_ => {
return Err(format_err!(
"Please provide a valid connection type: direct, pppoe, pptp or l2tp"
))
}
}
}
None => {
if metric.is_none() && mtu.is_none() {
return Err(format_err!("Please either provide a valid connection type: direct/pppoe/pptp/l2tp or use an optional flag. For help, try \"set wan-connection --help\""));
}
}
}
printer.println(format!("Sending: {:?}", wan_properties));
let response = router_admin
.set_wan_properties(&mut wan_id, wan_properties)
.await
.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
// This subcommand sets the IP configuration, dhcp, manual, etc... and sets an optional hostname
Set::WanIp { mut wan_id, mode, ipv4, gateway, hostname } => {
match mode {
Some(m) => {
match m.as_ref() {
// handling DHCP IP configuration
"dhcp" => {
if ipv4.is_some() {
return Err(format_err!(
"Cannot provide IP address when enabling dhcp"
));
}
if gateway.is_some() {
return Err(format_err!(
"Cannot provide gateway IP aadress when enabling dhcp"
));
}
// Setting the address_method option of wan_properties to automatic
wan_properties.address_method =
Some(fidl_fuchsia_router_config::WanAddressMethod::Automatic);
}
// handling manual IP configuration
"manual" => {
// The ipv4 field is in CIDR notation form and it has to be split into an IP address and a prefix before being sent to server
let (ipv4_address, prefix_length) = match ipv4 {
Some(m) => (Ipv4Address { addr: m.address.octets() }, m.prefix),
None => {
return Err(format_err!(
"No IP address provided for manual config"
))
}
};
// Extracting the gateway IP from Ipv4Addr struct
let gateway_ipv4 = match gateway {
Some(m) => Ipv4Address { addr: m.octets() },
None => {
return Err(format_err!(
"No gateway IP address provided for manual config"
))
}
};
// Setting the address_method option of wan_properties to manual
wan_properties.address_method =
Some(fidl_fuchsia_router_config::WanAddressMethod::Manual);
// Populating "address_v4" using the extracted IP address and the prefix
wan_properties.address_v4 =
Some(fidl_fuchsia_router_config::CidrAddress {
address: Some(fidl_fuchsia_net::IpAddress::Ipv4(ipv4_address)),
prefix_length: Some(prefix_length),
});
// gateway_v4 is of type IpAddress::Ipv4, which is constructed using the gateway_ipv4
wan_properties.gateway_v4 =
Some(fidl_fuchsia_net::IpAddress::Ipv4(gateway_ipv4));
}
_ => {
return Err(format_err!(
"Please provide a valid IP configuration: dhcp or manual"
))
}
}
}
None => {
if hostname.is_none() {
return Err(format_err!("Please either provide a valid connection method: manual/dhcp or use an optional flag"));
}
}
}
// Setting the hostname in case this was provided
wan_properties.hostname = hostname;
printer.println(format!("Sending: {:?}", wan_properties));
let response = router_admin
.set_wan_properties(&mut wan_id, wan_properties)
.await
.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
// This subcommand sets the MAC address for a specific WAN interface
Set::WanCloneMac { mut wan_id, mac } => {
wan_properties.clone_mac = Some(MacAddress { octets: mac.to_array() });
printer.println(format!("Sending: {:?}", wan_properties));
let response = router_admin
.set_wan_properties(&mut wan_id, wan_properties)
.await
.context("error getting response")?;
printer.println(format!("{:?}", response));
Ok(())
}
// TODO: Do the validation for "state" in opts.rs using enums
// This subcommand handles state change request for a specific LAN interface
Set::LanState { mut lan_id, state } => {
// Populating the "enable" field based on the user input
match state.as_ref() {
"up" => {
lan_properties.enable = Some(true);
}
"down" => {
lan_properties.enable = Some(false);
}
_ => return Err(format_err!("Please provide a valid state: up or down")),
}
printer.println(format!("Sending: {:?} ID: {:?}", lan_properties, lan_id));
let response = router_admin
.set_lan_properties(&mut lan_id, lan_properties)
.await
.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
Set::LanIp { mut lan_id, ipv4 } => {
// The ipv4 field is in CIDR notation form and it has to be split into an IP address and a prefix
let (ipv4_address, prefix_length) = match ipv4 {
Some(m) => (Ipv4Address { addr: m.address.octets() }, m.prefix),
None => return Err(format_err!("No IP address provided for manual config")),
};
// Populating "address_v4" using the extracted IP address and the prefix
lan_properties.address_v4 = Some(fidl_fuchsia_router_config::CidrAddress {
address: Some(fidl_fuchsia_net::IpAddress::Ipv4(ipv4_address)),
prefix_length: Some(prefix_length),
});
printer.println(format!("Sending: {:?} ID: {:?}", lan_properties, lan_id));
let response = router_admin
.set_lan_properties(&mut lan_id, lan_properties)
.await
.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
// TODO: Do the validation for "state" in opts.rs using enums
Set::LanDhcp { mut lan_id, state, lease_time_sec, gateway } => {
// Populating the "enable_dhcp_server" field based on the user input
match state.as_ref() {
"up" => {
lan_properties.enable_dhcp_server = Some(true);
}
"down" => {
lan_properties.enable_dhcp_server = Some(false);
}
_ => return Err(format_err!("Please provide a valid state: up or down")),
}
// TODO: Populate these fields with the values received from the user
if lease_time_sec.is_some() {}
if gateway.is_some() {}
printer.println(format!("Sending: {:?} ID: {:?}", lan_properties, lan_id));
let response = router_admin
.set_lan_properties(&mut lan_id, lan_properties)
.await
.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
Set::DhcpConfig { lan_id, dhcp_config } => {
printer.println(format!("Not Implemented {:?}, {:?}", lan_id, dhcp_config));
Ok(())
}
Set::DnsConfig { server } => {
printer.println(format!("Sending: {:?}", server));
let servers = match server {
IpAddr::V4(address) => {
vec![fidl_fuchsia_net::IpAddress::Ipv4(Ipv4Address { addr: address.octets() })]
}
IpAddr::V6(address) => {
vec![fidl_fuchsia_net::IpAddress::Ipv6(Ipv6Address { addr: address.octets() })]
}
};
let mut config = fidl_fuchsia_router_config::DnsResolverConfig {
element: Id { uuid: [0; 16], version: 0 },
policy: fidl_fuchsia_router_config::DnsPolicy::NotSet,
search: fidl_fuchsia_router_config::DnsSearch { domain_name: None, servers },
};
let response = router_admin.set_dns_resolver(&mut config).await;
printer.println(format!("Response: {:?}", response));
Ok(())
}
Set::DnsForwarder { lan_id, enabled } => {
printer.println(format!("Not Implemented {:?}, {:?}", lan_id, enabled));
Ok(())
}
Set::Route { route } => {
printer.println(format!("Not Implemented {:?}", route));
Ok(())
}
Set::SecurityConfig { feature, enabled } => {
let mut security_features = SecurityFeatures {
pptp_passthru: None,
l2_tp_passthru: None,
ipsec_passthru: None,
rtsp_passthru: None,
h323_passthru: None,
sip_passthru: None,
allow_multicast: None,
nat: None,
firewall: None,
v6_firewall: None,
upnp: None,
drop_icmp_echo: None,
};
printer.println(format!(
"Setting security feature: {:?}, enabled: {:?}",
feature, enabled
));
match feature {
SecurityFeature::NAT => security_features.nat = Some(enabled),
// TODO(cgibson): Support more security features.
}
let response = router_admin
.set_security_features(security_features)
.await
.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
Set::PortForward { rule } => {
printer.println(format!("Not Implemented {:?}", rule));
Ok(())
}
Set::Filter {
action,
src_address,
src_port_range,
dst_address,
dst_port_range,
protocol,
} => {
if src_address.is_none() && dst_address.is_none() {
return Err(format_err!(
"Must provide at least a 'src_address' or a 'dst_address'"
));
}
let mut filter_rule = FilterRule {
element: Id { uuid: [0; 16], version: 0 },
action: match action.as_ref() {
"allow" => FilterAction::Allow,
"drop" => FilterAction::Drop,
_ => {
return Err(format_err!("Filter action must be: 'allow' or 'drop'"));
}
},
selector: FlowSelector {
src_address: match src_address {
Some(src_addr) => Some(src_addr.try_into()?),
None => None,
},
dst_address: match dst_address {
Some(dst_addr) => Some(dst_addr.try_into()?),
None => None,
},
src_ports: match src_port_range {
Some(ranges) => Some(to_port_ranges(&ranges)?),
None => None,
},
dst_ports: match dst_port_range {
Some(ranges) => Some(to_port_ranges(&ranges)?),
None => None,
},
protocol: match protocol {
Some(proto) => match proto.as_ref() {
"tcp" => Some(Protocol::Tcp),
"udp" => Some(Protocol::Udp),
"both" => Some(Protocol::Both),
_ => {
return Err(format_err!(
"Protocol choices are: 'tcp', 'udp', or 'both'"
))
}
},
None => Some(Protocol::Both),
},
},
};
let response = router_admin
.set_filter(&mut filter_rule)
.await
.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
Set::DeleteFilter { mut rule_id } => {
let response =
router_admin.delete_filter(&mut rule_id).await.context("error getting response")?;
printer.println(format!("Response: {:?}", response));
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_output_to_buffer() {
let mut printer = Printer::new(Vec::new());
let msg = "Hello, World!";
let newline = "\n";
printer.println("".into());
printer.println(msg.to_string());
printer.println(msg.to_string());
assert_eq!(
// TODO(cgibson): Figure out a way to add this as a helper method on `Printer`.
str::from_utf8(&printer.into_inner().unwrap()).unwrap().to_string(),
format!("{}{}{}{}{}", newline, msg, newline, msg, newline)
);
}
#[test]
fn test_to_port_ranges() {
let from = "22";
let to = "222";
let port_range = to_port_ranges(format!("{}-{}", from, to).as_ref()).unwrap()[0];
assert_eq!(port_range.from, from.parse::<u16>().unwrap());
assert_eq!(port_range.to, to.parse::<u16>().unwrap());
let from = "22";
let to = "22";
let port_range = to_port_ranges(format!("{}-{}", from, to).as_ref()).unwrap()[0];
assert_eq!(port_range.from, from.parse::<u16>().unwrap());
assert_eq!(port_range.to, to.parse::<u16>().unwrap());
let port_ranges = to_port_ranges("20-21,25-25").unwrap();
let errors = port_ranges.iter().filter(|port_range| {
if (port_range.from == 20 && port_range.to == 21)
|| (port_range.from == 25 && port_range.to == 25)
{
false
} else {
true
}
});
assert_eq!(errors.count(), 0);
let port_range = to_port_ranges("80,");
assert_eq!(port_range.is_err(), true);
let port_range = to_port_ranges("-");
assert_eq!(port_range.is_err(), true);
let from = "222";
let to = "22";
let port_range = to_port_ranges(format!("{}-{}", from, to).as_ref());
assert_eq!(port_range.is_err(), true);
let from = "abc";
let to = "22";
let port_range = to_port_ranges(format!("{}-{}", from, to).as_ref());
assert_eq!(port_range.is_err(), true);
}
}