ipgeo 0.1.8

A CLI tool that finds the location and other information of IP addresses or DNS addresses.
use clap::{crate_version, App, Arg, ArgMatches};
use colored::Colorize;
use ipgeolocate::{Locator, Service};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use dns_lookup::lookup_host;
mod tools;

#[async_std::main]
async fn main() {
    let matches = App::new("ipgeo")
        .version(crate_version!())
        .author("Grant Handy <grantshandy@gmail.com>")
        .about("Finds IP Information")
        .arg(
            Arg::with_name("ADDRESS")
            .help("What IP or DNS address to look up, if none are selected then your network IP address will be chosen")
            .required(false)
            .index(1)
        )
        .arg(
            Arg::with_name("method")
            .long("method")
            .short("m")
            .help("Choose Geolocation API, if not set it defaults to ipapi.")
            .required(false)
            .takes_value(true)
            .value_name("SERVICE")
            .possible_values(&["ipwhois", "ipapi", "ipapico", "freegeoip"])
        )
        .arg(
            Arg::with_name("all")
            .long("all")
            .short("a")
            .help("Print all available information")
            .required(false)
            .takes_value(false)
            .conflicts_with("fields")
        )
        .arg(
            Arg::with_name("silent")
            .long("silent")
            .short("s")
            .help("Run without extra output")
            .required(false)
            .takes_value(false)
        )
        .arg(
            Arg::with_name("horizontal")
            .long("horizontal")
            .help("Print fields horizontally.")
            .required(false)
            .takes_value(false)
        )
        .arg(
            Arg::with_name("verbose")
            .long("verbose")
            .short("v")
            .help("Run with verbose output")
            .required(false)
            .takes_value(false)
            .conflicts_with("silent")
        )
        .arg(
            Arg::with_name("fields")
            .long("fields")
            .short("f")
            .help("Choose what fields to print about the IP address.")
            .takes_value(true)
            .value_name("FIELDS")
            .required(false)
            .multiple(true)
            .possible_values(&["ip", "latitude", "longitude", "city", "region", "country", "timezone", "method", "dns"])
        )
        .get_matches();

    // Right now clap doesn't really allow me to do this, so here's a bodge solution...
    // I'm sure there's a simpler way to do this with '&&' or '|' in my if statements, but this works well so I'll keep it how it is.
    // Using colored I can successfully imitate the error messages generated by clap. This means that this error looks the same as the other ones.
    if matches.is_present("horizontal") {
        if !matches.is_present("fields") && !matches.is_present("all") {
            eprintln!(
                "{} The argument '{}' requires '{}' or '{}'\n",
                "error:".red().bold(),
                "--horizontal".yellow(),
                "--fields".yellow(),
                "--all".yellow()
            );
            eprintln!("For more information try {}", "--help".green());
            std::process::exit(1);
        };
    };

    // Get IP target from clap, if the user didn't specify anything then use get_network_ip() to find your network IP instead.
    let mut ip: String = match matches.value_of("ADDRESS") {
        Some(value) => value.to_string(),
        None => {
                let i = tools::get_network_ip().await;

                if matches.is_present("verbose") {
                    println!("no IP address set, using network IP address \"{}\"", i);
                };
                
                i
        }
    };

    let address = ip.clone();
    let mut is_dns = false;

    // Parse the IP address as an IPv4 or IPv6 to find out which one it is.
    if ip.parse::<Ipv4Addr>().is_ok() {
        if matches.is_present("verbose") {
            println!("detected IPv4 address")
        };
    } else if ip.parse::<Ipv6Addr>().is_ok() {
        if matches.is_present("verbose") {
            println!("detected IPv6 address")
        };
    } else {
        // If it isn't either one of them then look it up as a DNS address.
        if matches.is_present("verbose") {
            println!(
                "neither ipv4 or ipv6 IP address found, looking \"{}\" up as a DNS address",
                ip
            );
        };
        match lookup_host(&ip) {
            Ok(data) => {
                is_dns = true;
                if matches.is_present("verbose") {
                    println!("DNS lookup for {} successful", ip);
                };

                // Go through the detected IP addresses and set IP as the first one it finds.
                for foo in data {
                    if foo.is_ipv4() | foo.is_ipv6() {
                        // I know this is a horrible way to do it but seriously how else should it be done?!
                        // There are multiple IP addresses set to a DNS address, which is good but not for me! AGH!
                        // This means that I'm just getting the first IP address that I can find and just using that crap.
                        ip = foo.to_string();
                        continue;
                    };
                }
            }
            Err(error) => {
                // If it isn't an IPv4, IPv6, or DNS address, give the user some instructions on why they're an idiot.
                eprintln!("can't find any information for \"{}\"", ip);
                eprintln!("this probably means that the value for <ADDRESS> is not an IP address or DNS address");
                eprintln!("DNS lookup error: {}", error);
                std::process::exit(1);
            }
        };
    };

    // Set the method variable. If the user hasn't specified anything then just set it as ipapi.
    // ipapi is probably the best in most situations because it has pretty reliable results and has minute by minute request limits so its pretty hard to break in a script.
    let service: Service = match matches.value_of("method") {
        Some(value) => tools::match_method(value),
        None => Service::IpApi,
    };

    // Once we have all the variables set, we can actually run the Locator and then print the results using print_data().
    match Locator::get(&ip, service).await {
        Ok(ip) => print_data(service, matches.clone(), ip, is_dns, &address),
        Err(error) => eprintln!("error getting location data: {}", error),
    };
}

