From 0883ae94b8ba3a78abf9abf36dbe3ca4553f422c Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 24 Sep 2023 13:03:24 +0800 Subject: [PATCH] test: sign in url test (#75) * test: sign in url test * fix: Tungstenite allows remote attackers to cause a denial of service * chore: enable feature * chore: update --- Cargo.lock | 9 +++--- Cargo.toml | 2 +- libs/client-api/Cargo.toml | 10 +++++-- libs/client-api/src/http.rs | 4 +-- libs/client-api/src/http_test.rs | 51 ++++++++++++++++++++++++++++++++ libs/client-api/src/lib.rs | 4 +++ src/api/collaborate.rs | 1 + src/api/user.rs | 4 +++ tests/client/login.rs | 34 --------------------- tests/client/password.rs | 40 ------------------------- tests/client/register.rs | 41 ------------------------- tests/client/sign_in.rs | 15 ++++++++-- tests/gotrue/admin.rs | 15 +--------- 13 files changed, 90 insertions(+), 140 deletions(-) create mode 100644 libs/client-api/src/http_test.rs delete mode 100644 tests/client/login.rs delete mode 100644 tests/client/password.rs delete mode 100644 tests/client/register.rs diff --git a/Cargo.lock b/Cargo.lock index 15d50aa2..16f3c616 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -839,6 +839,7 @@ dependencies = [ "opener", "parking_lot", "reqwest", + "scraper", "serde", "serde_json", "serde_repr", @@ -4038,9 +4039,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", @@ -4182,9 +4183,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ "byteorder", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 0e4c98a5..242d4b9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,11 +80,11 @@ once_cell = "1.7.2" collab-plugins = { version = "0.1.0", features = ["sync_plugin"] } collab-define = { version = "0.1.0" } collab-sync-protocol = { version = "0.1.0" } -client-api = { path = "libs/client-api" } tempfile = "3.4.0" assert-json-diff = "2.0.2" dotenv = "0.15.0" scraper = "0.17.1" +client-api = { path = "libs/client-api", features = ["client-api-test"]} [[bin]] name = "appflowy_cloud" diff --git a/libs/client-api/Cargo.toml b/libs/client-api/Cargo.toml index 12660006..a8fe96ec 100644 --- a/libs/client-api/Cargo.toml +++ b/libs/client-api/Cargo.toml @@ -22,11 +22,17 @@ parking_lot = "0.12.1" tracing = { version = "0.1" } thiserror = "1.0.39" serde = { version = "1.0", features = ["derive"] } -tokio-tungstenite = { version = "0.20" } +tokio-tungstenite = { version = "0.20.1" } tokio = { version = "1.26", features = ["full"] } futures-util = "0.3.26" tokio-retry = "0.3" bytes = "1.0" uuid = "1.4.1" - +scraper = { version = "0.17.1", optional = true } collab-sync-protocol = { version = "0.1.0" } + +[dev-dependencies] +scraper = "0.17.1" + +[features] +client-api-test = ["scraper"] \ No newline at end of file diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index a7f028c5..44632f30 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -27,8 +27,8 @@ use storage_entity::{AFWorkspaces, QueryCollabParams}; use storage_entity::{DeleteCollabParams, RawData}; pub struct Client { - cloud_client: reqwest::Client, - gotrue_client: gotrue::api::Client, + pub(crate) cloud_client: reqwest::Client, + pub(crate) gotrue_client: gotrue::api::Client, base_url: String, ws_addr: String, token: Arc>, diff --git a/libs/client-api/src/http_test.rs b/libs/client-api/src/http_test.rs new file mode 100644 index 00000000..212545fc --- /dev/null +++ b/libs/client-api/src/http_test.rs @@ -0,0 +1,51 @@ +use crate::Client; +use gotrue::grant::{Grant, PasswordGrant}; +use gotrue::params::GenerateLinkParams; +use gotrue_entity::GoTrueError; +use scraper::{Html, Selector}; + +impl Client { + pub async fn generate_sign_in_url( + &self, + admin_user_email: &str, + admin_user_password: &str, + user_email: &str, + ) -> Result { + let admin_token = self + .gotrue_client + .token(&Grant::Password(PasswordGrant { + email: admin_user_email.to_string(), + password: admin_user_password.to_string(), + })) + .await?; + + let admin_user_params: GenerateLinkParams = GenerateLinkParams { + email: user_email.to_string(), + ..Default::default() + }; + + let link_resp = self + .gotrue_client + .generate_link(&admin_token.access_token, &admin_user_params) + .await?; + assert_eq!(link_resp.email, user_email); + + let action_link = link_resp.action_link; + let resp = reqwest::Client::new().get(action_link).send().await?; + let resp_text = resp.text().await?; + Ok(extract_appflowy_sign_in_url(&resp_text)) + } +} + +pub fn extract_appflowy_sign_in_url(html_str: &str) -> String { + let fragment = Html::parse_fragment(html_str); + let selector = Selector::parse("a").unwrap(); + fragment + .select(&selector) + .next() + .unwrap() + .value() + .attr("href") + .unwrap() + .to_string() +} diff --git a/libs/client-api/src/lib.rs b/libs/client-api/src/lib.rs index f4d771d7..9eb6fb78 100644 --- a/libs/client-api/src/lib.rs +++ b/libs/client-api/src/lib.rs @@ -1,4 +1,8 @@ mod http; + +#[cfg(feature = "client-api-test")] +pub mod http_test; + pub mod notify; pub mod ws; diff --git a/src/api/collaborate.rs b/src/api/collaborate.rs index 9848db6e..09162ca5 100644 --- a/src/api/collaborate.rs +++ b/src/api/collaborate.rs @@ -111,6 +111,7 @@ async fn retrieve_snapshot_data_handler( Ok(Json(AppResponse::Ok().with_data(data))) } +#[tracing::instrument(level = "debug", skip_all)] async fn retrieve_snapshots_handler( payload: Json, storage: Data>, diff --git a/src/api/user.rs b/src/api/user.rs index 13018786..acf48c77 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -41,6 +41,7 @@ async fn verify_handler( Ok(AppResponse::Ok().with_data(resp).into()) } +#[tracing::instrument(level = "debug", skip(state))] async fn profile_handler( uuid: UserUuid, state: Data, @@ -59,6 +60,7 @@ async fn update_handler( Ok(AppResponse::Ok().into()) } +#[tracing::instrument(level = "debug", skip_all)] async fn login_handler( req: Json, state: Data, @@ -83,6 +85,7 @@ async fn login_handler( Ok(HttpResponse::Ok().json(resp)) } +#[tracing::instrument(level = "debug", skip(state))] async fn logout_handler(req: HttpRequest, state: Data) -> Result { let logged_user = logged_user_from_request(&req, &state.config.application.server_key)?; logout(logged_user, state.user.clone()).await; @@ -109,6 +112,7 @@ async fn register_handler( Ok(HttpResponse::Ok().json(resp)) } +#[tracing::instrument(level = "debug", skip_all)] async fn change_password_handler( req: HttpRequest, payload: Json, diff --git a/tests/client/login.rs b/tests/client/login.rs deleted file mode 100644 index 8de6c205..00000000 --- a/tests/client/login.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::client::utils::{register_deep_fake, LOCALHOST_URL}; -use appflowy_cloud::client::http; - -#[tokio::test] -async fn login_success() { - let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL); - let (email, _user, password) = register_deep_fake(&mut c).await; - let initial_token = c.logged_in_token().unwrap().to_string(); - c.login(&email, &password).await.unwrap(); - let relogin_token = c.logged_in_token().unwrap(); - assert_ne!(&initial_token, relogin_token); - assert!(c.logged_in_token().is_some()) -} - -#[tokio::test] -async fn login_with_empty_email() { - let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL); - let (_email, _user, password) = register_deep_fake(&mut c).await; - assert!(c.login("", &password).await.is_err()); -} - -#[tokio::test] -async fn login_with_empty_password() { - let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL); - let (email, _user, _password) = register_deep_fake(&mut c).await; - assert!(c.login(&email, "").await.is_err()); -} - -#[tokio::test] -async fn login_with_unknown_user() { - let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL); - let token = c.login("unknown@appflowy.io", "Abc@123!").await; - assert!(token.is_err()); -} diff --git a/tests/client/password.rs b/tests/client/password.rs deleted file mode 100644 index 07ace559..00000000 --- a/tests/client/password.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::client::utils::{register_deep_fake, LOCALHOST_URL}; -use appflowy_cloud::client::http; - -#[tokio::test] -async fn change_password_with_unmatch_password() { - let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL); - let new_password = "HelloWord@1a"; - let new_password_confirm = "HeloWord@1a"; - let (_email, _user, password) = register_deep_fake(&mut c).await; - let res = c - .change_password(&password, new_password, new_password_confirm) - .await; - assert!(res.is_err()) -} - -#[tokio::test] -async fn login_failed_after_change_password() { - let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL); - let new_password = "HelloWord@1a"; - let (email, _user, old_password) = register_deep_fake(&mut c).await; - let res = c - .change_password(&old_password, new_password, new_password) - .await; - assert!(res.is_ok()); - let res = c.login(&email, &old_password).await; - assert!(res.is_err()) -} - -#[tokio::test] -async fn login_success_after_change_password() { - let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL); - let new_password = "HelloWord@1a"; - let (email, _user, old_password) = register_deep_fake(&mut c).await; - let res = c - .change_password(&old_password, new_password, new_password) - .await; - assert!(res.is_ok()); - let res = c.login(&email, new_password).await; - assert!(res.is_ok()) -} diff --git a/tests/client/register.rs b/tests/client/register.rs deleted file mode 100644 index ba168608..00000000 --- a/tests/client/register.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::client::utils::timestamp_nano; -use crate::client::utils::LOCALHOST_URL; -use appflowy_cloud::client::http; - -#[tokio::test] -async fn register_success() { - let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL); - let email = format!("deep_fake{}@appflowy.io", timestamp_nano()); - c.register("user1", &email, "DeepFakePassword!123") - .await - .unwrap(); - assert!(c.logged_in_token().is_some()) -} - -#[tokio::test] -async fn register_with_invalid_password() { - let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL); - let email = format!("deep_fake{}@appflowy.io", timestamp_nano()); - let res = c.register("user1", &email, "123").await; - assert!(res.is_err()); - assert!(c.logged_in_token().is_none()) -} - -#[tokio::test] -async fn register_with_invalid_name() { - let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL); - let email = format!("deep_fake{}@appflowy.io", timestamp_nano()); - let res = c.register("", &email, "DeepFakePassword!123").await; - assert!(res.is_err()); - assert!(c.logged_in_token().is_none()) -} - -#[tokio::test] -async fn register_with_invalid_email() { - let c = http::Client::from(reqwest::Client::new(), LOCALHOST_URL); - let res = c - .register("user1", "appflowy.io", "DeepFakePassword!123") - .await; - assert!(res.is_err()); - assert!(c.logged_in_token().is_none()) -} diff --git a/tests/client/sign_in.rs b/tests/client/sign_in.rs index 8d8e6f66..07617763 100644 --- a/tests/client/sign_in.rs +++ b/tests/client/sign_in.rs @@ -1,6 +1,6 @@ use shared_entity::error_code::ErrorCode; -use crate::client::utils::{generate_unique_email, generate_unique_registered_user}; +use crate::client::utils::{generate_unique_email, generate_unique_registered_user, ADMIN_USER}; use crate::client_api_client; #[tokio::test] @@ -87,7 +87,7 @@ async fn sign_in_success() { } #[tokio::test] -async fn sign_in_with_url() { +async fn sign_in_with_invalid_url() { let url_str = "appflowy-flutter://#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQ1ODIyMjMsInN1YiI6Ijk5MGM2NDNjLTMyMWEtNGNmMi04OWY1LTNhNmJhZGFjMTg5NCIsImVtYWlsIjoiNG5uaWhpbGF0ZWRAZ21haWwuY29tIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJnb29nbGUiLCJwcm92aWRlcnMiOlsiZ29vZ2xlIl19LCJ1c2VyX21ldGFkYXRhIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJdGZpa28xX0lpMmZiNzM4VnpGekViLVBqT0NCY3FUQzdrNjVIX0hnRTQwOVk9czk2LWMiLCJlbWFpbCI6IjRubmloaWxhdGVkQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmdWxsX25hbWUiOiJmdSB6aXhpYW5nIiwiaXNzIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vdXNlcmluZm8vdjIvbWUiLCJuYW1lIjoiZnUgeml4aWFuZyIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJdGZpa28xX0lpMmZiNzM4VnpGekViLVBqT0NCY3FUQzdrNjVIX0hnRTQwOVk9czk2LWMiLCJwcm92aWRlcl9pZCI6IjEwMTQ5OTYxMDMxOTYxNjE0NTcyNSIsInN1YiI6IjEwMTQ5OTYxMDMxOTYxNjE0NTcyNSJ9LCJyb2xlIjoiIn0.I-7j-Tdj62P56zhzEqvBc7cHMldv5MA_MM7xtrBibbE&expires_in=3600&provider_token=ya29.a0AfB_byCovXs1CUiC9_f9VBTupQPsIxwh9aSlOg0PLYJvv1x1zvVfssrQfW6_Aq9no7EKpCzFUCLElOvK1Xz4x4K5r7tug79tr5b1yiOoUMWTeWTXyV61fZHQbZ9vscAiyKYtq5NqYTiytHcQEFlKr7UMfu6BTbKsUwaCgYKAaISARISFQGOcNnC0Vsx2QCAXgYO3XbfcF91WQ0169&refresh_token=Hi3Jc3I_pj9YrexcR91i5g&token_type=bearer"; let c = client_api_client(); match c.sign_in_url(url_str).await { @@ -100,3 +100,14 @@ async fn sign_in_with_url() { }, } } + +#[tokio::test] +async fn sign_in_with_url() { + let c = client_api_client(); + let user_email = generate_unique_email(); + let url_str = c + .generate_sign_in_url(&ADMIN_USER.email, &ADMIN_USER.password, &user_email) + .await + .unwrap(); + let _ = c.sign_in_url(&url_str).await.unwrap(); +} diff --git a/tests/gotrue/admin.rs b/tests/gotrue/admin.rs index 83a3ac27..f8cc3783 100644 --- a/tests/gotrue/admin.rs +++ b/tests/gotrue/admin.rs @@ -1,9 +1,9 @@ +use client_api::http_test::extract_appflowy_sign_in_url; use gotrue::{ api::Client, grant::{Grant, PasswordGrant}, params::{AdminUserParams, GenerateLinkParams}, }; -use scraper::{Html, Selector}; use crate::{ client::{ @@ -95,16 +95,3 @@ async fn admin_generate_link_and_user_sign_in() { let workspaces = client.workspaces().await.unwrap(); assert_eq!(workspaces.len(), 1); } - -fn extract_appflowy_sign_in_url(html_str: &str) -> String { - let fragment = Html::parse_fragment(html_str); - let selector = Selector::parse("a").unwrap(); - fragment - .select(&selector) - .next() - .unwrap() - .value() - .attr("href") - .unwrap() - .to_string() -}