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

@ -1,5 +1,8 @@
## 0.3.1 (2017-05-05)
- automatically check if grant is still valid and refresh if necessary (fixes #7)
## 0.3.0 (2017-05-04) ## 0.3.0 (2017-05-04)
- move hashes and content info into seperate files as to not clutter up the main config file. - move hashes and content info into seperate files as to not clutter up the main config file
## 0.2.4 (2017-03-23) ## 0.2.4 (2017-03-23)
- fix a bug in serial key parsing - fix a bug in serial key parsing

8
Cargo.lock generated
View File

@ -1,9 +1,9 @@
[root] [root]
name = "gog-sync" name = "gog-sync"
version = "0.3.0" version = "0.3.1"
dependencies = [ dependencies = [
"chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.23.3 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.24.0 (registry+https://github.com/rust-lang/crates.io-index)",
"curl 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "curl 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
@ -53,7 +53,7 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "2.23.3" version = "2.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -429,7 +429,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159" "checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159"
"checksum bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1370e9fc2a6ae53aea8b7a5110edbd08836ed87c88736dfabccade1c2b44bff4" "checksum bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1370e9fc2a6ae53aea8b7a5110edbd08836ed87c88736dfabccade1c2b44bff4"
"checksum chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d9123be86fd2a8f627836c235ecdf331fdd067ecf7ac05aa1a68fbcf2429f056" "checksum chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d9123be86fd2a8f627836c235ecdf331fdd067ecf7ac05aa1a68fbcf2429f056"
"checksum clap 2.23.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f57e9b63057a545ad2ecd773ea61e49422ed1b1d63d74d5da5ecaee55b3396cd" "checksum clap 2.24.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f31c42d0cecb245c1a0bee00ef433eb1bf253897fe472b6a3f4202e9dbbc4b25"
"checksum curl 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c90e1240ef340dd4027ade439e5c7c2064dd9dc652682117bd50d1486a3add7b" "checksum curl 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c90e1240ef340dd4027ade439e5c7c2064dd9dc652682117bd50d1486a3add7b"
"checksum curl-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "23e7e544dc5e1ba42c4a4a678bd47985e84b9c3f4d3404c29700622a029db9c3" "checksum curl-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "23e7e544dc5e1ba42c4a4a678bd47985e84b9c3f4d3404c29700622a029db9c3"
"checksum dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80c8b71fd71146990a9742fc06dcbbde19161a267e0ad4e572c35162f4578c90" "checksum dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80c8b71fd71146990a9742fc06dcbbde19161a267e0ad4e572c35162f4578c90"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "gog-sync" name = "gog-sync"
version = "0.3.0" version = "0.3.1"
authors = ["Sebastian Hugentobler <sebastian@vanwa.ch>"] authors = ["Sebastian Hugentobler <sebastian@vanwa.ch>"]
description = "Synchronizes a GOG library with a local folder." description = "Synchronizes a GOG library with a local folder."
documentation = "https://docs.rs/crate/gog-sync" documentation = "https://docs.rs/crate/gog-sync"

View File

@ -24,6 +24,7 @@ use std::fs::File;
use std::io; use std::io;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use std::path::PathBuf;
/// Wraps `ConfigError`, `HttpError`, `serde_json::Error`, and `io::Error`. /// Wraps `ConfigError`, `HttpError`, `serde_json::Error`, and `io::Error`.
#[derive(Debug)] #[derive(Debug)]
@ -76,8 +77,6 @@ impl From<io::Error> for GogError {
pub struct Gog<'a> { pub struct Gog<'a> {
client_id: String, client_id: String,
client_secret: String, client_secret: String,
redirect_uri: String,
games_uri: String,
http_client: &'a mut Http, http_client: &'a mut Http,
} }
@ -87,8 +86,6 @@ impl<'a> Gog<'a> {
Gog { Gog {
client_id: String::from("46899977096215655"), client_id: String::from("46899977096215655"),
client_secret: String::from("9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"), 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, 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 /// 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. /// login and copy the code parameter from the next page into the prompt.
pub fn login(&mut self) -> Result<(), GogError> { 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 config = ConfigFiles::new();
let mut token: Token = match config.load("token.json") { let mut token: Token = match config.load("token.json") {
Ok(value) => value, Ok(value) => value,
@ -113,10 +119,7 @@ impl<'a> Gog<'a> {
config.save("token.json", &token)?; config.save("token.json", &token)?;
let auth_header = format!("Authorization: Bearer {token}", token = token.access_token); Ok(token)
self.http_client.add_header(auth_header.as_str())?;
Ok(())
} }
/// Syncs the contents of a gog account with a local folder. /// 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); let data_uri = format!("https://embed.gog.com{}", data.manual_url);
info!("downloading {} for {}...", &data.manual_url, &content.title); 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 { let data_info = DataInfo {
hash: data_hash, hash: data_hash,
@ -222,13 +225,15 @@ impl<'a> Gog<'a> {
language: data.language, 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)?; ConfigFiles::save_to_path(&content_info_path, &content_info)?;
} }
for extra in content.extras { for extra in content.extras {
let extra_hash_saved = match content_info_saved.extras let extra_hash_saved =
.get(&content_id.to_string()) { match content_info_saved.extras.get(&content_id.to_string()) {
Some(value) => value.hash, Some(value) => value.hash,
None => u64::min_value(), None => u64::min_value(),
}; };
@ -243,7 +248,7 @@ impl<'a> Gog<'a> {
let extra_uri = format!("https://embed.gog.com{}", extra.manual_url); let extra_uri = format!("https://embed.gog.com{}", extra.manual_url);
info!("downloading {} for {}...", &extra.name, &content.title); 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 { let extra_info = ExtraInfo {
hash: extra_hash, hash: extra_hash,
@ -251,7 +256,9 @@ impl<'a> Gog<'a> {
name: extra.name, 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)?; ConfigFiles::save_to_path(&content_info_path, &content_info)?;
} }
} }
@ -259,8 +266,8 @@ impl<'a> Gog<'a> {
Ok(()) Ok(())
} }
fn get_code(&self) -> Result<String, GogError> { fn get_code(&mut self) -> Result<String, GogError> {
let auth_uri = self.auth_uri(self.client_id.as_str(), self.redirect_uri.as_str()); 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 \ println!("Open the following url in a browser, login to your account and paste the \
resulting code parameter."); resulting code parameter.");
@ -282,12 +289,9 @@ impl<'a> Gog<'a> {
let token_uri = self.token_uri(self.client_id.as_str(), let token_uri = self.token_uri(self.client_id.as_str(),
self.client_secret.as_str(), self.client_secret.as_str(),
code, code,
self.redirect_uri.as_str()); self.redirect_uri().as_str());
let token_response = match self.http_client.get(token_uri.as_str()) { let token_response = self.api_request_get(token_uri.as_str())?;
Ok(value) => value,
Err(error) => return Err(GogError::HttpError(error)),
};
match serde_json::from_str(&token_response) { match serde_json::from_str(&token_response) {
Ok(value) => Ok(value), Ok(value) => Ok(value),
@ -300,10 +304,7 @@ impl<'a> Gog<'a> {
self.client_secret.as_str(), self.client_secret.as_str(),
refresh_token); refresh_token);
let token_response = match self.http_client.get(token_refresh_uri.as_str()) { let token_response = self.api_request_get(token_refresh_uri.as_str())?;
Ok(value) => value,
Err(error) => return Err(GogError::HttpError(error)),
};
match serde_json::from_str(&token_response) { match serde_json::from_str(&token_response) {
Ok(value) => Ok(value), Ok(value) => Ok(value),
@ -312,7 +313,8 @@ impl<'a> Gog<'a> {
} }
fn get_content_ids(&mut self) -> Result<Vec<u64>, GogError> { 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_raw: Value = serde_json::from_str(response.as_str())?;
let content_ids_serde = &content_ids_raw["owned"]; 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(); let mut raw_cd_keys_fix = raw_cd_keys.to_owned();
if raw_cd_keys_fix.contains("<span>") { 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>", "")
.replace("</span>", ""); .replace("</span>", "");
} }
@ -379,6 +382,41 @@ impl<'a> Gog<'a> {
return cd_keys; 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, fn get_content(&mut self,
content_id: u64, content_id: u64,
os_filters: &Vec<String>, os_filters: &Vec<String>,
@ -398,7 +436,11 @@ impl<'a> Gog<'a> {
let downloads = &content_raw["downloads"]; 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")); return Err(GogError::Error("No forumLink property"));
} }
@ -500,6 +542,14 @@ impl<'a> Gog<'a> {
Ok(content) 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 { fn auth_uri(&self, client_id: &str, redirect_uri: &str) -> String {
format!("https://auth.gog.\ format!("https://auth.gog.\
com/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&layout=client2", com/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&layout=client2",

View File

@ -28,7 +28,7 @@ fn main() {
env_logger::init().unwrap(); env_logger::init().unwrap();
let matches = App::new("Gog Synchronizer") let matches = App::new("Gog Synchronizer")
.version("0.3.0") .version("0.3.1")
.author("Sebastian Hugentobler <sebastian@vanwa.ch>") .author("Sebastian Hugentobler <sebastian@vanwa.ch>")
.about("Synchronizes your gog library to a local folder.") .about("Synchronizes your gog library to a local folder.")
.arg(Arg::with_name("game-storage") .arg(Arg::with_name("game-storage")