428 lines
17 KiB
Rust
428 lines
17 KiB
Rust
#![feature(string_drain_as_str)]
|
|
#![feature(unsigned_abs)]
|
|
#![feature(array_methods)]
|
|
#![feature(fixed_size_array)]
|
|
|
|
mod util;
|
|
|
|
use crate::util::event::{Event, Events};
|
|
use crate::util::StatefulList;
|
|
use crate::Mode::{Browsing, NewPost};
|
|
use chrono::{NaiveDateTime, Utc};
|
|
use clipboard::{ClipboardContext, ClipboardProvider};
|
|
use crossbeam_queue::SegQueue;
|
|
use ed25519_dalek::{Signature, Verifier};
|
|
use integer_encoding::FixedInt;
|
|
use rand::distributions::Alphanumeric;
|
|
use rand::{thread_rng, Rng};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fs;
|
|
use std::fs::File;
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::thread::{sleep, spawn};
|
|
use std::time::Duration;
|
|
use std::{error::Error, io};
|
|
use tapir_cwtch::acns::tor::authentication::HashedPassword;
|
|
use tapir_cwtch::acns::tor::run::TorRunner;
|
|
use tapir_cwtch::acns::tor::torrc::TorrcGenerator;
|
|
use tapir_cwtch::acns::tor::validation::{hostname_to_public_key, public_key_to_hostname, validate_hostname};
|
|
use tapir_cwtch::acns::tor::TorProcess;
|
|
use tapir_cwtch::applications::authentication_app::AuthenicationApp;
|
|
use tapir_cwtch::connections::service::Service;
|
|
use tapir_cwtch::connections::{Connection, ConnectionInterface, InboundConnection, OutboundConnection};
|
|
use tapir_cwtch::primitives::identity::Identity;
|
|
use tapir_cwtch::primitives::transcript::Transcript;
|
|
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
|
use tui::layout::Corner;
|
|
use tui::style::{Color, Modifier, Style};
|
|
use tui::text::{Span, Spans};
|
|
use tui::widgets::{List, ListItem, Paragraph};
|
|
use tui::{
|
|
backend::TermionBackend,
|
|
layout::{Constraint, Direction, Layout},
|
|
widgets::{Block, Borders},
|
|
Terminal,
|
|
};
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub struct Orb {
|
|
message: String,
|
|
author: String,
|
|
timestamp: u64,
|
|
signature: ed25519_dalek::Signature,
|
|
rebroadcast_from: Option<String>,
|
|
rebroadcast_time: Option<u64>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
pub struct Profile {
|
|
identity: Arc<Identity>,
|
|
follows: Vec<String>,
|
|
orbs: Arc<Mutex<Vec<Orb>>>,
|
|
cache: Vec<Orb>,
|
|
}
|
|
|
|
impl Profile {
|
|
pub fn gen_cache(&mut self) {
|
|
self.cache.sort_by(|x, y| match (x.rebroadcast_time, y.rebroadcast_time) {
|
|
(Some(x), Some(y)) => y.cmp(&x),
|
|
(Some(x), None) => y.timestamp.cmp(&x),
|
|
(None, Some(y)) => y.cmp(&x.timestamp),
|
|
(None, None) => y.timestamp.cmp(&x.timestamp),
|
|
});
|
|
self.cache.dedup_by_key(|x| (x.signature.clone(), x.rebroadcast_from.clone()));
|
|
}
|
|
|
|
pub fn save(&self) -> std::io::Result<()> {
|
|
let j = serde_json::to_string(&self);
|
|
let mut file = match File::create(&"orb.profile") {
|
|
Err(why) => panic!("couldn't create : {}", why),
|
|
Ok(file) => file,
|
|
};
|
|
file.write_all(j.unwrap().as_bytes())
|
|
}
|
|
}
|
|
|
|
enum Mode {
|
|
Browsing,
|
|
NewPost,
|
|
}
|
|
|
|
static Q: SegQueue<Orb> = SegQueue::new();
|
|
|
|
struct App {
|
|
input: String,
|
|
mode: Mode,
|
|
orbs: StatefulList<Orb>,
|
|
status: String,
|
|
}
|
|
|
|
fn main() -> Result<(), Box<dyn Error>> {
|
|
if Path::new("orb.profile").exists() == false {
|
|
let profile = Profile {
|
|
identity: Arc::new(Identity::initialize_ephemeral_identity()),
|
|
follows: vec![],
|
|
orbs: Arc::new(Mutex::new(vec![])),
|
|
cache: vec![],
|
|
};
|
|
match profile.save() {
|
|
Err(e) => panic!("Could not generate orb.profile file {}", e),
|
|
_ => {}
|
|
}
|
|
}
|
|
let mut profile: Profile = match fs::read_to_string("orb.profile") {
|
|
Ok(json) => serde_json::from_str(json.as_str()).unwrap(),
|
|
Err(why) => {
|
|
panic!("couldn't read orb.profile : {}", why);
|
|
}
|
|
};
|
|
|
|
for orb in profile.orbs.lock().unwrap().iter() {
|
|
profile.cache.push(orb.clone());
|
|
}
|
|
profile.gen_cache();
|
|
|
|
let mut app = App {
|
|
input: "".to_string(),
|
|
mode: Browsing,
|
|
orbs: StatefulList::with_items(profile.cache.clone()),
|
|
status: "".to_string(),
|
|
};
|
|
|
|
let rng = rand::thread_rng();
|
|
let password = rng.sample_iter(&Alphanumeric).take(10).collect::<String>();
|
|
|
|
let tor_path = which::which("tor");
|
|
let mut rng = thread_rng();
|
|
let socks_port = rng.gen_range(10052, 11100);
|
|
let control_port = rng.gen_range(9052, 9100);
|
|
let mut tor_runner = TorRunner::run(
|
|
TorrcGenerator::new()
|
|
.with_socks_port(socks_port)
|
|
.with_control_port(control_port)
|
|
.with_hashed_control_password(password.as_str()),
|
|
"./orb.torrc",
|
|
tor_path.unwrap().to_str().unwrap(),
|
|
"./orb_data_dir/",
|
|
)
|
|
.unwrap();
|
|
|
|
tor_runner.wait_until_bootstrapped();
|
|
|
|
let mut auth_control_port = TorProcess::connect(control_port)
|
|
.unwrap()
|
|
.authenticate(Box::new(HashedPassword::new(password)))
|
|
.unwrap();
|
|
|
|
let mut rng = rand::thread_rng();
|
|
let port = rng.gen_range(10000, 65535);
|
|
|
|
match profile.identity.host_onion_service(&mut auth_control_port, 9878, port) {
|
|
Ok(_service_id) => {
|
|
let service = Service::init(profile.identity.clone(), socks_port);
|
|
|
|
let auth_identity = profile.identity.clone();
|
|
|
|
let orbs = profile.orbs.clone();
|
|
let inbound_service = move |conn: Connection<InboundConnection>| {
|
|
let mut transcript = Transcript::new_transcript("tapir-transcript");
|
|
let mut auth_app = AuthenicationApp::new(auth_identity);
|
|
match auth_app.run_inbound(conn, &mut transcript) {
|
|
Ok(mut conn) => {
|
|
let orbs = orbs.lock().unwrap().clone();
|
|
conn.send_json_encrypted::<Vec<Orb>>(orbs);
|
|
}
|
|
_ => {}
|
|
}
|
|
};
|
|
|
|
let mut _service = service.listen(port, inbound_service.clone()).unwrap_or_else(|_| panic!());
|
|
}
|
|
Err(_err) => panic!("Could not host orb listener at {}", profile.identity.hostname()),
|
|
}
|
|
|
|
for follow in profile.follows.iter() {
|
|
follow_orbs(profile.identity.clone(), socks_port, follow.clone());
|
|
}
|
|
let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
|
|
// Terminal initialization
|
|
let stdout = io::stdout().into_raw_mode()?;
|
|
let stdout = MouseTerminal::from(stdout);
|
|
let stdout = AlternateScreen::from(stdout);
|
|
let backend = TermionBackend::new(stdout);
|
|
let mut terminal = Terminal::new(backend)?;
|
|
let mut events = Events::new();
|
|
let title = format!(" ◉ Orbscura: {} ", profile.identity.hostname());
|
|
|
|
loop {
|
|
terminal.draw(|f| {
|
|
let chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([Constraint::Percentage(80), Constraint::Percentage(19), Constraint::Percentage(1)].as_ref())
|
|
.split(f.size());
|
|
|
|
let events: Vec<ListItem> = app
|
|
.orbs
|
|
.items
|
|
.iter()
|
|
.map(|orb| {
|
|
// Colorcode the level depending on its type
|
|
|
|
let header = match &orb.rebroadcast_from {
|
|
Some(_host) => Spans::from(vec![
|
|
Span::styled("⮔ ", Style::default().fg(Color::LightMagenta)),
|
|
Span::styled(format!("{:<9}", orb.message), Style::default()),
|
|
]),
|
|
None => Spans::from(vec![Span::styled(format!("{}", orb.message), Style::default())]),
|
|
};
|
|
|
|
// Here several things happen:
|
|
// 1. Add a `---` spacing line above the final list entry
|
|
// 2. Add the Level + datetime
|
|
// 3. Add a spacer line
|
|
// 4. Add the actual event
|
|
|
|
ListItem::new(vec![
|
|
header,
|
|
Spans::from(vec![Span::styled(
|
|
format!("{} {}", orb.author, NaiveDateTime::from_timestamp(orb.timestamp as i64, 0).to_string()),
|
|
Style::default().fg(Color::Magenta),
|
|
)]),
|
|
Spans::from("-".repeat(chunks[0].width as usize)),
|
|
])
|
|
})
|
|
.collect();
|
|
|
|
let events_list = List::new(events)
|
|
.block(Block::default().borders(Borders::ALL).title(title.as_str()))
|
|
.start_corner(Corner::TopLeft)
|
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
|
f.render_stateful_widget(events_list, chunks[0], &mut app.orbs.state);
|
|
|
|
let input = Paragraph::new(app.input.as_ref())
|
|
.style(Style::default().fg(Color::Magenta))
|
|
.block(Block::default().borders(Borders::ALL).title(" Compose New Orb "));
|
|
f.render_widget(input, chunks[1]);
|
|
|
|
let status_bar = Paragraph::new(app.status.as_ref()).style(Style::default().bg(Color::Magenta).fg(Color::White));
|
|
f.render_widget(status_bar, chunks[2]);
|
|
})?;
|
|
|
|
if let Event::Input(input) = events.next()? {
|
|
match app.mode {
|
|
Mode::Browsing => match input {
|
|
Key::Char('e') => {
|
|
app.mode = NewPost;
|
|
app.status = format!("Composing Mode");
|
|
events.disable_exit_key();
|
|
}
|
|
Key::Char('q') => {
|
|
break;
|
|
}
|
|
Key::Char('f') => {
|
|
let follow = ctx.get_contents().unwrap();
|
|
if validate_hostname(follow.as_str()) {
|
|
if profile.follows.contains(&follow) {
|
|
app.status = format!("You already follow {}", follow);
|
|
} else {
|
|
profile.follows.push(follow.clone());
|
|
app.status = format!("Followed {}", follow);
|
|
follow_orbs(profile.identity.clone(), socks_port, follow.clone());
|
|
match profile.save() {
|
|
Err(e) => app.status = format!("Could not save orb.profile: {}", e.to_string()),
|
|
_ => {}
|
|
}
|
|
}
|
|
} else {
|
|
app.status = format!("{} is not a valid hostname", follow);
|
|
}
|
|
}
|
|
Key::Char('c') => {
|
|
ctx.set_contents(profile.identity.hostname().clone().to_owned()).unwrap();
|
|
app.status = format!("Copied {} to Clipboard", profile.identity.hostname());
|
|
}
|
|
Key::Char('r') => {
|
|
match app.orbs.state.selected() {
|
|
Some(index) => {
|
|
app.status = format!("Rebroadcasting {}", index);
|
|
let mut orb_to_rebroadcast = app.orbs.items[index].clone();
|
|
orb_to_rebroadcast.rebroadcast_from = Some(profile.identity.hostname().clone());
|
|
orb_to_rebroadcast.rebroadcast_time = Some(Utc::now().timestamp().unsigned_abs());
|
|
profile.orbs.lock().unwrap().push(orb_to_rebroadcast.clone());
|
|
Q.push(orb_to_rebroadcast.clone()); // Flush Cache
|
|
app.orbs.unselect();
|
|
match profile.save() {
|
|
Err(e) => app.status = format!("Could not save orb.profile: {}", e.to_string()),
|
|
_ => {}
|
|
}
|
|
}
|
|
_ => {
|
|
app.orbs.unselect();
|
|
}
|
|
}
|
|
}
|
|
Key::Left => {
|
|
app.orbs.unselect();
|
|
app.status = format!("");
|
|
}
|
|
Key::Down => {
|
|
app.orbs.next();
|
|
app.status = format!("Selected {}. (R) to Rebroadcast", app.orbs.state.selected().unwrap());
|
|
}
|
|
Key::Up => {
|
|
app.orbs.previous();
|
|
app.status = format!("Selected {}. (R) to Rebroadcast", app.orbs.state.selected().unwrap());
|
|
}
|
|
_ => {}
|
|
},
|
|
Mode::NewPost => match input {
|
|
Key::Char('\n') => {
|
|
let mut orb = Orb {
|
|
message: app.input.clone(),
|
|
author: profile.identity.hostname(),
|
|
timestamp: Utc::now().timestamp().unsigned_abs(),
|
|
signature: Signature::new([0; 64]),
|
|
rebroadcast_from: None,
|
|
rebroadcast_time: None,
|
|
};
|
|
|
|
let signature = profile.identity.sign(serde_json::to_string_pretty(&orb).unwrap().as_bytes());
|
|
orb.signature = signature;
|
|
|
|
profile.orbs.lock().unwrap().push(orb.clone());
|
|
Q.push(orb.clone()); // Flush Cache
|
|
app.input = String::new();
|
|
app.mode = Browsing;
|
|
match profile.save() {
|
|
Err(e) => app.status = format!("Could not save orb.profile: {}", e.to_string()),
|
|
_ => {}
|
|
}
|
|
}
|
|
Key::Char(c) => {
|
|
app.input.push(c);
|
|
}
|
|
Key::Backspace => {
|
|
app.input.pop();
|
|
}
|
|
Key::Esc => {
|
|
app.mode = Browsing;
|
|
app.status = format!("");
|
|
events.enable_exit_key();
|
|
}
|
|
_ => {}
|
|
},
|
|
}
|
|
}
|
|
|
|
if Q.is_empty() == false {
|
|
let next = Q.pop().unwrap();
|
|
profile.cache.push(next);
|
|
profile.gen_cache();
|
|
app.orbs = StatefulList::with_items(profile.cache.clone());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn follow_orbs(profile_identity: Arc<Identity>, socks_port: u16, follow: String) {
|
|
spawn(move || {
|
|
let mut service = Service::init(profile_identity.clone(), socks_port);
|
|
let outbound_identity = profile_identity.clone();
|
|
|
|
let outbound_service = move |conn: Connection<OutboundConnection>| {
|
|
let mut transcript = Transcript::new_transcript("tapir-transcript");
|
|
let mut auth_app = AuthenicationApp::new(outbound_identity);
|
|
match auth_app.run_outbound(conn, &mut transcript) {
|
|
Ok(mut conn) => match hostname_to_public_key(conn.hostname().as_str()) {
|
|
Ok(public_key) => {
|
|
let orbs_json_bytes = conn.expect_encrypted();
|
|
let orbs_json = String::from_utf8(orbs_json_bytes).unwrap_or_default();
|
|
let orbs: Vec<Orb> = serde_json::from_str(orbs_json.as_str()).unwrap_or(vec![]);
|
|
for orb in orbs.iter() {
|
|
let mut unsigned_orb = Orb {
|
|
message: orb.message.clone(),
|
|
author: orb.author.clone(),
|
|
timestamp: orb.timestamp,
|
|
signature: Signature::new([0; 64]),
|
|
rebroadcast_from: None,
|
|
rebroadcast_time: None,
|
|
};
|
|
if orb.author == conn.hostname() {
|
|
if public_key
|
|
.verify(serde_json::to_string_pretty(&unsigned_orb).unwrap().as_bytes(), &orb.signature)
|
|
.is_ok()
|
|
{
|
|
Q.push(orb.clone());
|
|
}
|
|
} else {
|
|
match hostname_to_public_key(orb.author.as_str()) {
|
|
Ok(public_key) => {
|
|
if public_key
|
|
.verify(serde_json::to_string_pretty(&unsigned_orb).unwrap().as_bytes(), &orb.signature)
|
|
.is_ok()
|
|
{
|
|
Q.push(orb.clone());
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
},
|
|
Err(_err) => {}
|
|
}
|
|
};
|
|
loop {
|
|
match service.connect(String::from(&follow).as_str(), outbound_service.clone()) {
|
|
_ => {}
|
|
}
|
|
sleep(Duration::new(30, 0));
|
|
}
|
|
});
|
|
}
|