Compare commits

...

46 Commits

Author SHA1 Message Date
Lev 4e19042f5b Add requirements.txt 3 years ago
Lev a2c5059646 Name input in the python interface 3 years ago
Lev 3cef4b50d2 Improved text field in python interface, added images 3 years ago
Lev 93f83e6b4a add binary 3 years ago
Lev 3f49b7a551 Documentation 3 years ago
Lev d9afc776c0 Profile button 3 years ago
Lev ea5b473763 Made the network a separate crate 3 years ago
Lev f94c9849d6 Using a waker in rust's Degeon stream 3 years ago
Lev 8164fa9874 Integrating rust with python interface 3 years ago
Lev 05785e078c Add handling loop iteration for python 3 years ago
Alexey c712b5a7be The send button in interface 3 years ago
Alexey 82b9109585 Wrote a pygame button 3 years ago
Alexey 49824ef937 Input field in interface 3 years ago
Alexey b501b69f64 Rendering messages in interface 3 years ago
Lev 81d129ec59 Rendering active chat in interface (header) 3 years ago
Alexey 7c76981c1f Design in interface: changed colors and sizes 3 years ago
Alexey a509fb8e45 Split interface code into separate files 3 years ago
Alexey 204310e159 ActiveChat and Degeon.main_loop in interface 3 years ago
Alexey c09a392bc0 Degeon_py: started writing interface 3 years ago
Lev 8e58048c4e Move degeon protocol and worker into degeon_core 3 years ago
Alexey acb5d4f8d5 Profile screen 3 years ago
Lev 84e1b5c879 Documentation and serialization 3 years ago
Lev 0ccd0bbced Fixed the messenger 3 years ago
Lev 226b9536ce Images 3 years ago
Lev 114dd10448 README, images 3 years ago
Lev 2240ae80ae Fix some IP things for devices behind NAT and bad connections 3 years ago
Lev 68d2fb2af3 Wrote a messenger) 3 years ago
Lev c90fc8e0a6 Improved binary 3 years ago
Lev d8c85c96aa Add binary 3 years ago
Lev 3a7a320605 Serialization + main loop starter 3 years ago
Lev ae901e5009 Sending messages is now working 3 years ago
Lev 5089de2af6 The testnet can now create tunnels through IP protocol 3 years ago
Lev 53252f72b8 Sending messages 3 years ago
Lev 0c11f286f7 Tunnel generation (it's even working) 3 years ago
Lev 3e61c27317 Peer sharing in IP 3 years ago
Lev fa49945652 Wrote some logic in TestInterface, fixed some warnings 3 years ago
Lev 362102c7dc Wrote TestInterface 3 years ago
Prokhor 445b285b79 Created IPPackage header. Added receive queue 3 years ago
Alexey 9da108486c Created IPInterface constructor. Made nonblocking mainloop in IP 3 years ago
Lev 41a0842193 Cryptography in message (signing, checks), new serialization for PublicKey 3 years ago
Lev eede8b9c23 Hashing of everything 3 years ago
Lev e0e7a49520 Move test for ip from examples back to the file 3 years ago
Lev 2d6275debc Fix clippy warnings and no_std build 3 years ago
Alexey 57397e20fd Fixed incoming connections in IP interface 3 years ago
Alexey e46f611b4a Removed unfinished line 3 years ago
Alexey bb26f910ff Created IP inteface. Implemented send method 3 years ago
  1. 3419
      Cargo.lock
  2. 22
      Cargo.toml
  3. 74
      README.md
  4. 3238
      degeon/Cargo.lock
  5. 17
      degeon/Cargo.toml
  6. BIN
      degeon/src/CRC35.otf
  7. 152
      degeon/src/chat.rs
  8. 35
      degeon/src/degeon_worker.rs
  9. 2
      degeon/src/gui_events.rs
  10. 18
      degeon/src/main.rs
  11. 2
      degeon/src/message.rs
  12. BIN
      degeon/src/profile_logo.png
  13. 343
      degeon/src/state.rs
  14. 65
      degeon/src/styles.rs
  15. BIN
      degeon_bin
  16. 26
      degeon_core/Cargo.toml
  17. 89
      degeon_core/src/chat.rs
  18. 388
      degeon_core/src/degeon_worker.rs
  19. 54
      degeon_core/src/gui_events.rs
  20. 44
      degeon_core/src/lib.rs
  21. 125
      degeon_core/src/message.rs
  22. BIN
      degeon_py/CRC35.otf
  23. 104
      degeon_py/active_chat.py
  24. 101
      degeon_py/button.py
  25. 76
      degeon_py/chat_selector.py
  26. 32
      degeon_py/config.py
  27. 142
      degeon_py/degeon.py
  28. BIN
      degeon_py/degeon_core.so
  29. 77
      degeon_py/input_field.py
  30. 11
      degeon_py/main.py
  31. 48
      degeon_py/message.py
  32. 1
      degeon_py/requirements.txt
  33. 51
      degeon_py/utils.py
  34. BIN
      images/degeon_logo.png
  35. BIN
      images/dependency_graph.png
  36. 210
      images/dependency_graph.svg
  37. 674
      images/explanation.svg
  38. BIN
      images/logo.png
  39. 96
      images/logo.svg
  40. BIN
      images/scheme.png
  41. 35
      ironforce/Cargo.toml
  42. 57
      ironforce/src/bin/worker.rs
  43. 232
      ironforce/src/crypto.rs
  44. 175
      ironforce/src/interface.rs
  45. 614
      ironforce/src/interfaces/ip.rs
  46. 35
      ironforce/src/interfaces/mod.rs
  47. 635
      ironforce/src/ironforce.rs
  48. 34
      ironforce/src/lib.rs
  49. 398
      ironforce/src/message.rs
  50. 17
      ironforce/src/res.rs
  51. 192
      ironforce/src/transport.rs
  52. 96
      ironforce/src/tunnel.rs
  53. 105
      src/crypto.rs
  54. 78
      src/interface.rs
  55. 6
      src/interfaces/mod.rs
  56. 79
      src/ironforce.rs
  57. 21
      src/lib.rs
  58. 203
      src/message.rs
  59. 36
      src/tunnel.rs

3419
Cargo.lock generated

File diff suppressed because it is too large Load Diff

22
Cargo.toml

@ -1,13 +1,16 @@
[package]
name = "ironforce"
name = "ironforce_degeon"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = []
std = ["rayon"]
[workspace]
members = ["degeon", "degeon_core", "ironforce"]
[lib]
name = "ironforce"
path = "ironforce/src/lib.rs"
[dependencies]
rand_os = "*"
@ -20,6 +23,11 @@ serde = { version = "1.0", features = ["derive", "alloc"], default-features = fa
rayon = { version = "1.5.1", optional = true }
core-error = "0.0.1-rc4"
serde_cbor = "0.11.2"
[profile.dev.package.num-bigint-dig]
opt-level = 3
serde_json = "1.0.72"
spin = "0.9.2"
base64 = "0.13.0"
include_optional = "1.0.1"
iced = { version = "0.3.0", features = ["image"] }
futures = "0.3.18"
iced_native = "0.4.0"
chrono = "0.4.19"

74
README.md

@ -1,3 +1,75 @@
# Ironforce + Degeon
IronForce is a peer-to-peer decentralized network, Degeon is a messenger built on it.
In this repository there is three Cargo crates and one python project:
- `ironforce` is the decentralized network
- `degeon_core` contains messenger's protocol
- `degeon` is the messenger app in Rust + Iced
- `degeon_py` is a version of the messenger app in Python + Pygame
![](images/dependency_graph.png)
# Ironforce
IronForce is a decentralized network
The Ironforce network:
- Has messages encrypted and signed using RSA
- Can build efficient tunnels between two nodes
- Is anonymous: it's very hard to create a connection between a node's IP and its public key
- Can be extended to support any interface, like Bluetooth or even radio
- Can run on ARM microcontrollers
![](images/scheme.png)
### Running IronForce
The `IF` network has a worker daemon that can be used to read the broadcast messages and write them.
Also, you can us `IF` as a Rust library (as it's done in Degeon).
To get started:
1. Clone this repository: `git clone ssh://git@gitlab.ennucore.com:10022/ironforce/ironforce.git`
2. Install Rust and Cargo. The best way to do this is to use RustUp:
```shell
sudo pacman -S rustup # this is for Arch Linux, on other distros use the corresponding package manager
rustup default nightly # this will install rust & cargo (nightly toolchain)
```
3. Run the worker:
```shell
cd ironforce/ironforce
cargo build --features std # This will build ironforce network
# To run the worker, use this:
cargo run --bin worker --features std
```
# Degeon
Degeon is a messenger built on IronForce to show its abilities.
Its core is in the crate `degeon_core`. The crate can be used as a Python module or in Rust.
To build it, install Cargo and Rust, then do this:
```shell
cd ironforce/degeon_core
cargo build
```
You will find the `libdegeon_core.so` file that can be used in Python in the `target/debug` directory.
It should be renamed to `degeon_core.so`.
In Windows Rust should generate a `.dll` file that you will be able to use likewise.
There are two GUI options for this interface:
- A Rust + Iced client (preferred)
- A Pygame client
To run the Rust client, `cd` into the `degeon` folder of this repository, then use cargo:
`cargo run`.
Note: this application requires Vulkan to be installed.
For me on Arch Linux and Intel GPU the installation command was `sudo pacman -S vulkan-intel`.
On most systems it should be pre-installed.

3238
degeon/Cargo.lock generated

File diff suppressed because it is too large Load Diff

17
degeon/Cargo.toml

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

BIN
degeon/src/CRC35.otf

Binary file not shown.

152
degeon/src/chat.rs

@ -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()
}
}

35
degeon/src/degeon_worker.rs

@ -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)
}
}

2
degeon/src/gui_events.rs

@ -0,0 +1,2 @@
// Just reusing events from `degeon_core`
pub use degeon_core::{GuiEvent, AppScreen};

18
degeon/src/main.rs

@ -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)?)
}

2
degeon/src/message.rs

@ -0,0 +1,2 @@
/// Messages here are just from `degeon_core`
pub use degeon_core::{ProtocolMsg, Profile, DegMessage, DegMessageContent};

BIN
degeon/src/profile_logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

343
degeon/src/state.rs

@ -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!(),
}
}
}

65
degeon/src/styles.rs

@ -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
},
}
}
}
}

BIN
degeon_bin

Binary file not shown.

26
degeon_core/Cargo.toml

@ -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"]

89
degeon_core/src/chat.rs

@ -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(),
}
}
}

388
degeon_core/src/degeon_worker.rs

@ -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 &deg_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)))
}
}

54
degeon_core/src/gui_events.rs

@ -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()
}
}

44
degeon_core/src/lib.rs

@ -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(())
}

125
degeon_core/src/message.rs

@ -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,
}
}
}

BIN
degeon_py/CRC35.otf

Binary file not shown.

104
degeon_py/active_chat.py

@ -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()

101
degeon_py/button.py

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

76
degeon_py/chat_selector.py

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

32
degeon_py/config.py

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

142
degeon_py/degeon.py

@ -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()

BIN
degeon_py/degeon_core.so

Binary file not shown.

77
degeon_py/input_field.py

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

11
degeon_py/main.py

@ -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()

48
degeon_py/message.py

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

1
degeon_py/requirements.txt

@ -0,0 +1 @@
pygame

51
degeon_py/utils.py

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

BIN
images/degeon_logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

BIN
images/dependency_graph.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 KiB

210
images/dependency_graph.svg

@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="250mm"
height="240mm"
viewBox="0 0 250 240"
version="1.1"
id="svg9118"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
sodipodi:docname="dependency_graph.svg"
inkscape:export-filename="/home/ennucore/dev/ironforce/images/dependency_graph.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview9120"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.64765011"
inkscape:cx="217.71015"
inkscape:cy="561.25984"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
height="250mm" />
<defs
id="defs9115">
<marker
style="overflow:visible"
id="Arrow1Lend"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Lend"
inkscape:isstock="true">
<path
transform="matrix(-0.8,0,0,-0.8,-10,0)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path13831" />
</marker>
<marker
style="overflow:visible"
id="Arrow1Lstart"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Lstart"
inkscape:isstock="true">
<path
transform="matrix(0.8,0,0,0.8,10,0)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path13828" />
</marker>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient9344"
id="linearGradient9338"
x1="83.937424"
y1="159.45372"
x2="145.70277"
y2="112.71262"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.49339231,0,0,0.49339231,-105.19621,-43.347381)" />
<linearGradient
inkscape:collect="always"
id="linearGradient9344">
<stop
style="stop-color:#ab4251;stop-opacity:1"
offset="0"
id="stop9340" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop9342" />
</linearGradient>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:none;stroke:#282e46;stroke-width:2"
id="rect9201"
width="140.00172"
height="47.340633"
x="61.117081"
y="19.290443"
ry="3.7234547" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:16.0891px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.402227"
x="101.82484"
y="48.617577"
id="text10363"><tspan
sodipodi:role="line"
id="tspan10361"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';stroke-width:0.402227"
x="101.82484"
y="48.617577">IronForce</tspan></text>
<rect
style="fill:none;stroke:#282e46;stroke-width:2"
id="rect12337"
width="140.00172"
height="47.340633"
x="60.334785"
y="91.636803"
ry="3.7234547" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:16.0891px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.402227"
x="78.434021"
y="120.14689"
id="text12341"><tspan
sodipodi:role="line"
id="tspan12339"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';stroke-width:0.402227"
x="78.434021"
y="120.14689">Degeon Core</tspan></text>
<rect
style="fill:none;stroke:#282e46;stroke-width:1.54988"
id="rect12445"
width="108.49297"
height="36.686161"
x="7.193059"
y="174.48308"
ry="2.8854549" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:9.87778px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.311702"
x="21.218885"
y="196.57671"
id="text12449"><tspan
sodipodi:role="line"
id="tspan12447"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:9.87778px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';stroke-width:0.311702"
x="21.218885"
y="196.57671">Rust Interface</tspan></text>
<rect
style="fill:none;stroke:#282e46;stroke-width:1.54988"
id="rect13683"
width="108.49297"
height="36.686161"
x="135.25137"
y="174.48308"
ry="2.8854549" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:9.87778px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.311702"
x="142.9272"
y="196.57671"
id="text13687"><tspan
sodipodi:role="line"
id="tspan13685"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:9.87778px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';stroke-width:0.311702"
x="142.9272"
y="196.57671">Python Interface</tspan></text>
<path
style="fill:none;stroke:#000000;stroke-width:1.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)"
d="M 59.025518,171.39275 128.53989,142.25592"
id="path13824" />
<path
style="fill:none;stroke:#000000;stroke-width:1.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)"
d="M 185.9168,170.63863 128.53989,142.25592"
id="path13826" />
<path
style="fill:none;stroke:#000000;stroke-width:1.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)"
d="M 131.85776,89.509589 V 68.933222"
id="path14217" />
<g
id="g44157"
transform="matrix(0.50052676,0,0,0.50052676,110.21806,30.396512)"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49">
<circle
style="fill:#282e46;fill-opacity:1;stroke-width:0.130543"
id="path3611"
cx="-50.126827"
cy="23.901407"
r="26.844465" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:33.1511px;line-height:1.25;font-family:sans-serif;fill:url(#linearGradient9338);fill-opacity:1;stroke:none;stroke-width:0.207194"
x="-65.295166"
y="35.411083"
id="text4421"><tspan
sodipodi:role="line"
id="tspan4419"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:33.1511px;font-family:'Intro Rust G';-inkscape-font-specification:'Intro Rust G';fill:url(#linearGradient9338);fill-opacity:1;stroke-width:0.207194"
x="-65.295166"
y="35.411083">IF</tspan></text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB

674
images/explanation.svg

@ -0,0 +1,674 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="297mm"
height="210mm"
viewBox="0 0 297 210"
version="1.1"
id="svg5"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
sodipodi:docname="explanation.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.99504919"
inkscape:cx="558.26386"
inkscape:cy="397.46779"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<marker
style="overflow:visible;"
id="Arrow1Mend"
refX="0.0"
refY="0.0"
orient="auto"
inkscape:stockid="Arrow1Mend"
inkscape:isstock="true">
<path
transform="scale(0.4) rotate(180) translate(10,0)"
style="fill-rule:evenodd;fill:context-stroke;stroke:context-stroke;stroke-width:1.0pt;"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
id="path9101" />
</marker>
<marker
style="overflow:visible"
id="Arrow1Lend"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Lend"
inkscape:isstock="true">
<path
transform="matrix(-0.8,0,0,-0.8,-10,0)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path9095" />
</marker>
<marker
style="overflow:visible"
id="Arrow1Lend-9"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Lend"
inkscape:isstock="true">
<path
transform="matrix(-0.8,0,0,-0.8,-10,0)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path9095-1" />
</marker>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient9344"
id="linearGradient9338"
x1="83.937424"
y1="159.45372"
x2="145.70277"
y2="112.71262"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.49339231,0,0,0.49339231,-105.19621,-43.347381)" />
<linearGradient
inkscape:collect="always"
id="linearGradient9344">
<stop
style="stop-color:#ab4251;stop-opacity:1"
offset="0"
id="stop9340" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop9342" />
</linearGradient>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:none;stroke:#1a5fb4;stroke-width:0.450001"
id="rect846"
width="106.5354"
height="93.487419"
x="8.5933933"
y="5.2217455"
ry="8.0541668"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:9.34861px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.247343"
x="17.57098"
y="46.311989"
id="text2360"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan2358"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:9.34861px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';stroke-width:0.247343"
x="17.57098"
y="46.311989">IronForce worker</tspan></text>
<g
id="g44157"
transform="matrix(0.50052676,0,0,0.50052676,86.42112,10.813918)"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49">
<circle
style="fill:#282e46;fill-opacity:1;stroke-width:0.130543"
id="path3611"
cx="-50.126827"
cy="23.901407"
r="26.844465" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:33.1511px;line-height:1.25;font-family:sans-serif;fill:url(#linearGradient9338);fill-opacity:1;stroke:none;stroke-width:0.207194"
x="-65.295166"
y="35.411083"
id="text4421"><tspan
sodipodi:role="line"
id="tspan4419"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:33.1511px;font-family:'Intro Rust G';-inkscape-font-specification:'Intro Rust G';fill:url(#linearGradient9338);fill-opacity:1;stroke-width:0.207194"
x="-65.295166"
y="35.411083">IF</tspan></text>
</g>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:9.89368px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.247343"
x="177.74512"
y="23.484995"
id="text2360-3"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan2358-6"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';stroke-width:0.247343"
x="177.74512"
y="23.484995">Transport</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:9.89368px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.247343"
x="150.64513"
y="113.5675"
id="text2360-3-9"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan2358-6-3"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';stroke-width:0.247343"
x="150.64513"
y="113.5675">Tunnel</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;line-height:1.4;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';white-space:pre;inline-size:71.6192;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="33.883114"
y="151.60315"
id="text4446"
transform="translate(-15.875,-84.666647)"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
x="33.883114"
y="151.60315"
id="tspan6402">keys
</tspan><tspan
x="33.883114"
y="159.01149"
id="tspan6404">messages
</tspan><tspan
x="33.883114"
y="166.41983"
id="tspan6406">transport
</tspan><tspan
x="33.883114"
y="173.82817"
id="tspan6408">tunnels</tspan></text>
<path
style="fill:none;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)"
d="m 51.174695,80.963928 c 36.826182,0 59.037355,-4.807892 71.522105,-13.48959 15.03496,-10.455072 5.67718,-25.115468 43.44786,-25.115468"
id="path8991"
sodipodi:nodetypes="csc"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<rect
style="fill:none;stroke:#1a5fb4;stroke-width:0.264999;stroke-miterlimit:4;stroke-dasharray:none"
id="rect9382"
width="118.39396"
height="73.67466"
x="168.431"
y="9.6066799"
ry="8.0541553"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<rect
style="fill:none;stroke:#1a5fb4;stroke-width:0.264999;stroke-miterlimit:4;stroke-dasharray:none"
id="rect9627"
width="61.851238"
height="32.60709"
x="175.8605"
y="39.549267"
ry="8.0541553"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<rect
style="fill:none;stroke:#1a5fb4;stroke-width:0.264999;stroke-miterlimit:4;stroke-dasharray:1.06, 0.264999;stroke-dashoffset:0"
id="rect9711"
width="37.725643"
height="13.677299"
x="244.65224"
y="39.549267"
ry="3.5077736"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<rect
style="fill:none;stroke:#1a5fb4;stroke-width:0.264999;stroke-miterlimit:4;stroke-dasharray:1.06, 0.264999;stroke-dashoffset:0"
id="rect9735"
width="37.049065"
height="13.496747"
x="245.31032"
y="58.290329"
ry="3.5077665"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:7.05556px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="183.20638"
y="58.762375"
id="text10765"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan10763"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05556px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';stroke-width:0.264583"
x="183.20638"
y="58.762375">IP Interface</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:5.29167px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="250.38287"
y="48.283943"
id="text10765-7"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan10763-5"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';stroke-width:0.264583"
x="250.38287"
y="48.283943">Bluetooth</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:5.29167px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="255.28795"
y="67.200043"
id="text10765-7-3"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan10763-5-5"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';stroke-width:0.264583"
x="255.28795"
y="67.200043">Radio</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.93889px;line-height:1.25;font-family:sans-serif;fill:#646363;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="16.850649"
y="54.463711"
id="text15881"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan15879"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93889px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';fill:#646363;fill-opacity:1;stroke-width:0.264583"
x="16.850649"
y="54.463711">Manages all the high-level logic</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.93889px;line-height:1.25;font-family:sans-serif;fill:#646363;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="177.25208"
y="31.169495"
id="text15881-6"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan15879-2"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93889px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';fill:#646363;fill-opacity:1;stroke-width:0.264583"
x="177.25208"
y="31.169495">Manages communicating with neighbors</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.93889px;line-height:1;font-family:sans-serif;fill:#646363;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="151.44145"
y="121.18497"
id="text15881-6-6"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan15879-2-0"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93889px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';fill:#646363;fill-opacity:1;stroke-width:0.264583"
x="151.44145"
y="122.37146">An efficient path </tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93889px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';fill:#646363;fill-opacity:1;stroke-width:0.264583"
x="151.44145"
y="127.60938"
id="tspan31525">from one node to another</tspan></text>
<path
style="fill:none;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)"
d="m 60.158434,62.055153 c -5.042315,0 -11.192659,3.575902 -24.273172,3.575902"
id="path22086"
sodipodi:nodetypes="cc"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<path
style="fill:none;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend-9)"
d="m 61.630871,68.80989 c -5.042315,0 -6.677553,4.097977 -16.625154,4.097977"
id="path22086-2"
sodipodi:nodetypes="cc"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:3.52778px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="61.826317"
y="63.422222"
id="text23762"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan23760"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';stroke-width:0.264583"
x="61.826317"
y="63.422222">For encrypting messages</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:2.82222px;line-height:1.25;font-family:sans-serif;white-space:pre;inline-size:40.1471;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="62.18861"
y="50.598217"
id="text23762-7"
transform="translate(0,17.991672)"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
x="62.18861"
y="50.598217"
id="tspan6412"><tspan
style="font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC'"
id="tspan6410">Messages are collected
</tspan></tspan><tspan
x="62.18861"
y="54.296866"
id="tspan6416"><tspan
style="font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC'"
id="tspan6414">into a queue
</tspan></tspan><tspan
x="62.18861"
y="57.995517"
id="tspan6420"><tspan
style="font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC'"
id="tspan6418">in another thread</tspan></tspan></text>
<rect
style="fill:none;fill-opacity:1;stroke:#1a5fb4;stroke-width:0.264999;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
id="rect29127"
width="131.27422"
height="80.150284"
x="144.03688"
y="100.24098"
ry="3.5077665"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<circle
style="fill:none;fill-opacity:1;stroke:#1a5fb4;stroke-width:0.264999;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
id="path33335"
cx="175.70769"
cy="154.38387"
r="6.170032"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<circle
style="fill:none;fill-opacity:1;stroke:#1a5fb4;stroke-width:0.264999;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
id="path33335-6"
cx="195.01611"
cy="142.44"
r="6.170032"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<circle
style="fill:none;fill-opacity:1;stroke:#1a5fb4;stroke-width:0.264999;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
id="path33335-2"
cx="195.01611"
cy="168.89836"
r="6.170032"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<circle
style="fill:none;fill-opacity:1;stroke:#1a5fb4;stroke-width:0.264999;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
id="path33335-61"
cx="210.89113"
cy="154.08167"
r="6.170032"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<circle
style="fill:none;fill-opacity:1;stroke:#1a5fb4;stroke-width:0.264999;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
id="path33335-8"
cx="233.21059"
cy="169.51178"
r="6.170032"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<circle
style="fill:none;fill-opacity:1;stroke:#1a5fb4;stroke-width:0.264999;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
id="path33335-7"
cx="233.7605"
cy="140.73239"
r="6.170032"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<circle
style="fill:none;fill-opacity:1;stroke:#1a5fb4;stroke-width:0.264999;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
id="path33335-9"
cx="248.32843"
cy="154.43222"
r="6.170032"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<path
style="fill:#1c71d8;stroke:#1a5fb4;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 180.83782,150.86848 9.12905,-4.79725"
id="path33547"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<path
style="fill:none;stroke:#1a5fb4;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 200.13891,146.14863 5.96908,3.93308"
id="path33812"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<path
style="fill:none;stroke:#1a5fb4;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 216.18252,150.63428 12.1567,-6.80918"
id="path33847"
sodipodi:nodetypes="cc"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<path
style="fill:none;stroke:#1a5fb4;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 238.59271,144.77704 5.65362,5.05602"
id="path34269"
sodipodi:nodetypes="cc"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:9.87778px;line-height:1.25;font-family:sans-serif;fill:#1a5fb4;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="172.59557"
y="157.42624"
id="text35365"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan35363"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:9.87778px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';fill:#1a5fb4;fill-opacity:1;stroke-width:0.264583"
x="172.59557"
y="157.42624">A</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:9.87778px;line-height:1.25;font-family:sans-serif;fill:#1a5fb4;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="245.73502"
y="158.07724"
id="text35365-2"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan35363-0"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:9.87778px;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';fill:#1a5fb4;fill-opacity:1;stroke-width:0.264583"
x="245.73502"
y="158.07724">B</tspan></text>
<path
style="fill:none;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)"
d="m 49.263104,88.1016 c 48.905701,0 81.209076,-4.448346 111.236186,11.120146"
id="path39105"
sodipodi:nodetypes="cc"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<g
id="g39376"
transform="matrix(0.45409146,0,0,0.45409146,31.061511,94.796231)"
inkscape:export-filename="/home/ennucore/dev/ironforce/images/degeon_logo.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49">
<path
d="m 46.303616,184.7066 c 7.73086,-1.52982 13.840883,-6.70904 21.987933,-5.12339 1.985169,0.38629 7.916068,1.97432 5.763948,-2.24155 -1.360488,-2.66462 -6.466152,-2.6588 -8.965936,-3.09563 -2.146829,-0.37544 -2.518039,-0.38946 -2.028031,-2.56169 0.523611,-2.32119 1.455738,-2.86835 3.522398,-3.73592 3.063346,-1.28614 6.074569,-3.00249 8.859308,-4.69635 2.342621,-1.42505 4.705086,-3.10356 6.724915,-5.12339 1.521883,-1.52189 4.344458,-5.17155 6.564312,-4.42966 5.774796,1.92961 11.273107,3.95632 17.024877,5.39036 2.96836,0.74004 6.27671,2.46274 8.75268,2.66832 4.90988,0.40746 7.80865,-9.69804 7.57873,-13.66255 -0.14552,-2.50772 -0.5543,-6.75349 -2.88211,-8.11213 -1.42637,-0.83238 -4.18862,-0.77602 -5.76368,-0.85407 -9.29799,-0.46117 -18.443312,2.18889 -27.752411,1.06759 -4.295511,-0.51752 -7.162271,-2.45242 -10.780448,-4.58973 -3.092979,-1.82721 -6.380163,-2.81066 -9.660202,-4.21613 -3.353329,-1.43722 -7.07681,-2.21589 -10.513748,-3.46922 -2.35241,-0.85778 -2.785533,-1.82695 -4.376208,-3.4155 -2.010569,-2.00819 -3.066521,-4.88104 -5.123656,-6.93817 -2.100527,-2.10053 -3.953669,-4.31747 -6.190721,-6.29762 -4.250267,-3.76211 -9.460442,-7.94728 -15.530777,-5.92402 -2.823369,0.94113 -5.056717,2.1635 -6.884723,4.64318 -1.106487,1.50098 -1.403614,2.72282 -1.707621,4.58972 -0.167746,1.03029 0.01667,1.00965 -0.320146,1.81452 -0.294216,0.70299 -0.989541,1.21258 -1.280847,2.02803 -0.6731,1.88357 -0.305065,4.67042 1.600993,5.12365 -0.294216,-0.0701 1.657086,-0.41883 1.707886,-0.42703 0.841904,-0.136 1.715823,-0.0262 2.56196,-0.10663 4.463521,-0.42519 7.593277,2.07671 8.752681,6.61802 3.120496,12.22481 0.262731,28.58797 12.381706,36.50483 3.822436,2.49714 7.502261,1.60099 11.794861,1.60099 4.696618,0 6.56537,2.36591 6.777831,6.61802 -1.454415,-0.2667 -4.790281,-1.80392 -5.870311,-2.8821 -0.695854,-0.6948 -1.998397,-0.80751 -2.882106,-0.58711 -1.209939,0.30162 -1.621896,3.13822 -2.033323,3.16045 -4.399227,0.23759 -5.496718,0.42386 -4.584435,4.36456 0.225954,0.97552 0.509852,0.54743 0.2667,1.76107 -0.150283,0.75089 -0.382323,1.4224 -0.160073,2.1881 0.354013,1.22026 1.51421,2.07143 2.667794,2.34818"
id="path3"
style="clip-rule:evenodd;overflow:visible;fill:#0e0d0d;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 46.410243,183.42575 c 6.414293,-2.4421 12.911402,-5.93883 19.960166,-5.65732 1.456531,0.0582 6.461125,2.04523 6.617758,0.53393 0.16219,-1.56792 -2.650331,-1.72085 -3.629024,-1.9214 -1.344084,-0.27543 -2.86438,-0.34978 -4.2164,-0.80063 -2.890838,-0.96361 -3.500967,1.32847 -3.671359,-3.27607 -0.02937,-0.7964 -1.086908,-0.48816 -1.29196,-0.0863 -0.365654,0.71676 0.370681,1.84918 0.213519,2.56196 -0.467784,2.1217 -1.212321,1.33006 -2.882107,0.42678 -1.568714,-0.84852 -3.201722,-2.03306 -4.910137,-2.5617 -1.366837,-0.42307 -4.131998,-0.0259 -2.348177,1.60099 0.908315,0.82868 3.823758,0.31274 5.016764,0.53393 2.900098,0.53737 1.289844,1.37584 -0.854075,1.38774 -3.129227,0.0172 -6.254749,-1.54384 -9.446418,-0.48022 -0.645583,0.21511 -0.950383,0.8771 -0.579173,1.48035 0.869156,1.41261 1.078706,0.70141 2.447131,0.49424 1.383506,-0.20955 2.745846,-0.64029 4.162954,-0.64029 1.431132,0 4.189942,-0.89165 5.443538,-0.21379 2.311664,1.25069 -2.830248,1.39489 -3.842544,1.49437 -1.677987,0.16484 -4.191794,0.0437 -5.763948,0.64029 -1.862931,0.70723 -3.028156,3.6285 -0.426508,4.4831"
id="path5"
style="clip-rule:evenodd;overflow:visible;fill:#ff7f00;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 59.432505,170.40349 c 0.716756,-0.85725 2.687902,0.0294 2.722033,-0.10663 0.762265,-3.04932 3.513403,-3.44672 6.137275,-4.69662 3.504671,-1.66925 6.447632,-4.08225 9.766829,-5.97746 2.372255,-1.35467 7.277894,-5.23055 8.165307,-7.57846 -11.99859,5.84597 -36.092606,23.12961 -45.364135,4.26958 -1.60364,-3.26178 -2.006336,-6.68364 -0.854075,-10.14016 0.932921,-2.7985 0.230716,-5.22049 0.640291,-7.89887 0.447146,-2.92391 1.747309,0.35851 1.921405,2.13466 0.311679,3.1832 -1.494103,6.04229 -1.494103,9.28635 0,11.33607 9.555692,16.06206 18.999465,12.91537 3.006989,-1.00198 6.016096,-1.94125 8.859043,-3.36206 1.379273,-0.68924 2.789503,-1.27159 4.162955,-1.97485 2.193395,-1.12316 3.050116,-1.17872 3.41577,-3.73592 -2.993231,0.15319 -6.415616,2.13413 -9.286345,1.9214 -2.579952,-0.19129 -1.026584,-1.16893 0.640291,-1.70762 1.583532,-0.51197 3.278717,-0.83926 4.910138,-1.17422 0.697177,-0.14314 1.821921,-0.14287 2.454804,-0.42677 0.248179,-0.11139 1.138237,-0.0587 1.281112,-0.21352 0.19685,-0.21325 0.490538,-3.43588 0.320146,-3.62929 -0.642408,-0.72893 -3.509433,0.27887 -4.269581,0.42704 -1.430073,0.27913 -2.735527,0.53366 -4.216665,0.53366 -2.524124,0 0.780521,-1.43219 1.548078,-1.60126 1.291166,-0.28416 2.633133,-0.51858 3.949435,-0.74691 3.001698,-0.52097 3.068902,-0.25877 3.415506,-3.84281 -2.24155,0.1778 -4.483894,0.34554 -6.724385,0.53393 -0.747448,0.0627 -4.358481,0.88423 -3.735917,-0.42704 0.361156,-0.76041 3.059377,-0.86598 3.842279,-0.9607 1.348052,-0.16299 2.711715,-0.26088 4.056327,-0.42704 1.895211,-0.23416 3.9243,-1.28931 1.227667,-2.1881 -2.199481,-0.73316 -6.928908,0.50958 -8.592608,-1.12078 -1.385888,-1.35837 2.013479,-0.74692 2.828396,-0.74692 1.442772,0 2.826279,0.42677 4.269581,0.42677 0.5334,0 1.099343,0.10055 1.654439,0.1069 -1.236662,-2.15371 -6.133835,-3.65522 -8.43227,-4.80298 -2.816755,-1.40679 -6.320632,-2.2741 -9.392973,-3.04218 -2.693988,-0.67337 -6.529652,-3.30994 -8.752681,-3.14881 0.408516,-0.0296 -0.935302,0.84085 -0.960438,0.85408 0.184679,-0.0971 -2.752196,0.57652 -1.90156,0.62388 -2.321719,-0.12911 -0.695854,0.29501 -1.941248,1.19063 -1.255183,0.90276 -1.100402,-0.84535 -1.821127,1.67772 -0.260879,0.91281 -1.528498,-0.27384 -2.021417,1.20438 -0.391054,1.17264 -0.372004,0.59849 -0.960702,1.06733 -0.07832,0.0624 -1.267089,-0.46328 -1.387739,-0.53366 0.02884,1.34091 -0.806715,1.61634 -1.707886,0.85381 -0.580231,1.82377 -1.115748,3.28559 -2.668587,1.38774 0.09366,1.71027 -0.849577,1.76477 -1.707621,0.74718 -0.15875,1.11099 0.03387,2.8784 -1.38774,2.13466 0.686065,8.9199 1.653646,17.75037 7.471834,24.44353 2.697956,3.10356 6.583362,4.16904 10.727266,3.57584 1.941513,-0.27807 4.10845,-0.10689 6.030912,0.21326 3.04509,0.50641 2.727325,1.90632 4.163219,3.68194"
id="path7"
style="clip-rule:evenodd;overflow:visible;fill:#e6e6e8;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 112.26847,159.51589 c 1.35044,-0.50509 2.53392,-3.1295 3.41577,-4.37595 -2.35823,-1.75313 -5.06597,4.04707 -3.41577,4.37595"
id="path9"
style="clip-rule:evenodd;overflow:visible;fill:#666666;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 109.49326,158.76897 c 1.13929,-1.37345 2.02353,-2.79241 2.77521,-4.37595 -3.96319,-1.42345 -7.9756,-2.27779 -12.06156,-3.20251 -4.308737,-0.97499 -9.875041,-4.13068 -13.128886,0.64055 4.034366,1.21074 8.150489,2.23071 12.06156,3.73566 3.286386,1.2647 7.291656,1.73064 10.353676,3.20225"
id="path11"
style="clip-rule:evenodd;overflow:visible;fill:#e6e6e8;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 116.11075,153.75247 c 1.13083,-2.00978 2.55535,-5.59012 -0.64029,-5.76395 -0.36248,1.8124 -2.37993,5.91582 0.64029,5.76395"
id="path13"
style="clip-rule:evenodd;overflow:visible;fill:#666666;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 80.99393,153.11191 c 0.173302,-0.14287 0.424392,-0.23336 0.533664,-0.32014 -0.444764,0.004 -0.529695,0.3167 -0.533664,0.32014"
id="path15"
style="clip-rule:evenodd;overflow:visible;fill:#666666;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 112.90876,152.89839 c 0.40932,-1.143 0.70882,-2.32674 0.90753,-3.5224 0.30824,-1.85472 -0.0579,-1.49092 -1.65444,-1.81477 -2.23785,-0.45403 -4.97338,-1.06733 -7.25832,-1.06733 -4.02299,0 -8.6061,-1.26445 -11.84804,1.81451 6.662208,1.0795 13.37919,2.72971 19.85327,4.58999"
id="path17"
style="clip-rule:evenodd;overflow:visible;fill:#e6e6e8;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 81.207184,151.08388 c 3.694642,-0.51303 3.814498,-0.0971 4.056063,-3.84228 -3.395663,0.18812 -4.60719,0.0439 -4.056063,3.84228"
id="path19"
style="clip-rule:evenodd;overflow:visible;fill:#666666;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 88.145353,149.37626 c 1.352021,-0.81836 2.704041,-1.63698 4.056062,-2.45507 -2.995348,0.0831 -3.774016,-0.65008 -4.056062,2.45507"
id="path21"
style="clip-rule:evenodd;overflow:visible;fill:#e6e6e8;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 117.71201,147.13444 c 0.0397,-0.45587 0.46355,-2.11798 0.21378,-2.45506 -0.17224,-0.23204 -1.92643,-0.56119 -2.13492,-0.42704 -2.04496,1.31577 1.06627,2.59715 1.92114,2.8821"
id="path23"
style="clip-rule:evenodd;overflow:visible;fill:#666666;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 114.18961,146.17374 c 0.19103,-2.52836 -1.7018,-2.56169 -3.89572,-2.56169 -1.49596,0 -3.02631,-0.13944 -4.53655,-0.21352 -2.64424,-0.12991 -5.29801,0.0746 -7.898869,0 -0.0254,0.49345 -0.08149,1.04166 -0.106362,1.38774 5.518151,0.17039 10.945551,1.08744 16.437501,1.38747"
id="path25"
style="clip-rule:evenodd;overflow:visible;fill:#e6e6e8;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 81.420967,145.96049 c 4.021667,-0.43498 3.918744,-0.1569 4.375944,-4.16296 -3.052498,0.37227 -5.899414,0.35005 -4.375944,4.16296"
id="path27"
style="clip-rule:evenodd;overflow:visible;fill:#666666;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 93.909301,145.64008 c 1.84732,-1.34356 2.88872,-1.38906 1.17422,-2.5617 -0.707495,-0.48392 -1.906587,-0.51699 -2.775214,-0.74692 -0.893498,-0.23653 -1.836738,-0.39846 -2.722033,-0.694 -1.99443,-0.66569 -1.252803,2.22171 -1.33403,3.68247 1.73408,0 4.386263,-0.43418 5.657057,0.32015"
id="path29"
style="clip-rule:evenodd;overflow:visible;fill:#e6e6e8;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 116.8582,143.18501 c 0.41328,-0.91969 -0.28575,-2.72124 -1.70789,-2.66859 0.13441,1.41102 -0.13811,2.68843 1.70789,2.66859"
id="path31"
style="clip-rule:evenodd;overflow:visible;fill:#666666;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 97.324807,142.75797 c 1.586441,-1.3081 5.643563,-0.58658 7.791983,-0.53366 2.95962,0.073 5.75336,0.27755 8.64605,0.53366 -0.28363,-3.11758 -1.05145,-2.24155 -3.68247,-2.24155 -1.51342,0 -3.03133,-0.003 -4.53655,0.10663 -3.12605,0.2286 -6.198655,0.76994 -9.286607,1.38774 0.347133,0.24289 0.687917,0.5633 1.067594,0.74718"
id="path33"
style="clip-rule:evenodd;overflow:visible;fill:#e6e6e8;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 81.954632,140.62331 c 0.213519,0 0.426773,0 0.640292,0 -0.27358,-0.0413 -0.652727,-0.17198 -0.960438,-0.21352 -0.01085,0.0344 -0.108214,0.21379 -0.106892,0.21352 0.06932,0.0458 0.570442,0.096 0.427038,0"
id="path35"
style="clip-rule:evenodd;overflow:visible;fill:#666666;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 30.933176,136.56725 c 0.102923,-0.40243 0.213519,-0.98345 0.213519,-1.54781 0,-1.44066 0.8509,-0.51514 1.281113,-0.69374 0.992981,-0.41195 2.058722,-0.72152 2.668322,0.42677 0.306917,-1.2274 0.593196,-3.55414 2.028296,-1.92114 -0.02831,-2.06189 1.527175,-1.57268 2.348177,-0.53392 0.439209,-1.53644 0.631032,-3.02472 2.24155,-1.601 0.384175,-1.04695 -0.286543,-3.74226 1.707886,-2.45507 -0.364861,-2.01162 1.213643,-1.61713 2.134658,-0.85407 -0.3556,-3.08293 1.157552,-0.78264 1.814512,-1.06707 0.355336,-0.15398 0.576528,0.87366 0.668073,-0.22542 0.07646,-0.91546 -0.04683,-0.9914 0.719402,-1.05569 -2.419879,-3.37158 -4.561152,-6.91118 -7.471568,-9.82001 -2.223823,-2.2225 -4.935008,-3.869 -7.151423,-6.08409 -4.58761,-4.58417 -14.081919,-3.53986 -17.078589,2.45533 -0.585788,1.17158 -1.036373,2.92894 -0.533665,4.16269 0.456142,1.11892 1.31445,1.31498 2.24155,2.24155 0.664633,0.66437 0.599281,2.58392 1.334029,2.82866 1.016529,0.33867 2.193131,0.51911 3.095361,1.12078 1.838854,1.22607 3.398572,2.06507 4.750064,3.94943 2.371725,3.3065 2.134923,7.03368 2.988733,10.67382"
id="path37"
style="clip-rule:evenodd;overflow:visible;fill:#666666;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 16.523439,121.94426 c 3.790156,0 1.388269,-2.26589 0,0"
id="path39"
style="clip-rule:evenodd;overflow:visible;fill:#ffbf00;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 14.495408,121.51696 c 1.056216,-0.84482 3.352535,-1.89363 2.881841,-3.09536 -0.04445,-0.11351 -1.462352,-0.75565 -1.600993,-0.74719 -1.425575,0.0892 -1.843352,2.51672 -1.280848,3.84255"
id="path41"
style="clip-rule:evenodd;overflow:visible;fill:#ffbf00;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 19.298654,117.03412 c 2.201597,0.14341 2.681552,-3.39725 0.960702,-4.37647 -2.285207,-1.30016 -3.538538,3.77984 -0.960702,4.37647"
id="path43"
style="clip-rule:evenodd;overflow:visible;fill:#0e0d0d;fill-rule:evenodd;stroke-width:0.264583" />
<path
d="m 19.192027,115.32624 c 0.08864,-0.4408 0.509852,-0.67575 -0.106892,-0.85408 -0.01032,0.0617 0.08334,1.00965 0.106892,0.85408"
id="path45"
style="clip-rule:evenodd;overflow:visible;fill:#ffffff;fill-rule:evenodd;stroke-width:0.264583" />
</g>
<circle
style="fill:none;fill-opacity:1;stroke:#c01c28;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path39400"
cx="58.1259"
cy="167.87944"
r="39.348465"
inkscape:export-filename="/home/ennucore/dev/ironforce/images/degeon_logo.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="41.161209"
y="187.81125"
id="text41324"
inkscape:export-filename="/home/ennucore/dev/ironforce/images/degeon_logo.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49"><tspan
sodipodi:role="line"
id="tspan41322"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Inconsolata LGC';-inkscape-font-specification:'Inconsolata LGC';stroke-width:0.264583"
x="41.161209"
y="187.81125">Degeon</tspan></text>
<path
style="fill:none;stroke:#c01c28;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Mend)"
d="M 58.794197,125.3495 V 102.52049"
id="path42639"
sodipodi:nodetypes="cc"
inkscape:export-filename="/home/ennucore/dev/ironforce/scheme.png"
inkscape:export-xdpi="1121.49"
inkscape:export-ydpi="1121.49" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 40 KiB

BIN
images/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

96
images/logo.svg

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg5"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
sodipodi:docname="logo.svg"
inkscape:export-filename="/home/ennucore/dev/ironforce/logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.2953002"
inkscape:cx="336.21549"
inkscape:cy="568.59405"
inkscape:window-width="3840"
inkscape:window-height="2088"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient9344">
<stop
style="stop-color:#ab4251;stop-opacity:1"
offset="0"
id="stop9340" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop9342" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient9344"
id="linearGradient9338"
x1="83.937424"
y1="159.45372"
x2="145.70277"
y2="112.71262"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="54.465954"
y="101.28693"
id="text1970"><tspan
sodipodi:role="line"
id="tspan1968"
style="stroke-width:0.264583"
x="54.465954"
y="101.28693" /></text>
<circle
style="fill:#282e46;stroke-width:0.264583;fill-opacity:1"
id="path3611"
cx="111.61379"
cy="136.29881"
r="54.407951" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:67.1901px;line-height:1.25;font-family:sans-serif;fill:url(#linearGradient9338);fill-opacity:1;stroke:none;stroke-width:0.419938"
x="80.870842"
y="159.62645"
id="text4421"><tspan
sodipodi:role="line"
id="tspan4419"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:67.1901px;font-family:'Intro Rust G';-inkscape-font-specification:'Intro Rust G';fill:url(#linearGradient9338);fill-opacity:1;stroke-width:0.419938"
x="80.870842"
y="159.62645">IF</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
images/scheme.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

35
ironforce/Cargo.toml

@ -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"]

57
ironforce/src/bin/worker.rs

@ -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()?)?;
}
}

232
ironforce/src/crypto.rs

@ -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()
)
}

