Introduce a state machine for Bob's secure-join

This introduces a state machine which takes care of managing the
handshake transitions in the secure-join protocol.  This separates
user interactions from the protocol state handling.

This means that while handling the protocol state there are a bunch of
failures no longer possible due to all state information being
guaranteed to be present.  As part of this the QR-code state has been
extracted from the generic Lot structure to something suitable just
for the SecureJoin protocol.

A LogSink has been added to the testing tools allowing log messages to
be correctly displayed on test failures.
This commit is contained in:
Floris Bruynooghe
2021-01-10 19:07:24 +01:00
parent 4508eced37
commit 11e3380f65
7 changed files with 833 additions and 440 deletions

View File

@@ -3063,7 +3063,7 @@ mod tests {
#[async_std::test]
async fn test_chat_info() {
let t = TestContext::new().await;
let chat = t.chat_with_contact("bob", "bob@example.com").await;
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
let info = chat.get_info(&t).await.unwrap();
// Ensure we can serialize this.

View File

@@ -47,7 +47,7 @@ pub struct InnerContext {
pub(crate) blobdir: PathBuf,
pub(crate) sql: Sql,
pub(crate) os_name: Option<String>,
pub(crate) bob: RwLock<Bob>,
pub(crate) bob: Bob,
pub(crate) last_smeared_timestamp: RwLock<i64>,
pub(crate) running_state: RwLock<RunningState>,
/// Mutex to avoid generating the key for the user more than once.
@@ -129,7 +129,7 @@ impl Context {
os_name: Some(os_name),
running_state: RwLock::new(Default::default()),
sql: Sql::new(),
bob: RwLock::new(Default::default()),
bob: Default::default(),
last_smeared_timestamp: RwLock::new(0),
generating_key_mutex: Mutex::new(()),
oauth2_mutex: Mutex::new(()),
@@ -197,7 +197,10 @@ impl Context {
});
}
/// Get the next queued event.
/// Returns a receiver for emitted events.
///
/// Multiple emitters can be created, but note that in this case each emitted event will
/// only be received by one of the emitters, not by all of them.
pub fn get_event_emitter(&self) -> EventEmitter {
self.events.get_emitter()
}

View File

@@ -440,7 +440,6 @@ async fn add_parts(
}
Err(err) => {
*hidden = true;
context.stop_ongoing().await;
warn!(context, "Error in Secure-Join message handling: {}", err);
return Ok(());
}

View File

@@ -47,6 +47,17 @@ impl Events {
}
}
/// A receiver of events from a [`Context`].
///
/// See [`Context::get_event_emitter`] to create an instance. If multiple instances are
/// created events emitted by the [`Context`] will only be delivered to one of the
/// `EventEmitter`s.
///
/// The `EventEmitter` is also a [`Stream`], so a typical usage is in a `while let` loop.
///
/// [`Context`]: crate::context::Context
/// [`Context::get_event_emitter`]: crate::context::Context::get_event_emitter
/// [`Stream`]: async_std::stream::Stream
#[derive(Debug, Clone)]
pub struct EventEmitter(Receiver<Event>);
@@ -120,8 +131,9 @@ impl EventType {
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty)]
pub enum EventType {
/// The library-user may write an informational string to the log.
/// Passed to the callback given to dc_context_new().
/// This event should not be reported to the end-user using a popup or something like that.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
#[strum(props(id = "100"))]
Info(String),
@@ -154,14 +166,13 @@ pub enum EventType {
DeletedBlobFile(String),
/// The library-user should write a warning string to the log.
/// Passed to the callback given to dc_context_new().
///
/// This event should not be reported to the end-user using a popup or something like that.
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
#[strum(props(id = "300"))]
Warning(String),
/// The library-user should report an error to the end-user.
/// Passed to the callback given to dc_context_new().
///
/// As most things are asynchronous, things may go wrong at any time and the user
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.

View File

@@ -2125,7 +2125,7 @@ mod tests {
.await
.unwrap();
let chat = d.chat_with_contact("", "dest@example.com").await;
let chat = d.create_chat_with_contact("", "dest@example.com").await;
let mut msg = Message::new(Viewtype::Text);
@@ -2141,7 +2141,7 @@ mod tests {
let d = test::TestContext::new().await;
let ctx = &d.ctx;
let chat = d.chat_with_contact("", "dest@example.com").await;
let chat = d.create_chat_with_contact("", "dest@example.com").await;
let mut msg = Message::new(Viewtype::Text);
@@ -2331,7 +2331,7 @@ mod tests {
.await
.unwrap();
let chat = d.chat_with_contact("", "dest@example.com").await;
let chat = d.create_chat_with_contact("", "dest@example.com").await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("Quoted message".to_string()));

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
//! Utilities to help writing tests.
//!
//! This module is only compiled for test runs.
//! This private module is only compiled for test runs.
use std::collections::BTreeMap;
use std::fmt;
@@ -19,7 +19,7 @@ use tempfile::{tempdir, TempDir};
use crate::chat::{self, Chat, ChatId, ChatItem};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::DC_CONTACT_ID_SELF;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF, DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::dc_receive_imf::dc_receive_imf;
@@ -31,10 +31,6 @@ use crate::message::{update_msg_state, Message, MessageState, MsgId};
use crate::mimeparser::MimeMessage;
use crate::param::{Param, Params};
use crate::constants::Viewtype;
use crate::constants::DC_MSG_ID_DAYMARKER;
use crate::constants::DC_MSG_ID_MARKER1;
type EventSink =
dyn Fn(Event) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> + Send + Sync + 'static;
@@ -67,7 +63,7 @@ impl fmt::Debug for TestContext {
}
impl TestContext {
/// Creates a new [TestContext].
/// Creates a new [`TestContext`].
///
/// The [Context] will be created and have an SQLite database named "db.sqlite" in the
/// [TestContext.dir] directory. This directory is cleaned up when the [TestContext] is
@@ -120,7 +116,7 @@ impl TestContext {
}
}
/// Create a new configured [TestContext].
/// Creates a new configured [TestContext].
///
/// This is a shortcut which automatically calls [TestContext::configure_alice] after
/// creating the context.
@@ -130,7 +126,7 @@ impl TestContext {
t
}
/// Create a new configured [TestContext].
/// Creates a new configured [TestContext].
///
/// This is a shortcut which configures bob@example.net with a fixed key.
pub async fn new_bob() -> Self {
@@ -200,7 +196,7 @@ impl TestContext {
}
}
/// Retrieve a sent message from the jobs table.
/// Retrieves a sent message from the jobs table.
///
/// This retrieves and removes a message which has been scheduled to send from the jobs
/// table. Messages are returned in the order they have been sent.
@@ -258,7 +254,7 @@ impl TestContext {
}
}
/// Parse a message.
/// Parses a message.
///
/// Parsing a message does not run the entire receive pipeline, but is not without
/// side-effects either. E.g. if the message includes autocrypt headers the relevant
@@ -286,7 +282,7 @@ impl TestContext {
.unwrap();
}
/// Get the most recent message of a chat.
/// Gets the most recent message of a chat.
///
/// Panics on errors or if the most recent message is a marker.
pub async fn get_last_msg_in(&self, chat_id: ChatId) -> Message {
@@ -299,13 +295,17 @@ impl TestContext {
Message::load_from_db(&self.ctx, *msg_id).await.unwrap()
}
/// Get the most recent message over all chats.
/// Gets the most recent message over all chats.
pub async fn get_last_msg(&self) -> Message {
let chats = Chatlist::try_load(&self.ctx, 0, None, None).await.unwrap();
let msg_id = chats.get_msg_id(chats.len() - 1).unwrap();
Message::load_from_db(&self.ctx, msg_id).await.unwrap()
}
/// Creates or returns an existing 1:1 [`Chat`] with another account.
///
/// This first creates a contact using the configured details on the other account, then
/// creates a 1:1 chat with this contact.
pub async fn create_chat(&self, other: &TestContext) -> Chat {
let (contact_id, _modified) = Contact::add_or_lookup(
self,
@@ -324,7 +324,11 @@ impl TestContext {
Chat::load_from_db(self, chat_id).await.unwrap()
}
pub async fn chat_with_contact(&self, name: &str, addr: &str) -> Chat {
/// Creates or returns an existing [`Contact`] and 1:1 [`Chat`] with another email.
///
/// This first creates a contact from the `name` and `addr` and then creates a 1:1 chat
/// with this contact.
pub async fn create_chat_with_contact(&self, name: &str, addr: &str) -> Chat {
let contact = Contact::create(self, name, addr)
.await
.expect("failed to create contact");
@@ -332,6 +336,7 @@ impl TestContext {
Chat::load_from_db(self, chat_id).await.unwrap()
}
/// Retrieves the "self" chat.
pub async fn get_self_chat(&self) -> Chat {
let chat_id = chat::create_by_contact_id(self, DC_CONTACT_ID_SELF)
.await
@@ -339,7 +344,11 @@ impl TestContext {
Chat::load_from_db(self, chat_id).await.unwrap()
}
/// Sends out the text message. If the other side shall receive it, you have to call `recv_msg()` with the returned `SentMessage`.
/// Sends out the text message.
///
/// This is not hooked up to any SMTP-IMAP pipeline, so the other account must call
/// [`TestContext::recv_msg`] with the returned [`SentMessage`] if it wants to receive
/// the message.
pub async fn send_text(&self, chat_id: ChatId, txt: &str) -> SentMessage {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(txt.to_string()));
@@ -348,8 +357,11 @@ impl TestContext {
self.pop_sent_msg().await
}
/// You can use this to debug your test by printing a chat structure
// This code is mainly the same as `log_msglist` in `cmdline.rs`, so one day, we could merge them to a public function in the `deltachat` crate.
/// Prints out the entire chat to stdout.
///
/// You can use this to debug your test by printing the entire chat conversation.
// This code is mainly the same as `log_msglist` in `cmdline.rs`, so one day, we could
// merge them to a public function in the `deltachat` crate.
#[allow(dead_code)]
pub async fn print_chat(&self, chat: &Chat) {
let msglist = chat::get_chat_msgs(&self, chat.get_id(), 0x1, None).await;
@@ -432,7 +444,7 @@ impl SentMessage {
/// This saves CPU cycles by avoiding having to generate a key.
///
/// The keypair was created using the crate::key::tests::gen_key test.
pub(crate) fn alice_keypair() -> key::KeyPair {
pub fn alice_keypair() -> key::KeyPair {
let addr = EmailAddress::new("alice@example.com").unwrap();
let public =
key::SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
@@ -450,7 +462,7 @@ pub(crate) fn alice_keypair() -> key::KeyPair {
/// Load a pre-generated keypair for bob@example.net from disk.
///
/// Like [alice_keypair] but a different key and identity.
pub(crate) fn bob_keypair() -> key::KeyPair {
pub fn bob_keypair() -> key::KeyPair {
let addr = EmailAddress::new("bob@example.net").unwrap();
let public =
key::SignedPublicKey::from_base64(include_str!("../test-data/key/bob-public.asc")).unwrap();