Skip to content
This repository was archived by the owner on Jul 6, 2024. It is now read-only.

Commit 32ced45

Browse files
committed
feat: Detect Human Verification Request
Detect and provide the means to solve a human verification request. See captcha example on how to solve html captcha.
1 parent 2832de3 commit 32ced45

File tree

14 files changed

+547
-37
lines changed

14 files changed

+547
-37
lines changed

Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "proton-api-rs"
33
authors = ["Leander Beernaert <lbb-dev@pm.me>"]
4-
version = "0.8.1"
4+
version = "0.9.0"
55
edition = "2021"
66
license = "AGPL-3.0-only"
77
description = "Unofficial implemention of proton REST API in rust"
@@ -33,7 +33,6 @@ default = []
3333
http-ureq = ["dep:ureq"]
3434
http-reqwest = ["dep:reqwest"]
3535

36-
3736
[dependencies.reqwest]
3837
version = "0.11"
3938
default-features = false
@@ -43,6 +42,7 @@ optional = true
4342
[dev-dependencies]
4443
env_logger = "0.10"
4544
tokio = {version ="1", features = ["full"]}
45+
wry = {version = "0.28"}
4646

4747
[[example]]
4848
name = "user_id"
@@ -51,3 +51,7 @@ required-features = ["http-reqwest"]
5151
[[example]]
5252
name = "user_id_sync"
5353
required-features = ["http-ureq"]
54+
55+
[[example]]
56+
name = "captcha"
57+
required-features = ["http-ureq"]

examples/captcha.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use proton_api_rs::clientv2::ping;
2+
use proton_api_rs::domain::{HVCaptchaMessage, HumanVerificationLoginData, HumanVerificationType};
3+
use proton_api_rs::{captcha_get, http, LoginError, Session};
4+
use std::process::exit;
5+
6+
fn main() {
7+
env_logger::init();
8+
9+
let user_email = std::env::var("PAPI_USER_EMAIL").unwrap();
10+
let user_password = std::env::var("PAPI_USER_PASSWORD").unwrap();
11+
let app_version = std::env::var("PAPI_APP_VERSION").unwrap();
12+
13+
let client = http::ClientBuilder::new()
14+
.app_version(&app_version)
15+
.build::<http::ureq_client::UReqClient>()
16+
.unwrap();
17+
18+
ping(&client).unwrap();
19+
20+
let login_result = Session::login(&client, &user_email, &user_password, None, None);
21+
if let Err(LoginError::HumanVerificationRequired(hv)) = &login_result {
22+
let captcha_body = captcha_get(&client, &hv.token, true).unwrap();
23+
run_captcha(captcha_body, app_version, user_email, user_password);
24+
}
25+
26+
if let Err(e) = &login_result {
27+
eprintln!("Got login error:{e}");
28+
}
29+
30+
eprintln!("Human Verification request not triggered try again");
31+
return;
32+
}
33+
34+
fn run_captcha(html: String, app_version: String, user: String, password: String) -> ! {
35+
std::fs::write("/tmp/captcha.html", &html).unwrap();
36+
use wry::{
37+
application::{
38+
event::{Event, StartCause, WindowEvent},
39+
event_loop::{ControlFlow, EventLoop},
40+
window::WindowBuilder,
41+
},
42+
webview::WebViewBuilder,
43+
};
44+
45+
let event_loop = EventLoop::new();
46+
let window = WindowBuilder::new()
47+
.with_title("Proton API Captcha")
48+
.build(&event_loop)
49+
.unwrap();
50+
let _webview = WebViewBuilder::new(window)
51+
.unwrap()
52+
.with_html(html)
53+
//.with_url("http://127.0.0.1:8000/captcha.html")
54+
.unwrap()
55+
.with_devtools(true)
56+
.with_ipc_handler(move |_, req| match HVCaptchaMessage::new(&req) {
57+
Ok(m) => {
58+
println!("Got message {:?}", m);
59+
if let Some(token) = m.get_token() {
60+
let client = http::ClientBuilder::new()
61+
.app_version(&app_version)
62+
.build::<http::ureq_client::UReqClient>()
63+
.unwrap();
64+
65+
let login_result = Session::login(
66+
&client,
67+
&user,
68+
&password,
69+
Some(HumanVerificationLoginData {
70+
hv_type: HumanVerificationType::Captcha,
71+
token: token.to_string(),
72+
}),
73+
None,
74+
);
75+
76+
if let Err(e) = login_result {
77+
eprintln!("Captcha Err {e}");
78+
} else {
79+
println!("Log in success!!");
80+
exit(0);
81+
}
82+
}
83+
}
84+
Err(e) => {
85+
eprintln!("Failed to publish event{e}");
86+
}
87+
})
88+
.build()
89+
.unwrap();
90+
91+
_webview
92+
.evaluate_script(
93+
"postMessageToParent = function(message) { window.ipc.postMessage(JSON.stringify(message), '*')}",
94+
)
95+
.unwrap();
96+
97+
event_loop.run(move |event, _, control_flow| {
98+
*control_flow = ControlFlow::Wait;
99+
100+
match event {
101+
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
102+
Event::WindowEvent {
103+
event: WindowEvent::CloseRequested,
104+
..
105+
} => {
106+
println!("Close requested");
107+
*control_flow = ControlFlow::Exit
108+
}
109+
_ => (),
110+
}
111+
});
112+
}

