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_std::test]
async fn test_chat_info() { async fn test_chat_info() {
let t = TestContext::new().await; 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(); let info = chat.get_info(&t).await.unwrap();
// Ensure we can serialize this. // Ensure we can serialize this.

View File

@@ -47,7 +47,7 @@ pub struct InnerContext {
pub(crate) blobdir: PathBuf, pub(crate) blobdir: PathBuf,
pub(crate) sql: Sql, pub(crate) sql: Sql,
pub(crate) os_name: Option<String>, pub(crate) os_name: Option<String>,
pub(crate) bob: RwLock<Bob>, pub(crate) bob: Bob,
pub(crate) last_smeared_timestamp: RwLock<i64>, pub(crate) last_smeared_timestamp: RwLock<i64>,
pub(crate) running_state: RwLock<RunningState>, pub(crate) running_state: RwLock<RunningState>,
/// Mutex to avoid generating the key for the user more than once. /// Mutex to avoid generating the key for the user more than once.
@@ -129,7 +129,7 @@ impl Context {
os_name: Some(os_name), os_name: Some(os_name),
running_state: RwLock::new(Default::default()), running_state: RwLock::new(Default::default()),
sql: Sql::new(), sql: Sql::new(),
bob: RwLock::new(Default::default()), bob: Default::default(),
last_smeared_timestamp: RwLock::new(0), last_smeared_timestamp: RwLock::new(0),
generating_key_mutex: Mutex::new(()), generating_key_mutex: Mutex::new(()),
oauth2_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 { pub fn get_event_emitter(&self) -> EventEmitter {
self.events.get_emitter() self.events.get_emitter()
} }

View File

@@ -440,7 +440,6 @@ async fn add_parts(
} }
Err(err) => { Err(err) => {
*hidden = true; *hidden = true;
context.stop_ongoing().await;
warn!(context, "Error in Secure-Join message handling: {}", err); warn!(context, "Error in Secure-Join message handling: {}", err);
return Ok(()); 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)] #[derive(Debug, Clone)]
pub struct EventEmitter(Receiver<Event>); pub struct EventEmitter(Receiver<Event>);
@@ -120,8 +131,9 @@ impl EventType {
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty)] #[derive(Debug, Clone, PartialEq, Eq, EnumProperty)]
pub enum EventType { pub enum EventType {
/// The library-user may write an informational string to the log. /// 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"))] #[strum(props(id = "100"))]
Info(String), Info(String),
@@ -154,14 +166,13 @@ pub enum EventType {
DeletedBlobFile(String), DeletedBlobFile(String),
/// The library-user should write a warning string to the log. /// 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"))] #[strum(props(id = "300"))]
Warning(String), Warning(String),
/// The library-user should report an error to the end-user. /// 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 /// 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. /// should not be disturbed by a dialog or so. Instead, use a bubble or so.

View File

@@ -2125,7 +2125,7 @@ mod tests {
.await .await
.unwrap(); .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); let mut msg = Message::new(Viewtype::Text);
@@ -2141,7 +2141,7 @@ mod tests {
let d = test::TestContext::new().await; let d = test::TestContext::new().await;
let ctx = &d.ctx; 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); let mut msg = Message::new(Viewtype::Text);
@@ -2331,7 +2331,7 @@ mod tests {
.await .await
.unwrap(); .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); let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("Quoted message".to_string())); 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. //! 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::collections::BTreeMap;
use std::fmt; use std::fmt;
@@ -19,7 +19,7 @@ use tempfile::{tempdir, TempDir};
use crate::chat::{self, Chat, ChatId, ChatItem}; use crate::chat::{self, Chat, ChatId, ChatItem};
use crate::chatlist::Chatlist; use crate::chatlist::Chatlist;
use crate::config::Config; 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::contact::{Contact, Origin};
use crate::context::Context; use crate::context::Context;
use crate::dc_receive_imf::dc_receive_imf; 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::mimeparser::MimeMessage;
use crate::param::{Param, Params}; 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 = type EventSink =
dyn Fn(Event) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> + Send + Sync + 'static; dyn Fn(Event) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> + Send + Sync + 'static;
@@ -67,7 +63,7 @@ impl fmt::Debug for TestContext {
} }
impl 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 /// 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 /// [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 /// This is a shortcut which automatically calls [TestContext::configure_alice] after
/// creating the context. /// creating the context.
@@ -130,7 +126,7 @@ impl TestContext {
t t
} }
/// Create a new configured [TestContext]. /// Creates a new configured [TestContext].
/// ///
/// This is a shortcut which configures bob@example.net with a fixed key. /// This is a shortcut which configures bob@example.net with a fixed key.
pub async fn new_bob() -> Self { 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 /// 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. /// 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 /// 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 /// side-effects either. E.g. if the message includes autocrypt headers the relevant
@@ -286,7 +282,7 @@ impl TestContext {
.unwrap(); .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. /// Panics on errors or if the most recent message is a marker.
pub async fn get_last_msg_in(&self, chat_id: ChatId) -> Message { 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() 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 { pub async fn get_last_msg(&self) -> Message {
let chats = Chatlist::try_load(&self.ctx, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&self.ctx, 0, None, None).await.unwrap();
let msg_id = chats.get_msg_id(chats.len() - 1).unwrap(); let msg_id = chats.get_msg_id(chats.len() - 1).unwrap();
Message::load_from_db(&self.ctx, msg_id).await.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 { pub async fn create_chat(&self, other: &TestContext) -> Chat {
let (contact_id, _modified) = Contact::add_or_lookup( let (contact_id, _modified) = Contact::add_or_lookup(
self, self,
@@ -324,7 +324,11 @@ impl TestContext {
Chat::load_from_db(self, chat_id).await.unwrap() 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) let contact = Contact::create(self, name, addr)
.await .await
.expect("failed to create contact"); .expect("failed to create contact");
@@ -332,6 +336,7 @@ impl TestContext {
Chat::load_from_db(self, chat_id).await.unwrap() Chat::load_from_db(self, chat_id).await.unwrap()
} }
/// Retrieves the "self" chat.
pub async fn get_self_chat(&self) -> Chat { pub async fn get_self_chat(&self) -> Chat {
let chat_id = chat::create_by_contact_id(self, DC_CONTACT_ID_SELF) let chat_id = chat::create_by_contact_id(self, DC_CONTACT_ID_SELF)
.await .await
@@ -339,7 +344,11 @@ impl TestContext {
Chat::load_from_db(self, chat_id).await.unwrap() 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 { pub async fn send_text(&self, chat_id: ChatId, txt: &str) -> SentMessage {
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(txt.to_string())); msg.set_text(Some(txt.to_string()));
@@ -348,8 +357,11 @@ impl TestContext {
self.pop_sent_msg().await self.pop_sent_msg().await
} }
/// You can use this to debug your test by printing a chat structure /// Prints out the entire chat to stdout.
// 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. ///
/// 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)] #[allow(dead_code)]
pub async fn print_chat(&self, chat: &Chat) { pub async fn print_chat(&self, chat: &Chat) {
let msglist = chat::get_chat_msgs(&self, chat.get_id(), 0x1, None).await; 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. /// This saves CPU cycles by avoiding having to generate a key.
/// ///
/// The keypair was created using the crate::key::tests::gen_key test. /// 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 addr = EmailAddress::new("alice@example.com").unwrap();
let public = let public =
key::SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc")) 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. /// Load a pre-generated keypair for bob@example.net from disk.
/// ///
/// Like [alice_keypair] but a different key and identity. /// 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 addr = EmailAddress::new("bob@example.net").unwrap();
let public = let public =
key::SignedPublicKey::from_base64(include_str!("../test-data/key/bob-public.asc")).unwrap(); key::SignedPublicKey::from_base64(include_str!("../test-data/key/bob-public.asc")).unwrap();