diff --git a/.gitignore b/.gitignore index b30a24e..24ac554 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +config.json .idea/ GIT_IGNORE.txt # ---> macOS diff --git a/src/main.rs b/src/main.rs index fa0ad3b..dc6e4bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,15 @@ use std::collections::HashMap; use std::env; use std::process::exit; use std::time::Duration; + use clap::{App, Arg}; use reqwest::{ClientBuilder, Proxy}; -use rust_util::{debugging, information, opt_result, opt_value_result, XResult}; -use rust_util::util_msg::MessageType; -use crate::oidc::{OpenIdClientConfiguration, OpenIdConfiguration}; +use rust_util::{debugging, failure, information, opt_result, opt_value_result, success, XResult}; +use rust_util::util_file::read_file_content; +use rust_util::util_msg::{clear_lastline, MessageType, print_lastline}; +use tokio::time::sleep; + +use crate::oidc::{DeviceAuthorizationResponse, ErrorResponse, OpenIdClientConfiguration, OpenIdConfiguration, TokenResponse}; mod oidc; @@ -24,9 +28,10 @@ async fn main() -> XResult<()> { .author(AUTHORS) .arg(Arg::new("version").short('V').long("version").help("Print version")) .arg(Arg::new("verbose").short('v').long("verbose").help("Verbose")) - .arg(Arg::new("issuer").short('i').long("issuer").takes_value(true).help("Issuer")) .arg(Arg::new("proxy").short('p').long("proxy").takes_value(true).help("Proxy")) + .arg(Arg::new("hide-qr").long("hide-qr").help("Hide QR")) .arg(Arg::new("skip-certificate-check").short('k').long("skip-certificate-check").help("Skip certificate check")) + .arg(Arg::new("config").short('c').long("config").takes_value(true).help("Config file")) .get_matches(); if matches.is_present("verbose") { @@ -36,7 +41,14 @@ async fn main() -> XResult<()> { information!("{} v{}", NAME, VERSION); exit(1); } - let issuer = opt_value_result!(matches.value_of("issuer"), "Issuer is required"); + + let config = opt_value_result!(matches.value_of("config"), "Config file is required"); + let config_content = read_file_content(config)?; + + let openid_client_configuration: OpenIdClientConfiguration = + opt_result!( serde_json::from_str(&config_content), "Parse config failed: {}"); + + let issuer = &openid_client_configuration.issuer; let openid_configuration_url = format!("{}/.well-known/openid-configuration", issuer); debugging!("Open id configuration url: {}", openid_configuration_url); @@ -47,7 +59,7 @@ async fn main() -> XResult<()> { let proxy = opt_result!( Proxy::all(proxy), "Parse proxy: {} failed: {}", proxy); client_builder = client_builder.proxy(proxy); } - if (matches.is_present("skip-certificate-check")) { + if matches.is_present("skip-certificate-check") { client_builder = client_builder.danger_accept_invalid_certs(true); } let client = opt_result!( client_builder.build(), "Build reqwest client failed: {}"); @@ -71,13 +83,6 @@ async fn main() -> XResult<()> { information!("token_endpoint: {}", &token_endpoint); information!("userinfo_endpoint: {}", &userinfo_endpoint); - let openid_client_configuration = OpenIdClientConfiguration { - issuer: "".into(), - client_id: "".into(), - client_secret: "".into(), - scope: "".into(), - }; - // xh POST https://oauth2.googleapis.com/device/code // client_id==364023986785-o5mqsqd1ej2d1vmvcqai108b7m7v7vc9.apps.googleusercontent.com scope=openid //{ @@ -87,12 +92,30 @@ async fn main() -> XResult<()> { // "interval": 5, // "verification_url": "https://www.google.com/device" // } + debugging!("Get {}", device_authorization_endpoint); let mut params = HashMap::new(); - params.insert("client_id", &openid_client_configuration.issuer); + params.insert("client_id", &openid_client_configuration.client_id); params.insert("scope", &openid_client_configuration.scope); - let token_endpoint_builder = opt_result!(client.post(&device_authorization_endpoint) - .form(params).send().await, "Get token: {}, failed: {}", device_authorization_endpoint); - // token_endpoint_builder. + let device_authorization_result = opt_result!(client.post(&device_authorization_endpoint) + .form(¶ms).send().await, "Get device authorization: {}, failed: {}", device_authorization_endpoint); + let device_authorization = opt_result!( + device_authorization_result.json::().await, "Parse device authorization failed: {}"); + + let hide_qr = matches.is_present("hide-qr"); + let input = &device_authorization.get_complete_url(); + if !hide_qr { + let qr_url = format!("{}{}", "https://hatter.ink/qr/qr_terminal.action?qr=", urlencoding::encode(input)); + let qr_text = ClientBuilder::new().timeout(Duration::from_secs(10)) + .connect_timeout(Duration::from_secs(6)) + .danger_accept_invalid_certs(true) + .build()? + .get(qr_url) + .send().await? + .text().await?; + print!("{}", qr_text); + } + success!("User code url: {}", input); + success!("Get device authorization success: {}", serde_json::to_string_pretty(&device_authorization).unwrap()); // xh POST https://oauth2.googleapis.com/token // client_id==364023986785-o5mqsqd1ej2d1vmvcqai108b7m7v7vc9.apps.googleusercontent.com @@ -107,6 +130,50 @@ async fn main() -> XResult<()> { // "token_type": "Bearer", // "id_token": "***" // } + let grant_type_device_code = "urn:ietf:params:oauth:grant-type:device_code".to_string(); + debugging!("Get {}", token_endpoint); + for i in 0..1000 { + print_lastline(&format!("@{}", i)); + sleep(Duration::from_secs(device_authorization.interval as u64)).await; + let mut params = HashMap::new(); + params.insert("grant_type", &grant_type_device_code); + params.insert("client_id", &openid_client_configuration.client_id); + params.insert("client_secret", &openid_client_configuration.client_secret); + params.insert("device_code", &device_authorization.device_code); + + let token_response_result = opt_result!( + client.post(&token_endpoint).form(¶ms).send().await, "Post url: {}, failed: {}", token_endpoint); + + let token_response = opt_result!(token_response_result.text().await, "Get token failed: {}"); + debugging!("Get token response: {}", token_response); + + let try_parse_error: serde_json::error::Result = serde_json::from_str(&token_response); + if let Ok(error) = try_parse_error { + let err = &error.error; + if err == "authorization_pending" { + // JUST FINE + } else if err == "slow_down" { + sleep(Duration::from_secs(5)).await; + } else { + clear_lastline(); + failure!("Unknown error: {}, description: {}", + err, error.error_description.unwrap_or_else(|| "".to_string())); + break; + } + continue; + } + + let try_parse_token: serde_json::error::Result = serde_json::from_str(&token_response); + if let Ok(token) = try_parse_token { + clear_lastline(); + success!("Get token: {}", serde_json::to_string_pretty(&token).unwrap()); + break; + } + + clear_lastline(); + failure!("Get token failed, raw output: {}", token_response); + break; + } Ok(()) } diff --git a/src/oidc.rs b/src/oidc.rs index 9a0b339..c99e29d 100644 --- a/src/oidc.rs +++ b/src/oidc.rs @@ -30,7 +30,23 @@ pub struct DeviceAuthorizationResponse { pub user_code: String, pub expires_in: i64, pub interval: i64, - pub verification_url: String, + pub verification_url: Option, + pub verification_uri: Option, + pub verification_uri_complete: Option, +} + +impl DeviceAuthorizationResponse { + pub fn get_complete_url(&self) -> String { + let mut s = String::new(); + if let Some(u) = &self.verification_uri_complete { + s.push_str(u); + } else if let Some(u) = self.verification_uri.as_ref().or(self.verification_url.as_ref()) { + s.push_str(u); + s.push_str("?user_code="); + s.push_str(&self.user_code); + } + return s; + } } #[derive(Clone, Debug, Serialize, Deserialize)]