Add TLS transport support
Connect over TLS (default port 6697) using native-tls with the system OpenSSL trust store for certificate verification. Unify the plaintext and TLS transports behind a single Box<dyn Read + Write>, dropping the read/write socket clone that a TLS session cannot support. Add a `tls` config key and IRC_TLS override; the port auto-defaults to 6697 when TLS is enabled.
This commit is contained in:
parent
05c004d32d
commit
ab02a5436e
5 changed files with 78 additions and 28 deletions
|
|
@ -37,6 +37,8 @@ const HISTORY_GRACE_SECS: u64 = 60;
|
|||
pub struct Config {
|
||||
pub server: String,
|
||||
pub port: u16,
|
||||
/// Connect over TLS (typically port 6697) instead of plaintext.
|
||||
pub tls: bool,
|
||||
pub nick: String,
|
||||
pub user: String,
|
||||
pub realname: String,
|
||||
|
|
@ -56,6 +58,7 @@ impl Default for Config {
|
|||
Config {
|
||||
server: "irc.tchatou.fr".to_string(),
|
||||
port: 6667,
|
||||
tls: false,
|
||||
nick: "rubot".to_string(),
|
||||
user: "rubot".to_string(),
|
||||
realname: "rubot".to_string(),
|
||||
|
|
|
|||
53
src/irc.rs
53
src/irc.rs
|
|
@ -13,10 +13,12 @@
|
|||
#![allow(dead_code)] // This is a library-style module; not every helper is used yet.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use native_tls::TlsConnector;
|
||||
|
||||
/// The source of a message: `nick!user@host`, or a bare nick / server name.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Prefix {
|
||||
|
|
@ -182,27 +184,39 @@ fn unescape_tag_value(value: &str) -> String {
|
|||
out
|
||||
}
|
||||
|
||||
/// A blocking IRC connection over plain TCP.
|
||||
/// Any bidirectional byte stream: a plain `TcpStream` or a TLS session. The
|
||||
/// blanket impl covers both, so [`Connection`] can treat them uniformly.
|
||||
trait Stream: Read + Write {}
|
||||
impl<T: Read + Write> Stream for T {}
|
||||
|
||||
/// A blocking IRC connection over plain TCP or TLS.
|
||||
///
|
||||
/// Reading and writing use separate clones of the same socket, so the event
|
||||
/// loop can block on `read_message` while senders write independently.
|
||||
/// The event loop is single-threaded (read a message, handle it, maybe write a
|
||||
/// reply, repeat), so one stream serves both directions: reads go through the
|
||||
/// `BufReader`, writes through `get_mut()`. This is what lets a single TLS
|
||||
/// session — which cannot be cloned into independent halves — work unchanged.
|
||||
pub struct Connection {
|
||||
reader: BufReader<TcpStream>,
|
||||
writer: TcpStream,
|
||||
inner: BufReader<Box<dyn Stream>>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
/// Open a plaintext TCP connection to `host:port`.
|
||||
/// Connect to `host:port`, optionally wrapping the socket in TLS.
|
||||
///
|
||||
/// Note: this is cleartext (typically port 6667). TLS (port 6697) would be a
|
||||
/// natural next step by wrapping the stream in `rustls`/`native-tls`.
|
||||
pub fn connect(host: &str, port: u16) -> io::Result<Connection> {
|
||||
let stream = TcpStream::connect((host, port))?;
|
||||
let writer = stream.try_clone()?;
|
||||
Ok(Connection {
|
||||
reader: BufReader::new(stream),
|
||||
writer,
|
||||
})
|
||||
/// TLS uses the system trust store (via `native-tls`/OpenSSL), with `host`
|
||||
/// supplied as the SNI name and for certificate verification.
|
||||
pub fn connect(host: &str, port: u16, use_tls: bool) -> io::Result<Connection> {
|
||||
let tcp = TcpStream::connect((host, port))?;
|
||||
let stream: Box<dyn Stream> = if use_tls {
|
||||
let connector =
|
||||
TlsConnector::new().map_err(|e| io::Error::other(format!("TLS init: {e}")))?;
|
||||
let tls = connector
|
||||
.connect(host, tcp)
|
||||
.map_err(|e| io::Error::other(format!("TLS handshake: {e}")))?;
|
||||
Box::new(tls)
|
||||
} else {
|
||||
Box::new(tcp)
|
||||
};
|
||||
Ok(Connection { inner: BufReader::new(stream) })
|
||||
}
|
||||
|
||||
/// Read and parse the next message. Returns `Ok(None)` on a clean EOF
|
||||
|
|
@ -210,7 +224,7 @@ impl Connection {
|
|||
pub fn read_message(&mut self) -> io::Result<Option<Message>> {
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
if self.reader.read_line(&mut line)? == 0 {
|
||||
if self.inner.read_line(&mut line)? == 0 {
|
||||
return Ok(None); // EOF
|
||||
}
|
||||
let trimmed = line.trim_end_matches(['\r', '\n']);
|
||||
|
|
@ -230,8 +244,9 @@ impl Connection {
|
|||
pub fn send_raw(&mut self, line: &str) -> io::Result<()> {
|
||||
let clean = line.replace(['\r', '\n'], " ");
|
||||
eprintln!(">> {clean}");
|
||||
write!(self.writer, "{clean}\r\n")?;
|
||||
self.writer.flush()
|
||||
let writer = self.inner.get_mut();
|
||||
write!(writer, "{clean}\r\n")?;
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
// --- Convenience senders -------------------------------------------------
|
||||
|
|
|
|||
20
src/main.rs
20
src/main.rs
|
|
@ -44,7 +44,7 @@ fn main() {
|
|||
|
||||
/// Run a single connection lifecycle: connect, register, loop until disconnect.
|
||||
fn run_once(config: &Config) -> std::io::Result<()> {
|
||||
let conn = Connection::connect(&config.server, config.port)?;
|
||||
let conn = Connection::connect(&config.server, config.port, config.tls)?;
|
||||
let mut bot = Bot::new(config.clone(), conn);
|
||||
bot.run()
|
||||
}
|
||||
|
|
@ -62,6 +62,12 @@ fn load_config() -> Config {
|
|||
}
|
||||
apply_env(&mut c);
|
||||
|
||||
// Convention: TLS listens on 6697. If TLS is enabled but the port is still
|
||||
// the plaintext default, switch to the conventional TLS port.
|
||||
if c.tls && c.port == 6667 {
|
||||
c.port = 6697;
|
||||
}
|
||||
|
||||
c
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +107,7 @@ fn apply_config_file(c: &mut Config, path: &Path) -> io::Result<()> {
|
|||
Ok(p) => c.port = p,
|
||||
Err(_) => log(&format!("{}:{}: invalid port '{value}'", path.display(), i + 1)),
|
||||
},
|
||||
"tls" => c.tls = parse_bool(value),
|
||||
"nick" => c.nick = value.to_string(),
|
||||
"user" => c.user = value.to_string(),
|
||||
"realname" => c.realname = value.to_string(),
|
||||
|
|
@ -127,6 +134,9 @@ fn apply_env(c: &mut Config) {
|
|||
if let Some(p) = std::env::var("IRC_PORT").ok().and_then(|v| v.parse().ok()) {
|
||||
c.port = p;
|
||||
}
|
||||
if let Ok(v) = std::env::var("IRC_TLS") {
|
||||
c.tls = parse_bool(&v);
|
||||
}
|
||||
if let Ok(v) = std::env::var("IRC_NICK") {
|
||||
c.user = v.clone();
|
||||
c.nick = v;
|
||||
|
|
@ -151,6 +161,14 @@ fn apply_env(c: &mut Config) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parse a boolean config value. Accepts `1`, `true`, `yes`, `on` (any case).
|
||||
fn parse_bool(value: &str) -> bool {
|
||||
matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
"1" | "true" | "yes" | "on"
|
||||
)
|
||||
}
|
||||
|
||||
/// Split a channel list on commas and/or whitespace, dropping empties.
|
||||
fn split_list(value: &str) -> Vec<String> {
|
||||
value
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue