src/http/mod.rs
use std::fmt;
use std::error::Error;
use std::str;
use std::io::{Read, Write, BufReader, BufRead};
use std::net::{TcpListener};
use std::thread;
use std::sync::mpsc::channel;
use std::time::Duration;
use std::collections::BTreeMap;
extern crate curl;
use self::curl::easy::{Easy, List};
extern crate time;
extern crate open;
extern crate percent_encoding;
use super::settings;
#[derive(PartialEq, Debug)]
pub enum HttpMethod {
GET,
POST,
PUT,
}
pub type HttpErrorString = String;
pub struct HttpResponse {
pub code: Option<u32>,
pub data: Result<String, HttpErrorString>,
}
impl HttpResponse {
pub fn unwrap(self) -> String { self.data.unwrap() }
pub fn print(&self) {
let code: i32 = match self.code {
Some(x) => { x as i32 }
None => -1
};
println!("Code: {}", code);
match self.data {
Ok(ref s) => {println!("{}", s)}
Err(ref s) => {println!("ERROR: {}", s)}
}
}
}
impl fmt::Display for HttpResponse {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let code: i32 = match self.code {
Some(x) => { x as i32 }
None => -1
};
let _ = write!(f, "Code: {}\n", code);
match self.data {
Ok(ref s) => {write!(f, "Response: {}", s)}
Err(ref s) => {write!(f, "ERROR: {}", s)}
}
}
}
#[derive(Debug)]
pub enum AccessToken<'a> {
Bearer(&'a str),
Basic(&'a str),
None,
}
pub fn http(url: &str, query: Option<&str>, body: Option<&str>,
method: HttpMethod, access_token: AccessToken) -> HttpResponse {
let mut headers = List::new();
#[cfg(feature = "verbose_http")]
info!("HTTP URL: {:?} {}\nQuery: {:?}\nBody: {:?}\nToken: {:?}", method, url, query, body, access_token);
let data = match method {
HttpMethod::POST => {
match query {
Some(q) => {
let enc_query = percent_encoding::utf8_percent_encode(&q, percent_encoding::QUERY_ENCODE_SET).collect::<String>();
enc_query
},
None => {
let header = format!("Content-Type: application/json");
headers.append(&header).unwrap();
body.unwrap_or("").to_string()
}
}
},
_ => { body.unwrap_or("").to_string() },
};
let mut data = data.as_bytes();
let url = match method {
HttpMethod::GET | HttpMethod::PUT => match query {
None => url.to_string(),
Some(q) => format!("{}?{}", url, q),
},
_ => url.to_string()
};
let mut response = None;
let mut json_bytes = Vec::<u8>::new();
{
let mut easy = Easy::new();
let _ = easy.timeout(Duration::new(20,0)); // 20 sec timeout
easy.url(&url).unwrap();
match method {
HttpMethod::POST => {
easy.post(true).unwrap();
easy.post_field_size(data.len() as u64).unwrap();
}
HttpMethod::PUT => {
easy.put(true).unwrap();
easy.post_field_size(data.len() as u64).unwrap();
}
_ => {}
}
match access_token {
AccessToken::None => {},
access_token => {
let request = match access_token {
AccessToken::Bearer(token) => ("Bearer", token),
AccessToken::Basic(token) => ("Basic", token),
_ => ("",""),
};
let header = format!("Authorization: {} {}", request.0, request.1);
headers.append(&header).unwrap();
easy.http_headers(headers).unwrap();
}
}
{
let mut transfer = easy.transfer();
if method == HttpMethod::POST || method == HttpMethod::PUT {
transfer.read_function(|buf| {
Ok(data.read(buf).unwrap_or(0))
}).unwrap();
}
transfer.write_function(|x| {
json_bytes.extend(x);
Ok(x.len())
}).unwrap();
match transfer.perform() {
Err(x) => {
let result: Result<String,String> = Err(x.description().to_string());
#[cfg(feature = "verbose_http")]
warn!("HTTP response: err: {}", x.description().to_string());
return HttpResponse {code: response, data: result }
}
_ => {}
};
}
response = match easy.response_code() {
Ok(code) => { Some(code) }
_ => { None }
};
}
let result: Result<String,String> = match String::from_utf8(json_bytes) {
Ok(x) => { Ok(x) }
Err(x) => { Err(x.utf8_error().description().to_string()) }
};
#[cfg(feature = "verbose_http")]
info!("HTTP response: {}", result.clone().unwrap());
HttpResponse {code: response, data: result }
}
pub fn authenticate(scopes: &str, url: &str, settings: &settings::Settings) -> String {
let host = format!("http://127.0.0.1:{}", settings.port);
let url = format!("{}?client_id={}&response_type=code&scope={}&redirect_uri={}",
url,settings.client_id, scopes, host);
let query = percent_encoding::utf8_percent_encode(&url, percent_encoding::QUERY_ENCODE_SET).collect::<String>();
let response = "HTTP/1.1 200 OK\r\n\r\n<html><body>
Authenticated with Spotify.<br/><br/>
You can close this window.<br/><br/>
<button type=\"button\" onclick=\"window.open('', '_self', ''); window.close();\">Close</button><br/>
</body></html>";
let auth_lines = oauth_request_with_local_webserver(settings.port, &query, response);
let auth_code = spotify_auth_code(auth_lines);
auth_code
}
fn oauth_request_with_local_webserver(port: u32, url: &str, reply: &str) -> Vec<String> {
if !open::that(url).is_ok() {
return Vec::<String>::new()
}
let start = time::now_utc().to_timespec().sec as i64;
let host = format!("127.0.0.1:{}", port);
let listener = TcpListener::bind(host);
if listener.is_err() {
return Vec::<String>::new();
}
let timeout_sec = 20;
let listener = listener.unwrap();
let _ = listener.set_nonblocking(true);
loop {
let conn = listener.accept();
if conn.is_err() {
let now = time::now_utc().to_timespec().sec as i64;
if now >= start + timeout_sec {
warn!("Spotify OAuth request timed out.");
break;
}
thread::sleep(Duration::from_millis(100));
continue;
}
let stream = conn.unwrap().0;
let mut reader = BufReader::new(stream);
let mut response = Vec::<String>::new();
for line in reader.by_ref().lines() {
let line_str = line.unwrap();
response.push(line_str.clone());
if line_str == "" {
break;
}
}
let _ = reader.into_inner().write(reply.as_bytes());
return response;
}
Vec::<String>::new()
}
pub fn config_request_local_webserver(port: u32, form: String, reply: String) -> BTreeMap<String,String> {
let mut config = BTreeMap::<String,String>::new();
let (tx,rx) = channel::<Option<(String,String)>>();
let (tx_kill,rx_kill) = channel::<()>();
thread::spawn(move || {
// Implement a custom HTTP server, because YOU'RE NOT MY MOM.
let host = format!("127.0.0.1:{}", port);
let listener = TcpListener::bind(host);
if listener.is_err() {
let _ = tx.send(None); // Start data transfer
let _ = tx.send(None); // End data transfer
return;
}
let listener = listener.unwrap();
let _ = listener.set_nonblocking(true);
loop {
let conn = listener.accept();
if conn.is_err() {
match rx_kill.try_recv() {
Ok(_) => { break;},
Err(_) => {
thread::sleep(Duration::from_millis(100));
continue;
},
}
}
let stream = conn.unwrap().0;
let mut reader = BufReader::new(stream);
let mut response = Vec::<String>::new();
let post_bytes: u32 = 0;
for line in reader.by_ref().lines() {
if let Ok(line_str) = line {
if line_str.starts_with("Content-Length: ") {
line_str[16..].parse::<u32>().unwrap_or(0);
}
response.push(line_str.clone());
if line_str == "" {
break;
}
}
}
match post_bytes {
x if x > 0 => {
// Tell parent thread data is coming. Cancels timeout mechanism.
let _ = tx.send(None);
{
let mut post_reader = reader.by_ref().take(post_bytes as u64);
let mut post_data = Vec::<u8>::new();
let _ = post_reader.read_to_end(&mut post_data);
let post_data = String::from_utf8(post_data).unwrap();
for post_pair in post_data.split("&") {
let mut key_value = post_pair.split("=");
let key = key_value.next().unwrap();
let value = key_value.next().unwrap();
let _ = tx.send(Some((key.to_string(),value.to_string())));
}
}
let _ = reader.into_inner().write(reply.as_bytes());
// Tell parent thread that data is finished.
let _ = tx.send(None);
break;
},
_ => {
let _ = reader.into_inner().write(form.as_bytes());
}
}
}
});
if !open::that(format!("http://127.0.0.1:{}", port)).is_ok() {
return config;
}
// Run web server for an hour.
// In a proper world, this would be an async 'future'. The whole Spotify
// thread is blocked until the user saves.
let timeout = Duration::from_secs(60*60);
match rx.recv_timeout(timeout) {
Ok(_) => {
while let Some(pair) = rx.recv().unwrap() {
let key = pair.0.replace("+"," ").trim().to_string();
let key = percent_encoding::percent_decode(key.as_bytes()).decode_utf8_lossy();
let value = pair.1.replace("+"," ").trim().to_string();
let value = percent_encoding::percent_decode(value.trim().as_bytes()).decode_utf8_lossy();
config.insert(key.to_string(), value.to_string());
}
}
_ => {
warn!("Web configuration timed out.");
let _ = tx_kill.send(());
}
}
config
}
fn spotify_auth_code(lines: Vec<String>) -> String {
let mut auth_code = String::new();
// Looking for HTTP request header with format:
// GET /?code=<MASSIVE STRING> HTTP/1.1
for line in lines {
let line_str = line;
if !line_str.starts_with("GET ") {
continue;
}
let code_idx = line_str.find("code=").unwrap_or(line_str.len() - 5) + 5;
let code_end_idx = line_str[code_idx..].find(|c| { c == '&' || c == '?' || c == ' '}).unwrap_or(line_str.len() - code_idx) + code_idx;
auth_code.push_str(&line_str[code_idx..code_end_idx]);
}
auth_code
}