From c4b4c597fabe295a3df9b08f729e7be75b725b0f Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 2 Oct 2019 01:13:53 -0600 Subject: [PATCH 01/15] Minimally compiling rust code for patreon release --- Cargo.lock | 53 +++++++++++++++++++++++++++++++ Cargo.toml | 5 +++ bootstrap.windows.toml | 2 +- src/frontend/rest/services/mod.rs | 2 ++ src/installer.rs | 10 ++++++ src/main.rs | 2 ++ src/sources/mod.rs | 3 ++ ui/src/views/SelectPackages.vue | 1 + 8 files changed, 77 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 5bd5a73..6a56e75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -585,6 +585,20 @@ name = "itoa" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "jsonwebtoken" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.93 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.93 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -630,6 +644,7 @@ dependencies = [ "fern 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.11.27 (registry+https://github.com/rust-lang/crates.io-index)", + "jsonwebtoken 6.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 1.8.7 (registry+https://github.com/rust-lang/crates.io-index)", "number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -646,6 +661,7 @@ dependencies = [ "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "web-view 0.4.1 (git+https://github.com/j-selby/web-view.git?rev=752106e4637356cbdb39a0bf1113ea3ae8a14243)", + "webbrowser 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "which 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1242,6 +1258,19 @@ dependencies = [ "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ring" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rustc-demangle" version = "0.1.15" @@ -1402,6 +1431,11 @@ name = "smallvec" version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "stable_deref_trait" version = "1.1.1" @@ -1823,6 +1857,11 @@ name = "unicode-xid" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "untrusted" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "url" version = "1.7.2" @@ -1906,6 +1945,15 @@ dependencies = [ "webview-sys 0.1.2 (git+https://github.com/j-selby/web-view.git?rev=752106e4637356cbdb39a0bf1113ea3ae8a14243)", ] +[[package]] +name = "webbrowser" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "webview-sys" version = "0.1.2" @@ -2080,6 +2128,7 @@ dependencies = [ "checksum indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7e81a7c05f79578dbc15793d8b619db9ba32b4577003ef3af1a91c416798c58d" "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" +"checksum jsonwebtoken 6.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a81d1812d731546d2614737bee92aa071d37e9afa1409bc374da9e5e70e70b22" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" @@ -2148,6 +2197,7 @@ dependencies = [ "checksum relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a" "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" "checksum reqwest 0.9.18 (registry+https://github.com/rust-lang/crates.io-index)" = "00eb63f212df0e358b427f0f40aa13aaea010b470be642ad422bcbca2feff2e4" +"checksum ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)" = "426bc186e3e95cac1e4a4be125a4aca7e84c2d616ffc02244eef36e2a60a093c" "checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" "checksum ryu 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "b96a9549dc8d48f2c283938303c4b5a77aa29bfbc5b54b084fb1630408899a8f" @@ -2171,6 +2221,7 @@ dependencies = [ "checksum slug 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" "checksum smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4c8cbcd6df1e117c2210e13ab5109635ad68a929fcbb8964dc965b76cb5ee013" "checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7" +"checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" "checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" "checksum string 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0bbfb8937e38e34c3444ff00afb28b0811d9554f15c5ad64d12b0308d1d1995" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" @@ -2213,6 +2264,7 @@ dependencies = [ "checksum unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426" "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f" "checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" "checksum urlencoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3df3561629a8bb4c57e5a2e4c43348d9e29c7c29d9b1c4c1f47166deca8f37ed" "checksum utf8-ranges 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9d50aa7650df78abf942826607c62468ce18d9019673d4a2ebe1865dbb96ffde" @@ -2224,6 +2276,7 @@ dependencies = [ "checksum want 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a05d9d966753fa4b5c8db73fcab5eed4549cfe0e1e4e66911e5564a0085c35d1" "checksum want 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" "checksum web-view 0.4.1 (git+https://github.com/j-selby/web-view.git?rev=752106e4637356cbdb39a0bf1113ea3ae8a14243)" = "" +"checksum webbrowser 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "97d468a911faaaeb783693b004e1c62e0063e646b0afae5c146cd144e566e66d" "checksum webview-sys 0.1.2 (git+https://github.com/j-selby/web-view.git?rev=752106e4637356cbdb39a0bf1113ea3ae8a14243)" = "" "checksum which 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b57acb10231b9493c8472b20cb57317d0679a49e0bdbee44b3b803a6473af164" "checksum widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6" diff --git a/Cargo.toml b/Cargo.toml index 2e25f93..3e3d039 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,11 @@ chrono = "0.4.6" clap = "2.32.0" +# used to open a link to the users default browser +webbrowser = "0.5.2" +# used in JWT based package authentication +jsonwebtoken = "6" + [build-dependencies] walkdir = "2.2.7" serde = "1.0.89" diff --git a/bootstrap.windows.toml b/bootstrap.windows.toml index 1c8d2cf..e2f752c 100644 --- a/bootstrap.windows.toml +++ b/bootstrap.windows.toml @@ -1,2 +1,2 @@ name = "yuzu" -target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v8.toml" +target_url = "https://raw.githubusercontent.com/jroweboy/liftinstall/master/config.windows.v8.toml" diff --git a/src/frontend/rest/services/mod.rs b/src/frontend/rest/services/mod.rs index df9ea28..50eda2d 100644 --- a/src/frontend/rest/services/mod.rs +++ b/src/frontend/rest/services/mod.rs @@ -21,6 +21,7 @@ use futures::future::Future as _; use futures::sink::Sink; mod attributes; +mod authentication; mod config; mod default_path; mod dark_mode; @@ -137,6 +138,7 @@ impl Service for WebService { (Method::Get, "/api/exit") => exit::handle(self, req), (Method::Get, "/api/packages") => packages::handle(self, req), (Method::Get, "/api/installation-status") => installation_status::handle(self, req), + (Method::Post, "/api/check-auth") => authentication::handle(self, req), (Method::Post, "/api/start-install") => install::handle(self, req), (Method::Post, "/api/uninstall") => uninstall::handle(self, req), (Method::Post, "/api/update-updater") => update_updater::handle(self, req), diff --git a/src/installer.rs b/src/installer.rs index b64df8b..f7d9dce 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -53,11 +53,20 @@ pub enum InstallMessage { EOF, } +#[derive(Serialize, Deserialize, Clone)] +pub struct Credentials { + #[serde(default)] + pub username: String, + #[serde(default)] + pub token: String, +} + /// Metadata about the current installation itself. #[derive(Serialize, Deserialize, Clone)] pub struct InstallationDatabase { pub packages: Vec, pub shortcuts: Vec, + pub credentials: Credentials, } impl InstallationDatabase { @@ -66,6 +75,7 @@ impl InstallationDatabase { InstallationDatabase { packages: Vec::new(), shortcuts: Vec::new(), + credentials: Credentials{username: String::new(), token: String::new()}, } } } diff --git a/src/main.rs b/src/main.rs index 454ef24..6efa301 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,6 +47,8 @@ extern crate slug; #[cfg(not(windows))] extern crate sysinfo; +extern crate jsonwebtoken as jwt; + mod archives; mod config; mod frontend; diff --git a/src/sources/mod.rs b/src/sources/mod.rs index 345ffa6..2cf4965 100644 --- a/src/sources/mod.rs +++ b/src/sources/mod.rs @@ -6,12 +6,15 @@ pub mod types; pub mod github; +pub mod patreon; + use self::types::ReleaseSource; /// Returns a ReleaseSource by a name, if possible pub fn get_by_name(name: &str) -> Option> { match name { "github" => Some(Box::new(github::GithubReleases::new())), + "patreon" => Some(Box::new(patreon::PatreonReleases::new())), _ => None, } } diff --git a/ui/src/views/SelectPackages.vue b/ui/src/views/SelectPackages.vue index 863b5a9..110706b 100644 --- a/ui/src/views/SelectPackages.vue +++ b/ui/src/views/SelectPackages.vue @@ -77,6 +77,7 @@ export default { })) }, install: function () { + // TODO route instead to an authentication endpoint. if this package needs auth, then call the backend and route to the auth page this.$router.push('/install/regular') }, go_back: function () { From a7057dfed3676419e2abea6cb407902714b42367 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 21 Oct 2019 01:01:40 -0600 Subject: [PATCH 02/15] Add new dependencies and update Cargo.lock --- Cargo.lock | 27 ++++++++++++++++++--------- Cargo.toml | 4 +++- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a56e75..ec913bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -637,6 +637,7 @@ dependencies = [ name = "liftinstall" version = "0.1.0" dependencies = [ + "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -649,7 +650,7 @@ dependencies = [ "mime_guess 1.8.7 (registry+https://github.com/rust-lang/crates.io-index)", "number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.9.18 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.9.21 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.93 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.93 (registry+https://github.com/rust-lang/crates.io-index)", @@ -749,13 +750,11 @@ dependencies = [ [[package]] name = "mime_guess" -version = "2.0.0-alpha.6" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "mime 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", - "phf 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", - "phf_codegen 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 2.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1228,7 +1227,7 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.9.18" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1243,7 +1242,7 @@ dependencies = [ "hyper-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "mime 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", - "mime_guess 2.0.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mime_guess 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.93 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1256,6 +1255,7 @@ dependencies = [ "tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", + "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2015,6 +2015,14 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "winres" version = "0.1.11" @@ -2144,7 +2152,7 @@ dependencies = [ "checksum mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" "checksum mime 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)" = "3e27ca21f40a310bd06d9031785f4801710d566c184a6e15bad4f1d9b65f9425" "checksum mime_guess 1.8.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0d977de9ee851a0b16e932979515c0f3da82403183879811bc97d50bd9cc50f7" -"checksum mime_guess 2.0.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)" = "30de2e4613efcba1ec63d8133f344076952090c122992a903359be5a4f99c3ed" +"checksum mime_guess 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1a0ed03949aef72dbdf3116a383d7b38b4768e6f960528cd6a6044aa9ed68599" "checksum miniz_oxide 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c468f2369f07d651a5d0bb2c9079f8488a66d5466efe42d0c5c6466edcb7f71e" "checksum miniz_oxide_c_api 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7fe927a42e3807ef71defb191dc87d4e24479b221e67015fe38ae2b7b447bab" "checksum mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)" = "83f51996a3ed004ef184e16818edc51fadffe8e7ca68be67f9dee67d84d0ff23" @@ -2196,7 +2204,7 @@ dependencies = [ "checksum regex-syntax 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "9d76410686f9e3a17f06128962e0ecc5755870bb890c34820c7af7f1db2e1d48" "checksum relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a" "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" -"checksum reqwest 0.9.18 (registry+https://github.com/rust-lang/crates.io-index)" = "00eb63f212df0e358b427f0f40aa13aaea010b470be642ad422bcbca2feff2e4" +"checksum reqwest 0.9.21 (registry+https://github.com/rust-lang/crates.io-index)" = "02b7e953e14c6f3102b7e8d1f1ee3abf5ecee80b427f5565c9389835cecae95c" "checksum ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)" = "426bc186e3e95cac1e4a4be125a4aca7e84c2d616ffc02244eef36e2a60a093c" "checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" @@ -2286,6 +2294,7 @@ dependencies = [ "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" "checksum winres 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ff4fb510bbfe5b8992ff15f77a2e6fe6cf062878f0eda00c0f44963a807ca5dc" "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" "checksum xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" diff --git a/Cargo.toml b/Cargo.toml index 3e3d039..6830cb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ futures = "0.1.25" mime_guess = "1.8.6" url = "1.7.2" -reqwest = "0.9.12" +reqwest = "0.9.21" number_prefix = "0.3.0" serde = "1.0.89" @@ -42,6 +42,8 @@ clap = "2.32.0" webbrowser = "0.5.2" # used in JWT based package authentication jsonwebtoken = "6" +# used to decode the public key for verifying JWT tokens +base64 = "0.10.1" [build-dependencies] walkdir = "2.2.7" From 5409b32bf01b81c58012cd0da883f3bbce11baf0 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 21 Oct 2019 01:09:16 -0600 Subject: [PATCH 03/15] Add patreon authentication for early access releases --- config.linux.patreon.toml | 39 ++++ config.windows.patreon.toml | 51 +++++ src/config.rs | 28 +++ src/frontend/rest/services/authentication.rs | 187 +++++++++++++++++++ src/frontend/rest/services/browser.rs | 29 +++ src/frontend/rest/services/mod.rs | 2 + src/http.rs | 16 +- src/installer.rs | 5 +- src/main.rs | 2 + src/native/interop.cpp | 2 +- src/sources/github/mod.rs | 1 + src/sources/patreon.rs | 108 +++++++++++ src/sources/types.rs | 1 + src/tasks/download_pkg.rs | 2 +- ui/src/main.js | 39 ++++ ui/src/router.js | 6 + ui/src/views/AuthenticationView.vue | 124 ++++++++++++ ui/src/views/DownloadConfig.vue | 20 +- ui/src/views/SelectPackages.vue | 141 ++++++++------ 19 files changed, 728 insertions(+), 75 deletions(-) create mode 100644 config.linux.patreon.toml create mode 100644 config.windows.patreon.toml create mode 100644 src/frontend/rest/services/authentication.rs create mode 100644 src/frontend/rest/services/browser.rs create mode 100644 src/sources/patreon.rs create mode 100644 ui/src/views/AuthenticationView.vue diff --git a/config.linux.patreon.toml b/config.linux.patreon.toml new file mode 100644 index 0000000..14ca6e6 --- /dev/null +++ b/config.linux.patreon.toml @@ -0,0 +1,39 @@ +installing_message = "Reminder: yuzu is an experimental emulator. Stuff will break!" +hide_advanced = true + +[[authentication]] + # Base64 encoded version of the public key for validating the JWT token. Must be in DER format + pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB" + # URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure + # "patreonInfo": { "linked": false, "activeSubscription": false } + # If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download + auth_url = "https://api.yuzu-emu.org/jwt/installer/" + [[authentication.validation]] + iss = "citra-core" + aud = "installer" + +[[packages]] +name = "yuzu" +default = true +requires_authorization = false +description = "The yuzu Mainline is for plebs. Please upgrade to patreon to git gud." +default = true + [packages.source] + name = "github" + match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$" + [packages.source.config] + repo = "yuzu-emu/yuzu-nightly" + +[[packages]] +name = "yuzu Early Access" +description = "The build for all those epic Chads out there who didn't have to steal 5 dollar from their mom to pay for this." +# Displayed when the package has no authentication for the user +need_authentication_description = "Click here to sign in with your yuzu account for Early Access" +# Displayed when the package has an authentication, but the user is not authorized +need_authorization_description = "You are signed in, but you do not have a current subscription! Click here for more details" +requires_authorization = true + [packages.source] + name = "github" + match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$" + [packages.source.config] + repo = "yuzu-emu/yuzu-canary" diff --git a/config.windows.patreon.toml b/config.windows.patreon.toml new file mode 100644 index 0000000..7ade327 --- /dev/null +++ b/config.windows.patreon.toml @@ -0,0 +1,51 @@ +installing_message = "Reminder: yuzu is an experimental emulator. Stuff will break!" +hide_advanced = true + +[authentication] +# Base64 encoded version of the public key for validating the JWT token. Must be in DER format +pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB" +# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure +# "patreonInfo": { "linked": false, "activeSubscription": false } +# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download +auth_url = "https://api.yuzu-emu.org/jwt/installer/" + [authentication.validation] + iss = "citra-core" + aud = "installer" + +[[packages]] +name = "yuzu" +default = true +requires_authorization = false +description = "The yuzu Mainline is for plebs. Please upgrade to patreon to git gud." + [packages.source] + name = "github" + match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$" + [packages.source.config] + repo = "yuzu-emu/yuzu-nightly" + [[packages.shortcuts]] + name = "yuzu" + relative_path = "mainline/yuzu.exe" + description = "Launch yuzu (Mainline version)" + +[[packages]] +name = "yuzu Early Access" +description = "The build for all those epic Chads out there who didn't have to steal 5 dollar from their mom to pay for this." +# Displayed when the package has no authentication for the user +need_authentication_description = "Click here to sign in with your yuzu account for Early Access" +# Displayed when the package has an authentication, but the user has not linked their account +need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details" +# Displayed when the package has an authentication, but the user has not linked their account +need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details" +# Displayed when the package has an authentication, but the user has not linked their account +need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details" +requires_authorization = true + [packages.source] + name = "patreon" + match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$" + [packages.source.config] + repo = "earlyaccess" + [[packages.shortcuts]] + name = "yuzu Early Access" + relative_path = "earlyaccess/yuzu.exe" + description = "Launch yuzu Early Access" + diff --git a/src/config.rs b/src/config.rs index e5ac346..a1aeea5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -36,6 +36,32 @@ pub struct PackageDescription { pub source: PackageSource, #[serde(default)] pub shortcuts: Vec, + #[serde(default)] + pub requires_authorization: Option, + #[serde(default)] + pub need_authentication_description: Option, + #[serde(default)] + pub need_link_description: Option, + #[serde(default)] + pub need_subscription_description: Option, + #[serde(default)] + pub need_reward_tier_description: Option, +} + +/// Configuration for validating the JWT token +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct JWTValidation { + pub iss: Option, + // This can technically be a Vec as well, but thats a pain to support atm + pub aud: Option, +} + +/// The configuration for this release. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthenticationConfig { + pub pub_key_base64: String, + pub auth_url: String, + pub validation: Option, } /// Describes the application itself. @@ -66,6 +92,8 @@ pub struct Config { pub packages: Vec, #[serde(default)] pub hide_advanced: bool, + #[serde(default)] + pub authentication: Option, } impl Config { diff --git a/src/frontend/rest/services/authentication.rs b/src/frontend/rest/services/authentication.rs new file mode 100644 index 0000000..cdbb2fa --- /dev/null +++ b/src/frontend/rest/services/authentication.rs @@ -0,0 +1,187 @@ + +use http::build_async_client; + +use hyper::header::{ContentLength, ContentType}; +use reqwest::header::{USER_AGENT}; +use futures::{Stream, Future}; +use jwt::{decode, Validation, Algorithm}; + +use frontend::rest::services::{WebService, Request, Response, default_future}; +use frontend::rest::services::Future as InternalFuture; +use logging::LoggingErrors; +use url::form_urlencoded; +use std::collections::HashMap; +use std::sync::Arc; + +/// claims struct, it needs to derive `Serialize` and/or `Deserialize` +#[derive(Debug, Serialize, Deserialize)] +struct JWTClaims { + sub: String, + iss: String, + aud: String, + exp: usize, + #[serde(default)] + roles: Vec, + #[serde(rename = "releaseChannels", default)] + channels: Vec, + #[serde(rename = "IsPatreonAccountLinked")] + is_linked: bool, + #[serde(rename = "IsPatreonSubscriptionActive")] + is_subscribed: bool, +} + +fn get_text(future: impl Future) -> impl Future { + future.map(|mut response| { + // Get the body of the response + match response.status() { + reqwest::StatusCode::OK => + Ok(response.text() + .map_err(|e| { + error!("Error while converting the response to text {:?}", e); + Response::new() + .with_status(hyper::StatusCode::InternalServerError) + })), + _ => { + error!("Error wrong response code from server {:?}", response.status()); + Err(Response::new() + .with_status(hyper::StatusCode::InternalServerError)) + } + } + }) + .map_err(|err| { + error!("Error cannot get text on errored stream {:?}", err); + Response::new() + .with_status(hyper::StatusCode::InternalServerError) + }) + .and_then(|x| x) + .flatten() +} + +pub fn handle(service: &WebService, _req: Request) -> InternalFuture { + let framework = service.framework.read().log_expect("InstallerFramework has been dirtied"); + let credentials = framework.database.credentials.clone(); + let config = framework.config.clone().unwrap(); + + // If authentication isn't configured, just return immediately + if config.authentication.is_none() { + return default_future(Response::new().with_status(hyper::Ok).with_body("{}")); + } + + // Create moveable framework references so that the lambdas can write to them later + let write_cred_fw = Arc::clone(&service.framework); + + Box::new( + _req.body().concat2().map(move |body| { + let req = form_urlencoded::parse(body.as_ref()) + .into_owned() + .collect::>(); + + // Determine which credentials we should use + let (username, token) = { + let req_username = req.get("username").unwrap(); + let req_token = req.get("token").unwrap(); + // if the user didn't provide credentials, and theres nothing stored in the database, return an early error + let req_cred_valid = !req_username.is_empty() && !req_token.is_empty(); + let stored_cred_valid = !credentials.username.is_empty() && !credentials.token.is_empty(); + if !req_cred_valid && !stored_cred_valid { + info!("No passed in credential and no stored credentials to validate"); + return default_future(Response::new().with_status(hyper::BadRequest)); + } + if req_cred_valid { + (req.get("username").unwrap().clone(), req.get("token").unwrap().clone()) + } else { + (credentials.username.clone(), credentials.token.clone()) + } + }; + + let authentication = config.authentication.unwrap(); + + // Get the public key for this authentication url + let pub_key = if authentication.pub_key_base64.is_empty() { + vec![] + } else { + match base64::decode(&authentication.pub_key_base64) { + Ok(v) => v, + Err(err) => { + error!("Configured public key was not empty and did not decode as base64 {:?}", err); + return default_future(Response::new().with_status(hyper::StatusCode::InternalServerError)); + }, + } + }; + + // Build the HTTP client up + let client = match build_async_client() { + Ok(v) => v, + Err(_) => { + return default_future(Response::new().with_status(hyper::StatusCode::InternalServerError)); + }, + }; + + // call the authentication URL to see if we are authenticated + Box::new(get_text( + client.post(&authentication.auth_url) + .header(USER_AGENT, "liftinstall (j-selby)") + .header("X-USERNAME", username.clone()) + .header("X-TOKEN", token.clone()) + .send() + ).map(move |body| { + // Configure validation for audience and issuer if the configuration provides it + let validation = match authentication.validation { + Some(v) => { + let mut valid = Validation::new(Algorithm::RS256); + valid.iss = v.iss; + if v.aud.is_some() { + valid.set_audience(&v.aud.unwrap()); + } + valid + } + None => Validation::default() + }; + + // Verify the JWT token + let tok = match decode::(&body, pub_key.as_slice(), &validation) { + Ok(v) => v, + Err(v) => { + error!("Error while decoding the JWT. error: {:?} str: {:?}", v, &body); + return Err(Response::new().with_status(hyper::StatusCode::InternalServerError)); + }, + }; + + { + // Store the validated username and password into the installer database + let mut framework = write_cred_fw.write().log_expect("InstallerFramework has been dirtied"); + framework.database.credentials.username = username.clone(); + framework.database.credentials.token = token.clone(); + // And store the JWT token temporarily in the + framework.authorization_token = Some(body.clone()); + } + + // Convert the json to a string and return the json token + match serde_json::to_string(&tok.claims) { + Ok(v) => Ok(v), + Err(e) => { + error!("Error while converting the claims to JSON string: {:?}", e); + Err(Response::new().with_status(hyper::StatusCode::InternalServerError)) + } + } + }) + .and_then(|res| res) + .map(|out| { + // Finally return the JSON with the response + info!("successfully verified username and token"); + Response::new() + .with_header(ContentLength(out.len() as u64)) + .with_header(ContentType::json()) + .with_status(hyper::StatusCode::Ok) + .with_body(out) + }) + .or_else(|err| { + // Convert the Err value into an Ok value since the error code from this HTTP request is an Ok(response) + Ok(err) + }) + ) + }) + // Flatten the internal future into the output response future + .flatten() + ) +} \ No newline at end of file diff --git a/src/frontend/rest/services/browser.rs b/src/frontend/rest/services/browser.rs new file mode 100644 index 0000000..32885cc --- /dev/null +++ b/src/frontend/rest/services/browser.rs @@ -0,0 +1,29 @@ + + +use frontend::rest::services::{WebService, Request, Response}; +use frontend::rest::services::Future as InternalFuture; +use futures::{Stream, Future}; +use url::form_urlencoded; +use std::collections::HashMap; +use hyper::header::ContentType; + +pub fn handle(_service: &WebService, _req: Request) -> InternalFuture { + Box::new( + _req.body().concat2().map(move |body| { + let req = form_urlencoded::parse(body.as_ref()) + .into_owned() + .collect::>(); + if webbrowser::open( req.get("url").unwrap()).is_ok() { + Response::new() + .with_status(hyper::Ok) + .with_header(ContentType::json()) + .with_body("{}") + } else { + Response::new() + .with_status(hyper::BadRequest) + .with_header(ContentType::json()) + .with_body("{}") + } + })) +} + diff --git a/src/frontend/rest/services/mod.rs b/src/frontend/rest/services/mod.rs index 50eda2d..1b7473e 100644 --- a/src/frontend/rest/services/mod.rs +++ b/src/frontend/rest/services/mod.rs @@ -22,6 +22,7 @@ use futures::sink::Sink; mod attributes; mod authentication; +mod browser; mod config; mod default_path; mod dark_mode; @@ -140,6 +141,7 @@ impl Service for WebService { (Method::Get, "/api/installation-status") => installation_status::handle(self, req), (Method::Post, "/api/check-auth") => authentication::handle(self, req), (Method::Post, "/api/start-install") => install::handle(self, req), + (Method::Post, "/api/open-browser") => browser::handle(self, req), (Method::Post, "/api/uninstall") => uninstall::handle(self, req), (Method::Post, "/api/update-updater") => update_updater::handle(self, req), (Method::Get, _) => static_files::handle(self, req), diff --git a/src/http.rs b/src/http.rs index 1a813d5..e892d5e 100644 --- a/src/http.rs +++ b/src/http.rs @@ -36,16 +36,22 @@ pub fn build_async_client() -> Result { } /// Streams a file from a HTTP server. -pub fn stream_file(url: &str, mut callback: F) -> Result<(), String> +pub fn stream_file(url: &str, authorization: Option, mut callback: F) -> Result<(), String> where F: FnMut(Vec, u64) -> (), { assert_ssl(url)?; - let mut client = build_client()? - .get(url) - .send() - .map_err(|x| format!("Failed to GET resource: {:?}", x))?; + let mut client = if authorization.is_some() { + build_client()?.get(url) + .header("Authorization", format!("Bearer {}", authorization.unwrap())) + .send() + .map_err(|x| format!("Failed to GET resource: {:?}", x))? + } else { + build_client()?.get(url) + .send() + .map_err(|x| format!("Failed to GET resource: {:?}", x))? + }; let size = match client.headers().get(CONTENT_LENGTH) { Some(ref v) => v diff --git a/src/installer.rs b/src/installer.rs index f7d9dce..d0c9f10 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -92,6 +92,7 @@ pub struct InstallerFramework { // If we just completed an uninstall, and we should clean up after ourselves. pub burn_after_exit: bool, pub launcher_path: Option, + pub authorization_token: Option, } /// Contains basic properties on the status of the session. Subset of InstallationFramework. @@ -262,7 +263,7 @@ impl InstallerFramework { let mut downloaded = 0; let mut data_storage: Vec = Vec::new(); - http::stream_file(tool, |data, size| { + http::stream_file(tool, None, |data, size| { { data_storage.extend_from_slice(&data); } @@ -440,6 +441,7 @@ impl InstallerFramework { is_launcher: false, burn_after_exit: false, launcher_path: None, + authorization_token: None, } } @@ -467,6 +469,7 @@ impl InstallerFramework { is_launcher: false, burn_after_exit: false, launcher_path: None, + authorization_token: None, }) } } diff --git a/src/main.rs b/src/main.rs index 6efa301..22f04a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,6 +49,8 @@ extern crate sysinfo; extern crate jsonwebtoken as jwt; +extern crate base64; + mod archives; mod config; mod frontend; diff --git a/src/native/interop.cpp b/src/native/interop.cpp index 18be048..7121614 100644 --- a/src/native/interop.cpp +++ b/src/native/interop.cpp @@ -45,7 +45,7 @@ extern "C" int saveShortcut( const wchar_t *args, const wchar_t *workingDir) { - char *errStr = NULL; + const char *errStr = NULL; HRESULT h; IShellLink *shellLink = NULL; IPersistFile *persistFile = NULL; diff --git a/src/sources/github/mod.rs b/src/sources/github/mod.rs index 35c0f43..a8a75a7 100644 --- a/src/sources/github/mod.rs +++ b/src/sources/github/mod.rs @@ -107,6 +107,7 @@ impl ReleaseSource for GithubReleases { files.push(File { name: string.to_string(), url: url.to_string(), + requires_authorization: false, }); } diff --git a/src/sources/patreon.rs b/src/sources/patreon.rs new file mode 100644 index 0000000..b2b888b --- /dev/null +++ b/src/sources/patreon.rs @@ -0,0 +1,108 @@ +//! github/mod.rs +//! +//! Contains the Github API implementation of a release source. + +use sources::types::*; +use http::build_client; +use reqwest::header::USER_AGENT; +use reqwest::StatusCode; + +pub struct PatreonReleases {} + +/// The configuration for this release. +#[derive(Serialize, Deserialize)] +struct PatreonConfig { + repo: String, +} + +impl PatreonReleases { + pub fn new() -> Self { + PatreonReleases {} + } +} + +impl ReleaseSource for PatreonReleases { + fn get_current_releases(&self, _config: &TomlValue) -> Result, String> { + let config: PatreonConfig = match _config.clone().try_into() { + Ok(v) => v, + Err(v) => return Err(format!("Failed to parse release config: {:?}", v)), + }; + + let mut results: Vec = Vec::new(); + + // Build the HTTP client up + let client = build_client()?; + let mut response = client + .get(&format!( + "https://api.yuzu-emu.org/downloads/{}/", + config.repo + )) + .header(USER_AGENT, "liftinstall (j-selby)") + .send() + .map_err(|x| format!("Error while sending HTTP request: {:?}", x))?; + + match response.status() { + StatusCode::OK => {} + StatusCode::FORBIDDEN => { + return Err( + "You are not eligible to download this release".to_string(), + ); + } + _ => { + return Err(format!("Bad status code: {:?}.", response.status())); + } + } + + let body = response + .text() + .map_err(|x| format!("Failed to decode HTTP response body: {:?}", x))?; + + let result: serde_json::Value = serde_json::from_str(&body) + .map_err(|x| format!("Failed to parse response: {:?}", x))?; + + // Parse JSON from server + let mut files = Vec::new(); + + let id: u64 = match result["version"].as_u64() { + Some(v) => v, + None => return Err("JSON payload missing information about ID".to_string()), + }; + + let downloads = match result["files"].as_array() { + Some(v) => v, + None => return Err("JSON payload not an array".to_string()), + }; + + for file in downloads.iter() { + let string = match file["name"].as_str() { + Some(v) => v, + None => { + return Err( + "JSON payload missing information about release name".to_string() + ); + } + }; + + let url = match file["url"].as_str() { + Some(v) => v, + None => { + return Err( + "JSON payload missing information about release URL".to_string() + ); + } + }; + + files.push(File { + name: string.to_string(), + url: url.to_string(), + requires_authorization: true, + }); + } + + results.push(Release { + version: Version::new_number(id), + files, + }); + Ok(results) + } +} diff --git a/src/sources/types.rs b/src/sources/types.rs index e45656a..285c5d7 100644 --- a/src/sources/types.rs +++ b/src/sources/types.rs @@ -66,6 +66,7 @@ impl Ord for Version { pub struct File { pub name: String, pub url: String, + pub requires_authorization: bool, } impl File {} diff --git a/src/tasks/download_pkg.rs b/src/tasks/download_pkg.rs index 0817dc4..3262ec0 100644 --- a/src/tasks/download_pkg.rs +++ b/src/tasks/download_pkg.rs @@ -54,7 +54,7 @@ impl Task for DownloadPackageTask { let mut downloaded = 0; let mut data_storage: Vec = Vec::new(); - stream_file(&file.url, |data, size| { + stream_file(&file.url, context.authorization_token.clone(), |data, size| { { data_storage.extend_from_slice(&data); } diff --git a/ui/src/main.js b/ui/src/main.js index 076dafa..3bef367 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -84,6 +84,13 @@ var app = new Vue({ attrs: base_attributes, config: {}, install_location: '', + username: '', + token: '', + jwt_token: {}, + is_authenticated: false, + is_linked: false, + is_subscribed: false, + has_reward_tier: false, // If the option to pick an install location should be provided show_install_location: true, metadata: { @@ -111,6 +118,38 @@ var app = new Vue({ } ) }, + check_authentication: function (success, error) { + var that = this; + var app = this.$root; + + app.ajax('/api/check-auth', function (auth) { + that.jwt_token = auth; + that.is_authenticated = Object.keys(that.jwt_token).length !== 0 && that.jwt_token.constructor === Object; + if (that.is_authenticated) { + // Give all permissions to vip roles + if (that.jwt_token.roles.indexOf("vip") > -1) { + that.is_linked = true; + that.is_subscribed = true; + that.has_reward_tier = true; + } else { + that.is_linked = that.jwt_token.isPatreonAccountLinked; + that.is_subscribed = that.jwt_token.isPatreonSubscriptionActive; + that.has_reward_tier = that.jwt_token.releaseChannels.indexOf("early-release") > -1; + } + } + if (success) { + success(); + } + }, function (e) { + if (error) { + error(); + } + }, { + "username": app.$data.username, + "token": app.$data.token + }) + }, + ajax: ajax, stream_ajax: stream_ajax } diff --git a/ui/src/router.js b/ui/src/router.js index 8381ecf..29db2f4 100644 --- a/ui/src/router.js +++ b/ui/src/router.js @@ -7,6 +7,7 @@ import ErrorView from './views/ErrorView.vue' import InstallPackages from './views/InstallPackages.vue' import CompleteView from './views/CompleteView.vue' import ModifyView from './views/ModifyView.vue' +import AuthenticationView from './views/AuthenticationView.vue' Vue.use(Router) @@ -47,6 +48,11 @@ export default new Router({ name: 'modify', component: ModifyView }, + { + path: '/authentication', + name: 'authentication', + component: AuthenticationView + }, { path: '/', redirect: '/config' diff --git a/ui/src/views/AuthenticationView.vue b/ui/src/views/AuthenticationView.vue new file mode 100644 index 0000000..df7ea2e --- /dev/null +++ b/ui/src/views/AuthenticationView.vue @@ -0,0 +1,124 @@ + + + diff --git a/ui/src/views/DownloadConfig.vue b/ui/src/views/DownloadConfig.vue index 045150b..fd95dad 100644 --- a/ui/src/views/DownloadConfig.vue +++ b/ui/src/views/DownloadConfig.vue @@ -29,29 +29,27 @@ export default { this.$root.ajax('/api/config', function (e) { that.$root.config = e - that.choose_next_state() + // Update the updater if needed + if (that.$root.config.new_tool) { + this.$router.push('/install/updater') + return + } + + that.$root.check_authentication(that.choose_next_state, that.choose_next_state) }, function (e) { - console.error('Got error while downloading config: ' + - e) + console.error('Got error while downloading config: ' + e) if (that.$root.metadata.is_launcher) { // Just launch the target application that.$root.exit() } else { that.$router.replace({ name: 'showerr', - params: { msg: 'Got error while downloading config: ' + - e } }) + params: { msg: 'Got error while downloading config: ' + e } }) } }) }, choose_next_state: function () { var app = this.$root - // Update the updater if needed - if (app.config.new_tool) { - this.$router.push('/install/updater') - return - } - if (app.metadata.preexisting_install) { app.install_location = app.metadata.install_path diff --git a/ui/src/views/SelectPackages.vue b/ui/src/views/SelectPackages.vue index 110706b..9772c40 100644 --- a/ui/src/views/SelectPackages.vue +++ b/ui/src/views/SelectPackages.vue @@ -1,63 +1,87 @@ From 9bec77a2dbcc1d8402aeee67184278739e663b6c Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 21 Oct 2019 01:12:43 -0600 Subject: [PATCH 04/15] Update bootstrap repo to point to my url --- bootstrap.windows.toml | 2 +- config.windows.patreon.toml | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/bootstrap.windows.toml b/bootstrap.windows.toml index e2f752c..af7253a 100644 --- a/bootstrap.windows.toml +++ b/bootstrap.windows.toml @@ -1,2 +1,2 @@ name = "yuzu" -target_url = "https://raw.githubusercontent.com/jroweboy/liftinstall/master/config.windows.v8.toml" +target_url = "https://raw.githubusercontent.com/jroweboy/liftinstall/patreon/config.windows.patreon.toml" diff --git a/config.windows.patreon.toml b/config.windows.patreon.toml index 7ade327..673a379 100644 --- a/config.windows.patreon.toml +++ b/config.windows.patreon.toml @@ -14,18 +14,17 @@ auth_url = "https://api.yuzu-emu.org/jwt/installer/" [[packages]] name = "yuzu" +description = "Includes frequent updates to yuzu with all the latest reviewed and tested features." default = true -requires_authorization = false -description = "The yuzu Mainline is for plebs. Please upgrade to patreon to git gud." [packages.source] name = "github" - match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$" + match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$" [packages.source.config] - repo = "yuzu-emu/yuzu-nightly" + repo = "yuzu-emu/yuzu-mainline" [[packages.shortcuts]] name = "yuzu" - relative_path = "mainline/yuzu.exe" - description = "Launch yuzu (Mainline version)" + relative_path = "yuzu-windows-msvc/yuzu.exe" + description = "Launch yuzu" [[packages]] name = "yuzu Early Access" @@ -41,11 +40,11 @@ need_reward_tier_description = "You are signed in, but are not backing an eligib requires_authorization = true [packages.source] name = "patreon" - match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$" + match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$" [packages.source.config] repo = "earlyaccess" [[packages.shortcuts]] name = "yuzu Early Access" - relative_path = "earlyaccess/yuzu.exe" + relative_path = "yuzu-windows-msvc/yuzu.exe" description = "Launch yuzu Early Access" From 288518cd7897f07e42fcf9adb3f5631822567b7a Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 24 Oct 2019 09:15:10 -0600 Subject: [PATCH 05/15] Minor fixes --- ui/src/main.js | 4 ++-- ui/src/views/AuthenticationView.vue | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/main.js b/ui/src/main.js index 3bef367..fc9dca2 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -132,8 +132,8 @@ var app = new Vue({ that.is_subscribed = true; that.has_reward_tier = true; } else { - that.is_linked = that.jwt_token.isPatreonAccountLinked; - that.is_subscribed = that.jwt_token.isPatreonSubscriptionActive; + that.is_linked = that.jwt_token.IsPatreonAccountLinked; + that.is_subscribed = that.jwt_token.IsPatreonSubscriptionActive; that.has_reward_tier = that.jwt_token.releaseChannels.indexOf("early-release") > -1; } } diff --git a/ui/src/views/AuthenticationView.vue b/ui/src/views/AuthenticationView.vue index df7ea2e..1735e4d 100644 --- a/ui/src/views/AuthenticationView.vue +++ b/ui/src/views/AuthenticationView.vue @@ -84,7 +84,7 @@ export default { return this.verification_opened && this.$root.is_linked && !this.$root.is_subscribed; }, tier_not_selected: function() { - return this.verification_opened && this.$root.is_subscribed && !this.$root.has_reward_tier; + return this.verification_opened && this.$root.is_linked && this.$root.is_subscribed && !this.$root.has_reward_tier; } }, methods: { From 2b4b59320effa7c316f0d6472f62e1998c338ee8 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 1 Nov 2019 11:15:16 -0600 Subject: [PATCH 06/15] Add authentication task dependency to check for auth on install --- Cargo.lock | 6 +- config.windows.patreon.toml | 2 +- src/frontend/rest/services/authentication.rs | 233 +++++++++++-------- src/frontend/rest/services/mod.rs | 2 +- src/frontend/ui/mod.rs | 2 +- src/installer.rs | 9 +- src/tasks/check_authorization.rs | 69 ++++++ src/tasks/download_pkg.rs | 32 +-- src/tasks/mod.rs | 4 + ui/src/main.js | 10 +- ui/src/router.js | 6 + ui/src/views/AuthenticationView.vue | 8 +- ui/src/views/InstallPackages.vue | 11 +- ui/src/views/ReAuthenticationView.vue | 47 ++++ 14 files changed, 316 insertions(+), 125 deletions(-) create mode 100644 src/tasks/check_authorization.rs create mode 100644 ui/src/views/ReAuthenticationView.vue diff --git a/Cargo.lock b/Cargo.lock index ec913bc..4def34c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -698,7 +698,7 @@ dependencies = [ [[package]] name = "lzma-sys" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2053,7 +2053,7 @@ name = "xz2" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "lzma-sys 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "lzma-sys 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2145,7 +2145,7 @@ dependencies = [ "checksum lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" -"checksum lzma-sys 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "16b5c59c57cc4d39e7999f50431aa312ea78af7c93b23fbb0c3567bd672e7f35" +"checksum lzma-sys 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "53e48818fd597d46155132bbbb9505d6d1b3d360b4ee25cfa91c406f8a90fe91" "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" "checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39" "checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3" diff --git a/config.windows.patreon.toml b/config.windows.patreon.toml index 673a379..2276238 100644 --- a/config.windows.patreon.toml +++ b/config.windows.patreon.toml @@ -28,7 +28,7 @@ default = true [[packages]] name = "yuzu Early Access" -description = "The build for all those epic Chads out there who didn't have to steal 5 dollar from their mom to pay for this." +description = "Bonus preview release for project supporters. Thanks for your support!" # Displayed when the package has no authentication for the user need_authentication_description = "Click here to sign in with your yuzu account for Early Access" # Displayed when the package has an authentication, but the user has not linked their account diff --git a/src/frontend/rest/services/authentication.rs b/src/frontend/rest/services/authentication.rs index cdbb2fa..6ce4b63 100644 --- a/src/frontend/rest/services/authentication.rs +++ b/src/frontend/rest/services/authentication.rs @@ -1,5 +1,5 @@ -use http::build_async_client; +use http::{build_client, build_async_client}; use hyper::header::{ContentLength, ContentType}; use reqwest::header::{USER_AGENT}; @@ -12,49 +12,130 @@ use logging::LoggingErrors; use url::form_urlencoded; use std::collections::HashMap; use std::sync::Arc; +use config::JWTValidation; -/// claims struct, it needs to derive `Serialize` and/or `Deserialize` #[derive(Debug, Serialize, Deserialize)] -struct JWTClaims { - sub: String, - iss: String, - aud: String, - exp: usize, - #[serde(default)] - roles: Vec, - #[serde(rename = "releaseChannels", default)] - channels: Vec, - #[serde(rename = "IsPatreonAccountLinked")] - is_linked: bool, - #[serde(rename = "IsPatreonSubscriptionActive")] - is_subscribed: bool, +struct Auth { + username: String, + token: String, + jwt_token: JWTClaims, } -fn get_text(future: impl Future) -> impl Future { - future.map(|mut response| { - // Get the body of the response +/// claims struct, it needs to derive `Serialize` and/or `Deserialize` +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct JWTClaims { + pub sub: String, + pub iss: String, + pub aud: String, + pub exp: usize, + #[serde(default)] + pub roles: Vec, + #[serde(rename = "releaseChannels", default)] + pub channels: Vec, + #[serde(rename = "isPatreonAccountLinked")] + pub is_linked: bool, + #[serde(rename = "isPatreonSubscriptionActive")] + pub is_subscribed: bool, +} + +/// Calls the given server to obtain a JWT token and returns a Future with the response +pub fn authenticate_async(url: String, username: String, token: String) + -> Box> { + + // Build the HTTP client up + let client = match build_async_client() { + Ok(v) => v, + Err(_) => { + return Box::new(futures::future::err("Unable to build async web client".to_string())); + }, + }; + + Box::new(client.post(&url) + .header(USER_AGENT, "liftinstall (j-selby)") + .header("X-USERNAME", username.clone()) + .header("X-TOKEN", token.clone()) + .send() + .map_err(|err| { + format!("stream error {:?}, client: {:?}, http: {:?}, redirect: {:?}, serialization: {:?}, timeout: {:?}, server: {:?}", + err, err.is_client_error(), err.is_http(), err.is_redirect(), + err.is_serialization(), err.is_timeout(), err.is_server_error()) + }) + .map(|mut response| { match response.status() { reqwest::StatusCode::OK => Ok(response.text() .map_err(|e| { - error!("Error while converting the response to text {:?}", e); - Response::new() - .with_status(hyper::StatusCode::InternalServerError) + format!("Error while converting the response to text {:?}", e) })), _ => { - error!("Error wrong response code from server {:?}", response.status()); - Err(Response::new() - .with_status(hyper::StatusCode::InternalServerError)) + Err(format!("Error wrong response code from server {:?}", response.status())) } } }) - .map_err(|err| { - error!("Error cannot get text on errored stream {:?}", err); - Response::new() - .with_status(hyper::StatusCode::InternalServerError) - }) .and_then(|x| x) .flatten() + ) +} + +pub fn authenticate_sync(url: String, username: String, token: String) + -> Result { + + // Build the HTTP client up + let client = build_client()?; + + let mut response = client.post(&url) + .header(USER_AGENT, "liftinstall (j-selby)") + .header("X-USERNAME", username.clone()) + .header("X-TOKEN", token.clone()) + .send() + .map_err(|err| { + format!("stream error {:?}, client: {:?}, http: {:?}, redirect: {:?}, serialization: {:?}, timeout: {:?}, server: {:?}", + err, err.is_client_error(), err.is_http(), err.is_redirect(), + err.is_serialization(), err.is_timeout(), err.is_server_error()) + })?; + + match response.status() { + reqwest::StatusCode::OK => + Ok(response.text() + .map_err(|e| { + format!("Error while converting the response to text {:?}", e) + })?), + _ => { + Err(format!("Error wrong response code from server {:?}", response.status())) + } + } +} + +pub fn validate_token(body: String, pub_key_base64: String, validation: Option) -> Result { + // Get the public key for this authentication url + let pub_key = if pub_key_base64.is_empty() { + vec![] + } else { + match base64::decode(&pub_key_base64) { + Ok(v) => v, + Err(err) => { + return Err(format!("Configured public key was not empty and did not decode as base64 {:?}", err)); + }, + } + }; + + // Configure validation for audience and issuer if the configuration provides it + let validation = match validation { + Some(v) => { + let mut valid = Validation::new(Algorithm::RS256); + valid.iss = v.iss; + if v.aud.is_some() { + valid.set_audience(&v.aud.unwrap()); + } + valid + } + None => Validation::default() + }; + + // Verify the JWT token + decode::(&body, pub_key.as_slice(), &validation) + .map(|tok| tok.claims) + .map_err(|err| format!("Error while decoding the JWT. error: {:?} jwt: {:?}", err, body)) } pub fn handle(service: &WebService, _req: Request) -> InternalFuture { @@ -93,87 +174,53 @@ pub fn handle(service: &WebService, _req: Request) -> InternalFuture { (credentials.username.clone(), credentials.token.clone()) } }; + // second copy of the credentials so we can move them into a different closure + let (username_clone, token_clone) = (username.clone(), token.clone()); let authentication = config.authentication.unwrap(); - - // Get the public key for this authentication url - let pub_key = if authentication.pub_key_base64.is_empty() { - vec![] - } else { - match base64::decode(&authentication.pub_key_base64) { - Ok(v) => v, - Err(err) => { - error!("Configured public key was not empty and did not decode as base64 {:?}", err); - return default_future(Response::new().with_status(hyper::StatusCode::InternalServerError)); - }, - } - }; - - // Build the HTTP client up - let client = match build_async_client() { - Ok(v) => v, - Err(_) => { - return default_future(Response::new().with_status(hyper::StatusCode::InternalServerError)); - }, - }; + let auth_url = authentication.auth_url.clone(); + let pub_key_base64 = authentication.pub_key_base64.clone(); + let validation = authentication.validation.clone(); // call the authentication URL to see if we are authenticated - Box::new(get_text( - client.post(&authentication.auth_url) - .header(USER_AGENT, "liftinstall (j-selby)") - .header("X-USERNAME", username.clone()) - .header("X-TOKEN", token.clone()) - .send() - ).map(move |body| { - // Configure validation for audience and issuer if the configuration provides it - let validation = match authentication.validation { - Some(v) => { - let mut valid = Validation::new(Algorithm::RS256); - valid.iss = v.iss; - if v.aud.is_some() { - valid.set_audience(&v.aud.unwrap()); - } - valid - } - None => Validation::default() + Box::new(authenticate_async(auth_url, username.clone(), token.clone()) + .map(|body| { + validate_token(body, pub_key_base64, validation) + }) + .and_then(|res| res) + .map(move |claims| { + let out = Auth { + username: username_clone, + token: token_clone, + jwt_token: claims.clone(), }; - - // Verify the JWT token - let tok = match decode::(&body, pub_key.as_slice(), &validation) { - Ok(v) => v, - Err(v) => { - error!("Error while decoding the JWT. error: {:?} str: {:?}", v, &body); - return Err(Response::new().with_status(hyper::StatusCode::InternalServerError)); - }, - }; - - { - // Store the validated username and password into the installer database - let mut framework = write_cred_fw.write().log_expect("InstallerFramework has been dirtied"); - framework.database.credentials.username = username.clone(); - framework.database.credentials.token = token.clone(); - // And store the JWT token temporarily in the - framework.authorization_token = Some(body.clone()); - } - // Convert the json to a string and return the json token - match serde_json::to_string(&tok.claims) { + match serde_json::to_string(&out) { Ok(v) => Ok(v), Err(e) => { - error!("Error while converting the claims to JSON string: {:?}", e); - Err(Response::new().with_status(hyper::StatusCode::InternalServerError)) + Err(format!("Error while converting the claims to JSON string: {:?}", e)) } } }) .and_then(|res| res) - .map(|out| { + .map(move |json| { + { + // Store the validated username and password into the installer database + let mut framework = write_cred_fw.write().log_expect("InstallerFramework has been dirtied"); + framework.database.credentials.username = username; + framework.database.credentials.token = token; + } + // Finally return the JSON with the response info!("successfully verified username and token"); Response::new() - .with_header(ContentLength(out.len() as u64)) + .with_header(ContentLength(json.len() as u64)) .with_header(ContentType::json()) .with_status(hyper::StatusCode::Ok) - .with_body(out) + .with_body(json) + }) + .map_err(|err| { + Response::new().with_status(hyper::StatusCode::InternalServerError) }) .or_else(|err| { // Convert the Err value into an Ok value since the error code from this HTTP request is an Ok(response) diff --git a/src/frontend/rest/services/mod.rs b/src/frontend/rest/services/mod.rs index 1b7473e..f5d984d 100644 --- a/src/frontend/rest/services/mod.rs +++ b/src/frontend/rest/services/mod.rs @@ -21,7 +21,7 @@ use futures::future::Future as _; use futures::sink::Sink; mod attributes; -mod authentication; +pub mod authentication; mod browser; mod config; mod default_path; diff --git a/src/frontend/ui/mod.rs b/src/frontend/ui/mod.rs index 149a67b..82378cd 100644 --- a/src/frontend/ui/mod.rs +++ b/src/frontend/ui/mod.rs @@ -17,7 +17,7 @@ enum CallbackType { /// Starts the main web UI. Will return when UI is closed. pub fn start_ui(app_name: &str, http_address: &str, is_launcher: bool) { - let size = if is_launcher { (600, 300) } else { (1024, 500) }; + let size = (1024, 500); info!("Spawning web view instance"); diff --git a/src/installer.rs b/src/installer.rs index d0c9f10..2c6256b 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -50,6 +50,7 @@ pub enum InstallMessage { Status(String, f64), PackageInstalled, Error(String), + AuthorizationRequired(String), EOF, } @@ -92,7 +93,6 @@ pub struct InstallerFramework { // If we just completed an uninstall, and we should clean up after ourselves. pub burn_after_exit: bool, pub launcher_path: Option, - pub authorization_token: Option, } /// Contains basic properties on the status of the session. Subset of InstallationFramework. @@ -125,6 +125,11 @@ macro_rules! declare_messenger_callback { error!("Failed to submit queue message: {:?}", v); } } + TaskMessage::AuthorizationRequired(msg) => { + if let Err(v) = $target.send(InstallMessage::AuthorizationRequired(msg.to_string())) { + error!("Failed to submit queue message: {:?}", v); + } + } TaskMessage::PackageInstalled => { if let Err(v) = $target.send(InstallMessage::PackageInstalled) { error!("Failed to submit queue message: {:?}", v); @@ -441,7 +446,6 @@ impl InstallerFramework { is_launcher: false, burn_after_exit: false, launcher_path: None, - authorization_token: None, } } @@ -469,7 +473,6 @@ impl InstallerFramework { is_launcher: false, burn_after_exit: false, launcher_path: None, - authorization_token: None, }) } } diff --git a/src/tasks/check_authorization.rs b/src/tasks/check_authorization.rs new file mode 100644 index 0000000..e2ffb9f --- /dev/null +++ b/src/tasks/check_authorization.rs @@ -0,0 +1,69 @@ + +use installer::InstallerFramework; +use tasks::{Task, TaskDependency, TaskMessage, TaskOrdering, TaskParamType}; +use logging::LoggingErrors; +use frontend::rest::services::authentication; +use futures::{Stream, Future}; +use tasks::resolver::ResolvePackageTask; + +pub struct CheckAuthorizationTask { + pub name: String, +} + +impl Task for CheckAuthorizationTask { + fn execute( + &mut self, + mut input: Vec, + context: &mut InstallerFramework, + messenger: &dyn Fn(&TaskMessage), + ) -> Result { + + assert_eq!(input.len(), 1); + let params = input.pop().log_expect("Should have input from resolver!"); + let (version, file) = match params { + TaskParamType::File(v, f) => { Ok((v, f)) }, + _ => { Err("Unexpected TaskParamType in CheckAuthorization: {:?}") } + }?; + + if !file.requires_authorization { + return Ok(TaskParamType::Authentication(version, file, None)); + } + + let username = context.database.credentials.username.clone(); + let token = context.database.credentials.token.clone(); + let authentication = context.config.clone().unwrap().authentication.unwrap(); + let auth_url = authentication.auth_url.clone(); + let pub_key_base64 = authentication.pub_key_base64.clone(); + let validation = authentication.validation.clone(); + // Authorizaion is required for this package so post the username and token and get a jwt_token response + let jwt_token = match authentication::authenticate_sync(auth_url, username, token) { + Ok(jwt) => jwt, + Err(_) => return Ok(TaskParamType::Authentication(version, file, None)) + }; + let claims = match authentication::validate_token(jwt_token.clone(), pub_key_base64, validation) { + Ok(c) => c, + Err(_) => return Ok(TaskParamType::Authentication(version, file, None)) + }; + // Validate that they are authorized + let authorized = + claims.roles.contains(&"vip".to_string()) || (claims.channels.contains(&"early-access".to_string())); + + if !authorized { + return Ok(TaskParamType::Authentication(version, file, None)); + } + Ok(TaskParamType::Authentication(version, file, Some(jwt_token))) + } + + fn dependencies(&self) -> Vec { + vec![TaskDependency::build( + TaskOrdering::Pre, + Box::new(ResolvePackageTask { + name: self.name.clone(), + }), + )] + } + + fn name(&self) -> String { + format!("CheckAuthorizationTask (for {:?})", self.name) + } +} \ No newline at end of file diff --git a/src/tasks/download_pkg.rs b/src/tasks/download_pkg.rs index 3262ec0..396063c 100644 --- a/src/tasks/download_pkg.rs +++ b/src/tasks/download_pkg.rs @@ -2,11 +2,8 @@ use installer::InstallerFramework; -use tasks::Task; -use tasks::TaskDependency; -use tasks::TaskMessage; -use tasks::TaskOrdering; -use tasks::TaskParamType; +use tasks::check_authorization::CheckAuthorizationTask; +use tasks::{Task, TaskDependency, TaskMessage, TaskOrdering, TaskParamType}; use tasks::resolver::ResolvePackageTask; @@ -30,11 +27,18 @@ impl Task for DownloadPackageTask { assert_eq!(input.len(), 1); let file = input.pop().log_expect("Should have input from resolver!"); - let (version, file) = match file { - TaskParamType::File(v, f) => (v, f), + let (version, file, auth) = match file { + TaskParamType::Authentication(v, f, auth) => (v, f, auth), _ => return Err("Unexpected param type to download package".to_string()), }; +// TODO: move this back below checking for latest version after testing is done + if file.requires_authorization && auth.is_none() { + info!("Authorization required to update this package!"); + messenger(&TaskMessage::AuthorizationRequired("AuthorizationRequired")); + return Ok(TaskParamType::Break); + } + // Check to see if this is the newest file available already for element in &context.database.packages { if element.name == self.name { @@ -54,7 +58,7 @@ impl Task for DownloadPackageTask { let mut downloaded = 0; let mut data_storage: Vec = Vec::new(); - stream_file(&file.url, context.authorization_token.clone(), |data, size| { + stream_file(&file.url, auth, |data, size| { { data_storage.extend_from_slice(&data); } @@ -90,12 +94,12 @@ impl Task for DownloadPackageTask { } fn dependencies(&self) -> Vec { - vec![TaskDependency::build( - TaskOrdering::Pre, - Box::new(ResolvePackageTask { - name: self.name.clone(), - }), - )] + vec![TaskDependency::build( + TaskOrdering::Pre, + Box::new(CheckAuthorizationTask { + name: self.name.clone(), + }), + )] } fn name(&self) -> String { diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index ed6154b..a898f50 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -9,6 +9,7 @@ use installer::InstallerFramework; use sources::types::File; use sources::types::Version; +pub mod check_authorization; pub mod download_pkg; pub mod ensure_only_instance; pub mod install; @@ -29,6 +30,8 @@ pub enum TaskParamType { None, /// Metadata about a file File(Version, File), + /// Authentication token for a package + Authentication(Version, File, Option), /// Downloaded contents of a file FileContents(Version, File, Vec), /// List of shortcuts that have been generated @@ -62,6 +65,7 @@ impl TaskDependency { /// A message from a task. pub enum TaskMessage<'a> { DisplayMessage(&'a str, f64), + AuthorizationRequired(&'a str), PackageInstalled, } diff --git a/ui/src/main.js b/ui/src/main.js index fc9dca2..787db99 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -123,7 +123,9 @@ var app = new Vue({ var app = this.$root; app.ajax('/api/check-auth', function (auth) { - that.jwt_token = auth; + app.$data.username = auth.username; + app.$data.token = auth.token; + that.jwt_token = auth.jwt_token; that.is_authenticated = Object.keys(that.jwt_token).length !== 0 && that.jwt_token.constructor === Object; if (that.is_authenticated) { // Give all permissions to vip roles @@ -132,9 +134,9 @@ var app = new Vue({ that.is_subscribed = true; that.has_reward_tier = true; } else { - that.is_linked = that.jwt_token.IsPatreonAccountLinked; - that.is_subscribed = that.jwt_token.IsPatreonSubscriptionActive; - that.has_reward_tier = that.jwt_token.releaseChannels.indexOf("early-release") > -1; + that.is_linked = that.jwt_token.isPatreonAccountLinked; + that.is_subscribed = that.jwt_token.isPatreonSubscriptionActive; + that.has_reward_tier = that.jwt_token.releaseChannels.indexOf("early-access") > -1; } } if (success) { diff --git a/ui/src/router.js b/ui/src/router.js index 29db2f4..8b021d7 100644 --- a/ui/src/router.js +++ b/ui/src/router.js @@ -8,6 +8,7 @@ import InstallPackages from './views/InstallPackages.vue' import CompleteView from './views/CompleteView.vue' import ModifyView from './views/ModifyView.vue' import AuthenticationView from './views/AuthenticationView.vue' +import ReAuthenticationView from './views/ReAuthenticationView.vue' Vue.use(Router) @@ -53,6 +54,11 @@ export default new Router({ name: 'authentication', component: AuthenticationView }, + { + path: '/reauthenticate', + name: 'reauthenticate', + component: ReAuthenticationView + }, { path: '/', redirect: '/config' diff --git a/ui/src/views/AuthenticationView.vue b/ui/src/views/AuthenticationView.vue index 1735e4d..e1dc633 100644 --- a/ui/src/views/AuthenticationView.vue +++ b/ui/src/views/AuthenticationView.vue @@ -6,7 +6,7 @@

Before you can install this Early Access, you need to verify your account. - Click here to link your yuzu-emu.org account + Click here to link your yuzu-emu.org account and paste the token below.

@@ -30,17 +30,17 @@ Your credentials are valid, but you still need to link your patreon! - If this is an error, then click here to link your yuzu-emu.org account + If this is an error, then click here to link your yuzu-emu.org account Your patreon is linked, but you are not a current subscriber. - Log into your patreon account and support the project! + Log into your patreon account and support the project! Your patreon is linked, and you are supporting the project, but you must first join the Early Access reward tier! - Log into your patreon account and choose to back the Early Access reward tier. + Log into your patreon account and choose to back the Early Access reward tier.
diff --git a/ui/src/views/InstallPackages.vue b/ui/src/views/InstallPackages.vue index f90032b..514b6fd 100644 --- a/ui/src/views/InstallPackages.vue +++ b/ui/src/views/InstallPackages.vue @@ -25,6 +25,7 @@ export default { is_updater_update: false, is_update: false, failed_with_error: false, + authorization_required: false, packages_installed: 0 } }, @@ -41,10 +42,12 @@ export default { var app = this.$root var results = {} + var requires_authorization = false; for (var package_index = 0; package_index < app.config.packages.length; package_index++) { var current_package = app.config.packages[package_index] if (current_package.default != null) { + requires_authorization |= current_package.requires_authorization; results[current_package.name] = current_package.default } } @@ -71,6 +74,10 @@ export default { that.packages_installed += 1 } + if (line.hasOwnProperty('AuthorizationRequired')) { + that.authorization_required = true + } + if (line.hasOwnProperty('Error')) { that.failed_with_error = true that.$router.replace({ name: 'showerr', params: { msg: line.Error } }) @@ -90,7 +97,9 @@ export default { } } } else { - if (app.metadata.is_launcher) { + if (that.authorization_required) { + that.$router.push('/reauthenticate') + } else if (app.metadata.is_launcher) { app.exit() } else if (!that.failed_with_error) { if (that.is_uninstall) { diff --git a/ui/src/views/ReAuthenticationView.vue b/ui/src/views/ReAuthenticationView.vue new file mode 100644 index 0000000..7f16c3b --- /dev/null +++ b/ui/src/views/ReAuthenticationView.vue @@ -0,0 +1,47 @@ + + + + + \ No newline at end of file From e72a5f0420f7a8b0083cca903892dc979c33327d Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 1 Nov 2019 11:44:07 -0600 Subject: [PATCH 07/15] Change the link link to point to yuzu-emu.org --- ui/src/views/AuthenticationView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/views/AuthenticationView.vue b/ui/src/views/AuthenticationView.vue index e1dc633..6f245fc 100644 --- a/ui/src/views/AuthenticationView.vue +++ b/ui/src/views/AuthenticationView.vue @@ -6,7 +6,7 @@

Before you can install this Early Access, you need to verify your account. - Click here to link your yuzu-emu.org account + Click here to link your yuzu-emu.org account and paste the token below.

From 561f0071bde9b41236cbee1d42efed9277e43384 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 2 Nov 2019 16:33:45 -0600 Subject: [PATCH 08/15] Add is_new to config --- config.windows.patreon.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config.windows.patreon.toml b/config.windows.patreon.toml index 2276238..314bcef 100644 --- a/config.windows.patreon.toml +++ b/config.windows.patreon.toml @@ -38,6 +38,8 @@ need_subscription_description = "You are signed in, but you need to link your Pa # Displayed when the package has an authentication, but the user has not linked their account need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details" requires_authorization = true +# puts a "new" ribbon the package select +is_new = true [packages.source] name = "patreon" match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$" From d3c3b77e6be63813ab03c5673370c2cb695d5d92 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sun, 3 Nov 2019 01:22:55 -0600 Subject: [PATCH 09/15] Various fixes to various things --- src/config.rs | 2 + src/frontend/rest/services/authentication.rs | 4 +- src/frontend/ui/mod.rs | 2 +- ui/src/App.vue | 45 ++++++++++++++++ ui/src/views/AuthenticationView.vue | 19 ++++--- ui/src/views/CompleteView.vue | 54 ++++++++++---------- ui/src/views/SelectPackages.vue | 32 ++++++++++-- 7 files changed, 116 insertions(+), 42 deletions(-) diff --git a/src/config.rs b/src/config.rs index a1aeea5..ef3e91f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -39,6 +39,8 @@ pub struct PackageDescription { #[serde(default)] pub requires_authorization: Option, #[serde(default)] + pub is_new: Option, + #[serde(default)] pub need_authentication_description: Option, #[serde(default)] pub need_link_description: Option, diff --git a/src/frontend/rest/services/authentication.rs b/src/frontend/rest/services/authentication.rs index 6ce4b63..5f403d7 100644 --- a/src/frontend/rest/services/authentication.rs +++ b/src/frontend/rest/services/authentication.rs @@ -18,7 +18,7 @@ use config::JWTValidation; struct Auth { username: String, token: String, - jwt_token: JWTClaims, + jwt_token: Option, } /// claims struct, it needs to derive `Serialize` and/or `Deserialize` @@ -192,7 +192,7 @@ pub fn handle(service: &WebService, _req: Request) -> InternalFuture { let out = Auth { username: username_clone, token: token_clone, - jwt_token: claims.clone(), + jwt_token: Some(claims.clone()), }; // Convert the json to a string and return the json token match serde_json::to_string(&out) { diff --git a/src/frontend/ui/mod.rs b/src/frontend/ui/mod.rs index 82378cd..8d9028e 100644 --- a/src/frontend/ui/mod.rs +++ b/src/frontend/ui/mod.rs @@ -17,7 +17,7 @@ enum CallbackType { /// Starts the main web UI. Will return when UI is closed. pub fn start_ui(app_name: &str, http_address: &str, is_launcher: bool) { - let size = (1024, 500); + let size = (1024, 550); info!("Spawning web view instance"); diff --git a/ui/src/App.vue b/ui/src/App.vue index d5a5d19..60e865c 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -71,6 +71,7 @@ pre { .clickable-box { cursor: pointer; + position: relative; } .clickable-box label { @@ -126,4 +127,48 @@ pre { body.has-background-black-ter .subtitle, body.has-background-black-ter .column > div { color: hsl(0, 0%, 96%); } + +.ribbon { + position: absolute; + right: -5px; top: -5px; + z-index: 1; + overflow: hidden; + width: 75px; height: 75px; + text-align: right; +} +.ribbon span { + font-size: 10px; + font-weight: bold; + color: #FFF; + text-transform: uppercase; + text-align: center; + line-height: 20px; + transform: rotate(45deg); + -webkit-transform: rotate(45deg); + width: 100px; + display: block; + background: #79A70A; + background: linear-gradient(#FF3C28 0%, #FF3C28 100%); + box-shadow: 0 3px 10px -5px rgba(0, 0, 0, 1); + position: absolute; + top: 19px; right: -21px; +} +.ribbon span::before { + content: ""; + position: absolute; left: 0px; top: 100%; + z-index: -1; + border-left: 3px solid #FF3C28; + border-right: 3px solid transparent; + border-bottom: 3px solid transparent; + border-top: 3px solid #FF3C28; +} +.ribbon span::after { + content: ""; + position: absolute; right: 0px; top: 100%; + z-index: -1; + border-left: 3px solid transparent; + border-right: 3px solid #FF3C28; + border-bottom: 3px solid transparent; + border-top: 3px solid #FF3C28; +} diff --git a/ui/src/views/AuthenticationView.vue b/ui/src/views/AuthenticationView.vue index 6f245fc..651ffe9 100644 --- a/ui/src/views/AuthenticationView.vue +++ b/ui/src/views/AuthenticationView.vue @@ -5,11 +5,14 @@ When you are done, enter the username and token below.

- Before you can install this Early Access, you need to verify your account. - Click here to link your yuzu-emu.org account - and paste the token below. -

+ The Early Access release channel lets you try out the latest experimental features and fixes, before they are merged into yuzu. This channel includes all regular yuzu daily updates, plus these exclusive features. + To be an Early Access member, you must be a Patreon Early Access Subscriber. +

+ If you are a subscriber, click here to link your yuzu-emu.org account +

+ If you are not already a subscriber, click here to become one +


@@ -30,17 +33,17 @@ Your credentials are valid, but you still need to link your patreon! - If this is an error, then click here to link your yuzu-emu.org account + If this is an error, then click here to link your yuzu-emu.org account - Your patreon is linked, but you are not a current subscriber. - Log into your patreon account and support the project! + Your patreon is linked, but you are not a current subscriber! + Log into your patreon account and support the project! Your patreon is linked, and you are supporting the project, but you must first join the Early Access reward tier! - Log into your patreon account and choose to back the Early Access reward tier. + Log into your patreon account and choose to back the Early Access reward tier.
diff --git a/ui/src/views/CompleteView.vue b/ui/src/views/CompleteView.vue index 9515403..6ef1ff9 100644 --- a/ui/src/views/CompleteView.vue +++ b/ui/src/views/CompleteView.vue @@ -1,40 +1,40 @@ diff --git a/ui/src/views/SelectPackages.vue b/ui/src/views/SelectPackages.vue index 9772c40..34ce32b 100644 --- a/ui/src/views/SelectPackages.vue +++ b/ui/src/views/SelectPackages.vue @@ -7,30 +7,43 @@
-

{{ Lpackage.name }}

+
New!
+ + {{ Lpackage.name }} +

{{Lpackage.need_authentication_description}}

-

{{ Lpackage.name }}

+
New!
+ + {{ Lpackage.name }} +

{{Lpackage.need_link_description}}

-

{{ Lpackage.name }}

+
New!
+ + {{ Lpackage.name }} +

{{Lpackage.need_subscription_description}}

-

{{ Lpackage.name }}

+
New!
+ + {{ Lpackage.name }} +

{{Lpackage.need_reward_tier_description}}

+
New!