Compare commits
No commits in common. "main" and "v1.0.1" have entirely different histories.
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
secret.gpg
|
||||
public.gpg
|
||||
.env
|
||||
|
|
1406
Cargo.lock
generated
Normal file
1406
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
@ -1,21 +1,15 @@
|
|||
[package]
|
||||
name = "e2e-irc"
|
||||
version = "3.0.0"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
keywords = ["irc", "encryption"]
|
||||
description = "An IRC bouncer that can send encrypted messages"
|
||||
repository = "https://forgejo.vanten-s.com/vanten-s/e2e-irc/"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
argparse = "0.2.2"
|
||||
base64 = "0.21.4"
|
||||
dirs = "5.0.1"
|
||||
dotenv = "0.15.0"
|
||||
eyre = "0.6.8"
|
||||
ircparser-vanten = "0.2.1"
|
||||
ircparser = "0.2.1"
|
||||
openssl = "0.10"
|
||||
pgp = "0.10.2"
|
||||
rand = "0.8.5"
|
||||
toml = "0.8.2"
|
||||
|
|
42
README.md
42
README.md
|
@ -1,35 +1,25 @@
|
|||
# e2e-irc
|
||||
This is an IRC bouncer that supports end-to-end encryption and is horribly written.
|
||||
|
||||
# Configuration
|
||||
e2e-irc uses a config file in `~/.config/e2e-irc/config.toml` on linux.
|
||||
The configuration file is a toml file with two or three fields:
|
||||
- public_key the location of the public pgp key
|
||||
- secret_key the location of the secret pgp key
|
||||
- passwd the password of the pgp key
|
||||
|
||||
# Usage
|
||||
```
|
||||
e2e-irc [OPTIONS] SERVER
|
||||
|
||||
Encrypted IRC Bouncer
|
||||
|
||||
Positional arguments:
|
||||
server The Address Of The Server The Bouncer Connects To
|
||||
|
||||
Optional arguments:
|
||||
-h,--help Show this help message and exit
|
||||
-p,--port PORT The Port The Bouncer Binds To
|
||||
--sp,--server-port SERVER_PORT
|
||||
The TLS Enabled Port Of The Server
|
||||
```
|
||||
|
||||
# Install
|
||||
# Setup CWD
|
||||
To use this you need to build it and in your CWD you need to add two files. One called `secret.gpg` and one called `public.gpg` that are just pgp keys.
|
||||
You also need a .env file in your CWD like this:
|
||||
```bash
|
||||
cargo install e2e-irc
|
||||
SERVER={address of the server you want to connect to (has to support tls)}
|
||||
PORT={the port you want the program to bind to and the port that the IRC client should connect to}
|
||||
PASSWD={password of your pgp key} # Optional
|
||||
```
|
||||
|
||||
# Run
|
||||
Then you just run the binary with:
|
||||
`cargo run --release`
|
||||
or build the project to get a binary
|
||||
```bash
|
||||
e2e-irc [OPTIONS] SERVER
|
||||
cargo build --release
|
||||
cp target/release/e2e-irc ~/.local/bin/
|
||||
```
|
||||
and after running that once you can run the program with
|
||||
```bash
|
||||
e2e-irc
|
||||
```
|
||||
but you have to have the .env in the CWD
|
||||
|
|
|
@ -1,41 +1,10 @@
|
|||
use crate::helpers::bytes_to_privmsg_base64;
|
||||
use crate::{encryption, helpers, State};
|
||||
use crate::{encryption, helpers};
|
||||
use eyre::Result;
|
||||
use pgp::{Deserializable, SignedPublicKey};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InvalidCommand;
|
||||
|
||||
impl std::fmt::Display for InvalidCommand {
|
||||
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for InvalidCommand {}
|
||||
|
||||
fn parse_bouncer_command(message: String, state: &mut State) -> Result<()> {
|
||||
macro_rules! unwrap_option {
|
||||
($t:expr) => {
|
||||
match $t {
|
||||
Some(val) => val,
|
||||
None => return Err(InvalidCommand.into()),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let mut splitted = message.split(' ');
|
||||
match unwrap_option!(splitted.next()) {
|
||||
"ALLOW_UNENCRYPTED" => state
|
||||
.nicks_without_encryption
|
||||
.push(unwrap_option!(splitted.next()).to_string().to_lowercase()),
|
||||
_ => return Err(InvalidCommand.into()),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_message_from_client(
|
||||
recieved: &str,
|
||||
public_key: &Vec<u8>,
|
||||
|
@ -45,34 +14,10 @@ pub fn handle_message_from_client(
|
|||
writer_channel_rx: &Receiver<String>,
|
||||
listener_channel_tx: &Sender<String>,
|
||||
_listener_channel_rx: &Receiver<String>,
|
||||
state: &mut State,
|
||||
) -> Result<()> {
|
||||
let mut recieved = recieved.to_string();
|
||||
let command = &ircparser::parse(recieved).expect("Got an invalid IRC instruction")[0];
|
||||
|
||||
if recieved.split(' ').count() == 1 {
|
||||
recieved += " ";
|
||||
}
|
||||
|
||||
let parsed = ircparser::parse(&recieved);
|
||||
let command = match parsed {
|
||||
Ok(val) => val[0].clone(),
|
||||
Err(_) => {
|
||||
writer_channel_tx.send(recieved)?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if command.command == "PRIVMSG" && !command.params[0].starts_with('#') {
|
||||
if command.params[0] == "BOUNCER" {
|
||||
return parse_bouncer_command(command.params[1].clone(), state);
|
||||
}
|
||||
if state
|
||||
.nicks_without_encryption
|
||||
.contains(&command.params[0].to_lowercase())
|
||||
{
|
||||
writer_channel_tx.send(recieved)?;
|
||||
return Ok(());
|
||||
}
|
||||
if command.command == "PRIVMSG" && !command.params[0].starts_with("#") {
|
||||
let other = &command.params[0];
|
||||
|
||||
if !keys.contains_key(other) {
|
||||
|
@ -82,7 +27,7 @@ pub fn handle_message_from_client(
|
|||
listener_channel_tx,
|
||||
"127.0.0.1",
|
||||
server,
|
||||
other,
|
||||
&other,
|
||||
"END_KEY",
|
||||
)?;
|
||||
let key = SignedPublicKey::from_bytes(key.as_slice())?;
|
||||
|
@ -93,12 +38,12 @@ pub fn handle_message_from_client(
|
|||
|
||||
writer_channel_tx.send(format!("PRIVMSG {other} START_MESSAGE\r\n"))?;
|
||||
writer_channel_tx.send(bytes_to_privmsg_base64(
|
||||
&encryption::encrypt(foreign_key, &command.params[1])?,
|
||||
&encryption::encrypt(&foreign_key, &command.params[1])?,
|
||||
other,
|
||||
))?;
|
||||
writer_channel_tx.send(format!("PRIVMSG {other} END_MESSAGE\r\n"))?;
|
||||
} else {
|
||||
writer_channel_tx.send(recieved.replace("127.0.0.1", server))?;
|
||||
writer_channel_tx.send(recieved.replace("127.0.0.1", &server))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ pub fn encrypt(key: &SignedPublicKey, message: &str) -> Result<Vec<u8>, pgp::err
|
|||
|
||||
let message = message.encrypt_to_keys(&mut rng, SymmetricKeyAlgorithm::AES128, &[key])?;
|
||||
|
||||
message.to_bytes()
|
||||
Ok(message.to_bytes()?)
|
||||
}
|
||||
|
||||
pub fn decrypt<'a>(
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use base64::{engine::general_purpose, Engine as _};
|
||||
use eyre::Result;
|
||||
use ircparser;
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IrcParseError;
|
||||
struct IrcParseError;
|
||||
|
||||
impl std::fmt::Display for IrcParseError {
|
||||
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
|
@ -35,22 +36,6 @@ macro_rules! unwrap_or_return_option {
|
|||
};
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
pub nicks_without_encryption: Vec<String>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new() -> Self {
|
||||
State {
|
||||
nicks_without_encryption: vec![
|
||||
"nickserv".to_string(),
|
||||
"chanserv".to_string(),
|
||||
"hostserv".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn forward(
|
||||
message: String,
|
||||
stream: &Sender<String>,
|
||||
|
@ -60,7 +45,7 @@ fn forward(
|
|||
match ircparser::parse(&message) {
|
||||
Ok(val) => match val[0].command.as_str() {
|
||||
"PRIVMSG" => stream.send(message),
|
||||
_ => stream.send(message.replace(server_local, server_forward)),
|
||||
_ => stream.send(message.replace(&server_local, server_forward)),
|
||||
},
|
||||
Err(_) => stream.send(message.replace(server_local, server_forward)),
|
||||
}
|
||||
|
@ -128,7 +113,7 @@ pub fn recieve_message_base64(
|
|||
.clone()
|
||||
.unwrap_or("".to_string())
|
||||
.starts_with(&begin_source_reciever)
|
||||
|| recieved.params[0].starts_with('#')
|
||||
|| recieved.params[0].starts_with("#")
|
||||
{
|
||||
forward(recieved_raw, forward_stream, server_local, server_forward)?;
|
||||
continue;
|
||||
|
|
|
@ -31,13 +31,13 @@ fn stream_handler(tx: &mpsc::Sender<String>, rx: &mpsc::Receiver<String>, mut st
|
|||
}
|
||||
};
|
||||
}
|
||||
Err(TryRecvError::Empty) => {}
|
||||
Err(TryRecvError::Empty) => {},
|
||||
Err(TryRecvError::Disconnected) => return,
|
||||
}
|
||||
thread::sleep(Duration::from_micros(100));
|
||||
}
|
||||
|
||||
let _ = tx.send(dbg!(String::from_utf8_lossy(&buffer).to_string()));
|
||||
let _ = tx.send(String::from_utf8_lossy(&buffer).to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,5 @@ pub fn listen_to_client(tx: mpsc::Sender<String>, rx: mpsc::Receiver<String>, po
|
|||
|
||||
stream_handler(&tx, &rx, stream);
|
||||
println!("Closed connection with {ip}");
|
||||
let _ = tx.send("DUMMY CLOSE_CONNECTION".to_string());
|
||||
}
|
||||
}
|
||||
|
|
108
src/main.rs
108
src/main.rs
|
@ -1,14 +1,12 @@
|
|||
use argparse::{ArgumentParser, Store};
|
||||
use dirs::config_local_dir;
|
||||
use dotenv::{dotenv, vars};
|
||||
use eyre::Result;
|
||||
use helpers::State;
|
||||
use pgp::{Deserializable, SignedPublicKey, SignedSecretKey};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::net::{Shutdown, TcpStream};
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use toml::Table;
|
||||
|
||||
mod client_handler;
|
||||
mod encryption;
|
||||
|
@ -18,61 +16,46 @@ mod server_handler;
|
|||
mod writer_client;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let config_file = config_local_dir()
|
||||
.expect("Couldn't get config directory")
|
||||
.join("e2e-irc/config.toml");
|
||||
dotenv().expect("Couldn't load .env. It probably doesn't exist");
|
||||
let mut vars_hashmap = HashMap::new();
|
||||
|
||||
if !config_file.exists() {
|
||||
panic!("Create a config file at {}", config_file.display());
|
||||
for var in vars() {
|
||||
vars_hashmap.insert(var.0, var.1);
|
||||
}
|
||||
|
||||
let parsed_config = String::from_utf8_lossy(&fs::read(config_file)?).parse::<Table>()?;
|
||||
let server = &vars_hashmap["SERVER"];
|
||||
|
||||
let public_key_location = parsed_config
|
||||
.get("public_key")
|
||||
.expect("Coudln't get public_key. Try creating it in the config")
|
||||
.as_str()
|
||||
.expect("Couldn't convert public_key to str");
|
||||
let secret_key_location = parsed_config
|
||||
.get("secret_key")
|
||||
.expect("Coudln't get secret_key. Try creating it in the config")
|
||||
.as_str()
|
||||
.expect("Couldn't convert secret_key to str");
|
||||
let default_passwd = String::new();
|
||||
|
||||
let default_password = toml::Value::String(String::new());
|
||||
|
||||
let passwd = parsed_config
|
||||
.get("passwd")
|
||||
.unwrap_or(&default_password)
|
||||
.as_str()
|
||||
.expect("Coudln't convert passwd to str");
|
||||
|
||||
let mut server = "irc.vanten-s.com".to_string();
|
||||
let mut port = "6666".to_string();
|
||||
let mut server_port = "6697".to_string();
|
||||
|
||||
{
|
||||
let mut ap = ArgumentParser::new();
|
||||
ap.set_description("Encrypted IRC Bouncer");
|
||||
ap.refer(&mut server)
|
||||
.add_argument(
|
||||
"server",
|
||||
Store,
|
||||
"The Address Of The Server The Bouncer Connects To",
|
||||
)
|
||||
.required();
|
||||
ap.refer(&mut port)
|
||||
.add_option(&["-p", "--port"], Store, "The Port The Bouncer Binds To");
|
||||
ap.refer(&mut server_port).add_option(
|
||||
&["--sp", "--server-port"],
|
||||
Store,
|
||||
"The TLS Enabled Port Of The Server",
|
||||
);
|
||||
ap.parse_args_or_exit();
|
||||
let port = match vars_hashmap.get("PORT") {
|
||||
Some(val) => val,
|
||||
None => "6666",
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let public_key = fs::read(public_key_location)?;
|
||||
let secret_key = SignedSecretKey::from_bytes(fs::read(secret_key_location)?.as_slice())?;
|
||||
let passwd = vars_hashmap.get("PASSWD").unwrap_or(&default_passwd);
|
||||
|
||||
let stream = TcpStream::connect(format!("{server}:6697"))?;
|
||||
|
||||
let public_key = fs::read("public.gpg")?;
|
||||
let secret_key = SignedSecretKey::from_bytes(fs::read("secret.gpg")?.as_slice())?;
|
||||
|
||||
let reader_stream = match stream.try_clone() {
|
||||
Ok(stream) => stream,
|
||||
Err(_error) => {
|
||||
let _ = stream.shutdown(Shutdown::Both);
|
||||
panic!("Failed to create the reader stream")
|
||||
}
|
||||
};
|
||||
|
||||
let writer_stream = match stream.try_clone() {
|
||||
Ok(stream) => stream,
|
||||
Err(_error) => {
|
||||
let _ = stream.shutdown(Shutdown::Both);
|
||||
let _ = reader_stream.shutdown(Shutdown::Both);
|
||||
panic!("Failed to create the writer stream")
|
||||
}
|
||||
};
|
||||
|
||||
let (listener_channel_send_tx, listener_channel_rx) = mpsc::channel();
|
||||
let (listener_channel_tx, listener_channel_recv_rx) = mpsc::channel();
|
||||
|
@ -80,27 +63,20 @@ fn main() -> Result<()> {
|
|||
let (writer_channel_tx, writer_channel_send_rx) = mpsc::channel();
|
||||
let (writer_channel_recv_tx, writer_channel_rx) = mpsc::channel();
|
||||
|
||||
let tmp_port = port.clone();
|
||||
thread::spawn(move || {
|
||||
listener_server::listen_to_client(
|
||||
listener_channel_send_tx,
|
||||
listener_channel_recv_rx,
|
||||
tmp_port,
|
||||
)
|
||||
listener_server::listen_to_client(listener_channel_send_tx, listener_channel_recv_rx, port)
|
||||
});
|
||||
let tmp_port = server_port.clone();
|
||||
let tmp_server = server.clone();
|
||||
thread::spawn(move || {
|
||||
thread::spawn(|| {
|
||||
writer_client::write_to_server(
|
||||
&tmp_server,
|
||||
&tmp_port,
|
||||
writer_stream,
|
||||
tmp_server,
|
||||
writer_channel_send_rx,
|
||||
writer_channel_recv_tx,
|
||||
)
|
||||
});
|
||||
|
||||
let mut keys: HashMap<String, SignedPublicKey> = HashMap::new();
|
||||
let mut state = State::new();
|
||||
|
||||
loop {
|
||||
match listener_channel_rx.try_recv() {
|
||||
|
@ -108,13 +84,12 @@ fn main() -> Result<()> {
|
|||
let _ = client_handler::handle_message_from_client(
|
||||
&message,
|
||||
&public_key,
|
||||
&server,
|
||||
server,
|
||||
&mut keys,
|
||||
&writer_channel_tx,
|
||||
&writer_channel_rx,
|
||||
&listener_channel_tx,
|
||||
&listener_channel_rx,
|
||||
&mut state,
|
||||
);
|
||||
}
|
||||
Err(error) => match error {
|
||||
|
@ -129,14 +104,13 @@ fn main() -> Result<()> {
|
|||
&message,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
&server,
|
||||
server,
|
||||
passwd,
|
||||
&mut keys,
|
||||
&writer_channel_tx,
|
||||
&writer_channel_rx,
|
||||
&listener_channel_tx,
|
||||
&listener_channel_rx,
|
||||
&state,
|
||||
);
|
||||
}
|
||||
Err(error) => match error {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use crate::unwrap_or_return_option;
|
||||
use crate::unwrap_or_return_result;
|
||||
use crate::State;
|
||||
use crate::{encryption, helpers};
|
||||
use eyre::Result;
|
||||
use pgp::{Deserializable, SignedPublicKey, SignedSecretKey};
|
||||
|
@ -26,20 +25,15 @@ pub fn handle_message_from_server(
|
|||
writer_channel_rx: &Receiver<String>,
|
||||
listener_channel_tx: &Sender<String>,
|
||||
_listener_channel_rx: &Receiver<String>,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let recieved_parsed = &unwrap_or_return_result!(ircparser::parse(recieved))[0];
|
||||
|
||||
let default_reciever = String::new();
|
||||
|
||||
let reciever = match recieved_parsed.params.get(0) {
|
||||
Some(val) => val,
|
||||
None => &default_reciever,
|
||||
};
|
||||
|
||||
if recieved_parsed.command != "PRIVMSG"
|
||||
|| reciever.starts_with('#')
|
||||
|| state.nicks_without_encryption.contains(reciever)
|
||||
|| recieved_parsed
|
||||
.params
|
||||
.get(0)
|
||||
.unwrap_or(&String::new())
|
||||
.starts_with('#')
|
||||
{
|
||||
forward(recieved, listener_channel_tx, server)?;
|
||||
return Ok(());
|
||||
|
|
|
@ -6,19 +6,14 @@ use std::thread;
|
|||
use std::time::Duration;
|
||||
|
||||
pub fn write_to_server(
|
||||
server: &str,
|
||||
port: &str,
|
||||
tcp_stream: TcpStream,
|
||||
server: String,
|
||||
rx: mpsc::Receiver<String>,
|
||||
tx: mpsc::Sender<String>,
|
||||
) {
|
||||
'big: loop {
|
||||
println!("Connecting to {server}:{port}");
|
||||
let tcp_stream =
|
||||
TcpStream::connect(format!("{server}:{port}")).expect("Couldn't connect to server");
|
||||
|
||||
let connector = SslConnector::builder(SslMethod::tls()).unwrap().build();
|
||||
let mut stream = connector
|
||||
.connect(server, &tcp_stream)
|
||||
.connect(&server, tcp_stream)
|
||||
.expect("Couldn't start TLS");
|
||||
|
||||
stream
|
||||
|
@ -41,25 +36,17 @@ pub fn write_to_server(
|
|||
Err(_error) => match _error.io_error() {
|
||||
None => {
|
||||
dbg!(_error.ssl_error());
|
||||
continue 'big;
|
||||
}
|
||||
Some(error) => match error.kind() {
|
||||
ErrorKind::WouldBlock => {}
|
||||
_ => {
|
||||
dbg!(error.kind());
|
||||
println!("Couldn't read the stream");
|
||||
continue 'big;
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
let value = rx.try_recv().unwrap_or("".to_string());
|
||||
match value.as_str() {
|
||||
"DUMMY CLOSE_CONNECTION" => {
|
||||
continue 'big;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
match stream.write_all(value.as_bytes()) {
|
||||
Ok(_) => {}
|
||||
Err(_e) => println!("Couldn't send {value}"),
|
||||
|
@ -70,4 +57,3 @@ pub fn write_to_server(
|
|||
let _ = tx.send(dbg!(String::from_utf8_lossy(&buffer).to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue