Skip to main content

innernet_shared/
prompts.rs

1use crate::{
2    interface_config::InterfaceInfo, peer::NewPeerInfo, AddCidrOpts, AddDeleteAssociationOpts,
3    AddPeerOpts, Association, Cidr, CidrContents, CidrTree, DeleteCidrOpts, EnableDisablePeerOpts,
4    Endpoint, Error, Hostname, IpNetExt, ListenPortOpts, OverridePeerEndpointOpts, Peer,
5    PeerContents, RenameCidrOpts, RenamePeerOpts,
6};
7use anyhow::{anyhow, bail};
8use colored::*;
9use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
10use innernet_publicip::Preference;
11use once_cell::sync::Lazy;
12use std::{
13    collections::BTreeMap,
14    fmt::{Debug, Display},
15    io,
16    net::{IpAddr, Ipv4Addr, SocketAddr},
17    str::FromStr,
18};
19
20pub static THEME: Lazy<ColorfulTheme> = Lazy::new(ColorfulTheme::default);
21
22pub fn ensure_interactive(prompt: &str) -> Result<(), io::Error> {
23    if atty::is(atty::Stream::Stdin) {
24        Ok(())
25    } else {
26        Err(io::Error::new(
27            io::ErrorKind::BrokenPipe,
28            format!("Prompt \"{prompt}\" failed because TTY isn't connected."),
29        ))
30    }
31}
32
33pub fn confirm(prompt: &str) -> Result<bool, dialoguer::Error> {
34    ensure_interactive(prompt)?;
35    Confirm::with_theme(&*THEME)
36        .wait_for_newline(true)
37        .with_prompt(prompt)
38        .default(false)
39        .interact()
40}
41
42pub fn select<'a, T: ToString>(
43    prompt: &str,
44    items: &'a [T],
45) -> Result<(usize, &'a T), dialoguer::Error> {
46    ensure_interactive(prompt)?;
47    let choice = Select::with_theme(&*THEME)
48        .with_prompt(prompt)
49        .items(items)
50        .interact()?;
51    Ok((choice, &items[choice]))
52}
53
54pub enum Prefill<T> {
55    Default(T),
56    Editable(String),
57    None,
58}
59
60pub fn input<T>(prompt: &str, prefill: Prefill<T>) -> Result<T, dialoguer::Error>
61where
62    T: Clone + FromStr + Display,
63    T::Err: Display + Debug,
64{
65    ensure_interactive(prompt)?;
66    let input = Input::with_theme(&*THEME);
67    match prefill {
68        Prefill::Default(value) => input.default(value),
69        Prefill::Editable(value) => input.with_initial_text(value),
70        _ => input,
71    }
72    .with_prompt(prompt)
73    // TODO(mbernat): workaround for https://github.com/console-rs/dialoguer/issues/330
74    .interact_text()
75}
76
77/// Bring up a prompt to create a new CIDR. Returns the peer request.
78pub fn add_cidr(cidrs: &[Cidr], request: &AddCidrOpts) -> Result<Option<CidrContents>, Error> {
79    let parent_cidr = if let Some(parent_name) = &request.parent {
80        cidrs
81            .iter()
82            .find(|cidr| &cidr.name == parent_name)
83            .ok_or_else(|| anyhow!("No parent CIDR with that name exists."))?
84    } else {
85        choose_cidr(cidrs, "Parent CIDR")?
86    };
87
88    let name = if let Some(name) = &request.name {
89        name.clone()
90    } else {
91        input("Name", Prefill::None)?
92    };
93
94    let cidr = if let Some(cidr) = request.cidr {
95        cidr
96    } else {
97        input("CIDR", Prefill::None)?
98    };
99
100    let cidr_request = CidrContents {
101        name: name.to_string(),
102        cidr,
103        parent: Some(parent_cidr.id),
104    };
105
106    Ok(
107        if request.yes || confirm(&format!("Create CIDR \"{}\"?", cidr_request.name))? {
108            Some(cidr_request)
109        } else {
110            None
111        },
112    )
113}
114
115/// Bring up a prompt to rename an existing CIDR. Returns the CIDR request.
116pub fn rename_cidr(
117    cidrs: &[Cidr],
118    args: &RenameCidrOpts,
119) -> Result<Option<(CidrContents, String)>, Error> {
120    let old_cidr = if let Some(name) = &args.name {
121        cidrs
122            .iter()
123            .find(|c| &c.name == name)
124            .ok_or_else(|| anyhow!("CIDR '{}' does not exist", name))?
125            .clone()
126    } else {
127        let (cidr_index, _) = select(
128            "CIDR to rename",
129            &cidrs.iter().map(|ep| ep.name.clone()).collect::<Vec<_>>(),
130        )?;
131        cidrs[cidr_index].clone()
132    };
133    let old_name = old_cidr.name.clone();
134    let new_name = if let Some(name) = &args.new_name {
135        name.clone()
136    } else {
137        input("New Name", Prefill::None)?
138    };
139
140    let mut new_cidr = old_cidr;
141    new_cidr.contents.name.clone_from(&new_name);
142
143    Ok(
144        if args.yes
145            || confirm(&format!(
146                "Rename CIDR {} to {}?",
147                old_name.yellow(),
148                new_name.yellow()
149            ))?
150        {
151            Some((new_cidr.contents, old_name))
152        } else {
153            None
154        },
155    )
156}
157
158/// Bring up a prompt to delete a CIDR. Returns the peer request.
159pub fn delete_cidr(cidrs: &[Cidr], peers: &[Peer], request: &DeleteCidrOpts) -> Result<i64, Error> {
160    let eligible_cidrs: Vec<_> = cidrs
161        .iter()
162        .filter(|cidr| {
163            !peers.iter().any(|peer| peer.contents.cidr_id == cidr.id) &&
164            !cidrs.iter().any(
165                |cidr2| matches!(cidr2.contents.parent, Some(parent_id) if parent_id == cidr.id)
166            )
167        })
168        .collect();
169    let cidr = if let Some(name) = &request.name {
170        cidrs
171            .iter()
172            .find(|cidr| &cidr.name == name)
173            .ok_or_else(|| anyhow!("CIDR {} doesn't exist or isn't eligible for deletion", name))?
174    } else {
175        select("Delete CIDR", &eligible_cidrs)?.1
176    };
177
178    if request.yes || confirm(&format!("Delete CIDR \"{}\"?", cidr.name))? {
179        Ok(cidr.id)
180    } else {
181        Err(anyhow!("Canceled"))
182    }
183}
184
185pub fn choose_cidr<'a>(cidrs: &'a [Cidr], text: &'static str) -> Result<&'a Cidr, Error> {
186    let eligible_cidrs: Vec<_> = cidrs
187        .iter()
188        .filter(|cidr| cidr.name != "innernet-server")
189        .collect();
190    Ok(select(text, &eligible_cidrs)?.1)
191}
192
193pub fn choose_association<'a>(
194    associations: &'a [Association],
195    cidrs: &'a [Cidr],
196    args: &AddDeleteAssociationOpts,
197) -> Result<&'a Association, Error> {
198    match (&args.cidr1, &args.cidr2) {
199        (Some(cidr1_name), Some(cidr2_name)) => {
200            let cidr1 = find_cidr(cidrs, cidr1_name)?;
201            let cidr2 = find_cidr(cidrs, cidr2_name)?;
202            associations
203                .iter()
204                .find(|association| {
205                    (association.cidr_id_1 == cidr1.id && association.cidr_id_2 == cidr2.id)
206                        || (association.cidr_id_1 == cidr2.id && association.cidr_id_2 == cidr1.id)
207                })
208                .ok_or_else(|| anyhow!("CIDR association does not exist"))
209        },
210        _ => {
211            let names: Vec<_> = associations
212                .iter()
213                .map(|association| {
214                    format!(
215                        "{}: {} <=> {}",
216                        association.id,
217                        &cidrs
218                            .iter()
219                            .find(|c| c.id == association.cidr_id_1)
220                            .unwrap()
221                            .name,
222                        &cidrs
223                            .iter()
224                            .find(|c| c.id == association.cidr_id_2)
225                            .unwrap()
226                            .name
227                    )
228                })
229                .collect();
230            let (index, _) = select("Association", &names)?;
231
232            Ok(&associations[index])
233        },
234    }
235}
236
237fn find_cidr<'a>(cidrs: &'a [Cidr], name: &str) -> Result<&'a Cidr, Error> {
238    cidrs
239        .iter()
240        .find(|c| c.name == name)
241        .ok_or_else(|| anyhow!("can't find cidr '{}'", name))
242}
243
244fn find_or_prompt_cidr<'a>(
245    cidrs: &'a [Cidr],
246    sub_opt: &Option<String>,
247    prompt: &'static str,
248) -> Result<&'a Cidr, Error> {
249    if let Some(name) = sub_opt {
250        find_cidr(cidrs, name)
251    } else {
252        choose_cidr(cidrs, prompt)
253    }
254}
255
256pub fn add_association<'a>(
257    cidrs: &'a [Cidr],
258    args: &AddDeleteAssociationOpts,
259) -> Result<Option<(&'a Cidr, &'a Cidr)>, Error> {
260    let cidr1 = find_or_prompt_cidr(cidrs, &args.cidr1, "First CIDR")?;
261    let cidr2 = find_or_prompt_cidr(cidrs, &args.cidr2, "Second CIDR")?;
262
263    Ok(
264        if args.yes
265            || confirm(&format!(
266                "Add association: {} <=> {}?",
267                cidr1.name.yellow().bold(),
268                cidr2.name.yellow().bold()
269            ))?
270        {
271            Some((cidr1, cidr2))
272        } else {
273            None
274        },
275    )
276}
277
278pub fn print_invitation_info(peer: &Peer, target_file_name: &str) {
279    println!(
280        "\nPeer \"{}\" added\n\
281         Peer invitation file written to {}\n\
282         Please send it to them securely (eg. via magic-wormhole) \
283         to bootstrap them onto the network.",
284        peer.name.bold(),
285        target_file_name.bold()
286    );
287}
288
289pub fn delete_association<'a>(
290    associations: &'a [Association],
291    cidrs: &'a [Cidr],
292    args: &AddDeleteAssociationOpts,
293) -> Result<Option<&'a Association>, Error> {
294    let association = choose_association(associations, cidrs, args)?;
295
296    Ok(
297        if args.yes || confirm(&format!("Delete association #{}?", association.id))? {
298            Some(association)
299        } else {
300            None
301        },
302    )
303}
304
305/// Bring up a prompt to gather information about a new peer. Returns [`NewPeerInfo`] and a path
306/// where the invite file should be saved.
307pub fn gather_new_peer_info(
308    peers: &[Peer],
309    cidr_tree: &CidrTree,
310    args: &AddPeerOpts,
311) -> Result<Option<(NewPeerInfo, String)>, Error> {
312    let leaves = cidr_tree.leaves();
313
314    let cidr = if let Some(parent_name) = &args.cidr {
315        leaves
316            .iter()
317            .find(|cidr| &cidr.name == parent_name)
318            .ok_or_else(|| anyhow!("No eligible CIDR with that name exists."))?
319    } else {
320        choose_cidr(&leaves[..], "Eligible CIDRs for peer")?
321    };
322
323    let mut available_ip = None;
324    let candidate_ips = cidr.hosts().filter(|ip| cidr.is_assignable(ip));
325    for ip in candidate_ips {
326        if !peers.iter().any(|peer| peer.ip == ip) {
327            available_ip = Some(ip);
328            break;
329        }
330    }
331
332    let available_ip = available_ip.expect("No IPs in this CIDR are avavilable");
333
334    let ip = if let Some(ip) = args.ip {
335        ip
336    } else if args.auto_ip {
337        available_ip
338    } else {
339        input("IP", Prefill::Default(available_ip))?
340    };
341
342    let name = if let Some(name) = &args.name {
343        name.clone()
344    } else {
345        input("Name", Prefill::None)?
346    };
347
348    let is_admin = if let Some(is_admin) = args.admin {
349        is_admin
350    } else {
351        confirm(&format!("Make {name} an admin?"))?
352    };
353
354    let invite_expires = if let Some(invite_expires) = &args.invite_expires {
355        invite_expires.clone()
356    } else {
357        input(
358            "Invite expires after",
359            Prefill::Default("14d".parse().map_err(|s: &str| anyhow!(s))?),
360        )?
361    };
362
363    let invite_save_path = if let Some(location) = &args.save_config {
364        location.clone()
365    } else {
366        input(
367            "Save peer invitation file to",
368            Prefill::Default(format!("{name}.toml")),
369        )?
370    };
371
372    if args.yes || confirm(&format!("Create peer {}?", name.yellow()))? {
373        let input = NewPeerInfo {
374            name,
375            ip,
376            cidr_id: cidr.id,
377            is_admin,
378            invite_expires,
379        };
380
381        Ok(Some((input, invite_save_path)))
382    } else {
383        Ok(None)
384    }
385}
386
387/// Bring up a prompt to rename an existing peer. Returns the peer request.
388pub fn rename_peer(
389    peers: &[Peer],
390    args: &RenamePeerOpts,
391) -> Result<Option<(PeerContents, Hostname)>, Error> {
392    let eligible_peers = peers
393        .iter()
394        .filter(|p| &*p.name != "innernet-server")
395        .collect::<Vec<_>>();
396    let old_peer = if let Some(name) = &args.name {
397        eligible_peers
398            .into_iter()
399            .find(|p| &p.name == name)
400            .ok_or_else(|| anyhow!("Peer '{}' does not exist", name))?
401            .clone()
402    } else {
403        let (peer_index, _) = select(
404            "Peer to rename",
405            &eligible_peers
406                .iter()
407                .map(|ep| ep.name.clone())
408                .collect::<Vec<_>>(),
409        )?;
410        eligible_peers[peer_index].clone()
411    };
412    let old_name = old_peer.name.clone();
413    let new_name = if let Some(name) = &args.new_name {
414        name.clone()
415    } else {
416        input("New Name", Prefill::None)?
417    };
418
419    let mut new_peer = old_peer;
420    new_peer.contents.name = new_name.clone();
421
422    Ok(
423        if args.yes
424            || confirm(&format!(
425                "Rename peer {} to {}?",
426                old_name.yellow(),
427                new_name.yellow()
428            ))?
429        {
430            Some((new_peer.contents, old_name))
431        } else {
432            None
433        },
434    )
435}
436
437/// Presents a selection and confirmation of eligible peers for either disabling or enabling,
438/// and returns back the ID of the selected peer.
439pub fn enable_or_disable_peer(
440    peers: &[Peer],
441    args: &EnableDisablePeerOpts,
442    enable: bool,
443) -> Result<Option<Peer>, Error> {
444    let enabled_peers: Vec<_> = peers
445        .iter()
446        .filter(|peer| enable && peer.is_disabled || !enable && !peer.is_disabled)
447        .collect();
448
449    let peer = if let Some(name) = &args.name {
450        enabled_peers
451            .into_iter()
452            .find(|p| &p.name == name)
453            .ok_or_else(|| anyhow!("Peer '{}' does not exist", name))?
454    } else {
455        let peer_selection: Vec<_> = enabled_peers
456            .iter()
457            .map(|peer| format!("{} ({})", &peer.name, &peer.ip))
458            .collect();
459        let (index, _) = select(
460            &format!("Peer to {}able", if enable { "en" } else { "dis" }),
461            &peer_selection,
462        )?;
463        enabled_peers[index]
464    };
465
466    Ok(
467        if args.yes
468            || confirm(&format!(
469                "{}able peer {}?",
470                if enable { "En" } else { "Dis" },
471                peer.name.yellow()
472            ))?
473        {
474            Some(peer.clone())
475        } else {
476            None
477        },
478    )
479}
480
481pub fn set_listen_port(
482    interface: &InterfaceInfo,
483    args: ListenPortOpts,
484) -> Result<Option<Option<u16>>, Error> {
485    let listen_port = if let Some(listen_port) = args.listen_port {
486        Some(listen_port)
487    } else if !args.unset {
488        Some(input(
489            "Listen port",
490            Prefill::Default(interface.listen_port.unwrap_or(51820)),
491        )?)
492    } else {
493        None
494    };
495
496    let confirmation = Confirm::with_theme(&*THEME)
497        .wait_for_newline(true)
498        .with_prompt(
499            &(if let Some(port) = &listen_port {
500                format!("Set listen port to {port}?")
501            } else {
502                "Unset and randomize listen port?".to_string()
503            }),
504        )
505        .default(false);
506
507    if listen_port == interface.listen_port {
508        println!("No change necessary - interface already has this setting.");
509        Ok(None)
510    } else if args.yes || confirmation.interact()? {
511        Ok(Some(listen_port))
512    } else {
513        Ok(None)
514    }
515}
516
517pub fn unspecified_ip_and_auto_detection_flow() -> Result<Option<IpAddr>, Error> {
518    if confirm_unspecified_ip_usage()? {
519        Ok(Some(IpAddr::V4(Ipv4Addr::UNSPECIFIED)))
520    } else {
521        ip_auto_detection_flow()
522    }
523}
524
525pub fn ip_auto_detection_flow() -> Result<Option<IpAddr>, Error> {
526    let ip_addr = if confirm_ip_auto_detection()? {
527        innernet_publicip::get_any(Preference::Ipv4)
528    } else {
529        None
530    };
531
532    Ok(ip_addr)
533}
534
535fn confirm_ip_auto_detection() -> Result<bool, Error> {
536    let answer = Confirm::with_theme(&*THEME)
537        .wait_for_newline(true)
538        .with_prompt("Auto-detect external endpoint IP address (via DNS query to 9.9.9.9)?")
539        .interact()?;
540
541    Ok(answer)
542}
543
544fn confirm_unspecified_ip_usage() -> Result<bool, Error> {
545    log::info!(
546        "Note: use unspecified IP address (all zeros) if you do not have a fixed global IP but the \
547         port is forwarded." 
548    );
549    let answer = Confirm::with_theme(&*THEME)
550        .wait_for_newline(true)
551        .with_prompt("Use an unspecified IP address and override just the port?")
552        .interact()?;
553
554    Ok(answer)
555}
556
557pub fn input_external_endpoint(
558    external_ip: Option<IpAddr>,
559    listen_port: u16,
560) -> Result<Endpoint, Error> {
561    let endpoint = input(
562        "External endpoint",
563        match external_ip {
564            Some(ip) => Prefill::Editable(SocketAddr::new(ip, listen_port).to_string()),
565            None => Prefill::None,
566        },
567    )?;
568
569    Ok(endpoint)
570}
571
572/// Bring up a prompt to override the endpoint for an existing peer.
573/// Returns the peer and desired endpoint.
574pub fn override_peer_endpoint_prompt(
575    peers: &[Peer],
576    peer_endpoint_overrides: &BTreeMap<IpAddr, Endpoint>,
577    args: &OverridePeerEndpointOpts,
578) -> Result<Option<(Peer, Option<Endpoint>)>, Error> {
579    let eligible_peers = peers
580        .iter()
581        .filter(|p| &*p.name != "innernet-server")
582        // If we're unsetting, filter eligible_peers to just be peers that have
583        // an override already set.
584        .filter(|p| !args.unset || peer_endpoint_overrides.contains_key(&p.ip))
585        .collect::<Vec<_>>();
586
587    let peer = if let Some(name) = &args.name {
588        let Some(peer) = eligible_peers.into_iter().find(|p| &p.name == name) else {
589            return if args.unset && peers.iter().find(|p| &p.name == name).is_some() {
590                log::info!("Peer '{name}' does not have an override set");
591                Ok(None)
592            } else {
593                Err(anyhow!("Peer '{name}' does not exist"))
594            };
595        };
596        peer.clone()
597    } else {
598        let message = if args.unset {
599            if eligible_peers.is_empty() {
600                bail!("No peers have an override endpoint set");
601            }
602
603            "Peer endpoint override to unset"
604        } else {
605            "Peer endpoint to override"
606        };
607
608        let (peer_index, _) = select(
609            message,
610            &eligible_peers
611                .iter()
612                .map(|ep| ep.name.clone())
613                .collect::<Vec<_>>(),
614        )?;
615        eligible_peers[peer_index].clone()
616    };
617
618    let endpoint: Option<Endpoint> = if args.unset {
619        None
620    } else if let Some(endpoint) = &args.endpoint {
621        Some(endpoint.clone())
622    } else {
623        Some(input("Endpoint", Prefill::None)?)
624    };
625
626    let confirm_msg = if let Some(endpoint) = &endpoint {
627        &format!(
628            "Override endpoint for peer {} ({}) to {}?",
629            peer.name.yellow(),
630            peer.ip,
631            endpoint,
632        )
633    } else {
634        &format!(
635            "Unset endpoint override for peer {} ({})?",
636            peer.name.yellow(),
637            peer.ip,
638        )
639    };
640
641    Ok(if args.yes || confirm(confirm_msg)? {
642        Some((peer, endpoint))
643    } else {
644        None
645    })
646}