orbscura/src/main.rs

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