fxa_client/state_machine/internal_machines/
mod.rsmod auth_issues;
mod authenticating;
mod connected;
mod disconnected;
mod uninitialized;
use crate::{
internal::FirefoxAccount, DeviceConfig, Error, FxaError, FxaEvent, FxaRustAuthState, FxaState,
Result,
};
pub use auth_issues::AuthIssuesStateMachine;
pub use authenticating::AuthenticatingStateMachine;
pub use connected::ConnectedStateMachine;
pub use disconnected::DisconnectedStateMachine;
use error_support::convert_log_report_error;
pub use uninitialized::UninitializedStateMachine;
pub trait InternalStateMachine {
fn initial_state(&self, event: FxaEvent) -> Result<State>;
fn next_state(&self, state: State, event: Event) -> Result<State>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(clippy::enum_variant_names)]
pub enum State {
GetAuthState,
BeginOAuthFlow {
scopes: Vec<String>,
entrypoint: String,
},
BeginPairingFlow {
pairing_url: String,
scopes: Vec<String>,
entrypoint: String,
},
CompleteOAuthFlow {
code: String,
state: String,
},
InitializeDevice,
EnsureDeviceCapabilities,
CheckAuthorizationStatus,
Disconnect,
GetProfile,
Complete(FxaState),
Cancel,
}
#[derive(Clone, Debug)]
pub enum Event {
GetAuthStateSuccess {
auth_state: FxaRustAuthState,
},
BeginOAuthFlowSuccess {
oauth_url: String,
},
BeginPairingFlowSuccess {
oauth_url: String,
},
CompleteOAuthFlowSuccess,
InitializeDeviceSuccess,
EnsureDeviceCapabilitiesSuccess,
CheckAuthorizationStatusSuccess {
active: bool,
},
DisconnectSuccess,
GetProfileSuccess,
CallError,
EnsureCapabilitiesAuthError,
}
impl State {
pub fn make_call(
&self,
account: &mut FirefoxAccount,
device_config: &DeviceConfig,
) -> Result<Event> {
let mut error_handling = CallErrorHandler::new(self);
loop {
return match self.make_call_inner(account, device_config) {
Ok(event) => Ok(event),
Err(e) => match error_handling.handle_error(e, account) {
CallResult::Retry => continue,
CallResult::Finished(event) => Ok(event),
CallResult::InternalError(err) => Err(err),
},
};
}
}
fn make_call_inner(
&self,
account: &mut FirefoxAccount,
device_config: &DeviceConfig,
) -> Result<Event> {
Ok(match self {
State::GetAuthState => Event::GetAuthStateSuccess {
auth_state: account.get_auth_state(),
},
State::EnsureDeviceCapabilities => {
account.ensure_capabilities(&device_config.capabilities)?;
Event::EnsureDeviceCapabilitiesSuccess
}
State::BeginOAuthFlow { scopes, entrypoint } => {
let scopes: Vec<&str> = scopes.iter().map(String::as_str).collect();
let oauth_url = account.begin_oauth_flow(&scopes, entrypoint)?;
Event::BeginOAuthFlowSuccess { oauth_url }
}
State::BeginPairingFlow {
pairing_url,
scopes,
entrypoint,
} => {
let scopes: Vec<&str> = scopes.iter().map(String::as_str).collect();
let oauth_url = account.begin_pairing_flow(pairing_url, &scopes, entrypoint)?;
Event::BeginPairingFlowSuccess { oauth_url }
}
State::CompleteOAuthFlow { code, state } => {
account.complete_oauth_flow(code, state)?;
Event::CompleteOAuthFlowSuccess
}
State::InitializeDevice => {
account.initialize_device(
&device_config.name,
device_config.device_type,
&device_config.capabilities,
)?;
Event::InitializeDeviceSuccess
}
State::CheckAuthorizationStatus => {
let active = account.check_authorization_status()?.active;
Event::CheckAuthorizationStatusSuccess { active }
}
State::Disconnect => {
account.disconnect();
Event::DisconnectSuccess
}
State::GetProfile => {
account.get_profile(true)?;
Event::GetProfileSuccess
}
state => {
return Err(Error::StateMachineLogicError(format!(
"process_call: Don't know how to handle {state}"
)))
}
})
}
}
const NETWORK_RETRY_LIMIT: usize = 3;
struct CallErrorHandler<'a> {
network_retries: usize,
auth_retries: usize,
state: &'a State,
}
impl<'a> CallErrorHandler<'a> {
fn new(state: &'a State) -> Self {
Self {
network_retries: 0,
auth_retries: 0,
state,
}
}
fn handle_error(&mut self, e: Error, account: &mut FirefoxAccount) -> CallResult {
if matches!(e, Error::StateMachineLogicError(_)) {
return CallResult::InternalError(e);
}
log::warn!("handling error: {e}");
match convert_log_report_error(e) {
FxaError::Network => {
if self.network_retries < NETWORK_RETRY_LIMIT {
self.network_retries += 1;
CallResult::Retry
} else {
CallResult::Finished(Event::CallError)
}
}
FxaError::Authentication => {
if self.auth_retries < 1 && !matches!(self.state, State::CheckAuthorizationStatus) {
account.clear_access_token_cache();
match account.check_authorization_status() {
Ok(status) if status.active => {
self.auth_retries += 1;
CallResult::Retry
}
_ => CallResult::Finished(self.event_for_auth_error()),
}
} else {
CallResult::Finished(self.event_for_auth_error())
}
}
_ => CallResult::Finished(Event::CallError),
}
}
fn event_for_auth_error(&self) -> Event {
if matches!(self.state, State::EnsureDeviceCapabilities) {
Event::EnsureCapabilitiesAuthError
} else {
Event::CallError
}
}
}
enum CallResult {
Finished(Event),
Retry,
InternalError(Error),
}
fn invalid_transition(state: State, event: Event) -> Result<State> {
Err(Error::InvalidStateTransition(format!("{state} -> {event}")))
}
#[cfg(test)]
struct StateMachineTester<T> {
state_machine: T,
state: State,
}
#[cfg(test)]
impl<T: InternalStateMachine> StateMachineTester<T> {
fn new(state_machine: T, event: FxaEvent) -> Self {
let initial_state = state_machine
.initial_state(event)
.expect("Error getting initial state");
Self {
state_machine,
state: initial_state,
}
}
fn next_state(&mut self, event: Event) {
self.state = self.peek_next_state(event);
}
fn peek_next_state(&self, event: Event) -> State {
self.state_machine
.next_state(self.state.clone(), event.clone())
.unwrap_or_else(|e| {
panic!(
"Error getting next state: {e} state: {:?} event: {event:?}",
self.state
)
})
}
}