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 |
||||||
|
|
||||||
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