From 3454454bd569c7b993bfd55b8f92ad4536edf9e5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 19 Oct 2022 22:48:51 +0800 Subject: [PATCH 1/4] account oidc init rs Signed-off-by: fufesou --- Cargo.lock | 2 + Cargo.toml | 1 + src/hbbs_http.rs | 42 +++++++ src/hbbs_http/account.rs | 242 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 5 files changed, 289 insertions(+) create mode 100644 src/hbbs_http.rs create mode 100644 src/hbbs_http/account.rs diff --git a/Cargo.lock b/Cargo.lock index 26ed9455f..3fddb5c37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4419,6 +4419,7 @@ dependencies = [ "system_shutdown", "tray-item", "trayicon", + "url", "uuid", "virtual_display", "whoami", @@ -5492,6 +5493,7 @@ dependencies = [ "idna", "matches", "percent-encoding", + "serde 1.0.144", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c950c8723..9516d8526 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ wol-rs = "0.9.1" flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } errno = "0.2.8" rdev = { git = "https://github.com/asur4s/rdev" } +url = { version = "2.1", features = ["serde"] } [target.'cfg(not(target_os = "linux"))'.dependencies] reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false } diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs new file mode 100644 index 000000000..b0e8cdbab --- /dev/null +++ b/src/hbbs_http.rs @@ -0,0 +1,42 @@ +use hbb_common::{ + anyhow::{self, bail}, + tokio, ResultType, +}; +use reqwest::Response; +use serde_derive::Deserialize; +use serde_json::{Map, Value}; +use serde::de::DeserializeOwned; + +pub mod account; + +pub enum HbbHttpResponse { + ErrorFormat, + Error(String), + DataTypeFormat, + Data(T), +} + +#[tokio::main(flavor = "current_thread")] +async fn resp_to_serde_map(resp: Response) -> reqwest::Result> { + resp.json().await +} + +impl TryFrom for HbbHttpResponse { + type Error = reqwest::Error; + + fn try_from(resp: Response) -> Result>::Error> { + let map = resp_to_serde_map(resp)?; + if let Some(error) = map.get("error") { + if let Some(err) = error.as_str() { + Ok(Self::Error(err.to_owned())) + } else { + Ok(Self::ErrorFormat) + } + } else { + match serde_json::from_value(Value::Object(map)) { + Ok(v) => Ok(Self::Data(v)), + Err(_) => Ok(Self::DataTypeFormat), + } + } + } +} diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs new file mode 100644 index 000000000..85b2f7e82 --- /dev/null +++ b/src/hbbs_http/account.rs @@ -0,0 +1,242 @@ +use super::HbbHttpResponse; +use hbb_common::{config::Config, log, sleep, tokio, tokio::sync::RwLock, ResultType}; +use serde_derive::Deserialize; +use std::{ + collections::HashMap, + sync::Arc, + time::{Duration, Instant}, +}; +use url::Url; + +lazy_static::lazy_static! { + static ref API_SERVER: String = crate::get_api_server( + Config::get_option("api-server"), Config::get_option("custom-rendezvous-server")); + static ref OIDC_SESSION: Arc> = Arc::new(RwLock::new(OidcSession::new())); +} + +const QUERY_INTERVAL_SECS: f32 = 1.0; +const QUERY_TIMEOUT_SECS: u64 = 60; + +#[derive(Deserialize, Clone)] +pub struct OidcAuthUrl { + code: String, + url: Url, +} + +#[derive(Debug, Deserialize, Default, Clone)] +pub struct UserPayload { + pub id: String, + pub name: String, + pub email: Option, + pub note: Option, + pub status: Option, + pub grp: Option, + pub is_admin: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct AuthBody { + access_token: String, + token_type: String, + user: UserPayload, +} + +#[derive(Copy, Clone)] +pub enum OidcState { + // initial request + OidcRequest = 1, + // initial request failed + OidcRequestFailed = 2, + // request succeeded, loop querying + OidcQuerying = 11, + // loop querying failed + OidcQueryFailed = 12, + // query sucess before + OidcNotExists = 13, + // query timeout + OidcQueryTimeout = 14, + // already login + OidcLogin = 21, +} + +pub struct OidcSession { + client: reqwest::Client, + state: OidcState, + failed_msg: String, + code_url: Option, + auth_body: Option, + keep_querying: bool, + running: bool, + query_timeout: Duration, +} + +impl OidcSession { + fn new() -> Self { + Self { + client: reqwest::Client::new(), + state: OidcState::OidcRequest, + failed_msg: "".to_owned(), + code_url: None, + auth_body: None, + keep_querying: false, + running: false, + query_timeout: Duration::from_secs(QUERY_TIMEOUT_SECS), + } + } + + async fn auth(op: &str, id: &str, uuid: &str) -> ResultType> { + Ok(OIDC_SESSION + .read() + .await + .client + .post(format!("{}/api/oidc/auth", *API_SERVER)) + .json(&HashMap::from([("op", op), ("id", id), ("uuid", uuid)])) + .send() + .await? + .try_into()?) + } + + async fn query(code: &str, id: &str, uuid: &str) -> ResultType> { + let url = reqwest::Url::parse_with_params( + &format!("{}/api/oidc/auth-query", *API_SERVER), + &[("code", code), ("id", id), ("uuid", uuid)], + )?; + Ok(OIDC_SESSION + .read() + .await + .client + .get(url) + .send() + .await? + .try_into()?) + } + + fn reset(&mut self) { + self.state = OidcState::OidcRequest; + self.failed_msg = "".to_owned(); + self.keep_querying = true; + self.running = false; + self.code_url = None; + self.auth_body = None; + } + + async fn before_task(&mut self) { + self.reset(); + self.running = true; + } + + async fn after_task(&mut self) { + self.running = false; + } + + async fn auth_task(op: String, id: String, uuid: String) { + let code_url = match Self::auth(&op, &id, &uuid).await { + Ok(HbbHttpResponse::<_>::Data(code_url)) => code_url, + Ok(HbbHttpResponse::<_>::Error(err)) => { + OIDC_SESSION + .write() + .await + .set_state(OidcState::OidcRequestFailed, err); + return; + } + Ok(_) => { + OIDC_SESSION.write().await.set_state( + OidcState::OidcRequestFailed, + "Invalid auth response".to_owned(), + ); + return; + } + Err(err) => { + OIDC_SESSION + .write() + .await + .set_state(OidcState::OidcRequestFailed, err.to_string()); + return; + } + }; + + OIDC_SESSION + .write() + .await + .set_state(OidcState::OidcQuerying, "".to_owned()); + OIDC_SESSION.write().await.code_url = Some(code_url.clone()); + + let begin = Instant::now(); + let query_timeout = OIDC_SESSION.read().await.query_timeout; + while OIDC_SESSION.read().await.keep_querying && begin.elapsed() < query_timeout { + match Self::query(&code_url.code, &id, &uuid).await { + Ok(HbbHttpResponse::<_>::Data(auth_body)) => { + OIDC_SESSION + .write() + .await + .set_state(OidcState::OidcLogin, "".to_owned()); + OIDC_SESSION.write().await.auth_body = Some(auth_body); + return; + // to-do, set access-token + } + Ok(HbbHttpResponse::<_>::Error(err)) => { + if err.contains("No authed oidc is found") { + // ignore, keep querying + } else { + OIDC_SESSION + .write() + .await + .set_state(OidcState::OidcQueryFailed, err); + return; + } + } + Ok(_) => { + // ignore + } + Err(err) => { + log::trace!("Failed query oidc {}", err); + // ignore + } + } + sleep(QUERY_INTERVAL_SECS).await; + } + + if begin.elapsed() >= query_timeout { + OIDC_SESSION + .write() + .await + .set_state(OidcState::OidcQueryTimeout, "timeout".to_owned()); + } + + // no need to handle "keep_querying == false" + } + + fn set_state(&mut self, state: OidcState, failed_msg: String) { + self.state = state; + self.failed_msg = failed_msg; + } + + pub async fn account_auth(op: String, id: String, uuid: String) { + if OIDC_SESSION.read().await.running { + OIDC_SESSION.write().await.keep_querying = false; + } + let wait_secs = 0.3; + sleep(wait_secs).await; + while OIDC_SESSION.read().await.running { + sleep(wait_secs).await; + } + + tokio::spawn(async move { + OIDC_SESSION.write().await.before_task().await; + Self::auth_task(op, id, uuid).await; + OIDC_SESSION.write().await.after_task().await; + }); + } + + fn get_result_(&self) -> (u8, String, Option) { + ( + self.state as u8, + self.failed_msg.clone(), + self.auth_body.clone(), + ) + } + + pub async fn get_result() -> (u8, String, Option) { + OIDC_SESSION.read().await.get_result_() + } +} diff --git a/src/lib.rs b/src/lib.rs index 58dc50b04..eb8a876ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,8 @@ mod ui_cm_interface; mod ui_interface; mod ui_session_interface; +mod hbbs_http; + #[cfg(windows)] pub mod clipboard_file; From 87e53501e3bbaed4c3a9475b70500f1357463ad2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 20 Oct 2022 23:03:54 +0800 Subject: [PATCH 2/4] feat_account: mid commit Signed-off-by: fufesou --- flutter/lib/common/widgets/address_book.dart | 1 + .../lib/desktop/pages/desktop_home_page.dart | 2 +- .../desktop/pages/desktop_setting_page.dart | 1 + flutter/lib/desktop/widgets/login.dart | 469 ++++++++++++++++++ flutter/lib/models/user_model.dart | 4 +- src/flutter_ffi.rs | 10 + src/hbbs_http/account.rs | 86 ++-- src/ui_interface.rs | 13 +- 8 files changed, 536 insertions(+), 50 deletions(-) create mode 100644 flutter/lib/desktop/widgets/login.dart diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 570ff6e95..c96dc115a 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/desktop/widgets/login.dart'; import '../../consts.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:get/get.dart'; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index ad0aa4316..ef38bf443 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -472,7 +472,7 @@ class _DesktopHomePageState extends State /// common login dialog for desktop /// call this directly -Future loginDialog() async { +Future loginDialog2() async { String userName = ""; var userNameMsg = ""; String pass = ""; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b063c3c15..12bb935e9 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; +import 'package:flutter_hbb/desktop/widgets/login.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart new file mode 100644 index 000000000..dec304cda --- /dev/null +++ b/flutter/lib/desktop/widgets/login.dart @@ -0,0 +1,469 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import '../../common.dart'; +import '../widgets/button.dart'; + +class _IconOP extends StatelessWidget { + final String icon; + final double iconWidth; + const _IconOP({Key? key, required this.icon, required this.iconWidth}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4.0), + child: SvgPicture.asset( + 'assets/$icon.svg', + width: iconWidth, + ), + ); + } +} + +class ButtonOP extends StatelessWidget { + final String op; + final RxString curOP; + final double iconWidth; + final Color primaryColor; + final double height; + final Function() onTap; + + const ButtonOP({ + Key? key, + required this.op, + required this.curOP, + required this.iconWidth, + required this.primaryColor, + required this.height, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row(children: [ + Expanded( + child: Container( + height: height, + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: Obx(() => ElevatedButton( + style: ElevatedButton.styleFrom( + primary: curOP.value.isEmpty || curOP.value == op + ? primaryColor + : Colors.grey, + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), + onPressed: + curOP.value.isEmpty || curOP.value == op ? onTap : null, + child: Stack(children: [ + // to-do: translate + Center(child: Text('Continue with $op')), + Align( + alignment: Alignment.centerLeft, + child: SizedBox( + width: 120, + child: _IconOP( + icon: op, + iconWidth: iconWidth, + )), + ), + ]), + )), + ), + ) + ]); + } +} + +class ConfigOP { + final String op; + final double iconWidth; + ConfigOP({required this.op, required this.iconWidth}); +} + +class WidgetOP extends StatefulWidget { + final ConfigOP config; + final RxString curOP; + const WidgetOP({ + Key? key, + required this.config, + required this.curOP, + }) : super(key: key); + + @override + State createState() { + return _WidgetOPState(); + } +} + +class _WidgetOPState extends State { + Timer? _updateTimer; + String _stateMsg = ''; + String _stateFailedMsg = ''; + String _url = ''; + String _username = ''; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + _updateTimer?.cancel(); + } + + _beginQueryState() { + _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { + _updateState(); + }); + } + + _updateState() { + bind.mainAccountAuthResult().then((result) { + if (result.isEmpty) { + return; + } + final resultMap = jsonDecode(result); + if (resultMap == null) { + return; + } + final String stateMsg = resultMap['state_msg']; + final String failedMsg = resultMap['failed_msg']; + // to-do: test null url + final String url = resultMap['url']; + if (_stateMsg != stateMsg) { + if (_url.isEmpty && url.isNotEmpty) { + launchUrl(Uri.parse(url)); + _url = url; + } + setState(() { + _stateMsg = stateMsg; + _stateFailedMsg = failedMsg; + }); + } + }); + } + + _resetState() { + _stateMsg = ''; + _stateFailedMsg = ''; + _url = ''; + _username = ''; + } + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + children: [ + ButtonOP( + op: widget.config.op, + curOP: widget.curOP, + iconWidth: widget.config.iconWidth, + primaryColor: str2color(widget.config.op, 0x7f), + height: 40, + onTap: () { + widget.curOP.value = widget.config.op; + bind.mainAccountAuth(op: widget.config.op); + _beginQueryState(); + }, + ), + Obx(() => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: Text( + _stateMsg, + style: TextStyle(fontSize: 12), + ))), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: const SizedBox( + height: 5.0, + ), + ), + ), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 20), + child: ElevatedButton( + onPressed: () { + widget.curOP.value = ''; + _updateTimer?.cancel(); + _resetState(); + }, + child: Text( + translate('Cancel'), + style: TextStyle(fontSize: 15), + ), + ), + ), + ), + ), + ], + )); + } +} + +class LoginWidgetOP extends StatelessWidget { + final List ops; + final RxString curOP = ''.obs; + + LoginWidgetOP({ + Key? key, + required this.ops, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var children = ops + .map((op) => [ + WidgetOP( + config: op, + curOP: curOP, + ), + const Divider() + ]) + .expand((i) => i) + .toList(); + if (children.isNotEmpty) { + children.removeLast(); + } + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: children, + )); + } +} + +class LoginWidgetUserPass extends StatelessWidget { + final String username; + final String pass; + final String usernameMsg; + final String passMsg; + final bool isInProgress; + final Function(String, String) onLogin; + const LoginWidgetUserPass({ + Key? key, + required this.username, + required this.pass, + required this.usernameMsg, + required this.passMsg, + required this.isInProgress, + required this.onLogin, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var userController = TextEditingController(text: username); + var pwdController = TextEditingController(text: pass); + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: Text( + '${translate("Username")}:', + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + const SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: usernameMsg.isNotEmpty ? usernameMsg : null), + controller: userController, + focusNode: FocusNode()..requestFocus(), + ), + ), + ], + ), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: Text('${translate("Password")}:') + .marginOnly(bottom: 16.0)), + const SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: passMsg.isNotEmpty ? passMsg : null), + controller: pwdController, + ), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()), + const SizedBox( + height: 12.0, + ), + Row(children: [ + Expanded( + child: Container( + height: 50, + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: ElevatedButton( + child: const Text( + 'Login', + style: TextStyle(fontSize: 18), + ), + onPressed: () { + onLogin(userController.text, pwdController.text); + }, + ), + ), + ), + ]), + ], + ), + ); + } +} + +/// common login dialog for desktop +/// call this directly +Future loginDialog() async { + String username = ''; + var usernameMsg = ''; + String pass = ''; + var passMsg = ''; + var isInProgress = false; + var completer = Completer(); + + gFFI.dialogManager.show((setState, close) { + cancel() { + isInProgress = false; + completer.complete(false); + close(); + } + + onLogin(String username0, String pass0) async { + setState(() { + usernameMsg = ''; + passMsg = ''; + isInProgress = true; + }); + cancel() { + if (isInProgress) { + setState(() { + isInProgress = false; + }); + } + } + + username = username0; + pass = pass0; + if (username.isEmpty) { + usernameMsg = translate('Username missed'); + debugPrint('REMOVE ME ====================== username empty'); + cancel(); + return; + } + if (pass.isEmpty) { + passMsg = translate('Password missed'); + debugPrint('REMOVE ME ====================== password empty'); + cancel(); + return; + } + try { + final resp = await gFFI.userModel.login(username, pass); + if (resp.containsKey('error')) { + passMsg = resp['error']; + debugPrint('REMOVE ME ====================== password error'); + cancel(); + return; + } + // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, + // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} + debugPrint('$resp'); + completer.complete(true); + } catch (err) { + debugPrint(err.toString()); + debugPrint( + 'REMOVE ME ====================== login error ${err.toString()}'); + cancel(); + return; + } + close(); + } + + return CustomAlertDialog( + title: Text(translate('Login')), + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + LoginWidgetUserPass( + username: username, + pass: pass, + usernameMsg: usernameMsg, + passMsg: passMsg, + isInProgress: isInProgress, + onLogin: onLogin, + ), + const SizedBox( + height: 8.0, + ), + const Center( + child: Text( + 'or', + style: TextStyle(fontSize: 16), + )), + const SizedBox( + height: 8.0, + ), + LoginWidgetOP(ops: [ + ConfigOP(op: 'Github', iconWidth: 24), + ConfigOP(op: 'Google', iconWidth: 24), + ConfigOP(op: 'Okta', iconWidth: 46), + ]), + ], + ), + ), + actions: [ + TextButton(onPressed: cancel, child: Text(translate('Cancel'))), + ], + onCancel: cancel, + ); + }); + return completer.future; +} diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 721aac5b5..cfbe71cfe 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -77,7 +77,9 @@ class UserModel { return ""; } final m = jsonDecode(userInfo); - if (m != null) { + if (m == null) { + userName.value = ''; + } else { userName.value = m['name'] ?? ''; } return userName.value; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f20eb6153..937c108ed 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -13,6 +13,8 @@ use hbb_common::{ fs, log, }; +// use crate::hbbs_http::account::AuthResult; + use crate::flutter::{self, SESSIONS}; #[cfg(target_os = "android")] use crate::start_server; @@ -1082,6 +1084,14 @@ pub fn install_install_path() -> SyncReturn { SyncReturn(install_path()) } +pub fn main_account_auth(op: String) { + account_auth(op); +} + +pub fn main_account_auth_result() -> String { + account_auth_result() +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs index 85b2f7e82..4a2e365f4 100644 --- a/src/hbbs_http/account.rs +++ b/src/hbbs_http/account.rs @@ -1,6 +1,6 @@ use super::HbbHttpResponse; use hbb_common::{config::Config, log, sleep, tokio, tokio::sync::RwLock, ResultType}; -use serde_derive::Deserialize; +use serde_derive::{Deserialize, Serialize}; use std::{ collections::HashMap, sync::Arc, @@ -16,6 +16,9 @@ lazy_static::lazy_static! { const QUERY_INTERVAL_SECS: f32 = 1.0; const QUERY_TIMEOUT_SECS: u64 = 60; +const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth"; +const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth"; +const LOGIN_ACCOUNT_AUTH: &str = "Login account auth"; #[derive(Deserialize, Clone)] pub struct OidcAuthUrl { @@ -23,7 +26,7 @@ pub struct OidcAuthUrl { url: Url, } -#[derive(Debug, Deserialize, Default, Clone)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct UserPayload { pub id: String, pub name: String, @@ -34,34 +37,16 @@ pub struct UserPayload { pub is_admin: Option, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthBody { - access_token: String, - token_type: String, - user: UserPayload, -} - -#[derive(Copy, Clone)] -pub enum OidcState { - // initial request - OidcRequest = 1, - // initial request failed - OidcRequestFailed = 2, - // request succeeded, loop querying - OidcQuerying = 11, - // loop querying failed - OidcQueryFailed = 12, - // query sucess before - OidcNotExists = 13, - // query timeout - OidcQueryTimeout = 14, - // already login - OidcLogin = 21, + pub access_token: String, + pub token_type: String, + pub user: UserPayload, } pub struct OidcSession { client: reqwest::Client, - state: OidcState, + state_msg: &'static str, failed_msg: String, code_url: Option, auth_body: Option, @@ -70,11 +55,19 @@ pub struct OidcSession { query_timeout: Duration, } +#[derive(Serialize)] +pub struct AuthResult { + pub state_msg: String, + pub failed_msg: String, + pub url: Option, + pub auth_body: Option, +} + impl OidcSession { fn new() -> Self { Self { client: reqwest::Client::new(), - state: OidcState::OidcRequest, + state_msg: REQUESTING_ACCOUNT_AUTH, failed_msg: "".to_owned(), code_url: None, auth_body: None, @@ -112,7 +105,7 @@ impl OidcSession { } fn reset(&mut self) { - self.state = OidcState::OidcRequest; + self.state_msg = REQUESTING_ACCOUNT_AUTH; self.failed_msg = "".to_owned(); self.keep_querying = true; self.running = false; @@ -136,21 +129,21 @@ impl OidcSession { OIDC_SESSION .write() .await - .set_state(OidcState::OidcRequestFailed, err); + .set_state(REQUESTING_ACCOUNT_AUTH, err); return; } Ok(_) => { - OIDC_SESSION.write().await.set_state( - OidcState::OidcRequestFailed, - "Invalid auth response".to_owned(), - ); + OIDC_SESSION + .write() + .await + .set_state(REQUESTING_ACCOUNT_AUTH, "Invalid auth response".to_owned()); return; } Err(err) => { OIDC_SESSION .write() .await - .set_state(OidcState::OidcRequestFailed, err.to_string()); + .set_state(REQUESTING_ACCOUNT_AUTH, err.to_string()); return; } }; @@ -158,7 +151,7 @@ impl OidcSession { OIDC_SESSION .write() .await - .set_state(OidcState::OidcQuerying, "".to_owned()); + .set_state(WAITING_ACCOUNT_AUTH, "".to_owned()); OIDC_SESSION.write().await.code_url = Some(code_url.clone()); let begin = Instant::now(); @@ -169,7 +162,7 @@ impl OidcSession { OIDC_SESSION .write() .await - .set_state(OidcState::OidcLogin, "".to_owned()); + .set_state(LOGIN_ACCOUNT_AUTH, "".to_owned()); OIDC_SESSION.write().await.auth_body = Some(auth_body); return; // to-do, set access-token @@ -181,7 +174,7 @@ impl OidcSession { OIDC_SESSION .write() .await - .set_state(OidcState::OidcQueryFailed, err); + .set_state(WAITING_ACCOUNT_AUTH, err); return; } } @@ -200,14 +193,14 @@ impl OidcSession { OIDC_SESSION .write() .await - .set_state(OidcState::OidcQueryTimeout, "timeout".to_owned()); + .set_state(WAITING_ACCOUNT_AUTH, "timeout".to_owned()); } // no need to handle "keep_querying == false" } - fn set_state(&mut self, state: OidcState, failed_msg: String) { - self.state = state; + fn set_state(&mut self, state_msg: &'static str, failed_msg: String) { + self.state_msg = state_msg; self.failed_msg = failed_msg; } @@ -228,15 +221,16 @@ impl OidcSession { }); } - fn get_result_(&self) -> (u8, String, Option) { - ( - self.state as u8, - self.failed_msg.clone(), - self.auth_body.clone(), - ) + fn get_result_(&self) -> AuthResult { + AuthResult { + state_msg: self.state_msg.to_string(), + failed_msg: self.failed_msg.clone(), + url: self.code_url.as_ref().map(|x| x.url.to_string()), + auth_body: self.auth_body.clone(), + } } - pub async fn get_result() -> (u8, String, Option) { + pub async fn get_result() -> AuthResult { OIDC_SESSION.read().await.get_result_() } } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index cb2c178d6..78d9433b1 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -20,8 +20,7 @@ use hbb_common::{ tokio::{self, sync::mpsc, time}, }; -use crate::ipc; -use crate::{common::SOFTWARE_UPDATE_URL, platform}; +use crate::{common::SOFTWARE_UPDATE_URL, hbbs_http::account, ipc, platform}; type Message = RendezvousMessage; @@ -843,6 +842,16 @@ pub(crate) fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender String { + serde_json::to_string(&account::OidcSession::get_result().await).unwrap_or_default() +} + // notice: avoiding create ipc connecton repeatly, // because windows named pipe has serious memory leak issue. #[tokio::main(flavor = "current_thread")] From a84ee7a6ec9299b709683d1ea6eb9b959b721d6b Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 22 Oct 2022 22:19:14 +0800 Subject: [PATCH 3/4] oidc: init debug Signed-off-by: fufesou --- Cargo.toml | 3 +- flutter/lib/desktop/widgets/login.dart | 338 +++++++++++--------- flutter/lib/mobile/pages/settings_page.dart | 6 +- flutter/lib/models/user_model.dart | 51 +-- src/flutter_ffi.rs | 8 +- src/hbbs_http.rs | 12 +- src/hbbs_http/account.rs | 109 ++++--- src/main.rs | 15 +- src/server.rs | 13 +- src/ui_interface.rs | 14 +- 10 files changed, 322 insertions(+), 247 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9516d8526..1eb92c4e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,8 +66,7 @@ errno = "0.2.8" rdev = { git = "https://github.com/asur4s/rdev" } url = { version = "2.1", features = ["serde"] } -[target.'cfg(not(target_os = "linux"))'.dependencies] -reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false } +reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart index dec304cda..5f849d822 100644 --- a/flutter/lib/desktop/widgets/login.dart +++ b/flutter/lib/desktop/widgets/login.dart @@ -2,15 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:url_launcher/url_launcher_string.dart'; import '../../common.dart'; -import '../widgets/button.dart'; class _IconOP extends StatelessWidget { final String icon; @@ -92,10 +89,12 @@ class ConfigOP { class WidgetOP extends StatefulWidget { final ConfigOP config; final RxString curOP; + final Function(String) cbLogin; const WidgetOP({ Key? key, required this.config, required this.curOP, + required this.cbLogin, }) : super(key: key); @override @@ -107,9 +106,8 @@ class WidgetOP extends StatefulWidget { class _WidgetOPState extends State { Timer? _updateTimer; String _stateMsg = ''; - String _stateFailedMsg = ''; + String _FailedMsg = ''; String _url = ''; - String _username = ''; @override void initState() { @@ -138,17 +136,28 @@ class _WidgetOPState extends State { return; } final String stateMsg = resultMap['state_msg']; - final String failedMsg = resultMap['failed_msg']; - // to-do: test null url - final String url = resultMap['url']; - if (_stateMsg != stateMsg) { - if (_url.isEmpty && url.isNotEmpty) { + String failedMsg = resultMap['failed_msg']; + final String? url = resultMap['url']; + final authBody = resultMap['auth_body']; + if (_stateMsg != stateMsg || _FailedMsg != failedMsg) { + if (_url.isEmpty && url != null && url.isNotEmpty) { launchUrl(Uri.parse(url)); _url = url; } + if (authBody != null) { + _updateTimer?.cancel(); + final String username = authBody['user']['name']; + widget.curOP.value = ''; + widget.cbLogin(username); + } + setState(() { _stateMsg = stateMsg; - _stateFailedMsg = failedMsg; + _FailedMsg = failedMsg; + if (failedMsg.isNotEmpty) { + widget.curOP.value = ''; + _updateTimer?.cancel(); + } }); } }); @@ -156,74 +165,95 @@ class _WidgetOPState extends State { _resetState() { _stateMsg = ''; - _stateFailedMsg = ''; + _FailedMsg = ''; _url = ''; - _username = ''; } @override Widget build(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - children: [ - ButtonOP( - op: widget.config.op, - curOP: widget.curOP, - iconWidth: widget.config.iconWidth, - primaryColor: str2color(widget.config.op, 0x7f), - height: 40, - onTap: () { - widget.curOP.value = widget.config.op; - bind.mainAccountAuth(op: widget.config.op); - _beginQueryState(); - }, - ), - Obx(() => Offstage( - offstage: widget.curOP.value != widget.config.op, - child: Text( - _stateMsg, - style: TextStyle(fontSize: 12), - ))), - Obx( - () => Offstage( - offstage: widget.curOP.value != widget.config.op, - child: const SizedBox( - height: 5.0, - ), - ), - ), - Obx( - () => Offstage( - offstage: widget.curOP.value != widget.config.op, - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: 20), - child: ElevatedButton( - onPressed: () { - widget.curOP.value = ''; - _updateTimer?.cancel(); - _resetState(); - }, - child: Text( - translate('Cancel'), - style: TextStyle(fontSize: 15), + return Column( + children: [ + ButtonOP( + op: widget.config.op, + curOP: widget.curOP, + iconWidth: widget.config.iconWidth, + primaryColor: str2color(widget.config.op, 0x7f), + height: 40, + onTap: () async { + _resetState(); + widget.curOP.value = widget.config.op; + await bind.mainAccountAuth(op: widget.config.op); + _beginQueryState(); + }, + ), + Obx(() { + if (widget.curOP.isNotEmpty && + widget.curOP.value != widget.config.op) { + _FailedMsg = ''; + } + return Offstage( + offstage: + _FailedMsg.isEmpty && widget.curOP.value != widget.config.op, + child: Row( + children: [ + Text( + _stateMsg, + style: TextStyle(fontSize: 12), + ), + SizedBox(width: 8), + Text( + _FailedMsg, + style: TextStyle( + fontSize: 14, + color: Colors.red, ), ), + ], + )); + }), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: const SizedBox( + height: 5.0, + ), + ), + ), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 20), + child: ElevatedButton( + onPressed: () { + widget.curOP.value = ''; + _updateTimer?.cancel(); + _resetState(); + bind.mainAccountAuthCancel(); + }, + child: Text( + translate('Cancel'), + style: TextStyle(fontSize: 15), ), ), ), - ], - )); + ), + ), + ], + ); } } class LoginWidgetOP extends StatelessWidget { final List ops; - final RxString curOP = ''.obs; + final RxString curOP; + final Function(String) cbLogin; LoginWidgetOP({ Key? key, required this.ops, + required this.curOP, + required this.cbLogin, }) : super(key: key); @override @@ -233,6 +263,7 @@ class LoginWidgetOP extends StatelessWidget { WidgetOP( config: op, curOP: curOP, + cbLogin: cbLogin, ), const Divider() ]) @@ -256,6 +287,7 @@ class LoginWidgetUserPass extends StatelessWidget { final String usernameMsg; final String passMsg; final bool isInProgress; + final RxString curOP; final Function(String, String) onLogin; const LoginWidgetUserPass({ Key? key, @@ -264,6 +296,7 @@ class LoginWidgetUserPass extends StatelessWidget { required this.usernameMsg, required this.passMsg, required this.isInProgress, + required this.curOP, required this.onLogin, }) : super(key: key); @@ -271,86 +304,90 @@ class LoginWidgetUserPass extends StatelessWidget { Widget build(BuildContext context) { var userController = TextEditingController(text: username); var pwdController = TextEditingController(text: pass); - return ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - '${translate("Username")}:', - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: usernameMsg.isNotEmpty ? usernameMsg : null), - controller: userController, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], - ), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Password")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: passMsg.isNotEmpty ? passMsg : null), - controller: pwdController, - ), - ), - ], - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()), - const SizedBox( - height: 12.0, - ), - Row(children: [ + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: Text( + '${translate("Username")}:', + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + const SizedBox( + width: 24.0, + ), Expanded( - child: Container( - height: 50, - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - child: ElevatedButton( - child: const Text( - 'Login', - style: TextStyle(fontSize: 18), - ), - onPressed: () { - onLogin(userController.text, pwdController.text); - }, - ), + child: TextField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: usernameMsg.isNotEmpty ? usernameMsg : null), + controller: userController, + focusNode: FocusNode()..requestFocus(), ), ), - ]), - ], - ), + ], + ), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: + Text('${translate("Password")}:').marginOnly(bottom: 16.0)), + const SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: passMsg.isNotEmpty ? passMsg : null), + controller: pwdController, + ), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()), + const SizedBox( + height: 12.0, + ), + Row(children: [ + Expanded( + child: Container( + height: 50, + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: Obx(() => ElevatedButton( + style: curOP.value.isEmpty || curOP.value == 'rustdesk' + ? null + : ElevatedButton.styleFrom( + primary: Colors.grey, + ), + child: const Text( + 'Login', + style: TextStyle(fontSize: 18), + ), + onPressed: curOP.value.isEmpty || curOP.value == 'rustdesk' + ? () { + onLogin(userController.text, pwdController.text); + } + : null, + )), + ), + ), + ]), + ], ); } } @@ -364,6 +401,7 @@ Future loginDialog() async { var passMsg = ''; var isInProgress = false; var completer = Completer(); + final RxString curOP = ''.obs; gFFI.dialogManager.show((setState, close) { cancel() { @@ -379,6 +417,7 @@ Future loginDialog() async { isInProgress = true; }); cancel() { + curOP.value = ''; if (isInProgress) { setState(() { isInProgress = false; @@ -386,17 +425,16 @@ Future loginDialog() async { } } + curOP.value = 'rustdesk'; username = username0; pass = pass0; if (username.isEmpty) { usernameMsg = translate('Username missed'); - debugPrint('REMOVE ME ====================== username empty'); cancel(); return; } if (pass.isEmpty) { passMsg = translate('Password missed'); - debugPrint('REMOVE ME ====================== password empty'); cancel(); return; } @@ -404,7 +442,6 @@ Future loginDialog() async { final resp = await gFFI.userModel.login(username, pass); if (resp.containsKey('error')) { passMsg = resp['error']; - debugPrint('REMOVE ME ====================== password error'); cancel(); return; } @@ -414,8 +451,6 @@ Future loginDialog() async { completer.complete(true); } catch (err) { debugPrint(err.toString()); - debugPrint( - 'REMOVE ME ====================== login error ${err.toString()}'); cancel(); return; } @@ -438,6 +473,7 @@ Future loginDialog() async { usernameMsg: usernameMsg, passMsg: passMsg, isInProgress: isInProgress, + curOP: curOP, onLogin: onLogin, ), const SizedBox( @@ -451,11 +487,19 @@ Future loginDialog() async { const SizedBox( height: 8.0, ), - LoginWidgetOP(ops: [ - ConfigOP(op: 'Github', iconWidth: 24), - ConfigOP(op: 'Google', iconWidth: 24), - ConfigOP(op: 'Okta', iconWidth: 46), - ]), + LoginWidgetOP( + ops: [ + ConfigOP(op: 'Github', iconWidth: 24), + ConfigOP(op: 'Google', iconWidth: 24), + ConfigOP(op: 'Okta', iconWidth: 46), + ], + curOP: curOP, + cbLogin: (String username) { + gFFI.userModel.userName.value = username; + completer.complete(true); + close(); + }, + ), ], ), ), diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 7a82bcdd8..269439b1d 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -291,12 +291,12 @@ class _SettingsState extends State with WidgetsBindingObserver { return SettingsList( sections: [ SettingsSection( - title: Text(translate("Account")), + title: Text(translate('Account')), tiles: [ SettingsTile.navigation( title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty - ? translate("Login") - : '${translate("Logout")} (${gFFI.userModel.userName.value})')), + ? translate('Login') + : '${translate('Logout')} (${gFFI.userModel.userName.value})')), leading: Icon(Icons.person), onPressed: (context) { if (gFFI.userModel.userName.value.isEmpty) { diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index cfbe71cfe..d2e83990b 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -9,7 +10,7 @@ import 'model.dart'; import 'platform_model.dart'; class UserModel { - var userName = "".obs; + var userName = ''.obs; WeakReference parent; UserModel(this.parent) { @@ -18,7 +19,7 @@ class UserModel { void refreshCurrentUser() async { await getUserName(); - final token = await bind.mainGetLocalOption(key: "access_token"); + final token = await bind.mainGetLocalOption(key: 'access_token'); if (token == '') return; final url = await bind.mainGetApiServer(); final body = { @@ -28,8 +29,8 @@ class UserModel { try { final response = await http.post(Uri.parse('$url/api/currentUser'), headers: { - "Content-Type": "application/json", - "Authorization": "Bearer $token" + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token' }, body: json.encode(body)); final status = response.statusCode; @@ -44,9 +45,9 @@ class UserModel { } void resetToken() async { - await bind.mainSetLocalOption(key: "access_token", value: ""); - await bind.mainSetLocalOption(key: "user_info", value: ""); - userName.value = ""; + await bind.mainSetLocalOption(key: 'access_token', value: ''); + await bind.mainSetLocalOption(key: 'user_info', value: ''); + userName.value = ''; } Future _parseResp(String body) async { @@ -57,13 +58,13 @@ class UserModel { } final token = data['access_token']; if (token != null) { - await bind.mainSetLocalOption(key: "access_token", value: token); + await bind.mainSetLocalOption(key: 'access_token', value: token); } final info = data['user']; if (info != null) { final value = json.encode(info); - await bind.mainSetOption(key: "user_info", value: value); - userName.value = info["name"]; + await bind.mainSetOption(key: 'user_info', value: value); + userName.value = info['name']; } return ''; } @@ -74,7 +75,7 @@ class UserModel { } final userInfo = await bind.mainGetLocalOption(key: 'user_info'); if (userInfo.trim().isEmpty) { - return ""; + return ''; } final m = jsonDecode(userInfo); if (m == null) { @@ -88,10 +89,10 @@ class UserModel { Future logOut() async { final tag = gFFI.dialogManager.showLoading(translate('Waiting')); final url = await bind.mainGetApiServer(); - final _ = await http.post(Uri.parse("$url/api/logout"), + final _ = await http.post(Uri.parse('$url/api/logout'), body: { - "id": await bind.mainGetMyId(), - "uuid": await bind.mainGetUuid(), + 'id': await bind.mainGetMyId(), + 'uuid': await bind.mainGetUuid(), }, headers: await getHttpHeaders()); await Future.wait([ @@ -100,30 +101,30 @@ class UserModel { bind.mainSetLocalOption(key: 'selected-tags', value: ''), ]); parent.target?.abModel.clear(); - userName.value = ""; + userName.value = ''; gFFI.dialogManager.dismissByTag(tag); } Future> login(String userName, String pass) async { final url = await bind.mainGetApiServer(); try { - final resp = await http.post(Uri.parse("$url/api/login"), - headers: {"Content-Type": "application/json"}, + final resp = await http.post(Uri.parse('$url/api/login'), + headers: {'Content-Type': 'application/json'}, body: jsonEncode({ - "username": userName, - "password": pass, - "id": await bind.mainGetMyId(), - "uuid": await bind.mainGetUuid() + 'username': userName, + 'password': pass, + 'id': await bind.mainGetMyId(), + 'uuid': await bind.mainGetUuid() })); final body = jsonDecode(resp.body); bind.mainSetLocalOption( - key: "access_token", value: body['access_token'] ?? ""); + key: 'access_token', value: body['access_token'] ?? ''); bind.mainSetLocalOption( - key: "user_info", value: jsonEncode(body['user'])); - this.userName.value = body['user']?['name'] ?? ""; + key: 'user_info', value: jsonEncode(body['user'])); + this.userName.value = body['user']?['name'] ?? ''; return body; } catch (err) { - return {"error": "$err"}; + return {'error': '$err'}; } } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 937c108ed..9552bd36e 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1085,7 +1085,13 @@ pub fn install_install_path() -> SyncReturn { } pub fn main_account_auth(op: String) { - account_auth(op); + let id = get_id(); + let uuid = get_uuid(); + account_auth(op, id, uuid); +} + +pub fn main_account_auth_cancel() { + account_auth_cancel() } pub fn main_account_auth_result() -> String { diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index b0e8cdbab..4360b6e8c 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -2,13 +2,14 @@ use hbb_common::{ anyhow::{self, bail}, tokio, ResultType, }; -use reqwest::Response; +use reqwest::blocking::Response; +use serde::de::DeserializeOwned; use serde_derive::Deserialize; use serde_json::{Map, Value}; -use serde::de::DeserializeOwned; pub mod account; +#[derive(Debug)] pub enum HbbHttpResponse { ErrorFormat, Error(String), @@ -16,16 +17,11 @@ pub enum HbbHttpResponse { Data(T), } -#[tokio::main(flavor = "current_thread")] -async fn resp_to_serde_map(resp: Response) -> reqwest::Result> { - resp.json().await -} - impl TryFrom for HbbHttpResponse { type Error = reqwest::Error; fn try_from(resp: Response) -> Result>::Error> { - let map = resp_to_serde_map(resp)?; + let map = resp.json::>()?; if let Some(error) = map.get("error") { if let Some(err) = error.as_str() { Ok(Self::Error(err.to_owned())) diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs index 4a2e365f4..cdf724971 100644 --- a/src/hbbs_http/account.rs +++ b/src/hbbs_http/account.rs @@ -1,9 +1,13 @@ use super::HbbHttpResponse; -use hbb_common::{config::Config, log, sleep, tokio, tokio::sync::RwLock, ResultType}; +use hbb_common::{ + config::{Config, LocalConfig}, + log, sleep, tokio, ResultType, +}; +use reqwest::blocking::Client; use serde_derive::{Deserialize, Serialize}; use std::{ collections::HashMap, - sync::Arc, + sync::{Arc, RwLock}, time::{Duration, Instant}, }; use url::Url; @@ -15,12 +19,12 @@ lazy_static::lazy_static! { } const QUERY_INTERVAL_SECS: f32 = 1.0; -const QUERY_TIMEOUT_SECS: u64 = 60; +const QUERY_TIMEOUT_SECS: u64 = 60 * 3; const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth"; const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth"; const LOGIN_ACCOUNT_AUTH: &str = "Login account auth"; -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Debug)] pub struct OidcAuthUrl { code: String, url: Url, @@ -45,7 +49,7 @@ pub struct AuthBody { } pub struct OidcSession { - client: reqwest::Client, + client: Client, state_msg: &'static str, failed_msg: String, code_url: Option, @@ -66,7 +70,7 @@ pub struct AuthResult { impl OidcSession { fn new() -> Self { Self { - client: reqwest::Client::new(), + client: Client::new(), state_msg: REQUESTING_ACCOUNT_AUTH, failed_msg: "".to_owned(), code_url: None, @@ -77,30 +81,28 @@ impl OidcSession { } } - async fn auth(op: &str, id: &str, uuid: &str) -> ResultType> { + fn auth(op: &str, id: &str, uuid: &str) -> ResultType> { Ok(OIDC_SESSION .read() - .await + .unwrap() .client .post(format!("{}/api/oidc/auth", *API_SERVER)) .json(&HashMap::from([("op", op), ("id", id), ("uuid", uuid)])) - .send() - .await? + .send()? .try_into()?) } - async fn query(code: &str, id: &str, uuid: &str) -> ResultType> { + fn query(code: &str, id: &str, uuid: &str) -> ResultType> { let url = reqwest::Url::parse_with_params( &format!("{}/api/oidc/auth-query", *API_SERVER), &[("code", code), ("id", id), ("uuid", uuid)], )?; Ok(OIDC_SESSION .read() - .await + .unwrap() .client .get(url) - .send() - .await? + .send()? .try_into()?) } @@ -113,36 +115,42 @@ impl OidcSession { self.auth_body = None; } - async fn before_task(&mut self) { + fn before_task(&mut self) { self.reset(); self.running = true; } - async fn after_task(&mut self) { + fn after_task(&mut self) { self.running = false; } - async fn auth_task(op: String, id: String, uuid: String) { - let code_url = match Self::auth(&op, &id, &uuid).await { + fn sleep(secs: f32) { + std::thread::sleep(std::time::Duration::from_secs_f32(secs)); + } + + fn auth_task(op: String, id: String, uuid: String) { + let auth_request_res = Self::auth(&op, &id, &uuid); + log::info!("Request oidc auth result: {:?}", &auth_request_res); + let code_url = match auth_request_res { Ok(HbbHttpResponse::<_>::Data(code_url)) => code_url, Ok(HbbHttpResponse::<_>::Error(err)) => { OIDC_SESSION .write() - .await + .unwrap() .set_state(REQUESTING_ACCOUNT_AUTH, err); return; } Ok(_) => { OIDC_SESSION .write() - .await + .unwrap() .set_state(REQUESTING_ACCOUNT_AUTH, "Invalid auth response".to_owned()); return; } Err(err) => { OIDC_SESSION .write() - .await + .unwrap() .set_state(REQUESTING_ACCOUNT_AUTH, err.to_string()); return; } @@ -150,22 +158,29 @@ impl OidcSession { OIDC_SESSION .write() - .await + .unwrap() .set_state(WAITING_ACCOUNT_AUTH, "".to_owned()); - OIDC_SESSION.write().await.code_url = Some(code_url.clone()); + OIDC_SESSION.write().unwrap().code_url = Some(code_url.clone()); let begin = Instant::now(); - let query_timeout = OIDC_SESSION.read().await.query_timeout; - while OIDC_SESSION.read().await.keep_querying && begin.elapsed() < query_timeout { - match Self::query(&code_url.code, &id, &uuid).await { + let query_timeout = OIDC_SESSION.read().unwrap().query_timeout; + while OIDC_SESSION.read().unwrap().keep_querying && begin.elapsed() < query_timeout { + match Self::query(&code_url.code, &id, &uuid) { Ok(HbbHttpResponse::<_>::Data(auth_body)) => { + LocalConfig::set_option( + "access_token".to_owned(), + auth_body.access_token.clone(), + ); + LocalConfig::set_option( + "user_info".to_owned(), + serde_json::to_string(&auth_body.user).unwrap_or_default(), + ); OIDC_SESSION .write() - .await + .unwrap() .set_state(LOGIN_ACCOUNT_AUTH, "".to_owned()); - OIDC_SESSION.write().await.auth_body = Some(auth_body); + OIDC_SESSION.write().unwrap().auth_body = Some(auth_body); return; - // to-do, set access-token } Ok(HbbHttpResponse::<_>::Error(err)) => { if err.contains("No authed oidc is found") { @@ -173,7 +188,7 @@ impl OidcSession { } else { OIDC_SESSION .write() - .await + .unwrap() .set_state(WAITING_ACCOUNT_AUTH, err); return; } @@ -186,13 +201,13 @@ impl OidcSession { // ignore } } - sleep(QUERY_INTERVAL_SECS).await; + Self::sleep(QUERY_INTERVAL_SECS); } if begin.elapsed() >= query_timeout { OIDC_SESSION .write() - .await + .unwrap() .set_state(WAITING_ACCOUNT_AUTH, "timeout".to_owned()); } @@ -204,20 +219,20 @@ impl OidcSession { self.failed_msg = failed_msg; } - pub async fn account_auth(op: String, id: String, uuid: String) { - if OIDC_SESSION.read().await.running { - OIDC_SESSION.write().await.keep_querying = false; - } + fn wait_stop_querying() { let wait_secs = 0.3; - sleep(wait_secs).await; - while OIDC_SESSION.read().await.running { - sleep(wait_secs).await; + while OIDC_SESSION.read().unwrap().running { + Self::sleep(wait_secs); } + } - tokio::spawn(async move { - OIDC_SESSION.write().await.before_task().await; - Self::auth_task(op, id, uuid).await; - OIDC_SESSION.write().await.after_task().await; + pub fn account_auth(op: String, id: String, uuid: String) { + Self::auth_cancel(); + Self::wait_stop_querying(); + OIDC_SESSION.write().unwrap().before_task(); + std::thread::spawn(|| { + Self::auth_task(op, id, uuid); + OIDC_SESSION.write().unwrap().after_task(); }); } @@ -230,7 +245,11 @@ impl OidcSession { } } - pub async fn get_result() -> AuthResult { - OIDC_SESSION.read().await.get_result_() + pub fn auth_cancel() { + OIDC_SESSION.write().unwrap().keep_querying = false; + } + + pub fn get_result() -> AuthResult { + OIDC_SESSION.read().unwrap().get_result_() } } diff --git a/src/main.rs b/src/main.rs index ac8fd5219..9c7170309 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,8 +32,8 @@ fn main() { if !common::global_init() { return; } - use hbb_common::log; use clap::App; + use hbb_common::log; let args = format!( "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' -k, --key=[KEY] '' @@ -45,7 +45,7 @@ fn main() { .about("RustDesk command line tool") .args_from_usage(&args) .get_matches(); - use hbb_common::{env_logger::*, config::LocalConfig}; + use hbb_common::{config::LocalConfig, env_logger::*}; init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); if let Some(p) = matches.value_of("port-forward") { let options: Vec = p.split(":").map(|x| x.to_owned()).collect(); @@ -73,7 +73,14 @@ fn main() { } let key = matches.value_of("key").unwrap_or("").to_owned(); let token = LocalConfig::get_option("access_token"); - cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port, key, token); + cli::start_one_port_forward( + options[0].clone(), + port, + remote_host, + remote_port, + key, + token, + ); } common::global_clean(); -} \ No newline at end of file +} diff --git a/src/server.rs b/src/server.rs index 58aab8fd1..04814db42 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,4 +1,5 @@ use crate::ipc::Data; +use bytes::Bytes; pub use connection::*; use hbb_common::{ allow_err, @@ -20,7 +21,6 @@ use std::{ sync::{Arc, Mutex, RwLock, Weak}, time::Duration, }; -use bytes::Bytes; pub mod audio_service; cfg_if::cfg_if! { @@ -140,7 +140,8 @@ pub async fn create_tcp_connection( .write_to_bytes() .unwrap_or_default(), &sk, - ).into(), + ) + .into(), ..Default::default() }); timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??; @@ -310,9 +311,9 @@ pub fn check_zombie() { } /// Start the host server that allows the remote peer to control the current machine. -/// +/// /// # Arguments -/// +/// /// * `is_server` - Whether the current client is definitely the server. /// If true, the server will be started. /// Otherwise, client will check if there's already a server and start one if not. @@ -323,9 +324,9 @@ pub async fn start_server(is_server: bool) { } /// Start the host server that allows the remote peer to control the current machine. -/// +/// /// # Arguments -/// +/// /// * `is_server` - Whether the current client is definitely the server. /// If true, the server will be started. /// Otherwise, client will check if there's already a server and start one if not. diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 78d9433b1..04ba90cb2 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -842,14 +842,16 @@ pub(crate) fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender String { - serde_json::to_string(&account::OidcSession::get_result().await).unwrap_or_default() +pub fn account_auth_cancel() { + account::OidcSession::auth_cancel(); +} + +pub fn account_auth_result() -> String { + serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() } // notice: avoiding create ipc connecton repeatly, From dbd3df370a95a908ba09e718b41e774d94ce3238 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 7 Nov 2022 17:43:22 +0800 Subject: [PATCH 4/4] feat_account: update ui Signed-off-by: fufesou --- .../lib/desktop/pages/desktop_home_page.dart | 135 ------------------ flutter/lib/desktop/widgets/login.dart | 120 ++++++++-------- src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 2 + src/lang/de.rs | 2 + src/lang/en.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 2 + src/lang/fr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/kz.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/template.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/ua.rs | 2 + src/lang/vn.rs | 2 + 26 files changed, 112 insertions(+), 191 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index ef38bf443..e7d6f50e8 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -470,141 +470,6 @@ class _DesktopHomePageState extends State } } -/// common login dialog for desktop -/// call this directly -Future loginDialog2() async { - String userName = ""; - var userNameMsg = ""; - String pass = ""; - var passMsg = ""; - var userController = TextEditingController(text: userName); - var pwdController = TextEditingController(text: pass); - - var isInProgress = false; - var completer = Completer(); - gFFI.dialogManager.show((setState, close) { - submit() async { - setState(() { - userNameMsg = ""; - passMsg = ""; - isInProgress = true; - }); - cancel() { - setState(() { - isInProgress = false; - }); - } - - userName = userController.text; - pass = pwdController.text; - if (userName.isEmpty) { - userNameMsg = translate("Username missed"); - cancel(); - return; - } - if (pass.isEmpty) { - passMsg = translate("Password missed"); - cancel(); - return; - } - try { - final resp = await gFFI.userModel.login(userName, pass); - if (resp.containsKey('error')) { - passMsg = resp['error']; - cancel(); - return; - } - // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, - // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} - debugPrint("$resp"); - completer.complete(true); - } catch (err) { - debugPrint(err.toString()); - cancel(); - return; - } - close(); - } - - cancel() { - completer.complete(false); - close(); - } - - return CustomAlertDialog( - title: Text(translate("Login")), - content: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - "${translate('Username')}:", - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: userNameMsg.isNotEmpty ? userNameMsg : null), - controller: userController, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], - ), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text("${translate('Password')}:") - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: passMsg.isNotEmpty ? passMsg : null), - controller: pwdController, - ), - ), - ], - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], - ), - ), - actions: [ - TextButton(onPressed: cancel, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: cancel, - ); - }); - return completer.future; -} - void setPasswordDialog() async { final pw = await bind.mainGetPermanentPassword(); final p0 = TextEditingController(text: pw); diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart index 5f849d822..3e58a6de2 100644 --- a/flutter/lib/desktop/widgets/login.dart +++ b/flutter/lib/desktop/widgets/login.dart @@ -9,6 +9,8 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; +final kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0); + class _IconOP extends StatelessWidget { final String icon; final double iconWidth; @@ -51,7 +53,7 @@ class ButtonOP extends StatelessWidget { Expanded( child: Container( height: height, - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + padding: kMidButtonPadding, child: Obx(() => ElevatedButton( style: ElevatedButton.styleFrom( primary: curOP.value.isEmpty || curOP.value == op @@ -61,8 +63,7 @@ class ButtonOP extends StatelessWidget { onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null, child: Stack(children: [ - // to-do: translate - Center(child: Text('Continue with $op')), + Center(child: Text('${translate("Continue with")} $op')), Align( alignment: Alignment.centerLeft, child: SizedBox( @@ -178,7 +179,7 @@ class _WidgetOPState extends State { curOP: widget.curOP, iconWidth: widget.config.iconWidth, primaryColor: str2color(widget.config.op, 0x7f), - height: 40, + height: 36, onTap: () async { _resetState(); widget.curOP.value = widget.config.op; @@ -265,7 +266,10 @@ class LoginWidgetOP extends StatelessWidget { curOP: curOP, cbLogin: cbLogin, ), - const Divider() + const Divider( + indent: 5, + endIndent: 5, + ) ]) .expand((i) => i) .toList(); @@ -310,50 +314,56 @@ class LoginWidgetUserPass extends StatelessWidget { const SizedBox( height: 8.0, ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - '${translate("Username")}:', - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: usernameMsg.isNotEmpty ? usernameMsg : null), - controller: userController, - focusNode: FocusNode()..requestFocus(), + Container( + padding: kMidButtonPadding, + child: Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: Text( + '${translate("Username")}:', + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + const SizedBox( + width: 24.0, ), - ), - ], + Expanded( + child: TextField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: usernameMsg.isNotEmpty ? usernameMsg : null), + controller: userController, + focusNode: FocusNode()..requestFocus(), + ), + ), + ], + ), ), const SizedBox( height: 8.0, ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: - Text('${translate("Password")}:').marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: passMsg.isNotEmpty ? passMsg : null), - controller: pwdController, + Container( + padding: kMidButtonPadding, + child: Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: Text('${translate("Password")}:') + .marginOnly(bottom: 16.0)), + const SizedBox( + width: 24.0, ), - ), - ], + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: passMsg.isNotEmpty ? passMsg : null), + controller: pwdController, + ), + ), + ], + ), ), const SizedBox( height: 4.0, @@ -366,17 +376,17 @@ class LoginWidgetUserPass extends StatelessWidget { Row(children: [ Expanded( child: Container( - height: 50, - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 38, + padding: kMidButtonPadding, child: Obx(() => ElevatedButton( style: curOP.value.isEmpty || curOP.value == 'rustdesk' ? null : ElevatedButton.styleFrom( primary: Colors.grey, ), - child: const Text( - 'Login', - style: TextStyle(fontSize: 18), + child: Text( + translate('Login'), + style: TextStyle(fontSize: 16), ), onPressed: curOP.value.isEmpty || curOP.value == 'rustdesk' ? () { @@ -479,9 +489,9 @@ Future loginDialog() async { const SizedBox( height: 8.0, ), - const Center( + Center( child: Text( - 'or', + translate('or'), style: TextStyle(fontSize: 16), )), const SizedBox( @@ -489,9 +499,9 @@ Future loginDialog() async { ), LoginWidgetOP( ops: [ - ConfigOP(op: 'Github', iconWidth: 24), - ConfigOP(op: 'Google', iconWidth: 24), - ConfigOP(op: 'Okta', iconWidth: 46), + ConfigOP(op: 'Github', iconWidth: 20), + ConfigOP(op: 'Google', iconWidth: 20), + ConfigOP(op: 'Okta', iconWidth: 38), ], curOP: curOP, cbLogin: (String username) { @@ -503,9 +513,7 @@ Future loginDialog() async { ], ), ), - actions: [ - TextButton(onPressed: cancel, child: Text(translate('Cancel'))), - ], + actions: [msgBoxButton(translate('Close'), cancel)], onCancel: cancel, ); }); diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 3ee4735e3..7240f2a91 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"), ("Show RustDesk", "显示rustdesk"), ("This PC", "此电脑"), + ("or", "或"), + ("Continue with", "使用"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e0998a7bb..b51cb69e9 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protějšku)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 9aa4f00b9..c4d633b9b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på peer-siden)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 4ce6cbd56..9eb90ebcd 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den Bildschirm aus, der freigegeben werden soll (auf der Peer-Seite arbeiten)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 72aa45853..3415fa463 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -35,5 +35,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_foreground_window_warning", "Temporarily unable to use the mouse and keyboard, because the current window of the remote desktop requires higher privilege to operate, you can request the remote user to minimize the current window. To avoid this problem, it is recommended to install the software on the remote device or run it with administrator privileges."), ("JumpLink", "View"), ("Stop service", "Stop Service"), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 8fb58cf83..e7a35d937 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Bonvolu Elekti la ekranon por esti dividita (Funkciu ĉe la sama flanko)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index fa6aba297..4064d0fd7 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Seleccione la pantalla que se compartirá (Operar en el lado del par)."), ("Show RustDesk", "Mostrar RustDesk"), ("This PC", "Este PC"), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index f1119e166..a64fd6028 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l'écran à partager (opérer du côté pair)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 262574c43..c449c393d 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Kérjük, válassza ki a megosztani kívánt képernyőt (a társoldalon működjön)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 2d2ab9b1a..6f328f127 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Silakan Pilih layar yang akan dibagikan (Operasi di sisi rekan)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 6965e6610..75e7859ed 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Seleziona lo schermo da condividere (opera sul lato peer)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 12dc9ebaf..0e6931379 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "共有する画面を選択してください(ピア側で操作)。"), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 406de6cef..601db354d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하십시오(피어 측에서 작동)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index df1237bfb..359e14f55 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Бөлісетін экранды таңдаңыз (бірдей жағынан жұмыс жасаңыз)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 201a60811..382b254f0 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po stronie równorzędnej)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index a9189fc14..b99cb9db0 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Por favor, selecione a tela a ser compartilhada (operar no lado do peer)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index d6a7ccec0..600506979 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", ""), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index dfe1f7e8f..4eae0d153 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Пожалуйста, выберите экран для совместного использования (работайте на одноранговой стороне)."), ("Show RustDesk", "Показать RustDesk"), ("This PC", "Этот компьютер"), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 38196c011..4dfd8b02e 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte obrazovku, ktorú chcete zdieľať (Ovládajte na strane partnera)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index d5130d66b..7c1f18df3 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", ""), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index fe9b5d2fc..f856182f3 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Lütfen paylaşılacak ekranı seçiniz (Ekran tarafında çalıştırın)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0f986ffa1..6a196feb7 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的畫面(在對端操作)。"), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 1c34d0825..95d19b26b 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Будь ласка, виберіть екран, до якого потрібно надати доступ (працюйте на стороні однорангового пристрою)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 255b60def..c95498ca8 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Vui lòng Chọn màn hình để chia sẻ (Hoạt động ở phía ngang hàng)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); }