175
ironforce/src/interface.rs

@ -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"
);
}
}

614
ironforce/src/interfaces/ip.rs

@ -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()
})
}

35
ironforce/src/interfaces/mod.rs

@ -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())?)])
}
}

635
ironforce/src/ironforce.rs

@ -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(())
}
}

34
ironforce/src/lib.rs

@ -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 {
}

398
ironforce/src/message.rs

@ -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(())
}

17
src/res.rs → ironforce/src/res.rs

@ -9,10 +9,14 @@ pub enum IFError {
General(String),
/// A tunnel satisfying some conditions has not been found
TunnelNotFound,
/// Could not establish a connection
CouldNotConnect,
/// Error during serialization
SerializationError(String),
/// Error in rsa
CryptoError(String),
/// Error in std::io
IoError(String),
}
@ -23,12 +27,25 @@ pub enum IFError {
// }
// }
#[cfg(feature = "std")]
impl From<std::io::Error> for IFError {
fn from(e: std::io::Error) -> Self {
Self::IoError(format!("{:?}", e))
}
}
impl From<serde_cbor::Error> for IFError {
fn from(e: serde_cbor::Error) -> Self {
Self::SerializationError(format!("{:?}", e))
}
}
impl From<serde_json::Error> for IFError {
fn from(e: serde_json::Error) -> Self {
Self::SerializationError(format!("{:?}", e))
}
}
impl From<rsa::errors::Error> for IFError {
fn from(e: rsa::errors::Error) -> Self {
Self::CryptoError(format!("{:?}", e))

192
src/transport.rs → ironforce/src/transport.rs

@ -1,17 +1,19 @@
use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use alloc::vec;
use crate::interface::{Interface, TargetingData};
use crate::message::MessageBytes;
use crate::res::IFResult;
use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
#[cfg(std)]
#[cfg(feature = "std")]
use rayon::prelude::*;
#[cfg(feature = "std")]
use std::println;
/// An identification of a peer - something that we can use to send a message to id
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Hash)]
pub struct PeerInfo {
/// Something to locally identify this peer
pub peer_id: u64,
@ -31,31 +33,47 @@ impl PeerInfo {
}
}
/// The struct that manages all the communication with peers
pub struct Transport {
pub interfaces: Vec<Box<dyn Interface>>,
peers: Vec<PeerInfo>,
pub(crate) peers: Vec<PeerInfo>,
}
impl Transport {
/// Create new transport with given interfaces
pub fn new(interfaces: Vec<Box<dyn Interface>>) -> Self {
#[cfg(not(std))]
if interfaces.iter().map(|interface| interface.has_blocking_main() as u8).sum::<u8>() > 1 {
#[cfg(not(feature = "std"))]
if interfaces
.iter()
.map(|interface| interface.has_blocking_main() as u8)
.sum::<u8>()
> 1
{
panic!("There is two interfaces with blocking main loops and we have no threads because this is no_std!");
}
Self { interfaces, peers: vec![] }
Self {
interfaces,
peers: vec![],
}
}
/// Find a peer in `self.peers` by its id
fn get_peer_by_id(&self, peer_id: u64) -> Option<PeerInfo> {
self.peers.iter().find(|peer| peer.peer_id == peer_id).cloned()
self.peers
.iter()
.find(|peer| peer.peer_id == peer_id)
.cloned()
}
/// Try to find a peer in `self.peers` by interface_id and targeting data
fn get_peer_by_parameters(&self, interface_id: &str, data: &str /*&TargetingData*/) -> Option<&PeerInfo> {
self.peers.iter().find(|peer| peer.interface_id == interface_id && peer.interface_targeting_data == data)
fn get_peer_by_parameters(
&self,
interface_id: &str,
data: &str, /*&TargetingData*/
) -> Option<&PeerInfo> {
self.peers
.iter()
.find(|peer| peer.interface_id == interface_id && peer.interface_targeting_data == data)
}
/// Insert a new peer into out database and return its id or find an existing one with these parameters
@ -67,32 +85,42 @@ impl Transport {
self.peers.push(new_peer);
peer_id
}
Some(peer) => peer.peer_id
Some(peer) => peer.peer_id,
}
}
/// Get interface index by its ID
fn interface_index_by_id(&self, interface_id: &str) -> usize {
self.interfaces.iter().position(|interface| interface.id() == interface_id).unwrap_or_else(|| panic!("Invalid interface id"))
self.interfaces
.iter()
.position(|interface| interface.id() == interface_id)
.unwrap_or_else(|| panic!("Invalid interface id"))
}
/// Send message bytes to a peer if data is provided or broadcast the data if `peer == None`
pub fn send_message(&mut self, message: MessageBytes, peer_id: Option<u64>) -> IFResult<()> {
let peer = if let Some(peer_id) = peer_id { self.get_peer_by_id(peer_id) } else { None };
let peer = if let Some(peer_id) = peer_id {
self.get_peer_by_id(peer_id)
} else {
None
};
match peer {
// Broadcast
None => {
#[cfg(not(std))]
{
for interface in &mut self.interfaces {
interface.send(&message, None)?;
}
#[cfg(not(feature = "std"))]
{
for interface in &mut self.interfaces {
interface.send(&message, None)?;
}
}
// If we have concurrency, we will do it concurrently
#[cfg(std)]
{
self.interfaces.par_iter_mut().map(|interface| interface.send(&message, None)).for_each(drop);
}
#[cfg(feature = "std")]
{
self.interfaces
.par_iter_mut()
.map(|interface| interface.send(&message, None))
.for_each(drop);
}
Ok(())
}
// Singlecast
@ -107,81 +135,137 @@ impl Transport {
///
/// Returns a result with an option of `(message, peer_id)`
pub fn receive(&mut self) -> Option<(MessageBytes, u64 /* peer id*/)> {
if let Some((interface_id, (msg, peer_data))) = self.interfaces
if let Some((interface_id, (msg, peer_data))) = self
.interfaces
.iter_mut()
// For each interface return (interface id, message result)
.map(|interface| (interface.id().to_string(), interface.receive()))
// If there was an error, print it
.map(|res| match res {
(id, Err(e)) => {
#[cfg(std)]
#[cfg(feature = "std")]
println!("An error occurred while receiving: {:?}", e);
(id, Err(e))
}
(id, Ok(r)) => (id, Ok(r))
(id, Ok(r)) => (id, Ok(r)),
})
// Find a result where there is a message
.find(|r| matches!(r, (_, Ok(Some(_)))))
// Safely unwrap this result (we already matched `Ok(Some(_))`)
.map(|(id, r)| (id, r.unwrap().unwrap())) {
.map(|(id, r)| (id, r.unwrap().unwrap()))
{
Some((msg, self.find_or_add_peer(interface_id, peer_data)))
} else { None }
} else {
None
}
}
/// Run one iteration of the main loop
pub fn main_loop_iteration(&mut self) -> IFResult<()> {
#[cfg(std)]
self.interfaces.par_iter_mut().map(|interface| interface.main_loop_iteration()).collect::<IFResult<_>>()?;
#[cfg(not(std))]
{
self.interfaces.iter_mut().try_for_each(|interface| if !interface.has_blocking_main() { interface.main_loop_iteration() } else { Ok(()) })?;
let blocking_interface_index = self.interfaces.iter().position(|interface| interface.has_blocking_main());
if let Some(ind) = blocking_interface_index {
self.interfaces[ind].main_loop_iteration()?;
#[cfg(feature = "std")]
self.interfaces
.par_iter_mut()
.map(|interface| interface.main_loop_iteration())
.collect::<IFResult<_>>()?;
#[cfg(not(feature = "std"))]
{
self.interfaces.iter_mut().try_for_each(|interface| {
if !interface.has_blocking_main() {
interface.main_loop_iteration()
} else {
Ok(())
}
})?;
let blocking_interface_index = self
.interfaces
.iter()
.position(|interface| interface.has_blocking_main());
if let Some(ind) = blocking_interface_index {
self.interfaces[ind].main_loop_iteration()?;
}
}
Ok(())
}
pub fn get_interfaces_data(&self) -> Vec<String> {
self.interfaces.iter().map(|interface| interface.get_dump_data()).collect()
}
pub fn restore(peers: Vec<PeerInfo>, interfaces_data: Vec<String>) -> IFResult<Self> {
Ok(Transport { interfaces: crate::interfaces::restore_interfaces(interfaces_data)?, peers })
}
}
impl core::hash::Hash for Transport {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(&self.get_interfaces_data(), state);
core::hash::Hash::hash(&self.peers, state);
}
}
#[cfg(test)]
use crate::interface::test_interface::TestInterface;
use crate::interface::test_interface::SimpleTestInterface;
#[test]
fn test_adding_peer_to_transport() {
let mut transport = Transport::new(vec![Box::new(TestInterface::default())]);
let mut transport = Transport::new(vec![Box::new(SimpleTestInterface::default())]);
let (interface_id, interface_targeting_data) = ("test_interface".to_string(), "hi".to_string());
assert!(transport.get_peer_by_parameters(interface_id.as_str(), interface_targeting_data.as_str()).is_none());
let peer_id = transport.find_or_add_peer(interface_id.clone(), interface_targeting_data.clone());
assert!(transport
.get_peer_by_parameters(interface_id.as_str(), interface_targeting_data.as_str())
.is_none());
let peer_id =
transport.find_or_add_peer(interface_id.clone(), interface_targeting_data.clone());
let peer = PeerInfo {
peer_id,
interface_id: interface_id.clone(),
interface_targeting_data: interface_targeting_data.clone(),
};
assert_eq!(transport.get_peer_by_parameters(interface_id.as_str(), interface_targeting_data.as_str()), Some(&peer));
assert_eq!(
transport.get_peer_by_parameters(interface_id.as_str(), interface_targeting_data.as_str()),
Some(&peer)
);
assert_eq!(transport.get_peer_by_id(peer_id), Some(peer));
}
#[test]
fn test_transport_sending() {
let mut transport = Transport::new(vec![Box::new(TestInterface::default())]);
let mut transport = Transport::new(vec![Box::new(SimpleTestInterface::default())]);
let (interface_id, interface_targeting_data) = ("test_interface".to_string(), "hi".to_string());
let peer_id = transport.find_or_add_peer(interface_id.clone(), interface_targeting_data.clone());
let peer_id = transport.find_or_add_peer(interface_id, interface_targeting_data.clone());
transport.send_message(vec![239, 123], None).unwrap();
assert_eq!(transport.interfaces[0].receive().unwrap(), Some((vec![239u8, 123], "".to_string())));
assert_eq!(
transport.interfaces[0].receive().unwrap(),
Some((vec![239u8, 123], "".to_string()))
);
assert!(transport.interfaces[0].receive() == IFResult::Ok(None));
transport.send_message(vec![239, 123], Some(peer_id)).unwrap();
assert_eq!(transport.interfaces[0].receive(), IFResult::Ok(Some((vec![239, 123], interface_targeting_data))));
transport
.send_message(vec![239, 123], Some(peer_id))
.unwrap();
assert_eq!(
transport.interfaces[0].receive(),
IFResult::Ok(Some((vec![239, 123], interface_targeting_data)))
);
}
#[test]
fn test_transport_receiving() {
let mut transport = Transport::new(vec![Box::new(TestInterface::default())]);
let mut transport = Transport::new(vec![Box::new(SimpleTestInterface::default())]);
let (interface_id, interface_targeting_data) = ("test_interface".to_string(), "hi".to_string());
let peer_id = transport.find_or_add_peer(interface_id.clone(), interface_targeting_data.clone());
let peer_id = transport.find_or_add_peer(interface_id.clone(), interface_targeting_data);
transport.send_message(vec![239, 123], None).unwrap();
assert_eq!(transport.receive(), Some((vec![239u8, 123], transport.get_peer_by_parameters(interface_id.as_str(), "").unwrap().peer_id)));
assert_eq!(
transport.receive(),
Some((
vec![239u8, 123],
transport
.get_peer_by_parameters(interface_id.as_str(), "")
.unwrap()
.peer_id
))
);
assert!(transport.receive().is_none());
transport.send_message(vec![239, 123], Some(peer_id)).unwrap();
transport
.send_message(vec![239, 123], Some(peer_id))
.unwrap();
assert_eq!(transport.receive(), Some((vec![239, 123], peer_id)));
}

96
ironforce/src/tunnel.rs

@ -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());
}

105
src/crypto.rs

@ -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()));
}

78
src/interface.rs

@ -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()
)
}
}
}

6
src/interfaces/mod.rs

@ -1,6 +0,0 @@
use crate::interface::Interface;
use alloc::vec;
pub fn get_interfaces() -> alloc::vec::Vec<alloc::boxed::Box<dyn Interface>> {
vec![]
}

79
src/ironforce.rs

@ -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()
}
}

21
src/lib.rs

@ -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 {
}

203
src/message.rs

@ -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(),
})
}
}

36
src/tunnel.rs

@ -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…
Cancel
Save