Skip to content

Commit ca863c6

Browse files
authored
feat: support feilian v1 login (platform "feilian_v1") (#89)
* feat: support feilian v1 login (platform "feilian_v1") The newer feilian backend serves login at /api/v1/login with an AES-CBC-encrypted password instead of the old /api/login (see #78). Add an opt-in platform "feilian_v1" implementing it: - utils::feilian_v1_encrypt_password: AES-256-CBC password encoding (key/iv derived from fixed constants), matching the official client - POST requests now send Content-Type: application/json; the v1 login endpoint otherwise rejects the body with "参数错误" - after login, fetch the TOTP secret from /api/v2/p/otp so 2fa codes are generated locally, the same way the existing login flow does - on exit, log out the current terminal (/api/logout) to free the server-side terminal quota; only for feilian_v1, since SSO platforms would then need an interactive re-auth on the next run Refs #78 * fix(client): recover from expired session instead of failing to parse When the server-side session expires, the API returns a non-zero code (e.g. 101) with a `data` whose shape no longer matches the success type T — ListVPN, for example, returns an object where a VPN array is expected. Deserializing the response straight into Resp<T> failed at that point, so the run died with an opaque "invalid type: map, expected a sequence" error and the existing code==101 logout/relogin path never ran (it lives after the parse). Parse the envelope into Resp<Value> first to read `code`, and only coerce `data` into T when code == 0; otherwise carry the code/message through with data=None. This lets the existing logout handling reset the state to Init and trigger a (non-interactive, for feilian_v1) re-login and retry, so an expired session self-heals instead of requiring the user to manually flip `state` back to Init. * review: drop redundant comments, hoist crypto imports to file level Address @PinkD review on #89: - remove the Content-Type explanatory comment (keep the header) - shorten the logout comment to "only logout for feilian_v1" - move aes/cbc/sha1 imports in utils.rs to file level
1 parent 7901541 commit ca863c6

8 files changed

Lines changed: 268 additions & 7 deletions

File tree

Cargo.lock

Lines changed: 71 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ reqwest_cookie_store = "0.6"
2323
tinytemplate = "1.2"
2424
md5 = "0.7"
2525
sha2 = "0.10"
26+
sha1 = "0.10"
27+
aes = "0.8"
28+
cbc = { version = "0.1", features = ["alloc"] }
29+
hex = "0.4"
2630
base32 = "0.4"
2731
base64 = "0.21"
2832
rand = "0.8"

src/api.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,18 @@ const URL_GET_CORPLINK_LOGIN_METHOD: &str = "{{url}}/api/lookup?os={{os}}&os_ver
1616
const URL_REQUEST_CODE: &str = "{{url}}/api/login/code/send?os={{os}}&os_version={{version}}";
1717
const URL_VERIFY_CODE: &str = "{{url}}/api/login/code/verify?os={{os}}&os_version={{version}}";
1818
const URL_LOGIN_PASSWORD: &str = "{{url}}/api/login?os={{os}}&os_version={{version}}";
19+
const URL_LOGIN_PASSWORD_V1: &str =
20+
"{{url}}/api/v1/login?os={{os}}&os_version={{version}}&client_source=FeiLian";
1921
const URL_LIST_VPN: &str = "{{url}}/api/vpn/list?os={{os}}&os_version={{version}}";
2022

2123
const URL_PING_VPN_HOST: &str = "{{url}}/vpn/ping?os={{os}}&os_version={{version}}";
2224
const URL_FETCH_PEER_INFO: &str = "{{url}}/vpn/conn?os={{os}}&os_version={{version}}";
2325
const URL_OPERATE_VPN: &str = "{{url}}/vpn/report?os={{os}}&os_version={{version}}";
2426
const URL_OTP: &str = "{{url}}/api/v2/p/otp?os={{os}}&os_version={{version}}";
27+
// log out the current terminal so it frees the server-side session/terminal
28+
// quota. logout_all=false only signs out this device. responds with a 302.
29+
const URL_LOGOUT: &str =
30+
"{{url}}/api/logout?os={{os}}&os_version={{version}}&logout_all=false";
2531

2632
#[derive(Clone, Hash, Eq, PartialEq, Debug)]
2733
pub enum ApiName {
@@ -31,6 +37,7 @@ pub enum ApiName {
3137
CorplinkLoginMethod,
3238
RequestEmailCode,
3339
LoginPassword,
40+
LoginPasswordV1,
3441
LoginEmail,
3542
ListVPN,
3643

@@ -39,6 +46,7 @@ pub enum ApiName {
3946
KeepAliveVPN,
4047
DisconnectVPN,
4148
Otp,
49+
Logout,
4250
}
4351

4452
#[derive(Clone, Serialize)]
@@ -84,12 +92,17 @@ impl ApiUrl {
8492
api_template.insert(ApiName::RequestEmailCode, Template::new(URL_REQUEST_CODE));
8593
api_template.insert(ApiName::LoginEmail, Template::new(URL_VERIFY_CODE));
8694
api_template.insert(ApiName::LoginPassword, Template::new(URL_LOGIN_PASSWORD));
95+
api_template.insert(
96+
ApiName::LoginPasswordV1,
97+
Template::new(URL_LOGIN_PASSWORD_V1),
98+
);
8799
api_template.insert(ApiName::ListVPN, Template::new(URL_LIST_VPN));
88100
api_template.insert(ApiName::PingVPN, Template::new(URL_PING_VPN_HOST));
89101
api_template.insert(ApiName::ConnectVPN, Template::new(URL_FETCH_PEER_INFO));
90102
api_template.insert(ApiName::KeepAliveVPN, Template::new(URL_OPERATE_VPN));
91103
api_template.insert(ApiName::DisconnectVPN, Template::new(URL_OPERATE_VPN));
92104
api_template.insert(ApiName::Otp, Template::new(URL_OTP));
105+
api_template.insert(ApiName::Logout, Template::new(URL_LOGOUT));
93106

94107
Ok(ApiUrl {
95108
user_param: UserUrlParam {
@@ -120,8 +133,10 @@ impl ApiUrl {
120133
ApiName::RequestEmailCode => self.api_template[name].render(user_param),
121134
ApiName::LoginEmail => self.api_template[name].render(user_param),
122135
ApiName::LoginPassword => self.api_template[name].render(user_param),
136+
ApiName::LoginPasswordV1 => self.api_template[name].render(user_param),
123137
ApiName::ListVPN => self.api_template[name].render(user_param),
124138
ApiName::Otp => self.api_template[name].render(user_param),
139+
ApiName::Logout => self.api_template[name].render(user_param),
125140

126141
ApiName::PingVPN => self.api_template[name].render(vpn_param),
127142
ApiName::ConnectVPN => self.api_template[name].render(vpn_param),

0 commit comments

Comments
 (0)