diff --git a/Cargo.toml b/Cargo.toml index 842dffc..8b1bb83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,12 @@ keywords = ["irc", "client", "thread-safe"] documentation = "http://www.rust-ci.org/aaronweiss74/irc/doc/irc/" repository = "https://github.com/aaronweiss74/irc" readme = "README.md" + +[features] + +ssl = ["openssl"] + +[dependencies.openssl] + +git = "https://github.com/sfackler/rust-openssl.git" +optional = true diff --git a/examples/simple.rs b/examples/simple.rs index fb027cf..e30beb9 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -16,6 +16,7 @@ fn main() { password: "".into_string(), server: "irc.fyrechat.net".into_string(), port: 6667, + use_ssl: false, channels: vec!("#vana".into_string()), options: HashMap::new(), }; diff --git a/examples/simple_ssl.rs b/examples/simple_ssl.rs new file mode 100644 index 0000000..53a66eb --- /dev/null +++ b/examples/simple_ssl.rs @@ -0,0 +1,36 @@ +#![feature(if_let)] +#![feature(slicing_syntax)] +extern crate irc; + +use std::collections::HashMap; +use irc::data::config::Config; +use irc::server::{IrcServer, Server}; +use irc::server::utils::Wrapper; + +fn main() { + let config = Config { + owners: vec!("awe".into_string()), + nickname: "pickles".into_string(), + username: "pickles".into_string(), + realname: "pickles".into_string(), + password: "".into_string(), + server: "irc.fyrechat.net".into_string(), + port: 6697, + use_ssl: true, + channels: vec!("#vana".into_string()), + options: HashMap::new(), + }; + let irc_server = IrcServer::from_config(config).unwrap(); + let server = Wrapper::new(&irc_server); + server.identify().unwrap(); + for message in server.iter() { + print!("{}", message.into_string()); + if message.command[] == "PRIVMSG" { + if let Some(msg) = message.suffix { + if msg.contains("pickles") { + server.send_privmsg(message.args[0][], "Hi!").unwrap(); + } + } + } + } +} diff --git a/mktestconfig.sh b/mktestconfig.sh index 541a0a6..1a68cab 100755 --- a/mktestconfig.sh +++ b/mktestconfig.sh @@ -1 +1 @@ -echo "{\"owners\": [\"test\"],\"nickname\": \"test\",\"username\": \"test\",\"realname\": \"test\",\"password\": \"\",\"server\": \"irc.test.net\",\"port\": 6667,\"channels\": [\"#test\", \"#test2\"],\"options\": {}}" > config.json +echo "{\"owners\": [\"test\"],\"nickname\": \"test\",\"username\": \"test\",\"realname\": \"test\",\"password\": \"\",\"server\": \"irc.test.net\",\"port\": 6667,\"use_ssl\": false,\"channels\": [\"#test\", \"#test2\"],\"options\": {}}" > config.json diff --git a/src/conn.rs b/src/conn.rs index ee327d7..a02ea17 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -2,8 +2,11 @@ #![experimental] use std::sync::{Mutex, MutexGuard}; use std::io::{BufferedReader, BufferedWriter, IoResult, TcpStream}; +#[cfg(feature = "ssl")] use std::io::{IoError, OtherIoError}; use data::kinds::{IrcWriter, IrcReader}; use data::message::Message; +#[cfg(feature = "ssl")] use openssl::ssl::{SslContext, SslStream, Tlsv1}; +#[cfg(feature = "ssl")] use openssl::ssl::error::SslError; /// A thread-safe connection. #[experimental] @@ -15,9 +18,74 @@ pub struct Connection where T: IrcWriter, U: IrcReader { impl Connection, BufferedReader> { /// Creates a thread-safe TCP connection to the specified server. #[experimental] - pub fn connect(host: &str, port: u16) -> IoResult, BufferedReader>> { + pub fn connect(host: &str, port: u16) -> IoResult, BufferedReader>> { let socket = try!(TcpStream::connect(format!("{}:{}", host, port)[])); - Ok(Connection::new(BufferedWriter::new(socket.clone()), BufferedReader::new(socket))) + Ok(Connection::new(BufferedWriter::new(UnsecuredTcpStream(socket.clone())), + BufferedReader::new(UnsecuredTcpStream(socket)))) + } + + /// Creates a thread-safe TCP connection to the specified server over SSL. + /// If the library is compiled without SSL support, this method panics. + #[experimental] + #[cfg(feature = "ssl")] + pub fn connect_ssl(host: &str, port: u16) -> IoResult, BufferedReader>> { + let socket = try!(TcpStream::connect(format!("{}:{}", host, port)[])); + let ssl = try!(ssl_to_io(SslContext::new(Tlsv1))); + let input = try!(ssl_to_io(SslStream::new(&ssl, socket.clone()))); + let output = try!(ssl_to_io(SslStream::new(&ssl, socket))); + Ok(Connection::new(BufferedWriter::new(SslTcpStream(input)), + BufferedReader::new(SslTcpStream(output)))) + } + + /// Creates a thread-safe TCP connection to the specified server over SSL. + /// If the library is compiled without SSL support, this method panics. + #[experimental] + #[cfg(not(feature = "ssl"))] + pub fn connect_ssl(host: &str, port: u16) -> IoResult, BufferedReader>> { + panic!("Cannot connect to {}:{} over SSL without compiling with SSL support.", host, port) + } +} + +/// An abstraction over different networked streams. +#[experimental] +pub enum NetStream { + /// An unsecured TcpStream. + UnsecuredTcpStream(TcpStream), + /// An SSL-secured TcpStream. + /// This is only available when compiled with SSL support. + #[cfg(feature = "ssl")] + SslTcpStream(SslStream), +} + +impl Reader for NetStream { + fn read(&mut self, buf: &mut [u8]) -> IoResult { + match self { + &UnsecuredTcpStream(ref mut stream) => stream.read(buf), + #[cfg(feature = "ssl")] + &SslTcpStream(ref mut stream) => stream.read(buf), + } + } +} + +impl Writer for NetStream { + fn write(&mut self, buf: &[u8]) -> IoResult<()> { + match self { + &UnsecuredTcpStream(ref mut stream) => stream.write(buf), + #[cfg(feature = "ssl")] + &SslTcpStream(ref mut stream) => stream.write(buf), + } + } +} + +#[cfg(feature = "ssl")] +fn ssl_to_io(res: Result) -> IoResult { + match res { + Ok(x) => Ok(x), + Err(e) => Err(IoError { + kind: OtherIoError, + desc: "An SSL error occurred.", + detail: Some(format!("{}", e)), + }), } } diff --git a/src/data/config.rs b/src/data/config.rs index 1bfa6c2..146c10d 100644 --- a/src/data/config.rs +++ b/src/data/config.rs @@ -23,6 +23,8 @@ pub struct Config { pub server: String, /// The port to connect on. pub port: u16, + /// Whether or not to use SSL. + pub use_ssl: bool, /// A list of channels to join on connection. pub channels: Vec, /// A map of additional options to be stored in config. @@ -70,6 +72,7 @@ mod test { password: String::new(), server: format!("irc.test.net"), port: 6667, + use_ssl: false, channels: vec![format!("#test"), format!("#test2")], options: HashMap::new(), }; @@ -86,6 +89,7 @@ mod test { password: String::new(), server: format!("irc.test.net"), port: 6667, + use_ssl: false, channels: vec![format!("#test"), format!("#test2")], options: HashMap::new(), }; @@ -102,6 +106,7 @@ mod test { password: String::new(), server: format!("irc.test.net"), port: 6667, + use_ssl: false, channels: Vec::new(), options: HashMap::new(), }; diff --git a/src/lib.rs b/src/lib.rs index 8ba9ecc..052d1d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ #![feature(if_let)] #![feature(slicing_syntax)] extern crate serialize; +#[cfg(feature = "ssl")] extern crate openssl; pub mod conn; pub mod data; diff --git a/src/server/mod.rs b/src/server/mod.rs index 6eec350..769d768 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,7 +1,7 @@ //! Interface for working with IRC Servers #![experimental] -use std::io::{BufferedReader, BufferedWriter, IoResult, TcpStream}; -use conn::Connection; +use std::io::{BufferedReader, BufferedWriter, IoResult}; +use conn::{Connection, NetStream}; use data::command::{Command, JOIN, PONG}; use data::config::Config; use data::kinds::{IrcReader, IrcWriter}; @@ -29,20 +29,28 @@ pub struct IrcServer<'a, T, U> where T: IrcWriter, U: IrcReader { config: Config } -impl<'a> IrcServer<'a, BufferedWriter, BufferedReader> { +impl<'a> IrcServer<'a, BufferedWriter, BufferedReader> { /// Creates a new IRC Server connection from the configuration at the specified path, connecting immediately. #[experimental] - pub fn new(config: &str) -> IoResult, BufferedReader>> { + pub fn new(config: &str) -> IoResult, BufferedReader>> { let config = try!(Config::load_utf8(config)); - let conn = try!(Connection::connect(config.server[], config.port)); - Ok(IrcServer::from_connection(config, conn)) + let conn = try!(if config.use_ssl { + Connection::connect_ssl(config.server[], config.port) + } else { + Connection::connect(config.server[], config.port) + }); + Ok(IrcServer { config: config, conn: conn }) } /// Creates a new IRC server connection from the specified configuration, connecting immediately. #[experimental] - pub fn from_config(config: Config) -> IoResult, BufferedReader>> { - let conn = try!(Connection::connect(config.server[], config.port)); - Ok(IrcServer::from_connection(config, conn)) + pub fn from_config(config: Config) -> IoResult, BufferedReader>> { + let conn = try!(if config.use_ssl { + Connection::connect_ssl(config.server[], config.port) + } else { + Connection::connect(config.server[], config.port) + }); + Ok(IrcServer { config: config, conn: conn }) } } @@ -139,6 +147,7 @@ mod test { password: String::new(), server: format!("irc.test.net"), port: 6667, + use_ssl: false, channels: vec![format!("#test"), format!("#test2")], options: HashMap::new(), }