mirror of
https://github.com/yuzu-emu/liftinstall.git
synced 2024-11-25 18:45:37 +01:00
Add authentication task dependency to check for auth on install
This commit is contained in:
parent
288518cd78
commit
2b4b59320e
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -698,7 +698,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lzma-sys"
|
name = "lzma-sys"
|
||||||
version = "0.1.14"
|
version = "0.1.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
"cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@ -2053,7 +2053,7 @@ name = "xz2"
|
|||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
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]]
|
[[package]]
|
||||||
@ -2145,7 +2145,7 @@ dependencies = [
|
|||||||
"checksum lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c"
|
"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.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 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 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 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"
|
"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3"
|
||||||
|
@ -28,7 +28,7 @@ default = true
|
|||||||
|
|
||||||
[[packages]]
|
[[packages]]
|
||||||
name = "yuzu Early Access"
|
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
|
# 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"
|
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
|
# Displayed when the package has an authentication, but the user has not linked their account
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
use http::build_async_client;
|
use http::{build_client, build_async_client};
|
||||||
|
|
||||||
use hyper::header::{ContentLength, ContentType};
|
use hyper::header::{ContentLength, ContentType};
|
||||||
use reqwest::header::{USER_AGENT};
|
use reqwest::header::{USER_AGENT};
|
||||||
@ -12,49 +12,130 @@ use logging::LoggingErrors;
|
|||||||
use url::form_urlencoded;
|
use url::form_urlencoded;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use config::JWTValidation;
|
||||||
|
|
||||||
/// claims struct, it needs to derive `Serialize` and/or `Deserialize`
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct JWTClaims {
|
struct Auth {
|
||||||
sub: String,
|
username: String,
|
||||||
iss: String,
|
token: String,
|
||||||
aud: String,
|
jwt_token: JWTClaims,
|
||||||
exp: usize,
|
|
||||||
#[serde(default)]
|
|
||||||
roles: Vec<String>,
|
|
||||||
#[serde(rename = "releaseChannels", default)]
|
|
||||||
channels: Vec<String>,
|
|
||||||
#[serde(rename = "IsPatreonAccountLinked")]
|
|
||||||
is_linked: bool,
|
|
||||||
#[serde(rename = "IsPatreonSubscriptionActive")]
|
|
||||||
is_subscribed: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_text(future: impl Future<Item = reqwest::async::Response, Error = reqwest::Error>) -> impl Future<Item = String, Error = Response> {
|
/// claims struct, it needs to derive `Serialize` and/or `Deserialize`
|
||||||
future.map(|mut response| {
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
// Get the body of the response
|
pub struct JWTClaims {
|
||||||
|
pub sub: String,
|
||||||
|
pub iss: String,
|
||||||
|
pub aud: String,
|
||||||
|
pub exp: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub roles: Vec<String>,
|
||||||
|
#[serde(rename = "releaseChannels", default)]
|
||||||
|
pub channels: Vec<String>,
|
||||||
|
#[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<String> with the response
|
||||||
|
pub fn authenticate_async(url: String, username: String, token: String)
|
||||||
|
-> Box<dyn futures::Future<Item = String, Error = String>> {
|
||||||
|
|
||||||
|
// 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() {
|
match response.status() {
|
||||||
reqwest::StatusCode::OK =>
|
reqwest::StatusCode::OK =>
|
||||||
Ok(response.text()
|
Ok(response.text()
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("Error while converting the response to text {:?}", e);
|
format!("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(format!("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)
|
.and_then(|x| x)
|
||||||
.flatten()
|
.flatten()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authenticate_sync(url: String, username: String, token: String)
|
||||||
|
-> Result<String, String> {
|
||||||
|
|
||||||
|
// 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<JWTValidation>) -> Result<JWTClaims, String> {
|
||||||
|
// 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::<JWTClaims>(&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 {
|
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())
|
(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();
|
let authentication = config.authentication.unwrap();
|
||||||
|
let auth_url = authentication.auth_url.clone();
|
||||||
// Get the public key for this authentication url
|
let pub_key_base64 = authentication.pub_key_base64.clone();
|
||||||
let pub_key = if authentication.pub_key_base64.is_empty() {
|
let validation = authentication.validation.clone();
|
||||||
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
|
// call the authentication URL to see if we are authenticated
|
||||||
Box::new(get_text(
|
Box::new(authenticate_async(auth_url, username.clone(), token.clone())
|
||||||
client.post(&authentication.auth_url)
|
.map(|body| {
|
||||||
.header(USER_AGENT, "liftinstall (j-selby)")
|
validate_token(body, pub_key_base64, validation)
|
||||||
.header("X-USERNAME", username.clone())
|
})
|
||||||
.header("X-TOKEN", token.clone())
|
.and_then(|res| res)
|
||||||
.send()
|
.map(move |claims| {
|
||||||
).map(move |body| {
|
let out = Auth {
|
||||||
// Configure validation for audience and issuer if the configuration provides it
|
username: username_clone,
|
||||||
let validation = match authentication.validation {
|
token: token_clone,
|
||||||
Some(v) => {
|
jwt_token: claims.clone(),
|
||||||
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::<JWTClaims>(&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
|
// 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),
|
Ok(v) => Ok(v),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error while converting the claims to JSON string: {:?}", e);
|
Err(format!("Error while converting the claims to JSON string: {:?}", e))
|
||||||
Err(Response::new().with_status(hyper::StatusCode::InternalServerError))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.and_then(|res| res)
|
.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
|
// Finally return the JSON with the response
|
||||||
info!("successfully verified username and token");
|
info!("successfully verified username and token");
|
||||||
Response::new()
|
Response::new()
|
||||||
.with_header(ContentLength(out.len() as u64))
|
.with_header(ContentLength(json.len() as u64))
|
||||||
.with_header(ContentType::json())
|
.with_header(ContentType::json())
|
||||||
.with_status(hyper::StatusCode::Ok)
|
.with_status(hyper::StatusCode::Ok)
|
||||||
.with_body(out)
|
.with_body(json)
|
||||||
|
})
|
||||||
|
.map_err(|err| {
|
||||||
|
Response::new().with_status(hyper::StatusCode::InternalServerError)
|
||||||
})
|
})
|
||||||
.or_else(|err| {
|
.or_else(|err| {
|
||||||
// Convert the Err value into an Ok value since the error code from this HTTP request is an Ok(response)
|
// Convert the Err value into an Ok value since the error code from this HTTP request is an Ok(response)
|
||||||
|
@ -21,7 +21,7 @@ use futures::future::Future as _;
|
|||||||
use futures::sink::Sink;
|
use futures::sink::Sink;
|
||||||
|
|
||||||
mod attributes;
|
mod attributes;
|
||||||
mod authentication;
|
pub mod authentication;
|
||||||
mod browser;
|
mod browser;
|
||||||
mod config;
|
mod config;
|
||||||
mod default_path;
|
mod default_path;
|
||||||
|
@ -17,7 +17,7 @@ enum CallbackType {
|
|||||||
|
|
||||||
/// Starts the main web UI. Will return when UI is closed.
|
/// Starts the main web UI. Will return when UI is closed.
|
||||||
pub fn start_ui(app_name: &str, http_address: &str, is_launcher: bool) {
|
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");
|
info!("Spawning web view instance");
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ pub enum InstallMessage {
|
|||||||
Status(String, f64),
|
Status(String, f64),
|
||||||
PackageInstalled,
|
PackageInstalled,
|
||||||
Error(String),
|
Error(String),
|
||||||
|
AuthorizationRequired(String),
|
||||||
EOF,
|
EOF,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +93,6 @@ pub struct InstallerFramework {
|
|||||||
// If we just completed an uninstall, and we should clean up after ourselves.
|
// If we just completed an uninstall, and we should clean up after ourselves.
|
||||||
pub burn_after_exit: bool,
|
pub burn_after_exit: bool,
|
||||||
pub launcher_path: Option<String>,
|
pub launcher_path: Option<String>,
|
||||||
pub authorization_token: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains basic properties on the status of the session. Subset of InstallationFramework.
|
/// 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);
|
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 => {
|
TaskMessage::PackageInstalled => {
|
||||||
if let Err(v) = $target.send(InstallMessage::PackageInstalled) {
|
if let Err(v) = $target.send(InstallMessage::PackageInstalled) {
|
||||||
error!("Failed to submit queue message: {:?}", v);
|
error!("Failed to submit queue message: {:?}", v);
|
||||||
@ -441,7 +446,6 @@ impl InstallerFramework {
|
|||||||
is_launcher: false,
|
is_launcher: false,
|
||||||
burn_after_exit: false,
|
burn_after_exit: false,
|
||||||
launcher_path: None,
|
launcher_path: None,
|
||||||
authorization_token: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,7 +473,6 @@ impl InstallerFramework {
|
|||||||
is_launcher: false,
|
is_launcher: false,
|
||||||
burn_after_exit: false,
|
burn_after_exit: false,
|
||||||
launcher_path: None,
|
launcher_path: None,
|
||||||
authorization_token: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
69
src/tasks/check_authorization.rs
Normal file
69
src/tasks/check_authorization.rs
Normal file
@ -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<TaskParamType>,
|
||||||
|
context: &mut InstallerFramework,
|
||||||
|
messenger: &dyn Fn(&TaskMessage),
|
||||||
|
) -> Result<TaskParamType, String> {
|
||||||
|
|
||||||
|
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<TaskDependency> {
|
||||||
|
vec![TaskDependency::build(
|
||||||
|
TaskOrdering::Pre,
|
||||||
|
Box::new(ResolvePackageTask {
|
||||||
|
name: self.name.clone(),
|
||||||
|
}),
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
format!("CheckAuthorizationTask (for {:?})", self.name)
|
||||||
|
}
|
||||||
|
}
|
@ -2,11 +2,8 @@
|
|||||||
|
|
||||||
use installer::InstallerFramework;
|
use installer::InstallerFramework;
|
||||||
|
|
||||||
use tasks::Task;
|
use tasks::check_authorization::CheckAuthorizationTask;
|
||||||
use tasks::TaskDependency;
|
use tasks::{Task, TaskDependency, TaskMessage, TaskOrdering, TaskParamType};
|
||||||
use tasks::TaskMessage;
|
|
||||||
use tasks::TaskOrdering;
|
|
||||||
use tasks::TaskParamType;
|
|
||||||
|
|
||||||
use tasks::resolver::ResolvePackageTask;
|
use tasks::resolver::ResolvePackageTask;
|
||||||
|
|
||||||
@ -30,11 +27,18 @@ impl Task for DownloadPackageTask {
|
|||||||
assert_eq!(input.len(), 1);
|
assert_eq!(input.len(), 1);
|
||||||
|
|
||||||
let file = input.pop().log_expect("Should have input from resolver!");
|
let file = input.pop().log_expect("Should have input from resolver!");
|
||||||
let (version, file) = match file {
|
let (version, file, auth) = match file {
|
||||||
TaskParamType::File(v, f) => (v, f),
|
TaskParamType::Authentication(v, f, auth) => (v, f, auth),
|
||||||
_ => return Err("Unexpected param type to download package".to_string()),
|
_ => 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
|
// Check to see if this is the newest file available already
|
||||||
for element in &context.database.packages {
|
for element in &context.database.packages {
|
||||||
if element.name == self.name {
|
if element.name == self.name {
|
||||||
@ -54,7 +58,7 @@ impl Task for DownloadPackageTask {
|
|||||||
let mut downloaded = 0;
|
let mut downloaded = 0;
|
||||||
let mut data_storage: Vec<u8> = Vec::new();
|
let mut data_storage: Vec<u8> = 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);
|
data_storage.extend_from_slice(&data);
|
||||||
}
|
}
|
||||||
@ -92,7 +96,7 @@ impl Task for DownloadPackageTask {
|
|||||||
fn dependencies(&self) -> Vec<TaskDependency> {
|
fn dependencies(&self) -> Vec<TaskDependency> {
|
||||||
vec![TaskDependency::build(
|
vec![TaskDependency::build(
|
||||||
TaskOrdering::Pre,
|
TaskOrdering::Pre,
|
||||||
Box::new(ResolvePackageTask {
|
Box::new(CheckAuthorizationTask {
|
||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
}),
|
}),
|
||||||
)]
|
)]
|
||||||
|
@ -9,6 +9,7 @@ use installer::InstallerFramework;
|
|||||||
use sources::types::File;
|
use sources::types::File;
|
||||||
use sources::types::Version;
|
use sources::types::Version;
|
||||||
|
|
||||||
|
pub mod check_authorization;
|
||||||
pub mod download_pkg;
|
pub mod download_pkg;
|
||||||
pub mod ensure_only_instance;
|
pub mod ensure_only_instance;
|
||||||
pub mod install;
|
pub mod install;
|
||||||
@ -29,6 +30,8 @@ pub enum TaskParamType {
|
|||||||
None,
|
None,
|
||||||
/// Metadata about a file
|
/// Metadata about a file
|
||||||
File(Version, File),
|
File(Version, File),
|
||||||
|
/// Authentication token for a package
|
||||||
|
Authentication(Version, File, Option<String>),
|
||||||
/// Downloaded contents of a file
|
/// Downloaded contents of a file
|
||||||
FileContents(Version, File, Vec<u8>),
|
FileContents(Version, File, Vec<u8>),
|
||||||
/// List of shortcuts that have been generated
|
/// List of shortcuts that have been generated
|
||||||
@ -62,6 +65,7 @@ impl TaskDependency {
|
|||||||
/// A message from a task.
|
/// A message from a task.
|
||||||
pub enum TaskMessage<'a> {
|
pub enum TaskMessage<'a> {
|
||||||
DisplayMessage(&'a str, f64),
|
DisplayMessage(&'a str, f64),
|
||||||
|
AuthorizationRequired(&'a str),
|
||||||
PackageInstalled,
|
PackageInstalled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +123,9 @@ var app = new Vue({
|
|||||||
var app = this.$root;
|
var app = this.$root;
|
||||||
|
|
||||||
app.ajax('/api/check-auth', function (auth) {
|
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;
|
that.is_authenticated = Object.keys(that.jwt_token).length !== 0 && that.jwt_token.constructor === Object;
|
||||||
if (that.is_authenticated) {
|
if (that.is_authenticated) {
|
||||||
// Give all permissions to vip roles
|
// Give all permissions to vip roles
|
||||||
@ -132,9 +134,9 @@ var app = new Vue({
|
|||||||
that.is_subscribed = true;
|
that.is_subscribed = true;
|
||||||
that.has_reward_tier = true;
|
that.has_reward_tier = true;
|
||||||
} else {
|
} else {
|
||||||
that.is_linked = that.jwt_token.IsPatreonAccountLinked;
|
that.is_linked = that.jwt_token.isPatreonAccountLinked;
|
||||||
that.is_subscribed = that.jwt_token.IsPatreonSubscriptionActive;
|
that.is_subscribed = that.jwt_token.isPatreonSubscriptionActive;
|
||||||
that.has_reward_tier = that.jwt_token.releaseChannels.indexOf("early-release") > -1;
|
that.has_reward_tier = that.jwt_token.releaseChannels.indexOf("early-access") > -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -8,6 +8,7 @@ import InstallPackages from './views/InstallPackages.vue'
|
|||||||
import CompleteView from './views/CompleteView.vue'
|
import CompleteView from './views/CompleteView.vue'
|
||||||
import ModifyView from './views/ModifyView.vue'
|
import ModifyView from './views/ModifyView.vue'
|
||||||
import AuthenticationView from './views/AuthenticationView.vue'
|
import AuthenticationView from './views/AuthenticationView.vue'
|
||||||
|
import ReAuthenticationView from './views/ReAuthenticationView.vue'
|
||||||
|
|
||||||
Vue.use(Router)
|
Vue.use(Router)
|
||||||
|
|
||||||
@ -53,6 +54,11 @@ export default new Router({
|
|||||||
name: 'authentication',
|
name: 'authentication',
|
||||||
component: AuthenticationView
|
component: AuthenticationView
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/reauthenticate',
|
||||||
|
name: 'reauthenticate',
|
||||||
|
component: ReAuthenticationView
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/config'
|
redirect: '/config'
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</b-message>
|
</b-message>
|
||||||
<p>
|
<p>
|
||||||
Before you can install this Early Access, you need to verify your account.
|
Before you can install this Early Access, you need to verify your account.
|
||||||
<a v-on:click="launch_browser('https://yuzu-emu.org/')">Click here to link your yuzu-emu.org account</a>
|
<a v-on:click="launch_browser('https://profile.yuzu-emu.org/external/patreon/connect/')">Click here to link your yuzu-emu.org account</a>
|
||||||
and paste the token below.
|
and paste the token below.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -30,17 +30,17 @@
|
|||||||
|
|
||||||
<b-message type="is-danger" :active.sync="unlinked_patreon">
|
<b-message type="is-danger" :active.sync="unlinked_patreon">
|
||||||
Your credentials are valid, but you still need to link your patreon!
|
Your credentials are valid, but you still need to link your patreon!
|
||||||
If this is an error, then <a v-on:click="launch_browser('https://yuzu-emu.org/')">click here to link your yuzu-emu.org account</a>
|
If this is an error, then <a v-on:click="launch_browser('https://profile.yuzu-emu.org/external/patreon/connect/')">click here to link your yuzu-emu.org account</a>
|
||||||
</b-message>
|
</b-message>
|
||||||
|
|
||||||
<b-message type="is-danger" :active.sync="no_subscription">
|
<b-message type="is-danger" :active.sync="no_subscription">
|
||||||
Your patreon is linked, but you are not a current subscriber.
|
Your patreon is linked, but you are not a current subscriber.
|
||||||
<a v-on:click="launch_browser('https://patreon.com/')">Log into your patreon account</a> and support the project!
|
<a v-on:click="launch_browser('https://profile.yuzu-emu.org/')">Log into your patreon account</a> and support the project!
|
||||||
</b-message>
|
</b-message>
|
||||||
|
|
||||||
<b-message type="is-danger" :active.sync="tier_not_selected">
|
<b-message type="is-danger" :active.sync="tier_not_selected">
|
||||||
Your patreon is linked, and you are supporting the project, but you must first join the Early Access reward tier!
|
Your patreon is linked, and you are supporting the project, but you must first join the Early Access reward tier!
|
||||||
<a v-on:click="launch_browser('https://patreon.com/')">Log into your patreon account</a> and choose to back the Early Access reward tier.
|
<a v-on:click="launch_browser('https://profile.yuzu-emu.org/')">Log into your patreon account</a> and choose to back the Early Access reward tier.
|
||||||
</b-message>
|
</b-message>
|
||||||
|
|
||||||
<div class="is-left-floating is-bottom-floating">
|
<div class="is-left-floating is-bottom-floating">
|
||||||
|
@ -25,6 +25,7 @@ export default {
|
|||||||
is_updater_update: false,
|
is_updater_update: false,
|
||||||
is_update: false,
|
is_update: false,
|
||||||
failed_with_error: false,
|
failed_with_error: false,
|
||||||
|
authorization_required: false,
|
||||||
packages_installed: 0
|
packages_installed: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -41,10 +42,12 @@ export default {
|
|||||||
var app = this.$root
|
var app = this.$root
|
||||||
|
|
||||||
var results = {}
|
var results = {}
|
||||||
|
var requires_authorization = false;
|
||||||
|
|
||||||
for (var package_index = 0; package_index < app.config.packages.length; package_index++) {
|
for (var package_index = 0; package_index < app.config.packages.length; package_index++) {
|
||||||
var current_package = app.config.packages[package_index]
|
var current_package = app.config.packages[package_index]
|
||||||
if (current_package.default != null) {
|
if (current_package.default != null) {
|
||||||
|
requires_authorization |= current_package.requires_authorization;
|
||||||
results[current_package.name] = current_package.default
|
results[current_package.name] = current_package.default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,6 +74,10 @@ export default {
|
|||||||
that.packages_installed += 1
|
that.packages_installed += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (line.hasOwnProperty('AuthorizationRequired')) {
|
||||||
|
that.authorization_required = true
|
||||||
|
}
|
||||||
|
|
||||||
if (line.hasOwnProperty('Error')) {
|
if (line.hasOwnProperty('Error')) {
|
||||||
that.failed_with_error = true
|
that.failed_with_error = true
|
||||||
that.$router.replace({ name: 'showerr', params: { msg: line.Error } })
|
that.$router.replace({ name: 'showerr', params: { msg: line.Error } })
|
||||||
@ -90,7 +97,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (app.metadata.is_launcher) {
|
if (that.authorization_required) {
|
||||||
|
that.$router.push('/reauthenticate')
|
||||||
|
} else if (app.metadata.is_launcher) {
|
||||||
app.exit()
|
app.exit()
|
||||||
} else if (!that.failed_with_error) {
|
} else if (!that.failed_with_error) {
|
||||||
if (that.is_uninstall) {
|
if (that.is_uninstall) {
|
||||||
|
47
ui/src/views/ReAuthenticationView.vue
Normal file
47
ui/src/views/ReAuthenticationView.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="column has-padding">
|
||||||
|
<b-message type="is-info">
|
||||||
|
<h4 class="subtitle">Update Available!</h4>
|
||||||
|
There is a new Early Access update available, but you are no longer in the early access reward tier.
|
||||||
|
We'd love to have you check out this update, so <a v-on:click="go_authenticate">click here to refresh your access!</a>
|
||||||
|
</b-message>
|
||||||
|
<br>
|
||||||
|
Alternatively, you can install the regular version of yuzu by clicking <strong>Back</strong> below.
|
||||||
|
<br>
|
||||||
|
Click <strong>Launch Old Version</strong> to continue using the old version of yuzu Early Access.
|
||||||
|
|
||||||
|
<div class="is-left-floating is-bottom-floating">
|
||||||
|
<p class="control">
|
||||||
|
<a class="button is-medium" v-on:click="go_packages">Back</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="is-right-floating is-bottom-floating">
|
||||||
|
<p class="control">
|
||||||
|
<a class="button is-dark is-medium" v-on:click="launch_old_version">Launch Old Version</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "ReAuthenticationView",
|
||||||
|
methods: {
|
||||||
|
go_authenticate: function() {
|
||||||
|
this.$router.replace('/authentication')
|
||||||
|
},
|
||||||
|
launch_old_version: function () {
|
||||||
|
this.$root.exit()
|
||||||
|
},
|
||||||
|
go_packages: function () {
|
||||||
|
this.$router.push('/packages')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user