dok 0.2.0

A Docker Registry CLI
use std::io::Read;

use regex::Regex;
use reqwest;
use serde_json;

use credentials;
use error::{DokError, DokResult};

const REGISTRY_API_VERSION: &'static str = "v2";

macro_rules! eprintln {
    ($($tt:tt)*) => {{
        use std::io::{Write, stderr};
        writeln!(&mut stderr(), $($tt)*).unwrap();
    }}
}

#[derive(Debug)]
pub struct Api {
    pub host: String,
    pub path: String,
    pub creds: Option<String>,

    client: reqwest::Client,
}

impl Api {
    pub fn new(raw: &str) -> DokResult<Self> {
        let cl = reqwest::Client::new()?;

        // Remove trailing slashes
        let raw = raw.trim_matches('/');
        let (host, path) = split_once(raw, '/');

        let creds = match credentials::get(&host) {
            Err(e) => {
                eprintln!("{}", e);
                eprintln!("Trying without credentials...");
                None
            }
            Ok(creds) => Some(creds),
        };

        Ok(Api {
               host: host,
               path: path,
               creds: creds,
               client: cl,
           })
    }

    // Builds a URL in this format:
    // http://HOST/REGISTRY_API_VERSION/BEFORE/PATH/AFTER
    //
    // Example:
    // http://docker.registry/v2/_catalog/ubuntu
    fn build_url(&self, before: Option<&str>, after: Option<&str>) -> String {
        let mut parts = vec!["http:/", &self.host, REGISTRY_API_VERSION];

        if let Some(bef) = before {
            parts.push(bef);
        }

        parts.push(&self.path);

        if let Some(aft) = after {
            parts.push(aft);
        }

        parts.join("/")
    }

    fn get_token(&self, realm: &str, service: &str, scope: &str) -> DokResult<String> {
        #[derive(Deserialize)]
        struct TokenResponse {
            token: String,
        }

        let mut req = self.client
            .get(&format!("{}?service={}&scope={}", realm, service, scope));

        if let Some(ref s) = self.creds {
            req = req.header(reqwest::header::Authorization(format!("Basic {}", s)));
        }

        let res = req.send()?;
        let parsed: TokenResponse = serde_json::from_reader(res)?;

        Ok(parsed.token)
    }

    fn try_request_token(&self, url: &str, basic_res: reqwest::Response) -> DokResult<String> {
        let unauth = DokError::new("No permission to access registry");
        let www_auth_bytes = match basic_res.headers().get_raw("www-authenticate") {
            None => return Err(unauth),
            Some(h) => h[0].to_vec(),
        };

        let www_auth = match String::from_utf8(www_auth_bytes) {
            Err(e) => return Err(DokError::new(&e.to_string())),
            Ok(s) => s,
        };

        let re = match Regex::new("^Bearer realm=\"(.+?)\",service=\"(.+?)\",scope=\"(.+?)\"$") {
            Err(e) => return Err(DokError::new(&e.to_string())),
            Ok(r) => r,
        };

        let (realm, service, scope) = match re.captures(&www_auth) {
            None => return Err(DokError::new("WWW-Authenticate did not contain enough info")),
            Some(caps) => {
                let realm = match caps.get(1) {
                    None => return Err(DokError::new("Regex failed: no realm?")),
                    Some(m) => m.as_str(),
                };
                let service = match caps.get(2) {
                    None => return Err(DokError::new("Regex failed: no service?")),
                    Some(m) => m.as_str(),
                };
                let scope = match caps.get(3) {
                    None => return Err(DokError::new("Regex failed: no scope?")),
                    Some(m) => m.as_str(),
                };
                (realm, service, scope)
            }
        };

        let token = self.get_token(realm, service, scope)?;

        let mut res = self.client
            .get(url)
            .header(reqwest::header::Authorization(format!("Bearer {}", token)))
            .send()?;

        match res.status() {
            &reqwest::StatusCode::Ok => {}
            _ => return Err(DokError::new(&format!("Request failed: {:?}", res))),
        }

        let mut body = String::new();
        res.read_to_string(&mut body)?;

        Ok(body)
    }

    fn try_request_basic(&self, url: &str) -> DokResult<String> {
        let mut req = self.client.get(url);

        if let Some(ref s) = self.creds {
            req = req.header(reqwest::header::Authorization(format!("Basic {}", s)));
        }

        let mut res = req.send()?;

        match res.status() {
            &reqwest::StatusCode::Ok => {}
            &reqwest::StatusCode::Unauthorized => return self.try_request_token(url, res),
            _ => return Err(DokError::new(&format!("Request failed: {:?}", res))),
        }

        let mut body = String::new();
        res.read_to_string(&mut body)?;

        Ok(body)
    }

    pub fn catalog(&self) -> DokResult<String> {
        let url = self.build_url(Some("_catalog"), None);
        let body = self.try_request_basic(&url)?;

        Ok(body)
    }

    pub fn tags(&self) -> DokResult<String> {
        let url = self.build_url(None, Some("tags/list"));
        let body = self.try_request_basic(&url)?;

        Ok(body)
    }
}

fn split_once(data: &str, split: char) -> (String, String) {
    match data.find(split) {
        None => (data.to_string(), "".to_string()),
        Some(i) => (data[..i].to_string(), data[(i + 1)..].to_string()),
    }
}