// I'm particularly proud of this function.
// It goes through all the fields set from the user and then print each of the variables individually.
fn print_data(service: Service, app: ArgMatches, ip: Locator, is_dns: bool, address: &str) {
    if app.is_present("fields") {
        match app.values_of("fields") {
            Some(general_data) => {
                for data_type in general_data {
                    let data_value = match data_type {
                        "dns" => {
                            if !is_dns {
                                tools::get_dns(ip.ip.clone().parse::<IpAddr>().unwrap())
                            } else {
                                address.to_string()
                            }
                        },
                        "city" => ip.city.clone(),
                        "country" => ip.country.clone(),
                        "ip" => ip.ip.clone(),
                        "latitude" => ip.latitude.clone(),
                        "longitude" => ip.longitude.clone(),
                        "region" => ip.region.clone(),
                        "timezone" => ip.timezone.clone(),
                        "method" => service.to_string().clone(),
                        &_ => String::from("NONE"),
                    };

                    print_field(&data_value, data_type, &app);
                }
            }
            None => eprintln!("field interpretation error, unexpected!"),
        };
    } else {
        if app.is_present("all") {
            print_all_fields(&app, &ip, service, is_dns, address);
        } else {
            // If the user didn't specify any fields, just print it kinda pretty like this:
            println!("{} - {} ({})", ip.ip, ip.city, ip.country);
        };
    };

    if app.is_present("horizontal") {
        print!("\n");
    };
}

fn print_all_fields(app: &ArgMatches, ip: &Locator, service: Service, is_dns: bool, address: &str) {
    let data_types = vec![
        "ip",
        "dns",
        "city",
        "region",
        "country",
        "latitude",
        "longitude",
        "timezone",
        "method",
    ];

    for data_type in data_types {
        let data_value = match data_type {
            "dns" => {
                if !is_dns {
                    tools::get_dns(ip.ip.clone().parse::<IpAddr>().unwrap())
                } else {
                    address.to_string()
                }
            },
            "city" => ip.city.clone(),
            "country" => ip.country.clone(),
            "ip" => ip.ip.clone(),
            "latitude" => ip.latitude.clone(),
            "longitude" => ip.longitude.clone(),
            "region" => ip.region.clone(),
            "timezone" => ip.timezone.clone(),
            "method" => service.to_string().clone(),
            &_ => String::from("NONE"),
        };

        print_field(&data_value, data_type, &app);
    }
}

fn print_field(data_value: &str, data_type: &str, app: &ArgMatches) {
    if app.is_present("silent") {
        if app.is_present("horizontal") {
            print!("{} ", data_value);
        } else {
            println!("{}", data_value);
        };
    } else {
        if app.is_present("horizontal") {
            print!("{}: {}, ", data_type, data_value);
        } else {
            println!("{}: {}", data_type, data_value);
        };
    };
}