#![allow(unused_assignments)]
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate serde_json;
#[macro_use]
extern crate log;
extern crate env_logger;
extern crate crypto;
extern crate base64;
extern crate url;
extern crate chrono;
extern crate rand;
extern crate curl;
extern crate webbrowser;
extern crate tiny_http;
extern crate hostname;
extern crate flickr_derive;
pub mod methods;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::iter::{FromIterator};
use rand::rngs::{OsRng};
use rand::{RngCore};
use std::str;
use std::fmt;
use std::error::{Error};
use std::io;
use crypto::hmac::{Hmac};
use crypto::mac::{Mac};
use crypto::sha1::{Sha1};
use crypto::digest::{Digest};
use url::{Url, form_urlencoded};
use chrono::prelude::{Utc};
use curl::easy::Easy;
use tiny_http::{Server, Response};
use hostname::{get_hostname};
static OAUTH_REQUEST_TOKEN_URL: &'static str =
"https://www.flickr.com/services/oauth/request_token";
static OAUTH_AUTHORIZATION_URL: &'static str =
"https://www.flickr.com/services/oauth/authorize";
static OAUTH_ACCESS_TOKEN_URL: &'static str =
"https://www.flickr.com/services/oauth/access_token";
static OAUTH_CALLBACK_URL_WITHOUT_PORT: &'static str =
"http://localhost";
static SERVICE_URL: &'static str =
"https://www.flickr.com/services/rest";
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FlickrAPI {
#[serde(default, skip_serializing, skip_deserializing)]
nonce_suffix: u128,
#[serde(default)]
flickr_api_key: String,
#[serde(default)]
flickr_api_secret: String,
#[serde(default)]
oauth_token: String,
#[serde(default)]
token_secret: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
user_nsid: Option<String>,
#[serde(default, skip_serializing, skip_deserializing)]
test_response: Option<String>,
}
impl FlickrAPI {
pub fn new(flickr_api_key: &str, flickr_api_secret: &str) -> FlickrAPI {
FlickrAPI {
nonce_suffix: 0u128,
flickr_api_key: flickr_api_key.into(),
flickr_api_secret: flickr_api_secret.into(),
oauth_token: String::new(),
token_secret: String::new(),
user_nsid: Option::None,
test_response: Option::None,
}
}
fn is_authenticated(&self) -> bool {
self.oauth_token != "" && self.token_secret != "" && self.nonce_suffix > 0
}
fn do_auth(&mut self) -> Result<(), FlickrError> {
if self.nonce_suffix == 0 {
self.nonce_suffix = (Utc::now().timestamp() as u128) << 64;
}
if self.test_response.is_some() {
self.oauth_token = "something".into();
self.token_secret = "something".into();
return Ok(());
}
match self.auth().oauth().check_token().perform() {
Ok(result) => {
if result.stat == "ok" {
if let Some(o) = result.oauth {
self.user_nsid = Some(o.user.nsid);
return Ok(());
}
} else {
warn!("Check token resulted status: {}", result.stat);
}
},
Err(e) => {
debug!("Failed to check token: {}", e);
}
}
self.oauth_token = String::new();
self.token_secret = String::new();
let port = 19753;
let addr = format!("{}:{}", "127.0.0.1", port);
let server = Server::http(&addr).unwrap();
debug!("Bound http server for OAUTH callback at http://{}", addr);
debug!("OAUTH step 1: Get a Request token");
{
let signed_url = make_signed_url(OAUTH_REQUEST_TOKEN_URL, [
("oauth_nonce", self.next_nonce()),
("oauth_timestamp", Utc::now().timestamp().to_string()),
("oauth_consumer_key", self.flickr_api_key.clone()),
("oauth_version", String::from("1.0")),
("oauth_callback", format!("{}:{}", OAUTH_CALLBACK_URL_WITHOUT_PORT, port)),
("oauth_signature_method", String::from("HMAC-SHA1")),
].iter().cloned().collect(),
&self.flickr_api_secret, &self.token_secret);
match rest_call(signed_url) {
Ok((content, content_type, _content_encoding)) => {
let mut result = HashMap::new();
if content_type == "text/plain" {
result = parse_hashmap(&content);
} else {
return Err(FlickrError::OauthFailed(format!("Unexpected content type: {}", content_type)));
}
if let Some(value) = result.get("oauth_callback_confirmed") {
if value == "true" {
self.oauth_token = result.get("oauth_token").unwrap_or(&String::new()).to_string();
self.token_secret = result.get("oauth_token_secret").unwrap().to_string();
debug!("OAUTH token: {}", self.oauth_token);
debug!("OAUTH token secret: {}", self.token_secret);
} else {
return Err(FlickrError::OauthFailed(format!("Unexpected oauth_callback_confirmed value: {}", value)));
}
} else if let Some(problem) = result.get("oauth_problem") {
return Err(FlickrError::OauthFailed(problem.to_string()));
} else {
}
},
Err(e) => { return Err(e); }
}
}
debug!("OAUTH step 2: Direct user to Flickr for Authorization");
let mut oauth_verifier = Option::None;
{
let signed_url = make_signed_url(OAUTH_AUTHORIZATION_URL, [
("oauth_token", self.oauth_token.clone()),
("perms", "delete".into()), ].iter().cloned().collect(),
&self.flickr_api_secret, &self.token_secret);
if webbrowser::open(&signed_url).is_ok() {
debug!("Web browser launched with url {}", signed_url);
let mut keep_serving = true;
for mut request in server.incoming_requests() {
match Url::parse(&format!("http://localhost:{}/{}", port, request.url())) {
Ok(url) => {
for (key, value) in url.query_pairs() {
if key == "oauth_verifier" {
oauth_verifier = Some(value.into());
debug!("Got OAUTH verifier: {}", oauth_verifier.clone().unwrap_or("???".into()));
keep_serving = false;
}
}
},
Err(e) => {
warn!("Url parse failed: {}", e);
}
}
let response = Response::from_string("You can close this page"); request.respond(response).unwrap_or(());
if !keep_serving { break; }
}
}
}
debug!("OAUTH step 3: Exchange the Request Token for an Access Token");
if let Some(ov) = oauth_verifier {
let signed_url = make_signed_url(OAUTH_ACCESS_TOKEN_URL, [
("oauth_nonce", self.next_nonce()),
("oauth_timestamp", Utc::now().timestamp().to_string()),
("oauth_verifier", ov),
("oauth_consumer_key", self.flickr_api_key.clone()),
("oauth_version", String::from("1.0")),
("oauth_token", self.oauth_token.clone()),
("oauth_signature_method", String::from("HMAC-SHA1")),
].iter().cloned().collect(),
&self.flickr_api_secret, &self.token_secret);
match rest_call(signed_url) {
Ok((content, content_type, _content_encoding)) => {
let mut result = HashMap::new();
if content_type == "text/plain" {
result = parse_hashmap(&content);
if let Some(user_nsid) = result.get("user_nsid") {
self.user_nsid = Some(user_nsid.clone());
} else {
return Err(FlickrError::OauthFailed("Parameter user_nsid not received".into()));
}
if let Some(oauth_token) = result.get("oauth_token") {
self.oauth_token = oauth_token.clone();
debug!("OAUTH token: {}", self.oauth_token);
} else {
return Err(FlickrError::OauthFailed("Parameter oauth_token not received".into()));
}
if let Some(oauth_token_secret) = result.get("oauth_token_secret") {
self.token_secret = oauth_token_secret.clone();
debug!("token secret: {}", self.token_secret);
} else {
return Err(FlickrError::OauthFailed("Parameter oauth_token_secret not received".into()));
}
} else {
return Err(FlickrError::OauthFailed(format!("Unexpected content type: {}", content_type)));
}
},
Err(e) => { return Err(e); }
}
} else {
return Err(FlickrError::OauthFailed("Parameter oauth_verifier not received".into()));
}
info!("Flickr authentication completed successfully for {}",
self.user_nsid.clone().unwrap_or("".into()));
return Ok(());
}
fn call_method(&mut self, method: &str, params: BTreeMap<&str, String>) -> Result<String, FlickrError> {
let mut params0: BTreeMap<&str, String> = [
("method", method.into()),
("format", "json".into()),
("nojsoncallback", "1".into()),
("api_key", self.flickr_api_key.to_string()),
("oauth_nonce", self.next_nonce()),
("oauth_timestamp", Utc::now().timestamp().to_string()),
("oauth_consumer_key", self.flickr_api_key.clone()),
("oauth_version", String::from("1.0")),
("oauth_token", self.oauth_token.clone()),
("oauth_signature_method", String::from("HMAC-SHA1")),
].iter().cloned().collect();
params0.extend(params);
let signed_url = make_signed_url(SERVICE_URL, params0,
&self.flickr_api_secret, &self.token_secret);
debug!("REST CALL: {}", signed_url);
if let Some(ref content) = self.test_response {
return Ok(content.clone());
} else {
match rest_call(signed_url) {
Ok((content, content_type, content_encoding)) => {
let ce = content_encoding.to_lowercase();
if ce != "" && ce != "charset=UTF-8" {
return Err(FlickrError::RestCallFailed(
format!("Unexpected content encoding: {}", content_encoding)));
}
if content_type == "application/json" {
return Ok(content);
} else if content_type == "text/plain" {
debug!("content: {}", content);
let h = parse_hashmap(&content.replace("&", "\n"));
let j = format!("{{ \"stat\": \"{}\" }}",
h.get("oauth_problem").unwrap_or(&String::new()));
return Ok(j);
} else {
return Err(FlickrError::RestCallFailed(
format!("Unexpected content type: {}", content_type)));
}
},
Err(e) => {
Err(e)
}
}
}
}
pub fn activity(&mut self) -> methods::activity::Builder {
return methods::activity::Builder::new(self);
}
fn auth(&mut self) -> methods::auth::Builder {
return methods::auth::Builder::new(self);
}
pub fn favorites(&mut self) -> methods::favorites::Builder {
return methods::favorites::Builder::new(self);
}
pub fn photos(&mut self) -> methods::photos::Builder {
return methods::photos::Builder::new(self);
}
fn next_nonce(&mut self) -> String {
let mut hasher = Sha1::new();
let hostname = get_hostname().unwrap_or("localhost".into());
hasher.input_str(&format!("{}.{:p}", hostname, self));
let quite_unique_thing = hasher.result_str();
self.nonce_suffix += 1;
return format!("{}.{}", quite_unique_thing, self.nonce_suffix.to_string());
}
}
fn make_signed_url(base_url: &str, params: BTreeMap<&str, String>, consumer_secret: &str, token_secret: &str) -> String {
let mut url_params = String::new();
for key in BTreeSet::from_iter(params.keys()) {
if url_params.len() > 0 {
url_params.push_str("&");
}
url_params.push_str(key);
url_params.push_str("=");
url_params.push_str(&url_encode(params.get(key).unwrap()));
}
let url = format!("{}?{}", base_url, url_params);
let signature_base_string =
format!("{}&{}&{}", "GET", url_encode(base_url), url_encode(&url_params));
let raw_key = &format!("{}&{}", &consumer_secret, &token_secret);
let key = base64::encode(&raw_key);
let digest = hmac_sha1_digest(&signature_base_string, Some(&key));
let url_final = format!("{}&oauth_signature={}", url, url_encode(&digest));
return url_final;
}
fn url_encode(s: &str) -> String {
String::from(&form_urlencoded::Serializer::new(String::new()).append_pair("", s).finish()[1..])
}
fn hmac_sha1_digest(text: &String, hmac_key_base64: Option<&str>) -> String {
let mut hmac_key = vec![0u8; 32];
if let Some(ref hmac_key_base64) = hmac_key_base64 {
hmac_key = base64::decode(hmac_key_base64).expect("Failed to parse base64 encoded HMAC key");
} else {
let mut gen = OsRng::new().ok().expect("Failed to get OS random generator");
gen.fill_bytes(hmac_key.as_mut_slice());
}
let mut hmac = Hmac::new(Sha1::new(), hmac_key.as_slice());
hmac.input(text.as_bytes());
let res = base64::encode(hmac.result().code());
return res;
}
fn rest_call(url: String) -> Result<(String, String, String), FlickrError> {
let mut dest = Vec::new();
let mut content_type = String::from("text/plain");
let mut content_encoding = String::from("");
{
let mut easy = Easy::new();
if let Err(e) = easy.url(&url) {
return Err(FlickrError::RestCallFailed(e.to_string()));
}
{
let mut transfer = easy.transfer();
transfer.header_function(|header| {
if let Ok((header, value)) = parse_key_value(str::from_utf8(header).unwrap(), ":", true) {
if header.to_lowercase() == "content-type" {
if let Ok((ct, enc)) = parse_key_value(&value, ";", true) {
content_type = ct;
content_encoding = enc;
} else {
content_type = value;
content_encoding = String::from("");
}
}
}
true
})?;
transfer.write_function(|data| {
dest.extend_from_slice(data);
Ok(data.len())
})?;
transfer.perform()?;
}
if let Ok(code) = easy.response_code() {
debug!("HTTP response code = {}", code);
if code >= 400 {
return Err(FlickrError::RestCallFailed(
format!("REST API responded with status code {}", code)));
}
}
}
let body = String::from_utf8(dest.clone()).unwrap();
debug!("{}", body);
return Ok((body, content_type, content_encoding));
}
fn parse_hashmap(doc: &str) -> HashMap<String, String> {
let mut result = HashMap::new();
for line in doc.split("\n") {
for field in line.split("&") {
if let Ok((key, value)) = parse_key_value(field, "=", true) {
result.insert(key, value);
}
}
}
return result;
}
fn parse_key_value(pair: &str, sep: &str, trim: bool) -> Result<(String, String), &'static str> {
if let Some(_) = pair.find(sep) {
let trimmed: String = {
if trim {
pair.replace("\n", "").replace("\r", "")
} else {
String::from(pair)
}
};
let key_value: Vec<&str> = trimmed.split(sep).collect();
if trim {
return Ok( (String::from(key_value[0].trim()), String::from(key_value[1].trim())) );
} else {
return Ok( (String::from(key_value[0]), String::from(key_value[1])) );
}
} else {
return Err("No separator found to parse a key pair");
}
}
#[derive(Debug)]
pub enum FlickrError {
Io{source: Box<io::Error>},
OauthFailed(String),
RestCallFailed(String),
}
impl From<std::io::Error> for FlickrError {
fn from(err: std::io::Error) -> FlickrError {
FlickrError::Io { source: Box::new(err) }
}
}
impl From<curl::Error> for FlickrError {
fn from(err: curl::Error) -> FlickrError {
FlickrError::RestCallFailed(format!("REST call failed: {}", err))
}
}
impl From<serde_json::Error> for FlickrError {
fn from(err: serde_json::Error) -> FlickrError {
FlickrError::RestCallFailed(format!("Deserialization error: {}", err))
}
}
impl Error for FlickrError {
fn description(& self) -> &str {
match self {
FlickrError::Io{source} => source.description(),
FlickrError::OauthFailed(msg) => msg,
FlickrError::RestCallFailed(msg) => msg,
}
}
fn cause(&self) -> Option<&Error> {
match self {
FlickrError::Io{source} => Some(source),
FlickrError::OauthFailed(_msg) => Option::None,
FlickrError::RestCallFailed(_msg) => Option::None,
}
}
}
impl fmt::Display for FlickrError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
FlickrError::Io{source}
=> write!(f, "I/O Error: {}", source.description()),
FlickrError::OauthFailed(msg)
=> write!(f, "OAUTH error: {}", msg),
FlickrError::RestCallFailed(msg)
=> write!(f, "REST call failed: {}", msg),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hma_sha1_digest_generation() {
let resulting_digest = hmac_sha1_digest(&String::from(
"The quick brown fox jumps over the lazy dog."),
Some("5+VD/kuzKaVotvJCeX1NNP/I4+mJNcqRoz6dSXx0fCM="));
let expected_digest = "01fDvSHJYOhRjoxKzPWWWD0Ej1Y=";
assert_eq!(resulting_digest, expected_digest);
}
#[test]
fn test_parse_key_value() {
if let Ok((_, _)) = parse_key_value("aa bb", ":", false) {
assert!(false);
} else {
assert!(true);
}
if let Ok((key, value)) = parse_key_value("aa: bb ", ":", false) {
assert_eq!(key, "aa");
assert_eq!(value, " bb ");
} else {
assert!(false);
}
if let Ok((key, value)) = parse_key_value("aa: bb ", ":", true) {
assert_eq!(key, "aa");
assert_eq!(value, "bb");
} else {
assert!(false);
}
}
#[test]
fn test_flickrapi_serde() {
let flickr1 = FlickrAPI::new("aaaa", "bbbb");
let j = serde_json::to_string(&flickr1).unwrap();
println!("flickr1: {:?}", flickr1);
println!("j: {}", j);
let flickr2: FlickrAPI = serde_json::from_str(&j).unwrap();
assert_eq!(flickr1.flickr_api_key, flickr2.flickr_api_key);
assert_eq!(flickr1.flickr_api_secret, flickr2.flickr_api_secret);
assert_eq!(format!("{:?}", flickr1), format!("{:?}", flickr2));
}
}