#![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, rebroadcast_time: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct Profile { identity: Arc, follows: Vec, orbs: Arc>>, cache: Vec, } 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 = SegQueue::new(); struct App { input: String, mode: Mode, orbs: StatefulList, status: String, } fn main() -> Result<(), Box> { 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::(); 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| { 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::>(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 = 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, 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| { 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 = 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)); } }); }