examples/user_id.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ async fn main() {
1616

1717
ping_async(&client).await.unwrap();
1818

19-
let session = match Session::login_async(&client, &user_email, &user_password, None)
19+
let session = match Session::login_async(&client, &user_email, &user_password, None, None)
2020
.await
2121
.unwrap()
2222
{

examples/user_id_sync.rs

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use proton_api_rs::clientv2::{ping, SessionType};
2-
use proton_api_rs::{http, Session};
2+
use proton_api_rs::domain::CaptchaErrorDetail;
3+
use proton_api_rs::{captcha_get, http, LoginError, Session};
34
use std::io::{BufRead, Write};
45

56
fn main() {
@@ -16,7 +17,49 @@ fn main() {
1617

1718
ping(&client).unwrap();
1819

19-
let session = match Session::login(&client, &user_email, &user_password, None).unwrap() {
20+
let login_result = Session::login(&client, &user_email, &user_password, None);
21+
if let Err(LoginError::Request(http::Error::API(e))) = &login_result {
22+
if e.api_code != 9001 {
23+
panic!("{e}")
24+
}
25+
let captcha_desc =
26+
serde_json::from_value::<CaptchaErrorDetail>(e.details.clone().unwrap()).unwrap();
27+
28+
let captcha_body = captcha_get(&client, &captcha_desc.human_verification_token).unwrap();
29+
run_captcha(captcha_body);
30+
// TODO: Start webview with the downloaded body - use https://github.com/tauri-apps/wry
31+
// Click
32+
// Handle postMessageToParent which has token & token type
33+
// repeat submission with x-pm-human-verification-token and x-pm-human-verification-token-type
34+
// Use the event below to catch this
35+
// window.addEventListener(
36+
// "message",
37+
// (event) => {
38+
// -> event.Data
39+
// },
40+
// false
41+
// );
42+
// On Success
43+
// postMessageToParent({
44+
// "type": "pm_captcha",
45+
// "token": response
46+
// });
47+
//
48+
// on expired
49+
// postMessageToParent({
50+
// "type": "pm_captcha_expired",
51+
// "token": response
52+
// });
53+
//
54+
// on height:
55+
// postMessageToParent({
56+
// 'type': 'pm_height',
57+
// 'height': height
58+
// });
59+
return;
60+
}
61+
62+
let session = match login_result.unwrap() {
2063
SessionType::Authenticated(s) => s,
2164
SessionType::AwaitingTotp(mut t) => {
2265
let mut line_reader = std::io::BufReader::new(std::io::stdin());
@@ -65,3 +108,53 @@ fn main() {
65108

66109
session.logout(&client).unwrap();
67110
}
111+
112+
fn run_captcha(html: String) {
113+
std::fs::write("/tmp/captcha.html", html).unwrap();
114+
use wry::{
115+
application::{
116+
event::{Event, StartCause, WindowEvent},
117+
event_loop::{ControlFlow, EventLoop},
118+
window::WindowBuilder,
119+
},
120+
webview::WebViewBuilder,
121+
};
122+
123+
let event_loop = EventLoop::new();
124+
let window = WindowBuilder::new()
125+
.with_title("Proton API Captcha")
126+
.build(&event_loop)
127+
.unwrap();
128+
let _webview = WebViewBuilder::new(window)
129+
.unwrap()
130+
.with_url("http://127.0.0.1:8000/captcha.html")
131+
.unwrap()
132+
.with_devtools(true)
133+
.with_ipc_handler(|window, req| {
134+
println!("Window IPC: {req}");
135+
})
136+
.build()
137+
.unwrap();
138+
139+
_webview
140+
.evaluate_script(
141+
"postMessageToParent = function(message) { window.ipc.postMessage(JSON.stringify(message), '*')}",
142+
)
143+
.unwrap();
144+
145+
event_loop.run(move |event, _, control_flow| {
146+
*control_flow = ControlFlow::Wait;
147+
148+
match event {
149+
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
150+
Event::WindowEvent {
151+
event: WindowEvent::CloseRequested,
152+
..
153+
} => {
154+
println!("Close requested");
155+
*control_flow = ControlFlow::Exit
156+
}
157+
_ => (),
158+
}
159+
});
160+
}

src/clientv2/client.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::http;
22
use crate::http::Request;
3-
use crate::requests::Ping;
3+
use crate::requests::{CaptchaRequest, Ping};
44

55
pub fn ping<T: http::ClientSync>(client: &T) -> Result<(), http::Error> {
66
Ping.execute_sync::<T>(client, &http::DefaultRequestFactory {})
@@ -10,3 +10,21 @@ pub async fn ping_async<T: http::ClientAsync>(client: &T) -> Result<(), http::Er
1010
Ping.execute_async::<T>(client, &http::DefaultRequestFactory {})
1111
.await
1212
}
13+
14+
pub fn captcha_get<T: http::ClientSync>(
15+
client: &T,
16+
token: &str,
17+
force_web: bool,
18+
) -> Result<String, http::Error> {
19+
CaptchaRequest::new(token, force_web).execute_sync(client, &http::DefaultRequestFactory {})
20+
}
21+
22+
pub async fn captcha_get_async<T: http::ClientAsync>(
23+
client: &T,
24+
token: &str,
25+
force_web: bool,
26+
) -> Result<String, http::Error> {
27+
CaptchaRequest::new(token, force_web)
28+
.execute_async(client, &http::DefaultRequestFactory {})
29+
.await
30+
}

src/clientv2/request_repeater.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
//! Automatic request repeater based on the expectations Proton has for their clients.
22
3-
use crate::domain::UserUid;
3+
use crate::domain::{SecretString, UserUid};
44
use crate::http::{
55
ClientAsync, ClientSync, DefaultRequestFactory, Method, Request, RequestData, RequestFactory,
66
};
77
use crate::requests::{AuthRefreshRequest, UserAuth};
88
use crate::{http, SessionRefreshData};
9-
use secrecy::ExposeSecret;
9+
use secrecy::{ExposeSecret, Secret};
1010

11-
pub type OnAuthRefreshedCallback = Box<dyn Fn(&UserUid, &str)>;
11+
pub trait OnAuthRefreshed: Send + Sync {
12+
fn on_auth_refreshed(&self, user: &Secret<UserUid>, token: &SecretString);
13+
}
1214

1315
pub struct RequestRepeater {
1416
user_auth: parking_lot::RwLock<UserAuth>,
15-
on_auth_refreshed: Option<OnAuthRefreshedCallback>,
17+
on_auth_refreshed: Option<Box<dyn OnAuthRefreshed>>,
1618
}
1719

1820
impl std::fmt::Debug for RequestRepeater {
@@ -31,7 +33,7 @@ impl std::fmt::Debug for RequestRepeater {
3133
}
3234

3335
impl RequestRepeater {
34-
pub fn new(user_auth: UserAuth, on_auth_refreshed: Option<OnAuthRefreshedCallback>) -> Self {
36+
pub fn new(user_auth: UserAuth, on_auth_refreshed: Option<Box<dyn OnAuthRefreshed>>) -> Self {
3537
Self {
3638
user_auth: parking_lot::RwLock::new(user_auth),
3739
on_auth_refreshed,
@@ -50,10 +52,7 @@ impl RequestRepeater {
5052
let mut borrow = self.user_auth.write();
5153
*borrow = UserAuth::from_auth_refresh_response(&s);
5254
if let Some(cb) = &self.on_auth_refreshed {
53-
(cb)(
54-
borrow.uid.expose_secret(),
55-
borrow.access_token.expose_secret(),
56-
);
55+
cb.on_auth_refreshed(&borrow.uid, &borrow.access_token);
5756
}
5857
Ok(())
5958
}
@@ -75,10 +74,7 @@ impl RequestRepeater {
7574
let mut borrow = self.user_auth.write();
7675
*borrow = UserAuth::from_auth_refresh_response(&s);
7776
if let Some(cb) = &self.on_auth_refreshed {
78-
(cb)(
79-
borrow.uid.expose_secret(),
80-
borrow.access_token.expose_secret(),
81-
);
77+
cb.on_auth_refreshed(&borrow.uid, &borrow.access_token);
8278
}
8379
Ok(())
8480
}
@@ -105,6 +101,8 @@ impl RequestRepeater {
105101

106102
// Execute request again
107103
return request.execute_sync(client, self);
104+
} else if api_err.http_code == 422 && api_err.http_code == 9001 {
105+
//TODO: Handle captcha .....
108106
}
109107
}
110108
Err(original_error)

0 commit comments

Comments
 (0)