use super::hyper;
use super::rustc_serialize;
use super::url;
use hyper::header;
use hyper_native_tls::NativeTlsClient;
use rustc_serialize::Decodable;
use url::Url;
use super::Error;
use std::env;
use std::io::Read;
const ACCESS_TOKEN_URL: &'static str = "https://github.com/login/oauth/access_token";
pub fn auth_callback(auth_code: String) -> Result<String, Error> {
OAuthClient::new(env::var("CLOWDER_GH_CLIENT_ID")?)
?
.set_secret(env::var("CLOWDER_GH_CLIENT_SECRET")?)
.set_oauth_code(auth_code)
.user()
.map(|u| u.username().to_string())
}
struct OAuthClient {
id: String,
http: hyper::Client,
secret: Option<String>,
oauth_code: Option<String>,
token: Option<String>,
user: Option<UserData>,
}
#[derive(Clone, Debug, RustcDecodable)]
pub struct UserData {
login: String,
avatar_url: Option<String>,
}
impl UserData {
pub fn username(&self) -> &str {
&self.login
}
}
impl OAuthClient {
pub fn new<S: Into<String>>(id: S) -> Result<OAuthClient, Error> {
let tls_connector = hyper::net::HttpsConnector::new(NativeTlsClient::new()?);
Ok(OAuthClient {
id: id.into(),
secret: None,
http: hyper::Client::with_connector(tls_connector),
oauth_code: None,
token: None,
user: None,
})
}
pub fn set_secret<S: Into<String>>(mut self, secret: S) -> Self {
self.secret = Some(secret.into());
self
}
pub fn set_oauth_code<S: Into<String>>(mut self, code: S) -> Self {
self.oauth_code = Some(code.into());
self
}
pub fn user(&mut self) -> Result<UserData, Error> {
if self.user.is_none() {
let url = Url::parse("https://api.github.com/user").expect("bad GitHub URI");
let user: UserData = self.query(url)?;
self.user = Some(user);
}
self.user
.as_ref()
.map(Clone::clone)
.map(Ok)
.expect("UNREACHABLE: self.user was just set; should not be None")
}
fn access_token(&mut self) -> Result<&str, Error> {
if let Some(ref token) = self.token {
Ok(token)
} else {
self.retrieve_access_token()
}
}
fn query<T, U>(&mut self, url: U) -> Result<T, Error>
where T: Decodable,
U: hyper::client::IntoUrl
{
let access_token = self.access_token()?.to_string();
let mut response = self.http
.get(url)
.header(header::Authorization(header::Bearer { token: access_token }))
.header(header::UserAgent(String::from("musec/clowder")))
.send()?;
let body = response_str(&mut response)?;
rustc_serialize::json::decode(&body)
.map_err(|err| format!["failed to parse {}: {}", body, err])
.map_err(Error::InvalidData)
}
fn retrieve_access_token(&mut self) -> Result<&str, Error> {
let secret = self.secret
.as_ref()
.ok_or(Error::AuthError(String::from("no GitHub client secret has been set")))?;
let code = self.oauth_code
.as_ref()
.ok_or(Error::AuthError(String::from("no OAuth code has been set")))?;
let form_data = url::form_urlencoded::Serializer::new(String::new())
.append_pair("client_id", &self.id)
.append_pair("client_secret", secret)
.append_pair("code", code)
.finish();
let mut response = try! {
self.http.post(url::Url::parse(ACCESS_TOKEN_URL).expect("malformed GitHub URI"))
.body(&form_data)
.send()
};
let token = response_str(&mut response).and_then(access_token)?;
self.token = Some(token);
Ok(self.token.as_ref().expect("Token was just set to Some(token), should not ever be None"))
}
}
fn access_token(form_body: String) -> Result<String, Error> {
for (key, value) in url::form_urlencoded::parse(form_body.as_bytes()) {
match &*key {
"access_token" => {
return Ok(value.to_string());
}
"error_description" => {
return Err(Error::InvalidData(value.to_string()));
}
_ => {}
};
}
Err(Error::InvalidData(format!["no access token in GitHub response: '{}'", form_body]))
}
fn response_str(response: &mut hyper::client::response::Response) -> Result<String, Error> {
let mut body = String::new();
response.read_to_string(&mut body)
.map(|_| body)
.map_err(|e| Error::InvalidData(format!["invalid response from GitHub: {}", e]))
}