use std::io::Read;
use regex;
use reqwest;
use serde_json;
use credentials;
use error::{DokError, DokResult};
const REGISTRY_API_VERSION: &'static str = "v2";
#[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) => {
println!("{}", e);
println!("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::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()),
}
}