automatically refresh invalid grants

This commit is contained in:
Sebastian Hugentobler 2017-05-05 15:35:50 +02:00
parent 910a3575d7
commit e484084e4c
5 changed files with 128 additions and 75 deletions

View file

@ -24,6 +24,7 @@ use std::fs::File;
use std::io;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
/// Wraps `ConfigError`, `HttpError`, `serde_json::Error`, and `io::Error`.
#[derive(Debug)]
@ -76,8 +77,6 @@ impl From<io::Error> for GogError {
pub struct Gog<'a> {
client_id: String,
client_secret: String,
redirect_uri: String,
games_uri: String,
http_client: &'a mut Http,
}
@ -87,8 +86,6 @@ impl<'a> Gog<'a> {
Gog {
client_id: String::from("46899977096215655"),
client_secret: String::from("9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"),
redirect_uri: String::from("https://embed.gog.com/on_login_success?origin=client"),
games_uri: String::from("https://embed.gog.com/user/data/games"),
http_client: http_client,
}
}
@ -98,6 +95,15 @@ impl<'a> Gog<'a> {
/// will get an url which you have to open in a browser. There you have to
/// login and copy the code parameter from the next page into the prompt.
pub fn login(&mut self) -> Result<(), GogError> {
let token = self.refresh_token_from_file()?;
let auth_header = format!("Authorization: Bearer {token}", token = token.access_token);
self.http_client.add_header(auth_header.as_str())?;
Ok(())
}
fn refresh_token_from_file(&mut self) -> Result<Token, GogError> {
let config = ConfigFiles::new();
let mut token: Token = match config.load("token.json") {
Ok(value) => value,
@ -113,10 +119,7 @@ impl<'a> Gog<'a> {
config.save("token.json", &token)?;
let auth_header = format!("Authorization: Bearer {token}", token = token.access_token);
self.http_client.add_header(auth_header.as_str())?;
Ok(())
Ok(token)
}
/// Syncs the contents of a gog account with a local folder.
@ -214,7 +217,7 @@ impl<'a> Gog<'a> {
let data_uri = format!("https://embed.gog.com{}", data.manual_url);
info!("downloading {} for {}...", &data.manual_url, &content.title);
let filename = self.http_client.download(data_uri.as_str(), &data_root)?;
let filename = self.api_request_download(data_uri.as_str(), &data_root)?;
let data_info = DataInfo {
hash: data_hash,
@ -222,16 +225,18 @@ impl<'a> Gog<'a> {
language: data.language,
};
content_info.data.insert(data.manual_url.clone(), data_info);
content_info
.data
.insert(data.manual_url.clone(), data_info);
ConfigFiles::save_to_path(&content_info_path, &content_info)?;
}
for extra in content.extras {
let extra_hash_saved = match content_info_saved.extras
.get(&content_id.to_string()) {
Some(value) => value.hash,
None => u64::min_value(),
};
let extra_hash_saved =
match content_info_saved.extras.get(&content_id.to_string()) {
Some(value) => value.hash,
None => u64::min_value(),
};
let extra_hash = models::get_hash(&extra);
@ -243,7 +248,7 @@ impl<'a> Gog<'a> {
let extra_uri = format!("https://embed.gog.com{}", extra.manual_url);
info!("downloading {} for {}...", &extra.name, &content.title);
let filename = self.http_client.download(extra_uri.as_str(), &content_root)?;
let filename = self.api_request_download(extra_uri.as_str(), &content_root)?;
let extra_info = ExtraInfo {
hash: extra_hash,
@ -251,7 +256,9 @@ impl<'a> Gog<'a> {
name: extra.name,
};
content_info.extras.insert(extra.manual_url.clone(), extra_info);
content_info
.extras
.insert(extra.manual_url.clone(), extra_info);
ConfigFiles::save_to_path(&content_info_path, &content_info)?;
}
}
@ -259,8 +266,8 @@ impl<'a> Gog<'a> {
Ok(())
}
fn get_code(&self) -> Result<String, GogError> {
let auth_uri = self.auth_uri(self.client_id.as_str(), self.redirect_uri.as_str());
fn get_code(&mut self) -> Result<String, GogError> {
let auth_uri = self.auth_uri(self.client_id.as_str(), self.redirect_uri().as_str());
println!("Open the following url in a browser, login to your account and paste the \
resulting code parameter.");
@ -282,12 +289,9 @@ impl<'a> Gog<'a> {
let token_uri = self.token_uri(self.client_id.as_str(),
self.client_secret.as_str(),
code,
self.redirect_uri.as_str());
self.redirect_uri().as_str());
let token_response = match self.http_client.get(token_uri.as_str()) {
Ok(value) => value,
Err(error) => return Err(GogError::HttpError(error)),
};
let token_response = self.api_request_get(token_uri.as_str())?;
match serde_json::from_str(&token_response) {
Ok(value) => Ok(value),
@ -300,10 +304,7 @@ impl<'a> Gog<'a> {
self.client_secret.as_str(),
refresh_token);
let token_response = match self.http_client.get(token_refresh_uri.as_str()) {
Ok(value) => value,
Err(error) => return Err(GogError::HttpError(error)),
};
let token_response = self.api_request_get(token_refresh_uri.as_str())?;
match serde_json::from_str(&token_response) {
Ok(value) => Ok(value),
@ -312,7 +313,8 @@ impl<'a> Gog<'a> {
}
fn get_content_ids(&mut self) -> Result<Vec<u64>, GogError> {
let response = self.http_client.get(self.games_uri.as_str())?;
let games_uri = self.games_uri();
let response = self.api_request_get(games_uri.as_str())?;
let content_ids_raw: Value = serde_json::from_str(response.as_str())?;
let content_ids_serde = &content_ids_raw["owned"];
@ -347,7 +349,8 @@ impl<'a> Gog<'a> {
let mut raw_cd_keys_fix = raw_cd_keys.to_owned();
if raw_cd_keys_fix.contains("<span>") {
raw_cd_keys_fix = raw_cd_keys_fix.replace("</span><span>", ":")
raw_cd_keys_fix = raw_cd_keys_fix
.replace("</span><span>", ":")
.replace("<span>", "")
.replace("</span>", "");
}
@ -379,6 +382,41 @@ impl<'a> Gog<'a> {
return cd_keys;
}
fn api_request_ensure_token(&mut self, response: &str) -> Result<bool, GogError> {
let response_json: Value = match serde_json::from_str(response) {
Ok(value) => value,
Err(_) => return Ok(false),
};
if response_json.is_object() && response_json.as_object().unwrap().contains_key("error") &&
response_json["error"] == "invalid_grant" {
debug!("invalid grant, refreshing token...");
self.refresh_token_from_file()?;
Ok(true)
} else {
Ok(false)
}
}
fn api_request_get(&mut self, uri: &str) -> Result<String, GogError> {
let response = self.http_client.get(uri)?;
if self.api_request_ensure_token(&response)? {
Ok(self.http_client.get(uri)?)
} else {
Ok(response)
}
}
fn api_request_download(&mut self, uri: &str, path: &PathBuf) -> Result<String, GogError> {
let response = self.http_client.download(uri, path)?;
if self.api_request_ensure_token(&response)? {
Ok(self.http_client.download(uri, path)?)
} else {
Ok(response)
}
}
fn get_content(&mut self,
content_id: u64,
os_filters: &Vec<String>,
@ -398,7 +436,11 @@ impl<'a> Gog<'a> {
let downloads = &content_raw["downloads"];
if content_raw.is_object() && !content_raw.as_object().unwrap().contains_key("forumLink") {
if content_raw.is_object() &&
!content_raw
.as_object()
.unwrap()
.contains_key("forumLink") {
return Err(GogError::Error("No forumLink property"));
}
@ -479,11 +521,11 @@ impl<'a> Gog<'a> {
let data = Data {
manual_url: String::from(download["manualUrl"]
.as_str()
.unwrap()),
.as_str()
.unwrap()),
version: String::from(download["version"]
.as_str()
.unwrap_or("")),
.as_str()
.unwrap_or("")),
os: system.clone(),
language: data_language.clone(),
};
@ -500,6 +542,14 @@ impl<'a> Gog<'a> {
Ok(content)
}
fn games_uri(&self) -> String {
String::from("https://embed.gog.com/user/data/games")
}
fn redirect_uri(&self) -> String {
String::from("https://embed.gog.com/on_login_success?origin=client")
}
fn auth_uri(&self, client_id: &str, redirect_uri: &str) -> String {
format!("https://auth.gog.\
com/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&layout=client2",

View file

@ -28,50 +28,50 @@ fn main() {
env_logger::init().unwrap();
let matches = App::new("Gog Synchronizer")
.version("0.3.0")
.version("0.3.1")
.author("Sebastian Hugentobler <sebastian@vanwa.ch>")
.about("Synchronizes your gog library to a local folder.")
.arg(Arg::with_name("game-storage")
.short("s")
.long("game-storage")
.value_name("FOLDER")
.help("Sets the download folder (defaults to the working directory).")
.takes_value(true))
.short("s")
.long("game-storage")
.value_name("FOLDER")
.help("Sets the download folder (defaults to the working directory).")
.takes_value(true))
.arg(Arg::with_name("movie-storage")
.short("m")
.long("movie-storage")
.value_name("FOLDER")
.help("Sets the download folder for movies (defaults to the working directory).")
.takes_value(true))
.short("m")
.long("movie-storage")
.value_name("FOLDER")
.help("Sets the download folder for movies (defaults to the working directory).")
.takes_value(true))
.arg(Arg::with_name("os")
.short("o")
.long("os")
.value_name("FILTER")
.help("Only sync files for this comma seperated list of operating systems.\n\
.short("o")
.long("os")
.value_name("FILTER")
.help("Only sync files for this comma seperated list of operating systems.\n\
Valid values are 'linux', 'mac' and 'windows'.")
.takes_value(true))
.takes_value(true))
.arg(Arg::with_name("language")
.short("l")
.long("language")
.value_name("FILTER")
.help("Only sync files for this comma seperated list of languages.")
.takes_value(true))
.short("l")
.long("language")
.value_name("FILTER")
.help("Only sync files for this comma seperated list of languages.")
.takes_value(true))
.arg(Arg::with_name("resolution")
.short("r")
.long("resolution")
.value_name("FILTER")
.help("Only sync movies for this comma seperated list of resolutions.")
.takes_value(true))
.short("r")
.long("resolution")
.value_name("FILTER")
.help("Only sync movies for this comma seperated list of resolutions.")
.takes_value(true))
.arg(Arg::with_name("skip-movies")
.short("f")
.long("skip-movies")
.help("Skip movie content.")
.takes_value(false))
.short("f")
.long("skip-movies")
.help("Skip movie content.")
.takes_value(false))
.arg(Arg::with_name("skip-games")
.short("g")
.long("skip-games")
.help("Skip game content.")
.takes_value(false))
.short("g")
.long("skip-games")
.help("Skip game content.")
.takes_value(false))
.get_matches();
let configfiles = ConfigFiles::new();