From cfdb33d4a3c545787fd3906a8986bb7399480ce0 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Tue, 8 Feb 2022 20:20:52 -0600 Subject: [PATCH] burp: Begin BURP stats client implementation The `burp` module contains an implementation of a BURP stats client. It uses *tokio* for asynchronous network communication with the BURP stats TCP socket. The `ClientConnector` struct follows the builder pattern for specifying connection options, ultimately producing a `Client` struct that manages communication over the socket. BURP uses mutual TLS authentication for all its communication. The client authenticates the server by verifying its certificate using a trusted CA certificate. This certificate is not usually trusted system-wide, but specifically by BURP clients. The server also authenticates the client using a certificate. The official BURP client uses a normal PEM-encoded X.509 certificate and PKCS #8 key, however, the *native-tls* library does not support loading these. As such, the certificate and private key must be encapsulated in a PKCS #12 container. --- Cargo.lock | 522 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 11 + src/burp/client.rs | 301 ++++++++++++++++++++++++++ src/burp/error.rs | 34 +++ src/burp/mod.rs | 10 + 5 files changed, 878 insertions(+) create mode 100644 src/burp/client.rs create mode 100644 src/burp/error.rs create mode 100644 src/burp/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 66edd7d..7057b39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,528 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "burp_exporter" version = "0.1.0" +dependencies = [ + "openssl", + "tokio", + "tokio-native-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "core-foundation" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mio" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tokio" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "once_cell", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23" +dependencies = [ + "lazy_static", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74786ce43333fcf51efe947aed9718fbe46d5c7328ec3f1029e818083966d9aa" +dependencies = [ + "ansi_term", + "lazy_static", + "matchers", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 807509c..f6ae94a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,14 @@ license = "MIT OR Apache-2.0" edition = "2021" [dependencies] +openssl = "^0.10.38" +tokio-native-tls = "^0.3.0" +tracing = "^0.1.30" + +[dependencies.tokio] +version = "^1.16.1" +features = ["io-util", "macros", "net", "rt", "signal"] + +[dependencies.tracing-subscriber] +version = "^0.3.8" +features = ["env-filter"] diff --git a/src/burp/client.rs b/src/burp/client.rs new file mode 100644 index 0000000..0c9d8cf --- /dev/null +++ b/src/burp/client.rs @@ -0,0 +1,301 @@ +//! BURP client +use std::char; +use std::fs::File; +use std::io::prelude::*; +use std::path::Path; +use std::str; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio_native_tls::native_tls::TlsConnector as NativeTlsConnector; +use tokio_native_tls::native_tls::{Certificate, Identity, Protocol}; +use tokio_native_tls::TlsConnector; +use tokio_native_tls::TlsStream; +use tracing::{debug, info, trace}; + +use super::error::Error; + +/// The BURP client version emulated by this implementation +const CLIENT_VERSION: &str = "2.1.32"; + +#[doc(hidden)] +macro_rules! unexpected_response { + ($msg:ident) => { + Err(Error::Protocol(format!( + "Unexpected response from server: {}", + $msg, + ))) + }; +} + +/// Specify client connection options using the builder pattern +/// +/// To connect to a BURP server, create a [`ClientConnector`] and use +/// its methods to specify connection options. After all options have +/// been set, call [`ClientConnector::connect`] to initiate the +/// connection. +/// +/// ## Example +/// +/// ```rust +/// let mut client = ClientConnector::new("::1") +/// .port(49720) +/// .server_name("burp.example.org") +/// .ca_cert(cert) +/// .connect() +/// .await?; +/// ``` +pub struct ClientConnector { + host: String, + port: u16, + server_name: Option, + ca_cert: Option, + identity: Option, +} + +impl ClientConnector { + /// Create a new client connector + /// + /// `host` is the string hostname or IP address of the server + pub fn new(host: impl Into) -> Self { + ClientConnector { + host: host.into(), + port: 4972, + server_name: None, + ca_cert: None, + identity: None, + } + } + + /// Set the server port + pub fn port(mut self, port: u16) -> Self { + self.port = port; + self + } + + /// Set the CA certificate used to verify the server's certificate + pub fn ca_cert(mut self, cert: Certificate) -> Self { + self.ca_cert = Some(cert); + self + } + + /// Set the CA certificate from a file + /// + /// This method may return an error if the certificate could not be + /// loaded from the specified file. + pub fn ca_cert_file(self, path: impl AsRef) -> Result { + let mut f = File::open(path)?; + let mut buf = Vec::new(); + f.read_to_end(&mut buf)?; + Ok(self.ca_cert(Certificate::from_pem(&buf)?)) + } + + /// Set the certificate and private key for client authentication + pub fn identity(mut self, identity: Identity) -> Self { + self.identity = Some(identity); + self + } + + /// Set the certificate and private key from a file + /// + /// This method may return an error if the certificate or private + /// key could not be loaded from the specified file, or the file is + /// not a valid PKCS#12 container. + pub fn identity_file( + self, + path: impl AsRef, + password: &str, + ) -> Result { + let mut f = File::open(path)?; + let mut buf = Vec::new(); + f.read_to_end(&mut buf)?; + Ok(self.identity(Identity::from_pkcs12(&buf, password)?)) + } + + /// Set the name of the server to use for certificate validation + /// + /// If the name used to connect to the server does not match any of + /// the names on its certificate, this method can be used to specify + /// the expected name. + pub fn server_name(mut self, name: impl Into) -> Self { + self.server_name = Some(name.into()); + self + } + + /// Connect to the server + /// + /// Returns an [`Error`] if the connection could not be established + /// for any reason (e.g. name does not resolve, address is + /// unreachable, TLS handshake fails, etc.) + pub async fn connect(self) -> Result { + let stream = + TcpStream::connect((self.host.as_ref(), self.port)).await?; + info!( + "Successfully connected to {}, port {}", + &self.host, self.port + ); + let mut builder = NativeTlsConnector::builder(); + builder.min_protocol_version(Some(Protocol::Tlsv12)); + if let Some(ca_cert) = self.ca_cert { + builder.add_root_certificate(ca_cert); + } + if let Some(identity) = self.identity { + builder.identity(identity); + } + let conn: TlsConnector = builder.build()?.into(); + let socket = conn + .connect(&self.server_name.unwrap_or(self.host), stream) + .await?; + Ok(Client { socket }) + } +} + +/// BURP stats client +pub struct Client { + socket: TlsStream, +} + +impl Client { + /// Perform the initial handshake and authentication + /// + /// * `name`: The BURP client name (must match certificate) + /// * `password`: (Optional) The client password; use `None` to + /// avoid sending a password to the server + /// + /// If the handshake fails, [`Error`] is returned. + pub async fn handshake( + &mut self, + name: &str, + password: Option<&str>, + ) -> Result<(), Error> { + let res = self + .command('c', &format!("hello:{}", CLIENT_VERSION)) + .await?; + let mut parts = res.split(':'); + if let Some(msg) = parts.next() { + if msg != "whoareyou" { + return unexpected_response!(res); + } + if let Some(version) = parts.next() { + info!("Server version: {}", version); + if version.is_empty() { + return Err(Error::UnsupportedVersion( + "Unknown server version".into(), + )); + } else if !version.starts_with('2') { + return Err(Error::UnsupportedVersion(format!( + "Unsupported server version: {}", + version + ))); + } + } + } else { + return unexpected_response!(res); + } + let res = self.command('c', name).await?; + if res != "okpassword" { + return unexpected_response!(res); + } + if let Some(password) = password { + let res = self.command('c', password).await?; + if res != "ok" { + return unexpected_response!(res); + } + } + let res = self.command('c', "nocsr").await?; + if res != "nocsr ok" { + return unexpected_response!(res); + } + let res = self.command('c', "extra_comms_begin").await?; + let mut parts = res.split_terminator(':'); + if let Some(msg) = parts.next() { + if msg != "extra_comms_begin ok" { + return unexpected_response!(msg); + } + } else { + return unexpected_response!(res); + } + let capabilities: Vec<&str> = parts.collect(); + info!("Server capabilities: {:?}", capabilities); + for capability in capabilities { + match capability { + "counters_json" => { + self.send_command('c', "counters_json ok").await? + } + "uname" => self.send_command('c', "uname=Linux").await?, + "msg" => self.send_command('c', "msg").await?, + _ => {} + } + } + let res = self.command('c', "extra_comms_end").await?; + if res != "extra_comms_end ok" { + return unexpected_response!(res); + } + Ok(()) + } + + /// Send a command to the server and wait for a response + /// + /// * `command`: BURP command (usually `c`) + /// * `data`: Data to send with the command, if applicable + /// + /// If the server returns a successful (`c`) response, its data + /// will be returned as a string. If it returns an error (`e`) + /// response, its data will be returned in an [`Error`]. + pub async fn command( + &mut self, + command: char, + data: &str, + ) -> Result { + self.send_command(command, data).await?; + self.read_response().await + } + + /// Send a command to the server + /// + /// * `command`: BURP command (usually `c`) + /// * `data`: Data to send with the command, if applicable + /// + /// To read the response from the server, use [`read_response`]. + /// See also [`command`], which sends a command and then waits for + /// a response automatically. + pub async fn send_command( + &mut self, + command: char, + data: &str, + ) -> Result<(), Error> { + debug!("Sending command {} {}", &command, &data); + let msg = format!("{}{:>04X}{}\0", &command, data.len() + 1, &data); + trace!("Sending {:?}", msg); + self.socket.write_all(msg.as_bytes()).await?; + Ok(()) + } + + /// Read a response from the server + /// + /// This method should be called after sending a command that + /// requires a response from the server. If the server returns + /// a successful (`c`) response, its data will be returned as a + /// string. If it returns an error (`e`) response, its data will be + /// returned in an [`Error`]. + pub async fn read_response(&mut self) -> Result { + let mut buf = [0; 5]; + self.socket.read_exact(&mut buf).await?; + trace!("Got response header {:?}", str::from_utf8(&buf)); + let msgtype = buf[0]; + let sz = usize::from_str_radix(str::from_utf8(&buf[1..]).unwrap(), 16) + .unwrap(); + trace!("Response body length: {} bytes", sz); + let mut buf = vec![0; sz]; + self.socket.read_exact(&mut buf).await?; + let response = String::from_utf8(buf).unwrap(); + debug!("Server response: {} {}", char::from(msgtype), &response); + match msgtype { + b'c' => Ok(response), + b'e' => Err(Error::Command(response)), + _ => { + panic!("don't know what to do with {:?}", response); + } + } + } +} diff --git a/src/burp/error.rs b/src/burp/error.rs new file mode 100644 index 0000000..5498d91 --- /dev/null +++ b/src/burp/error.rs @@ -0,0 +1,34 @@ +//! Client error handling +use std::io; +use tokio_native_tls::native_tls; + +/// BURP client errors +/// +/// Most functions and methods in this module return this error type. +/// Depending on the cause of the error, a more specific error type may +/// be encapsulated. +#[derive(Debug)] +pub enum Error { + /// An I/O (e.g. file, network) error has occurred + Io(io::Error), + /// A TLS error has occurred + Tls(native_tls::Error), + /// The BURP server returned an error response to a command + Command(String), + /// The BURP server did not return the expected response + Protocol(String), + /// The version of the BURP server is not supported + UnsupportedVersion(String), +} + +impl From for Error { + fn from(error: io::Error) -> Self { + Self::Io(error) + } +} + +impl From for Error { + fn from(error: native_tls::Error) -> Self { + Self::Tls(error) + } +} diff --git a/src/burp/mod.rs b/src/burp/mod.rs new file mode 100644 index 0000000..a5e6a34 --- /dev/null +++ b/src/burp/mod.rs @@ -0,0 +1,10 @@ +//! BURP Stats Client +//! +//! This module provides a client implementation for the BURP stats +//! socket protocol. It can be used to connect to the stats socket of a +//! BURP server and retrieve statistics about BURP clients and their +//! backups. + +#![warn(missing_docs)] +pub mod client; +pub mod error;