1use super::{config::Config, util};
12use crate::{Error, Result};
13use error_support::breadcrumb;
14use parking_lot::Mutex;
15use rc_crypto::{
16 digest,
17 hawk::{Credentials, Key, PayloadHasher, RequestBuilder, SHA256},
18 hkdf, hmac,
19};
20use serde_derive::{Deserialize, Serialize};
21use serde_json::json;
22use std::{
23 collections::HashMap,
24 sync::atomic::{AtomicBool, Ordering},
25 time::{Duration, Instant},
26};
27use sync15::DeviceType;
28use url::Url;
29use viaduct::{header_names, status_codes, Method, Request, Response};
30
31const HAWK_HKDF_SALT: [u8; 32] = [0b0; 32];
32const HAWK_KEY_LENGTH: usize = 32;
33const RETRY_AFTER_DEFAULT_SECONDS: u64 = 10;
34const DEVICES_FILTER_DAYS: u64 = 21;
36
37#[allow(clippy::needless_lifetimes)]
60#[cfg_attr(test, mockall::automock)]
61pub(crate) trait FxAClient {
62 fn create_refresh_token_using_authorization_code<'a>(
63 &self,
64 config: &Config,
65 session_token: Option<&'a str>,
66 code: &str,
67 code_verifier: &str,
68 ) -> Result<OAuthTokenResponse>;
69 fn create_refresh_token_using_session_token<'a>(
70 &self,
71 config: &Config,
72 session_token: &str,
73 scopes: &[&'a str],
74 ) -> Result<OAuthTokenResponse>;
75 fn check_refresh_token_status(
76 &self,
77 config: &Config,
78 refresh_token: &str,
79 ) -> Result<IntrospectResponse>;
80 fn create_access_token_using_refresh_token<'a>(
81 &self,
82 config: &Config,
83 refresh_token: &str,
84 ttl: Option<u64>,
85 scopes: &[&'a str],
86 ) -> Result<OAuthTokenResponse>;
87 fn create_access_token_using_session_token<'a>(
88 &self,
89 config: &Config,
90 session_token: &str,
91 scopes: &[&'a str],
92 ) -> Result<OAuthTokenResponse>;
93 fn create_authorization_code_using_session_token(
94 &self,
95 config: &Config,
96 session_token: &str,
97 auth_params: AuthorizationRequestParameters,
98 ) -> Result<OAuthAuthResponse>;
99 #[allow(dead_code)]
100 fn duplicate_session_token(
101 &self,
102 config: &Config,
103 session_token: &str,
104 ) -> Result<DuplicateTokenResponse>;
105 fn destroy_access_token(&self, config: &Config, token: &str) -> Result<()>;
106 fn destroy_refresh_token(&self, config: &Config, token: &str) -> Result<()>;
107 fn get_profile(
108 &self,
109 config: &Config,
110 profile_access_token: &str,
111 etag: Option<String>,
112 ) -> Result<Option<ResponseAndETag<ProfileResponse>>>;
113 fn get_pending_commands(
114 &self,
115 config: &Config,
116 refresh_token: &str,
117 index: u64,
118 limit: Option<u64>,
119 ) -> Result<PendingCommandsResponse>;
120 fn invoke_command(
121 &self,
122 config: &Config,
123 refresh_token: &str,
124 command: &str,
125 target: &str,
126 payload: &serde_json::Value,
127 ttl: Option<u64>,
128 ) -> Result<()>;
129 fn update_device_record<'a>(
130 &self,
131 config: &Config,
132 refresh_token: &str,
133 update: DeviceUpdateRequest<'a>,
134 ) -> Result<UpdateDeviceResponse>;
135 fn destroy_device_record(&self, config: &Config, refresh_token: &str, id: &str) -> Result<()>;
136 fn get_devices(&self, config: &Config, refresh_token: &str) -> Result<Vec<GetDeviceResponse>>;
137 fn get_attached_clients(
138 &self,
139 config: &Config,
140 session_token: &str,
141 ) -> Result<Vec<GetAttachedClientResponse>>;
142 fn get_scoped_key_data(
143 &self,
144 config: &Config,
145 session_token: &str,
146 client_id: &str,
147 scope: &str,
148 ) -> Result<HashMap<String, ScopedKeyDataResponse>>;
149 #[allow(dead_code)]
150 fn get_fxa_client_configuration(&self, config: &Config) -> Result<ClientConfigurationResponse>;
151 #[allow(dead_code)]
152 fn get_openid_configuration(&self, config: &Config) -> Result<OpenIdConfigurationResponse>;
153 fn simulate_network_error(&self) {}
154}
155
156enum HttpClientState {
157 Ok,
158 Backoff {
159 backoff_end_duration: Duration,
160 time_since_backoff: Instant,
161 },
162}
163
164pub struct Client {
165 state: Mutex<HashMap<String, HttpClientState>>,
166 simulate_network_error: AtomicBool,
167}
168impl FxAClient for Client {
169 fn get_fxa_client_configuration(&self, config: &Config) -> Result<ClientConfigurationResponse> {
170 fxa_client_configuration(config.client_config_url()?)
174 }
175 fn get_openid_configuration(&self, config: &Config) -> Result<OpenIdConfigurationResponse> {
176 openid_configuration(config.openid_config_url()?)
177 }
178
179 fn get_profile(
180 &self,
181 config: &Config,
182 access_token: &str,
183 etag: Option<String>,
184 ) -> Result<Option<ResponseAndETag<ProfileResponse>>> {
185 let url = config.userinfo_endpoint()?;
186 let mut request =
187 Request::get(url).header(header_names::AUTHORIZATION, bearer_token(access_token))?;
188 if let Some(etag) = etag {
189 request = request.header(header_names::IF_NONE_MATCH, format!("\"{}\"", etag))?;
190 }
191 let resp = self.make_request(request)?;
192 if resp.status == status_codes::NOT_MODIFIED {
193 return Ok(None);
194 }
195 let etag = resp
196 .headers
197 .get(header_names::ETAG)
198 .map(ToString::to_string);
199 Ok(Some(ResponseAndETag {
200 etag,
201 response: resp.json()?,
202 }))
203 }
204
205 fn create_refresh_token_using_authorization_code(
206 &self,
207 config: &Config,
208 session_token: Option<&str>,
209 code: &str,
210 code_verifier: &str,
211 ) -> Result<OAuthTokenResponse> {
212 let req_body = OAauthTokenRequest::UsingCode {
213 code: code.to_string(),
214 client_id: config.client_id.to_string(),
215 code_verifier: code_verifier.to_string(),
216 ttl: None,
217 };
218 self.make_oauth_token_request(
219 config,
220 session_token,
221 serde_json::to_value(req_body).unwrap(),
222 )
223 }
224
225 fn create_refresh_token_using_session_token(
226 &self,
227 config: &Config,
228 session_token: &str,
229 scopes: &[&str],
230 ) -> Result<OAuthTokenResponse> {
231 let url = config.token_endpoint()?;
232 let key = derive_auth_key_from_session_token(session_token)?;
233 let body = json!({
234 "client_id": config.client_id,
235 "scope": scopes.join(" "),
236 "grant_type": "fxa-credentials",
237 "access_type": "offline",
238 });
239 let request = HawkRequestBuilder::new(Method::Post, url, &key)
240 .body(body)
241 .build()?;
242 Ok(self.make_request(request)?.json()?)
243 }
244
245 fn create_access_token_using_refresh_token(
248 &self,
249 config: &Config,
250 refresh_token: &str,
251 ttl: Option<u64>,
252 scopes: &[&str],
253 ) -> Result<OAuthTokenResponse> {
254 let req = OAauthTokenRequest::UsingRefreshToken {
255 client_id: config.client_id.clone(),
256 refresh_token: refresh_token.to_string(),
257 scope: Some(scopes.join(" ")),
258 ttl,
259 };
260 self.make_oauth_token_request(config, None, serde_json::to_value(req).unwrap())
261 }
262
263 fn create_access_token_using_session_token(
264 &self,
265 config: &Config,
266 session_token: &str,
267 scopes: &[&str],
268 ) -> Result<OAuthTokenResponse> {
269 let parameters = json!({
270 "client_id": config.client_id,
271 "grant_type": "fxa-credentials",
272 "scope": scopes.join(" ")
273 });
274 let key = derive_auth_key_from_session_token(session_token)?;
275 let url = config.token_endpoint()?;
276 let request = HawkRequestBuilder::new(Method::Post, url, &key)
277 .body(parameters)
278 .build()?;
279 self.make_request(request)?.json().map_err(Into::into)
280 }
281
282 fn create_authorization_code_using_session_token(
283 &self,
284 config: &Config,
285 session_token: &str,
286 auth_params: AuthorizationRequestParameters,
287 ) -> Result<OAuthAuthResponse> {
288 let parameters = serde_json::to_value(auth_params)?;
289 let key = derive_auth_key_from_session_token(session_token)?;
290 let url = config.auth_url_path("v1/oauth/authorization")?;
291 let request = HawkRequestBuilder::new(Method::Post, url, &key)
292 .body(parameters)
293 .build()?;
294
295 Ok(self.make_request(request)?.json()?)
296 }
297
298 fn check_refresh_token_status(
299 &self,
300 config: &Config,
301 refresh_token: &str,
302 ) -> Result<IntrospectResponse> {
303 let body = json!({
304 "token_type_hint": "refresh_token",
305 "token": refresh_token,
306 });
307 let url = config.introspection_endpoint()?;
308 Ok(self.make_request(Request::post(url).json(&body))?.json()?)
309 }
310
311 fn duplicate_session_token(
312 &self,
313 config: &Config,
314 session_token: &str,
315 ) -> Result<DuplicateTokenResponse> {
316 let url = config.auth_url_path("v1/session/duplicate")?;
317 let key = derive_auth_key_from_session_token(session_token)?;
318 let duplicate_body = json!({
319 "reason": "migration"
320 });
321 let request = HawkRequestBuilder::new(Method::Post, url, &key)
322 .body(duplicate_body)
323 .build()?;
324
325 Ok(self.make_request(request)?.json()?)
326 }
327
328 fn destroy_access_token(&self, config: &Config, access_token: &str) -> Result<()> {
329 let body = json!({
330 "token": access_token,
331 });
332 self.destroy_token_helper(config, &body)
333 }
334
335 fn destroy_refresh_token(&self, config: &Config, refresh_token: &str) -> Result<()> {
336 let body = json!({
337 "refresh_token": refresh_token,
338 });
339 self.destroy_token_helper(config, &body)
340 }
341
342 fn get_pending_commands(
343 &self,
344 config: &Config,
345 refresh_token: &str,
346 index: u64,
347 limit: Option<u64>,
348 ) -> Result<PendingCommandsResponse> {
349 let url = config.auth_url_path("v1/account/device/commands")?;
350 let mut request = Request::get(url)
351 .header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
352 .query(&[("index", &index.to_string())]);
353 if let Some(limit) = limit {
354 request = request.query(&[("limit", &limit.to_string())])
355 }
356 Ok(self.make_request(request)?.json()?)
357 }
358
359 fn invoke_command(
360 &self,
361 config: &Config,
362 refresh_token: &str,
363 command: &str,
364 target: &str,
365 payload: &serde_json::Value,
366 ttl: Option<u64>,
367 ) -> Result<()> {
368 let body = serde_json::to_string(&InvokeCommandRequest {
369 command,
370 target,
371 payload,
372 ttl,
373 })?;
374 let url = config.auth_url_path("v1/account/devices/invoke_command")?;
375 let request = Request::post(url)
376 .header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
377 .header(header_names::CONTENT_TYPE, "application/json")?
378 .body(body);
379 self.make_request(request)?;
380 Ok(())
381 }
382
383 fn get_devices(&self, config: &Config, refresh_token: &str) -> Result<Vec<GetDeviceResponse>> {
384 let url = config.auth_url_path("v1/account/devices")?;
385 let timestamp = util::past_timestamp(DEVICES_FILTER_DAYS).to_string();
386 breadcrumb!(
387 "get_devices timestamp: {timestamp}, refresh len: {}",
388 refresh_token.len()
389 );
390 let request = Request::get(url)
391 .header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
392 .query(&[("filterIdleDevicesTimestamp", ×tamp)]);
393 Ok(self.make_request(request)?.json()?)
394 }
395
396 fn update_device_record(
397 &self,
398 config: &Config,
399 refresh_token: &str,
400 update: DeviceUpdateRequest<'_>,
401 ) -> Result<UpdateDeviceResponse> {
402 let url = config.auth_url_path("v1/account/device")?;
403 let request = Request::post(url)
404 .header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
405 .header(header_names::CONTENT_TYPE, "application/json")?
406 .body(serde_json::to_string(&update)?);
407 Ok(self.make_request(request)?.json()?)
408 }
409
410 fn destroy_device_record(&self, config: &Config, refresh_token: &str, id: &str) -> Result<()> {
411 let body = json!({
412 "id": id,
413 });
414 let url = config.auth_url_path("v1/account/device/destroy")?;
415 let request = Request::post(url)
416 .header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
417 .header(header_names::CONTENT_TYPE, "application/json")?
418 .body(body.to_string());
419
420 self.make_request(request)?;
421 Ok(())
422 }
423
424 fn get_attached_clients(
425 &self,
426 config: &Config,
427 session_token: &str,
428 ) -> Result<Vec<GetAttachedClientResponse>> {
429 let url = config.auth_url_path("v1/account/attached_clients")?;
430 let key = derive_auth_key_from_session_token(session_token)?;
431 let request = HawkRequestBuilder::new(Method::Get, url, &key).build()?;
432 Ok(self.make_request(request)?.json()?)
433 }
434
435 fn get_scoped_key_data(
436 &self,
437 config: &Config,
438 session_token: &str,
439 client_id: &str,
440 scope: &str,
441 ) -> Result<HashMap<String, ScopedKeyDataResponse>> {
442 let body = json!({
443 "client_id": client_id,
444 "scope": scope,
445 });
446 let url = config.auth_url_path("v1/account/scoped-key-data")?;
447 let key = derive_auth_key_from_session_token(session_token)?;
448 let request = HawkRequestBuilder::new(Method::Post, url, &key)
449 .body(body)
450 .build()?;
451 self.make_request(request)?.json().map_err(|e| e.into())
452 }
453
454 fn simulate_network_error(&self) {
455 self.simulate_network_error.store(true, Ordering::Relaxed);
456 }
457}
458
459macro_rules! fetch {
460 ($url:expr) => {
461 viaduct::Request::get($url)
462 .send()?
463 .require_success()?
464 .json()?
465 };
466}
467
468#[inline]
469pub(crate) fn fxa_client_configuration(url: Url) -> Result<ClientConfigurationResponse> {
470 Ok(fetch!(url))
471}
472#[inline]
473pub(crate) fn openid_configuration(url: Url) -> Result<OpenIdConfigurationResponse> {
474 Ok(fetch!(url))
475}
476
477impl Client {
478 pub fn new() -> Self {
479 Self {
480 state: Mutex::new(HashMap::new()),
481 simulate_network_error: AtomicBool::new(false),
482 }
483 }
484
485 fn destroy_token_helper(&self, config: &Config, body: &serde_json::Value) -> Result<()> {
486 let url = config.oauth_url_path("v1/destroy")?;
487 self.make_request(Request::post(url).json(body))?;
488 Ok(())
489 }
490
491 fn make_oauth_token_request(
492 &self,
493 config: &Config,
494 session_token: Option<&str>,
495 body: serde_json::Value,
496 ) -> Result<OAuthTokenResponse> {
497 let url = config.token_endpoint()?;
498 if let Some(session_token) = session_token {
499 let key = derive_auth_key_from_session_token(session_token)?;
500 let request = HawkRequestBuilder::new(Method::Post, url, &key)
501 .body(body)
502 .build()?;
503
504 Ok(self.make_request(request)?.json()?)
505 } else {
506 Ok(self.make_request(Request::post(url).json(&body))?.json()?)
507 }
508 }
509
510 fn handle_too_many_requests(&self, resp: Response) -> Result<Response> {
511 let path = resp.url.path().to_string();
512 if let Some(retry_after) = resp.headers.get_as::<u64, _>(header_names::RETRY_AFTER) {
513 let retry_after = retry_after.unwrap_or(RETRY_AFTER_DEFAULT_SECONDS);
514 let time_out_state = HttpClientState::Backoff {
515 backoff_end_duration: Duration::from_secs(retry_after),
516 time_since_backoff: Instant::now(),
517 };
518 self.state.lock().insert(path, time_out_state);
519 return Err(Error::BackoffError(retry_after));
520 }
521 Self::default_handle_response_error(resp)
522 }
523
524 fn default_handle_response_error(resp: Response) -> Result<Response> {
525 let json: std::result::Result<serde_json::Value, _> = resp.json();
526 match json {
527 Ok(json) => Err(Error::RemoteError {
528 code: json["code"].as_u64().unwrap_or(0),
529 errno: json["errno"].as_u64().unwrap_or(0),
530 error: json["error"].as_str().unwrap_or("").to_string(),
531 message: json["message"].as_str().unwrap_or("").to_string(),
532 info: json["info"].as_str().unwrap_or("").to_string(),
533 }),
534 Err(_) => Err(resp.require_success().unwrap_err().into()),
535 }
536 }
537
538 fn make_request(&self, request: Request) -> Result<Response> {
539 if self.simulate_network_error.swap(false, Ordering::Relaxed) {
540 return Err(Error::RequestError(viaduct::Error::NetworkError(
541 "Simulated error".to_owned(),
542 )));
543 }
544
545 let url = request.url.path().to_string();
546 if let HttpClientState::Backoff {
547 backoff_end_duration,
548 time_since_backoff,
549 } = self.state.lock().get(&url).unwrap_or(&HttpClientState::Ok)
550 {
551 let elapsed_time = time_since_backoff.elapsed();
552 if elapsed_time < *backoff_end_duration {
553 let remaining = *backoff_end_duration - elapsed_time;
554 return Err(Error::BackoffError(remaining.as_secs()));
555 }
556 }
557 self.state.lock().insert(url, HttpClientState::Ok);
558 let resp = request.send()?;
559 if resp.is_success() || resp.status == status_codes::NOT_MODIFIED {
560 Ok(resp)
561 } else {
562 match resp.status {
563 status_codes::TOO_MANY_REQUESTS => self.handle_too_many_requests(resp),
564 _ => Self::default_handle_response_error(resp),
565 }
566 }
567 }
568}
569
570fn bearer_token(token: &str) -> String {
571 format!("Bearer {}", token)
572}
573
574fn kw(name: &str) -> Vec<u8> {
575 format!("identity.mozilla.com/picl/v1/{}", name)
576 .as_bytes()
577 .to_vec()
578}
579
580pub fn derive_auth_key_from_session_token(session_token: &str) -> Result<Vec<u8>> {
581 let session_token_bytes = hex::decode(session_token)?;
582 let context_info = kw("sessionToken");
583 let salt = hmac::SigningKey::new(&digest::SHA256, &HAWK_HKDF_SALT);
584 let mut out = vec![0u8; HAWK_KEY_LENGTH * 2];
585 hkdf::extract_and_expand(&salt, &session_token_bytes, &context_info, &mut out)?;
586 Ok(out)
587}
588
589#[derive(Serialize, Deserialize)]
590pub struct AuthorizationRequestParameters {
591 pub client_id: String,
592 pub scope: String,
593 pub state: String,
594 pub access_type: String,
595 pub code_challenge: Option<String>,
596 pub code_challenge_method: Option<String>,
597 pub keys_jwe: Option<String>,
598}
599
600struct HawkRequestBuilder<'a> {
601 url: Url,
602 method: Method,
603 body: Option<String>,
604 hkdf_sha256_key: &'a [u8],
605}
606
607impl<'a> HawkRequestBuilder<'a> {
608 pub fn new(method: Method, url: Url, hkdf_sha256_key: &'a [u8]) -> Self {
609 rc_crypto::ensure_initialized();
610 HawkRequestBuilder {
611 url,
612 method,
613 body: None,
614 hkdf_sha256_key,
615 }
616 }
617
618 pub fn body(mut self, body: serde_json::Value) -> Self {
621 self.body = Some(body.to_string());
622 self
623 }
624
625 fn make_hawk_header(&self) -> Result<String> {
626 let hash;
628 let method = format!("{}", self.method);
629 let mut hawk_request_builder = RequestBuilder::from_url(method.as_str(), &self.url)?;
630 if let Some(ref body) = self.body {
631 hash = PayloadHasher::hash("application/json", SHA256, body)?;
632 hawk_request_builder = hawk_request_builder.hash(&hash[..]);
633 }
634 let hawk_request = hawk_request_builder.request();
635 let token_id = hex::encode(&self.hkdf_sha256_key[0..HAWK_KEY_LENGTH]);
636 let hmac_key = &self.hkdf_sha256_key[HAWK_KEY_LENGTH..(2 * HAWK_KEY_LENGTH)];
637 let hawk_credentials = Credentials {
638 id: token_id,
639 key: Key::new(hmac_key, SHA256)?,
640 };
641 let header = hawk_request.make_header(&hawk_credentials)?;
642 Ok(format!("Hawk {}", header))
643 }
644
645 pub fn build(self) -> Result<Request> {
646 let hawk_header = self.make_hawk_header()?;
647 let mut request =
648 Request::new(self.method, self.url).header(header_names::AUTHORIZATION, hawk_header)?;
649 if let Some(body) = self.body {
650 request = request
651 .header(header_names::CONTENT_TYPE, "application/json")?
652 .body(body);
653 }
654 Ok(request)
655 }
656}
657
658#[derive(Deserialize)]
659pub(crate) struct ClientConfigurationResponse {
660 pub(crate) auth_server_base_url: String,
661 pub(crate) oauth_server_base_url: String,
662 pub(crate) profile_server_base_url: String,
663 pub(crate) sync_tokenserver_base_url: String,
664}
665
666#[derive(Deserialize)]
667pub(crate) struct OpenIdConfigurationResponse {
668 pub(crate) authorization_endpoint: String,
669 pub(crate) introspection_endpoint: String,
670 pub(crate) issuer: String,
671 pub(crate) jwks_uri: String,
672 #[allow(dead_code)]
673 pub(crate) token_endpoint: String,
674 pub(crate) userinfo_endpoint: String,
675}
676
677#[derive(Clone)]
678pub struct ResponseAndETag<T> {
679 pub response: T,
680 pub etag: Option<String>,
681}
682
683#[derive(Deserialize)]
684pub struct PendingCommandsResponse {
685 pub index: u64,
686 #[allow(dead_code)]
687 pub last: Option<bool>,
688 pub messages: Vec<PendingCommand>,
689}
690
691#[derive(Deserialize)]
692pub struct PendingCommand {
693 pub index: u64,
694 pub data: CommandData,
695}
696
697#[derive(Debug, Deserialize)]
698pub struct CommandData {
699 pub command: String,
700 pub payload: serde_json::Value, pub sender: Option<String>,
702}
703
704#[derive(Clone, Debug, Deserialize, Serialize)]
705pub struct PushSubscription {
706 #[serde(rename = "pushCallback")]
707 pub endpoint: String,
708 #[serde(rename = "pushPublicKey")]
709 pub public_key: String,
710 #[serde(rename = "pushAuthKey")]
711 pub auth_key: String,
712}
713
714impl From<crate::DevicePushSubscription> for PushSubscription {
715 fn from(sub: crate::DevicePushSubscription) -> Self {
716 PushSubscription {
717 endpoint: sub.endpoint,
718 public_key: sub.public_key,
719 auth_key: sub.auth_key,
720 }
721 }
722}
723
724impl From<PushSubscription> for crate::DevicePushSubscription {
725 fn from(sub: PushSubscription) -> Self {
726 crate::DevicePushSubscription {
727 endpoint: sub.endpoint,
728 public_key: sub.public_key,
729 auth_key: sub.auth_key,
730 }
731 }
732}
733
734#[derive(Serialize)]
742#[allow(clippy::option_option)]
743pub struct DeviceUpdateRequest<'a> {
744 #[serde(skip_serializing_if = "Option::is_none")]
745 #[serde(rename = "name")]
746 display_name: Option<Option<&'a str>>,
747 #[serde(skip_serializing_if = "Option::is_none")]
748 #[serde(rename = "type")]
749 device_type: Option<&'a DeviceType>,
750 #[serde(flatten)]
751 push_subscription: Option<&'a PushSubscription>,
752 #[serde(skip_serializing_if = "Option::is_none")]
753 #[serde(rename = "availableCommands")]
754 available_commands: Option<Option<&'a HashMap<String, String>>>,
755}
756
757#[allow(clippy::option_option)]
758pub struct DeviceUpdateRequestBuilder<'a> {
759 device_type: Option<&'a DeviceType>,
760 display_name: Option<Option<&'a str>>,
761 push_subscription: Option<&'a PushSubscription>,
762 available_commands: Option<Option<&'a HashMap<String, String>>>,
763}
764
765impl<'a> DeviceUpdateRequestBuilder<'a> {
766 pub fn new() -> Self {
767 Self {
768 device_type: None,
769 display_name: None,
770 push_subscription: None,
771 available_commands: None,
772 }
773 }
774
775 pub fn push_subscription(mut self, push_subscription: &'a PushSubscription) -> Self {
776 self.push_subscription = Some(push_subscription);
777 self
778 }
779
780 pub fn available_commands(mut self, available_commands: &'a HashMap<String, String>) -> Self {
781 self.available_commands = Some(Some(available_commands));
782 self
783 }
784
785 pub fn display_name(mut self, display_name: &'a str) -> Self {
786 self.display_name = Some(Some(display_name));
787 self
788 }
789
790 pub fn clear_display_name(mut self) -> Self {
791 self.display_name = Some(None);
792 self
793 }
794
795 pub fn device_type(mut self, device_type: &'a DeviceType) -> Self {
796 self.device_type = Some(device_type);
797 self
798 }
799
800 pub fn build(self) -> DeviceUpdateRequest<'a> {
801 DeviceUpdateRequest {
802 display_name: self.display_name,
803 device_type: self.device_type,
804 push_subscription: self.push_subscription,
805 available_commands: self.available_commands,
806 }
807 }
808}
809
810#[derive(Clone, Debug, Serialize, Deserialize)]
811pub struct DeviceLocation {
812 pub city: Option<String>,
813 pub country: Option<String>,
814 pub state: Option<String>,
815 #[serde(rename = "stateCode")]
816 pub state_code: Option<String>,
817}
818
819#[derive(Clone, Debug, Serialize, Deserialize)]
820pub struct GetDeviceResponse {
821 #[serde(flatten)]
822 pub common: DeviceResponseCommon,
823 #[serde(rename = "isCurrentDevice")]
824 pub is_current_device: bool,
825 pub location: DeviceLocation,
826 #[serde(rename = "lastAccessTime")]
827 pub last_access_time: Option<u64>,
828}
829
830impl std::ops::Deref for GetDeviceResponse {
831 type Target = DeviceResponseCommon;
832 fn deref(&self) -> &DeviceResponseCommon {
833 &self.common
834 }
835}
836
837pub type UpdateDeviceResponse = DeviceResponseCommon;
838
839#[derive(Clone, Debug, Serialize, Deserialize)]
840pub struct DeviceResponseCommon {
841 pub id: String,
842 #[serde(rename = "name")]
843 pub display_name: String,
844 #[serde(rename = "type")]
845 pub device_type: DeviceType,
846 #[serde(flatten)]
847 pub push_subscription: Option<PushSubscription>,
848 #[serde(rename = "availableCommands")]
849 pub available_commands: HashMap<String, String>,
850 #[serde(rename = "pushEndpointExpired")]
851 pub push_endpoint_expired: bool,
852}
853
854#[derive(Clone, Debug, Serialize, Deserialize)]
855#[serde(rename_all = "camelCase")]
856pub struct GetAttachedClientResponse {
857 pub client_id: Option<String>,
858 pub session_token_id: Option<String>,
859 pub refresh_token_id: Option<String>,
860 pub device_id: Option<String>,
861 pub device_type: DeviceType,
862 pub is_current_session: bool,
863 pub name: Option<String>,
864 pub created_time: Option<u64>,
865 pub last_access_time: Option<u64>,
866 pub scope: Option<Vec<String>>,
867 pub user_agent: String,
868 pub os: Option<String>,
869}
870
871#[derive(Serialize)]
876#[serde(tag = "grant_type")]
877enum OAauthTokenRequest {
878 #[serde(rename = "refresh_token")]
879 UsingRefreshToken {
880 client_id: String,
881 refresh_token: String,
882 #[serde(skip_serializing_if = "Option::is_none")]
883 scope: Option<String>,
884 #[serde(skip_serializing_if = "Option::is_none")]
885 ttl: Option<u64>,
886 },
887 #[serde(rename = "authorization_code")]
888 UsingCode {
889 client_id: String,
890 code: String,
891 code_verifier: String,
892 #[serde(skip_serializing_if = "Option::is_none")]
893 ttl: Option<u64>,
894 },
895}
896
897#[derive(Deserialize)]
898pub struct OAuthTokenResponse {
899 pub keys_jwe: Option<String>,
900 pub refresh_token: Option<String>,
901 pub session_token: Option<String>,
902 pub expires_in: u64,
903 pub scope: String,
904 pub access_token: String,
905}
906
907#[derive(Deserialize, Debug)]
908pub struct OAuthAuthResponse {
909 #[allow(dead_code)]
910 pub redirect: String,
911 pub code: String,
912 #[allow(dead_code)]
913 pub state: String,
914}
915
916#[derive(Deserialize)]
917pub struct IntrospectResponse {
918 pub active: bool,
919 }
922
923#[derive(Clone, Serialize, Deserialize)]
924#[serde(rename_all = "camelCase")]
925pub struct ProfileResponse {
926 pub uid: String,
927 pub email: String,
928 pub display_name: Option<String>,
929 pub avatar: String,
930 pub avatar_default: bool,
931}
932
933impl From<ProfileResponse> for crate::Profile {
934 fn from(p: ProfileResponse) -> Self {
935 crate::Profile {
936 uid: p.uid,
937 email: p.email,
938 display_name: p.display_name,
939 avatar: p.avatar,
940 is_default_avatar: p.avatar_default,
941 }
942 }
943}
944
945#[derive(Deserialize)]
946pub struct ScopedKeyDataResponse {
947 #[allow(dead_code)]
948 pub identifier: String,
949 #[allow(dead_code)]
950 #[serde(rename = "keyRotationSecret")]
951 pub key_rotation_secret: String,
952 #[allow(dead_code)]
953 #[serde(rename = "keyRotationTimestamp")]
954 pub key_rotation_timestamp: u64,
955}
956
957#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
958pub struct DuplicateTokenResponse {
959 pub uid: String,
960 #[serde(rename = "sessionToken")]
961 pub session_token: String,
962 pub verified: bool,
963 #[serde(rename = "authAt")]
964 pub auth_at: u64,
965}
966
967#[derive(Serialize)]
968struct InvokeCommandRequest<'a> {
969 pub command: &'a str,
970 pub target: &'a str,
971 pub payload: &'a serde_json::Value,
972 #[serde(skip_serializing_if = "Option::is_none")]
973 pub ttl: Option<u64>,
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979 use mockito::mock;
980 #[test]
981 #[allow(non_snake_case)]
982 fn check_OAauthTokenRequest_serialization() {
983 let using_code = OAauthTokenRequest::UsingCode {
985 code: "foo".to_owned(),
986 client_id: "bar".to_owned(),
987 code_verifier: "bobo".to_owned(),
988 ttl: None,
989 };
990 assert_eq!("{\"grant_type\":\"authorization_code\",\"client_id\":\"bar\",\"code\":\"foo\",\"code_verifier\":\"bobo\"}", serde_json::to_string(&using_code).unwrap());
991 let using_code = OAauthTokenRequest::UsingRefreshToken {
992 client_id: "bar".to_owned(),
993 refresh_token: "foo".to_owned(),
994 scope: Some("bobo".to_owned()),
995 ttl: Some(123),
996 };
997 assert_eq!("{\"grant_type\":\"refresh_token\",\"client_id\":\"bar\",\"refresh_token\":\"foo\",\"scope\":\"bobo\",\"ttl\":123}", serde_json::to_string(&using_code).unwrap());
998 }
999
1000 #[test]
1001 #[allow(non_snake_case)]
1002 fn check_InvokeCommandRequest_serialization() -> Result<()> {
1003 let payload = json!({
1004 "a": "b",
1005 });
1006 let with_ttl = InvokeCommandRequest {
1007 command: "with_ttl",
1008 target: "a",
1009 payload: &payload,
1010 ttl: Some(30),
1011 };
1012 assert_eq!(
1013 serde_json::to_value(with_ttl)?,
1014 json!({
1015 "command": "with_ttl",
1016 "target": "a",
1017 "payload": {
1018 "a": "b",
1019 },
1020 "ttl": 30,
1021 })
1022 );
1023
1024 let without_ttl = InvokeCommandRequest {
1025 command: "without_ttl",
1026 target: "b",
1027 payload: &payload,
1028 ttl: None,
1029 };
1030 assert_eq!(
1031 serde_json::to_value(without_ttl)?,
1032 json!({
1033 "command": "without_ttl",
1034 "target": "b",
1035 "payload": {
1036 "a": "b",
1037 },
1038 })
1039 );
1040
1041 Ok(())
1042 }
1043
1044 #[test]
1045 fn test_backoff() {
1046 viaduct_reqwest::use_reqwest_backend();
1047 let m = mock("POST", "/v1/account/devices/invoke_command")
1048 .with_status(429)
1049 .with_header("Content-Type", "application/json")
1050 .with_header("retry-after", "1000000")
1051 .with_body(
1052 r#"{
1053 "code": 429,
1054 "errno": 120,
1055 "error": "Too many requests",
1056 "message": "Too many requests",
1057 "retryAfter": 1000000,
1058 "info": "Some information"
1059 }"#,
1060 )
1061 .create();
1062 let client = Client::new();
1063 let path = format!(
1064 "{}/{}",
1065 mockito::server_url(),
1066 "v1/account/devices/invoke_command"
1067 );
1068 let url = Url::parse(&path).unwrap();
1069 let path = url.path().to_string();
1070 let request = Request::post(url);
1071 assert!(client.make_request(request.clone()).is_err());
1072 let state = client.state.lock();
1073 if let HttpClientState::Backoff {
1074 backoff_end_duration,
1075 time_since_backoff: _,
1076 } = state.get(&path).unwrap()
1077 {
1078 assert_eq!(*backoff_end_duration, Duration::from_secs(1_000_000));
1079 std::mem::drop(state);
1082 assert!(client.make_request(request).is_err());
1083 m.expect(1).assert();
1086 } else {
1087 panic!("HttpClientState should be a timeout!");
1088 }
1089 }
1090
1091 #[test]
1092 fn test_backoff_then_ok() {
1093 viaduct_reqwest::use_reqwest_backend();
1094 let m = mock("POST", "/v1/account/devices/invoke_command")
1095 .with_status(429)
1096 .with_header("Content-Type", "application/json")
1097 .with_header("retry-after", "1")
1098 .with_body(
1099 r#"{
1100 "code": 429,
1101 "errno": 120,
1102 "error": "Too many requests",
1103 "message": "Too many requests",
1104 "retryAfter": 1,
1105 "info": "Some information"
1106 }"#,
1107 )
1108 .create();
1109 let client = Client::new();
1110 let path = format!(
1111 "{}/{}",
1112 mockito::server_url(),
1113 "v1/account/devices/invoke_command"
1114 );
1115 let url = Url::parse(&path).unwrap();
1116 let path = url.path().to_string();
1117 let request = Request::post(url);
1118 assert!(client.make_request(request.clone()).is_err());
1119 let state = client.state.lock();
1120 if let HttpClientState::Backoff {
1121 backoff_end_duration,
1122 time_since_backoff: _,
1123 } = state.get(&path).unwrap()
1124 {
1125 assert_eq!(*backoff_end_duration, Duration::from_secs(1));
1126 std::thread::sleep(*backoff_end_duration);
1128
1129 std::mem::drop(state);
1132 assert!(client.make_request(request).is_err());
1133 m.expect(2).assert();
1136 } else {
1137 panic!("HttpClientState should be a timeout!");
1138 }
1139 }
1140
1141 #[test]
1142 fn test_backoff_per_path() {
1143 viaduct_reqwest::use_reqwest_backend();
1144 let m1 = mock("POST", "/v1/account/devices/invoke_command")
1145 .with_status(429)
1146 .with_header("Content-Type", "application/json")
1147 .with_header("retry-after", "1000000")
1148 .with_body(
1149 r#"{
1150 "code": 429,
1151 "errno": 120,
1152 "error": "Too many requests",
1153 "message": "Too many requests",
1154 "retryAfter": 1000000,
1155 "info": "Some information"
1156 }"#,
1157 )
1158 .create();
1159 let m2 = mock("GET", "/v1/account/device/commands")
1160 .with_status(200)
1161 .with_header("Content-Type", "application/json")
1162 .with_body(
1163 r#"
1164 {
1165 "index": 3,
1166 "last": true,
1167 "messages": []
1168 }"#,
1169 )
1170 .create();
1171 let client = Client::new();
1172 let path = format!(
1173 "{}/{}",
1174 mockito::server_url(),
1175 "v1/account/devices/invoke_command"
1176 );
1177 let url = Url::parse(&path).unwrap();
1178 let path = url.path().to_string();
1179 let request = Request::post(url);
1180 assert!(client.make_request(request).is_err());
1181 let state = client.state.lock();
1182 if let HttpClientState::Backoff {
1183 backoff_end_duration,
1184 time_since_backoff: _,
1185 } = state.get(&path).unwrap()
1186 {
1187 assert_eq!(*backoff_end_duration, Duration::from_secs(1_000_000));
1188
1189 let path2 = format!("{}/{}", mockito::server_url(), "v1/account/device/commands");
1190 std::mem::drop(state);
1193 let second_request = Request::get(Url::parse(&path2).unwrap());
1194 assert!(client.make_request(second_request).is_ok());
1195 m1.expect(1).assert();
1198 m2.expect(1).assert();
1199 } else {
1200 panic!("HttpClientState should be a timeout!");
1201 }
1202 }
1203}