Clsoe #62: Make topic part of init packet

Signed-off-by: Lee Smet <lee.smet@hotmail.com>
This commit is contained in:
Lee Smet
2023-11-15 15:46:50 +01:00
parent c0953cfd22
commit d1ff31b084
5 changed files with 259 additions and 150 deletions

View File

@@ -56,12 +56,13 @@ paths:
a message if present, or return immediately if there isn't
example: 60
- in: query
name: filter
name: topic
required: false
schema:
type: string
format: byte
minLength: 0
maxLength: 255
maxLength: 340
description: |
Optional filter for loading messages. If set, the system checks if the message has the given string at the start. This way
a topic can be encoded.
@@ -217,6 +218,13 @@ components:
minLength: 64
maxLength: 64
example: 02468ace13579bdf02468ace13579bdf02468ace13579bdf02468ace13579bdf
topic:
description: An optional message topic
type: string
format: byte
minLength: 0
maxLength: 340
example: hpV+
payload:
description: The message payload, encoded in standard alphabet base64
type: string
@@ -229,6 +237,13 @@ components:
properties:
dst:
$ref: '#/components/schemas/MessageDestination'
topic:
description: An optional message topic
type: string
format: byte
minLength: 0
maxLength: 340
example: hpV+
payload:
description: The message to send, base64 encoded
type: string

View File

