api::error::HttpError,
sys::linux::tty,
tools::fs::{file_get_json, replace_file, CreateOptions},
- tools::future::TimeoutFutureExt,
};
+use proxmox_http::client::HttpsConnector;
+use proxmox_http::uri::build_authority;
+
+use pbs_api_types::{Authid, Userid};
+use pbs_tools::broadcast_future::BroadcastFuture;
+use pbs_tools::json::json_object_to_query;
+use pbs_tools::ticket;
+use pbs_tools::percent_encoding::DEFAULT_ENCODE_SET;
+
use super::pipe_to_stream::PipeToSendStream;
-use crate::api2::types::{Authid, Userid};
-use crate::tools::{
- self,
- BroadcastFuture,
- DEFAULT_ENCODE_SET,
- http::HttpsConnector,
-};
+use super::PROXMOX_BACKUP_TCP_KEEPALIVE_TIME;
/// Timeout used for several HTTP operations that are expected to finish quickly but may block in
-/// certain error conditions.
-const HTTP_TIMEOUT: Duration = Duration::from_secs(20);
+/// certain error conditions. Keep it generous, to avoid false-positive under high load.
+const HTTP_TIMEOUT: Duration = Duration::from_secs(2 * 60);
#[derive(Clone)]
pub struct AuthInfo {
impl HttpClientOptions {
- pub fn new() -> Self {
+ pub fn new_interactive(password: Option<String>, fingerprint: Option<String>) -> Self {
Self {
- prefix: None,
- password: None,
- fingerprint: None,
- interactive: false,
- ticket_cache: false,
- fingerprint_cache: false,
- verify_cert: true,
+ password,
+ fingerprint,
+ fingerprint_cache: true,
+ ticket_cache: true,
+ interactive: true,
+ prefix: Some("proxmox-backup".to_string()),
+ ..Self::default()
+ }
+ }
+
+ pub fn new_non_interactive(password: String, fingerprint: Option<String>) -> Self {
+ Self {
+ password: Some(password),
+ fingerprint,
+ ..Self::default()
}
}
}
}
+impl Default for HttpClientOptions {
+ fn default() -> Self {
+ Self {
+ prefix: None,
+ password: None,
+ fingerprint: None,
+ interactive: false,
+ ticket_cache: false,
+ fingerprint_cache: false,
+ verify_cert: true,
+ }
+ }
+}
+
/// HTTP(S) API client
pub struct HttpClient {
client: Client<HttpsConnector>,
raw.split('\n').for_each(|line| {
let items: Vec<String> = line.split_whitespace().map(String::from).collect();
if items.len() == 2 {
- if &items[0] == server {
+ if items[0] == server {
// found, add later with new fingerprint
} else {
result.push_str(line);
for line in raw.split('\n') {
let items: Vec<String> = line.split_whitespace().map(String::from).collect();
- if items.len() == 2 && &items[0] == server {
+ if items.len() == 2 && items[0] == server {
return Some(items[1].clone());
}
}
let mut new_data = json!({});
- let ticket_lifetime = tools::ticket::TICKET_LIFETIME - 60;
+ let ticket_lifetime = ticket::TICKET_LIFETIME - 60;
let empty = serde_json::map::Map::new();
for (server, info) in data.as_object().unwrap_or(&empty) {
let path = base.place_runtime_file("tickets").ok()?;
let data = file_get_json(&path, None).ok()?;
let now = proxmox::tools::time::epoch_i64();
- let ticket_lifetime = tools::ticket::TICKET_LIFETIME - 60;
+ let ticket_lifetime = ticket::TICKET_LIFETIME - 60;
let uinfo = data[server][userid.as_str()].as_object()?;
let timestamp = uinfo["timestamp"].as_i64()?;
let age = now - timestamp;
}
}
+fn build_uri(server: &str, port: u16, path: &str, query: Option<String>) -> Result<Uri, Error> {
+ Uri::builder()
+ .scheme("https")
+ .authority(build_authority(server, port)?)
+ .path_and_query(match query {
+ Some(query) => format!("/{}?{}", path, query),
+ None => format!("/{}", path),
+ })
+ .build()
+ .map_err(|err| format_err!("error building uri - {}", err))
+}
+
impl HttpClient {
pub fn new(
server: &str,
let verified_fingerprint = Arc::new(Mutex::new(None));
- let mut fingerprint = options.fingerprint.take();
+ let mut expected_fingerprint = options.fingerprint.take();
- if fingerprint.is_some() {
+ if expected_fingerprint.is_some() {
// do not store fingerprints passed via options in cache
options.fingerprint_cache = false;
} else if options.fingerprint_cache && options.prefix.is_some() {
- fingerprint = load_fingerprint(options.prefix.as_ref().unwrap(), server);
+ expected_fingerprint = load_fingerprint(options.prefix.as_ref().unwrap(), server);
}
let mut ssl_connector_builder = SslConnector::builder(SslMethod::tls()).unwrap();
let fingerprint_cache = options.fingerprint_cache;
let prefix = options.prefix.clone();
ssl_connector_builder.set_verify_callback(openssl::ssl::SslVerifyMode::PEER, move |valid, ctx| {
- let (valid, fingerprint) = Self::verify_callback(valid, ctx, fingerprint.clone(), interactive);
- if valid {
- if let Some(fingerprint) = fingerprint {
+ match Self::verify_callback(valid, ctx, expected_fingerprint.as_ref(), interactive) {
+ Ok(None) => true,
+ Ok(Some(fingerprint)) => {
if fingerprint_cache && prefix.is_some() {
if let Err(err) = store_fingerprint(
prefix.as_ref().unwrap(), &server, &fingerprint) {
}
}
*verified_fingerprint.lock().unwrap() = Some(fingerprint);
- }
+ true
+ },
+ Err(err) => {
+ eprintln!("certificate validation failed - {}", err);
+ false
+ },
}
- valid
});
} else {
ssl_connector_builder.set_verify(openssl::ssl::SslVerifyMode::NONE);
httpc.enforce_http(false); // we want https...
httpc.set_connect_timeout(Some(std::time::Duration::new(10, 0)));
- let https = HttpsConnector::with_connector(httpc, ssl_connector_builder.build());
+ let https = HttpsConnector::with_connector(httpc, ssl_connector_builder.build(), PROXMOX_BACKUP_TCP_KEEPALIVE_TIME);
let client = Client::builder()
//.http2_initial_stream_window_size( (1 << 31) - 2)
};
match Self::credentials(client2.clone(), server2.clone(), port, auth_id.user().clone(), ticket).await {
Ok(auth) => {
- if use_ticket_cache & &prefix2.is_some() {
+ if use_ticket_cache && prefix2.is_some() {
let _ = store_ticket_info(prefix2.as_ref().unwrap(), &server2, &auth.auth_id.to_string(), &auth.ticket, &auth.token);
}
*auth2.write().unwrap() = auth;
server.to_owned(),
port,
auth_id.user().clone(),
- password.to_owned(),
+ password,
).map_ok({
let server = server.to_string();
let prefix = options.prefix.clone();
let authinfo = auth.clone();
move |auth| {
- if use_ticket_cache & &prefix.is_some() {
+ if use_ticket_cache && prefix.is_some() {
let _ = store_ticket_info(prefix.as_ref().unwrap(), &server, &auth.auth_id.to_string(), &auth.ticket, &auth.token);
}
*authinfo.write().unwrap() = auth;
}
fn verify_callback(
- valid: bool, ctx:
- &mut X509StoreContextRef,
- expected_fingerprint: Option<String>,
+ openssl_valid: bool,
+ ctx: &mut X509StoreContextRef,
+ expected_fingerprint: Option<&String>,
interactive: bool,
- ) -> (bool, Option<String>) {
- if valid { return (true, None); }
+ ) -> Result<Option<String>, Error> {
+
+ if openssl_valid {
+ return Ok(None);
+ }
let cert = match ctx.current_cert() {
Some(cert) => cert,
- None => return (false, None),
+ None => bail!("context lacks current certificate."),
};
let depth = ctx.error_depth();
- if depth != 0 { return (false, None); }
+ if depth != 0 { bail!("context depth != 0") }
let fp = match cert.digest(openssl::hash::MessageDigest::sha256()) {
Ok(fp) => fp,
- Err(_) => return (false, None), // should not happen
+ Err(err) => bail!("failed to calculate certificate FP - {}", err), // should not happen
};
let fp_string = proxmox::tools::digest_to_hex(&fp);
let fp_string = fp_string.as_bytes().chunks(2).map(|v| std::str::from_utf8(v).unwrap())
.collect::<Vec<&str>>().join(":");
if let Some(expected_fingerprint) = expected_fingerprint {
- if expected_fingerprint.to_lowercase() == fp_string {
- return (true, Some(fp_string));
+ let expected_fingerprint = expected_fingerprint.to_lowercase();
+ if expected_fingerprint == fp_string {
+ return Ok(Some(fp_string));
} else {
- return (false, None);
+ eprintln!("WARNING: certificate fingerprint does not match expected fingerprint!");
+ eprintln!("expected: {}", expected_fingerprint);
}
}
// If we're on a TTY, query the user
if interactive && tty::stdin_isatty() {
- println!("fingerprint: {}", fp_string);
+ eprintln!("fingerprint: {}", fp_string);
loop {
- print!("Are you sure you want to continue connecting? (y/n): ");
+ eprint!("Are you sure you want to continue connecting? (y/n): ");
let _ = std::io::stdout().flush();
use std::io::{BufRead, BufReader};
let mut line = String::new();
Ok(_) => {
let trimmed = line.trim();
if trimmed == "y" || trimmed == "Y" {
- return (true, Some(fp_string));
+ return Ok(Some(fp_string));
} else if trimmed == "n" || trimmed == "N" {
- return (false, None);
+ bail!("Certificate fingerprint was not confirmed.");
} else {
continue;
}
}
- Err(_) => return (false, None),
+ Err(err) => bail!("Certificate fingerprint was not confirmed - {}.", err),
}
}
}
- (false, None)
+
+ bail!("Certificate fingerprint was not confirmed.");
}
pub async fn request(&self, mut req: Request<Body>) -> Result<Value, Error> {
let enc_ticket = format!("PBSAuthCookie={}", percent_encode(auth.ticket.as_bytes(), DEFAULT_ENCODE_SET));
req.headers_mut().insert("Cookie", HeaderValue::from_str(&enc_ticket).unwrap());
- let resp = client
- .request(req)
- .or_timeout_err(HTTP_TIMEOUT, format_err!("http download request timed out"))
- .await?;
+ let resp = tokio::time::timeout(
+ HTTP_TIMEOUT,
+ client.request(req)
+ )
+ .await
+ .map_err(|_| format_err!("http download request timed out"))??;
let status = resp.status();
if !status.is_success() {
HttpClient::api_response(resp)
data: Option<Value>,
) -> Result<Value, Error> {
- let path = path.trim_matches('/');
- let mut url = format!("https://{}:{}/{}", &self.server, self.port, path);
-
- if let Some(data) = data {
- let query = tools::json_object_to_query(data).unwrap();
- url.push('?');
- url.push_str(&query);
- }
-
- let url: Uri = url.parse().unwrap();
+ let query = match data {
+ Some(data) => Some(json_object_to_query(data)?),
+ None => None,
+ };
+ let url = build_uri(&self.server, self.port, path, query)?;
let req = Request::builder()
.method("POST")
req.headers_mut().insert("UPGRADE", HeaderValue::from_str(&protocol_name).unwrap());
- let resp = client
- .request(req)
- .or_timeout_err(HTTP_TIMEOUT, format_err!("http upgrade request timed out"))
- .await?;
+ let resp = tokio::time::timeout(
+ HTTP_TIMEOUT,
+ client.request(req)
+ )
+ .await
+ .map_err(|_| format_err!("http upgrade request timed out"))??;
let status = resp.status();
if status != http::StatusCode::SWITCHING_PROTOCOLS {
bail!("unknown error");
}
- let upgraded = resp
- .into_body()
- .on_upgrade()
- .await?;
+ let upgraded = hyper::upgrade::on(resp).await?;
let max_window_size = (1 << 31) - 2;
req: Request<Body>
) -> Result<Value, Error> {
- client.request(req)
- .or_timeout_err(HTTP_TIMEOUT, format_err!("http request timed out"))
- .and_then(Self::api_response)
- .await
+ Self::api_response(
+ tokio::time::timeout(
+ HTTP_TIMEOUT,
+ client.request(req)
+ )
+ .await
+ .map_err(|_| format_err!("http request timed out"))??
+ ).await
}
// Read-only access to server property
}
pub fn request_builder(server: &str, port: u16, method: &str, path: &str, data: Option<Value>) -> Result<Request<Body>, Error> {
- let path = path.trim_matches('/');
- let url: Uri = format!("https://{}:{}/{}", server, port, path).parse()?;
-
if let Some(data) = data {
if method == "POST" {
+ let url = build_uri(server, port, path, None)?;
let request = Request::builder()
.method(method)
.uri(url)
.header("User-Agent", "proxmox-backup-client/1.0")
.header(hyper::header::CONTENT_TYPE, "application/json")
.body(Body::from(data.to_string()))?;
- return Ok(request);
+ Ok(request)
} else {
- let query = tools::json_object_to_query(data)?;
- let url: Uri = format!("https://{}:{}/{}?{}", server, port, path, query).parse()?;
+ let query = json_object_to_query(data)?;
+ let url = build_uri(server, port, path, Some(query))?;
let request = Request::builder()
.method(method)
.uri(url)
.header("User-Agent", "proxmox-backup-client/1.0")
.header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(Body::empty())?;
- return Ok(request);
+ Ok(request)
}
- }
-
- let request = Request::builder()
- .method(method)
- .uri(url)
- .header("User-Agent", "proxmox-backup-client/1.0")
- .header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
- .body(Body::empty())?;
+ } else {
+ let url = build_uri(server, port, path, None)?;
+ let request = Request::builder()
+ .method(method)
+ .uri(url)
+ .header("User-Agent", "proxmox-backup-client/1.0")
+ .header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
+ .body(Body::empty())?;
- Ok(request)
+ Ok(request)
+ }
}
}
let path = path.trim_matches('/');
let content_type = content_type.unwrap_or("application/x-www-form-urlencoded");
+ let query = match param {
+ Some(param) => {
+ let query = json_object_to_query(param)?;
+ // We detected problem with hyper around 6000 characters - so we try to keep on the safe side
+ if query.len() > 4096 {
+ bail!("h2 query data too large ({} bytes) - please encode data inside body", query.len());
+ }
+ Some(query)
+ }
+ None => None,
+ };
- if let Some(param) = param {
- let query = tools::json_object_to_query(param)?;
- // We detected problem with hyper around 6000 characters - seo we try to keep on the safe side
- if query.len() > 4096 { bail!("h2 query data too large ({} bytes) - please encode data inside body", query.len()); }
- let url: Uri = format!("https://{}:8007/{}?{}", server, path, query).parse()?;
- let request = Request::builder()
- .method(method)
- .uri(url)
- .header("User-Agent", "proxmox-backup-client/1.0")
- .header(hyper::header::CONTENT_TYPE, content_type)
- .body(())?;
- Ok(request)
- } else {
- let url: Uri = format!("https://{}:8007/{}", server, path).parse()?;
- let request = Request::builder()
- .method(method)
- .uri(url)
- .header("User-Agent", "proxmox-backup-client/1.0")
- .header(hyper::header::CONTENT_TYPE, content_type)
- .body(())?;
-
- Ok(request)
- }
+ let url = build_uri(server, 8007, path, query)?;
+ let request = Request::builder()
+ .method(method)
+ .uri(url)
+ .header("User-Agent", "proxmox-backup-client/1.0")
+ .header(hyper::header::CONTENT_TYPE, content_type)
+ .body(())?;
+ Ok(request)
}
}