use crate::spotify::SpotifyError;
use base64;
use getrandom;
use json;
use open;
use querystring::{querify, stringify};
use random_string;
use reqwest;
use sha2::{Digest, Sha256};
use std::{
io::{prelude::*, BufReader},
net::{TcpListener, TcpStream},
};
use urlencoding::encode;
const AUTHORIZATION_SUCCESSFUL_HTML: &str = r###"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Success</title>
</head>
<body>
<h1>Success!</h1>
<p>Thank you for authenticating with Spotify! You can close this page now.</p>
</body>
</html>"###;
pub fn generate_verifier() -> (String, String) {
let mut buf = [0u8; 32];
getrandom::getrandom(&mut buf).unwrap();
let code_verifier = base64::encode_config(buf, base64::URL_SAFE).replace("=", "");
let mut code_challenge_hasher = Sha256::new(); code_challenge_hasher.update(&code_verifier); let code_challenge_raw = code_challenge_hasher.finalize();
let code_challenge =
base64::encode_config(code_challenge_raw, base64::URL_SAFE).replace("=", "");
(code_verifier, code_challenge)
}
pub fn requesturl_authorization_code(
client_id: &str,
redirect_uri: &str,
scope: &str,
code_challenge: &str,
) -> (String, String) {
let authorization_code_endpoint = String::from("https://accounts.spotify.com/authorize?"); let character_set = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let state = random_string::generate(16, character_set);
let encoded_redirect_uri = encode(redirect_uri).into_owned();
let parameters = vec![
("response_type", "code"),
("client_id", client_id),
("redirect_uri", &encoded_redirect_uri),
("scope", scope),
("show_dialog", "true"),
("state", &state),
("code_challenge", code_challenge),
("code_challenge_method", "S256"),
];
let query_parameters = stringify(parameters);
let auth_url = authorization_code_endpoint + &query_parameters;
(auth_url, state)
}
pub fn get_authorization_code(
client_id: &str,
localhost_port: &str,
redirect_uri: &str,
scope: &str,
code_challenge: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let (auth_url, state) =
requesturl_authorization_code(client_id, redirect_uri, scope, code_challenge);
match open::that(auth_url) {
Ok(()) => println!("Opened authorization url in browser"),
Err(e) => panic!("Failed to open authorization url in browser: {}", e), }
return listen_for_auth_code(localhost_port, &state);
}
fn listen_for_auth_code(port: &str, state: &str) -> Result<String, Box<dyn std::error::Error>> {
let listener = TcpListener::bind(String::from("127.0.0.1:") + &port).unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
let auth_code = handle_connection(stream, &state);
match auth_code {
Some(result) => match result {
Ok(code) => return Ok(code),
Err(e) => return Err(e),
},
None => continue,
}
}
Err("Failed to find authorization code.".into())
}
fn handle_connection(
mut stream: TcpStream,
state: &str,
) -> Option<Result<String, Box<dyn std::error::Error>>> {
let buf_reader = BufReader::new(&mut stream);
let http_request = buf_reader.lines().next().unwrap().unwrap();
let http_request_len = http_request.len();
if &http_request[0..13] == "GET /callback"
&& &http_request[(http_request_len - 9)..] == " HTTP/1.1"
{
let query = querify(&http_request[14..http_request_len - 9]);
if query[1].0 == "state" && query[1].1 == state {
if query[0].0 == "code" {
let authorization_code = String::from(query[0].1);
let status_line = "HTTP/1.1 200 OK"; let contents = AUTHORIZATION_SUCCESSFUL_HTML.to_string(); let content_length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {content_length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
return Some(Ok(authorization_code)); } else if query[0].0 == "error" {
return Some(Err(format!("Authorization error: {}", query[0].1).into()));
} else {
return Some(Err("Authorization error".into())); }
} else {
return Some(Err(format!(
"Invalid state. Expected {} got {}. Authorization failed",
state, query[1].1
)
.into())); }
} else {
return None; }
}
pub fn get_access_token(
authorization_code: &str,
client_id: &str,
code_verifier: &str,
redirect_uri: &str,
) -> Result<(String, String, i64), Box<dyn std::error::Error>> {
let request_uri = "https://accounts.spotify.com/api/token?";
let client = reqwest::blocking::Client::new();
let encoded_redirect_uri = encode(&redirect_uri).into_owned();
let query_parameters = vec![
("grant_type", "authorization_code"),
("code", authorization_code),
("redirect_uri", &encoded_redirect_uri),
("client_id", client_id),
("code_verifier", code_verifier),
];
let query_string = stringify(query_parameters);
let response = client
.post(String::from(request_uri) + &query_string)
.header("Content-Type", "application/x-www-form-urlencoded") .header("Content-Length", "0") .send()?;
if response.status().is_success() {
let response_body = json::parse(&response.text().unwrap()).unwrap();
let access_token = response_body["access_token"].to_string(); let refresh_token = response_body["refresh_token"].to_string(); let expires_in_str = response_body["expires_in"].to_string(); let expires_in: i64 = expires_in_str.parse().unwrap();
return Ok((access_token, refresh_token, expires_in)); } else {
return Err(format!("Error: {}", response.status()).into()); }
}
pub fn refresh_access_token(
refresh_token: &str,
client_id: &str,
) -> Result<(String, i64, String), SpotifyError> {
let request_uri = "https://accounts.spotify.com/api/token?";
let client = reqwest::blocking::Client::new();
let query_parameters = vec![
("grant_type", "refresh_token"),
("refresh_token", refresh_token),
("client_id", client_id),
];
let query_string = stringify(query_parameters);
let response = client
.post(String::from(request_uri) + &query_string)
.header("Content-Type", "application/x-www-form-urlencoded") .header("Content-Length", "0") .send()
.unwrap();
if response.status().is_success() {
let response_body = json::parse(&response.text().unwrap()).unwrap();
let access_token = response_body["access_token"].to_string(); let expires_in_str = response_body["expires_in"].to_string(); let expires_in: i64 = expires_in_str.parse().unwrap(); let new_refresh_token = match response_body["refresh_token"] {
json::JsonValue::Null => refresh_token.to_string(),
_ => response_body["refresh_token"].to_string(),
};
return Ok((access_token, expires_in, new_refresh_token)); } else {
let response_code = response.status().as_u16();
let response_body = json::parse(&response.text().unwrap()).unwrap();
match response_code {
400 => {
return Err(SpotifyError::BadRequest(format!(
"Error {}: {}",
response_code, response_body["error_description"]
)))
}
401 => {
return Err(SpotifyError::Unauthorized(format!(
"Error {}: {}",
response_code, response_body["error_description"]
)))
}
_ => {
return Err(SpotifyError::GeneralError(format!(
"Error: {}",
response_code
)))
}
}
}
}