Skip to content
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

Merge sync15 adapter #12

Merged
merged 28 commits into from
May 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f826805
WIP work on storage adapter -- not working yet
Apr 21, 2018
d75ed6c
Fix compiler errors and add serialization tests
Apr 23, 2018
f9d5436
Don't version Cargo.lock
Apr 23, 2018
6de7d68
Fix compiler warnings
Apr 23, 2018
bafddda
Keybundle tests
Apr 23, 2018
0461cda
Simplify how BsoRecords are deserialized
Apr 23, 2018
88ebe67
Fix + test decrypt BsoRecord
Apr 23, 2018
18cc6de
Firm up some stuff around crypto and logins
Apr 24, 2018
ebfa93a
Figure out how tombstones play into this, fixup deserialization so th…
Apr 24, 2018
b22952b
Untested (but compiling, yay WIP) work for getting keys, and the flow…
Apr 25, 2018
fec1cf5
Break code out of lib.rs into token.rs and collection_keys.rs. Misc u…
Apr 25, 2018
4ec9483
Move tombstone stuff into record_types.rs
Apr 25, 2018
c049ccc
Merge branch 'master' into tcsc/sync15-adapter-WIP
Apr 25, 2018
9f1888e
Merge branch 'master' into tcsc/sync15-adapter-WIP
Apr 26, 2018
e77fb59
Small refactorings
Apr 26, 2018
584a869
Merge branch 'master' into tcsc/sync15-adapter-WIP
Apr 26, 2018
0b148c5
Drop fxa-rust-client from workspace for now because it breaks the build
Apr 26, 2018
8b12dc4
Boxlocker-parity example (that doesn't quite work)
Apr 26, 2018
3ad7ef4
Work towards doing more correct things during setup
Apr 26, 2018
c0a1c41
More type safe handling of server time stamps, and add an api for col…
Apr 27, 2018
7ca3dca
WIP postqueue (compiles, only sort of tested)
Apr 27, 2018
a73408d
Whole mess of tests for batching uploader (it could use more, though)
Apr 28, 2018
fa977ef
Get batch upload demonstratably working
Apr 30, 2018
d4ef087
Merge branch 'master' into tcsc/sync15-adapter-WIP
Apr 30, 2018
fc4a5e9
Merge branch 'master' into tcsc/sync15-adapter-WIP
May 1, 2018
6356639
Implement an FFI for sync15-adapter
May 2, 2018
88da475
Fix warnings and stale comments
May 2, 2018
bcbb71c
Clean up file structure a bit based on review
May 2, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
website/build
target/
target
Cargo.lock
credentials.json
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"boxlocker",
"fxa-rust-client"
"sync15-adapter",
"sync15-adapter/ffi"
]
21 changes: 21 additions & 0 deletions sync15-adapter/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "sync15-adapter"
version = "0.1.0"
authors = ["Thom Chiovoloni <[email protected]>"]

[dependencies]
base64 = "0.9.0"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
url = "1.6.0"
reqwest = "0.8.2"
error-chain = "0.11"
openssl = "0.10.7"
hawk = { git = "https://github.com/eoger/rust-hawk", branch = "use-openssl" }
hyper = "0.11"
log = "0.4"
lazy_static = "1.0"

[dev-dependencies]
env_logger = "0.5"
188 changes: 188 additions & 0 deletions sync15-adapter/examples/boxlocker-parity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@

extern crate sync15_adapter as sync;
extern crate error_chain;
extern crate url;
extern crate base64;
extern crate reqwest;

extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;

extern crate env_logger;

use std::io::{self, Read, Write};
use std::error::Error;
use std::fs;
use std::process;
use std::time;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Deserialize)]
struct OAuthCredentials {
access_token: String,
refresh_token: String,
keys: HashMap<String, ScopedKeyData>,
expires_in: u64,
auth_at: u64,
}

#[derive(Debug, Deserialize)]
struct ScopedKeyData {
k: String,
kid: String,
scope: String,
}

fn do_auth(recur: bool) -> Result<OAuthCredentials, Box<Error>> {
match fs::File::open("./credentials.json") {
Err(_) => {
if recur {
panic!("Failed to open credentials 2nd time");
}
println!("No credentials found, invoking boxlocker.py...");
process::Command::new("python")
.arg("../boxlocker/boxlocker.py").output()
.expect("Failed to run boxlocker.py");
return do_auth(true);
},
Ok(mut file) => {
let mut s = String::new();
file.read_to_string(&mut s)?;
let creds: OAuthCredentials = serde_json::from_str(&s)?;
let time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
if creds.expires_in + creds.auth_at < time {
println!("Warning, credentials may be stale.");
}
Ok(creds)
}
}
}

fn prompt_string<S: AsRef<str>>(prompt: S) -> Option<String> {
print!("{}: ", prompt.as_ref());
let _ = io::stdout().flush(); // Don't care if flush fails really.
let mut s = String::new();
io::stdin().read_line(&mut s).expect("Failed to read line...");
if let Some('\n') = s.chars().next_back() { s.pop(); }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using trim_matches seems like it would be clearer, although presumably requiring additional allocation of string objects. For my education, is it written this way for efficiency or for some other reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, I wouldn't worry about allocations as a result of user input which should be rare. I just saw that https://doc.rust-lang.org/std/str/pattern/trait.Pattern.html was not stable, and got confused as to whether or not trim_matches was usable.

if let Some('\r') = s.chars().next_back() { s.pop(); }
if s.len() == 0 {
None
} else {
Some(s)
}
}

