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, 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; const NAME: &str = env!("CARGO_PKG_NAME"); const VERSION: &str = env!("CARGO_PKG_VERSION"); const AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); #[tokio::main] async fn main() -> XResult<()> { let matches = App::new(NAME) .version(VERSION) .about(DESCRIPTION) .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("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") { env::set_var("LOGGER_LEVEL", "*"); } if matches.is_present("version") { information!("{} v{}", NAME, VERSION); exit(1); } 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); let mut client_builder = ClientBuilder::new() .timeout(Duration::from_secs(10)) .connect_timeout(Duration::from_secs(3)); if let Some(proxy) = matches.value_of("proxy") { let proxy = opt_result!( Proxy::all(proxy), "Parse proxy: {} failed: {}", proxy); client_builder = client_builder.proxy(proxy); } 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: {}"); debugging!("Sending request..."); let get_openid_configuration_result = opt_result!( client.get(&openid_configuration_url).send().await, "Get url: {}, failed: {}", openid_configuration_url); let openid_configuration = opt_result!( get_openid_configuration_result.json::().await, "Parse open id configuration failed: {}"); rust_util::util_msg::when(MessageType::DEBUG, || { debugging!("Return open id configuration: {}", serde_json::to_string_pretty(&openid_configuration).unwrap()); }); let device_authorization_endpoint = opt_value_result!( openid_configuration.device_authorization_endpoint, "Open id configuration has not configured device authorization endpoint"); let token_endpoint = openid_configuration.token_endpoint; let userinfo_endpoint = openid_configuration.userinfo_endpoint; information!("device_authorization_endpoint: {}", &device_authorization_endpoint); information!("token_endpoint: {}", &token_endpoint); information!("userinfo_endpoint: {}", &userinfo_endpoint); // xh POST https://oauth2.googleapis.com/device/code // client_id==364023986785-o5mqsqd1ej2d1vmvcqai108b7m7v7vc9.apps.googleusercontent.com scope=openid //{ // "device_code": "***", // "user_code": "STLB-RNKS", // "expires_in": 1800, // "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.client_id); params.insert("scope", &openid_client_configuration.scope); 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 // client_secret==*** // device_code==*** // grant_type==urn:ietf:params:oauth:grant-type:device_code // { // "access_token": "***", // "expires_in": 3599, // "refresh_token": "***", // "scope": "openid", // "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(()) }