protocol: add connector
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
812dd935ce
commit
41310cb96e
@ -16,3 +16,5 @@ endian_trait = "0.6"
|
||||
failure = "0.1"
|
||||
libc = "0.2"
|
||||
openssl = "0.10"
|
||||
serde_json = "1.0"
|
||||
url = "1.7"
|
||||
|
225
proxmox-protocol/src/connect.rs
Normal file
225
proxmox-protocol/src/connect.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -13,6 +13,9 @@ pub use chunker::*;
|
||||
mod client;
|
||||
pub use client::*;
|
||||
|
||||
mod connect;
|
||||
pub use connect::*;
|
||||
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user