diff --git a/proxmox-protocol/Cargo.toml b/proxmox-protocol/Cargo.toml index 8ae08a86..fe3d6ab8 100644 --- a/proxmox-protocol/Cargo.toml +++ b/proxmox-protocol/Cargo.toml @@ -16,3 +16,5 @@ endian_trait = "0.6" failure = "0.1" libc = "0.2" openssl = "0.10" +serde_json = "1.0" +url = "1.7" diff --git a/proxmox-protocol/src/connect.rs b/proxmox-protocol/src/connect.rs new file mode 100644 index 00000000..37b653b2 --- /dev/null +++ b/proxmox-protocol/src/connect.rs @@ -0,0 +1,225 @@ +//! This module provides a `Connector` used to log into a Proxmox Backup API server and connect to +//! the proxmox protocol via an HTTP Upgrade request. + +use std::io::{Read, Write}; +use std::net::TcpStream; + +use failure::{bail, format_err, Error}; +use openssl::ssl::{self, SslStream}; +use url::form_urlencoded; + +use crate::Client; + +enum Authentication { + Password(String), + Ticket(String, String), +} + +/// Connector used to log into a Proxmox Backup API server and open a backup protocol connection. +/// If successful, this will create a `Client` used to communicate over the Proxmox Backup +/// Protocol. +pub struct Connector { + user: String, + server: String, + store: String, + auth: Option, + certificate_validation: bool, +} + +fn build_login(host: &str, user: &str, pass: &str) -> Vec { + let formdata = form_urlencoded::Serializer::new(String::new()) + .append_pair("username", user) + .append_pair("password", pass) + .finish(); + + format!("\ + POST /api2/json/access/ticket HTTP/1.1\r\n\ + host: {}\r\n\ + content-length: {}\r\n\ + content-type: application/x-www-form-urlencoded\r\n\ + \r\n\ + {}", + host, + formdata.as_bytes().len(), + formdata, + ) + .into_bytes() +} + +fn build_protocol_connect(host: &str, store: &str, ticket: &str, token: &str) -> Vec { + format!("\ + GET /api2/json/admin/datastore/{}/test-upload HTTP/1.1\r\n\ + host: {}\r\n\ + connection: upgrade\r\n\ + upgrade: proxmox-backup-protocol-1\r\n\ + cookie: PBSAuthCookie={}\r\n\ + CSRFPreventionToken: {}\r\n\ + \r\n", + store, + host, + ticket, + token, + ) + .into_bytes() +} + +// Minimalistic http response reader. The only things we care about here are the status code and +// the payload... +fn read_http_response(sock: T) -> Result<(u16, Vec), Error> { + use std::io::BufRead; + let mut reader = std::io::BufReader::new(sock); + + let mut status = String::new(); + reader.read_line(&mut status)?; + + let status = status.trim_end(); + let mut parts = status.splitn(3, ' '); + let _version = parts + .next() + .ok_or_else(|| format_err!("bad http response (missing version)"))?; + let code = parts + .next() + .ok_or_else(|| format_err!("bad http response (missing status code)"))?; + let _reason = parts.next(); + + let code: u16 = code.parse()?; + + // We need the payload's length if there is one: + let mut length: Option = None; + let mut line = String::new(); + loop { + line.clear(); + reader.read_line(&mut line)?; + let line = line.trim_end(); + + if line.len() == 0 { + break; + } + + let parts: Vec<&str> = line.splitn(2, ':').collect(); + if parts.len() != 2 { + bail!("invalid header in http response"); + } + + let name = parts[0].trim().to_lowercase().to_string(); + let value = parts[1].trim(); + + // The only important header (important to know how much we need to read!) + if name == "content-length" { + length = Some(value.parse()?); + } + + // Don't care about any other header contents currently... + } + + match length { + None => Ok((code, Vec::new())), + Some(length) => { + let length = length as usize; + + let mut out = Vec::with_capacity(length); + unsafe { + out.set_len(length); + } + + reader.read_exact(&mut out)?; + Ok((code, out)) + }, + } +} + +fn parse_login_response(data: &[u8]) -> Result<(String, String), Error> { + let value: serde_json::Value = serde_json::from_slice(data)?; + let ticket = value["data"]["ticket"] + .as_str() + .ok_or_else(|| format_err!("no ticket found in login response"))? + .to_string(); + let token = value["data"]["CSRFPreventionToken"] + .as_str() + .ok_or_else(|| format_err!("no token found in login response"))? + .to_string(); + Ok((ticket, token)) +} + +impl Connector { + /// Create a new connector for a specified user, server and remote backup store. + pub fn new(user: String, server: String, store: String) -> Self { + Self { + user, + server, + store, + auth: None, + certificate_validation: true, + } + } + + /// Use a password to authenticate with the remote server. + pub fn password(mut self, pass: String) -> Self { + self.auth = Some(Authentication::Password(pass)); + self + } + + /// Use an already existing ticket to connect to the server. + pub fn ticket(mut self, ticket: String, token: String) -> Self { + self.auth = Some(Authentication::Ticket(ticket, token)); + self + } + + /// Disable TLS certificate validation. + pub fn certificate_validation(mut self, on: bool) -> Self { + self.certificate_validation = on; + self + } + + pub(crate) fn do_connect(self) -> Result, Error> { + if self.auth.is_none() { + bail!("missing authentication"); + } + + let stream = TcpStream::connect(&self.server)?; + + let mut connector = ssl::SslConnector::builder(ssl::SslMethod::tls())?; + if !self.certificate_validation { + connector.set_verify(ssl::SslVerifyMode::NONE); + } + let connector = connector.build(); + + let mut stream = connector.connect(&self.server, stream)?; + let (ticket, token) = match self.auth { + None => unreachable!(), // checked above + Some(Authentication::Password(password)) => { + let login_request = build_login(&self.server, &self.user, &password); + stream.write_all(&login_request)?; + + let (code, ticket) = read_http_response(&mut stream)?; + if code != 200 { + bail!("login failed"); + } + + parse_login_response(&ticket)? + } + Some(Authentication::Ticket(ticket, token)) => (ticket, token), + }; + + let protocol_request = build_protocol_connect(&self.server, &self.store, &ticket, &token); + stream.write_all(&protocol_request)?; + let (code, _empty_body) = read_http_response(&mut stream)?; + if code != 101 { + bail!("expected 101 Switching Protocol, received code: {}", code); + } + + Ok(stream) + } + + /// This creates creates a synchronous client (via blocking I/O), tries to authenticate with + /// the server and connect to the protocol endpoint. On success, a `Client` is returned. + pub fn connect(self) -> Result>, Error> { + let stream = self.do_connect()?; + + let mut client = Client::new(stream); + if !client.wait_for_handshake()? { + bail!("handshake failed"); + } + Ok(client) + } +} diff --git a/proxmox-protocol/src/lib.rs b/proxmox-protocol/src/lib.rs index 2e571d4a..4664a5ce 100644 --- a/proxmox-protocol/src/lib.rs +++ b/proxmox-protocol/src/lib.rs @@ -13,6 +13,9 @@ pub use chunker::*; mod client; pub use client::*; +mod connect; +pub use connect::*; + mod types; pub use types::*;