Compare commits
46 Commits
interface-
...
master
Author | SHA1 | Date |
---|---|---|
Lev | 4e19042f5b | 3 years ago |
Lev | a2c5059646 | 3 years ago |
Lev | 3cef4b50d2 | 3 years ago |
Lev | 93f83e6b4a | 3 years ago |
Lev | 3f49b7a551 | 3 years ago |
Lev | d9afc776c0 | 3 years ago |
Lev | ea5b473763 | 3 years ago |
Lev | f94c9849d6 | 3 years ago |
Lev | 8164fa9874 | 3 years ago |
Lev | 05785e078c | 3 years ago |
Alexey | c712b5a7be | 3 years ago |
Alexey | 82b9109585 | 3 years ago |
Alexey | 49824ef937 | 3 years ago |
Alexey | b501b69f64 | 3 years ago |
Lev | 81d129ec59 | 3 years ago |
Alexey | 7c76981c1f | 3 years ago |
Alexey | a509fb8e45 | 3 years ago |
Alexey | 204310e159 | 3 years ago |
Alexey | c09a392bc0 | 3 years ago |
Lev | 8e58048c4e | 3 years ago |
Alexey | acb5d4f8d5 | 3 years ago |
Lev | 84e1b5c879 | 3 years ago |
Lev | 0ccd0bbced | 3 years ago |
Lev | 226b9536ce | 3 years ago |
Lev | 114dd10448 | 3 years ago |
Lev | 2240ae80ae | 3 years ago |
Lev | 68d2fb2af3 | 3 years ago |
Lev | c90fc8e0a6 | 3 years ago |
Lev | d8c85c96aa | 3 years ago |
Lev | 3a7a320605 | 3 years ago |
Lev | ae901e5009 | 3 years ago |
Lev | 5089de2af6 | 3 years ago |
Lev | 53252f72b8 | 3 years ago |
Lev | 0c11f286f7 | 3 years ago |
Lev | 3e61c27317 | 3 years ago |
Lev | fa49945652 | 3 years ago |
Lev | 362102c7dc | 3 years ago |
Prokhor | 445b285b79 | 3 years ago |
Alexey | 9da108486c | 3 years ago |
Lev | 41a0842193 | 3 years ago |
Lev | eede8b9c23 | 3 years ago |
Lev | e0e7a49520 | 3 years ago |
Lev | 2d6275debc | 3 years ago |
Alexey | 57397e20fd | 3 years ago |
Alexey | e46f611b4a | 3 years ago |
Alexey | bb26f910ff | 3 years ago |
59 changed files with 11924 additions and 860 deletions
@ -1,3 +1,75 @@
|
||||
# Ironforce + Degeon |
||||
|
||||
IronForce is a peer-to-peer decentralized network, Degeon is a messenger built on it. |
||||
|
||||
In this repository there is three Cargo crates and one python project: |
||||
|
||||
- `ironforce` is the decentralized network |
||||
- `degeon_core` contains messenger's protocol |
||||
- `degeon` is the messenger app in Rust + Iced |
||||
- `degeon_py` is a version of the messenger app in Python + Pygame |
||||
|
||||
![](images/dependency_graph.png) |
||||
|
||||
# Ironforce |
||||
|
||||
IronForce is a decentralized network |
||||
The Ironforce network: |
||||
|
||||
- Has messages encrypted and signed using RSA |
||||
|
||||
- Can build efficient tunnels between two nodes |
||||
|
||||
- Is anonymous: it's very hard to create a connection between a node's IP and its public key |
||||
|
||||
- Can be extended to support any interface, like Bluetooth or even radio |
||||
|
||||
- Can run on ARM microcontrollers |
||||
|
||||
![](images/scheme.png) |
||||
|
||||
### Running IronForce |
||||
|
||||
The `IF` network has a worker daemon that can be used to read the broadcast messages and write them. |
||||
|
||||
Also, you can us `IF` as a Rust library (as it's done in Degeon). |
||||
|
||||
To get started: |
||||
|
||||
1. Clone this repository: `git clone ssh://git@gitlab.ennucore.com:10022/ironforce/ironforce.git` |
||||
2. Install Rust and Cargo. The best way to do this is to use RustUp: |
||||
```shell |
||||
sudo pacman -S rustup # this is for Arch Linux, on other distros use the corresponding package manager |
||||
rustup default nightly # this will install rust & cargo (nightly toolchain) |
||||
``` |
||||
3. Run the worker: |
||||
```shell |
||||
cd ironforce/ironforce |
||||
cargo build --features std # This will build ironforce network |
||||
# To run the worker, use this: |
||||
cargo run --bin worker --features std |
||||
``` |
||||
|
||||
# Degeon |
||||
|
||||
Degeon is a messenger built on IronForce to show its abilities. |
||||
Its core is in the crate `degeon_core`. The crate can be used as a Python module or in Rust. |
||||
|
||||
To build it, install Cargo and Rust, then do this: |
||||
```shell |
||||
cd ironforce/degeon_core |
||||
cargo build |
||||
``` |
||||
|
||||
You will find the `libdegeon_core.so` file that can be used in Python in the `target/debug` directory. |
||||
It should be renamed to `degeon_core.so`. |
||||
In Windows Rust should generate a `.dll` file that you will be able to use likewise. |
||||
|
||||
There are two GUI options for this interface: |
||||
- A Rust + Iced client (preferred) |
||||
- A Pygame client |
||||
|
||||
To run the Rust client, `cd` into the `degeon` folder of this repository, then use cargo: |
||||
`cargo run`. |
||||
Note: this application requires Vulkan to be installed. |
||||
For me on Arch Linux and Intel GPU the installation command was `sudo pacman -S vulkan-intel`. |
||||
On most systems it should be pre-installed. |
@ -0,0 +1,17 @@
|
||||
[package] |
||||
name = "degeon" |
||||
version = "0.1.0" |
||||
edition = "2021" |
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||
|
||||
[dependencies] |
||||
iced = { version = "0.3.0", features = ["image"] } |
||||
ironforce = { path = "../ironforce", features = ["std"] } |
||||
degeon_core = { path = "../degeon_core" } |
||||
base64 = "0.13.0" |
||||
serde = { version = "1.0" } |
||||
serde_json = "1.0.72" |
||||
futures = "0.3.18" |
||||
iced_native = "0.4.0" |
||||
chrono = "0.4.19" |
Binary file not shown.
@ -0,0 +1,152 @@
|
||||
use crate::gui_events::GuiEvent; |
||||
use crate::state; |
||||
use crate::styles::style; |
||||
pub use degeon_core::Chat; |
||||
use iced::{Align, Button, Column, Element, Length, Row, Text, TextInput}; |
||||
use iced_native::button; |
||||
|
||||
/// A chat that can be rendered in iced
|
||||
pub trait RenderableChat { |
||||
/// Render header of the chat
|
||||
fn header<'a>(name: String, bio: String) -> Element<'a, GuiEvent>; |
||||
|
||||
/// Render the sending field
|
||||
fn send_field<'a>( |
||||
input: String, |
||||
text_input_state: &'a mut iced::text_input::State, |
||||
send_button_state: &'a mut iced::button::State, |
||||
) -> Element<'a, GuiEvent>; |
||||
|
||||
/// Render chat preview
|
||||
fn preview<'a>( |
||||
&'a self, |
||||
state: &'a mut button::State, |
||||
i: usize, |
||||
is_selected: bool, |
||||
) -> Element<'a, GuiEvent>; |
||||
|
||||
/// Render the chat view
|
||||
fn view<'a>( |
||||
&'a self, |
||||
text_input_state: &'a mut iced::text_input::State, |
||||
send_button_state: &'a mut iced::button::State, |
||||
scroll_state: &'a mut iced::scrollable::State, |
||||
) -> Element<'a, GuiEvent>; |
||||
} |
||||
|
||||
impl RenderableChat for Chat { |
||||
/// Render header of the chat
|
||||
///
|
||||
/// The header is a rectangle (container) with name and bio
|
||||
fn header<'a>(name: String, bio: String) -> Element<'a, GuiEvent> { |
||||
iced::container::Container::new(Column::with_children( |
||||
vec![ |
||||
Text::new(name.as_str()).size(20).color(iced::Color::WHITE).into(), |
||||
Text::new(bio.as_str()).size(13).color(iced::Color::from_rgb(0.6, 0.6, 0.6)).into(), |
||||
])) |
||||
.style(style::Container::Primary) |
||||
.width(Length::Fill) |
||||
.height(Length::Units(50)) |
||||
.padding(10) |
||||
.into() |
||||
} |
||||
|
||||
/// Render the sending field
|
||||
///
|
||||
/// This field consists of a text input and a send button.
|
||||
/// When the button is clicked, this element creates an event
|
||||
fn send_field<'a>( |
||||
input: String, |
||||
text_input_state: &'a mut iced::text_input::State, |
||||
send_button_state: &'a mut iced::button::State, |
||||
) -> Element<'a, GuiEvent> { |
||||
Row::new() |
||||
.width(Length::Fill) |
||||
.padding(15) |
||||
// Text field
|
||||
.push( |
||||
TextInput::new(text_input_state, "Message", input.as_str(), |st| { |
||||
GuiEvent::Typed(st) |
||||
}) |
||||
.padding(8) |
||||
.width(Length::Fill), |
||||
) |
||||
// Send button
|
||||
.push( |
||||
Button::new(send_button_state, Text::new("Send")) |
||||
.on_press(GuiEvent::SendMessage) |
||||
.style(style::Button::Secondary) |
||||
.padding(20) |
||||
.width(Length::Units(80)), |
||||
) |
||||
.spacing(25) |
||||
.height(Length::Units(100)) |
||||
.into() |
||||
} |
||||
|
||||
/// Render chat preview
|
||||
fn preview<'a>( |
||||
&'a self, |
||||
state: &'a mut button::State, |
||||
i: usize, |
||||
is_selected: bool, |
||||
) -> Element<'a, GuiEvent> { |
||||
Button::new(state, Text::new(self.profile.name.as_str())) |
||||
.width(Length::Fill) |
||||
.padding(10) |
||||
.style(if is_selected { |
||||
style::Button::Primary |
||||
} else { |
||||
style::Button::InactiveChat |
||||
}) |
||||
.on_press(GuiEvent::ChatSelect(i)) |
||||
.into() |
||||
} |
||||
|
||||
/// Render the chat view
|
||||
///
|
||||
/// Chat view consists of a header, the messages and the input field
|
||||
fn view<'a>( |
||||
&'a self, |
||||
text_input_state: &'a mut iced::text_input::State, |
||||
send_button_state: &'a mut iced::button::State, |
||||
scroll_state: &'a mut iced::scrollable::State, |
||||
) -> Element<'a, GuiEvent> { |
||||
let pkey_clone = self.pkey.clone(); |
||||
let msgs = self |
||||
.messages |
||||
.iter() |
||||
.filter_map(move |msg| state::view_message(msg, pkey_clone.clone())) |
||||
.collect(); |
||||
Column::new() |
||||
.align_items(Align::End) |
||||
.height(Length::Fill) |
||||
.width(Length::FillPortion(4)) |
||||
// Header
|
||||
.push(Self::header(self.profile.name.clone(), self.profile.bio.clone())) |
||||
// Messages (scrollable)
|
||||
.push( |
||||
iced::Container::new( |
||||
iced::Scrollable::new(scroll_state) |
||||
.push( |
||||
Column::with_children(msgs) |
||||
.padding(20) |
||||
.spacing(10) |
||||
.align_items(Align::End), |
||||
) |
||||
.align_items(Align::End) |
||||
.width(Length::Fill), |
||||
) |
||||
.align_y(Align::End) |
||||
.height(Length::FillPortion(9)), |
||||
) |
||||
.spacing(10) |
||||
// New message field
|
||||
.push(Self::send_field( |
||||
self.input.to_string(), |
||||
text_input_state, |
||||
send_button_state, |
||||
)) |
||||
.into() |
||||
} |
||||
} |
@ -0,0 +1,35 @@
|
||||
use crate::gui_events::GuiEvent; |
||||
pub use degeon_core::{Degeon, DegeonData}; |
||||
|
||||
/// A wrapper around `degeon_core`'s `Degeon` that can create a subscription for events
|
||||
pub(crate) struct DegeonContainer { |
||||
inner: Degeon, |
||||
} |
||||
|
||||
impl DegeonContainer { |
||||
/// Create a new container from inner data
|
||||
pub fn new(data: &Degeon) -> Self { |
||||
Self { inner: data.clone() } |
||||
} |
||||
} |
||||
|
||||
impl<H, I> iced_native::subscription::Recipe<H, I> for DegeonContainer |
||||
where |
||||
H: std::hash::Hasher, |
||||
{ |
||||
type Output = GuiEvent; |
||||
|
||||
fn hash(&self, state: &mut H) { |
||||
use std::hash::Hash; |
||||
|
||||
std::any::TypeId::of::<Self>().hash(state); |
||||
self.inner.ironforce.lock().unwrap().hash(state); |
||||
} |
||||
|
||||
fn stream( |
||||
self: Box<Self>, |
||||
_input: futures::stream::BoxStream<'static, I>, |
||||
) -> futures::stream::BoxStream<'static, Self::Output> { |
||||
Box::pin(self.inner) |
||||
} |
||||
} |
@ -0,0 +1,2 @@
|
||||
// Just reusing events from `degeon_core`
|
||||
pub use degeon_core::{GuiEvent, AppScreen}; |
@ -0,0 +1,18 @@
|
||||
extern crate serde; |
||||
mod message; |
||||
mod state; |
||||
mod gui_events; |
||||
mod chat; |
||||
mod styles; |
||||
mod degeon_worker; |
||||
|
||||
use iced::Application; |
||||
use crate::state::DegeonApp; |
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> { |
||||
// Change default font
|
||||
let mut settings = iced::Settings::default(); |
||||
settings.default_font = Some(include_bytes!("CRC35.otf")); |
||||
// Create and run the app
|
||||
Ok(DegeonApp::run(settings)?) |
||||
} |
@ -0,0 +1,2 @@
|
||||
/// Messages here are just from `degeon_core`
|
||||
pub use degeon_core::{ProtocolMsg, Profile, DegMessage, DegMessageContent}; |
After Width: | Height: | Size: 20 KiB |
@ -0,0 +1,343 @@
|
||||
use crate::chat::Chat; |
||||
use crate::chat::RenderableChat; |
||||
use crate::degeon_worker::{Degeon, DegeonContainer}; |
||||
use crate::gui_events::GuiEvent; |
||||
use crate::message::{DegMessage, DegMessageContent, ProtocolMsg}; |
||||
use crate::styles::style; |
||||
use core::default::Default; |
||||
use degeon_core::AppScreen; |
||||
use iced::{ |
||||
button, Align, Application, Button, Column, Element, HorizontalAlignment, Length, Row, Text, |
||||
TextInput, VerticalAlignment, |
||||
}; |
||||
use ironforce::PublicKey; |
||||
|
||||
/// Render a message into an iced `Element`
|
||||
///
|
||||
/// A message is a rounded rectangle with text.
|
||||
/// The background color of the rectangle depends on the sender of the message.
|
||||
pub fn view_message(msg: &DegMessage, pkey: PublicKey) -> Option<Element<GuiEvent>> { |
||||
let is_from_me = pkey != msg.sender; |
||||
match &msg.content { |
||||
DegMessageContent::Text(t) => Some( |
||||
iced::Row::new() |
||||
.push( |
||||
iced::Container::new(Text::new(t.as_str())) |
||||
.padding(10) |
||||
.style(if is_from_me { |
||||
style::Container::MyMessage |
||||
} else { |
||||
style::Container::NotMyMessage |
||||
}) |
||||
.align_x(if is_from_me { Align::End } else { Align::Start }), |
||||
) |
||||
.align_items(if is_from_me { Align::End } else { Align::Start }) |
||||
.width(if is_from_me { |
||||
Length::Shrink |
||||
} else { |
||||
Length::Fill |
||||
}) |
||||
.into(), |
||||
), |
||||
DegMessageContent::File(_) => None, |
||||
DegMessageContent::Service => None, |
||||
} |
||||
} |
||||
|
||||
/// The main application struct (for iced)
|
||||
#[derive(Default)] |
||||
pub struct DegeonApp { |
||||
/// The container for logic, data and IF
|
||||
pub data: Degeon, |
||||
/// Current screen
|
||||
screen: AppScreen, |
||||
/// Selected chat (on the main screen)
|
||||
selected_chat: usize, |
||||
/// Send button
|
||||
send_button_state: iced::button::State, |
||||
/// Message input field
|
||||
text_input_state: iced::text_input::State, |
||||
/// Buttons for chat previews
|
||||
preview_button_states: Vec<button::State>, |
||||
/// Name input on profile screen
|
||||
name_input_state: iced::text_input::State, |
||||
/// Bio input on profile screen
|
||||
bio_input_state: iced::text_input::State, |
||||
/// Button on the profile screen
|
||||
profile_done_button_state: iced::button::State, |
||||
/// Scroll state
|
||||
scroll: iced::scrollable::State, |
||||
/// The button at the left-upper corner of the screen
|
||||
profile_logo_state: iced::button::State, |
||||
} |
||||
|
||||
impl DegeonApp { |
||||
/// Render the list of chats with all previews
|
||||
fn chat_list<'a>( |
||||
chats: &'a [Chat], |
||||
preview_button_states: &'a mut Vec<button::State>, |
||||
selected: usize, |
||||
profile_logo_state: &'a mut button::State, |
||||
) -> Element<'a, GuiEvent> { |
||||
while preview_button_states.len() < chats.len() { |
||||
preview_button_states.push(Default::default()) |
||||
} |
||||
Column::new() |
||||
.push( |
||||
iced::Button::new( |
||||
profile_logo_state, |
||||
iced::Container::new(iced::Image::new(iced::image::Handle::from_memory( |
||||
include_bytes!("profile_logo.png").to_vec(), |
||||
))) |
||||
.width(Length::Units(25)), |
||||
) |
||||
.on_press(GuiEvent::ChangeScreen(AppScreen::ProfileEditor)), |
||||
) |
||||
.push( |
||||
Column::with_children( |
||||
chats |
||||
.iter() |
||||
.zip(preview_button_states.iter_mut()) |
||||
.enumerate() |
||||
.map(|(i, (chat, state))| chat.preview(state, i, i == selected)) |
||||
.collect(), |
||||
) |
||||
.padding(20) |
||||
.spacing(10) |
||||
.align_items(Align::Start), |
||||
) |
||||
.width(Length::FillPortion(1)) |
||||
.into() |
||||
} |
||||
|
||||
/// Render active chat section
|
||||
pub fn active_chat<'a>( |
||||
chats: &'a [Chat], |
||||
selected_chat: usize, |
||||
send_button_state: &'a mut button::State, |
||||
text_input_state: &'a mut iced::text_input::State, |
||||
scroll_state: &'a mut iced::scrollable::State, |
||||
) -> Element<'a, GuiEvent> { |
||||
if selected_chat >= chats.len() { |
||||
Text::new("No chat") |
||||
.horizontal_alignment(HorizontalAlignment::Center) |
||||
.vertical_alignment(VerticalAlignment::Center) |
||||
.width(Length::FillPortion(4)) |
||||
.into() |
||||
} else { |
||||
chats[selected_chat].view(text_input_state, send_button_state, scroll_state) |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl DegeonApp { |
||||
/// Render the main screen with chat list and active chat on it
|
||||
fn main_view(&mut self) -> Element<GuiEvent> { |
||||
let Self { |
||||
data: Degeon { chats, .. }, |
||||
selected_chat, |
||||
send_button_state, |
||||
text_input_state, |
||||
preview_button_states, |
||||
scroll: scroll_state, |
||||
profile_logo_state, |
||||
.. |
||||
} = self; |
||||
Row::new() |
||||
.padding(20) |
||||
.push(Self::chat_list( |
||||
chats, |
||||
preview_button_states, |
||||
*selected_chat, |
||||
profile_logo_state, |
||||
)) |
||||
.push(Self::active_chat( |
||||
chats, |
||||
*selected_chat, |
||||
send_button_state, |
||||
text_input_state, |
||||
scroll_state, |
||||
)) |
||||
.height(Length::Fill) |
||||
.into() |
||||
} |
||||
|
||||
/// Render the screen with profile settings
|
||||
fn profile_editor(&mut self) -> Element<GuiEvent> { |
||||
let Self { |
||||
data: |
||||
Degeon { |
||||
profile: crate::message::Profile { name, bio }, |
||||
.. |
||||
}, |
||||
profile_done_button_state, |
||||
name_input_state, |
||||
bio_input_state, |
||||
.. |
||||
} = self; |
||||
iced::Container::new( |
||||
Column::new() |
||||
.align_items(Align::Center) |
||||
.width(Length::Fill) |
||||
.spacing(60) |
||||
.padding(40) |
||||
.push(Text::new("Profile").size(40)) |
||||
// Name input
|
||||
.push( |
||||
iced::Container::new( |
||||
Row::with_children(vec![ |
||||
Text::new("Name").into(), |
||||
TextInput::new(name_input_state, "Name", name.as_str(), |name| { |
||||
GuiEvent::ChangeName(name) |
||||
}) |
||||
.padding(5) |
||||
.into(), |
||||
]) |
||||
.spacing(10) |
||||
.max_width(250) |
||||
.align_items(Align::Center), |
||||
) |
||||
.align_x(Align::Center) |
||||
.padding(40) |
||||
.style(style::Container::Field), |
||||
) |
||||
// Bio input
|
||||
.push( |
||||
iced::Container::new( |
||||
Row::with_children(vec![ |
||||
Text::new("Bio").into(), |
||||
TextInput::new(bio_input_state, "Bio", bio.as_str(), |bio| { |
||||
GuiEvent::ChangeBio(bio) |
||||
}) |
||||
.padding(5) |
||||
.into(), |
||||
]) |
||||
.spacing(10) |
||||
.max_width(250) |
||||
.align_items(Align::Center), |
||||
) |
||||
.align_x(Align::Center) |
||||
.padding(40) |
||||
.style(style::Container::Field), |
||||
) |
||||
.push( |
||||
Button::new(profile_done_button_state, Text::new("Done")) |
||||
.style(style::Button::Primary) |
||||
.on_press(GuiEvent::ChangeScreen(AppScreen::Main)), |
||||
), |
||||
) |
||||
.align_x(Align::Center) |
||||
.width(Length::Fill) |
||||
.into() |
||||
} |
||||
} |
||||
|
||||
impl Application for DegeonApp { |
||||
type Executor = iced::executor::Default; |
||||
type Message = GuiEvent; |
||||
type Flags = (); |
||||
|
||||
// Create a new application instance
|
||||
fn new(_: ()) -> (Self, iced::Command<GuiEvent>) { |
||||
let mut data = Degeon::restore_from_file("".to_string()).unwrap(); |
||||
if data.chats.is_empty() { |
||||
data.chats = vec![Chat::example(0, &data.keys), Chat::example(1, &data.keys)]; |
||||
} |
||||
data.send_multicast(ProtocolMsg::Ping).unwrap(); |
||||
let mut scroll: iced::scrollable::State = Default::default(); |
||||
scroll.scroll_to( |
||||
1., |
||||
iced::Rectangle::with_size(iced::Size::ZERO), |
||||
iced::Rectangle::with_size(iced::Size::UNIT), |
||||
); |
||||
let st = DegeonApp { |
||||
screen: if data.profile.name.is_empty() { |
||||
AppScreen::ProfileEditor |
||||
} else { |
||||
AppScreen::Main |
||||
}, |
||||
data, |
||||
selected_chat: 0, |
||||
send_button_state: Default::default(), |
||||
text_input_state: Default::default(), |
||||
preview_button_states: vec![Default::default(), Default::default()], |
||||
name_input_state: Default::default(), |
||||
bio_input_state: Default::default(), |
||||
profile_done_button_state: Default::default(), |
||||
scroll, |
||||
profile_logo_state: Default::default(), |
||||
}; |
||||
(st, iced::Command::none()) |
||||
} |
||||
|
||||
fn title(&self) -> String { |
||||
String::from("Degeon") |
||||
} |
||||
|
||||
fn update(&mut self, message: GuiEvent, _: &mut iced::Clipboard) -> iced::Command<GuiEvent> { |
||||
self.data.process_event(&message, false).unwrap(); |
||||
match message { |
||||
GuiEvent::ChatSelect(i) => self.selected_chat = i, |
||||
GuiEvent::Typed(st) => self.data.chats[self.selected_chat].input = st, |
||||
GuiEvent::SendMessage => { |
||||
if self.data.chats[self.selected_chat].input.is_empty() { |
||||
return iced::Command::none(); |
||||
} |
||||
let new_msg = DegMessage::new_text( |
||||
self.data.chats[self.selected_chat].input.clone(), |
||||
&self.data.keys.get_public(), |
||||
); |
||||
self.data.chats[self.selected_chat].input = String::new(); |
||||
self.data.chats[self.selected_chat] |
||||
.messages |
||||
.push(new_msg.clone()); |
||||
let data_cloned = self.data.clone(); |
||||
let target = self.data.chats[self.selected_chat].pkey.clone(); |
||||
std::thread::spawn(move || { |
||||
data_cloned |
||||
.send_message(ProtocolMsg::NewMessage(new_msg), &target) |
||||
.unwrap() |
||||
}); |
||||
self.data.save_to_file("".to_string()).unwrap(); |
||||
} |
||||
GuiEvent::ChangeScreen(sc) => { |
||||
let prev_screen = self.screen; |
||||
self.screen = sc; |
||||
self.data.save_to_file("".to_string()).unwrap(); |
||||
if prev_screen == AppScreen::ProfileEditor { |
||||
return self |
||||
.data |
||||
.get_broadcast_send_command(ProtocolMsg::ProfileResponse( |
||||
self.data.get_profile(), |
||||
)); |
||||
} |
||||
} |
||||
GuiEvent::ChangeName(name) => self.data.profile.name = name, |
||||
GuiEvent::ChangeBio(bio) => self.data.profile.bio = bio, |
||||
// The following events are already handled in Degeon::process_event
|
||||
GuiEvent::NewMessageInChat(_pkey, _msg) => {} |
||||
GuiEvent::SetProfile(_pkey, _profile) => {} |
||||
GuiEvent::None => {} |
||||
GuiEvent::WeHaveToSendProfile(target) => { |
||||
println!("WHTSP"); |
||||
return self.data.get_send_command( |
||||
ProtocolMsg::ProfileResponse(self.data.get_profile()), |
||||
&target, |
||||
); |
||||
} |
||||
} |
||||
iced::Command::none() |
||||
} |
||||
|
||||
fn subscription(&self) -> iced::Subscription<GuiEvent> { |
||||
iced::Subscription::from_recipe(DegeonContainer::new(&self.data)) |
||||
} |
||||
|
||||
fn view(&mut self) -> Element<'_, Self::Message> { |
||||
match self.screen { |
||||
AppScreen::Main => self.main_view(), |
||||
AppScreen::ProfileEditor => self.profile_editor(), |
||||
AppScreen::PeerInput => todo!(), |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,65 @@
|
||||
pub mod style { |
||||
use iced::container::Style; |
||||
use iced::{button, Background, Color, Vector}; |
||||
|
||||
/// The styles for buttons used in the app
|
||||
#[derive(Clone, Copy, PartialEq, Eq)] |
||||
pub enum Button { |
||||
Primary, |
||||
Secondary, |
||||
#[allow(dead_code)] |
||||
Destructive, |
||||
InactiveChat, |
||||
} |
||||
|
||||
impl button::StyleSheet for Button { |
||||
fn active(&self) -> button::Style { |
||||
button::Style { |
||||
background: Some(Background::Color(match self { |
||||
Button::Primary => Color::from_rgb(0.11, 0.35, 0.75), |
||||
Button::Secondary => Color::from_rgb(0.3, 0.1, 0.7), |
||||
Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2), |
||||
Button::InactiveChat => Color::from_rgb(0.3, 0.52, 0.9), |
||||
})), |
||||
border_radius: 5.0, |
||||
shadow_offset: Vector::new(1.0, 1.0), |
||||
text_color: if self != &Button::InactiveChat { |
||||
Color::WHITE |
||||
} else { |
||||
Color::BLACK |
||||
}, |
||||
..button::Style::default() |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// The styles for containers used in the app
|
||||
#[derive(Copy, Clone, PartialEq)] |
||||
pub enum Container { |
||||
Primary, |
||||
MyMessage, |
||||
NotMyMessage, |
||||
Field, |
||||
} |
||||
|
||||
impl iced::container::StyleSheet for Container { |
||||
fn style(&self) -> Style { |
||||
iced::container::Style { |
||||
text_color: if *self != Container::Field { Some(Color::WHITE) } else { Some(Color::BLACK) }, |
||||
background: Some(Background::Color(match self { |
||||
Container::Primary => Color::from_rgb(18. / 256., 25. / 256., 70. / 256.), |
||||
Container::MyMessage => Color::from_rgb(0., 0.1, 0.8), |
||||
Container::NotMyMessage => Color::from_rgb(0., 0.1, 0.4), |
||||
Container::Field => Color::TRANSPARENT, |
||||
})), |
||||
border_radius: 5.0, |
||||
border_width: 0.9, |
||||
border_color: if *self != Container::Field { |
||||
Color::TRANSPARENT |
||||
} else { |
||||
Color::BLACK |
||||
}, |
||||
} |
||||
} |
||||
} |
||||
} |
Binary file not shown.
@ -0,0 +1,26 @@
|
||||
[package] |
||||
name = "degeon_core" |
||||
version = "0.1.0" |
||||
edition = "2021" |
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||
|
||||
[dependencies] |
||||
ironforce = { path = "../ironforce", features = ["std"] } |
||||
base64 = "0.13.0" |
||||
serde = { version = "1.0" } |
||||
serde_json = "1.0.72" |
||||
futures = "0.3.18" |
||||
chrono = "0.4.19" |
||||
iced = { version = "0.3.0" } |
||||
rand = "0.8.4" |
||||
|
||||
[dependencies.pyo3] |
||||
version = "0.14.0" |
||||
features = ["extension-module"] |
||||
|
||||
[lib] |
||||
name = "degeon_core" |
||||
path = "src/lib.rs" |
||||
crate-type = ["cdylib", "rlib"] |
||||
|
@ -0,0 +1,89 @@
|
||||
use crate::message::{DegMessage, DegMessageContent, Profile}; |
||||
use ironforce::{Keys, PublicKey}; |
||||
use pyo3::prelude::*; |
||||
use serde::{Deserialize, Serialize}; |
||||
|
||||
/// A chat in the messenger
|
||||
///
|
||||
/// Contains user's profile and all the messages
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||
#[pyclass] |
||||
pub struct Chat { |
||||
/// Public key of the other user
|
||||
pub pkey: PublicKey, |
||||
/// Messages in this chat
|
||||
#[pyo3(get)] |
||||
pub messages: Vec<DegMessage>, |
||||
/// Profile of the other user
|
||||
#[pyo3(get, set)] |
||||
pub profile: Profile, |
||||
/// Scroll position
|
||||
pub scrolled: f32, |
||||
/// Message in the field
|
||||
pub input: String, |
||||
} |
||||
|
||||
impl Chat { |
||||
/// Create a new chat
|
||||
pub fn new(pkey: PublicKey) -> Self { |
||||
Self { |
||||
pkey, |
||||
messages: vec![], |
||||
profile: Profile { |
||||
name: "".to_string(), |
||||
bio: "".to_string(), |
||||
}, |
||||
scrolled: 0.0, |
||||
input: "".to_string(), |
||||
} |
||||
} |
||||
|
||||
/// Create an example chat
|
||||
pub fn example(i: usize, my_keys: &Keys) -> Chat { |
||||
// A dummy key
|
||||
let pkey = Keys::generate().get_public(); |
||||
let mut messages = vec![]; |
||||
if i == 0 { |
||||
for st in vec![ |
||||
"Welcome to Degeon!", |
||||
"It's a messenger app built on the IronForce decentralized network", |
||||
"Thanks to IronForce decentralized network, it's completely secure and anonymous", |
||||
"Your messages are encrypted and signed by you", |
||||
] { |
||||
messages.push(DegMessage { |
||||
sender: pkey.clone(), |
||||
timestamp: 0, |
||||
content: DegMessageContent::Text(st.to_string()), |
||||
}); |
||||
} |
||||
} else { |
||||
messages.push(DegMessage { |
||||
sender: pkey.clone(), |
||||
timestamp: 0, |
||||
content: DegMessageContent::Text( |
||||
"You can save information by sending messages in this chat:".to_string(), |
||||
), |
||||
}); |
||||
messages.push(DegMessage { |
||||
sender: my_keys.get_public(), |
||||
timestamp: 0, |
||||
content: DegMessageContent::Text("Example saved message".to_string()), |
||||
}); |
||||
} |
||||
Self { |
||||
messages, |
||||
profile: Profile { |
||||
name: (if i == 0 { |
||||
"Degeon Info" |
||||
} else { |
||||
"Saved messages" |
||||
}) |
||||
.to_string(), |
||||
bio: "".to_string(), |
||||
}, |
||||
scrolled: 0.0, |
||||
pkey, |
||||
input: "".to_string(), |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,388 @@
|
||||
use crate::chat::Chat; |
||||
use crate::gui_events::GuiEvent; |
||||
use crate::message::{Profile, ProtocolMsg}; |
||||
use crate::DegMessage; |
||||
use futures::Stream; |
||||
use ironforce::res::IFResult; |
||||
use ironforce::{IronForce, Keys, Message, MessageType, PublicKey}; |
||||
use pyo3::prelude::*; |
||||
use serde::{Deserialize, Serialize}; |
||||
use std::pin::Pin; |
||||
use std::sync::{Arc, Mutex}; |
||||
use std::task::{Context, Poll}; |
||||
|
||||
/// The container for logic, data, IF and protocol interactions
|
||||
#[derive(Clone)] |
||||
#[pyclass] |
||||
pub struct Degeon { |
||||
/// The list of all chats for this instance
|
||||
#[pyo3(get, set)] |
||||
pub chats: Vec<Chat>, |
||||
/// Profile of this user
|
||||
#[pyo3(get, set)] |
||||
pub profile: Profile, |
||||
/// Keys of this user
|
||||
pub keys: Keys, |
||||
/// The IF worker
|
||||
pub ironforce: Arc<Mutex<IronForce>>, |
||||
} |
||||
|
||||
/// Data for serialization
|
||||
#[derive(Serialize, Deserialize)] |
||||
pub struct DegeonData { |
||||
pub chats: Vec<Chat>, |
||||
pub profile: Profile, |
||||
pub keys: Keys, |
||||
} |
||||
|
||||
/// Load IF and launch the main loop
|
||||
///
|
||||
/// Returns ironforce and keys
|
||||
fn get_initialized_ironforce() -> (Arc<Mutex<IronForce>>, Keys) { |
||||
let ironforce = IronForce::from_file("".to_string()).unwrap(); |
||||
let keys = ironforce.keys.clone(); |
||||
println!("ID: {}", keys.get_public().get_short_id()); |
||||
let (_thread, ironforce) = ironforce.launch_main_loop(270); |
||||
(ironforce, keys) |
||||
} |
||||
|
||||
impl Default for Degeon { |
||||
fn default() -> Self { |
||||
let (ironforce, keys) = get_initialized_ironforce(); |
||||
let st = Self { |
||||
chats: vec![Chat::example(0, &keys), Chat::example(1, &keys)], |
||||
profile: Profile::default(), |
||||
keys, |
||||
ironforce, |
||||
}; |
||||
let self_clone = st.clone(); |
||||
std::thread::spawn(move || { |
||||
std::thread::sleep(std::time::Duration::from_secs(2)); |
||||
loop { |
||||
self_clone.send_multicast(ProtocolMsg::Ping).unwrap(); |
||||
std::thread::sleep(std::time::Duration::from_secs(120)); |
||||
} |
||||
}); |
||||
st |
||||
} |
||||
} |
||||
|
||||
impl Degeon { |
||||
/// Get profile for the current user
|
||||
pub fn get_profile(&self) -> Profile { |
||||
self.profile.clone() |
||||
} |
||||
|
||||
/// Find a chat in the list for a given public key
|
||||
pub fn chat_with(&self, pkey: &PublicKey) -> Option<usize> { |
||||
self.chats.iter().position(|chat| &chat.pkey == pkey) |
||||
} |
||||
|
||||
/// Process the incoming message and act accordingly
|
||||
pub fn process_message(&self, msg: ironforce::Message) -> IFResult<Option<GuiEvent>> { |
||||
let deg_msg: ProtocolMsg = |
||||
match serde_json::from_slice(msg.get_decrypted(&self.keys)?.as_slice()) { |
||||
Ok(r) => r, |
||||
Err(_) => return Ok(None), |
||||
}; |
||||
let sender = msg.get_sender(&self.keys).unwrap(); |
||||
println!( |
||||
"check_rec: {:?}, sender==self: {:?}", |
||||
msg.check_recipient(&self.keys), |
||||
sender == self.keys.get_public() |
||||
); |
||||
if !msg.check_recipient(&self.keys) || sender == self.keys.get_public() { |
||||
return Ok(None); |
||||
} |
||||
println!("{:?}", deg_msg); |
||||
Ok(match °_msg { |
||||
ProtocolMsg::NewMessage(deg_msg) => { |
||||
Some(GuiEvent::NewMessageInChat(sender, deg_msg.clone())) |
||||
} |
||||
ProtocolMsg::ProfileRequest | ProtocolMsg::Ping => { |
||||
Some(GuiEvent::WeHaveToSendProfile(sender)) |
||||
} |
||||
ProtocolMsg::ProfileResponse(prof) => Some(GuiEvent::SetProfile(sender, prof.clone())), |
||||
}) |
||||
} |
||||
|
||||
/// Send a multicast message through the network
|
||||
pub fn send_multicast(&self, msg: ProtocolMsg) -> IFResult<()> { |
||||
self.ironforce.lock().unwrap().send_to_all( |
||||
Message::build() |
||||
.message_type(MessageType::Broadcast) |
||||
.content(serde_json::to_vec(&msg)?) |
||||
.sign(&self.keys) |
||||
.build()?, |
||||
) |
||||
} |
||||
|
||||
/// Send a message to a target through the network
|
||||
pub fn send_message(&self, msg: ProtocolMsg, target: &PublicKey) -> IFResult<()> { |
||||
// if self.ironforce.lock().unwrap().get_tunnel(target).is_none() {
|
||||
// println!("Creating a tunnel");
|
||||
// self.ironforce
|
||||
// .lock()
|
||||
// .unwrap()
|
||||
// .initialize_tunnel_creation(target)?;
|
||||
// let mut counter = 0;
|
||||
// while self.ironforce.lock().unwrap().get_tunnel(target).is_none() {
|
||||
// std::thread::sleep(std::time::Duration::from_millis(350));
|
||||
// counter += 1;
|
||||
// if counter > 100 {
|
||||
// return Err(IFError::TunnelNotFound);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
println!("Sending: {:?}", msg); |
||||
self.ironforce.lock().unwrap().send_to_all( |
||||
Message::build() |
||||
.message_type(MessageType::Broadcast) |
||||
.content(serde_json::to_vec(&msg)?) |
||||
.recipient(target) |
||||
.sign(&self.keys) |
||||
.build()?, |
||||
) |
||||
} |
||||
|
||||
/// Created an iced command that sends a message to a target
|
||||
pub fn get_send_command( |
||||
&self, |
||||
msg: ProtocolMsg, |
||||
target: &PublicKey, |
||||
) -> iced::Command<GuiEvent> { |
||||
let if_clone = self.ironforce.clone(); |
||||
let target = target.clone(); |
||||
let keys = self.keys.clone(); |
||||
|
||||
println!("Creating a send command: {:?}", msg); |
||||
iced::Command::perform(async {}, move |_| { |
||||
println!("Sending message: {:?}", msg); |
||||
if_clone |
||||
.lock() |
||||
.unwrap() |
||||
.send_to_all( |
||||
Message::build() |
||||
.message_type(MessageType::Broadcast) |
||||
.content(serde_json::to_vec(&msg).unwrap()) |
||||
.recipient(&target) |
||||
.sign(&keys) |
||||
.build() |
||||
.unwrap(), |
||||
) |
||||
.unwrap(); |
||||
GuiEvent::None |
||||
}) |
||||
} |
||||
|
||||
/// Created an iced command that sends a message to everybody
|
||||
pub fn get_broadcast_send_command( |
||||
&self, |
||||
msg: ProtocolMsg, |
||||
) -> iced::Command<GuiEvent> { |
||||
let if_clone = self.ironforce.clone(); |
||||
let keys = self.keys.clone(); |
||||
|
||||
println!("Creating a send command: {:?}", msg); |
||||
iced::Command::perform(async {}, move |_| { |
||||
println!("Sending message: {:?}", msg); |
||||
if_clone |
||||
.lock() |
||||
.unwrap() |
||||
.send_to_all( |
||||
Message::build() |
||||
.message_type(MessageType::Broadcast) |
||||
.content(serde_json::to_vec(&msg).unwrap()) |
||||
.sign(&keys) |
||||
.build() |
||||
.unwrap(), |
||||
) |
||||
.unwrap(); |
||||
GuiEvent::None |
||||
}) |
||||
} |
||||
|
||||
/// Process a GuiEvent if it's connected to the worker (like sending a message)
|
||||
pub fn process_event(&mut self, event: &GuiEvent, perform_sending: bool) -> IFResult<()> { |
||||
match event { |
||||
GuiEvent::NewMessageInChat(pkey, msg) => { |
||||
if self.chat_with(&pkey).is_none() { |
||||
self.chats.push(Chat::new(pkey.clone())) |
||||
} |
||||
let ind = self.chat_with(&pkey).unwrap(); |
||||
self.chats[ind].messages.push(msg.clone()); |
||||
self.save_to_file("".to_string())?; |
||||
} |
||||
GuiEvent::SetProfile(pkey, profile) => { |
||||
if self.chat_with(&pkey).is_none() { |
||||
self.chats.push(Chat::new(pkey.clone())) |
||||
} |
||||
let ind = self.chat_with(&pkey).unwrap(); |
||||
self.chats[ind].profile = profile.clone(); |
||||
self.save_to_file("".to_string())?; |
||||
} |
||||
GuiEvent::WeHaveToSendProfile(target) if perform_sending => { |
||||
let target = target.clone(); |
||||
let self_cloned = self.clone(); |
||||
std::thread::spawn(move || { |
||||
self_cloned.send_message( |
||||
ProtocolMsg::ProfileResponse(self_cloned.get_profile()), |
||||
&target, |
||||
) |
||||
}); |
||||
} |
||||
_ => {} |
||||
} |
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl Degeon { |
||||
/// Create a new text message and send it (in thread)
|
||||
///
|
||||
/// `text` is the content of the message,
|
||||
/// `chat_i` is the index of the chat in the list
|
||||
pub fn send_text_message(&mut self, text: String, chat_i: usize) { |
||||
let msg = DegMessage::new_text(text, &self.keys.get_public()); |
||||
self.chats[chat_i].messages.push(msg.clone()); |
||||
let cloned_self = self.clone(); |
||||
std::thread::spawn(move || { |
||||
cloned_self |
||||
.send_message( |
||||
ProtocolMsg::NewMessage(msg), |
||||
&cloned_self.chats[chat_i].pkey, |
||||
) |
||||
.map_err(|e| println!("There was an error in Rust: {:?}", e)) |
||||
}); |
||||
} |
||||
|
||||
/// Handle one message
|
||||
pub fn handling_loop_iteration(&mut self) { |
||||
let event = self.read_message_and_create_event(); |
||||
if let Some(event) = event { |
||||
self.process_event(&event, true) |
||||
.unwrap_or_else(|e| println!("Error: {:?}", e)); |
||||
} |
||||
} |
||||
|
||||
/// Get length of the IF's message queue
|
||||
///
|
||||
/// If IF worker is locked, returns 0
|
||||
pub fn message_queue_len(&self) -> usize { |
||||
self.ironforce.try_lock().map(|r| r.messages.len()).unwrap_or(0) |
||||
} |
||||
|
||||
/// Check if the message was written by the current user (since `PublicKey` isn't a python type).
|
||||
/// Returns True if the author of the message is the current user
|
||||
pub fn check_message_ownership(&self, message: &DegMessage) -> bool { |
||||
message.sender == self.keys.get_public() |
||||
} |
||||
} |
||||
|
||||
pub const DEFAULT_FILENAME: &str = ".degeon.json"; |
||||
|
||||
impl Degeon { |
||||
/// Store most of the necessary data to string
|
||||
pub fn serialize_to_string(&self) -> serde_json::Result<String> { |
||||
let data = DegeonData { |
||||
chats: self.chats.clone(), |
||||
profile: self.get_profile(), |
||||
keys: self.keys.clone(), |
||||
}; |
||||
serde_json::to_string(&data) |
||||
} |
||||
|
||||
/// Restore `Degeon` from serialized data
|
||||
pub fn restore_from_string(data: String) -> IFResult<Self> { |
||||
let data_res: serde_json::Result<DegeonData> = serde_json::from_str(data.as_str()); |
||||
let data = match data_res { |
||||
Ok(r) => r, |
||||
Err(_) => return Ok(Self::default()), |
||||
}; |
||||
let (ironforce, _keys) = get_initialized_ironforce(); |
||||
ironforce.lock().unwrap().keys = data.keys.clone(); |
||||
let deg = Degeon { |
||||
chats: data.chats, |
||||
profile: data.profile, |
||||
keys: data.keys, |
||||
ironforce, |
||||
}; |
||||
Ok(deg) |
||||
} |
||||
|
||||
/// Save to a file. If no filename is provided, the default is used
|
||||
pub fn save_to_file(&self, filename: String) -> IFResult<()> { |
||||
let data = self.serialize_to_string()?; |
||||
let filename = if filename.is_empty() { |
||||
DEFAULT_FILENAME.to_string() |
||||
} else { |
||||
filename |
||||
}; |
||||
std::fs::write(filename, data)?; |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Restore from a file. If no filename is provided, the default is used
|
||||
pub fn restore_from_file(filename: String) -> IFResult<Self> { |
||||
let filename = if filename.is_empty() { |
||||
DEFAULT_FILENAME.to_string() |
||||
} else { |
||||
filename |
||||
}; |
||||
let content = std::fs::read_to_string(filename).unwrap_or_default(); |
||||
Self::restore_from_string(content) |
||||
} |
||||
} |
||||
|
||||
impl Degeon { |
||||
/// Get one message from the IF message queue and process it, resulting in an event
|
||||
pub fn read_message_and_create_event(&self) -> Option<GuiEvent> { |
||||
let msg_raw = self.ironforce.lock().unwrap().read_message(); |
||||
let msg = msg_raw |
||||
.as_ref() |
||||
.map(|msg| self.process_message(msg.clone()).unwrap()); |
||||
if msg_raw.is_some() { |
||||
let msg_deg: ProtocolMsg = match serde_json::from_slice( |
||||
msg_raw |
||||
.as_ref() |
||||
.unwrap() |
||||
.get_decrypted(&self.keys) |
||||
.unwrap() |
||||
.as_slice(), |
||||
) { |
||||
Ok(r) => r, |
||||
Err(_) => { |
||||
println!("Couldn't deserialize {:?}", msg_raw); |
||||
return Some(GuiEvent::None); |
||||
} |
||||
}; |
||||
println!("{:?} -> {:?}", msg_deg, msg); |
||||
} |
||||
match msg { |
||||
Some(Some(event)) => Some(event), |
||||
_ => None, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Stream for Degeon { |
||||
type Item = GuiEvent; |
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { |
||||
if self.ironforce.lock().unwrap().messages.is_empty() { |
||||
let waker = cx.waker().clone(); |
||||
std::thread::spawn(move || { |
||||
std::thread::sleep(std::time::Duration::from_millis(1000)); |
||||
waker.wake(); |
||||
}); |
||||
return Poll::Pending; |
||||
} |
||||
let timestamp_0 = std::time::Instant::now(); |
||||
let msg = self.read_message_and_create_event(); |
||||
if timestamp_0.elapsed() > std::time::Duration::from_millis(800) { |
||||
println!("Poll_next took {:?}", timestamp_0.elapsed()); |
||||
} |
||||
Poll::Ready(Some(msg.unwrap_or(GuiEvent::None))) |
||||
} |
||||
} |
@ -0,0 +1,54 @@
|
||||
use crate::message::{DegMessage, Profile}; |
||||
use ironforce::PublicKey; |
||||
use serde::{Serialize, Deserialize}; |
||||
|
||||
|
||||
/// The screens (pages) of the app
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] |
||||
pub enum AppScreen { |
||||
/// The one with the chats
|
||||
Main, |
||||
/// Settings and profile
|
||||
ProfileEditor, |
||||
/// The screen that appears if no peers are available and asks for the IP address of at least one peer
|
||||
#[allow(dead_code)] |
||||
PeerInput, |
||||
} |
||||
|
||||
|
||||
impl Default for AppScreen { |
||||
fn default() -> Self { |
||||
AppScreen::Main |
||||
} |
||||
} |
||||
|
||||
/// An enum with all possible events for this application
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||
pub enum GuiEvent { |
||||
/// Selection of a chat
|
||||
ChatSelect(usize), |
||||
/// The user changed the value of "new message" field
|
||||
Typed(String), |
||||
/// The user clicked "Send"
|
||||
SendMessage, |
||||
/// A new messaged arrived
|
||||
NewMessageInChat(PublicKey, DegMessage), |
||||
/// A profile response arrived and we should store it
|
||||
SetProfile(PublicKey, Profile), |
||||
/// We should send profile (in response to profile request)
|
||||
WeHaveToSendProfile(PublicKey), |
||||
/// Go to another screen
|
||||
ChangeScreen(AppScreen), |
||||
/// Changed the name in the field on profile screen
|
||||
ChangeName(String), |
||||
/// Changed the bio in the field on profile screen
|
||||
ChangeBio(String), |
||||
/// Nothing happened
|
||||
None, |
||||
} |
||||
|
||||
impl GuiEvent { |
||||
pub fn get_json(&self) -> String { |
||||
serde_json::to_string(&self).unwrap() |
||||
} |
||||
} |
@ -0,0 +1,44 @@
|
||||
extern crate serde; |
||||
extern crate pyo3; |
||||
mod chat; |
||||
mod degeon_worker; |
||||
mod gui_events; |
||||
mod message; |
||||
|
||||
pub use chat::Chat; |
||||
pub use degeon_worker::{Degeon, DegeonData, DEFAULT_FILENAME}; |
||||
pub use message::{DegMessage, Profile, ProtocolMsg, DegMessageContent, DegMessageContentPy}; |
||||
pub use gui_events::{AppScreen, GuiEvent}; |
||||
use pyo3::prelude::*; |
||||
use pyo3::wrap_pyfunction; |
||||
|
||||
#[pyfunction] |
||||
fn new_degeon() -> PyResult<Degeon> { |
||||
Ok(Degeon::restore_from_file("".to_string()).unwrap()) |
||||
} |
||||
|
||||
/// Check if there is stored Degeon profile here
|
||||
#[pyfunction] |
||||
fn is_data_available() -> bool { |
||||
std::path::Path::new(DEFAULT_FILENAME).exists() |
||||
} |
||||
|
||||
/// Create a new profile with name
|
||||
#[pyfunction] |
||||
fn new_degeon_with_name(name: String) -> PyResult<Degeon> { |
||||
let mut deg = Degeon::default(); |
||||
deg.profile.name = name; |
||||
Ok(deg) |
||||
} |
||||
|
||||
/// Define structs and functions that are exported into python
|
||||
#[pymodule] |
||||
fn degeon_core(_py: Python, m: &PyModule) -> PyResult<()> { |
||||
m.add_class::<Degeon>()?; |
||||
m.add_class::<DegMessageContentPy>()?; |
||||
m.add_class::<DegMessage>()?; |
||||
m.add_function(wrap_pyfunction!(new_degeon, m)?)?; |
||||
m.add_function(wrap_pyfunction!(is_data_available, m)?)?; |
||||
m.add_function(wrap_pyfunction!(new_degeon_with_name, m)?)?; |
||||
Ok(()) |
||||
} |
@ -0,0 +1,125 @@
|
||||
use ironforce::PublicKey; |
||||
use pyo3::prelude::*; |
||||
use serde::{Deserialize, Serialize}; |
||||
|
||||
/// A message in the messenger
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||
#[pyclass] |
||||
pub struct DegMessage { |
||||
/// Sender's public key
|
||||
pub sender: PublicKey, |
||||
/// UNIX epoch time at which this message was created
|
||||
pub timestamp: i64, |
||||
/// Message content
|
||||
pub content: DegMessageContent, |
||||
} |
||||
|
||||
impl DegMessage { |
||||
/// Create a simple text message
|
||||
pub fn new_text(text: String, my_key: &PublicKey) -> DegMessage { |
||||
Self { |
||||
sender: my_key.clone(), |
||||
timestamp: chrono::Utc::now().timestamp(), |
||||
content: DegMessageContent::Text(text), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl DegMessage { |
||||
pub fn get_content_py(&self) -> DegMessageContentPy { |
||||
self.content.get_py() |
||||
} |
||||
} |
||||
|
||||
/// The content of the message
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||
pub enum DegMessageContent { |
||||
/// A text message
|
||||
Text(String), |
||||
File(Vec<u8>), |
||||
Service, |
||||
} |
||||
|
||||
/// User's profile
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)] |
||||
#[pyclass] |
||||
pub struct Profile { |
||||
/// User's name
|
||||
#[pyo3(get, set)] |
||||
pub name: String, |
||||
/// User's description
|
||||
#[pyo3(get, set)] |
||||
pub bio: String, |
||||
} |
||||
|
||||
/// A protocol message (that's sent through IF)
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||
pub enum ProtocolMsg { |
||||
/// Requesting profile
|
||||
ProfileRequest, |
||||
/// Responding to the profile request with a profile
|
||||
ProfileResponse(Profile), |
||||
/// A peer discovery
|
||||
Ping, |
||||
/// A message is sent
|
||||
NewMessage(DegMessage), |
||||
} |
||||
|
||||
/// DegMessageContent, but a struct for python
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||
#[pyclass] |
||||
pub struct DegMessageContentPy { |
||||
/// If `DegMessageContent` is `Text(st)`, then this is `Some(st)`
|
||||
#[pyo3(get, set)] |
||||
pub text: Option<String>, |
||||
/// If `DegMessageContent` is `File(data)`, then this is `Some(data)`
|
||||
#[pyo3(get, set)] |
||||
pub file: Option<Vec<u8>>, |
||||
} |
||||
|
||||
impl DegMessageContent { |
||||
/// Convert from `DegMessageContent` to the corresponding `DegMessageContentPy`
|
||||
pub fn get_py(&self) -> DegMessageContentPy { |
||||
match self { |
||||
DegMessageContent::Text(text) => DegMessageContentPy { |
||||
text: Some(text.clone()), |
||||
file: None, |
||||
}, |
||||
DegMessageContent::File(data) => DegMessageContentPy { |
||||
text: None, |
||||
file: Some(data.clone()), |
||||
}, |
||||
DegMessageContent::Service => DegMessageContentPy { |
||||
text: None, |
||||
file: None, |
||||
}, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl DegMessageContentPy { |
||||
/// Convert from `DegMessageContentPy` to the corresponding `DegMessageContent`
|
||||
pub fn to_enum(&self) -> DegMessageContent { |
||||
match self { |
||||
DegMessageContentPy { |
||||
text: Some(text), .. |
||||
} => DegMessageContent::Text(text.clone()), |
||||
DegMessageContentPy { |
||||
file: Some(data), .. |
||||
} => DegMessageContent::File(data.clone()), |
||||
_ => DegMessageContent::Service, |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl DegMessageContentPy { |
||||
#[staticmethod] |
||||
pub fn new_text(text: String) -> Self { |
||||
Self { |
||||
text: Some(text), |
||||
file: None, |
||||
} |
||||
} |
||||
} |
Binary file not shown.
@ -0,0 +1,104 @@
|
||||
from __future__ import annotations |
||||
|
||||
import typing |
||||
from dataclasses import dataclass, field |
||||
import pygame |
||||
|
||||
from button import Button |
||||
from config import HEIGHT, WIDTH, CHAT_SELECTOR_WIDTH, DARKER_BLUE, CHAT_PREVIEW_HEIGHT, WHITE, font, DARK_BLUE, \ |
||||
MESSAGE_HEIGHT |
||||
import degeon_core as dc |
||||
|
||||
from input_field import TextField |
||||
from message import Message |
||||
|
||||
|
||||
@dataclass |
||||
class ActiveChat: |
||||
""" |
||||
The widget with the current chat |
||||
|
||||
Attributes: |
||||
:param chat (dc.Chat): the chat (Rust Chat type) |
||||
:param height (int): the height of the active chat widget |
||||
:param width (int): its width |
||||
:param delta_x (int): distance from the left edge of the application to the left edge of the screen |
||||
:param header_height (int): height of the header (the title) |
||||
""" |
||||
chat: dc.Chat |
||||
input_field: TextField = field(default_factory=lambda: TextField(placeholder='New message')) |
||||
send_button: Button = field(default_factory=lambda: Button(height=CHAT_PREVIEW_HEIGHT, width=100, text='Send', |
||||
top_left=(WIDTH - 112, HEIGHT - 85))) |
||||
height: int = HEIGHT |
||||
width: int = WIDTH - CHAT_SELECTOR_WIDTH - 10 |
||||
delta_x: int = CHAT_SELECTOR_WIDTH + 10 |
||||
header_height: int = int(CHAT_PREVIEW_HEIGHT * 1.5) |
||||
|
||||
@classmethod |
||||
def new(cls, chat: dc.Chat, **kwargs) -> ActiveChat: |
||||
""" |
||||
Create a new `Chat` from a rust Chat object |
||||
:param chat: rusty chat |
||||
:param kwargs: optional other paraeters |
||||
:return: the `Chat` |
||||
""" |
||||
return cls(chat=chat, **kwargs) |
||||
|
||||
def get_messages(self, core: dc.Degeon) -> typing.Iterable[Message]: |
||||
""" |
||||
Get an iterator over all messages in this chat in the backwards order |
||||
This function creates a python `message.Message` object from rust instances |
||||
:return: an iterator of `message.Message` objects |
||||
""" |
||||
for msg in reversed(self.chat.messages): |
||||
yield Message(text=msg.get_content_py().text, is_from_me=core.check_message_ownership(msg)) |
||||
|
||||
def get_header(self) -> pygame.Surface: |
||||
""" |
||||
Render a pygame surface with the header. |
||||
The header is (for now) just a name of the user written on a background |
||||
|
||||
:return: the header |
||||
""" |
||||
surface: pygame.Surface = pygame.Surface((self.width, self.header_height)) |
||||
surface.fill(DARK_BLUE) |
||||
name_surface: pygame.Surface = font.render(self.chat.profile.name, True, WHITE) |
||||
surface.blit(name_surface, (20, 20)) |
||||
return surface |
||||
|
||||
def render(self, core: dc.Degeon) -> pygame.Surface: |
||||
""" |
||||
Creates a pygame surface and draws the chat on it |
||||
:return: the surface with the chat on it |
||||
""" |
||||
surface: pygame.Surface = pygame.Surface((self.width, self.height)) |
||||
surface.fill(DARKER_BLUE) |
||||
|
||||
# Render messages |
||||
# This is the y0 for the last message |
||||
last_message_y = self.height - MESSAGE_HEIGHT * 2 |
||||
for i, message in zip(range(30), self.get_messages(core)): |
||||
msg_surface = message.render() |
||||
last_message_y -= msg_surface.get_height() + 30 |
||||
surface.blit(msg_surface, (0, last_message_y - 30)) |
||||
# Render header |
||||
header = self.get_header() |
||||
surface.blit(header, (0, 10)) |
||||
# Render message input |
||||
input_field_surface: pygame.Surface = self.input_field.render() |
||||
surface.blit(input_field_surface, (0, self.height - input_field_surface.get_height())) |
||||
# Render sending button |
||||
sending_button_surface: pygame.Surface = self.send_button.render() |
||||
surface.blit(sending_button_surface, |
||||
(self.send_button.top_left[0] - self.delta_x, self.send_button.top_left[1])) |
||||
return surface |
||||
|
||||
def process_event(self, event: pygame.event.Event) -> typing.Optional[str]: |
||||
""" |
||||
Process clicks in the active chat widget. Return a message to send if needed |
||||
:param event: a pygame event |
||||
:return: A message to send if there is one |
||||
""" |
||||
self.input_field.process_event(event) |
||||
if self.send_button.process_event(event): |
||||
return self.input_field.collect() |
@ -0,0 +1,101 @@
|
||||
import pygame |
||||
import typing |
||||
|
||||
from config import font_large, WHITE, BLUE, BLACK |
||||
from dataclasses import dataclass |
||||
|
||||
|
||||
@dataclass |
||||
class Button: |
||||
""" |
||||
Just a button element for pygame |
||||
Attributes: |
||||
:param text (str): Text on the button |
||||
:param top_left ((int, int)): the coordinates of the top-left point of the button |
||||
:param width (int): the width of the button |
||||
:param height (int): the height of the button |
||||
:param padding (int): padding - the distance between rectangle border and button content |
||||
:param bg_color (Color): the background color |
||||
:param text_color (Color): color of the text |
||||
:param hovered_color (Color): the background color when the button is hovered |
||||
:param hovered_text_color (Color): the text color when the button is hovered |
||||
:param pressed_color (Color): the background color when the button is pressed |
||||
:param pressed_text_color (Color): the text color when the button is pressed |
||||
""" |
||||
text: str |
||||
top_left: typing.Tuple[int, int] |
||||
width: int |
||||
height: int |
||||
padding: int = 13 |
||||
bg_color: pygame.Color = BLUE |
||||
text_color: pygame.Color = WHITE |
||||
hovered_color: pygame.Color = (BLUE * 3 + WHITE) // 4 |
||||
hovered_text_color: pygame.Color = BLACK |
||||
pressed_color: pygame.Color = (BLUE + WHITE * 3) // 4 |
||||
pressed_text_color: pygame.Color = BLACK |
||||
is_hovered: bool = False |
||||
is_pressed: bool = False |
||||
|
||||
@property |
||||
def bottom(self) -> int: |
||||
""" |
||||
Return the y coordinate of the bottom of the button |
||||
:return: y_bottom |
||||
""" |
||||
return self.top_left[1] + self.height |
||||
|
||||
@property |
||||
def right(self) -> int: |
||||
""" |
||||
Return the x coordinate of the right edge of the button |
||||
:return: x_right |
||||
""" |
||||
return self.top_left[0] + self.width |
||||
|
||||
def get_colors(self) -> typing.Tuple[pygame.Color, pygame.Color]: |
||||
""" |
||||
Get background and text color considering hovered and pressed states |
||||
:return: the button's background color and the button's text color |
||||
""" |
||||
if self.is_pressed: |
||||
return self.pressed_color, self.pressed_text_color |
||||
if self.is_hovered: |
||||
return self.hovered_color, self.hovered_text_color |
||||
return self.bg_color, self.text_color |
||||
|
||||
def render(self) -> pygame.Surface: |
||||
""" |
||||
Draw the button |
||||
:return: a pygame surface with the button |
||||
""" |
||||
surface: pygame.Surface = pygame.Surface((self.width, self.height)) |
||||
bg_color, text_color = self.get_colors() |
||||
surface.fill(bg_color) |
||||
text_surface: pygame.Surface = font_large.render(self.text, True, text_color) |
||||
text_height: int = self.height - 2 * self.padding |
||||
text_width: int = round(text_surface.get_width() * text_height / text_surface.get_height()) |
||||
text_surface: pygame.Surface = pygame.transform.scale(text_surface, (text_width, text_height)) |
||||
surface.blit(text_surface, ((self.width - text_width) // 2, self.padding)) |
||||
return surface |
||||
|
||||
def process_event(self, event: pygame.event.Event) -> bool: |
||||
""" |
||||
Process a pygame event. If it's a click on this button, return true |
||||
:param event: a pygame event |
||||
:return: True if there was a click on the button |
||||
""" |
||||
# if this is a mouse event |
||||
if event.type in [pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION, pygame.MOUSEBUTTONDOWN]: |
||||
# if the mouse event is inside the button |
||||
if self.top_left[0] <= event.pos[0] < self.right and self.top_left[1] <= event.pos[1] < self.bottom: |
||||
if event.type == pygame.MOUSEBUTTONUP: |
||||
self.is_pressed = False |
||||
return True |
||||
elif event.type == pygame.MOUSEBUTTONDOWN: |
||||
self.is_hovered = False |
||||
self.is_pressed = True |
||||
elif event.type == pygame.MOUSEMOTION: |
||||
self.is_hovered = True |
||||
else: |
||||
self.is_hovered = False |
||||
return False |
@ -0,0 +1,76 @@
|
||||
from __future__ import annotations |
||||
|
||||
import typing |
||||
from dataclasses import dataclass, field |
||||
|
||||
import pygame |
||||
|
||||
from config import CHAT_SELECTOR_WIDTH, HEIGHT, DARK_BLUE, WHITE, GREY, BLUE, font, CHAT_PREVIEW_HEIGHT, MEDIUM_BLUE |
||||
|
||||
|
||||
@dataclass |
||||
class ChatSelector: |
||||
""" |
||||
The widget with the list of chats. |
||||
It's a dataclass, it should be initialized using the `from_chats` classmethod |
||||
|
||||
Attributes: |
||||
chats (List[dc.Chat]): list of all chats, where each chat is a native Rust struct Chat |
||||
active_chat (int): the index of the current selected chat |
||||
hovered_chat (int or None): the index of the current hovered chat |
||||
width (int): the width of this widget |
||||
height (int): the height of this widget |
||||
chat_height (int): height of one chat |
||||
""" |
||||
chats: typing.List['dc.Chat'] = field(default_factory=list) |
||||
active_chat: int = -1 |
||||
hovered_chat: typing.Optional[int] = None |
||||
width: int = CHAT_SELECTOR_WIDTH |
||||
height: int = HEIGHT |
||||
chat_height: int = CHAT_PREVIEW_HEIGHT |
||||
|
||||
def render(self) -> pygame.Surface: |
||||
""" |
||||
Creates a pygame surface and draws the list of chats on it |
||||
:return: the surface with the chat selector |
||||
""" |
||||
surface: pygame.Surface = pygame.Surface((self.width, self.height)) |
||||
surface.fill(GREY) |
||||
for i, chat in enumerate(self.chats): |
||||
bg_color, text_color = DARK_BLUE, WHITE |
||||
if i == self.hovered_chat: |
||||
bg_color = MEDIUM_BLUE |
||||
if i == self.active_chat: |
||||
bg_color = BLUE |
||||
title_surface: pygame.Surface = font.render(chat.profile.name, True, text_color) |
||||
pygame.draw.rect(surface, bg_color, (3, i * self.chat_height + 1, self.width - 6, self.chat_height - 2)) |
||||
surface.blit(title_surface, (7, i * self.chat_height + 10)) |
||||
return surface |
||||
|
||||
def process_event(self, event: pygame.event.Event) -> bool: |
||||
""" |
||||
Process a click: select the necessary chat if this click is in the widget |
||||
:param event: a pygame event |
||||
:return: True if a chat was changed |
||||
""" |
||||
if event.type == pygame.MOUSEMOTION: |
||||
if 0 < event.pos[0] < self.width \ |
||||
and 0 < event.pos[1] < min(self.height, len(self.chats) * self.chat_height) - 2: |
||||
self.hovered_chat = event.pos[1] // self.chat_height |
||||
else: |
||||
self.hovered_chat = None |
||||
|
||||
if event.type == pygame.MOUSEBUTTONUP and event.pos[0] < self.width: |
||||
self.hovered_chat = None |
||||
self.active_chat = event.pos[1] // self.chat_height |
||||
return True |
||||
return False |
||||
|
||||
@classmethod |
||||
def from_chats(cls, chats: typing.List['dc.Chat'], **kwargs) -> ChatSelector: |
||||
""" |
||||
Create a new ChatSelector from a list of Rust chats |
||||
:param chats: the list of chats |
||||
:param kwargs: optional additional arguments |
||||
""" |
||||
return cls(chats, **kwargs) |
@ -0,0 +1,32 @@
|
||||
import pygame |
||||
|
||||
|
||||
pygame.init() |
||||
|
||||
# Fonts |
||||
font_medium = pygame.font.Font('CRC35.otf', 25) |
||||
font = pygame.font.Font('CRC35.otf', 32) |
||||
font_large = pygame.font.Font('CRC35.otf', 50) |
||||
|
||||
# Colors used in the app |
||||
RED = 0xFF0000 |
||||
BLUE = 0x0000FF |
||||
YELLOW = 0xFFC91F |
||||
GREEN = 0x00FF00 |
||||
MAGENTA = 0xFF03B8 |
||||
CYAN = 0x00FFCC |
||||
BLACK = 0x000 |
||||
WHITE = 0xFFFFFF |
||||
MEDIUM_BLUE = 0x2f2f4e |
||||
DARK_BLUE = 0x282e46 |
||||
DARKER_BLUE = 0x202033 |
||||
GREY = 0x383e4F |
||||
|
||||
# Geometrical parameters |
||||
WIDTH = 1000 |
||||
HEIGHT = 800 |
||||
CHAT_PREVIEW_HEIGHT = 80 |
||||
CHAT_SELECTOR_WIDTH = WIDTH // 3 |
||||
MESSAGE_HEIGHT = 60 |
||||
|
||||
FPS = 30 |
@ -0,0 +1,142 @@
|
||||
from __future__ import annotations |
||||
|
||||
from dataclasses import dataclass, field |
||||
import typing |
||||
import pygame |
||||
|
||||
from button import Button |
||||
from chat_selector import ChatSelector |
||||
from active_chat import ActiveChat |
||||
from config import FPS, DARKER_BLUE, font, WHITE, WIDTH, CHAT_SELECTOR_WIDTH, HEIGHT |
||||
|
||||
import degeon_core as dc |
||||
|
||||
from input_field import TextField |
||||
|
||||
|
||||
@dataclass |
||||
class Degeon: |
||||
""" |
||||
The main class with everything connected to the app: the data, the |
||||
|
||||
Attributes: |
||||
:param core (dc.Degeon): the rust worker |
||||
:param chat_selector (ChatSelector): chat list widget |
||||
:param active_chat (typing.Optional[ActiveChat]): current chat widget |
||||
:param fps (int): FPS rate |
||||
""" |
||||
core: typing.Optional[dc.Degeon] |
||||
chat_selector: ChatSelector |
||||
active_chat: typing.Optional[ActiveChat] = None |
||||
name_input_field: TextField = field( |
||||
default_factory=lambda: TextField(placeholder='Name', top_left_corner=(WIDTH // 5, HEIGHT // 3))) |
||||
name_input_button: Button = field(default_factory=lambda: Button(text='Done', width=200, height=100, |
||||
top_left=(round(2 * WIDTH / 5), 2 * HEIGHT // 3))) |
||||
has_profile_popup_opened: bool = False |
||||
has_no_peers_popup: bool = False |
||||
clock: pygame.time.Clock = field(default_factory=pygame.time.Clock) |
||||
fps: int = FPS |
||||
|
||||
@classmethod |
||||
def new(cls) -> Degeon: |
||||
""" |
||||
Create a new default instance with settings from file |
||||
:return: a Degeon instance |
||||
""" |
||||
if dc.is_data_available(): |
||||
core: dc.Degeon = dc.new_degeon() |
||||
else: |
||||
# core: dc.Degeon = dc.new_degeon_with_name(input('Enter name: ')) |
||||
core = None |
||||
chat_selector = ChatSelector() |
||||
return cls(core=core, chat_selector=chat_selector) |
||||
|
||||
def register(self, name: str): |
||||
""" |
||||
Create new rust-Degeon instance with name |
||||
:param name: user's name |
||||
""" |
||||
self.core = dc.new_degeon_with_name(name) |
||||
self.chat_selector.chats = self.core.chats |
||||
|
||||
def render(self, screen: pygame.Surface): |
||||
""" |
||||
Render everything on the screen |
||||
:param screen: the main screen |
||||
""" |
||||
if self.core is None: |
||||
screen.fill(DARKER_BLUE) |
||||
button_surface: pygame.Surface = self.name_input_button.render() |
||||
screen.blit(button_surface, self.name_input_button.top_left) |
||||
name_field_surface: pygame.Surface = self.name_input_field.render() |
||||
screen.blit(name_field_surface, self.name_input_field.top_left_corner) |
||||
return |
||||
chats_surface = self.chat_selector.render() |
||||
screen.blit(chats_surface, (0, 0)) |
||||
if self.active_chat is not None: |
||||
active_chat_surface = self.active_chat.render(self.core) |
||||
screen.blit(active_chat_surface, (self.active_chat.delta_x, 0)) |
||||
else: |
||||
text_surface: pygame.Surface = font.render('<- Select chat in the menu', True, WHITE) |
||||
screen.blit(text_surface, |
||||
(round(WIDTH / 2 + CHAT_SELECTOR_WIDTH / 2 - text_surface.get_width() / 2), HEIGHT // 2)) |
||||
|
||||
def process_core_messages(self): |
||||
""" |
||||
Do all the necessary Rust work |
||||
""" |
||||
if self.core is None: |
||||
return |
||||
while self.core.message_queue_len(): |
||||
self.core.handling_loop_iteration() |
||||
|
||||
def tick(self): |
||||
""" |
||||
Handle incoming messages, update chats, create no_peers popup if necessary |
||||
""" |
||||
if self.core is None: |
||||
return |
||||
# process events in core |
||||
self.process_core_messages() |
||||
if self.core is not None: |
||||
self.chat_selector.chats = self.core.chats |
||||
if 0 <= self.chat_selector.active_chat < len(self.chat_selector.chats) and self.active_chat is None: |
||||
self.active_chat = ActiveChat.new(self.chat_selector.chats[self.chat_selector.active_chat]) |
||||
if self.active_chat is not None and self.core is not None: |
||||
self.active_chat.chat = self.core.chats[self.chat_selector.active_chat] |
||||
|
||||
def process_event(self, event: pygame.event.Event): |
||||
""" |
||||
Process an event |
||||
:param event: pygame event |
||||
""" |
||||
if self.core is None: |
||||
self.name_input_field.process_event(event) |
||||
if self.name_input_button.process_event(event): |
||||
self.register(self.name_input_field.value) |
||||
return |
||||
if self.chat_selector.process_event(event): |
||||
if 0 <= self.chat_selector.active_chat < len(self.chat_selector.chats): |
||||
self.active_chat = ActiveChat.new(self.chat_selector.chats[self.chat_selector.active_chat]) |
||||
else: |
||||
self.active_chat = None |
||||
if self.active_chat is not None: |
||||
# If the result is a string, it's a message |
||||
result: typing.Optional[str] = self.active_chat.process_event(event) |
||||
if result: |
||||
self.core.send_text_message(result, self.chat_selector.active_chat) |
||||
|
||||
def main_loop(self, screen: pygame.Surface): |
||||
""" |
||||
Drawing everything and handling events |
||||
""" |
||||
while True: |
||||
screen.fill(DARKER_BLUE) |
||||
for event in pygame.event.get(): |
||||
if event.type == pygame.QUIT: |
||||
return |
||||
self.process_event(event) |
||||
self.tick() |
||||
self.render(screen) |
||||
self.clock.tick(self.fps) |
||||
pygame.display.update() |
Binary file not shown.
@ -0,0 +1,77 @@
|
||||
import time |
||||
import typing |
||||
|
||||
import pygame |
||||
|
||||
from config import font, MESSAGE_HEIGHT, WIDTH, CHAT_SELECTOR_WIDTH, HEIGHT, WHITE, DARKER_BLUE, GREY |
||||
from utils import render_text |
||||
|
||||
from dataclasses import dataclass |
||||
|
||||
|
||||
@dataclass |
||||
class TextField: |
||||
""" |
||||
Field for message input |
||||
""" |
||||
value: str = '' |
||||
width: int = WIDTH - CHAT_SELECTOR_WIDTH - 140 |
||||
height: int = MESSAGE_HEIGHT * 1.5 |
||||
top_left_corner: typing.Tuple[int, int] = (CHAT_SELECTOR_WIDTH, HEIGHT - MESSAGE_HEIGHT * 2) |
||||
is_focused: bool = False |
||||
placeholder: str = '' |
||||
cursor_position: int = 0 |
||||
|
||||
def render(self) -> pygame.Surface: |
||||
""" |
||||
Render the text field onto a pygame surface |
||||
:return: a surface with the field |
||||
""" |
||||
surface = pygame.Surface((self.width, self.height)) |
||||
surface.fill(WHITE) |
||||
padding = 5 |
||||
pygame.draw.rect(surface, DARKER_BLUE, (padding, padding, self.width - padding * 2, self.height - padding * 2)) |
||||
if not self.value and self.placeholder: |
||||
placeholder_text: pygame.Surface = font.render(self.placeholder, True, (GREY + WHITE) // 2) |
||||
surface.blit( |
||||
placeholder_text, |
||||
( |
||||
(self.width - placeholder_text.get_width()) // 2, |
||||
(self.height - placeholder_text.get_height()) // 2 |
||||
) |
||||
) |
||||
if self.value: |
||||
render_text(surface, (10, 10), font, self.value, WHITE, self.cursor_position if time.time() % 1 < 0.5 else None) |
||||
return surface |
||||
|
||||
def process_event(self, event: pygame.event.Event): |
||||
""" |
||||
Handle a typing event or a click (to focus) |
||||
:param event: a pygame event |
||||
""" |
||||
# If we have a click, we should focus the field if the click was inside or unfocus if it was outside |
||||
if event.type == pygame.MOUSEBUTTONUP: |
||||
self.is_focused = self.top_left_corner[0] <= event.pos[0] < self.top_left_corner[0] + self.width \ |
||||
and self.top_left_corner[1] <= event.pos[1] < self.top_left_corner[1] + self.height |
||||
if self.is_focused and hasattr(event, 'key') and event.type == 768: |
||||
if event.key == pygame.K_BACKSPACE: |
||||
self.value = self.value[:-1] |
||||
self.cursor_position -= 1 |
||||
elif event.key == pygame.K_LEFT: |
||||
self.cursor_position -= 1 |
||||
elif event.key == pygame.K_RIGHT: |
||||
self.cursor_position += 1 |
||||
elif event.unicode: |
||||
self.value = self.value[:self.cursor_position] + event.unicode + self.value[self.cursor_position:] |
||||
self.cursor_position += 1 |
||||
self.cursor_position = max(0, min(self.cursor_position, len(self.value))) |
||||
|
||||
def collect(self) -> str: |
||||
""" |
||||
Get the current value and clear the field |
||||
:return: the value of the text input |
||||
""" |
||||
value = self.value |
||||
self.value = '' |
||||
self.cursor_position = 0 |
||||
return value |
@ -0,0 +1,11 @@
|
||||
from __future__ import annotations |
||||
|
||||
import pygame |
||||
from degeon import Degeon |
||||
from config import WIDTH, HEIGHT |
||||
|
||||
|
||||
deg = Degeon.new() |
||||
screen = pygame.display.set_mode((WIDTH, HEIGHT)) |
||||
deg.main_loop(screen) |
||||
pygame.quit() |
@ -0,0 +1,48 @@
|
||||
from __future__ import annotations |
||||
|
||||
from dataclasses import dataclass |
||||
|
||||
import pygame |
||||
|
||||
from config import WIDTH, CHAT_SELECTOR_WIDTH, MESSAGE_HEIGHT, font_medium, BLUE, DARK_BLUE, WHITE, DARKER_BLUE |
||||
from utils import render_text |
||||
|
||||
|
||||
@dataclass |
||||
class Message: |
||||
""" |
||||
The message (for now, it consists only of text) |
||||
|
||||
Attributes: |
||||
:param text (str): the message text |
||||
:param is_from_me (bool): False if the message is not from the current user |
||||
:param chat_width (int): the width of the active chat widget |
||||
""" |
||||
text: str |
||||
is_from_me: bool |
||||
chat_width: int = WIDTH - CHAT_SELECTOR_WIDTH - 10 |
||||
height: int = MESSAGE_HEIGHT |
||||
|
||||
def render(self) -> pygame.Surface: |
||||
""" |
||||
Creates a surface with a rectangle and the message text written on it |
||||
:return: the surface with rendered message |
||||
""" |
||||
surface = pygame.Surface((self.chat_width, self.height * 10)) |
||||
surface.fill(DARKER_BLUE) |
||||
bg_color = BLUE * self.is_from_me + DARK_BLUE * (not self.is_from_me) |
||||
padding = 7 |
||||
# Size of the scaled text surface |
||||
blit_height: int = self.height - padding * 2 |
||||
blit_width: int = self.chat_width // 2 |
||||
x: int = 0 if not self.is_from_me else self.chat_width - blit_width - padding * 3.5 |
||||
pygame.draw.rect(surface, bg_color, (x, 0, blit_width + padding * 2, self.height * 10)) |
||||
text_surface: pygame.Surface = pygame.Surface((blit_width, blit_height * 10)) |
||||
text_surface.fill(bg_color) |
||||
text_height = render_text(text_surface, (0, 0), font_medium, self.text, WHITE) |
||||
# text_surface = pygame.transform.chop(text_surface, (0, 0, blit_width, text_height)) |
||||
surface.blit(text_surface, (x + padding, padding, blit_width, text_height)) |
||||
new_surface = pygame.Surface((self.chat_width, text_height + 2 * padding)) |
||||
new_surface.fill(bg_color) |
||||
new_surface.blit(surface, (0, 0, new_surface.get_width(), new_surface.get_height())) |
||||
return new_surface |
@ -0,0 +1,51 @@
|
||||
from typing import Optional |
||||
import typing |
||||
|
||||
import pygame |
||||
|
||||
|
||||
def draw_cursor(surface, cur_position, high, c, size=4): |
||||
pygame.draw.line(surface, c, |
||||
cur_position, |
||||
(cur_position[0], cur_position[1] + high), size) |
||||
|
||||
|
||||
def get_word_length(ft, word: str): |
||||
word_surface = ft.render(word, 0, pygame.Color('black')) |
||||
return word_surface.get_size() |
||||
|
||||
|
||||
def render_text(surface, position: typing.Tuple[int, int], font: pygame.font, text: str, c=pygame.Color('black'), |
||||
cursor_position: Optional[int] = None) -> int: |
||||
"""Render text with hyphenation""" |
||||
|
||||
if len(list(filter(bool, text.split()))) == 0: |
||||
return 0 |
||||
|
||||
need_cursor = cursor_position is not None |
||||
lines = [word.split() for word in text.splitlines()] |
||||
space_size = font.size(' ')[0] |
||||
max_width, max_height = surface.get_size() |
||||
|
||||
x, y = position |
||||
symbols_counter = 0 |
||||
for line in lines: |
||||
for word in line: |
||||
w, h = get_word_length(font, word) |
||||
if w > max_width or h > max_height: |
||||
raise ValueError("Surface is too small") |
||||
if x + w >= max_width: |
||||
x = position[0] |
||||
y += h |
||||
surface.blit(font.render(word, 0, c), (x, y)) |
||||
if need_cursor and symbols_counter + len(word) >= cursor_position: |
||||
# It means that cursor is in this word or after this word |
||||
cur_coord = (x + get_word_length(font, |
||||
word[:cursor_position - symbols_counter])[0], y) |
||||
draw_cursor(surface, cur_coord, h, c) |
||||
need_cursor = False |
||||
symbols_counter += len(word) + 1 |
||||
x += w + space_size |
||||
y += h |
||||
x = position[0] |
||||
return y |
After Width: | Height: | Size: 422 KiB |
After Width: | Height: | Size: 898 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 2.1 MiB |
@ -0,0 +1,35 @@
|
||||
[package] |
||||
name = "ironforce" |
||||
version = "0.1.0" |
||||
edition = "2021" |
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||
|
||||
[lib] |
||||
name = "ironforce" |
||||
path = "src/lib.rs" |
||||
|
||||
|
||||
[features] |
||||
default = [] |
||||
std = ["rayon"] |
||||
|
||||
[dependencies] |
||||
rand_os = "*" |
||||
# x25519-dalek = "0.6.0" |
||||
# ed25519-dalek = { version = "1.0", features = ["serde"] } |
||||
sha2 = "0.8.1" |
||||
rand = "*" |
||||
rsa = { version = "0.5", features = ["serde"] } |
||||
serde = { version = "1.0", features = ["derive", "alloc"], default-features = false } |
||||
rayon = { version = "1.5.1", optional = true } |
||||
core-error = "0.0.1-rc4" |
||||
serde_cbor = "0.11.2" |
||||
serde_json = "1.0.72" |
||||
spin = "0.9.2" |
||||
base64 = "0.13.0" |
||||
include_optional = "1.0.1" |
||||
|
||||
[[bin]] |
||||
name = "worker" |
||||
required-features = ["std"] |
@ -0,0 +1,57 @@
|
||||
use ironforce::res::IFResult; |
||||
use ironforce::{IronForce, Message, MessageType, PublicKey}; |
||||
|
||||
fn main() -> IFResult<()> { |
||||
let ironforce = IronForce::from_file("".to_string())?; |
||||
let if_keys = ironforce.keys.clone(); |
||||
println!( |
||||
"Our public key: {}", |
||||
base64::encode(if_keys.get_public().to_vec().as_slice()) |
||||
); |
||||
let (_thread, if_mutex) = ironforce.launch_main_loop(50); |
||||
let stdin = std::io::stdin(); |
||||
let if_mutex_clone = if_mutex.clone(); |
||||
let if_keys_clone = if_keys.clone(); |
||||
std::thread::spawn(move || loop { |
||||
if let Some(msg) = if_mutex_clone.lock().unwrap().read_message() { |
||||
println!( |
||||
"New message: {}", |
||||
String::from_utf8(msg.get_decrypted(&if_keys_clone).unwrap()).unwrap() |
||||
); |
||||
} |
||||
std::thread::sleep(std::time::Duration::from_millis(200)) |
||||
}); |
||||
loop { |
||||
let mut buf = String::new(); |
||||
stdin.read_line(&mut buf)?; |
||||
let msg_base = if buf.starts_with('>') { |
||||
let target_base64 = buf |
||||
.split(')') |
||||
.next() |
||||
.unwrap() |
||||
.trim_start_matches(">(") |
||||
.to_string(); |
||||
let target = if let Ok(res) = base64::decode(target_base64) { |
||||
res |
||||
} else { |
||||
println!("Wrong b64."); |
||||
continue; |
||||
}; |
||||
buf = buf |
||||
.split(')') |
||||
.skip(1) |
||||
.map(|s| s.to_string()) |
||||
.collect::<Vec<String>>() |
||||
.join(")"); |
||||
Message::build() |
||||
.message_type(MessageType::SingleCast) |
||||
.recipient(&PublicKey::from_vec(target).unwrap()) |
||||
} else { |
||||
Message::build().message_type(MessageType::Broadcast) |
||||
}; |
||||
if_mutex |
||||
.lock() |
||||
.unwrap() |
||||
.send_to_all(msg_base.content(buf.into_bytes()).sign(&if_keys).build()?)?; |
||||
} |
||||
} |
@ -0,0 +1,232 @@
|
||||
use crate::res::{IFError, IFResult}; |
||||
use alloc::string::String; |
||||
use alloc::vec; |
||||
/// This module has wrappers for cryptography with RSA algorithms.
|
||||
/// Its main structs - `PublicKey` and `Keys` implement all functions for key generation, signatures and asymmetric encryption
|
||||
use alloc::vec::Vec; |
||||
use rand::rngs::OsRng; |
||||
use rsa::errors::Result as RsaRes; |
||||
use rsa::{BigUint, PaddingScheme, PublicKey as RPK, PublicKeyParts, RsaPrivateKey, RsaPublicKey}; |
||||
use serde::{Deserialize, Serialize}; |
||||
use sha2::{Digest, Sha224}; |
||||
use core::hash::Hash; |
||||
use core::hash::Hasher; |
||||
|
||||
static KEY_LENGTH: usize = 2048; |
||||
static ENCRYPTION_CHUNK_SIZE: usize = 240; |
||||
static ENCRYPTION_OUTPUT_CHUNK_SIZE: usize = 256; |
||||
|
||||
/// Public key of a node
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)] |
||||
pub struct PublicKey { |
||||
pub key: RsaPublicKey, |
||||
} |
||||
|
||||
impl PublicKey { |
||||
/// Check if the sign is valid for given data and key
|
||||
pub fn verify_sign(&self, data: &[u8], sign: &[u8]) -> bool { |
||||
self.key |
||||
.verify( |
||||
PaddingScheme::PKCS1v15Sign { hash: None }, |
||||
&Sha224::new().chain(data).result().to_vec(), |
||||
sign, |
||||
) |
||||
.is_ok() |
||||
} |
||||
|
||||
/// Encrypt some data for a user with this public key
|
||||
pub fn encrypt_data(&self, data: &[u8]) -> RsaRes<Vec<u8>> { |
||||
if data.len() <= ENCRYPTION_CHUNK_SIZE { |
||||
self.key |
||||
.encrypt(&mut OsRng {}, PaddingScheme::PKCS1v15Encrypt, data) |
||||
} else { |
||||
let mut res = self.key.encrypt( |
||||
&mut OsRng {}, |
||||
PaddingScheme::PKCS1v15Encrypt, |
||||
&data[..ENCRYPTION_CHUNK_SIZE], |
||||
)?; |
||||
res.extend(self.encrypt_data(&data[ENCRYPTION_CHUNK_SIZE..])?); |
||||
Ok(res) |
||||
} |
||||
} |
||||
|
||||
pub fn to_vec(&self) -> Vec<u8> { |
||||
let n_bytes = self.key.n().to_bytes_be(); |
||||
let e_bytes = self.key.e().to_bytes_be(); |
||||
let mut res = vec![ |
||||
(n_bytes.len() / 256) as u8, |
||||
(n_bytes.len() % 256) as u8, |
||||
(e_bytes.len() / 256) as u8, |
||||
(e_bytes.len() % 256) as u8, |
||||
]; |
||||
res.extend(n_bytes); |
||||
res.extend(e_bytes); |
||||
res |
||||
} |
||||
|
||||
pub fn from_vec(data: Vec<u8>) -> IFResult<Self> { |
||||
if data.len() < 4 { |
||||
return Err(IFError::SerializationError(String::from( |
||||
"Not enough bytes in serialized PublicKey", |
||||
))); |
||||
} |
||||
let n_len = data[0] as usize * 256 + data[1] as usize; |
||||
let e_len = data[2] as usize * 256 + data[3] as usize; |
||||
if data.len() != e_len + n_len + 4 { |
||||
return Err(IFError::SerializationError(String::from( |
||||
"Not enough bytes in serialized PublicKey", |
||||
))); |
||||
} |
||||
let n_bytes = &data[4..n_len + 4]; |
||||
let e_bytes = &data[4 + n_len..e_len + n_len + 4]; |
||||
Ok(Self { |
||||
key: RsaPublicKey::new( |
||||
BigUint::from_bytes_be(n_bytes), |
||||
BigUint::from_bytes_be(e_bytes), |
||||
)?, |
||||
}) |
||||
} |
||||
|
||||
/// Get a short string that's kind of a hash
|
||||
pub fn get_short_id(&self) -> String { |
||||
alloc::string::String::from_utf8( |
||||
self.to_vec() |
||||
.iter() |
||||
.skip(90) |
||||
.take(5) |
||||
.map(|c| c % 26 + 97) |
||||
.collect::<Vec<u8>>(), |
||||
) |
||||
.unwrap() |
||||
} |
||||
} |
||||
|
||||
impl Hash for PublicKey { |
||||
fn hash<H: Hasher>(&self, state: &mut H) { |
||||
Hash::hash(&self.to_vec(), state) |
||||
} |
||||
} |
||||
|
||||
impl PartialEq for PublicKey { |
||||
fn eq(&self, other: &Self) -> bool { |
||||
self.key == other.key |
||||
} |
||||
} |
||||
|
||||
impl PublicKey { |
||||
fn hash(&self) -> Vec<u8> { |
||||
self.to_vec() |
||||
} |
||||
} |
||||
|
||||
/// Key pair (public and secret) for a node, should be stored locally
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)] |
||||
pub struct Keys { |
||||
public_key: RsaPublicKey, |
||||
private_key: RsaPrivateKey, |
||||
} |
||||
|
||||
impl Hash for Keys { |
||||
fn hash<H: Hasher>(&self, state: &mut H) { |
||||
Hash::hash(&self.get_public(), state) |
||||
} |
||||
} |
||||
|
||||
impl Keys { |
||||
/// Generate new random key
|
||||
pub fn generate() -> Self { |
||||
let mut rng = OsRng; |
||||
let private_key = |
||||
RsaPrivateKey::new(&mut rng, KEY_LENGTH).expect("failed to generate a key"); |
||||
let public_key = RsaPublicKey::from(&private_key); |
||||
Self { |
||||
private_key, |
||||
public_key, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Keys { |
||||
/// Sign content using these keys
|
||||
pub fn sign(&self, content: &[u8]) -> RsaRes<Vec<u8>> { |
||||
self.private_key.sign( |
||||
PaddingScheme::PKCS1v15Sign { hash: None }, |
||||
&Sha224::new().chain(content).result().to_vec(), |
||||
) |
||||
} |
||||
|
||||
/// Decrypt data
|
||||
pub fn decrypt_data(&self, data_encrypted: &[u8]) -> RsaRes<Vec<u8>> { |
||||
if data_encrypted.len() <= ENCRYPTION_OUTPUT_CHUNK_SIZE { |
||||
self.private_key |
||||
.decrypt(PaddingScheme::PKCS1v15Encrypt, data_encrypted) |
||||
} else { |
||||
let mut res = self.private_key.decrypt( |
||||
PaddingScheme::PKCS1v15Encrypt, |
||||
&data_encrypted[..ENCRYPTION_OUTPUT_CHUNK_SIZE], |
||||
)?; |
||||
res.extend(self.decrypt_data(&data_encrypted[ENCRYPTION_OUTPUT_CHUNK_SIZE..])?); |
||||
Ok(res) |
||||
} |
||||
} |
||||
|
||||
/// Get public key
|
||||
pub fn get_public(&self) -> PublicKey { |
||||
PublicKey { |
||||
key: self.public_key.clone(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn test_encrypt() { |
||||
let data = vec![0, 5, 8, 135, 67, 45, 32, 5]; |
||||
let keys = Keys::generate(); |
||||
let data_encrypted = keys.get_public().encrypt_data(&data).unwrap(); |
||||
assert_eq!(keys.decrypt_data(&data_encrypted).unwrap(), data); |
||||
assert_eq!( |
||||
keys.decrypt_data(&keys.get_public().encrypt_data(&data.repeat(300)).unwrap()) |
||||
.unwrap(), |
||||
data.repeat(300) |
||||
); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_invalid_encrypt() { |
||||
let data = vec![0, 5, 8, 135, 67]; |
||||
let keys_1 = Keys::generate(); |
||||
let keys_2 = Keys::generate(); |
||||
assert!(keys_2 |
||||
.decrypt_data(&keys_1.get_public().encrypt_data(&data).unwrap()) |
||||
.is_err()); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_signing() { |
||||
let data = vec![0, 5, 8, 135, 67]; |
||||
let keys = Keys::generate(); |
||||
assert!(keys |
||||
.get_public() |
||||
.verify_sign(&data, &keys.sign(&data).unwrap())); |
||||
assert!(keys |
||||
.get_public() |
||||
.verify_sign(&data.repeat(340), &keys.sign(&data.repeat(340)).unwrap())); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_invalid_signing() { |
||||
let data = vec![0, 5, 8, 135, 67]; |
||||
let keys_1 = Keys::generate(); |
||||
let keys_2 = Keys::generate(); |
||||
assert!(!keys_2 |
||||
.get_public() |
||||
.verify_sign(&data, &keys_1.sign(&data).unwrap())); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_pkey_caching() { |
||||
assert_ne!( |
||||
Keys::generate().get_public().hash(), |
||||
Keys::generate().get_public().hash() |
||||
) |
||||
} |
@ -0,0 +1,175 @@
|
||||
use crate::message::MessageBytes; |
||||
use crate::res::IFResult; |
||||
use alloc::string::String; |
||||
|
||||
/// Some data that can be provided to the interface to send the message to a target.
|
||||
///
|
||||
/// For IP this might be `IP:port`.
|
||||
/// Radio interface, for example, may not have the functionality of targeting, but that's fine
|
||||
pub(crate) type TargetingData = String; |
||||
|
||||
/// In an std environment we require that the interface can be send safely between threads
|
||||
#[cfg(not(feature = "std"))] |
||||
pub trait InterfaceRequirements {} |
||||
|
||||
#[cfg(feature = "std")] |
||||
pub trait InterfaceRequirements: Send + Sync {} |
||||
|
||||
/// An interface that can be used to
|
||||
pub trait Interface: InterfaceRequirements { |
||||
/// Run one main loop iteration.
|
||||
/// On platforms that support concurrency, these functions will be run simultaneously for all interfaces.
|
||||
/// Most likely, this function will accept messages and save them somewhere internally to give out later in `Interface.receive()`.
|
||||
///
|
||||
/// For systems that don't support concurrency, there can be only one interface in this function waits for a message (to avoid blocking).
|
||||
/// That's why it's necessary to check if it is the case for this interface, and it's done using function `Interface::has_blocking_main()`
|
||||
fn main_loop_iteration(&mut self) -> IFResult<()>; |
||||
|
||||
/// Check if `main_loop_iteration` stops execution and waits for a message
|
||||
fn has_blocking_main(&self) -> bool { |
||||
false // hopefully...
|
||||
} |
||||
|
||||
/// Get some way of identification for this interface
|
||||
fn id(&self) -> &str; |
||||
|
||||
/// Send a message. If no `interface_data` is provided, we should consider it to be a broadcast.
|
||||
/// If, on the other hand, `interface_data` is not `None`, it should be used to send the message to the target.
|
||||
fn send( |
||||
&mut self, |
||||
message: &[u8], /*MessageBytes*/ |
||||
interface_data: Option<TargetingData>, |
||||
) -> IFResult<()>; |
||||
|
||||
/// Receive a message through this interface. Returns a result with an option of (message bytes, target).
|
||||
/// `None` means there is no message available at the time.
|
||||
/// The implementations of this function shouldn't wait for new messages, but
|
||||
fn receive(&mut self) -> IFResult<Option<(MessageBytes, TargetingData /*interface data*/)>>; |
||||
|
||||
/// Dump the interface to string
|
||||
fn get_dump_data(&self) -> String; |
||||
|
||||
/// Create the interface from dumped data
|
||||
fn from_dump(data: String) -> IFResult<Self> where Self: Sized; |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
pub mod test_interface { |
||||
use crate::interface::{Interface, InterfaceRequirements, TargetingData}; |
||||
use crate::message::MessageBytes; |
||||
use crate::res::IFResult; |
||||
use alloc::string::String; |
||||
use alloc::string::ToString; |
||||
use alloc::sync::Arc; |
||||
use alloc::vec; |
||||
use alloc::vec::Vec; |
||||
use spin::Mutex; |
||||
|
||||
#[derive(Default)] |
||||
pub struct SimpleTestInterface { |
||||
messages: Vec<(Vec<u8>, TargetingData)>, |
||||
} |
||||
|
||||
impl InterfaceRequirements for SimpleTestInterface {} |
||||
|
||||
impl Interface for SimpleTestInterface { |
||||
fn main_loop_iteration(&mut self) -> IFResult<()> { |
||||
Ok(()) |
||||
} |
||||
|
||||
fn id(&self) -> &str { |
||||
"test_interface" |
||||
} |
||||
|
||||
fn send(&mut self, message: &[u8], interface_data: Option<TargetingData>) -> IFResult<()> { |
||||
self.messages |
||||
.push((Vec::from(message), interface_data.unwrap_or_default())); |
||||
Ok(()) |
||||
} |
||||
|
||||
fn receive(&mut self) -> IFResult<Option<(MessageBytes, TargetingData)>> { |
||||
Ok(self.messages.pop()) |
||||
} |
||||
|
||||
fn get_dump_data(&self) -> String { |
||||
"".to_string() |
||||
} |
||||
|
||||
fn from_dump(_data: String) -> IFResult<Self> { |
||||
Ok(Default::default()) |
||||
} |
||||
} |
||||
|
||||
pub type Storage = Vec<(Vec<u8>, TargetingData)>; |
||||
|
||||
#[derive(Default)] |
||||
pub struct TestInterface { |
||||
this_peer_id: String, |
||||
storage: Arc<Mutex<Storage>>, |
||||
messages: Vec<(Vec<u8>, TargetingData)>, |
||||
} |
||||
|
||||
impl Interface for TestInterface { |
||||
fn main_loop_iteration(&mut self) -> IFResult<()> { |
||||
let mut storage_locked = self.storage.lock(); |
||||
while let Some(i) = storage_locked |
||||
.iter() |
||||
.position(|msg| msg.1 == self.this_peer_id || msg.1.is_empty()) |
||||
{ |
||||
self.messages.push(storage_locked.remove(i)); |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
fn id(&self) -> &str { |
||||
"test_interface" |
||||
} |
||||
|
||||
fn send(&mut self, message: &[u8], target: Option<TargetingData>) -> IFResult<()> { |
||||
self.storage |
||||
.lock() |
||||
.push((Vec::from(message), target.unwrap_or_default())); |
||||
Ok(()) |
||||
} |
||||
|
||||
fn receive(&mut self) -> IFResult<Option<(MessageBytes, TargetingData)>> { |
||||
Ok(self.messages.pop()) |
||||
} |
||||
|
||||
fn get_dump_data(&self) -> String { |
||||
"".to_string() |
||||
} |
||||
|
||||
fn from_dump(_data: String) -> IFResult<Self> { |
||||
Ok(TestInterface { |
||||
this_peer_id: "".to_string(), |
||||
storage: Arc::new(Default::default()), |
||||
messages: vec![], |
||||
}) |
||||
} |
||||
} |
||||
|
||||
impl InterfaceRequirements for TestInterface {} |
||||
|
||||
pub fn create_test_interfaces(n: usize) -> Vec<TestInterface> { |
||||
let storage_mutex = Arc::new(Mutex::new(vec![])); |
||||
(0..n) |
||||
.map(|i| TestInterface { |
||||
this_peer_id: i.to_string(), |
||||
storage: storage_mutex.clone(), |
||||
messages: vec![], |
||||
}) |
||||
.collect() |
||||
} |
||||
|
||||
#[test] |
||||
fn test_test_interface() { |
||||
let mut interfaces = create_test_interfaces(2); |
||||
interfaces[0].send(b"123", Some("1".to_string())).unwrap(); |
||||
interfaces[1].main_loop_iteration().unwrap(); |
||||
assert_eq!( |
||||
interfaces[1].receive().unwrap().unwrap().0.as_slice(), |
||||
b"123" |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,614 @@
|
||||
use alloc::borrow::ToOwned; |
||||
use alloc::string::{String, ToString}; |
||||
use alloc::vec; |
||||
use alloc::vec::Vec; |
||||
use core::str::FromStr; |
||||
use core::time::Duration; |
||||
use include_optional::include_str_optional; |
||||
use rayon::prelude::*; |
||||
use serde::{Deserialize, Serialize}; |
||||
use std::net::TcpStream; |
||||
use std::{format, net}; |
||||
|
||||
use crate::interface::{Interface, InterfaceRequirements, TargetingData}; |
||||
use crate::message::MessageBytes; |
||||
use crate::res::{IFError, IFResult}; |
||||
use crate::std::io::{Read, Write}; |
||||
use crate::std::println; |
||||
|
||||
pub const DEFAULT_PORT: u16 = 50000; |
||||
|
||||
/// The threshold for the number of peers below which we are desperate
|
||||
const PEER_THRESHOLD: usize = 70; |
||||
|
||||
/// Default peers
|
||||
const DEFAULT_PEERS_FILE: Option<&'static str> = include_str_optional!(".if_ip_peers"); |
||||
|
||||
type Peer = (net::IpAddr, u16); |
||||
|
||||
/// Interface for interactions using tcp sockets
|
||||
pub struct IPInterface { |
||||
id: String, |
||||
connections: Vec<net::TcpStream>, |
||||
listener: net::TcpListener, |
||||
peers: Vec<Peer>, |
||||
package_queue: Vec<(IPPackage, String /* from_peer */)>, |
||||
main_loop_iterations: u64, |
||||
} |
||||
|
||||
/// Data for the serialization of `IPInterface`
|
||||
#[derive(Serialize, Deserialize)] |
||||
pub struct SerData { |
||||
pub peers: Vec<Peer>, |
||||
pub port: u16, |
||||
} |
||||
|
||||
#[derive(Debug, Clone)] |
||||
struct IPPackage { |
||||
version: u8, |
||||
package_type: MessageType, |
||||
size: u32, |
||||
message: MessageBytes, |
||||
} |
||||
|
||||
#[derive(Debug, Copy, Clone)] |
||||
enum MessageType { |
||||
Common, |
||||
PeerRequest, |
||||
PeersShared, |
||||
} |
||||
|
||||
impl MessageType { |
||||
fn from_u8(id: u8) -> IFResult<MessageType> { |
||||
match id { |
||||
0 => Ok(MessageType::Common), |
||||
1 => Ok(MessageType::PeerRequest), |
||||
2 => Ok(MessageType::PeersShared), |
||||
_ => Err(IFError::General("Incorrect message type".to_string())), |
||||
} |
||||
} |
||||
|
||||
fn as_u8(&self) -> u8 { |
||||
match self { |
||||
MessageType::Common => 0, |
||||
MessageType::PeerRequest => 1, |
||||
MessageType::PeersShared => 2, |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn compare_addrs(peer: &Peer, addr: net::SocketAddr) -> bool { |
||||
addr.ip() == peer.0 && addr.port() == peer.1 |
||||
} |
||||
|
||||
impl InterfaceRequirements for IPInterface {} |
||||
|
||||
impl Interface for IPInterface { |
||||
fn main_loop_iteration(&mut self) -> IFResult<()> { |
||||
if let Some(conn) = self.listener.incoming().next() { |
||||
match conn { |
||||
Ok(stream) => { |
||||
stream.set_nonblocking(true)?; |
||||
let addr = stream.peer_addr()?; |
||||
println!( |
||||
"({:?}): New client: {:?}", |
||||
addr, |
||||
self.listener.local_addr().unwrap() |
||||
); |
||||
if self.peers.iter().all(|(ip, _)| *ip != addr.ip()) { |
||||
self.peers.push((addr.ip(), addr.port())); |
||||
} |
||||
self.connections.push(stream) |
||||
} |
||||
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {} |
||||
Err(e) => println!("An error happened with an incoming connection: {:?}", e), |
||||
} |
||||
} |
||||
let mut new_connections: Vec<TcpStream> = vec![]; |
||||
let mut connections_to_delete = vec![]; |
||||
for (i, connection) in self.connections.iter_mut().enumerate() { |
||||
let res: std::io::Result<()> = { |
||||
connection.set_nonblocking(true)?; |
||||
let mut buf = [0u8; 6]; |
||||
let peek_res = connection.peek(&mut buf); |
||||
if peek_res.is_err() || peek_res.unwrap() < 6 { |
||||
continue; |
||||
} |
||||
let mut header: [u8; 6] = [0, 0, 0, 0, 0, 0]; |
||||
match connection.read_exact(&mut header) { |
||||
Ok(_) => {} |
||||
Err(ref e) |
||||
if e.kind() == std::io::ErrorKind::WouldBlock |
||||
|| e.kind() == std::io::ErrorKind::UnexpectedEof => |
||||
{ |
||||
continue
|
||||
} |
||||
Err(e) => { |
||||
println!("Error: {:?}", e); |
||||
connections_to_delete.push(i); |
||||
let connection_addr = if let Ok(r) = connection.peer_addr() { |
||||
r |
||||
} else { |
||||
continue; |
||||
}; |
||||
if let Some(peer) = self |
||||
.peers |
||||
.iter() |
||||
.find(|p| compare_addrs(p, connection_addr)) |
||||
{ |
||||
if let Ok(Some(conn)) = IPInterface::new_connection(peer) { |
||||
new_connections.push(conn) |
||||
} |
||||
} |
||||
continue; |
||||
} |
||||
}; |
||||
let version = header[0]; |
||||
let package_type = MessageType::from_u8(header[1])?; |
||||
let size = bytes_to_size([header[2], header[3], header[4], header[5]]); |
||||
connection.set_nonblocking(false)?; |
||||
connection.set_read_timeout(Some(std::time::Duration::from_millis(500)))?; |
||||
|
||||
let mut message_take = connection.take(size as u64); |
||||
let mut message: Vec<u8> = vec![]; |
||||
message_take.read_to_end(&mut message)?; |
||||
|
||||
match package_type { |
||||
MessageType::PeerRequest => { |
||||
let peers_to_share = if self.peers.len() < PEER_THRESHOLD { |
||||
self.peers.clone() |
||||
} else { |
||||
self.peers.iter().skip(7).step_by(2).cloned().collect() |
||||
}; |
||||
let message = serde_cbor::to_vec(&peers_to_share)?; |
||||
IPInterface::send_package( |
||||
connection, |
||||
IPPackage { |
||||
version, |
||||
package_type: MessageType::PeersShared, |
||||
size: message.len() as u32, |
||||
message, |
||||
}, |
||||
)?; |
||||
} |
||||
MessageType::Common => { |
||||
let package = IPPackage { |
||||
version, |
||||
package_type, |
||||
size, |
||||
message, |
||||
}; |
||||
self.package_queue |
||||
.insert(0, (package, format!("{:?}", connection.peer_addr()?))); |
||||
} |
||||
MessageType::PeersShared => { |
||||
let peers: Vec<Peer> = serde_cbor::from_slice(message.as_slice())?; |
||||
for peer in peers { |
||||
if !self.peers.contains(&peer) { |
||||
if let Some(conn) = IPInterface::new_connection(&peer)? { |
||||
new_connections.push(conn) |
||||
} |
||||
self.peers.push(peer); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Ok(()) |
||||
}; |
||||
if res.is_err() && res.unwrap_err().kind() == std::io::ErrorKind::BrokenPipe { |
||||
connections_to_delete.push(i) |
||||
}; |
||||
} |
||||
for (j, index) in connections_to_delete.iter().enumerate() { |
||||
self.connections.remove(index - j); |
||||
} |
||||
|
||||
for conn in new_connections.iter_mut() { |
||||
self.initialize_connection(conn) |
||||
.unwrap_or_else(|e| println!("Couldn't initialize connection: {:?}", e)); |
||||
} |
||||
self.connections.extend(new_connections); |
||||
|
||||
self.main_loop_iterations += 1; |
||||
// Every 50 iterations we connect to everyone we know
|
||||
if self.main_loop_iterations % 50 == 0 { |
||||
let peers_we_do_not_have_connections_with = self.disconnected_peers(); |
||||
self.connections |
||||
.extend(IPInterface::get_connections_to_peers( |
||||
&peers_we_do_not_have_connections_with, |
||||
self.peers.len() < PEER_THRESHOLD * 2, |
||||
)); |
||||
} |
||||
if self.connections.is_empty() { |
||||
for peer in self.peers.clone() { |
||||
self.obtain_connection(&peer) |
||||
.map(|_| ()) |
||||
.unwrap_or_else(|e| println!("Error in obtaining connection: {:?}", e)); |
||||
} |
||||
} |
||||
|
||||
// We do a peer exchange every 30 iterations
|
||||
if self.main_loop_iterations % 30 == 0 && !self.connections.is_empty() { |
||||
let connection_index = |
||||
(self.main_loop_iterations / 30) as usize % self.connections.len(); |
||||
match IPInterface::request_peers(&mut self.connections[connection_index]) { |
||||
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => { |
||||
let peer = ( |
||||
self.connections[connection_index].peer_addr()?.ip(), |
||||
self.connections[connection_index].peer_addr()?.port(), |
||||
); |
||||
self.connections.remove(connection_index); |
||||
let connection_index = self.obtain_connection(&peer)?; |
||||
IPInterface::request_peers(&mut self.connections[connection_index])?; |
||||
} |
||||
Err(e) => println!("An error in peer sharing: {:?}", e), |
||||
_ => {} |
||||
}; |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
fn has_blocking_main(&self) -> bool { |
||||
false |
||||
} |
||||
fn id(&self) -> &str { |
||||
&*self.id |
||||
} |
||||
|
||||
fn send(&mut self, message: &[u8], interface_data: Option<TargetingData>) -> IFResult<()> { |
||||
let package = IPPackage { |
||||
version: 0, |
||||
package_type: MessageType::Common, |
||||
size: message.len() as u32, |
||||
message: Vec::from(message), |
||||
}; |
||||
|
||||
match interface_data { |
||||
Some(ip_string) => { |
||||
let addr: net::SocketAddr = ip_string.parse().expect("Unable to parse address"); |
||||
let peer = (addr.ip(), addr.port()); |
||||
let index = self.obtain_connection(&peer)?; |
||||
match IPInterface::send_package(&mut self.connections[index], package.clone()) { |
||||
Ok(_) => {} |
||||
Err(_) => { |
||||
self.remove_all_connections_to_peer(&peer); |
||||
let index = self.obtain_connection(&(addr.ip(), addr.port()))?; |
||||
IPInterface::send_package(&mut self.connections[index], package) |
||||
.map_err(|e| { |
||||
println!("Error while sending: {:?}", e); |
||||
e |
||||
}) |
||||
.unwrap_or_default(); |
||||
} |
||||
} |
||||
} |
||||
None => { |
||||
if self.connections.len() < PEER_THRESHOLD |
||||
&& self.connections.len() < self.peers.len() |
||||
{ |
||||
let new_connections = IPInterface::get_connections_to_peers( |
||||
&self.disconnected_peers(), |
||||
self.peers.len() < PEER_THRESHOLD, |
||||
); |
||||
self.connections.extend(new_connections); |
||||
} |
||||
let connections_to_delete = self |
||||
.connections |
||||
.iter_mut() |
||||
.enumerate() |
||||
.filter_map(|(i, conn)| { |
||||
IPInterface::send_package(conn, package.clone()) |
||||
.err() |
||||
.map(|_| i) |
||||
}) |
||||
.collect::<Vec<_>>(); |
||||
for (j, index) in connections_to_delete.iter().enumerate() { |
||||
self.connections.remove(index - j); |
||||
} |
||||
self.connections |
||||
.extend(IPInterface::get_connections_to_peers( |
||||
&self.disconnected_peers(), |
||||
self.peers.len() < PEER_THRESHOLD, |
||||
)) |
||||
} |
||||
}; |
||||
Ok(()) |
||||
} |
||||
fn receive(&mut self) -> IFResult<Option<(MessageBytes, TargetingData)>> { |
||||
// if !self.package_queue.is_empty() {
|
||||
// println!(
|
||||
// "({:?}): New message from {}. By the way, I know {} peers and have {} connections",
|
||||
// self.listener.local_addr().unwrap(),
|
||||
// self.package_queue.last().unwrap().1,
|
||||
// self.peers.len(),
|
||||
// self.connections.len()
|
||||
// );
|
||||
// }
|
||||
match self.package_queue.pop() { |
||||
Some((ip_package, data)) => Ok(Some((ip_package.message, data))), |
||||
None => Ok(None), |
||||
} |
||||
} |
||||
|
||||
fn get_dump_data(&self) -> String { |
||||
let data = SerData { |
||||
peers: self.peers.clone(), |
||||
port: self.listener.local_addr().unwrap().port(), |
||||
}; |
||||
serde_json::to_string(&data).unwrap() |
||||
} |
||||
|
||||
fn from_dump(data: String) -> IFResult<Self> { |
||||
if !data.is_empty() { |
||||
let data: SerData = serde_json::from_str(data.as_str()).unwrap(); |
||||
IPInterface::new(data.port, data.peers) |
||||
} else { |
||||
let ip_path = std::path::Path::new(".if_ip_peers"); |
||||
let data = if ip_path.exists() { |
||||
std::fs::read_to_string(ip_path).unwrap() |
||||
} else if let Some(data) = DEFAULT_PEERS_FILE { |
||||
data.to_string() |
||||
} else { |
||||
println!("Warning: there are no peers in IP, which makes it essentially useless"); |
||||
"".to_string() |
||||
}; |
||||
let peers = data |
||||
.split('\n') |
||||
.filter_map(|line| net::SocketAddr::from_str(line).ok()) |
||||
.map(|addr| (addr.ip(), addr.port())) |
||||
.collect(); |
||||
IPInterface::new(DEFAULT_PORT, peers) |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl IPInterface { |
||||
fn get_connections_to_peers(peers: &[Peer], do_peer_request: bool) -> Vec<TcpStream> { |
||||
peers |
||||
.par_iter() |
||||
.map(Self::new_connection) |
||||
.filter_map(|r| r.ok()) |
||||
.filter_map(|r| r) |
||||
.map(|mut c| -> IFResult<TcpStream> { |
||||
println!("Requesting peers from {:?}", c.peer_addr().ok()); |
||||
if do_peer_request { |
||||
Self::request_peers(&mut c)?; |
||||
} |
||||
Ok(c) |
||||
}) |
||||
.filter_map(|r| r.ok()) |
||||
.collect::<Vec<_>>() |
||||
} |
||||
|
||||
fn connected_addresses(&self) -> Vec<net::SocketAddr> { |
||||
self.connections |
||||
.iter() |
||||
.filter_map(|conn| conn.peer_addr().ok()) |
||||
.collect::<Vec<_>>() |
||||
} |
||||
|
||||
fn disconnected_peers(&self) -> Vec<Peer> { |
||||
let connected_addresses = self.connected_addresses(); |
||||
self.peers |
||||
.iter() |
||||
.filter(|p| { |
||||
!connected_addresses |
||||
.iter() |
||||
.any(|addr| compare_addrs(p, *addr)) |
||||
}) |
||||
.copied() |
||||
.collect::<Vec<_>>() |
||||
} |
||||
|
||||
fn remove_all_connections_to_peer(&mut self, peer: &Peer) { |
||||
while let Some(ind) = self |
||||
.connections |
||||
.iter() |
||||
.filter_map(|conn| conn.peer_addr().ok()) |
||||
.position(|addr| compare_addrs(peer, addr)) |
||||
{ |
||||
self.connections.remove(ind); |
||||
} |
||||
} |
||||
|
||||
pub fn new(port: u16, peers: Vec<Peer>) -> IFResult<Self> { |
||||
let listener = match create_tcp_listener(port) { |
||||
Some(listener) => listener, |
||||
None => { |
||||
return Err(IFError::General(String::from( |
||||
"Unable to open TCP listener", |
||||
))); |
||||
} |
||||
}; |
||||
|
||||
listener.set_nonblocking(true)?; |
||||
|
||||
let connections = Self::get_connections_to_peers(&peers, true); |
||||
|
||||
Ok(IPInterface { |
||||
id: String::from("IP Interface"), |
||||
connections, |
||||
listener, |
||||
peers, |
||||
package_queue: vec![], |
||||
main_loop_iterations: 0, |
||||
}) |
||||
} |
||||
pub fn dump(&self) -> IFResult<Vec<u8>> { |
||||
Ok(serde_cbor::to_vec(&self.peers)?) |
||||
} |
||||
|
||||
pub fn load(&mut self, data: Vec<u8>) -> IFResult<()> { |
||||
let peers: Vec<Peer> = serde_cbor::from_slice(&data)?; |
||||
self.peers = peers; |
||||
Ok(()) |
||||
} |
||||
|
||||
fn send_package(stream: &mut net::TcpStream, package: IPPackage) -> std::io::Result<()> { |
||||
stream.set_write_timeout(Some(std::time::Duration::from_millis(700)))?; |
||||
stream.set_nonblocking(false)?; |
||||
let mut header: Vec<u8> = vec![package.version, package.package_type.as_u8()]; |
||||
for byte in size_to_bytes(package.size) { |
||||
header.push(byte); |
||||
} |
||||
stream.write_all(&*header)?; |
||||
stream.write_all(&*package.message)?; |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
fn initialize_connection(&self, conn: &mut TcpStream) -> IFResult<()> { |
||||
if self.peers.len() < PEER_THRESHOLD { |
||||
Self::request_peers(conn)?; |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
fn request_peers(conn: &mut TcpStream) -> std::io::Result<()> { |
||||
IPInterface::send_package( |
||||
conn, |
||||
IPPackage { |
||||
version: 0, |
||||
package_type: MessageType::PeerRequest, |
||||
size: 0, |
||||
message: vec![], |
||||
}, |
||||
)?; |
||||
Ok(()) |
||||
} |
||||
|
||||
fn obtain_connection(&mut self, addr: &Peer) -> IFResult<usize> { |
||||
if let Some(pos) = self |
||||
.connections |
||||
.iter() |
||||
.filter_map(|conn| conn.peer_addr().ok()) |
||||
.position(|pa| compare_addrs(addr, pa)) |
||||
{ |
||||
return Ok(pos); |
||||
} |
||||
if let Some(conn) = Self::new_connection(addr)? { |
||||
self.connections.push(conn); |
||||
Ok(self.connections.len() - 1) |
||||
} else { |
||||
Err(IFError::CouldNotConnect) |
||||
} |
||||
} |
||||
|
||||
fn new_connection(addr: &Peer) -> std::io::Result<Option<TcpStream>> { |
||||
for port in addr.1..addr.1 + 3 { |
||||
match net::TcpStream::connect_timeout( |
||||
&net::SocketAddr::new(addr.0, port as u16), |
||||
Duration::from_millis(300), |
||||
) { |
||||
Ok(connection) => { |
||||
return Ok(Some(connection)); |
||||
} |
||||
Err(_) => continue, |
||||
}; |
||||
} |
||||
Ok(None) |
||||
} |
||||
} |
||||
|
||||
fn create_tcp_listener(port: u16) -> Option<net::TcpListener> { |
||||
for port in port..port + 5 { |
||||
match net::TcpListener::bind("0.0.0.0:".to_owned() + &port.to_string()) { |
||||
Ok(listener) => return Some(listener), |
||||
Err(_e) => {} |
||||
} |
||||
} |
||||
None |
||||
} |
||||
|
||||
fn parse_header(data: Vec<u8>) -> IFResult<IPPackage> { |
||||
Ok(IPPackage { |
||||
version: data[0], |
||||
package_type: MessageType::from_u8(data[1])?, |
||||
size: bytes_to_size([data[3], data[4], data[5], data[6]]), |
||||
message: vec![], |
||||
}) |
||||
} |
||||
|
||||
fn size_to_bytes(mut a: u32) -> [u8; 4] { |
||||
let mut arr: [u8; 4] = [0, 0, 0, 0]; |
||||
for i in [3, 2, 1, 0] { |
||||
arr[i] = (a % 256) as u8; |
||||
a /= 256; |
||||
} |
||||
arr |
||||
} |
||||
|
||||
fn bytes_to_size(arr: [u8; 4]) -> u32 { |
||||
let mut size = 0; |
||||
for size_byte in &arr { |
||||
size = size * 256 + *size_byte as u32; |
||||
} |
||||
size |
||||
} |
||||
|
||||
#[test] |
||||
fn test_creating_connection() -> IFResult<()> { |
||||
let message = *b"Hello world from ironforest"; |
||||
let original_msg_copy = message; |
||||
|
||||
let mut interface1 = IPInterface::new(50000, vec![])?; |
||||
let mut interface2 = IPInterface::new(50001, vec![])?; |
||||
|
||||
let t2 = std::thread::spawn(move || { |
||||
for _ in 0..30 { |
||||
interface2.main_loop_iteration().unwrap(); |
||||
std::thread::sleep(std::time::Duration::from_millis(150)); |
||||
} |
||||
interface2 |
||||
}); |
||||
let t1 = std::thread::spawn(move || { |
||||
interface1 |
||||
.send(&message, Some(String::from("0.0.0.0:50001"))) |
||||
.unwrap(); |
||||
interface1 |
||||
}); |
||||
let res1 = t1.join(); |
||||
match res1 { |
||||
Ok(_res) => { |
||||
println!("Thread Ok"); |
||||
} |
||||
Err(e) => println!("{:?}", e), |
||||
} |
||||
let res2 = t2.join(); |
||||
match res2 { |
||||
Ok(mut res) => { |
||||
println!("Thread Ok"); |
||||
match res.receive() { |
||||
Ok(tmp) => match tmp { |
||||
Some((message, _metadata)) => { |
||||
println!("Received {:?}", message); |
||||
assert_eq!(message, original_msg_copy) |
||||
} |
||||
None => { |
||||
println!("None"); |
||||
panic!(); |
||||
} |
||||
}, |
||||
Err(e) => println!("{:?}", e), |
||||
} |
||||
} |
||||
Err(e) => println!("{:?}", e), |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
pub fn create_test_interfaces(n: usize) -> impl Iterator<Item = IPInterface> { |
||||
let ip_addr = std::net::IpAddr::from_str("0.0.0.0").unwrap(); |
||||
(0..n).map(move |i| { |
||||
IPInterface::new( |
||||
(5000 + 5 * i) as u16, |
||||
// (0..n)
|
||||
// .filter(|j| *j != i)
|
||||
// .map(|j| (ip_addr, (5000 + 5 * j) as u16))
|
||||
// .collect(),
|
||||
vec![(ip_addr, (5000 + 5 * ((i + 1) % n)) as u16)], |
||||
) |
||||
.unwrap() |
||||
}) |
||||
} |
@ -0,0 +1,35 @@
|
||||
#[cfg(feature = "std")] |
||||
pub mod ip; |
||||
|
||||
use crate::interface::Interface; |
||||
use alloc::vec; |
||||
use alloc::vec::Vec; |
||||
use alloc::boxed::Box; |
||||
use alloc::string::String; |
||||
#[cfg(feature = "std")] |
||||
use crate::interfaces::ip::IPInterface; |
||||
use crate::res::IFResult; |
||||
|
||||
#[cfg(not(feature = "std"))] |
||||
pub fn get_interfaces() -> Vec<Box<dyn Interface>> { |
||||
vec![] |
||||
} |
||||
|
||||
#[cfg(feature = "std")] |
||||
pub fn get_interfaces() -> Vec<Box<dyn Interface>> { |
||||
vec![Box::new(IPInterface::from_dump(Default::default()).unwrap())] |
||||
} |
||||
|
||||
#[cfg(not(feature = "std"))] |
||||
pub fn restore_interfaces(_data: Vec<String>) -> IFResult<Vec<Box<dyn Interface>>> { |
||||
Ok(vec![]) |
||||
} |
||||
|
||||
#[cfg(feature = "std")] |
||||
pub fn restore_interfaces(data: Vec<String>) -> IFResult<Vec<Box<dyn Interface>>> { |
||||
if data.is_empty() { |
||||
Ok(get_interfaces()) |
||||
} else { |
||||
Ok(vec![Box::new(IPInterface::from_dump(data[0].clone())?)]) |
||||
} |
||||
} |
@ -0,0 +1,635 @@
|
||||
use crate::crypto::{Keys, PublicKey}; |
||||
use crate::message::{Message, MessageType, ServiceMessageType}; |
||||
use crate::res::{IFError, IFResult}; |
||||
use crate::transport::{PeerInfo, Transport}; |
||||
use crate::tunnel::{Tunnel, TunnelPublic}; |
||||
use alloc::collections::BTreeMap; |
||||
#[cfg(feature = "std")] |
||||
use alloc::string::ToString; |
||||
use alloc::vec; |
||||
use alloc::vec::Vec; |
||||
use serde::{Deserialize, Serialize}; |
||||
#[cfg(feature = "std")] |
||||
use std::println; |
||||
|
||||
const TUNNEL_MAX_REPEAT_COUNT: u32 = 3; |
||||
#[cfg(feature = "std")] |
||||
pub const DEFAULT_FILE: &str = ".if_data.json"; |
||||
|
||||
/// Main IF worker
|
||||
#[derive(Hash)] |
||||
pub struct IronForce { |
||||
/// Keys for this instance
|
||||
pub keys: Keys, |
||||
/// the struct that manages communicating with neighbor nodes
|
||||
transport: Transport, |
||||
/// Tunnels that are known to this node
|
||||
tunnels: Vec<Tunnel>, |
||||
/// Additional modules that may be plugged in later,
|
||||
/// for example internet access (like Tor)
|
||||
/// and some kind of decentralized storage
|
||||
additional_modules: Vec<()>, |
||||
/// Non-service messages to give outside
|
||||
pub messages: Vec<Message>, |
||||
/// Tunnels that has not been confirmed yet (no backward spread)
|
||||
///
|
||||
/// `[(Tunnel, Optional target node, local peer ids)]`
|
||||
tunnels_pending: Vec<( |
||||
TunnelPublic, |
||||
Option<PublicKey>, /* target node */ |
||||
(u64, u64), /* local peer ids */ |
||||
)>, |
||||
/// True if this instance has background thread
|
||||
has_background_worker: bool, |
||||
/// Messages that were already processed (stored to avoid "echo chambers")
|
||||
processed_messages: Vec<u64>, |
||||
/// Counters of how many times the tunnel has passed through this node on its forward movement (so that we don't do a shitposting)
|
||||
///
|
||||
/// Maps tunnel's first local_id to the number
|
||||
tunnel_counters: BTreeMap<u64, u32>, |
||||
/// Auto save
|
||||
auto_save: bool, |
||||
} |
||||
|
||||
/// Data for the serialization of IF
|
||||
#[derive(Serialize, Deserialize)] |
||||
pub struct IFSerializationData { |
||||
/// Worker's keys
|
||||
pub keys: Keys, |
||||
/// Saved tunnels that go through this node
|
||||
pub tunnels: Vec<Tunnel>, |
||||
/// Peers for transport
|
||||
pub peers: Vec<PeerInfo>, |
||||
/// Data for all interfaces (in IP, for example, that's port and IPs of peers)
|
||||
pub interfaces_data: Vec<alloc::string::String>, |
||||
} |
||||
|
||||
impl IFSerializationData { |
||||
pub fn default() -> IFSerializationData { |
||||
IFSerializationData { |
||||
keys: Keys::generate(), |
||||
tunnels: vec![], |
||||
peers: vec![], |
||||
interfaces_data: vec![], |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Default for IronForce { |
||||
fn default() -> Self { |
||||
Self::new() |
||||
} |
||||
} |
||||
|
||||
impl IronForce { |
||||
/// Create new worker
|
||||
pub fn new() -> Self { |
||||
Self { |
||||
keys: Keys::generate(), |
||||
transport: Transport::new(crate::interfaces::get_interfaces()), |
||||
tunnels: vec![], |
||||
additional_modules: vec![], |
||||
messages: vec![], |
||||
tunnels_pending: vec![], |
||||
has_background_worker: false, |
||||
processed_messages: vec![], |
||||
tunnel_counters: Default::default(), |
||||
auto_save: true, |
||||
} |
||||
} |
||||
|
||||
/// Create a new tunnel to another node
|
||||
pub fn initialize_tunnel_creation(&mut self, destination: &PublicKey) -> IFResult<()> { |
||||
let tunnel = TunnelPublic::new_singlecast(); |
||||
self.tunnels_pending |
||||
.push((tunnel.clone(), Some(destination.clone()), (0, 0))); |
||||
let message = Message::build() |
||||
.message_type(MessageType::Service( |
||||
ServiceMessageType::TunnelBuildingForwardMovement( |
||||
tunnel, |
||||
destination.encrypt_data(&self.keys.get_public().to_vec())?, |
||||
), |
||||
)) |
||||
.recipient(destination) |
||||
.sign(&self.keys) |
||||
.build()?; |
||||
self.send_to_all(message)?; |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Send a multicast or broadcast message
|
||||
pub fn send_to_all(&mut self, message: Message) -> IFResult<()> { |
||||
self.processed_messages.push(message.message_id); |
||||
self.transport |
||||
.send_message(serde_cbor::to_vec(&message)?, None) |
||||
} |
||||
|
||||
/// Send a message through tunnel
|
||||
fn send_through_tunnel( |
||||
&mut self, |
||||
tunnel_id: u64, |
||||
mut message: Message, |
||||
direction: Option<bool>, |
||||
) -> IFResult<()> { |
||||
self.processed_messages.push(message.message_id); |
||||
let tunnel: Tunnel = if let Some(tun) = self |
||||
.tunnels |
||||
.iter() |
||||
.cloned() |
||||
.find(|t| t.id == Some(tunnel_id)) |
||||
{ |
||||
tun |
||||
} else { |
||||
return Err(IFError::TunnelNotFound); |
||||
}; |
||||
message.tunnel_id = (tunnel_id, tunnel.peer_ids.0 != 0); |
||||
let peer_ids = match (direction, tunnel.peer_ids) { |
||||
(_, (x, 0)) => vec![x], |
||||
(_, (0, x)) => vec![x], |
||||
(None, (x1, x2)) => vec![x1, x2], |
||||
(Some(true), (x1, _x2)) => vec![x1], |
||||
(Some(false), (_x1, x2)) => vec![x2], |
||||
}; |
||||
let msg_bytes = serde_cbor::to_vec(&message)?; |
||||
for peer in peer_ids { |
||||
self.transport.send_message(msg_bytes.clone(), Some(peer))?; |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Find a tunnel to another node (and return its id)
|
||||
pub fn get_tunnel(&self, destination: &PublicKey) -> Option<u64> { |
||||
if let Some(Some(tun)) = self |
||||
.tunnels |
||||
.iter() |
||||
.find(|t| { |
||||
t.target_node.as_ref() == Some(destination) |
||||
|| t.nodes_in_tunnel |
||||
.as_ref() |
||||
.map(|nodes| nodes.contains(destination)) |
||||
== Some(true) |
||||
}) |
||||
.map(|tunnel| tunnel.id) |
||||
{ |
||||
Some(tun) |
||||
} else { |
||||
None |
||||
} |
||||
} |
||||
|
||||
/// Send a message to another node,
|
||||
/// creating a new tunnel if needed
|
||||
pub fn send_message(&mut self, message: Message, destination: &PublicKey) -> IFResult<()> { |
||||
if let Some(tunnel_id) = self.get_tunnel(destination) { |
||||
self.send_through_tunnel(tunnel_id, message, None) |
||||
} else { |
||||
self.initialize_tunnel_creation(destination)?; |
||||
while self.get_tunnel(destination).is_none() { |
||||
if !self.has_background_worker { |
||||
self.main_loop_iteration()? |
||||
} |
||||
#[cfg(feature = "std")] |
||||
std::thread::sleep(std::time::Duration::from_millis(10)); |
||||
} |
||||
let tunnel_id = self.get_tunnel(destination).unwrap(); |
||||
self.send_through_tunnel(tunnel_id, message, None) |
||||
} |
||||
} |
||||
|
||||
/// Process a message: if it's a service message, act accordingly.
|
||||
/// Otherwise, add to `self.messages`
|
||||
fn process_message(&mut self, message: Message, inc_peer: u64) -> IFResult<()> { |
||||
if self.processed_messages.contains(&message.message_id) { |
||||
return Ok(()); |
||||
} |
||||
self.processed_messages.push(message.message_id); |
||||
match &message.message_type { |
||||
MessageType::Service(msg_type) => { |
||||
match msg_type { |
||||
ServiceMessageType::TunnelBuildingForwardMovement(tunnel, sender_enc) => { |
||||
let count = *self |
||||
.tunnel_counters |
||||
.get(&tunnel.local_ids[0]) |
||||
.unwrap_or(&0u32); |
||||
if count > TUNNEL_MAX_REPEAT_COUNT { |
||||
return Ok(()); |
||||
} |
||||
self.tunnel_counters.insert(tunnel.local_ids[0], count + 1); |
||||
if message.check_recipient(&self.keys) { |
||||
let mut tunnel_pub = tunnel.clone(); |
||||
tunnel_pub.id = Some(rand::random()); |
||||
let sender = PublicKey::from_vec(self.keys.decrypt_data(sender_enc)?)?; |
||||
let tunnel = Tunnel { |
||||
id: tunnel_pub.id, |
||||
local_ids: tunnel_pub.local_ids.clone(), |
||||
peer_ids: (0, inc_peer), |
||||
ttd: 0, |
||||
nodes_in_tunnel: None, |
||||
is_multicast: false, |
||||
target_node: Some(sender), |
||||
}; |
||||
self.tunnels.push(tunnel); |
||||
self.transport.send_message( |
||||
serde_cbor::to_vec( |
||||
&Message::build() |
||||
.message_type(MessageType::Service( |
||||
ServiceMessageType::TunnelBuildingBackwardMovement( |
||||
tunnel_pub.clone(), |
||||
), |
||||
)) |
||||
.tunnel((tunnel_pub.id.unwrap(), false)) |
||||
.sign(&self.keys) |
||||
.build()?, |
||||
)?, |
||||
Some(inc_peer), |
||||
)?; |
||||
} else { |
||||
let mut tunnel = tunnel.clone(); |
||||
tunnel.add_local_id(); |
||||
self.tunnels_pending |
||||
.push((tunnel.clone(), None, (inc_peer, 0))); |
||||
self.send_to_all(message)?; |
||||
} |
||||
} |
||||
ServiceMessageType::TunnelBuildingBackwardMovement(tunnel_p) => { |
||||
match self.tunnels_pending.iter().find(|tun| { |
||||
tunnel_p.local_ids.contains(tun.0.local_ids.last().unwrap()) |
||||
}) { |
||||
// This doesn't concern us
|
||||
None => {} |
||||
// This is a tunnel initialization proposed by us (and we got it back, yay)
|
||||
Some((_, Some(target), peers)) => { |
||||
let tunnel = Tunnel { |
||||
id: tunnel_p.id, |
||||
local_ids: tunnel_p.local_ids.clone(), |
||||
peer_ids: (peers.0, inc_peer), |
||||
ttd: 0, |
||||
nodes_in_tunnel: None, |
||||
is_multicast: false, |
||||
target_node: Some(target.clone()), |
||||
}; |
||||
self.tunnels.push(tunnel); |
||||
#[cfg(feature = "std")] |
||||
println!("[{}] Successfully created a new tunnel", self.short_id()); |
||||
// Send some initialization message or something
|
||||
} |
||||
// This is a tunnel initialization proposed by someone else that has passed through us on its forward movement
|
||||
Some((_, None, peers)) => { |
||||
let tunnel = Tunnel { |
||||
id: tunnel_p.id, |
||||
local_ids: tunnel_p.local_ids.clone(), |
||||
peer_ids: (peers.0, inc_peer), |
||||
ttd: 0, |
||||
nodes_in_tunnel: None, |
||||
is_multicast: false, |
||||
target_node: None, |
||||
}; |
||||
self.tunnels.push(tunnel); |
||||
#[cfg(feature = "std")] |
||||
println!("[{}] Successfully created a new tunnel", self.short_id()); |
||||
self.transport |
||||
.send_message(serde_cbor::to_vec(&message)?, Some(peers.0))?; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
MessageType::SingleCast if message.check_recipient(&self.keys) => { |
||||
#[cfg(feature = "std")] |
||||
println!("New message: {:?}", message.get_decrypted(&self.keys)); |
||||
self.messages.insert(0, message.clone()) |
||||
} |
||||
MessageType::SingleCast => { |
||||
if let Some(tunnel) = self |
||||
.tunnels |
||||
.iter() |
||||
.find(|tun| tun.id == Some(message.tunnel_id.0)) |
||||
{ |
||||
let peer_id = if message.tunnel_id.1 { |
||||
tunnel.peer_ids.0 |
||||
} else { |
||||
tunnel.peer_ids.1 |
||||
}; |
||||
self.transport |
||||
.send_message(serde_cbor::to_vec(&message)?, Some(peer_id))?; |
||||
} |
||||
} |
||||
MessageType::Broadcast => { |
||||
#[cfg(feature = "std")] |
||||
println!("New message: {:?}", message.get_decrypted(&self.keys)); |
||||
if message.check_recipient(&self.keys) { |
||||
self.messages.insert(0, message.clone()); |
||||
} |
||||
self.send_to_all(message)?; |
||||
} |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Get a message from `self.messages`
|
||||
pub fn read_message(&mut self) -> Option<Message> { |
||||
self.messages.pop() |
||||
} |
||||
|
||||
/// Run one iteration of main loop: accepting incoming connections and messages, processing them
|
||||
pub fn main_loop_iteration(&mut self) -> IFResult<()> { |
||||
self.transport.main_loop_iteration()?; |
||||
while let Some((msg, inc_peer)) = self.transport.receive() { |
||||
self.process_message(serde_cbor::from_slice(msg.as_slice())?, inc_peer)? |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Get an id for the public key of the worker
|
||||
fn short_id(&self) -> alloc::string::String { |
||||
self.keys.get_public().get_short_id() |
||||
} |
||||
|
||||
/// Get `IFSerializationData` that can be stored in a file
|
||||
pub fn get_serialization_data(&self) -> IFSerializationData { |
||||
IFSerializationData { |
||||
keys: self.keys.clone(), |
||||
tunnels: self.tunnels.clone(), |
||||
peers: self.transport.peers.clone(), |
||||
interfaces_data: self.transport.get_interfaces_data(), |
||||
} |
||||
} |
||||
|
||||
/// Restore from `IFSerializationData`
|
||||
pub fn from_serialization_data(data: IFSerializationData) -> IFResult<Self> { |
||||
Ok(Self { |
||||
keys: data.keys, |
||||
transport: Transport::restore(data.peers.clone(), data.interfaces_data.clone())?, |
||||
tunnels: data.tunnels, |
||||
additional_modules: vec![], |
||||
messages: vec![], |
||||
tunnels_pending: vec![], |
||||
has_background_worker: false, |
||||
processed_messages: vec![], |
||||
tunnel_counters: Default::default(), |
||||
auto_save: true, |
||||
}) |
||||
} |
||||
|
||||
/// Load from file (`filename`) with `IFSerializationData`
|
||||
///
|
||||
/// If the filename is empty, the default filename is used
|
||||
#[cfg(feature = "std")] |
||||
pub fn from_file(filename: alloc::string::String) -> IFResult<Self> { |
||||
let filename = if filename.is_empty() { |
||||
DEFAULT_FILE.to_string() |
||||
} else { |
||||
filename |
||||
}; |
||||
if std::path::Path::new(&filename).exists() { |
||||
Self::from_serialization_data(serde_json::from_str( |
||||
std::fs::read_to_string(filename)?.as_str(), |
||||
)?) |
||||
} else { |
||||
Ok(Self::new()) |
||||
} |
||||
} |
||||
|
||||
/// Save `IFSerializationData` to a file with `filename`
|
||||
///
|
||||
/// If `filename` is None, the default filename is used
|
||||
#[cfg(feature = "std")] |
||||
pub fn save_to_file(&self, filename: Option<alloc::string::String>) -> IFResult<()> { |
||||
std::fs::write( |
||||
filename.unwrap_or_else(|| DEFAULT_FILE.to_string()), |
||||
serde_json::to_string(&self.get_serialization_data())?, |
||||
)?; |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Spawn a thread with IF main loop and return `Arc<Mutex<IF>>`
|
||||
#[cfg(feature = "std")] |
||||
pub fn launch_main_loop( |
||||
mut self, |
||||
sleep_millis: u64, |
||||
) -> ( |
||||
std::thread::JoinHandle<!>, |
||||
std::sync::Arc<std::sync::Mutex<Self>>, |
||||
) { |
||||
self.has_background_worker = true; |
||||
let container = std::sync::Arc::new(std::sync::Mutex::new(self)); |
||||
let container_clone = container.clone(); |
||||
let thread = std::thread::spawn(move || { |
||||
let mut counter: u64 = 0; |
||||
loop { |
||||
match container_clone.lock().unwrap().main_loop_iteration() { |
||||
Ok(_) => {} |
||||
Err(e) => println!("An error happened in the main loop: {:?}", e), |
||||
} |
||||
counter += 1; |
||||
std::thread::sleep(std::time::Duration::from_millis(sleep_millis)); |
||||
if counter % 50 == 0 { |
||||
container_clone.lock().unwrap().save_to_file(None).unwrap() |
||||
} |
||||
} |
||||
}); |
||||
(thread, container) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod if_testing { |
||||
use crate::crypto::Keys; |
||||
use crate::interface::test_interface::create_test_interfaces; |
||||
use crate::ironforce::IronForce; |
||||
use crate::message::{Message, MessageType}; |
||||
use crate::res::IFResult; |
||||
use crate::transport::Transport; |
||||
use alloc::boxed::Box; |
||||
use alloc::vec; |
||||
use alloc::vec::Vec; |
||||
|
||||
fn create_test_network() -> Vec<IronForce> { |
||||
let interfaces = create_test_interfaces(5); |
||||
let transports = interfaces |
||||
.into_iter() |
||||
.map(|interface| Transport::new(vec![Box::new(interface)])); |
||||
transports |
||||
.map(|tr| IronForce { |
||||
keys: Keys::generate(), |
||||
transport: tr, |
||||
tunnels: vec![], |
||||
additional_modules: vec![], |
||||
messages: vec![], |
||||
tunnels_pending: vec![], |
||||
has_background_worker: false, |
||||
processed_messages: vec![], |
||||
tunnel_counters: Default::default(), |
||||
auto_save: false, |
||||
}) |
||||
.collect() |
||||
} |
||||
|
||||
#[test] |
||||
fn test_creating_a_tunnel() -> IFResult<()> { |
||||
let mut network = create_test_network(); |
||||
let key_1 = network[1].keys.get_public(); |
||||
network[0].initialize_tunnel_creation(&key_1)?; |
||||
network[0].main_loop_iteration()?; |
||||
network[1].main_loop_iteration()?; |
||||
network[0].main_loop_iteration()?; |
||||
assert!(!network[0].tunnels.is_empty()); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[test] |
||||
fn test_sending_message() -> IFResult<()> { |
||||
let mut network = create_test_network(); |
||||
let key_1 = network[1].keys.get_public(); |
||||
network[0].initialize_tunnel_creation(&key_1)?; |
||||
network[0].main_loop_iteration()?; |
||||
network[1].main_loop_iteration()?; |
||||
network[0].main_loop_iteration()?; |
||||
let zero_keys = network[0].keys.clone(); |
||||
network[0].send_message( |
||||
Message::build() |
||||
.message_type(MessageType::SingleCast) |
||||
.sign(&zero_keys) |
||||
.recipient(&key_1) |
||||
.content(b"hello".to_vec()) |
||||
.build()?, |
||||
&key_1, |
||||
)?; |
||||
network[1].main_loop_iteration()?; |
||||
let msg = network[1].read_message(); |
||||
assert!(msg.is_some()); |
||||
assert_eq!( |
||||
msg.unwrap() |
||||
.get_decrypted(&network[1].keys) |
||||
.unwrap() |
||||
.as_slice(), |
||||
b"hello" |
||||
); |
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
#[cfg(feature = "std")] |
||||
mod test_with_ip { |
||||
use crate::crypto::Keys; |
||||
use crate::interfaces::ip::create_test_interfaces; |
||||
use crate::ironforce::IronForce; |
||||
use crate::message::{Message, MessageType}; |
||||
use crate::res::IFResult; |
||||
use crate::transport::Transport; |
||||
use alloc::boxed::Box; |
||||
use alloc::vec; |
||||
use alloc::vec::Vec; |
||||
use std::println; |
||||
|
||||
// fn create_test_interfaces(n: usize) -> impl Iterator<Item = IPInterface> {
|
||||
// let ip_addr = std::net::IpAddr::from_str("127.0.0.1").unwrap();
|
||||
// (0..n).map(move |i| {
|
||||
// IPInterface::new(
|
||||
// (5000 + 5 * i) as u16,
|
||||
// (0..n)
|
||||
// .filter(|j| *j != i)
|
||||
// .map(|j| (ip_addr, (5000 + 5 * j) as u16))
|
||||
// .collect(),
|
||||
// )
|
||||
// .unwrap()
|
||||
// })
|
||||
// }
|
||||
|
||||
fn create_test_network() -> Vec<IronForce> { |
||||
let interfaces = create_test_interfaces(4); |
||||
let transports = interfaces |
||||
.into_iter() |
||||
.map(|interface| Transport::new(vec![Box::new(interface)])); |
||||
transports |
||||
.map(|tr| IronForce { |
||||
keys: Keys::generate(), |
||||
transport: tr, |
||||
additional_modules: vec![], |
||||
tunnels: vec![], |
||||
messages: vec![], |
||||
tunnels_pending: vec![], |
||||
has_background_worker: false, |
||||
processed_messages: vec![], |
||||
tunnel_counters: Default::default(), |
||||
auto_save: false, |
||||
}) |
||||
.collect() |
||||
} |
||||
|
||||
// MAIN TEST RIGHT HERE
|
||||
#[test] |
||||
fn test_creating_a_tunnel_and_sending_message() -> IFResult<()> { |
||||
let mut network = create_test_network(); |
||||
let key_1 = network[1].keys.get_public(); |
||||
let (mut node0, mut node1) = (network.remove(0), network.remove(0)); |
||||
let node0_keys = node0.keys.clone(); |
||||
println!("node0 id: {}", node0.short_id()); |
||||
println!("node1 id: {}", node1.short_id()); |
||||
let (mut node2, mut node3) = (network.remove(0), network.remove(0)); |
||||
let t1 = std::thread::spawn(move || { |
||||
for _i in 0..170 { |
||||
// println!("Iteration {} (1)", i);
|
||||
node0.main_loop_iteration().unwrap(); |
||||
std::thread::sleep(std::time::Duration::from_millis(10)); |
||||
} |
||||
node0 |
||||
}); |
||||
let t2 = std::thread::spawn(move || { |
||||
for _i in 0..250 { |
||||
// println!("Iteration {} (2)", i);
|
||||
node1.main_loop_iteration().unwrap(); |
||||
std::thread::sleep(std::time::Duration::from_millis(10)); |
||||
} |
||||
node1 |
||||
}); |
||||
std::thread::spawn(move || loop { |
||||
node2.main_loop_iteration().unwrap(); |
||||
std::thread::sleep(std::time::Duration::from_millis(10)); |
||||
}); |
||||
std::thread::spawn(move || loop { |
||||
std::thread::sleep(std::time::Duration::from_millis(10)); |
||||
node3.main_loop_iteration().unwrap(); |
||||
}); |
||||
let mut node0 = t1.join().unwrap(); |
||||
node0.initialize_tunnel_creation(&key_1)?; |
||||
let mut node1 = t2.join().unwrap(); |
||||
let t1 = std::thread::spawn(move || { |
||||
for _ in 0..18 { |
||||
node0.main_loop_iteration().unwrap(); |
||||
std::thread::sleep(std::time::Duration::from_millis(50)); |
||||
} |
||||
node0 |
||||
}); |
||||
let t2 = std::thread::spawn(move || { |
||||
for _ in 0..18 { |
||||
node1.main_loop_iteration().unwrap(); |
||||
} |
||||
node1 |
||||
}); |
||||
let mut node0 = t1.join().unwrap(); |
||||
let mut node1 = t2.join().unwrap(); |
||||
assert!(!node0.tunnels.is_empty()); |
||||
node0.send_message( |
||||
Message::build() |
||||
.message_type(MessageType::SingleCast) |
||||
.content(b"Hello!".to_vec()) |
||||
.recipient(&key_1) |
||||
.sign(&node0_keys) |
||||
.build()?, |
||||
&key_1, |
||||
)?; |
||||
let t2 = std::thread::spawn(move || { |
||||
for _ in 0..18 { |
||||
node1.main_loop_iteration().unwrap(); |
||||
} |
||||
node1 |
||||
}); |
||||
let mut node1 = t2.join().unwrap(); |
||||
let msg = node1.read_message(); |
||||
assert!(msg.is_some()); |
||||
assert_eq!(msg.unwrap().get_decrypted(&node1.keys)?, b"Hello!".to_vec()); |
||||
Ok(()) |
||||
} |
||||
} |
@ -0,0 +1,34 @@
|
||||
#![no_std] |
||||
#![allow(dead_code)] |
||||
#![feature(trait_alias)] |
||||
#![feature(never_type)] |
||||
|
||||
#[cfg(feature = "std")] |
||||
extern crate std; |
||||
|
||||
extern crate alloc; |
||||
extern crate rand; |
||||
extern crate rsa; |
||||
extern crate serde; |
||||
extern crate core_error; |
||||
extern crate spin; |
||||
#[cfg(feature = "std")] |
||||
extern crate include_optional; |
||||
|
||||
mod crypto; |
||||
mod ironforce; |
||||
mod message; |
||||
mod transport; |
||||
pub mod interface; |
||||
pub mod interfaces; |
||||
pub mod res; |
||||
mod tunnel; |
||||
|
||||
|
||||
pub use ironforce::IronForce; |
||||
pub use message::{Message, MessageType}; |
||||
pub use crypto::{Keys, PublicKey}; |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
} |
@ -0,0 +1,398 @@
|
||||
use crate::crypto::{Keys, PublicKey}; |
||||
use crate::res::IFResult; |
||||
use crate::tunnel::TunnelPublic; |
||||
use alloc::string::String; |
||||
use alloc::vec::Vec; |
||||
use serde::{Deserialize, Serialize}; |
||||
use sha2::Digest; |
||||
|
||||
/// A serialized message
|
||||
pub(crate) type MessageBytes = Vec<u8>; |
||||
|
||||
/// Signature of the message: optional and optionally encrypted sender's key and signed hash
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Hash)] |
||||
pub enum Signature { |
||||
/// The message is signed. Author is unknown
|
||||
NotSigned, |
||||
/// The message is signed with the sender's key visible to everyone
|
||||
Signed { |
||||
sender: PublicKey, |
||||
signature: Vec<u8>, |
||||
}, |
||||
/// Sender's key is encrypted for the recipient
|
||||
SignedPrivately { |
||||
sender_encrypted: Vec<u8>, |
||||
signature: Vec<u8>, |
||||
}, |
||||
} |
||||
|
||||
impl Signature { |
||||
/// Get sender's key or its encrypted version for hashing
|
||||
pub(crate) fn sender_or_encrypted_sender(&self) -> Option<Vec<u8>> { |
||||
match &self { |
||||
Signature::NotSigned => None, |
||||
Signature::Signed { sender, .. } => Some(sender.to_vec()), |
||||
Signature::SignedPrivately { |
||||
sender_encrypted, .. |
||||
} => Some(sender_encrypted.clone()), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Network name and version
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Hash)] |
||||
pub struct NetworkInfo { |
||||
network_name: String, |
||||
version: String, |
||||
} |
||||
|
||||
impl Default for NetworkInfo { |
||||
fn default() -> Self { |
||||
Self { |
||||
version: String::from("0.1.0"), |
||||
network_name: String::from("test"), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Hash)] |
||||
pub enum MessageType { |
||||
SingleCast, |
||||
Broadcast, |
||||
Service(ServiceMessageType), |
||||
} |
||||
|
||||
impl MessageType { |
||||
fn hash(&self) -> Vec<u8> { |
||||
match self { |
||||
MessageType::SingleCast => Vec::from([0]), |
||||
MessageType::Broadcast => Vec::from([1]), |
||||
MessageType::Service(ServiceMessageType::TunnelBuildingForwardMovement( |
||||
tunnel, |
||||
sender_enc, |
||||
)) => [2, 0] |
||||
.iter() |
||||
.chain(tunnel.hash().iter()) |
||||
.chain(sender_enc) |
||||
.copied() |
||||
.collect(), |
||||
MessageType::Service(ServiceMessageType::TunnelBuildingBackwardMovement(tunnel)) => { |
||||
[3, 0].iter().chain(tunnel.hash().iter()).copied().collect() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Hash)] |
||||
pub enum ServiceMessageType { |
||||
/// Creating a tunnel - stage 1
|
||||
///
|
||||
/// (tunnel to be created, sending node encrypted for the recipient)
|
||||
TunnelBuildingForwardMovement(TunnelPublic, Vec<u8>), |
||||
TunnelBuildingBackwardMovement(TunnelPublic), |
||||
} |
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Hash)] |
||||
pub enum MessageContent { |
||||
/// Just plaintext message content
|
||||
Plain(Vec<u8>), |
||||
/// Message content bytes encrypted for the recipient
|
||||
Encrypted(Vec<u8>), |
||||
/// There is no content
|
||||
None, |
||||
} |
||||
|
||||
impl Default for MessageContent { |
||||
fn default() -> Self { |
||||
MessageContent::None |
||||
} |
||||
} |
||||
|
||||
impl MessageContent { |
||||
pub fn hash(&self) -> Vec<u8> { |
||||
match self { |
||||
MessageContent::Plain(v) => sha2::Sha512::new() |
||||
.chain(&[0u8; 1]) |
||||
.chain(v.as_slice()) |
||||
.result() |
||||
.to_vec(), |
||||
MessageContent::Encrypted(v) => sha2::Sha512::new() |
||||
.chain(&[1; 1]) |
||||
.chain(v.as_slice()) |
||||
.result() |
||||
.to_vec(), |
||||
MessageContent::None => Vec::new(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// The struct for messages that are sent in the network
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Hash)] |
||||
pub struct Message { |
||||
/// Content of the message (not to be confused with the bytes that we are sending through interfaces)
|
||||
///
|
||||
/// AKA useful payload
|
||||
pub content: MessageContent, |
||||
/// The type of this message
|
||||
pub message_type: MessageType, |
||||
/// Sender's signature
|
||||
pub signature: Signature, |
||||
/// A random number that is used in hash together with the content
|
||||
pub message_id: u64, |
||||
/// Hash of message content and the salt
|
||||
hash: Vec<u8>, |
||||
/// Optional: hash of the message encrypted for the recipient, so that the recipient can know that this message is for them, but nobody else
|
||||
recipient_verification: Option<Vec<u8>>, |
||||
/// ID of the tunnel that is used and the direction
|
||||
pub tunnel_id: (u64, bool), |
||||
/// Network info
|
||||
network_info: NetworkInfo, |
||||
} |
||||
|
||||
impl Message { |
||||
/// Verify message's hash
|
||||
pub fn verify_hash(&self) -> bool { |
||||
self.hash |
||||
== Self::calculate_hash( |
||||
&self.content, |
||||
self.message_type.clone(), |
||||
self.signature.sender_or_encrypted_sender(), |
||||
&self.network_info, |
||||
) |
||||
} |
||||
|
||||
/// Verify sender's signature
|
||||
pub fn verify_signature(&self, recipient_keys: Keys) -> bool { |
||||
match &self.signature { |
||||
Signature::NotSigned => true, |
||||
Signature::Signed { signature, sender } => { |
||||
sender.verify_sign(self.hash.as_slice(), signature.as_slice()) |
||||
} |
||||
Signature::SignedPrivately { signature, .. } => { |
||||
if let Some(sender) = self.get_sender(&recipient_keys) { |
||||
sender.verify_sign( |
||||
self.hash.as_slice(), |
||||
&match recipient_keys.decrypt_data(signature.as_slice()) { |
||||
Ok(r) => r, |
||||
Err(_e) => return false, |
||||
}, |
||||
) |
||||
} else { |
||||
false |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Check if this message is for this set of keys
|
||||
pub fn check_recipient(&self, keys: &Keys) -> bool { |
||||
if self.recipient_verification.is_none() { |
||||
true |
||||
} else { |
||||
keys.decrypt_data(&self.recipient_verification.clone().unwrap()) |
||||
.is_ok() |
||||
} |
||||
} |
||||
|
||||
/// Get decrypted content of the message
|
||||
pub fn get_decrypted(&self, keys: &Keys) -> IFResult<Vec<u8>> { |
||||
Ok(match &self.content { |
||||
MessageContent::Plain(c) => c.clone(), |
||||
MessageContent::Encrypted(encrypted_content) => { |
||||
keys.decrypt_data(encrypted_content.as_slice())? |
||||
} |
||||
MessageContent::None => Vec::new(), |
||||
}) |
||||
} |
||||
|
||||
pub fn calculate_hash( |
||||
content: &MessageContent, |
||||
message_type: MessageType, |
||||
sender_or_encrypted_sender: Option<Vec<u8>>, |
||||
network_info: &NetworkInfo, |
||||
) -> Vec<u8> { |
||||
sha2::Sha512::new() |
||||
.chain(content.hash().as_slice()) |
||||
.chain(message_type.hash().as_slice()) |
||||
.chain(sender_or_encrypted_sender.unwrap_or_default().as_slice()) |
||||
.chain(network_info.network_name.as_bytes()) |
||||
.chain(network_info.version.as_bytes()) |
||||
.result() |
||||
.to_vec() |
||||
} |
||||
|
||||
/// Encrypt hash of the message for the recipient
|
||||
pub fn generate_recipient_verification( |
||||
hash: Vec<u8>, |
||||
recipient: PublicKey, |
||||
) -> rsa::errors::Result<Vec<u8>> { |
||||
recipient.encrypt_data(&hash) |
||||
} |
||||
|
||||
/// Try to get sender from the signature
|
||||
pub fn get_sender(&self, keys: &Keys) -> Option<PublicKey> { |
||||
match &self.signature { |
||||
Signature::NotSigned => None, |
||||
Signature::Signed { sender, .. } => Some(sender.clone()), |
||||
Signature::SignedPrivately { |
||||
sender_encrypted, .. |
||||
} => { |
||||
if let Some(Some(res)) = keys |
||||
.decrypt_data(sender_encrypted.as_slice()) |
||||
.ok() |
||||
.map(|k| PublicKey::from_vec(k).ok()) |
||||
{ |
||||
Some(res) |
||||
} else { |
||||
None |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Create new MessageBuilder
|
||||
pub fn build() -> MessageBuilder { |
||||
MessageBuilder::new() |
||||
} |
||||
} |
||||
|
||||
/// Message builder to create a new message step-by-step, like `Message::build().message_type(...).sign(...)`
|
||||
#[derive(Default)] |
||||
pub struct MessageBuilder { |
||||
content: MessageContent, |
||||
/// The type of the message to be built
|
||||
message_type: Option<MessageType>, |
||||
/// Sender's keys
|
||||
sender: Option<Keys>, |
||||
/// Recipient's public key (if present, the content will be encrypted and recipient verification field will be set)
|
||||
recipient: Option<PublicKey>, |
||||
/// ID of the tunnel that is used
|
||||
tunnel_id: (u64, bool), |
||||
} |
||||
|
||||
impl MessageBuilder { |
||||
/// Create a new `MessageBuilder` with default parameters
|
||||
pub fn new() -> Self { |
||||
Default::default() |
||||
} |
||||
|
||||
pub fn content(mut self, cont: Vec<u8>) -> Self { |
||||
self.content = MessageContent::Plain(cont); |
||||
self |
||||
} |
||||
|
||||
/// Sign the message
|
||||
pub fn sign(mut self, keys: &Keys) -> Self { |
||||
self.sender = Some(keys.clone()); |
||||
self |
||||
} |
||||
|
||||
/// Set message's recipient (and therefore set recipient verification and encrypt the content)
|
||||
pub fn recipient(mut self, recipient: &PublicKey) -> Self { |
||||
self.recipient = Some(recipient.clone()); |
||||
self |
||||
} |
||||
|
||||
/// Set tunnel id
|
||||
pub fn tunnel(mut self, tunnel_id: (u64, bool)) -> Self { |
||||
self.tunnel_id = tunnel_id; |
||||
self |
||||
} |
||||
|
||||
/// Set message's type
|
||||
pub fn message_type(mut self, message_type: MessageType) -> Self { |
||||
self.message_type = Some(message_type); |
||||
self |
||||
} |
||||
|
||||
/// Get the resulting message
|
||||
pub fn build(self) -> IFResult<Message> { |
||||
let salt = rand::random(); |
||||
let sender_encrypted = if let (Some(sender_keys), Some(recipient)) = |
||||
(self.sender.as_ref(), self.recipient.as_ref()) |
||||
{ |
||||
Some(recipient.encrypt_data(&sender_keys.get_public().to_vec())?) |
||||
} else { |
||||
None |
||||
}; |
||||
let network_info = NetworkInfo::default(); |
||||
let hash = Message::calculate_hash( |
||||
&self.content, |
||||
self.message_type.clone().unwrap(), |
||||
sender_encrypted.clone().or_else(|| { |
||||
self.sender |
||||
.as_ref() |
||||
.map(|sender_keys| sender_keys.get_public().to_vec()) |
||||
}), |
||||
&network_info, |
||||
); |
||||
let recipient_verification = self |
||||
.recipient |
||||
.as_ref() |
||||
.map(|rec| rec.encrypt_data(&hash).unwrap()); |
||||
let signature = match (self.sender, self.recipient) { |
||||
(Some(sender_keys), Some(recipient_key)) => Signature::SignedPrivately { |
||||
sender_encrypted: sender_encrypted.unwrap(), |
||||
signature: recipient_key.encrypt_data(&sender_keys.sign(&hash)?)?, |
||||
}, |
||||
(Some(sender_keys), None) => Signature::Signed { |
||||
sender: sender_keys.get_public(), |
||||
signature: sender_keys.sign(&hash)?, |
||||
}, |
||||
(None, _) => Signature::NotSigned, |
||||
}; |
||||
Ok(Message { |
||||
content: self.content, |
||||
message_type: self.message_type.unwrap(), |
||||
signature, |
||||
message_id: salt, |
||||
hash, |
||||
recipient_verification, |
||||
tunnel_id: self.tunnel_id, |
||||
network_info, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
use alloc::vec; |
||||
|
||||
#[test] |
||||
fn test_hashing_message_type() { |
||||
let msg_type_1 = MessageType::Broadcast; |
||||
let msg_type_2 = MessageType::Service(ServiceMessageType::TunnelBuildingForwardMovement( |
||||
TunnelPublic::new_for_test(), |
||||
vec![1, 2, 3], |
||||
)); |
||||
assert_eq!(msg_type_1.hash(), msg_type_1.hash()); |
||||
assert_eq!(msg_type_2.hash(), msg_type_2.hash()); |
||||
assert_ne!(msg_type_1.hash(), msg_type_2.hash()) |
||||
} |
||||
|
||||
#[test] |
||||
fn test_hash_message_content() { |
||||
let content_1 = MessageContent::Plain(vec![1, 2, 4, 5]); |
||||
let content_2 = MessageContent::Encrypted(vec![1, 2, 4, 5]); |
||||
let content_3 = MessageContent::Plain(vec![1, 3, 4, 5]); |
||||
assert_eq!(content_1.hash(), content_1.hash()); |
||||
assert_ne!(content_1.hash(), MessageContent::None.hash()); |
||||
assert_ne!(content_1.hash(), content_2.hash()); |
||||
assert_ne!(content_1.hash(), content_3.hash()); |
||||
assert_ne!(content_3.hash(), content_2.hash()); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_building_message() -> IFResult<()> { |
||||
let keys_1 = Keys::generate(); |
||||
let keys_2 = Keys::generate(); |
||||
let msg = Message::build() |
||||
.content(b"hello".to_vec()) |
||||
.sign(&keys_1) |
||||
.recipient(&keys_2.get_public()) |
||||
.tunnel((1, false)) |
||||
.message_type(MessageType::SingleCast) |
||||
.build()?; |
||||
assert!(msg.verify_hash()); |
||||
assert!(msg.verify_signature(keys_2)); |
||||
Ok(()) |
||||
} |
@ -0,0 +1,96 @@
|
||||
use alloc::vec::Vec; |
||||
use crate::crypto::PublicKey; |
||||
use serde::{Serialize, Deserialize}; |
||||
use sha2::Digest; |
||||
use alloc::vec; |
||||
|
||||
/// A tunnel that is used for communication
|
||||
#[derive(Serialize, Clone, Deserialize, Debug, Hash)] |
||||
pub struct Tunnel { |
||||
/// Tunnel's id.
|
||||
/// By the way, this id is `None` until the tunnel is validated in the backward movement
|
||||
pub id: Option<u64>, |
||||
/// Ids, each of them is just for local storage on each node until a final global id is created
|
||||
pub local_ids: Vec<u64>, |
||||
/// Ids of peers (in transport) by which we can send a message - one for backward direction, another for forward
|
||||
pub peer_ids: (u64, u64), |
||||
/// Time at which this tunnel should be destroyed (UNIX epoch)
|
||||
pub ttd: u64, |
||||
/// Public keys of nodes in the tunnel
|
||||
pub nodes_in_tunnel: Option<Vec<PublicKey>>, |
||||
/// Is this tunnel used for multicast?
|
||||
pub is_multicast: bool, |
||||
/// If we created this tunnel, then this is the node that it's used to communicate with
|
||||
pub target_node: Option<PublicKey>, |
||||
} |
||||
|
||||
/// Tunnel, but only the fields that are ok to share
|
||||
#[derive(Serialize, Clone, Deserialize, Debug, Hash)] |
||||
pub struct TunnelPublic { |
||||
/// Tunnel's id
|
||||
pub id: Option<u64>, |
||||
/// Ids, each of them is just for local storage on each node until a final global id is created
|
||||
pub local_ids: Vec<u64>, |
||||
/// Time at which this tunnel should be destroyed (UNIX epoch)
|
||||
pub ttd: u64, |
||||
/// Public keys of nodes in the tunnel
|
||||
nodes_in_tunnel: Option<Vec<PublicKey>>, |
||||
/// Is this tunnel used for multicast?
|
||||
pub is_multicast: bool, |
||||
} |
||||
|
||||
impl TunnelPublic { |
||||
pub fn new_singlecast() -> Self { |
||||
let mut tun = Self { |
||||
id: None, |
||||
local_ids: vec![], |
||||
ttd: 0, |
||||
nodes_in_tunnel: None, |
||||
is_multicast: false, |
||||
}; |
||||
tun.add_local_id(); |
||||
tun |
||||
} |
||||
|
||||
/// Get the hash of the tunnel for verification
|
||||
pub fn hash(&self) -> Vec<u8> { |
||||
sha2::Sha224::new() |
||||
.chain(serde_cbor::to_vec(self).unwrap().as_slice()) |
||||
.result().to_vec() |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
pub fn new_for_test() -> Self { |
||||
TunnelPublic { |
||||
id: Some(56), |
||||
local_ids: vec![5, 500, 120], |
||||
ttd: 56, |
||||
nodes_in_tunnel: Some(vec![crate::crypto::Keys::generate().get_public()]), |
||||
is_multicast: true, |
||||
} |
||||
} |
||||
|
||||
pub fn add_local_id(&mut self) -> u64 { |
||||
let local_id = rand::random(); |
||||
// Add 0 to 7 random ids so it's impossible to get the length of the tunnel
|
||||
// todo: enable it (after debug)
|
||||
// for _ in 0..(rand::random() as u64 % 7) {
|
||||
// self.local_ids.push(rand::random())
|
||||
// }
|
||||
self.local_ids.push(local_id); |
||||
local_id |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn test_tunnel_hashing() { |
||||
let tun = TunnelPublic::new_for_test(); |
||||
assert_eq!(tun.hash(), tun.hash()); |
||||
assert_ne!(tun.hash(), TunnelPublic { |
||||
id: Some(56), |
||||
local_ids: vec![5, 500, 120], |
||||
ttd: 56, |
||||
nodes_in_tunnel: Some(vec![crate::crypto::Keys::generate().get_public()]), |
||||
is_multicast: false, |
||||
}.hash()); |
||||
} |
@ -1,105 +0,0 @@
|
||||
/// This module has wrappers for cryptography with RSA algorithms.
|
||||
/// Its main structs - `PublicKey` and `Keys` implement all functions for key generation, signatures and asymmetric encryption
|
||||
use alloc::vec::Vec; |
||||
use serde::{Deserialize, Serialize}; |
||||
use rsa::{RsaPublicKey, RsaPrivateKey, PaddingScheme, PublicKey as RPK}; |
||||
use rsa::errors::Result as RsaRes; |
||||
use rand::rngs::OsRng; |
||||
|
||||
static KEY_LENGTH: usize = 2048; |
||||
|
||||
|
||||
/// Public key of a node
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] |
||||
pub struct PublicKey { |
||||
pub key: RsaPublicKey, |
||||
} |
||||
|
||||
impl PublicKey { |
||||
/// Check if the sign is valid for given data and key
|
||||
pub fn verify_sign(&self, data: &[u8], sign: &[u8]) -> bool { |
||||
self.key.verify(PaddingScheme::PKCS1v15Sign { hash: None }, data, sign).is_ok() |
||||
} |
||||
|
||||
/// Encrypt some data for a user with this public key
|
||||
pub fn encrypt_data(&self, data: &[u8]) -> RsaRes<Vec<u8>> { |
||||
self.key.encrypt(&mut OsRng {}, PaddingScheme::PKCS1v15Encrypt, data) |
||||
} |
||||
|
||||
pub fn to_vec(&self) -> serde_cbor::Result<Vec<u8>> { |
||||
serde_cbor::to_vec(&self) |
||||
} |
||||
} |
||||
|
||||
/// Key pair (public and secret) for a node, should be stored locally
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)] |
||||
pub struct Keys { |
||||
public_key: RsaPublicKey, |
||||
private_key: RsaPrivateKey, |
||||
} |
||||
|
||||
impl Keys { |
||||
/// Generate new random key
|
||||
pub fn generate() -> Self { |
||||
let mut rng = OsRng; |
||||
let private_key = RsaPrivateKey::new(&mut rng, KEY_LENGTH).expect("failed to generate a key"); |
||||
let public_key = RsaPublicKey::from(&private_key); |
||||
Self { |
||||
private_key, |
||||
public_key, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Keys { |
||||
/// Sign content using these keys
|
||||
pub fn sign(&self, content: &[u8]) -> RsaRes<Vec<u8>> { |
||||
self.private_key.sign(PaddingScheme::PKCS1v15Sign { hash: None }, content) |
||||
} |
||||
|
||||
/// Decrypt data
|
||||
pub fn decrypt_data(&self, data_encrypted: &[u8]) -> RsaRes<Vec<u8>> { |
||||
self.private_key.decrypt(PaddingScheme::PKCS1v15Encrypt, data_encrypted) |
||||
} |
||||
|
||||
/// Get public key
|
||||
pub fn get_public(&self) -> PublicKey { |
||||
PublicKey { key: self.public_key.clone() } |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn test_encrypt() { |
||||
let data = vec![0, 5, 8, 135, 67]; |
||||
let keys = Keys::generate(); |
||||
assert_eq!( |
||||
keys.decrypt_data(&keys.get_public().encrypt_data(&data).unwrap()).unwrap(), |
||||
data |
||||
); |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
use alloc::vec; |
||||
|
||||
#[test] |
||||
fn test_invalid_encrypt() { |
||||
let data = vec![0, 5, 8, 135, 67]; |
||||
let keys_1 = Keys::generate(); |
||||
let keys_2 = Keys::generate(); |
||||
assert!(keys_2.decrypt_data(&keys_1.get_public().encrypt_data(&data).unwrap()).is_err()); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_signing() { |
||||
let data = vec![0, 5, 8, 135, 67]; |
||||
let keys = Keys::generate(); |
||||
assert!(keys.get_public().verify_sign(&data, &keys.sign(&data).unwrap())); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_invalid_signing() { |
||||
let data = vec![0, 5, 8, 135, 67]; |
||||
let keys_1 = Keys::generate(); |
||||
let keys_2 = Keys::generate(); |
||||
assert!(!keys_2.get_public().verify_sign(&data, &keys_1.sign(&data).unwrap())); |
||||
} |
@ -1,78 +0,0 @@
|
||||
use alloc::string::String; |
||||
use crate::message::MessageBytes; |
||||
use crate::res::IFResult; |
||||
|
||||
|
||||
/// Some data that can be provided to the interface to send the message to a target.
|
||||
///
|
||||
/// For IP this might be `IP:port`.
|
||||
/// Radio interface, for example, may not have the functionality of targeting, but that's fine
|
||||
pub(crate) type TargetingData = String; |
||||
|
||||
/// In an std environment we require that the interface can be send safely between threads
|
||||
#[cfg(not(std))] |
||||
pub trait InterfaceRequirements {} |
||||
|
||||
#[cfg(std)] |
||||
pub trait InterfaceRequirements = Send + Sync; |
||||
|
||||
|
||||
/// An interface that can be used to
|
||||
pub trait Interface: InterfaceRequirements { |
||||
/// Run one main loop iteration.
|
||||
/// On platforms that support concurrency, these functions will be run simultaneously for all interfaces.
|
||||
/// Most likely, this function will accept messages and save them somewhere internally to give out later in `Interface.receive()`.
|
||||
///
|
||||
/// For systems that don't support concurrency, there can be only one interface in this function waits for a message (to avoid blocking).
|
||||
/// That's why it's necessary to check if it is the case for this interface, and it's done using function `Interface::has_blocking_main()`
|
||||
fn main_loop_iteration(&mut self) -> IFResult<()>; |
||||
/// Check if `main_loop_iteration` stops execution and waits for a message
|
||||
fn has_blocking_main(&self) -> bool { |
||||
false // hopefully...
|
||||
} |
||||
/// Get some way of identification for this interface
|
||||
fn id(&self) -> &str; |
||||
/// Send a message. If no `interface_data` is provided, we should consider it to be a broadcast.
|
||||
/// If, on the other hand, `interface_data` is not `None`, it should be used to send the message to the target.
|
||||
fn send(&mut self, message: &[u8] /*MessageBytes*/, interface_data: Option<TargetingData>) -> IFResult<()>; |
||||
/// Receive a message through this interface. Returns a result with an option of (message bytes, target).
|
||||
/// `None` means there is no message available at the time.
|
||||
/// The implementations of this function shouldn't wait for new messages, but
|
||||
fn receive(&mut self) -> IFResult<Option<(MessageBytes, TargetingData /*interface data*/)>>; |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
pub mod test_interface { |
||||
use crate::interface::{Interface, InterfaceRequirements, TargetingData}; |
||||
use crate::message::MessageBytes; |
||||
use crate::res::IFResult; |
||||
use alloc::vec::Vec; |
||||
|
||||
#[derive(Default)] |
||||
pub struct TestInterface { |
||||
messages: Vec<(Vec<u8>, TargetingData)>, |
||||
} |
||||
|
||||
impl InterfaceRequirements for TestInterface {} |
||||
|
||||
impl Interface for TestInterface { |
||||
fn main_loop_iteration(&mut self) -> IFResult<()> { |
||||
Ok(()) |
||||
} |
||||
|
||||
fn id(&self) -> &str { |
||||
"test_interface" |
||||
} |
||||
|
||||
fn send(&mut self, message: &[u8], interface_data: Option<TargetingData>) -> IFResult<()> { |
||||
self.messages.push((Vec::from(message), interface_data.unwrap_or_default())); |
||||
Ok(()) |
||||
} |
||||
|
||||
fn receive(&mut self) -> IFResult<Option<(MessageBytes, TargetingData)>> { |
||||
Ok( |
||||
self.messages.pop() |
||||
) |
||||
} |
||||
} |
||||
} |
@ -1,6 +0,0 @@
|
||||
use crate::interface::Interface; |
||||
use alloc::vec; |
||||
|
||||
pub fn get_interfaces() -> alloc::vec::Vec<alloc::boxed::Box<dyn Interface>> { |
||||
vec![] |
||||
} |
@ -1,79 +0,0 @@
|
||||
use alloc::vec::Vec; |
||||
use alloc::vec; |
||||
use crate::crypto::PublicKey; |
||||
use crate::message::{Message, MessageType, ServiceMessageType}; |
||||
use crate::res::{IFError, IFResult}; |
||||
use crate::transport::Transport; |
||||
use crate::tunnel::Tunnel; |
||||
|
||||
|
||||
/// Main worker
|
||||
pub struct IronForce { |
||||
/// the struct that manages communicating with neighbor nodes
|
||||
transport: Transport, |
||||
/// Tunnels that are known to this node
|
||||
tunnels: Vec<Tunnel>, |
||||
/// Additional modules that may be plugged in later,
|
||||
/// for example internet access (like Tor)
|
||||
/// and some kind of decentralized storage
|
||||
additional_modules: Vec<()>, |
||||
/// Non-service messages to give outside
|
||||
messages: Vec<Message>, |
||||
} |
||||
|
||||
impl IronForce { |
||||
/// Create new worker
|
||||
pub fn new() -> Self { |
||||
Self { |
||||
transport: Transport::new(crate::interfaces::get_interfaces()), |
||||
tunnels: vec![], |
||||
additional_modules: vec![], |
||||
messages: vec![], |
||||
} |
||||
} |
||||
|
||||
/// Create a new tunnel to another node
|
||||
fn create_new_tunnel(&mut self, _destination: PublicKey) -> IFResult<Tunnel> { |
||||
todo!() |
||||
} |
||||
|
||||
/// Send a multicast or broadcast message
|
||||
fn send_to_all(&mut self, _message: Message) -> IFResult<()> { |
||||
todo!() |
||||
} |
||||
|
||||
/// Send a message through tunnel
|
||||
fn send_through_tunnel(&mut self, _tunnel_id: u64, _message: Message, _direction: Option<bool>) -> IFResult<()> { |
||||
todo!() |
||||
} |
||||
|
||||
/// Send a message to another node,
|
||||
/// creating a new tunnel if needed
|
||||
pub fn send_message(&mut self, message: Message, destination: PublicKey) -> IFResult<()> { |
||||
if let Some(Some(tunnel_id)) = self.tunnels.iter() |
||||
.find(|t| t.target_node.as_ref() == Some(&destination) || t.nodes_in_tunnel.as_ref().map(|nodes| nodes.contains(&destination)) == Some(true)) |
||||
.map(|tunnel| tunnel.id) { |
||||
self.send_through_tunnel(tunnel_id, message, None) |
||||
} else { |
||||
Err(IFError::TunnelNotFound) |
||||
} |
||||
} |
||||
|
||||
/// Process a message: if it's a service message, act accordingly.
|
||||
/// Otherwise, add to `self.messages`
|
||||
fn process_message(&mut self, message: Message) { |
||||
match message.message_type { |
||||
MessageType::Service(msg_type) => match msg_type { |
||||
ServiceMessageType::TunnelBuilding(_tunnel) => { |
||||
todo!() |
||||
} |
||||
} |
||||
MessageType::SingleCast | MessageType::Broadcast => { self.messages.push(message) } |
||||
} |
||||
} |
||||
|
||||
/// Get a message from `self.messages`
|
||||
pub fn read_message(&mut self) -> Option<Message> { |
||||
self.messages.pop() |
||||
} |
||||
} |
@ -1,21 +0,0 @@
|
||||
#![no_std] |
||||
#![allow(dead_code)] |
||||
|
||||
extern crate alloc; |
||||
extern crate rand; |
||||
extern crate rsa; |
||||
extern crate serde; |
||||
extern crate core_error; |
||||
|
||||
mod crypto; |
||||
mod ironforce; |
||||
mod message; |
||||
mod transport; |
||||
mod interface; |
||||
mod interfaces; |
||||
mod res; |
||||
mod tunnel; |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
} |
@ -1,203 +0,0 @@
|
||||
use alloc::string::String; |
||||
use alloc::vec::Vec; |
||||
use crate::crypto::{Keys, PublicKey}; |
||||
use crate::res::IFResult; |
||||
use crate::tunnel::TunnelPublic; |
||||
use serde::{Serialize, Deserialize}; |
||||
|
||||
|
||||
/// A serialized message
|
||||
pub(crate) type MessageBytes = Vec<u8>; |
||||
|
||||
/// Signature of the message: optional and optionally encrypted sender's key and signed hash
|
||||
#[derive(Serialize, Deserialize, Clone)] |
||||
pub enum Signature { |
||||
/// The message is signed. Author is unknown
|
||||
NotSigned, |
||||
/// The message is signed with the sender's key visible to everyone
|
||||
Signed { |
||||
sender: PublicKey, |
||||
signature: Vec<u8>, |
||||
}, |
||||
/// Sender's key is encrypted for the recipient
|
||||
SignedPrivately { |
||||
sender_encrypted: Vec<u8>, |
||||
signature: Vec<u8>, |
||||
}, |
||||
} |
||||
|
||||
/// Network name and version
|
||||
#[derive(Serialize, Deserialize, Clone)] |
||||
struct NetworkInfo { |
||||
network_name: String, |
||||
version: String, |
||||
} |
||||
|
||||
impl Default for NetworkInfo { |
||||
fn default() -> Self { |
||||
Self { version: String::from("0.1.0"), network_name: String::from("test") } |
||||
} |
||||
} |
||||
|
||||
#[derive(Serialize, Deserialize, Clone)] |
||||
pub enum MessageType { |
||||
SingleCast, |
||||
Broadcast, |
||||
Service(ServiceMessageType), |
||||
} |
||||
|
||||
#[derive(Serialize, Deserialize, Clone)] |
||||
pub enum ServiceMessageType { |
||||
TunnelBuilding(TunnelPublic) |
||||
} |
||||
|
||||
#[derive(Serialize, Deserialize, Clone)] |
||||
pub enum MessageContent { |
||||
/// Just plaintext message content
|
||||
Plain(Vec<u8>), |
||||
/// Message content bytes encrypted for the recipient
|
||||
Encrypted(Vec<u8>), |
||||
/// There is no content
|
||||
None, |
||||
} |
||||
|
||||
/// The struct for messages that are sent in the network
|
||||
#[derive(Serialize, Deserialize, Clone)] |
||||
pub struct Message { |
||||
/// Content of the message (not to be confused with the bytes that we are sending through interfaces)
|
||||
///
|
||||
/// AKA useful payload
|
||||
pub content: MessageContent, |
||||
/// The type of this message
|
||||
pub message_type: MessageType, |
||||
/// Sender's signature
|
||||
pub signature: Signature, |
||||
/// A random number that is used in hash together with the content
|
||||
salt: u64, |
||||
/// Hash of message content and the salt
|
||||
hash: Vec<u8>, |
||||
/// Optional: hash of the message encrypted for the recipient, so that the recipient can know that this message is for them, but nobody else
|
||||
recipient_verification: Option<Vec<u8>>, |
||||
/// ID of the tunnel that is used
|
||||
tunnel_id: u64, |
||||
/// Network info
|
||||
network_info: NetworkInfo, |
||||
} |
||||
|
||||
impl Message { |
||||
/// Verify message's hash
|
||||
pub fn verify(&self) -> bool { |
||||
todo!() |
||||
} |
||||
|
||||
/// Check if this message is for this set of keys
|
||||
pub fn check_recipient(&self, _keys: Keys) -> bool { |
||||
todo!() |
||||
} |
||||
|
||||
/// Get decrypted content of the message
|
||||
pub fn get_decrypted(&self, _keys: Keys) -> IFResult<Vec<u8>> { |
||||
todo!() |
||||
} |
||||
|
||||
pub fn calculate_hash(_content: &MessageContent, _message_type: MessageType, _sender_or_encrypted_sender: Option<Vec<u8>>) -> Vec<u8> { |
||||
todo!() |
||||
} |
||||
|
||||
/// Encrypt hash of the message for the recipient
|
||||
pub fn generate_recipient_verification(hash: Vec<u8>, recipient: PublicKey) -> rsa::errors::Result<Vec<u8>> { |
||||
recipient.encrypt_data(&hash) |
||||
} |
||||
} |
||||
|
||||
|
||||
/// Message builder to create a new message step-by-step, like `Message::build().message_type(...).sign(...)`
|
||||
pub struct MessageBuilder { |
||||
content: MessageContent, |
||||
/// The type of the message to be built
|
||||
message_type: Option<MessageType>, |
||||
/// Sender's keys
|
||||
sender: Option<Keys>, |
||||
/// Recipient's public key (if present, the content will be encrypted and recipient verification field will be set)
|
||||
recipient: Option<PublicKey>, |
||||
/// ID of the tunnel that is used
|
||||
tunnel_id: u64, |
||||
} |
||||
|
||||
impl MessageBuilder { |
||||
/// Create a new `MessageBuilder` with default parameters
|
||||
pub fn new() -> Self { |
||||
Self { |
||||
content: MessageContent::None, |
||||
message_type: None, |
||||
sender: None, |
||||
recipient: None, |
||||
tunnel_id: 0, |
||||
} |
||||
} |
||||
|
||||
pub fn content(mut self, cont: Vec<u8>) -> Self { |
||||
self.content = MessageContent::Plain(cont); |
||||
self |
||||
} |
||||
|
||||
/// Sign the message
|
||||
pub fn sign(mut self, keys: &Keys) -> Self { |
||||
self.sender = Some(keys.clone()); |
||||
self |
||||
} |
||||
|
||||
/// Set message's recipient (and therefore set recipient verification and encrypt the content)
|
||||
pub fn recipient(mut self, recipient: PublicKey) -> Self { |
||||
self.recipient = Some(recipient); |
||||
self |
||||
} |
||||
|
||||
/// Set tunnel id
|
||||
pub fn tunnel(mut self, tunnel_id: u64) -> Self { |
||||
self.tunnel_id = tunnel_id; |
||||
self |
||||
} |
||||
|
||||
/// Set message's type
|
||||
pub fn message_type(mut self, message_type: MessageType) -> Self { |
||||
self.message_type = Some(message_type); |
||||
self |
||||
} |
||||
|
||||
/// Get the resulting message
|
||||
pub fn build(self) -> IFResult<Message> { |
||||
let salt = rand::random(); |
||||
let sender_encrypted = if let (Some(sender_keys), Some(recipient)) = (self.sender.as_ref(), self.recipient.as_ref()) { |
||||
Some(recipient.encrypt_data(&sender_keys.get_public().to_vec()?)?) |
||||
} else { None }; |
||||
let hash = Message::calculate_hash( |
||||
&self.content, |
||||
self.message_type.clone().unwrap(), |
||||
sender_encrypted.clone() |
||||
.or_else( |
||||
|| self.sender.as_ref() |
||||
.map(|sender_keys| sender_keys.get_public().to_vec().unwrap()) |
||||
), |
||||
); |
||||
let recipient_verification = self.recipient.as_ref().map(|rec| rec.encrypt_data(&hash).unwrap()); |
||||
let signature = match (self.sender, self.recipient) { |
||||
(Some(sender_keys), Some(recipient_key)) => Signature::SignedPrivately { |
||||
sender_encrypted: sender_encrypted.unwrap(), |
||||
signature: recipient_key.encrypt_data(&sender_keys.sign(&hash)?)?, |
||||
}, |
||||
(Some(sender_keys), None) => Signature::Signed { sender: sender_keys.get_public(), signature: sender_keys.sign(&hash)? }, |
||||
(None, _) => Signature::NotSigned, |
||||
}; |
||||
Ok(Message { |
||||
content: self.content, |
||||
message_type: self.message_type.unwrap(), |
||||
signature, |
||||
salt, |
||||
hash, |
||||
recipient_verification, |
||||
tunnel_id: self.tunnel_id, |
||||
network_info: Default::default(), |
||||
}) |
||||
} |
||||
} |
@ -1,36 +0,0 @@
|
||||
use alloc::vec::Vec; |
||||
use crate::crypto::PublicKey; |
||||
use serde::{Serialize, Deserialize}; |
||||
|
||||
/// A tunnel that is used for communication
|
||||
#[derive(Serialize, Clone, Deserialize)] |
||||
pub struct Tunnel { |
||||
/// Tunnel's id.
|
||||
/// By the way, this id is `None` until the tunnel is validated in the backward movement
|
||||
pub id: Option<u64>, |
||||
/// Ids, each of them is just for local storage on each node until a final global id is created
|
||||
pub local_ids: Vec<u64>, |
||||
/// Ids of peers (in transport) by which we can send a message - one for backward direction, another for forward
|
||||
pub peer_ids: (u64, u64), |
||||
/// Time at which this tunnel should be destroyed (UNIX epoch)
|
||||
pub ttd: u64, |
||||
/// Public keys of nodes in the tunnel
|
||||
pub nodes_in_tunnel: Option<Vec<PublicKey>>, |
||||
/// Is this tunnel used for multicast?
|
||||
pub is_multicast: bool, |
||||
/// If we created this tunnel, then this is the node that it's used to communicate with
|
||||
pub target_node: Option<PublicKey>, |
||||
} |
||||
|
||||
/// Tunnel, but only the fields that are ok to share
|
||||
#[derive(Serialize, Clone, Deserialize)] |
||||
pub struct TunnelPublic { |
||||
/// Tunnel's id
|
||||
id: Option<u64>, |
||||
/// Ids, each of them is just for local storage on each node until a final global id is created
|
||||
local_ids: Vec<u64>, |
||||
/// Time at which this tunnel should be destroyed (UNIX epoch)
|
||||
ttd: u64, |
||||
/// Public keys of nodes in the tunnel
|
||||
nodes_in_tunnel: Option<Vec<PublicKey>>, |
||||
} |
Loading…
Reference in new issue