protocol: add connector

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2019-03-15 12:33:51 +01:00
parent 812dd935ce
commit 41310cb96e
3 changed files with 230 additions and 0 deletions

View File

@ -16,3 +16,5 @@ endian_trait = "0.6"
failure = "0.1"
libc = "0.2"
openssl = "0.10"
serde_json = "1.0"
url = "1.7"

View File

@ -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<Authentication>,
certificate_validation: bool,
}
fn build_login(host: &str, user: &str, pass: &str) -> Vec<u8> {
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<u8> {
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<T: Read>(sock: T) -> Result<(u16, Vec<u8>), 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<u32> = 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<SslStream<TcpStream>, 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<Client<SslStream<TcpStream>>, Error> {
let stream = self.do_connect()?;
let mut client = Client::new(stream);
if !client.wait_for_handshake()? {
bail!("handshake failed");
}
Ok(client)
}
}

View File

@ -13,6 +13,9 @@ pub use chunker::*;
mod client;
pub use client::*;
mod connect;
pub use connect::*;
mod types;
pub use types::*;