@@ -39,7 +39,11 @@ struct HttpServerState {
#[serde(rename_all = "camelCase")]
pub struct MessageSendInfo {
pub dst: MessageDestination,
#[serde(with = "base64")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "base64::optional_binary")]
pub topic: Option<Vec<u8>>,
#[serde(with = "base64::binary")]
pub payload: Vec<u8>,
}
@@ -58,7 +62,11 @@ pub struct MessageReceiveInfo {
pub src_pk: PublicKey,
pub dst_ip: IpAddr,
pub dst_pk: PublicKey,
#[serde(with = "base64")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "base64::optional_binary")]
pub topic: Option<Vec<u8>>,
#[serde(with = "base64::binary")]
pub payload: Vec<u8>,
}
@@ -102,8 +110,11 @@ impl Http {
struct GetMessageQuery {
peek: Option<bool>,
timeout: Option<u64>,
/// Optional filter for start of the message.
filter: Option<String>,
/// Optional filter for start of the message, base64 encoded.
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "base64::optional_binary")]
topic: Option<Vec<u8>>,
}
impl GetMessageQuery {
@@ -127,14 +138,13 @@ async fn get_message(
query.peek(),
query.timeout_secs()
);
// A timeout of 0 seconds essentially means get a message if there is one, and return
// immediatly if there isn't. This is the result of the implementation of Timeout, which does a
// poll of the internal future first, before polling the delay.
tokio::time::timeout(
Duration::from_secs(query.timeout_secs()),
state
.message_stack
.message(!query.peek(), query.filter.map(String::into_bytes)),
state.message_stack.message(!query.peek(), query.topic),
)
.await
.or(Err(StatusCode::NO_CONTENT))
@@ -145,6 +155,11 @@ async fn get_message(
src_pk: m.src_pk,
dst_ip: m.dst_ip,
dst_pk: m.dst_pk,
topic: if m.topic.is_empty() {
None
} else {
Some(m.topic)
},
payload: m.data,
})
})
@@ -160,8 +175,8 @@ pub struct MessageIdReply {
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum PushMessageResponse {
Id(MessageIdReply),
Reply(MessageReceiveInfo),
Id(MessageIdReply),
}
#[derive(Deserialize)]
@@ -192,12 +207,22 @@ async fn push_message(
message_info.payload.len(),
);
let (id, sub) = state.message_stack.new_message(
let (id, sub) = match state.message_stack.new_message(
dst,
message_info.payload,
if let Some(topic) = message_info.topic {
topic
} else {
vec![]
},
DEFAULT_MESSAGE_TRY_DURATION,
query.await_reply(),
);
) {
Ok((id, sub)) => (id, sub),
Err(_) => {
return Err(StatusCode::BAD_REQUEST);
}
};
if !query.await_reply() {
// If we don't wait for the reply just return here.
@@ -219,6 +244,7 @@ async fn push_message(
src_pk: m.src_pk,
dst_ip: m.dst_ip,
dst_pk: m.dst_pk,
topic: if m.topic.is_empty() { None } else { Some(m.topic.clone()) },
payload: m.data.clone(),
}))))
} else {
@@ -277,24 +303,55 @@ async fn message_status(
mod base64 {
use base64::alphabet;
use base64::engine::{GeneralPurpose, GeneralPurposeConfig};
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde::{Deserializer, Serializer};
const B64ENGINE: GeneralPurpose = base64::engine::general_purpose::GeneralPurpose::new(
&alphabet::STANDARD,
GeneralPurposeConfig::new(),
);
pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
let base64 = B64ENGINE.encode(v);
String::serialize(&base64, s)
pub mod binary {
use super::B64ENGINE;
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
let base64 = B64ENGINE.encode(v);
String::serialize(&base64, s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let base64 = String::deserialize(d)?;
B64ENGINE
.decode(base64.as_bytes())
.map_err(serde::de::Error::custom)
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let base64 = String::deserialize(d)?;
B64ENGINE
.decode(base64.as_bytes())
.map_err(serde::de::Error::custom)
pub mod optional_binary {
use super::B64ENGINE;
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(v: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
if let Some(v) = v {
let base64 = B64ENGINE.encode(v);
String::serialize(&base64, s)
} else {
<Option<String>>::serialize(&None, s)
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
if let Some(base64) = <Option<String>>::deserialize(d)? {
B64ENGINE
.decode(base64.as_bytes())
.map_err(serde::de::Error::custom)
.map(Option::Some)
} else {
Ok(None)
}
}
}
}

View File

@@ -385,11 +385,14 @@ enum Payload {
#[serde(rename_all = "camelCase")]
struct CliMessage {
id: MessageId,
topic: Option<String>,
src_ip: IpAddr,
src_pk: PublicKey,
dst_ip: IpAddr,
dst_pk: PublicKey,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(serialize_with = "serialize_payload")]
topic: Option<Payload>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(serialize_with = "serialize_payload")]
payload: Option<Payload>,
}
@@ -407,16 +410,9 @@ fn serialize_payload<S: Serializer>(p: &Option<Payload>, s: S) -> Result<S::Ok,
<Option<String>>::serialize(&base64, s)
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ReadableCliMessage {
id: MessageId,
topic: Option<String>,
src_ip: IpAddr,
src_pk: PublicKey,
dst_ip: IpAddr,
dst_pk: PublicKey,
payload: String,
/// Encode arbitrary data in standard base64.
pub fn encode_base64(input: &[u8]) -> String {
B64ENGINE.encode(input)
}
/// Send a message to a receiver.
@@ -478,29 +474,8 @@ async fn send_msg(
}
};
// Load msg, files have prio. If a topic is present, include that first
// The layout of these messages (in binary) is:
// - 1 byte topic length
// - topic
// - actual message
//
// Meaning a message without topic has length 0.
let mut msg_buf = if let Some(topic) = topic {
if topic.len() > 255 {
error!("{topic} is longer than the maximum allowed topic length of 255");
return Err(
std::io::Error::new(std::io::ErrorKind::InvalidInput, "Topic too long").into(),
);
}
let mut tmp = Vec::with_capacity(topic.len() + 1);
tmp.push(topic.len() as u8);
tmp.extend_from_slice(topic.as_bytes());
tmp
} else {
vec![0; 1]
};
msg_buf.extend_from_slice(&if let Some(path) = msg_path {
// Load msg, files have prio.
let msg = if let Some(path) = msg_path {
match tokio::fs::read(&path).await {
Err(e) => {
error!("Could not read file at {:?}: {e}", path);
@@ -517,7 +492,7 @@ async fn send_msg(
"Message is a required argument if `--msg-path` is not provided",
)
.into());
});
};
let mut url = format!("http://{server_addr}/api/v1/messages");
if let Some(reply_to) = reply_to {
@@ -526,15 +501,15 @@ async fn send_msg(
if wait {
// A year should be sufficient to wait
let reply_timeout = timeout.unwrap_or(60 * 60 * 24 * 365);
url.push_str("?reply_timeout=");
url.push_str(&format!("{reply_timeout}"));
url.push_str(&format!("?reply_timeout={reply_timeout}"));
}
match reqwest::Client::new()
.post(url)
.json(&MessageSendInfo {
dst: destination,
payload: msg_buf,
topic: topic.map(String::into_bytes),
payload: msg,
})
.send()
.await
@@ -543,55 +518,57 @@ async fn send_msg(
error!("Failed to send request: {e}");
return Err(e.into());
}
Ok(res) => match res.json::<PushMessageResponse>().await {
Err(e) => {
error!("Failed to load response body {e}");
return Err(e.into());
Ok(res) => {
if res.status() == STATUSCODE_NO_CONTENT {
return Ok(());
}
Ok(resp) => {
match resp {
PushMessageResponse::Id(id) => {
let _ = serde_json::to_writer(std::io::stdout(), &id);
}
PushMessageResponse::Reply(mri) => {
let filter_len = mri.payload[0] as usize;
let cm = CliMessage {
id: mri.id,
topic: if filter_len == 1 {
None
} else {
Some(
String::from_utf8(mri.payload[1..filter_len].to_vec())
.map_err(|e| {
error!("Failed to parse topic, not valid UTF-8 ({e})");
e
})?,
)
},
src_ip: mri.src_ip,
src_pk: mri.src_pk,
dst_ip: mri.dst_ip,
dst_pk: mri.dst_pk,
payload: Some({
let p = mri.payload[filter_len..].to_vec();
if let Ok(s) = String::from_utf8(p.clone()) {
Payload::Readable(s)
} else {
Payload::NotReadable(p)
}
}),
};
let _ = serde_json::to_writer(std::io::stdout(), &cm);
}
match res.json::<PushMessageResponse>().await {
Err(e) => {
error!("Failed to load response body {e}");
return Err(e.into());
}
Ok(resp) => {
match resp {
PushMessageResponse::Id(id) => {
let _ = serde_json::to_writer(std::io::stdout(), &id);
}
PushMessageResponse::Reply(mri) => {
let cm = CliMessage {
id: mri.id,
topic: mri.topic.map(|topic| {
if let Ok(s) = String::from_utf8(topic.clone()) {
Payload::Readable(s)
} else {
Payload::NotReadable(topic)
}
}),
src_ip: mri.src_ip,
src_pk: mri.src_pk,
dst_ip: mri.dst_ip,
dst_pk: mri.dst_pk,
payload: Some({
if let Ok(s) = String::from_utf8(mri.payload.clone()) {
Payload::Readable(s)
} else {
Payload::NotReadable(mri.payload)
}
}),
};
let _ = serde_json::to_writer(std::io::stdout(), &cm);
}
}
println!();
}
println!();
}
},
}
}
Ok(())
}
const STATUSCODE_NO_CONTENT: u16 = 204;
async fn recv_msg(
timeout: Option<u64>,
topic: Option<String>,
@@ -601,25 +578,27 @@ async fn recv_msg(
) -> Result<(), Box<dyn std::error::Error>> {
// One year timeout should be sufficient
let timeout = timeout.unwrap_or(60 * 60 * 24 * 365);
let mut url = format!("http:{server_addr}/api/v1/messages?timeout={timeout}");
let filter_len = if let Some(ref filter) = topic {
if filter.len() > 255 {
error!("{filter} is longer than the maximum allowed topic length of 255");
let mut url = format!("http://{server_addr}/api/v1/messages?timeout={timeout}");
if let Some(ref topic) = topic {
if topic.len() > 255 {
error!("{topic} is longer than the maximum allowed topic length of 255");
return Err(
std::io::Error::new(std::io::ErrorKind::InvalidInput, "Topic too long").into(),
);
}
url.push_str(&format!("&filter={filter}"));
filter.len() + 1
} else {
1
};
url.push_str(&format!("&topic={}", encode_base64(topic.as_bytes())));
}
let mut cm = match reqwest::get(url).await {
Err(e) => {
error!("Failed to wait for message: {e}");
return Err(e.into());
}
Ok(resp) => {
if resp.status() == STATUSCODE_NO_CONTENT {
debug!("No message ready yet");
return Ok(());
}
debug!("Received message response");
match resp.json::<MessageReceiveInfo>().await {
Err(e) => {
@@ -628,28 +607,22 @@ async fn recv_msg(
}
Ok(mri) => CliMessage {
id: mri.id,
topic: if filter_len == 1 {
None
} else {
Some(
String::from_utf8(mri.payload[1..filter_len].to_vec()).map_err(
|e| {
error!("Failed to parse topic, not valid UTF-8 ({e})");
e
},
)?,
)
},
topic: mri.topic.map(|topic| {
if let Ok(s) = String::from_utf8(topic.clone()) {
Payload::Readable(s)
} else {
Payload::NotReadable(topic)
}
}),
src_ip: mri.src_ip,
src_pk: mri.src_pk,
dst_ip: mri.dst_ip,
dst_pk: mri.dst_pk,
payload: Some({
let p = mri.payload[filter_len..].to_vec();
if let Ok(s) = String::from_utf8(p.clone()) {
if let Ok(s) = String::from_utf8(mri.payload.clone()) {
Payload::Readable(s)
} else {
Payload::NotReadable(p)
Payload::NotReadable(mri.payload)
}
}),
},

View File

@@ -78,8 +78,12 @@ const FLAG_MESSAGE_ACK: u16 = 0b0000_0001_0000_0000;
/// Length of a message checksum in bytes.
const MESSAGE_CHECKSUM_LENGTH: usize = 32;
/// Checksum of a message used to verify received message integrity.
pub type Checksum = [u8; MESSAGE_CHECKSUM_LENGTH];
/// Response type when pushing a message.
pub type MessagePushResponse = (MessageId, Option<watch::Receiver<Option<ReceivedMessage>>>);
#[derive(Clone)]
pub struct MessageStack {
// The DataPlane is wrappen in a Mutex since it does not implement Sync.
@@ -116,6 +120,8 @@ struct ReceivedMessageInfo {
dst: IpAddr,
/// Length of the finished message.
len: u64,
/// Optional topic of the message.
topic: Vec<u8>,
chunks: Vec<Option<Chunk>>,
}
@@ -133,6 +139,8 @@ pub struct ReceivedMessage {
pub dst_ip: IpAddr,
/// The public key of the receiver of the message. This is always ours.
pub dst_pk: PublicKey,
/// The possible topic of the message.
pub topic: Vec<u8>,
/// Actual message.
pub data: Vec<u8>,
}
@@ -181,6 +189,12 @@ enum TransmissionState {
Aborted,
}
#[derive(Debug, Clone, Copy)]
pub enum PushMessageError {
/// The topic set in the message is too large.
TopicTooLarge,
}
impl MessageInbox {
fn new(notify: watch::Sender<()>) -> Self {
Self {
@@ -374,6 +388,7 @@ impl MessageStack {
src,
dst,
len: mi.length(),
topic: mi.topic().into(),
chunks,
};
@@ -468,6 +483,7 @@ impl MessageStack {
id: inbound_message.id,
src: inbound_message.src,
dst: inbound_message.dst,
topic: inbound_message.topic.clone(),
data: message_data,
};
@@ -500,6 +516,7 @@ impl MessageStack {
src_pk: src_pubkey,
dst_ip: message.dst,
dst_pk: dst_pubkey,
topic: message.topic,
data: message.data,
};
@@ -578,10 +595,11 @@ impl MessageStack {
&self,
dst: IpAddr,
data: Vec<u8>,
topic: Vec<u8>,
try_duration: Duration,
subscribe_reply: bool,
) -> (MessageId, Option<watch::Receiver<Option<ReceivedMessage>>>) {
self.push_message(None, dst, data, try_duration, subscribe_reply)
) -> Result<MessagePushResponse, PushMessageError> {
self.push_message(None, dst, data, topic, try_duration, subscribe_reply)
}
/// Push a new message which is a reply to the message with [the provided id](MessageId).
@@ -592,7 +610,8 @@ impl MessageStack {
data: Vec<u8>,
try_duration: Duration,
) -> MessageId {
self.push_message(Some(reply_to), dst, data, try_duration, false)
self.push_message(Some(reply_to), dst, data, vec![], try_duration, false)
.expect("Empty topic is never too large")
.0
}
@@ -616,9 +635,14 @@ impl MessageStack {
id: Option<MessageId>,
dst: IpAddr,
data: Vec<u8>,
topic: Vec<u8>,
try_duration: Duration,
subscribe: bool,
) -> (MessageId, Option<watch::Receiver<Option<ReceivedMessage>>>) {
) -> Result<MessagePushResponse, PushMessageError> {
if topic.len() > 255 {
return Err(PushMessageError::TopicTooLarge);
}
let src = self
.data_plane
.lock()
@@ -635,7 +659,13 @@ impl MessageStack {
};
let len = data.len();
let msg = Message { id, src, dst, data };
let msg = Message {
id,
src,
dst,
topic,
data,
};
let created = std::time::SystemTime::now();
let deadline = created + try_duration;
@@ -655,12 +685,7 @@ impl MessageStack {
None
};
self.outbox
.lock()
.expect("Outbox lock isn't poisoned; qed")
.insert(obmi);
// Already send the init packet.
// Already prepare the init packet for sending..
let mut mp = MessagePacket::new(PacketBuffer::new());
mp.header_mut().set_message_id(id);
if reply {
@@ -669,6 +694,14 @@ impl MessageStack {
let mut mi = MessageInit::new(mp);
mi.set_length(len as u64);
mi.set_topic(&obmi.msg.topic);
self.outbox
.lock()
.expect("Outbox lock isn't poisoned; qed")
.insert(obmi);
// Actually send the init packet
match (src, dst) {
(IpAddr::V6(src), IpAddr::V6(dst)) => {
self.data_plane.lock().unwrap().inject_message_packet(
@@ -711,6 +744,7 @@ impl MessageStack {
let mut mi = MessageInit::new(mp);
mi.set_length(len as u64);
mi.set_topic(&msg.msg.topic);
match (msg.msg.src, msg.msg.dst) {
(IpAddr::V6(src), IpAddr::V6(dst)) => {
message_stack
@@ -898,7 +932,7 @@ impl MessageStack {
}
});
(id, subscription)
Ok((id, subscription))
}
/// Get information about the status of an outbound message.
@@ -948,7 +982,7 @@ impl MessageStack {
///
/// If pop is false, the message is not removed and the next call of this method will return
/// the same message.
pub async fn message(&self, pop: bool, filter: Option<Vec<u8>>) -> ReceivedMessage {
pub async fn message(&self, pop: bool, topic: Option<Vec<u8>>) -> ReceivedMessage {
// Copy the subscriber since we need mutable access to it.
let mut subscriber = self.subscriber.clone();
@@ -958,15 +992,12 @@ impl MessageStack {
'check: {
let mut inbox = self.inbox.lock().unwrap();
// If a filter is set only check for those messages.
if let Some(ref filter) = filter {
if let Some((idx, _)) =
inbox.complete_msges.iter().enumerate().find(|(_, v)| {
if v.data.len() < filter.len() + 1 {
return false;
}
v.data[0] == filter.len() as u8
&& v.data[1..filter.len() + 1] == filter[..]
})
if let Some(ref topic) = topic {
if let Some((idx, _)) = inbox
.complete_msges
.iter()
.enumerate()
.find(|(_, v)| &v.topic == topic)
{
return inbox.complete_msges.remove(idx).unwrap();
} else {
@@ -1381,7 +1412,9 @@ pub struct Message {
src: IpAddr,
/// Destination IP
dst: IpAddr,
/// Data
/// An optional topic of the message, usefull to differentiate messages before reading.
topic: Vec<u8>,
/// Data of the message
data: Vec<u8>,
}
@@ -1412,6 +1445,16 @@ impl Message {
}
}
impl fmt::Display for PushMessageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TopicTooLarge => f.write_str("topic too large, topic is limitted to 255 bytes"),
}
}
}
impl std::error::Error for PushMessageError {}
#[cfg(test)]
mod tests {

View File

@@ -11,7 +11,7 @@ pub struct MessageInit {
impl MessageInit {
/// Create a new `MessageInit` in the provided [`MessagePacket`].
pub fn new(mut buffer: MessagePacket) -> Self {
buffer.set_used_buffer_size(8);
buffer.set_used_buffer_size(9);
buffer.header_mut().flags_mut().set_init();
Self { buffer }
}
@@ -25,11 +25,32 @@ impl MessageInit {
)
}
/// Return the topic of the message, as written in the body.
pub fn topic(&self) -> &[u8] {
let topic_len = self.buffer.buffer()[8] as usize;
&self.buffer.buffer()[9..9 + topic_len]
}
/// Set the length field of the message body.
pub fn set_length(&mut self, length: u64) {
self.buffer.buffer_mut()[..8].copy_from_slice(&length.to_be_bytes())
}
/// Set the topic in the message body.
///
/// # Panics
///
/// This function panics if the topic is longer than 255 bytes.
pub fn set_topic(&mut self, topic: &[u8]) {
assert!(
topic.len() <= u8::MAX as usize,
"Topic can be 255 bytes long at most"
);
self.buffer.set_used_buffer_size(9 + topic.len());
self.buffer.buffer_mut()[8] = topic.len() as u8;
self.buffer.buffer_mut()[9..9 + topic.len()].copy_from_slice(topic);
}
/// Convert the `MessageInit` into a reply. This does nothing if it is already a reply.
pub fn into_reply(mut self) -> Self {
self.buffer.header_mut().flags_mut().set_ack();