New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add offline use case for 'migrateFromSessionToken' #2492
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,10 @@ | ||
use cli_support::prompt::prompt_string; | ||
use fxa_client::FirefoxAccount; | ||
use std::{thread, time}; | ||
|
||
static CLIENT_ID: &str = "3c49430b43dfba77"; | ||
//static CONTENT_SERVER: &str = "https://latest.dev.lcip.org"; | ||
static CONTENT_SERVER: &str = "http://127.0.0.1:3030"; | ||
//static REDIRECT_URI: &str = "https://latest.dev.lcip.org/oauth/success/3c49430b43dfba77"; | ||
static REDIRECT_URI: &str = "http://127.0.0.1:3030/oauth/success/3c49430b43dfba77"; | ||
static CONTENT_SERVER: &str = "https://accounts.firefox.com"; | ||
static REDIRECT_URI: &str = "https://accounts.firefox.com/oauth/success/3c49430b43dfba77"; | ||
|
||
fn main() { | ||
let mut fxa = FirefoxAccount::new(CONTENT_SERVER, CLIENT_ID, REDIRECT_URI); | ||
|
@@ -15,8 +14,22 @@ fn main() { | |
let k_sync: String = prompt_string("k_sync").unwrap(); | ||
println!("Enter kXCS (hex-string):"); | ||
let k_xcs: String = prompt_string("k_xcs").unwrap(); | ||
fxa.migrate_from_session_token(&session_token, &k_sync, &k_xcs, true) | ||
.unwrap(); | ||
println!("WOW! You've been migrated."); | ||
let migration_result = | ||
match fxa.migrate_from_session_token(&session_token, &k_sync, &k_xcs, true) { | ||
Ok(migration_result) => migration_result, | ||
Err(err) => { | ||
println!("Error: {}", err); | ||
// example for offline behaviour | ||
loop { | ||
thread::sleep(time::Duration::from_millis(5000)); | ||
let retry = fxa.try_migration(); | ||
match retry { | ||
Ok(result) => break result, | ||
Err(_) => println!("Retrying... Are you connected to the internet?"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Continuing discussion from the previous PR, each migration attempt could fail for either both fatal or non-fatal reasons. It would be useful for this example to show how to handle the two different cases. For example right now, if I enter an invalid sessionToken into this example script, then IIUC it will loop forever asking me whether I'm connected to the internet. We don't want to get anyone's migrated Fennec stuck in such a loop in practice. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (I'll also note that "not connected to the internet" is not necessarily the only transient error; 500-level server errors should probably also be treated as transient and retried). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know it's only for testing purposes, but I'd like to see this example use the same logic as we expect consumers to use in practice, which IIUC is basically:
|
||
} | ||
} | ||
} | ||
}; | ||
println!("WOW! You've been migrated in {:?}.", migration_result); | ||
println!("JSON: {}", fxa.to_json().unwrap()); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,14 @@ | |
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
use crate::{error::*, scoped_keys::ScopedKey, scopes, FirefoxAccount}; | ||
use crate::{error::*, scoped_keys::ScopedKey, scopes, FirefoxAccount, MigrationData}; | ||
use serde_derive::*; | ||
use std::time::Instant; | ||
|
||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)] | ||
pub struct FxAMigrationResult { | ||
pub total_duration: u128, | ||
} | ||
|
||
impl FirefoxAccount { | ||
/// Migrate from a logged-in with a sessionToken Firefox Account. | ||
|
@@ -24,20 +31,79 @@ impl FirefoxAccount { | |
k_sync: &str, | ||
k_xcs: &str, | ||
copy_session_token: bool, | ||
) -> Result<()> { | ||
) -> Result<FxAMigrationResult> { | ||
// if there is already a session token on account, we error out. | ||
if self.state.session_token.is_some() { | ||
return Err(ErrorKind::IllegalState("Session Token is already set.").into()); | ||
} | ||
|
||
let migration_session_token = if copy_session_token { | ||
self.state.in_flight_migration = Some(MigrationData { | ||
k_sync: k_sync.to_string(), | ||
k_xcs: k_xcs.to_string(), | ||
copy_session_token, | ||
session_token: session_token.to_string(), | ||
}); | ||
|
||
self.try_migration() | ||
} | ||
|
||
/// Check if the client is in a pending migration state | ||
pub fn is_in_migration_state(&self) -> bool { | ||
self.state.in_flight_migration.is_some() | ||
} | ||
|
||
pub fn try_migration(&mut self) -> Result<FxAMigrationResult> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Naming nit: I think the overall structure of the code would be a bit clearer if this were called |
||
let import_start = Instant::now(); | ||
|
||
match self.network_migration() { | ||
Ok(_) => {} | ||
Err(err) => { | ||
match err.kind() { | ||
ErrorKind::RemoteError { | ||
code: 500..=599, .. | ||
} | ||
| ErrorKind::RemoteError { code: 429, .. } | ||
| ErrorKind::RequestError(_) => { | ||
// network errors that will allow hopefully migrate later | ||
log::warn!("Network error: {:?}", err); | ||
return Err(err); | ||
} | ||
_ => { | ||
// probably will not recover | ||
|
||
self.state.in_flight_migration = None; | ||
|
||
return Err(err); | ||
} | ||
}; | ||
} | ||
} | ||
|
||
self.state.in_flight_migration = None; | ||
|
||
let metrics = FxAMigrationResult { | ||
total_duration: import_start.elapsed().as_millis(), | ||
}; | ||
|
||
Ok(metrics) | ||
} | ||
|
||
fn network_migration(&mut self) -> Result<()> { | ||
let migration_data = match self.state.in_flight_migration { | ||
Some(ref data) => data.clone(), | ||
None => { | ||
return Err(ErrorKind::NoMigrationData.into()); | ||
} | ||
}; | ||
|
||
let migration_session_token = if migration_data.copy_session_token { | ||
let duplicate_session = self | ||
.client | ||
.duplicate_session(&self.state.config, &session_token)?; | ||
.duplicate_session(&self.state.config, &migration_data.session_token)?; | ||
|
||
duplicate_session.session_token | ||
} else { | ||
session_token.to_string() | ||
migration_data.session_token.to_string() | ||
}; | ||
|
||
// Trade our session token for a refresh token. | ||
|
@@ -49,9 +115,9 @@ impl FirefoxAccount { | |
self.handle_oauth_response(oauth_response, None)?; | ||
|
||
// Synthesize a scoped key from our kSync. | ||
let k_sync = hex::decode(k_sync)?; | ||
let k_sync = hex::decode(&migration_data.k_sync)?; | ||
let k_sync = base64::encode_config(&k_sync, base64::URL_SAFE_NO_PAD); | ||
let k_xcs = hex::decode(k_xcs)?; | ||
let k_xcs = hex::decode(&migration_data.k_xcs)?; | ||
let k_xcs = base64::encode_config(&k_xcs, base64::URL_SAFE_NO_PAD); | ||
let scoped_key_data = self.client.scoped_key_data( | ||
&self.state.config, | ||
|
@@ -72,6 +138,7 @@ impl FirefoxAccount { | |
self.state | ||
.scoped_keys | ||
.insert(scopes::OLD_SYNC.to_string(), k_sync_scoped_key); | ||
|
||
Ok(()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest rewording this for clarity, to focus on the things that consumers need to know in the order they need to know them. Along the lines of:
migrateFromSessionToken
now has special handling for transient failures such as network errors. If the migration fails due to a transient error, then the provided credentials will be cached so that the migration can be retried later. Consumers can call theisInMigrationState
method to check if there is a cached migration in progress, andretryMigrateFromSessionToken
to retry it.