1use base64::{Engine, prelude::BASE64_STANDARD};
2use reqwest::{Response, header::HeaderValue};
3use serde::Deserialize;
4
5use crate::{
6 ApiError, Error,
7 api::auth,
8 challenge::{
9 CHALLENGE_ID_HEADER, CHALLENGE_METADATA_HEADER, CHALLENGE_TYPE_HEADER, Challenge,
10 ChallengeMetadata, ChallengeType, ChefChallengeMetadata,
11 },
12 client::Client,
13};
14
15const TOKEN_HEADER: &str = "x-csrf-token";
16
17#[derive(Debug, Deserialize)]
18pub struct ErrorJson {
19 code: u8,
20 message: String,
21}
22
23#[derive(Debug, Deserialize)]
24pub struct ErrorsJson {
25 errors: Vec<ErrorJson>,
26}
27
28#[derive(Debug, Deserialize)]
29pub struct DataErrorJson {
30 #[serde(rename = "isValid")]
31 is_valid: bool,
32 data: Option<String>, #[serde(rename = "error")]
34 message: String,
35}
36
37impl Client {
38 fn set_token(&mut self, token: &str) {
39 self.requestor
40 .default_headers
41 .insert(TOKEN_HEADER, HeaderValue::from_str(token).unwrap());
42 }
43
44 pub async fn ensure_token(&mut self) -> Result<(), Error> {
47 let result = self
48 .requestor
49 .client
50 .post(format!("{}//", auth::URL))
51 .headers(self.requestor.default_headers.clone())
52 .send()
53 .await;
54
55 let result = self.validate_response(result).await;
56
57 if let Err(Error::ApiError(ApiError::TokenValidation)) = result {
58 return Ok(());
59 }
60
61 if result.is_err() {
62 return Err(result.err().unwrap());
63 }
64
65 Ok(())
66 }
67
68 pub(crate) async fn validate_response(
73 &mut self,
74 result: Result<Response, reqwest::Error>,
75 ) -> Result<Response, Error> {
76 self.remove_challenge();
78
79 match result {
80 Ok(response) => {
81 let code = response.status().as_u16();
82
83 let token = response.headers().get(TOKEN_HEADER);
84 if let Some(token) = token {
85 self.set_token(String::from_utf8_lossy(token.as_bytes()).as_ref());
87 }
88
89 if code == 200 {
91 return Ok(response);
92 }
93
94 let challenge = {
96 let challenge_id = response.headers().get(CHALLENGE_ID_HEADER);
97 let challenge_type = response.headers().get(CHALLENGE_TYPE_HEADER);
98 let challenge_metadata_b64 = response.headers().get(CHALLENGE_METADATA_HEADER);
99
100 if let (Some(id), Some(kind), Some(metadata_b64)) =
101 (challenge_id, challenge_type, challenge_metadata_b64)
102 {
103 let kind = ChallengeType::from(kind.to_str().unwrap());
104 match kind {
105 ChallengeType::Chef => {
106 let _metadata: ChefChallengeMetadata = serde_json::from_slice(
107 BASE64_STANDARD
108 .decode(metadata_b64.to_str().unwrap())
109 .unwrap()
110 .as_slice(),
111 )
112 .unwrap();
113
114 todo!("Unsupported challenge-type: \"chef\"");
115 }
116
117 ChallengeType::Captcha => {
118 todo!("Unsupported challenge-type: \"captcha\"")
119 }
120
121 _ => {
122 let metadata: ChallengeMetadata = serde_json::from_slice(
123 BASE64_STANDARD
124 .decode(metadata_b64.to_str().unwrap())
125 .unwrap()
126 .as_slice(),
127 )
128 .unwrap();
129
130 Some(Challenge {
131 id: id.to_str().unwrap().to_string(),
132 kind,
133 metadata,
134 })
135 }
136 }
137 } else {
138 None
139 }
140 };
141
142 let bytes = response.bytes().await.unwrap().to_owned();
143 let errors = if let Ok(errors) = serde_json::from_slice::<ErrorsJson>(&bytes) {
144 errors
145 } else if let Ok(error) = serde_json::from_slice::<ErrorJson>(&bytes) {
146 ErrorsJson {
147 errors: vec![error],
148 }
149 } else if let Ok(error) = serde_json::from_slice::<DataErrorJson>(&bytes) {
150 ErrorsJson {
151 errors: vec![ErrorJson {
152 code: 0,
153 message: error.message,
154 }],
155 }
156 } else {
157 ErrorsJson {
158 errors: vec![ErrorJson {
159 code: 0,
160 message: String::from_utf8_lossy(&bytes).to_string(),
161 }],
162 }
163 };
164
165 match code {
166 400 => {
167 let errors: Vec<ApiError> = errors
168 .errors
169 .iter()
170 .map(|x| match x.message.as_str() {
171 "The asset id is invalid." => ApiError::InvalidAssetId,
172 "Invalid challenge ID." => ApiError::InvalidChallengeId,
173 "User not found." => ApiError::UserNotFound,
174 "The user ID is invalid." => ApiError::InvalidUserId,
175 "The gender provided is invalid." => ApiError::InvalidGender,
176 "The two step verification challenge code is invalid." => {
177 ApiError::InvalidTwoStepVerificationCode
178 }
179
180 "Invalid display name." => ApiError::InvalidDisplayName,
181
182 "Request must contain a birthdate" => {
183 ApiError::RequestMissingArgument("Birthdate".to_string())
184 }
185
186 _ => ApiError::Unknown(code),
187 })
188 .collect();
189
190 if errors.len() == 1 {
191 Err(Error::ApiError(errors.first().unwrap().clone()))
192 } else {
193 Err(Error::ApiError(ApiError::Multiple(errors)))
194 }
195 }
196
197 401 => Err(Error::ApiError(ApiError::Unauthorized)),
198 403 => {
199 let errors: Vec<ApiError> = errors
200 .errors
201 .iter()
202 .map(|x| match x.message.as_str() {
203 "Token Validation Failed"
204 | "XSRF token invalid"
205 | "XSRF Token Validation Failed"
206 | "\"XSRF Token Validation Failed\"" => ApiError::TokenValidation,
207
208 "Incorrect username or password. Please try again." => {
209 ApiError::InvalidCredentials
210 }
211
212 "You must pass the robot test before logging in." => {
213 ApiError::CaptchaFailed
214 }
215
216 "Account has been locked. Please request a password reset." => {
217 ApiError::AccontLocked
218 }
219
220 "Unable to login. Please use Social Network sign on." => {
221 ApiError::SocialNetworkLoginRequired
222 }
223
224 "Account issue. Please contact Support." => {
225 ApiError::AccountIssue
226 }
227
228 "Unable to login with provided credentials. Default login is required." => {
229 ApiError::DefaultLoginRequired
230 }
231
232 "Received credentials are unverified." => {
233 ApiError::UnverifiedCredentials
234 }
235
236 "Existing login session found. Please log out first." => {
237 ApiError::ExistingLoginSession
238 }
239
240 "The account is unable to log in. Please log in to the LuoBu app." => {
241 ApiError::LuoBuAppLoginRequired
242 }
243
244 "Too many attempts. Please wait a bit." => {
245 ApiError::Ratelimited
246 }
247
248 "The account is unable to login. Please log in with the VNG app." => {
249 ApiError::VNGAppLoginRequired
250 }
251
252 "PIN is locked." => ApiError::PinIsLocked,
253 "Invalid birthdate change." => ApiError::InvalidBirthdate,
254
255 "Challenge is required to authorize the request" => {
256 ApiError::ChallengeRequired(challenge.clone().unwrap())
257 }
258
259 "Challenge failed to authorize request" => {
260 ApiError::ChallengeFailed
261 }
262
263 "You do not have permission to view the owners of this asset." => {
264 ApiError::PermissionError
265 }
266
267 "an internal error occurred" => ApiError::Internal,
268
269 _ => ApiError::Unknown(code),
271 })
272 .collect();
273
274 if errors.len() == 1 {
275 Err(Error::ApiError(errors.first().unwrap().clone()))
276 } else {
277 Err(Error::ApiError(ApiError::Multiple(errors)))
278 }
279 }
280
281 429 => Err(Error::ApiError(ApiError::Ratelimited)),
282 500 => Err(Error::ApiError(ApiError::Internal)),
283
284 _ => Err(Error::ApiError(ApiError::Unknown(code))),
285 }
286 }
287
288 Err(error) => Err(Error::ReqwestError(error)),
289 }
290 }
291}