fn read_login() -> sync::record_types::PasswordRecord {
let username = prompt_string("username").unwrap_or(String::new());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unwrap_or_default() is shorter, unless it won't work here... 😄 #ihavenoideawhatimdoing

let password = prompt_string("password").unwrap_or(String::new());
let form_submit_url = prompt_string("form_submit_url");
let hostname = prompt_string("hostname");
let http_realm = prompt_string("http_realm");
let username_field = prompt_string("username_field").unwrap_or(String::new());
let password_field = prompt_string("password_field").unwrap_or(String::new());
let since_unix_epoch = time::SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap();
let dur_ms = since_unix_epoch.as_secs() * 1000 + ((since_unix_epoch.subsec_nanos() / 1_000_000) as u64);
let ms_i64 = dur_ms as i64;
sync::record_types::PasswordRecord {
id: sync::util::random_guid().unwrap(),
username,
password,
username_field,
password_field,
form_submit_url,
http_realm,
hostname,
time_created: ms_i64,
time_password_changed: ms_i64,
times_used: None,
time_last_used: Some(ms_i64),
}
}

fn prompt_bool(msg: &str) -> Option<bool> {
let result = prompt_string(msg);
result.and_then(|r| match r.chars().next().unwrap() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could you spell this result.and_then(|r| r.chars().next()).and_then(|c| match c { ...? Using unwrap together with and_then, and having it wrap the result back up in an Option, seems a bit roundabout.

'y' | 'Y' | 't' | 'T' => Some(true),
'n' | 'N' | 'f' | 'F' => Some(false),
_ => None
})
}

fn prompt_chars(msg: &str) -> Option<char> {
prompt_string(msg).and_then(|r| r.chars().next())
}

fn start() -> Result<(), Box<Error>> {
let oauth_data = do_auth(false)?;

let scope = &oauth_data.keys["https://identity.mozilla.com/apps/oldsync"];

let mut svc = sync::Sync15Service::new(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your intuition that Sync15Service isn't quite the right abstraction, at least as it's implemented here, is right. It matches the Desktop design, where we have a Service singleton, but I'm liking @mhammond's suggestion to tease apart some of this state.

Some of it could be managed by an HTTP client wrapper, some by a collections manager, and some by a collection that handles version and sync ID changes, and persistence. (Maybe the collection is also the right place to plug in storage? It would be a merger of the Collection and Engine from Desktop, roughly).

Consumers would create these object, hold on to some state, and pass it around. So the app would create the key bundle, Sync client (which maybe manages its own reqwest::Client under the hood, or takes one that the app creates...we could always add a wrapper to create all the objects, once we figure out how we want to interact with them), and token server client. We could have TokenserverClient, and our storage node client, store internal state to handle backoff and return an error (maybe with a retry_at property) that the app could use to show errors and figure out when to retry. Once a good pattern emerges from all this, we can lift it up into the adapter.

In short: let's break up what we have into smaller pieces, try to write an end-to-end consumer, and figure out how to reassemble them in a way that makes sense.

sync::Sync15ServiceInit {
key_id: scope.kid.clone(),
sync_key: scope.k.clone(),
access_token: oauth_data.access_token.clone(),
tokenserver_base_url: "https://oauth-sync.dev.lcip.org/syncserver/token".into(),
}
)?;

svc.remote_setup()?;
let passwords = svc.all_records::<sync::record_types::PasswordRecord>("passwords")?
.into_iter()
.filter_map(|r| r.record())
.collect::<Vec<_>>();

println!("Found {} passwords", passwords.len());

for pw in passwords.iter() {
println!("{:?}", pw.payload);
}

if !prompt_bool("Would you like to make changes? [y/N]").unwrap_or(false) {
return Ok(());
}

let mut ids: Vec<String> = passwords.iter().map(|p| p.id.clone()).collect();

let mut upd = sync::CollectionUpdate::new(&svc, false);
loop {
match prompt_chars("Add, delete, or commit [adc]:").unwrap_or('s') {
'A' | 'a' => {
let record = read_login();
upd.add_record(record);
},
'D' | 'd' => {
for (i, id) in ids.iter().enumerate() {
println!("{}: {}", i, id);
}
if let Some(index) = prompt_string("Index to delete (enter index)").and_then(|x| x.parse::<usize>().ok()) {
let result = ids.swap_remove(index);
upd.add_tombstone(result);
} else {
println!("???");
}
},
'C' | 'c' => {
println!("committing!");
let (good, bad) = upd.upload()?;
println!("Uploded {} ids successfully, and {} unsuccessfully",
good.len(), bad.len());
break;
},
c => {
println!("Unknown action '{}', exiting.", c);
break;
}
}
}

Ok(())
}

fn main() {
env_logger::init();
start().unwrap();
}
14 changes: 14 additions & 0 deletions sync15-adapter/ffi/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "sync-adapter-ffi"
version = "0.1.0"
authors = ["Thom Chiovoloni <[email protected]>"]

[lib]
name = "sync_adapter"
crate-type = ["staticlib"]

[dependencies]
libc = "0.2"

[dependencies.sync15-adapter]
path = "../"
9 changes: 9 additions & 0 deletions sync15-adapter/ffi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Sync 1.5 Client FFI

This README is shamelessly stolen from the one in the fxa client directory

## iOS build

- Make sure you have the nightly compiler in order to get LLVM Bitcode generation.
- Install [cargo-lipo](https://github.com/TimNN/cargo-lipo/#installation).
- Build with: `OPENSSL_DIR=/usr/local/opt/openssl cargo +nightly lipo --release`
Loading