my_public_ip 0.1.0

Crate for getting your machine's public IP address
Documentation
use std::net::{IpAddr};

use lazy_static::lazy_static;
use rand::{thread_rng, Rng};
use trust_dns_resolver::Resolver;
use trust_dns_resolver::config::{ResolverConfig, ResolverOpts, NameServerConfigGroup};
use curl::easy::{Easy2, Handler, WriteError};

// TODO make dns resolution a feature

#[derive(Debug)]
pub enum IpResolveError {
    /// Library was not able to determine public IP for some reason
    /// TODO make errors more specific
    UnResolvableError { message: String }
}

struct Collector(Vec<u8>);

impl Handler for Collector {
    fn write(&mut self, data: &[u8]) -> Result<usize, WriteError> {
        self.0.extend_from_slice(data);
        Ok(data.len())
    }
}

static OPENDNS_MYIP: &str = "myip.opendns.com.";

lazy_static! {
    static ref OPENDNS_RESOLVER: Resolver = {
        let ips: Vec<IpAddr> = vec![
            "208.67.222.222",
            "208.67.220.220",
            "208.67.222.220",
            "208.67.220.222",
            "2620:0:ccc::2",
            "2620:0:ccd::2"
        ]
        .iter()
        .map(|addr_str| { addr_str.parse::<IpAddr>().unwrap() })
        .collect();

        let config = ResolverConfig::from_parts(
            None,
            vec![],
            NameServerConfigGroup::from_ips_clear(&ips, 53)
        );

        Resolver::new(
            config,
            ResolverOpts::default()
        ).unwrap()
    };

    /// HTTP endpoints providing public IP resolution
    static ref HTTP_ENDPOINTS: Vec<&'static str> = vec![
        "https://diagnostic.opendns.com/myip",
        "https://tnx.nl/ip",
        "https://icanhazip.com/"
    ];
}

fn dns_query() ->  Option<IpAddr> {
    let response = OPENDNS_RESOLVER.lookup_ip(&OPENDNS_MYIP).unwrap();
    response.iter().next()
}

fn http_query(endpoint: &str) -> Option<IpAddr> {
    let mut easy = Easy2::new(Collector(Vec::new()));
    easy.get(true).unwrap();
    easy.url(endpoint).unwrap();
    easy.perform().unwrap();
    let response_code = easy.response_code().unwrap();
    if response_code != 200 {
        return None
    }

    let contents = easy.get_ref();
    match String::from_utf8_lossy(&contents.0).trim().parse::<IpAddr>() {
        Ok(addr) => Some(addr),
        _ => None
    }
}

/// Try to resolve public IP address of the machine this code is run on
///
/// As first various HTTP endpoints are tried. If all HTTP endpoints fail
/// a DNS request is made to opendns.com servers
///
/// # E.g.
/// ```
/// let ip: ::std::net::IpAddr = my_public_ip::resolve().unwrap();
/// ```
pub fn resolve() -> Result<IpAddr, IpResolveError> {
    let mut rng = thread_rng();
    let mut endpoints = HTTP_ENDPOINTS.clone();
    rng.shuffle(&mut endpoints);

    for endpoint in endpoints {
        match http_query(endpoint) {
            Some(addr) => return Ok(addr),
            None => continue
        }
    }

    dns_query().ok_or(IpResolveError::UnResolvableError{ message: "Cannot resolve IP neither using HTTP nor DNS".to_owned() })
}

#[cfg(test)]
mod tests {
    use super::resolve;

    #[test]
    fn test_resolve() {
        let ip = resolve().unwrap();
        eprintln!("{:?}", ip);
        assert!(ip.is_ipv4() || ip.is_ipv6());

        #[cfg(nightly)]
        assert!(ip.is_global())
    }
}