Merge branch '7-access-token-expired' into 'master'

automatically refresh invalid grants

Closes #7

See merge request !8
This commit is contained in:
Sebastian Hugentobler 2017-05-05 14:11:57 +00:00
commit db1c5af255
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,16 +225,18 @@ 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(),
}; };
let extra_hash = models::get_hash(&extra); 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); 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"));
} }
@ -479,11 +521,11 @@ impl<'a> Gog<'a> {
let data = Data { let data = Data {
manual_url: String::from(download["manualUrl"] manual_url: String::from(download["manualUrl"]
.as_str() .as_str()
.unwrap()), .unwrap()),
version: String::from(download["version"] version: String::from(download["version"]
.as_str() .as_str()
.unwrap_or("")), .unwrap_or("")),
os: system.clone(), os: system.clone(),
language: data_language.clone(), language: data_language.clone(),
}; };
@ -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,50 +28,50 @@ 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")
.short("s") .short("s")
.long("game-storage") .long("game-storage")
.value_name("FOLDER") .value_name("FOLDER")
.help("Sets the download folder (defaults to the working directory).") .help("Sets the download folder (defaults to the working directory).")
.takes_value(true)) .takes_value(true))
.arg(Arg::with_name("movie-storage") .arg(Arg::with_name("movie-storage")
.short("m") .short("m")
.long("movie-storage") .long("movie-storage")
.value_name("FOLDER") .value_name("FOLDER")
.help("Sets the download folder for movies (defaults to the working directory).") .help("Sets the download folder for movies (defaults to the working directory).")
.takes_value(true)) .takes_value(true))
.arg(Arg::with_name("os") .arg(Arg::with_name("os")
.short("o") .short("o")
.long("os") .long("os")
.value_name("FILTER") .value_name("FILTER")
.help("Only sync files for this comma seperated list of operating systems.\n\ .help("Only sync files for this comma seperated list of operating systems.\n\
Valid values are 'linux', 'mac' and 'windows'.") Valid values are 'linux', 'mac' and 'windows'.")
.takes_value(true)) .takes_value(true))
.arg(Arg::with_name("language") .arg(Arg::with_name("language")
.short("l") .short("l")
.long("language") .long("language")
.value_name("FILTER") .value_name("FILTER")
.help("Only sync files for this comma seperated list of languages.") .help("Only sync files for this comma seperated list of languages.")
.takes_value(true)) .takes_value(true))
.arg(Arg::with_name("resolution") .arg(Arg::with_name("resolution")
.short("r") .short("r")
.long("resolution") .long("resolution")
.value_name("FILTER") .value_name("FILTER")
.help("Only sync movies for this comma seperated list of resolutions.") .help("Only sync movies for this comma seperated list of resolutions.")
.takes_value(true)) .takes_value(true))
.arg(Arg::with_name("skip-movies") .arg(Arg::with_name("skip-movies")
.short("f") .short("f")
.long("skip-movies") .long("skip-movies")
.help("Skip movie content.") .help("Skip movie content.")
.takes_value(false)) .takes_value(false))
.arg(Arg::with_name("skip-games") .arg(Arg::with_name("skip-games")
.short("g") .short("g")
.long("skip-games") .long("skip-games")
.help("Skip game content.") .help("Skip game content.")
.takes_value(false)) .takes_value(false))
.get_matches(); .get_matches();
let configfiles = ConfigFiles::new(); let configfiles = ConfigFiles::new();