#![cfg_attr(docsrs, feature(doc_cfg))]
use snafu::{Backtrace, OptionExt, ResultExt, Snafu};
use std::collections::HashMap;
use std::fmt::{Debug, Write as FmtWrite};
use std::io::{self, BufRead, BufReader, Write};
use std::net::{Shutdown, TcpStream, ToSocketAddrs};
use std::string::FromUtf8Error;
use std::time::Duration;
mod data;
#[cfg_attr(docsrs, doc(cfg(feature = "managed")))]
#[cfg(feature = "managed")]
pub mod managed;
pub mod raw;
pub use data::*;
use io::Read;
use raw::*;
use std::fmt;
pub enum MessageTarget {
Client(ClientId),
Channel,
Server,
}
impl fmt::Display for MessageTarget {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::Client(id) => write!(f, "targetmode=1 target={}", id),
Self::Channel => write!(f, "targetmode=2"),
Self::Server => write!(f, "targetmode=3"),
}
}
}
#[derive(Snafu, Debug)]
pub enum Ts3Error {
#[snafu(display("Input was invalid UTF-8: {}", source))]
Utf8Error { source: FromUtf8Error },
#[snafu(display("IO Error: {}{}, kind: {:?}", context, source, source.kind()))]
Io {
context: &'static str,
source: io::Error,
},
#[snafu(display("IO Error: Connection closed"))]
ConnectionClosed { backtrace: Backtrace },
#[snafu(display("No valid socket address provided."))]
InvalidSocketAddress { backtrace: Backtrace },
#[snafu(display("Received invalid response, {}{:?}", context, data))]
InvalidResponse {
context: &'static str,
data: String,
},
#[snafu(display("Got invalid int response {}: {}", data, source))]
InvalidIntResponse {
data: String,
source: std::num::ParseIntError,
backtrace: Backtrace,
},
#[snafu(display("Server responded with error: {}", response))]
ServerError {
response: ErrorResponse,
backtrace: Backtrace,
},
#[snafu(display("Invalid response, DDOS limit reached: {:?}", response))]
ResponseLimit {
response: Vec<String>,
backtrace: Backtrace,
},
#[cfg(feature = "managed")]
#[snafu(display("Invalid name length: {} max: {}!", length, expected))]
InvalidNameLength { length: usize, expected: usize },
#[snafu(display("Expected entry for key {}, key not found!", key))]
NoEntryResponse {
key: &'static str,
backtrace: Backtrace,
},
#[snafu(display("Expected value for key {}, got none!", key))]
NoValueResponse {
key: &'static str,
backtrace: Backtrace,
},
}
impl Ts3Error {
pub fn is_error_response(&self) -> bool {
match self {
Ts3Error::ServerError { .. } => true,
_ => false,
}
}
pub fn error_response(&self) -> Option<&ErrorResponse> {
match self {
Ts3Error::ServerError { response, .. } => Some(response),
_ => None,
}
}
}
impl From<io::Error> for Ts3Error {
fn from(error: io::Error) -> Self {
Ts3Error::Io {
context: "",
source: error,
}
}
}
pub struct QueryClient {
rx: BufReader<TcpStream>,
tx: TcpStream,
limit_lines: usize,
limit_lines_bytes: u64,
}
pub const LIMIT_READ_LINES: usize = 100;
pub const LIMIT_LINE_BYTES: u64 = 64_000;
type Result<T> = ::std::result::Result<T, Ts3Error>;
impl Drop for QueryClient {
fn drop(&mut self) {
#[allow(unused_variables)]
if let Err(e) = self.quit() {
#[cfg(feature = "debug_response")]
eprintln!("Can't quit on drop: {}", e);
}
let _ = self.tx.shutdown(Shutdown::Both);
}
}
impl QueryClient {
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<Self> {
let (rx, tx) = Self::new_inner(addr, None, None)?;
Ok(Self {
rx,
tx,
limit_lines: LIMIT_READ_LINES,
limit_lines_bytes: LIMIT_LINE_BYTES,
})
}
pub fn with_timeout<A: ToSocketAddrs>(
addr: A,
t_connect: Option<Duration>,
timeout: Option<Duration>,
) -> Result<Self> {
let (rx, tx) = Self::new_inner(addr, timeout, t_connect)?;
Ok(Self {
rx,
tx,
limit_lines: LIMIT_READ_LINES,
limit_lines_bytes: LIMIT_LINE_BYTES,
})
}
pub fn limit_lines(&mut self, limit: usize) {
self.limit_lines = limit;
}
pub fn limit_line_bytes(&mut self, limit: u64) {
self.limit_lines_bytes = limit;
}
pub fn rename<T: AsRef<str>>(&mut self, name: T) -> Result<()> {
writeln!(
&mut self.tx,
"clientupdate client_nickname={}",
escape_arg(name)
)?;
let _ = self.read_response()?;
Ok(())
}
pub fn rename_channel<T: AsRef<str>>(&mut self, channel: ChannelId, name: T) -> Result<()> {
writeln!(
&mut self.tx,
"channeledit cid={} channel_name={}",
channel,escape_arg(name)
)?;
let _ = self.read_response()?;
Ok(())
}
pub fn update_description<T: AsRef<str>>(
&mut self,
descr: T,
target: Option<ClientId>,
) -> Result<()> {
if let Some(clid) = target {
writeln!(
&mut self.tx,
"clientedit clid={} CLIENT_DESCRIPTION={}",
clid,
escape_arg(descr)
)?;
} else {
writeln!(
&mut self.tx,
"clientupdate CLIENT_DESCRIPTION={}",
escape_arg(descr)
)?;
}
let _ = self.read_response()?;
Ok(())
}
pub fn poke_client<T: AsRef<str>>(&mut self, client: ClientId, msg: T) -> Result<()> {
writeln!(
&mut self.tx,
"clientpoke clid={} msg={}",
client,
msg.as_ref()
)?;
let _ = self.read_response()?;
Ok(())
}
pub fn send_message<T: AsRef<str>>(&mut self, target: MessageTarget, msg: T) -> Result<()> {
writeln!(
&mut self.tx,
"sendtextmessage {} msg={}",
target,
escape_arg(msg)
)?;
let _ = self.read_response()?;
Ok(())
}
fn quit(&mut self) -> Result<()> {
writeln!(&mut self.tx, "quit")?;
let _ = self.read_response()?;
Ok(())
}
fn new_inner<A: ToSocketAddrs>(
addr: A,
timeout: Option<Duration>,
conn_timeout: Option<Duration>,
) -> Result<(BufReader<TcpStream>, TcpStream)> {
let addr = addr
.to_socket_addrs()
.context(Io {
context: "invalid socket address",
})?
.next()
.context(InvalidSocketAddress {})?;
let stream = if let Some(dur) = conn_timeout {
TcpStream::connect_timeout(&addr, dur).context(Io {
context: "while connecting: ",
})?
} else {
TcpStream::connect(addr).context(Io {
context: "while connecting: ",
})?
};
stream.set_write_timeout(timeout).context(Io {
context: "setting write timeout: ",
})?;
stream.set_read_timeout(timeout).context(Io {
context: "setting read timeout: ",
})?;
stream.set_nodelay(true).context(Io {
context: "setting nodelay: ",
})?;
let mut reader = BufReader::new(stream.try_clone().context(Io {
context: "splitting connection: ",
})?);
let mut buffer = Vec::new();
reader.read_until(b'\r', &mut buffer).context(Io {
context: "reading response: ",
})?;
buffer.clear();
if let Err(e) = reader.read_until(b'\r', &mut buffer) {
use std::io::ErrorKind::*;
match e.kind() {
TimedOut | WouldBlock => (), _ => return Err(e.into()),
}
}
Ok((reader, stream))
}
pub fn raw_command<T: AsRef<str>>(&mut self, command: T) -> Result<Vec<String>> {
writeln!(&mut self.tx, "{}", command.as_ref())?;
let v = self.read_response()?;
Ok(v)
}
pub fn whoami(&mut self, unescape: bool) -> Result<HashMap<String, Option<String>>> {
writeln!(&mut self.tx, "whoami")?;
let v = self.read_response()?;
Ok(parse_hashmap(v, unescape))
}
pub fn logout(&mut self) -> Result<()> {
writeln!(&mut self.tx, "logout")?;
let _ = self.read_response()?;
Ok(())
}
pub fn login<T: AsRef<str>, S: AsRef<str>>(&mut self, user: T, password: S) -> Result<()> {
writeln!(
&mut self.tx,
"login {} {}",
escape_arg(user),
escape_arg(password)
)?;
let _ = self.read_response()?;
Ok(())
}
pub fn select_server_by_port(&mut self, port: u16) -> Result<()> {
writeln!(&mut self.tx, "use port={}", port)?;
let _ = self.read_response()?;
Ok(())
}
pub fn move_client(
&mut self,
client: ClientId,
channel: ChannelId,
password: Option<&str>,
) -> Result<()> {
let pw_arg = if let Some(pw) = password {
format!("cpw={}", raw::escape_arg(pw).as_str())
} else {
String::new()
};
writeln!(
&mut self.tx,
"clientmove clid={} cid={} {}",
client, channel, pw_arg
)?;
let _ = self.read_response()?;
Ok(())
}
pub fn kick_client(
&mut self,
client: ClientId,
server: bool,
message: Option<&str>,
) -> Result<()> {
let msg_arg = if let Some(pw) = message {
format!("reasonmsg={}", raw::escape_arg(pw).as_str())
} else {
String::new()
};
let rid = if server { 5 } else { 4 };
writeln!(
&mut self.tx,
"clientkick clid={} reasonid={} {}",
client, rid, msg_arg
)?;
let _ = self.read_response()?;
Ok(())
}
pub fn create_dir<T: AsRef<str>>(&mut self, channel: ChannelId, path: T) -> Result<()> {
writeln!(
&mut self.tx,
"ftcreatedir cid={} cpw= dirname={}",
channel,
escape_arg(path)
)?;
let _ = self.read_response()?;
Ok(())
}
pub fn delete_file<T: AsRef<str>>(&mut self, channel: ChannelId, path: T) -> Result<()> {
writeln!(
&mut self.tx,
"ftdeletefile cid={} cpw= name={}",
channel,
escape_arg(path)
)?;
let _ = self.read_response()?;
Ok(())
}
pub fn ping(&mut self) -> Result<()> {
writeln!(&mut self.tx, "whoami")?;
let _ = self.read_response()?;
Ok(())
}
pub fn select_server_by_id(&mut self, sid: ServerId) -> Result<()> {
writeln!(&mut self.tx, "use sid={}", sid)?;
let _ = self.read_response()?;
Ok(())
}
pub fn server_group_del_clients(
&mut self,
group: ServerGroupID,
cldbid: &[usize],
) -> Result<()> {
if cldbid.is_empty() {
return Ok(());
}
writeln!(
&mut self.tx,
"servergroupdelclient sgid={} {}",
group,
Self::format_cldbids(cldbid)
)?;
let _ = self.read_response()?;
Ok(())
}
pub fn server_group_add_clients(
&mut self,
group: ServerGroupID,
cldbid: &[usize],
) -> Result<()> {
if cldbid.is_empty() {
return Ok(());
}
let v = Self::format_cldbids(cldbid);
writeln!(&mut self.tx, "servergroupaddclient sgid={} {}", group, v)?;
let _ = self.read_response()?;
Ok(())
}
fn format_cldbids(it: &[usize]) -> String {
let mut res = String::new();
let mut it = it.iter();
if let Some(n) = it.next() {
write!(res, "cldbid={}", n).unwrap();
}
for n in it {
write!(res, "|cldbid={}", n).unwrap();
}
res
}
fn read_response(&mut self) -> Result<Vec<String>> {
let mut result: Vec<String> = Vec::new();
let mut lr = (&mut self.rx).take(self.limit_lines_bytes);
for _ in 0..self.limit_lines {
let mut buffer = Vec::new();
if lr.read_until(b'\r', &mut buffer).context(Io {
context: "reading response: ",
})? == 0
{
return ConnectionClosed {}.fail();
}
if buffer.ends_with(&[b'\r']) {
buffer.pop();
if buffer.ends_with(&[b'\n']) {
buffer.pop();
}
} else if lr.limit() == 0 {
return ResponseLimit { response: result }.fail();
} else {
return InvalidResponse {
context: "expected \\r delimiter, got: ",
data: String::from_utf8_lossy(&buffer),
}
.fail();
}
if !buffer.is_empty() {
let line = String::from_utf8(buffer).context(Utf8Error)?;
#[cfg(feature = "debug_response")]
println!("Read: {:?}", &line);
if line.starts_with("error ") {
Self::check_ok(&line)?;
return Ok(result);
}
result.push(line);
}
lr.set_limit(LIMIT_LINE_BYTES);
}
ResponseLimit { response: result }.fail()
}
pub fn online_clients_full(&mut self) -> Result<Vec<OnlineClientFull>> {
writeln!(
&mut self.tx,
"clientlist -uid -away -voice -times -groups -info -country -ip -badges"
)?;
let res = self.read_response()?;
let clients = raw::parse_multi_hashmap(res, false)
.into_iter()
.map(|v| Ok(OnlineClientFull::from_raw(v)?))
.collect::<Result<_>>()?;
Ok(clients)
}
pub fn online_clients(&mut self) -> Result<Vec<OnlineClient>> {
writeln!(&mut self.tx, "clientlist")?;
let res = self.read_response()?;
let clients = raw::parse_multi_hashmap(res, false)
.into_iter()
.map(|v| Ok(OnlineClient::from_raw(v)?))
.collect::<Result<_>>()?;
Ok(clients)
}
pub fn channels(&mut self) -> Result<Vec<Channel>> {
writeln!(&mut self.tx, "channellist")?;
let res = self.read_response()?;
let channels = raw::parse_multi_hashmap(res, false)
.into_iter()
.map(|v| Ok(Channel::from_raw(v)?))
.collect::<Result<_>>()?;
Ok(channels)
}
pub fn channels_full(&mut self) -> Result<Vec<ChannelFull>> {
writeln!(
&mut self.tx,
"channellist -topic -flags -voice -limits -icon -secondsempty"
)?;
let res = self.read_response()?;
let channels = raw::parse_multi_hashmap(res, false)
.into_iter()
.map(|v| Ok(ChannelFull::from_raw(v)?))
.collect::<Result<_>>()?;
Ok(channels)
}
pub fn delete_channel(&mut self, id: ChannelId, force: bool) -> Result<()> {
writeln!(
&mut self.tx,
"channeldelete cid={} force={}",
id,
if force { 1 } else { 0 }
)?;
let _ = self.read_response()?;
Ok(())
}
pub fn create_channel(&mut self, channel: &ChannelEdit) -> Result<ChannelId> {
writeln!(&mut self.tx, "channelcreate{}", &channel.to_raw())?;
let res = self.read_response()?;
let mut response = raw::parse_hashmap(res, false);
let cid = int_val_parser(&mut response, "cid")?;
Ok(cid)
}
pub fn server_groups(&mut self) -> Result<Vec<ServerGroup>> {
writeln!(&mut self.tx, "servergrouplist")?;
let res = self.read_response()?;
let groups = raw::parse_multi_hashmap(res, false)
.into_iter()
.map(|v| Ok(ServerGroup::from_raw(v)?))
.collect::<Result<_>>()?;
Ok(groups)
}
pub fn servergroup_client_cldbids(&mut self, group: ServerGroupID) -> Result<Vec<usize>> {
writeln!(&mut self.tx, "servergroupclientlist sgid={}", group)?;
let resp = self.read_response()?;
if let Some(line) = resp.get(0) {
let data: Vec<usize> = line
.split('|')
.map(|e| {
if let Some(cldbid) = e.split('=').collect::<Vec<_>>().get(1) {
Ok(cldbid
.parse::<usize>()
.map_err(|_| Ts3Error::InvalidResponse {
context: "expected usize, got ",
data: line.to_string(),
})?)
} else {
Err(Ts3Error::InvalidResponse {
context: "expected data of cldbid=1, got ",
data: line.to_string(),
})
}
})
.collect::<Result<Vec<usize>>>()?;
Ok(data)
} else {
Ok(Vec::new())
}
}
fn check_ok(msg: &str) -> Result<()> {
let result: Vec<&str> = msg.split(' ').collect();
#[cfg(debug)]
{
assert_eq!(
"check_ok invoked on non-error line",
result.get(0),
Some(&"error")
);
}
if let (Some(id), Some(msg)) = (result.get(1), result.get(2)) {
let split_id: Vec<&str> = id.split('=').collect();
let split_msg: Vec<&str> = msg.split('=').collect();
if let (Some(id), Some(msg)) = (split_id.get(1), split_msg.get(1)) {
let id = id.parse::<usize>().map_err(|_| Ts3Error::InvalidResponse {
context: "expected usize, got ",
data: (*msg).to_string(), })?;
if id != 0 {
return ServerError {
response: ErrorResponse {
id,
msg: unescape_val(*msg),
},
}
.fail();
} else {
return Ok(());
}
}
}
Err(Ts3Error::InvalidResponse {
context: "expected id and msg, got ",
data: msg.to_string(),
})
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_format_cldbids() {
let ids = vec![0, 1, 2, 3];
assert_eq!(
"cldbid=0|cldbid=1|cldbid=2|cldbid=3",
QueryClient::format_cldbids(&ids)
);
assert_eq!("", QueryClient::format_cldbids(&[]));
assert_eq!("cldbid=0", QueryClient::format_cldbids(&ids[0..1]));
}
}