...
 
Commits (26)
This diff is collapsed.
......@@ -7,6 +7,8 @@ license = "GPL-3.0"
[dependencies]
libqaul = { path = "../../libqaul" }
messaging = { path = "../../libqaul/service/messaging", package = "qaul-messaging" }
ratman = { path = "../../ratman" }
ratman-netmod = { path = "../../ratman/netmod" }
netmod-mem = { path = "../../netmod-mem" }
\ No newline at end of file
use std::sync::Arc;
use {
libqaul::Qaul,
libqaul::{messages::Recipient, Qaul},
messaging::{Messaging, TextPayload},
netmod_mem::MemMod,
ratman::{netmod::Endpoint, Router, Identity},
ratman::{netmod::Endpoint, Router},
};
// This function implements a very simple message send and
// bootstrapping procedure. It is heavily documented to be useful as
// an onboarding device.
fn main() {
// Create virtual network with two devices
let mut mm1 = MemMod::new();
let mut mm2 = MemMod::new();
let mut mm3 = MemMod::new();
let mut mm4 = MemMod::new();
// Link the two devices together
mm1.link(&mut mm2);
mm3.link(&mut mm4);
// Print the sizehint for good measure to see how large a Message
// we can send through this endpoint
println!("mm1.sizehint() = {} bytes", mm1.size_hint());
// Create two routers. These hold routing state and journals and
// Create three routers. These hold routing state and journals and
// are responsible for routing packets through the network
let r1 = Router::new();
let r2 = Router::new();
let r3 = Router::new();
// Add the endpoints to their respective routers
r1.modify().add_ep(mm1);
r2.modify().add_ep(mm2);
r2.modify().add_ep(mm3);
r3.modify().add_ep(mm4);
// Generate two network IDs we will use instead of real user profiles
let id1 = Identity::with_digest(&vec![1]);
let id2 = Identity::with_digest(&vec![2]);
// This step is a hack because we don't have actual discovery
// Messages yet. It will be replaced soon though!
r1.modify().discover(id2.clone());
r1.modify().local(id1.clone());
// While `libqaul` can't add users to the routing scope yet, we
// need to now create Qaul structures so we can create users
let q1 = Arc::new(Qaul::new(r1));
let q2 = Qaul::new(r2);
let q3 = Qaul::new(r3);
// Teach Router 2 about ID1 and it's own local ID
r2.modify().discover(id1.clone());
r2.modify().local(id2.clone());
// Generate two user profiles on node 1 and 3
let u1 = q1.users().create("abc").unwrap();
let u2 = q3.users().create("abc").unwrap();
// Initialise two Qaul instances with their respective Routers.
// At this point it's important that the routers were previously
// initialised. Changes _can_ be made, but only from inside libqaul,
// i.e. via some configuration service NOT
let q1 = Qaul::new(r1);
let q2 = Qaul::new(r2);
// Manually make Routers discover each other
#[allow(deprecated)]
{
q1.router().discover(u2.0, 0);
q2.router().discover(u1.0, 0);
q2.router().discover(u2.0, 1);
q3.router().discover(u1.0, 0);
}
// Send a test message from id1 to id2 that says "hello world"
q1.send_test_message(id1, id2);
// We could now send a Service API message. But let's keep going
// up the stack. We initialise the `Messaging` service now.
let msg = Messaging::new(Arc::clone(&q1));
msg.send(
u1,
Recipient::User(u2.0),
TextPayload {
text: "Hello, world!".into(),
},
)
.unwrap();
// This delay is required to make the main thread wait enough time
// for the exchange to complete. In a real app this is not a
......
......@@ -7,15 +7,20 @@ license = "AGPL-3.0"
edition = "2018"
[dependencies]
platform = { path = "platform", package = "qaul-platform" }
identity = { path = "../ratman/identity", package = "ratman-identity", features = ["digest"] }
alexandria = { git = "https://git.open-communication.net/qaul/alexandria" }
platform = { path = "platform", package = "qaul-platform" }
ratman = { path = "../ratman" }
async-std = "0.99"
rand = "0.7"
alexandria = { git = "https://git.open-communication.net/qaul/alexandria" }
conjoiner = { git = "https://git.open-communication.net/qaul/conjoiner-engine", package = "conjoiner-engine" }
serde = { version = "1.0", features = [ "derive" ] }
base64 = "0.10"
blake2 = "0.8.0"
mime = "0.3"
rand = "0.7"
[features]
default = ["generate-message"]
generate-message = []
......@@ -50,7 +50,7 @@ impl AuthError {
fn into_error(&self) -> (Error, Status) {
let status = match self {
AuthError::QaulError(QaulError::NotAuthorised) => Status::Unauthorized,
AuthError::QaulError(QaulError::UnknownUser) => Status::NotFound,
AuthError::QaulError(QaulError::NoUser) => Status::NotFound,
AuthError::QaulError(QaulError::CallbackTimeout) => Status::InternalServerError,
AuthError::NotLoggedIn => Status::Unauthorized,
_ => Status::BadRequest,
......@@ -63,7 +63,7 @@ impl AuthError {
AuthError::NoAttributes => Some("No Attributes".into()),
AuthError::InvalidIdentity(_) => Some("Invalid identity".into()),
AuthError::QaulError(QaulError::NotAuthorised) => Some("Not Authorized".into()),
AuthError::QaulError(QaulError::UnknownUser) => Some("Unknown User".into()),
AuthError::QaulError(QaulError::NoUser) => Some("Unknown User".into()),
AuthError::QaulError(QaulError::InvalidQuery) => Some("Invalid Query".into()),
AuthError::QaulError(QaulError::InvalidPayload) => Some("Invalid Payload".into()),
AuthError::QaulError(QaulError::CallbackTimeout) => None,
......@@ -85,7 +85,7 @@ impl AuthError {
Some(format!("Failed to decode identity, decoded identity is {} bytes long when it should be {}", l, ID_LEN)),
AuthError::QaulError(QaulError::NotAuthorised) =>
Some("Current user is not authorised to perform this action".into()),
AuthError::QaulError(QaulError::UnknownUser) =>
AuthError::QaulError(QaulError::NoUser) =>
Some("Target user is not known to Qaul".into()),
AuthError::QaulError(QaulError::InvalidQuery) => None,
AuthError::QaulError(QaulError::InvalidPayload) =>
......
......@@ -7,3 +7,4 @@
//! Depending on how important this is, this crate might either be
//! a shim crate, or be removed again in the future because it's redundant.
#![allow(unused)]
//! `qaul.net` filesharing service
// //! `qaul.net` filesharing service
// #![allow(unused)]
use qaul::{Qaul, QaulResult, UserAuth};
use identity::Identity;
pub use mime::Mime;
// use qaul::{Qaul, QaulResult, UserAuth};
// use identity::Identity;
// pub use mime::Mime;
/// A typed file that can be sent across the network
pub struct File {
pub name: String,
pub mime: Mime,
pub data: Vec<u8>,
}
// /// A typed file that can be sent across the network
// pub struct File {
// pub name: String,
// pub mime: Mime,
// pub data: Vec<u8>,
// }
// TODO: Partial files/ file progress
// TODO: Download links with tokens
// // TODO: Partial files/ file progress
// // TODO: Download links with tokens
/// Filesharing service state
pub struct Filesharing<'q> {
qaul: &'q Qaul,
}
// /// Filesharing service state
// pub struct Filesharing<'q> {
// qaul: &'q Qaul,
// }
impl<'q> Filesharing<'q> {
/// Send a single file to a group of people
pub fn send_file(
&self,
user: UserAuth,
recipients: Vec<Identity>,
file: File,
) -> QaulResult<()> {
unimplemented!()
}
// impl<'q> Filesharing<'q> {
// /// Send a single file to a group of people
// pub fn send_file(
// &self,
// user: UserAuth,
// recipients: Vec<Identity>,
// file: File,
// ) -> QaulResult<()> {
// unimplemented!()
// }
/// Get all files that were received since the last poll
pub fn poll_files(&self, user: UserAuth) -> QaulResult<Vec<File>> {
unimplemented!()
}
}
// /// Get all files that were received since the last poll
// pub fn poll_files(&self, user: UserAuth) -> QaulResult<Vec<File>> {
// unimplemented!()
// }
// }
......@@ -8,6 +8,6 @@ license = "AGPL-3.0"
[dependencies]
qaul = { path = "../../", package = "libqaul" }
files = { path = "../files", package = "qaul-files" }
identity = { path = "../../../ratman/identity", package = "ratman-identity" }
serde = { version = "1.0", features = ["derive"] }
\ No newline at end of file
serde = { version = "1.0", features = ["derive"] }
conjoiner = { git = "https://git.open-communication.net/qaul/conjoiner-engine", package = "conjoiner-engine" }
......@@ -6,22 +6,60 @@
//! It's basically decentralised e-mail.
//! It's basically e-mail.
use files::File;
use identity::Identity;
use qaul::{Qaul, QaulResult, Recipient, UserAuth};
use serde::{Serialize, Deserialize};
use std::sync::Arc;
use conjoiner;
use qaul::{
error::{Error, Result},
messages::{Message, Recipient, SigTrust},
users::UserAuth,
Identity, Qaul,
};
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, sync::Arc};
const ASC_NAME: &'static str = "qaul-messaging";
/// A list of file-attachments
pub type Attachments = Vec<File>;
// A list of file-attachments
// pub type Attachments = Vec<File>;
/// A plain-text message with optional attachments
#[derive(Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextPayload {
pub text: String,
}
/// An incoming text message, with optional file attachments
///
/// This abstraction is a simplification of the `libqaul` internal
/// `Message` abstraction, and is not stored internally in this
/// form. Generally, when querying from the service API, the return
/// value needs to be mapped into this type.
pub struct TextMessage {
text: String,
// attachments: Option<Attachments>,
pub sender: Identity,
pub recipient: Recipient,
pub sign: SigTrust,
pub payload: TextPayload,
}
impl TryFrom<Message> for TextMessage {
type Error = Error;
/// Map from a `Message::In` into a `TextMessage`
fn try_from(msg: Message) -> Result<Self> {
match msg {
Message::In {
sender,
recipient,
sign,
payload,
} => Ok(Self {
sender,
recipient,
sign,
payload: conjoiner::deserialise(&payload)?,
}),
Message::Out { .. } => Err(Error::InvalidPayload),
}
}
}
/// Messaging service state
......@@ -39,7 +77,7 @@ impl Messaging {
/// but also check for the existence of a `filesharing` service.
/// Depending on this check, the `async_files` capability
/// can be set.
pub fn init(qaul: Arc<Qaul>) -> Self {
pub fn new(qaul: Arc<Qaul>) -> Self {
Self {
async_files: false,
qaul,
......@@ -57,21 +95,33 @@ impl Messaging {
/// `Message`, signs it and optionally encrypts it, if it's
/// `recipient` isn't `Recipient::Flood`, then queues it in the
/// routing layer.
pub fn send(&self, user: UserAuth, recipient: Recipient, msg: TextMessage) -> QaulResult<()> {
// self.qaul.message_send(user, recipient, ASC_NAME,
unimplemented!()
pub fn send(&self, user: UserAuth, recipient: Recipient, payload: TextPayload) -> Result<()> {
self.qaul.messages().send(
user,
recipient,
ASC_NAME,
conjoiner::serialise(&payload)?,
)
}
/// Non-blockingly poll for new `TextMessage`s for a session
pub fn poll(&self, user: UserAuth) -> QaulResult<Vec<TextMessage>> {
unimplemented!()
pub fn poll(&self, user: UserAuth) -> Result<TextMessage> {
self.qaul
.messages()
.poll(user, ASC_NAME)
.map(|msg| TextMessage::try_from(msg))?
}
/// Setup a `TextMessage` listener for a specific user session
pub fn listen<F>(&self, user: UserAuth, listener: F) -> QaulResult<()>
pub fn listen<F: 'static>(&self, user: UserAuth, listener: F) -> Result<()>
where
F: Fn(TextMessage) -> QaulResult<()>,
F: Fn(TextMessage) -> Result<()>,
{
unimplemented!()
self.qaul
.messages()
.listen(user, ASC_NAME, move |msg| match TextMessage::try_from(msg) {
Ok(text) => listener(text),
Err(e) => return Err(e),
})
}
}
//! Service API contact book
//!
//! A user can store information about other users and query this
//! information later. The contact book is zoned per-user. Ideally it
//! would be encrypted (not for now).
use super::models::{QaulResult, UserAuth};
use crate::{ContactData, ContactList, ContactQuery, ContactStore, Qaul, UserProfile};
impl Qaul {
/// Add a new contact to a user's known contacts
pub fn contacts_add(&self, user: UserAuth, contact: UserProfile) -> QaulResult<()> {
let (ref my_id, ref token) = user.trusted()?;
self.auth.verify_token(my_id, token)?;
self.contacts.modify(&my_id, &contact.id, |mut c| {});
use crate::{
error::Result,
users::{UserAuth, UserProfile},
Identity, Qaul,
};
/// A user-local set of contact metadata in their contact book
///
/// All fields in this structure are entirely optional and can not be
/// relied on. They are additional points of data, that a user can
/// specify about another user, that are not available or shared with
/// the network. This is meant to allow users to curate a list of
/// trusted contacts, or build friend circles.
#[derive(Default, Debug, Clone)]
pub struct ContactEntry {
/// The name by which the associated contact is known by the owning user.
pub nick: Option<String>,
/// Set a user trust level
pub trust: i8,
/// The user has met this person
pub met: bool,
/// A free text location
pub location: Option<String>,
/// A general plain text notes section
pub notes: Option<String>,
}
/// Query structure to find contacts by
///
/// A query is always applied to a field that is present in
/// `ContactEntry`, and will filter contacts by what set of
/// prerequisites they fulfill.
pub enum ContactQuery<'a> {
/// A fuzzy nickname search
Nick(&'a str),
/// A fuzzy trust level search
Trust { val: i8, fuz: i8 },
/// Filter by physical meeting
Met(bool),
/// A fuzzy location string search
Location(&'a str),
/// A fuzzy notes string search
Notes(&'a str),
}
/// API scope type to access contact book functions
///
/// The contact book is a user local store of metadata, that can be
/// assigned for each `Identity`, that a user is aware of on the
/// network. A contact entry is backed by an entry in the user store,
/// available via the `users()` endpoint scope.
///
/// A `ContactEntry` should be considered additional information a
/// user can keep on someone they interact with on the network,
/// independent of the `UserProfile`, which is fetched from the remote
/// user themselves.
///
/// When assembling a complete view of a user, it's important to
/// consider both their primary profile, as well as the contact
/// metadata stored via this API. Because of this, queries only return
/// the `ContactEntry` structures, not the profile itself.
///
/// Furthermore, it is possible to query users via metadata set in a
/// user's local contact book, such as their nick, trust, location and
/// more.
pub struct Contacts<'chain> {
pub(crate) q: &'chain Qaul,
}
impl<'qaul> Contacts<'qaul> {
/// Drop this scope and return back to global `Qaul` scope
pub fn drop(&'qaul self) -> &'qaul Qaul {
self.q
}
/// Modify a user's contact entry in a user-local contact book
///
/// The `modify` lambda allows a user to add personal metadata for
/// a contact, such as a nickname, or trust levels. Each contact
/// list is user local and it's not possible to access other
/// user's contact metadata.
///
/// If no contact entry existed before, a fresh one will be
/// created before calling the passed-in lambda.
pub fn modify<F>(&self, user: UserAuth, contact: &Identity, modify: F) -> Result<()>
where
F: Fn(&mut ContactEntry),
{
let (ref id, _) = self.q.auth.trusted(user)?;
self.q.contacts.modify(id, contact, modify);
Ok(())
}
/// Find a subset of contacts with some query
pub fn contacts_query(
&self,
user: UserAuth,
query: ContactQuery,
) -> QaulResult<Vec<UserProfile>> {
let (ref my_id, ref token) = user.trusted()?;
self.auth.verify_token(my_id, token)?;
self.contacts
.query(my_id, query)?
.into_iter()
.map(|ref id| self.users.get(id))
.collect()
/// Get a single `ContactEntry` from a user's contact book
///
/// Considering that a `ContactEntry` might contain large amounts
/// of data, this is the only way to return a reference to the
/// full object. When trying to query all data from all contact
/// entries, it's advised to get a list of Identities via
/// `Contacts::get_all` first, and then map this collection over
/// `Contacts::get` afterwards.
pub fn get(&self, user: UserAuth, contact: &Identity) -> Result<ContactEntry> {
let (ref id, _) = self.q.auth.trusted(user)?;
self.q.contacts.get(id, contact)
}
/// Enumerate all contacts known by a user
pub fn contacts_get_all(&self, user: UserAuth) -> QaulResult<Vec<UserProfile>> {
let (ref my_id, ref token) = user.trusted()?;
self.auth.verify_token(my_id, token)?;
/// Query for a subset of users that have a `ContactEntry`
///
/// To get a list of all `ContactEntry` objects, map the result of
/// this function over `Contacts::get`.
///
/// ```rust
/// # use libqaul::{Qaul, error::Result, contacts::ContactQuery};
/// # let qaul = Qaul::dummy();
/// # let user = qaul.users().create("abc").unwrap();
/// let contacts = qaul.contacts();
/// # (|| -> Result<()> {
/// contacts
/// .query(user.clone(), ContactQuery::Nick("buddy"))?
/// .into_iter()
/// .map(|i| contacts.get(user.clone(), &i));
/// # Ok(())
/// # })().unwrap();
/// ````
pub fn query(&self, user: UserAuth, query: ContactQuery) -> Result<Vec<Identity>> {
let (ref id, _) = self.q.auth.trusted(user)?;
self.q.contacts.query(id, query)
}
self.contacts
.get_all(my_id)?
.into_iter()
.map(|ref id| self.users.get(id))
.collect()
/// Get all users that have a `ContactEntry` for this user
pub fn all(&self, user: UserAuth) -> Result<Vec<Identity>> {
let (ref id, _) = self.q.auth.trusted(user)?;
Ok(vec![])
}
}
// impl Qaul {
// /// Find a subset of contacts with some query
// pub fn contacts_query(
// &self,
// user: UserAuth,
// query: ContactQuery,
// ) -> QaulResult<Vec<UserProfile>> {
// let (ref my_id, ref token) = user.trusted()?;
// self.auth.verify_token(my_id, token)?;
// self.contacts
// .query(my_id, query)?
// .into_iter()
// .map(|ref id| self.users.get(id))
// .collect()
// }
// /// Enumerate all contacts known by a user
// pub fn contacts_get_all(&self, user: UserAuth) -> QaulResult<Vec<UserProfile>> {
// let (ref my_id, ref token) = user.trusted()?;
// self.auth.verify_token(my_id, token)?;
// self.contacts
// .get_all(my_id)?
// .into_iter()
// .map(|ref id| self.users.get(id))
// .collect()
// }
// }
//! Service API keystore access
//!
//! Provide simple read-only access to a users own keystore. Keys are
//! gathered and managed automatically. This endpoint is primarily
//! meant for service UIs to indicate to users whether or not
//! encrypted communication with another user is possible.
use super::UserAuth;
use crate::{Qaul, QaulResult};
impl Qaul {
/// Check if another user's public key is available
pub fn keystore_key_exists(user: UserAuth, id: &str) -> QaulResult<bool> {
Ok(false)
}
}
//! Service API: message store
use super::UserAuth;
use crate::{api::Message, Qaul, QaulResult};
impl Qaul {
/// A temporary endpoint designed to get all messages from a user
///
/// After passing authentication, it always returns the same list
/// of messages
#[deprecated]
pub fn store_all(&self, user: UserAuth) -> QaulResult<Vec<Message>> {
let (ref id, ref token) = user.trusted()?;
self.auth.verify_token(id, token)?;
Ok(vec![])
}
}
This diff is collapsed.
//! # `libqaul` service API
//! # Service API scope providers
//!
//! The idea behind this interface is further documented in the
//! `contribute` book. It goes into detail about using it to write
......@@ -9,23 +9,19 @@
//! "core" service. All of them are implemented via this API,
//! allowing external developers to write their own services using
//! qaul.net libraries and networks.
//!
//! ## Models
//!
//! Models defined in this submodule are different from any other
//! models defined in `libqaul`: they are the public representations,
//! i.e. only fields that are relevant for service developers to
//! interact with, not including shared service state or secrets.
// #[cfg(test)]
// mod tests;
// mod files;
pub use contacts::Contacts;
pub(crate) mod contacts;
pub use messages::Messages;
pub(crate) mod messages;
mod contacts;
mod files;
mod message_store;
mod messages;
mod models;
mod service;
mod users;
pub(crate) mod services;
pub use services::Services;
pub use models::*;
pub use users::Users;
pub(crate) mod users;
//! Service API exchange models
use std::fmt::{self, Debug, Formatter};
use identity::Identity;
use ratman::Identity;
use mime::Mime;
/// Convenience type for API functions
pub type QaulResult<T> = Result<T, QaulError>;
/// Service API error wrapper
#[derive(Debug, Clone, PartialEq)]
pub enum QaulError {
/// Not authorised to perform this action
NotAuthorised,
/// The desired user was not known
UnknownUser,
/// Invalid search query
InvalidQuery,
/// Invalid payload (probably too big)
InvalidPayload,
/// A function callback timed out
CallbackTimeout,
}
// /// A security token to authenticate sessions
// #[derive(Clone, PartialEq, Eq)]
// pub struct Token(String);
// impl Debug for Token {
// fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// write!(f, "<TOKEN>")
// }
// }
// impl From<String> for Token {
// fn from(s: String) -> Self {
// assert!(s.len() == 64);
// Token(s)
// }
// }
pub type Token = String;
/// A wrapper around user authentication state
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum UserAuth {
/// A user ID which has not been verified
Untrusted(Identity),
/// The user ID of the currently logged-in user
Trusted(Identity, Token),
}
impl UserAuth {
/// Returns an error if the UserAuth isn't Trusted.
pub fn trusted(self) -> QaulResult<(Identity, Token)> {
match self {
UserAuth::Trusted(id, s) => Ok((id, s)),
UserAuth::Untrusted(_) => Err(QaulError::NotAuthorised),
}
}
/// Returns the interior identity, regardless of trust status.
pub fn identity(self) -> Identity {
match self {
UserAuth::Trusted(id, _) => id,
UserAuth::Untrusted(id) => id,
}
}
/// Returns the interior identity as an `Untrusted`, regardless of trust status.
pub fn as_untrusted(&self) -> Self {
UserAuth::Untrusted(self.clone().identity())
}
}
/// Signature trust information embedded into service messages
pub enum SigTrust {
/// A verified signature by a known contact
Trusted(Identity),
/// An unverified signature by a known contact
/// (pubkey not available!)
Unverified(Identity),
/// A fraudulent signature
Invalid,
}
/// A service message
///
/// Differs from the `RATMAN` abstraction for messages
/// because it's signature has already been verified.
/// Instead of delivering the raw signature to a service,
/// this message only embeds validity information.
///
/// This makes it easier for service authors to trust
/// data provided by `libqaul`, without having to do
/// calls into some crypto library themselves.
///
/// In comparison to the `RATMAN` message, the `associator`
/// has also been removed because at this stage, only the
/// relevant related service is being handed a message anyway.
pub struct Message {
pub sender: Identity,
pub recipient: Recipient,
pub payload: Vec<u8>,
pub signature: SigTrust,
}
/// Service message recipient
///
/// A recipient is either a single user or the entire network. The
/// "flood" mechanic is passed through to `RATMAN`, which might
/// implement this in the networking module, or emulate
/// it. Performance may vary.
pub enum Recipient {
/// A single user, known to this node
User(Identity),
/// A collection of users, sometimes called a Group
Group(Vec<Identity>),
/// Addressed to nobody, flooded into the network
Flood,
}
/// Local file abstraction
pub struct File {
pub name: String,
......
//! Service interconnect interface
use super::models::Message;
use std::error::Error;
/// The interface through which communication with a service occurs
pub trait ServiceConnect<E: Error> {
/// The ID of the service this ServiceConnect is connected to
fn service_id(&self) -> String;
/// Send a Message to the service. Calls to this method must not block.
fn send_msg(&mut self) -> Result<(), E>;
/// Check for Messages received from the service. Calls to this message may block,
/// but not if the last call to poll_messages() returned true (if ServiceConnectAsync
/// is implemented for this type).
fn messages(&mut self) -> Result<Vec<Message>, E>;
}
/// Add-on interface for async communication with a service
pub trait ServiceConnectAsync<E: Error>: ServiceConnect<E> {
/// Check whether or not the next call to messages() will block.
fn poll_messages(&mut self) -> Result<bool, E>;
}
//! Service interconnect interface
use crate::{error::Result, messages::Message, Qaul};
/// API scope type to access service management functions
///
/// Used entirely to namespace API endpoints on `Qaul` instance,
/// without having long type identifiers.
pub struct Services<'chain> {
pub(crate) q: &'chain Qaul,
}
impl<'qaul> Services<'qaul> {
/// Drop this scope and return back to global `Qaul` scope
pub fn drop(&'qaul self) -> &'qaul Qaul {
self.q
}
/// Add an external service to the qaul service registry
///
/// Registering a service means that future `Message` listeners
/// can be allocated for this service, as well as enabling polling.
///
/// Names of services need to be unique, so it's advised to
/// namespace them on some other key, for example the application
/// package name (such as `com.example.myapp`)
pub fn register<S: Into<String>>(&self, name: S) -> Result<()> {
self.q.services.register(name.into())
}
/// Remove an external service from the qaul service registry
///
/// Calling this function will disable the ability to poll for
/// messages, as well as deleting all already registered message
/// listeners already existing for this service.
///
/// Will return `Error::NoService` if no such service name could
/// be found.
pub fn unregister<S: Into<String>>(&self, name: S) -> Result<()> {
self.q.services.unregister(name.into())
}
}
//! libqaul API tests
//!
//! Run some simple tests against the service API to check that users
//! can be created, modified, deleted, etc
use crate::*;
#[test]
fn user_lifecycle() {
let qaul = Qaul::start();
// can we make a user?
let auth = qaul.user_create("password").unwrap();
// does something get added to the correct parts of state?
{
assert_eq!(qaul.auth.lock().unwrap().len(), 1);
assert_eq!(qaul.keys.lock().unwrap().len(), 1);
assert_eq!(qaul.users.lock().unwrap().len(), 1);
}
// are we trusted?
let (id, key) = qaul.user_authenticate(auth.clone()).unwrap();
// can we login as that user?
let auth2 = qaul.user_login(id.clone(), "password").unwrap();
// is the state updated appropriately?
{
assert_eq!(qaul.auth.lock().unwrap().len(), 1);
assert_eq!(qaul.keys.lock().unwrap().len(), 2);
assert_eq!(qaul.users.lock().unwrap().len(), 1);
}
// are we trusted?
let (id2, key2) = qaul.user_authenticate(auth2.clone()).unwrap();
// do we get back the same id?
assert_eq!(id.clone(), id2);
// do we get a different key?
assert_ne!(key.clone(), key2);
// can we log out?
qaul.user_logout(auth2.clone()).unwrap();
// is the state updated appropriately
{
assert_eq!(qaul.auth.lock().unwrap().len(), 1);
assert_eq!(qaul.keys.lock().unwrap().len(), 1);
assert_eq!(qaul.users.lock().unwrap().len(), 1);
}
// are we not trusted now?
assert!(qaul.user_authenticate(auth2).is_err());
// does logging in with the wrong password actually error?
assert!(qaul.user_login(id.clone(), "not password").is_err());
}
//! Service API: user endpoints
use super::models::{QaulError, QaulResult, UserAuth};
use crate::{
auth::{AuthStore, PwHash},
qaul::Qaul,
users::{User, UserProfile, UserUpdate},
utils, Identity,
error::Result,
users::{User, UserProfile},
utils, Identity, Qaul,
};
impl Qaul {
/// Create a new fouser
///
/// Generates a new `Identity` and takes a passphrase that is used to encrypt
pub fn user_create(&self, pw: &str) -> QaulResult<UserAuth> {
// FIXME: Generate ID from pubkey
let id = Identity::truncate(&utils::random(16));
let user = User::Local(UserProfile::new(id.clone()));
/// A random authentication token
pub type Token = String;
/// Wrapper to encode `User` authentication state
///
/// This structure can be aquired by challenging an authentication
/// endpoint, such as `User::login` to yield a token. If a session for
/// this `Identity` already exists, it will be re-used.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UserAuth(pub Identity, pub(crate) Token);
/// API scope type to access user functions
///
/// Used entirely to namespace API endpoints on `Qaul` instance,
/// without having long type identifiers.
pub struct Users<'chain> {
pub(crate) q: &'chain Qaul,
}
self.users.add_user(user);
self.auth.set_pw(id.clone(), pw);
self.auth
.new_login(id, pw)
.map(|token| UserAuth::Trusted(id, token))
impl<'qaul> Users<'qaul> {
/// Drop this scope and return back to global `Qaul` scope
pub fn drop(&'qaul self) -> &'qaul Qaul {
self.q
}
/// Get a list of all available local users
pub fn user_local_users(&self) -> Vec<UserProfile> {
self.users.get_local()
/// Enumerate locally registered users available
///
/// No information about sessions or existing login state is
/// stored or accessible via this API.
pub fn list(&self) -> Vec<UserProfile> {
self.q.users.get_local()
}
/// Inject a `UserAuth` into this `Qaul`.
/// Create a new user and authenticated session
///
/// This is not, in general, a sensible thing for regular
/// applications to do, but is necessary for testing.
/// The specified password `pw` is used to encrypt the user's
/// private key and message stores and should be kept safe from
/// potential attackers.
///
/// ## Note
///
/// In it's current form, this function can not be implemented
/// with the new `AuthStore` backend, because it doesn't map
/// tokens to users, but the other way around. The code is still
/// in the repo, albeit commented out. We should check what this
/// function should actually do and if it can be aproximated
/// better.
///
/// # Panics
/// Panics if the provided `UserAuth` describes a user that is already known to this
/// `Qaul` instance.
/// Panics if the provided `UserAuth` users a key that is already known to this
/// `Qaul` instance.
pub fn user_inject(&self, _user: UserAuth) -> QaulResult<UserAuth> {
// let (id, key) = user.trusted()?;
// let mut user = User::new();
// user.id = id;
// let mut users = self.users.lock().unwrap();
// if users.contains_key(&id) {
// panic!("The user {:?} already exists within the Qaul state.", id);
// }
// let mut keys = self.keys.lock().unwrap();
// if keys.contains_key(&key) {
// panic!("The key {:?} already exists within the Qaul state.", key);
// }
// users.insert(id.clone(), user);
// keys.insert(key.clone(), id.clone());
// Ok(UserAuth::Trusted(id, key))
unimplemented!()
}
/// Update an existing (logged-in) user to use the given details.
pub fn user_update(&self, user: UserAuth, update: UserUpdate) -> QaulResult<UserProfile> {
// let (user_id, _) = self.user_authenticate(user)?;
// let mut users = self.users.lock().unwrap();
// let user = match users.get_mut(&user_id) {
// Some(v) => v,
// None => {
// return Err(QaulError::UnknownUser);
// }
// };
// update.apply_to(&mut user.data);
/// It's mandatory to choose a password here, however it is
/// possible for a frontend to choose a random sequence _for_ a
/// user, instead of leaving files completely unencrypted. In this
/// case, there's no real security, but a drive-by will still only
/// grab encrypted files.
pub fn create(&self, pw: &str) -> Result<UserAuth> {
let id = Identity::truncate(&utils::random(16));
let user = User::Local(UserProfile::new(id));
// Ok(user.clone())
unimplemented!()
}
// Inform Router about new local user
self.q.router.local(id);
/// Get information for any user
pub fn user_get(&self, user: UserAuth) -> QaulResult<UserProfile> {
unimplemented!()
// let user_id = user.identity();
// let users = self.users.lock().unwrap();
// match users.get(&user_id) {
// Some(user) => Ok(user.clone()),
// None => Err(QaulError::UnknownUser),
// }
self.q.users.add_user(user);
self.q.auth.set_pw(id, pw);
self.q.auth.new_login(id, pw).map(|t| UserAuth(id, t))
}
/// Delete the currently logged-in user
pub fn user_delete(&self, user: UserAuth) -> QaulResult<()> {
unimplemented!()
// let (user_id, _) = self.user_authenticate(user)?;
// let mut users = self.users.lock().unwrap();
// if !users.contains_key(&user_id) {
// return Err(QaulError::UnknownUser);
// }
// users.remove(&user_id);
// Ok(())
/// Change the passphrase for an authenticated user
pub fn change_pw(&self, user: UserAuth, newpw: &str) -> Result<()> {
let (id, _) = self.q.auth.trusted(user)?;
self.q.auth.set_pw(id, newpw);
Ok(())
}
/// Log-in to an existing user
pub fn user_login(&self, id: Identity, pw: &str) -> QaulResult<UserAuth> {
unimplemented!()
// let token = self.auth.new_login(id, pw)?;
// Ok(UserAuth::Trusted(id, token))
/// Create a new session login for a local User
pub fn login(&self, user: Identity, pw: &str) -> Result<UserAuth> {
let token = self.q.auth.new_login(user, pw)?;
Ok(UserAuth(user, token))
}
/// End a currently active user session
pub fn user_logout(&self, user: UserAuth) -> QaulResult<()> {
unimplemented!()
// let (id, token) = self.user_authenticate(user)?;
// self.auth.logout(&id, &token)
/// Drop the current session Token, invalidating it
pub fn logout(&self, user: UserAuth) -> Result<()> {
let (ref id, ref token) = self.q.auth.trusted(user)?;
self.q.auth.logout(id, token)
}
/// Change a currently logged in user's password
pub fn user_change_pw(&self, user: UserAuth, pw: &str) -> QaulResult<()> {
let (id, _) = user.trusted()?;
self.auth.set_pw(id, pw);
Ok(())
/// Fetch the `UserProfile` for a known identity, remote or local
///
/// No athentication is required for this endpoint, seeing as only
/// public information is exposed via the `UserProfile`
/// abstraction anyway.
pub fn get(&self, user: Identity) -> Result<UserProfile> {
self.q.users.get(&user)
}
}
......@@ -6,7 +6,12 @@
mod pwhash;
pub(crate) use pwhash::PwHash;
use crate::{utils, DataStore, Identity, Persisted, QaulError, QaulResult, Token};
use crate::{
error::{Error, Result},
users::{Token, UserAuth},
utils, Identity,
};
use base64::{encode_config, URL_SAFE};
use std::{
collections::BTreeMap,
......@@ -40,30 +45,37 @@ impl AuthStore {
.insert(user, PwHash::new(pw));
}
/// `UserAuth` convenience wrapper for `AuthStore::verify_token`
pub(crate) fn trusted(&self, user: UserAuth) -> Result<(Identity, Token)> {
let UserAuth(id, token) = user;
self.verify_token(&id, &token)?;
Ok((id, token))
}
/// Generate a new login token, if password is valid
///
/// If a token already exists, and the password is valid, it will
/// be returned instead of generating a new one.
pub(crate) fn new_login(&self, user: Identity, pw: &str) -> QaulResult<Token> {
pub(crate) fn new_login(&self, user: Identity, pw: &str) -> Result<Token> {
self.hashes
.lock()
.expect("Failed to unlock hash store")
.get(&user)
.filter(|hash| hash.matches_with(pw))
.map_or(Err(QaulError::UnknownUser), |_| Ok(()))?;
.map_or(Err(Error::NoUser), |_| Ok(()))?;
let mut tokens = self.tokens.lock().expect("Failed to lock token store!");
let token = tokens
.get(&user)
.cloned()
.map_or_else(|| Ok(Self::generate()), |t| Ok(t))?;
.map_or_else(|| Self::generate(), |t| t);
tokens.insert(user, token.clone());
Ok(token)
}
/// Yield a token for a session, logging out a user
pub(crate) fn logout(&self, user: &Identity, token: &Token) -> QaulResult<()> {
pub(crate) fn logout(&self, user: &Identity, token: &Token) -> Result<()> {
match self
.tokens
.lock()
......@@ -71,19 +83,18 @@ impl AuthStore {
.remove(user)
{
Some(ref t) if t == token => Ok(()),
Some(_) | None => Err(QaulError::NotAuthorised),
Some(_) | None => Err(Error::NotAuthorised),
}
}
/// Verify that a user's token is valid
pub(crate) fn verify_token(&self, user: &Identity, token: &Token) -> QaulResult<()> {
pub(crate) fn verify_token(&self, user: &Identity, token: &Token) -> Result<()> {
self.tokens
.lock()
.expect("Failed to lock token store")
.get(user)
.map(|t| t == token)
.map_or(Err(QaulError::NotAuthorised), |_| Ok(()))?;
.map_or(Err(Error::NotAuthorised), |_| Ok(()))?;
Ok(())
}
......
......@@ -5,7 +5,7 @@
use blake2::{Blake2b, Digest as _};
use rand::prelude::*;
use std::fmt::{self, Debug, Formatter, Result};
use std::fmt::{self, Debug, Formatter};
/// A wrapper around a salted password hash
pub(crate) struct PwHash {
......
......@@ -5,78 +5,91 @@
//! - lists of users they know about, by identity, plus
//! - local-only information about those users, like personal nicknames
use crate::{Identity, QaulError, QaulResult};
// Public exports
pub use crate::api::contacts::{ContactEntry, ContactQuery};
use crate::{
error::{Error, Result},
Identity,
};
use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
};
/// A collection of contacts associated with their local-only data.
pub(crate) type ContactList = BTreeMap<Identity, ContactEntry>;
/// Wraps around user-local contact books
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug)]
pub(crate) struct ContactStore {
inner: Arc<Mutex<BTreeMap<Identity, ContactList>>>,
}
/// A collection of contacts associated with their local-only data.
pub(crate) type ContactList = BTreeMap<Identity, ContactData>;
/// Query structure to find contacts by
pub enum ContactQuery<'a> {
/// A fuzzy nickname search
Nick(&'a str),
/// A fuzzy trust level search
Trust { val: i8, fuz: i8 },
}
/// Data about a contact that is relevant only from a single user's perspective.
#[derive(Default, Debug)]
pub struct ContactData {
/// The name by which the associated contact is known by the owning user.
nick: Option<String>,
/// Set a user trust level
trust: i8,
}
impl ContactStore {
pub(crate) fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(BTreeMap::new())),
}
}
/// Modify a users personal contact entry via a callback
///
/// `id` in this case is the current session user, `user` is the
/// contact entry they want to modify. If none previously existed,
/// a fresh one will be created.
pub(crate) fn modify<F>(&self, id: &Identity, user: &Identity, modify: F) -> QaulResult<()>
/// contact entry they want to modify. **If none previously
/// existed, a fresh one will be created.**
pub(crate) fn modify<F>(&self, id: &Identity, user: &Identity, modify: F)
where
F: Fn(&mut ContactData),
F: Fn(&mut ContactEntry),
{
let mut inner = self.inner.lock().expect("Failed to lock ContactStore");
let mut contact = inner
.entry(id.clone())
let contact = inner
.entry(*id)
.or_insert(Default::default())
.get_mut(user)
.unwrap();
.entry(*user)
.or_insert(Default::default());
modify(contact);
Ok(())
}
pub(crate) fn query(&self, id: &Identity, query: ContactQuery) -> QaulResult<Vec<Identity>> {
/// Query a user's contact book data
///
/// If this user hasn't yet specified any contact entries, then
/// just return an empty list instead.
pub(crate) fn query(&self, id: &Identity, query: ContactQuery) -> Result<Vec<Identity>> {
let mut inner = self.inner.lock().expect("Failed to lock ContactStore");
Ok(inner
.get(id)
.map_or(Err(QaulError::UnknownUser), |x| Ok(x))?
.entry(*id)
.or_insert(Default::default())
.iter()
.filter(|(_, con)| match query {
ContactQuery::Nick(nick) if con.nick.is_none() => false,
ContactQuery::Nick(_) if con.nick.is_none() => false,
ContactQuery::Nick(nick) => con.nick.as_ref().unwrap().contains(nick),
ContactQuery::Trust { val, fuz } => con.trust + fuz < val || con.trust - fuz > val,
ContactQuery::Met(met) => con.met == met,
ContactQuery::Location(_) if con.location.is_none() => false,
ContactQuery::Location(loc) => con.location.as_ref().unwrap().contains(loc),
ContactQuery::Notes(_) if con.notes.is_none() => false,
ContactQuery::Notes(notes) => con.notes.as_ref().unwrap().contains(notes),
})
.map(|(id, _)| id.clone())
.collect())
}
pub(crate) fn get_all(&self, id: &Identity) -> QaulResult<Vec<Identity>> {
let mut inner = self.inner.lock().expect("Failed to lock ContactStore");
pub(crate) fn get(&self, id: &Identity, contact: &Identity) -> Result<ContactEntry> {
let inner = self.inner.lock().expect("Failed to lock ContactStore");
inner
.get(id)
.map_or(Err(Error::NoUser), |x| Ok(x))?
.get(contact)
.map_or(Err(Error::NoContact), |x| Ok(x.clone()))
}
pub(crate) fn get_all(&self, id: &Identity) -> Result<Vec<Identity>> {
let inner = self.inner.lock().expect("Failed to lock ContactStore");
Ok(inner
.get(id)
.map_or(Err(QaulError::UnknownUser), |x| Ok(x))?
.map_or(Err(Error::NoUser), |x| Ok(x))?
.iter()
.map(|(id, _)| id.clone())
.collect())
......
//! Error and Result handling
//!
//! `libqaul` spans over a huge abstraction surface, from drivers all
//! the way to "business logic" functions in the service API (see
//! `api` module). This makes communicating errors challenging at
//! times. Generally, no lower layer `Error` objects are wrapped here,
//! to avoid introducing new dependencies in service code.
//!
//! Instead, `Error` attempts to provide a comprehensive set of
//! failures modes, that can be returned to communicate a failure,
//! that then needs tobe interpreted and addressed by an implementing
//! application. This way, it becomes easier for _your_ service to
//! wrap errors, or to enumerate them more easily.
//!
//! On an `Error` enum, it is also possible to call `description()` to
//! get a plain text error description of what went wrong, and what it
//! probably means. These are meant to simplify front-end development
//! and avoid having applications return arbitrary codes. You can also
//! set `QAUL_LANG=ar` (or others) as an environment variable to get
//! translations of these messages, with `en` being the fallback.
use conjoiner::Error as ConjError;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
result::Result as StdResult,
};
/// `libqaul` specific Result with embedded Error
///
/// The returned `Error` can sometimes be considered non-fatal. Check
/// the `Error` documentation for the specific returned variant to
/// see, what level of fatality it should be interpreted as. Crashing
/// on every returned `Err(_)` however is a bad idea.
pub type Result<T> = StdResult<T, Error>;
/// `libqaul` service API error states
///
/// All errors that can occur in interaction with the API are encoded
/// as variants on this enum. In most cases, no additional metadata is
/// provided and needs to be inferred from whatever context or
/// function call emitted the error. Check the variant doc comments
/// for a broad overview, as well as detailed usage instructions.
///
/// ## A note on language
///
/// Most variants of this enum use either an `Invalid` or `No`
/// prefix. Invalid data is data that was either not expected or badly
/// formatted. `No` in this case takes the place of `Unknown`, meaning
/// that a query could not be fulfilled.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Error {
/// Not authorised to perform this action
NotAuthorised,
/// The desired user was not known
NoUser,
/// The provided contact already exists
ContactExists,
/// The desired contact does not exist
NoContact,
/// Invalid search query
InvalidQuery,
/// Invalid payload (probably too big)
InvalidPayload,
/// A function callback timed out
CallbackTimeout,
/// Signature with an unknown public key
NoSign,
/// Fraudulent signature for a known public key
BadSign,
/// A generic networking error occured
NetworkFault,
/// Failed to find a route to this user
NoRoute,
/// Some serialisation action failed
BadSerialise,
/// No such service was found
NoService,
/// A sevice with this name already exists
ServiceExists,
/// Some internal components failed to communicate
CommFault,
}
impl Error {
pub fn help(&self) -> String {
match std::env::var("QAUL_LANG").as_ref().map(|s| s.as_str()) {
Ok("ar") => "حدث خطأ غير معروف",
Ok("de") => "Ein unbekannter Fehler ist aufgetreten",
_ => "An unknown Error occured",
}
.into()
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.description(),)
}
}
impl StdError for Error {}
impl From<ConjError> for Error {
fn from(_: ConjError) -> Self {
Error::BadSerialise
}
}