src/client/http_client.rs: new struct HttpClientOptions
This commit is contained in:
parent
6afbe1d846
commit
d59dbeca1b
@ -256,6 +256,7 @@ pub async fn pull_group(
|
|||||||
list.sort_unstable_by(|a, b| a.backup_time.cmp(&b.backup_time));
|
list.sort_unstable_by(|a, b| a.backup_time.cmp(&b.backup_time));
|
||||||
|
|
||||||
let auth_info = client.login().await?;
|
let auth_info = client.login().await?;
|
||||||
|
let fingerprint = client.fingerprint();
|
||||||
|
|
||||||
let last_sync = tgt_store.last_successful_backup(group)?;
|
let last_sync = tgt_store.last_successful_backup(group)?;
|
||||||
|
|
||||||
@ -269,11 +270,11 @@ pub async fn pull_group(
|
|||||||
if last_sync_time > backup_time { continue; }
|
if last_sync_time > backup_time { continue; }
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_client = HttpClient::new(
|
let options = HttpClientOptions::new()
|
||||||
src_repo.host(),
|
.password(Some(auth_info.ticket.clone()))
|
||||||
src_repo.user(),
|
.fingerprint(fingerprint.clone());
|
||||||
Some(auth_info.ticket.clone())
|
|
||||||
)?;
|
let new_client = HttpClient::new(src_repo.host(), src_repo.user(), options)?;
|
||||||
|
|
||||||
let reader = BackupReader::start(
|
let reader = BackupReader::start(
|
||||||
new_client,
|
new_client,
|
||||||
@ -406,7 +407,11 @@ async fn pull (
|
|||||||
let (remote_config, _digest) = remote::config()?;
|
let (remote_config, _digest) = remote::config()?;
|
||||||
let remote: remote::Remote = remote_config.lookup("remote", &remote)?;
|
let remote: remote::Remote = remote_config.lookup("remote", &remote)?;
|
||||||
|
|
||||||
let client = HttpClient::new(&remote.host, &remote.userid, Some(remote.password.clone()))?;
|
let options = HttpClientOptions::new()
|
||||||
|
.password(Some(remote.password.clone()))
|
||||||
|
.fingerprint(remote.fingerprint.clone());
|
||||||
|
|
||||||
|
let client = HttpClient::new(&remote.host, &remote.userid, options)?;
|
||||||
let _auth_info = client.login() // make sure we can auth
|
let _auth_info = client.login() // make sure we can auth
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format_err!("remote connection to '{}' failed - {}", remote.host, err))?;
|
.map_err(|err| format_err!("remote connection to '{}' failed - {}", remote.host, err))?;
|
||||||
|
@ -4,7 +4,7 @@ use failure::*;
|
|||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use proxmox_backup::client::{HttpClient, BackupReader};
|
use proxmox_backup::client::{HttpClient, HttpClientOptions, BackupReader};
|
||||||
|
|
||||||
pub struct DummyWriter {
|
pub struct DummyWriter {
|
||||||
bytes: usize,
|
bytes: usize,
|
||||||
@ -29,7 +29,11 @@ async fn run() -> Result<(), Error> {
|
|||||||
|
|
||||||
let username = "root@pam";
|
let username = "root@pam";
|
||||||
|
|
||||||
let client = HttpClient::new(host, username, None)?;
|
let options = HttpClientOptions::new()
|
||||||
|
.interactive(true)
|
||||||
|
.ticket_cache(true);
|
||||||
|
|
||||||
|
let client = HttpClient::new(host, username, options)?;
|
||||||
|
|
||||||
let backup_time = "2019-06-28T10:49:48Z".parse::<DateTime<Utc>>()?;
|
let backup_time = "2019-06-28T10:49:48Z".parse::<DateTime<Utc>>()?;
|
||||||
|
|
||||||
|
@ -163,6 +163,15 @@ fn complete_repository(_arg: &str, _param: &HashMap<String, String>) -> Vec<Stri
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn connect(server: &str, userid: &str) -> Result<HttpClient, Error> {
|
||||||
|
|
||||||
|
let options = HttpClientOptions::new()
|
||||||
|
.interactive(true)
|
||||||
|
.ticket_cache(true);
|
||||||
|
|
||||||
|
HttpClient::new(server, userid, options)
|
||||||
|
}
|
||||||
|
|
||||||
async fn view_task_result(
|
async fn view_task_result(
|
||||||
client: HttpClient,
|
client: HttpClient,
|
||||||
result: Value,
|
result: Value,
|
||||||
@ -317,7 +326,7 @@ async fn list_backup_groups(param: Value) -> Result<Value, Error> {
|
|||||||
|
|
||||||
let repo = extract_repository_from_value(¶m)?;
|
let repo = extract_repository_from_value(¶m)?;
|
||||||
|
|
||||||
let client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
let path = format!("api2/json/admin/datastore/{}/groups", repo.store());
|
let path = format!("api2/json/admin/datastore/{}/groups", repo.store());
|
||||||
|
|
||||||
@ -411,7 +420,7 @@ async fn list_snapshots(param: Value) -> Result<Value, Error> {
|
|||||||
|
|
||||||
let output_format = param["output-format"].as_str().unwrap_or("text").to_owned();
|
let output_format = param["output-format"].as_str().unwrap_or("text").to_owned();
|
||||||
|
|
||||||
let client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
let group = if let Some(path) = param["group"].as_str() {
|
let group = if let Some(path) = param["group"].as_str() {
|
||||||
Some(BackupGroup::parse(path)?)
|
Some(BackupGroup::parse(path)?)
|
||||||
@ -473,7 +482,7 @@ async fn forget_snapshots(param: Value) -> Result<Value, Error> {
|
|||||||
let path = tools::required_string_param(¶m, "snapshot")?;
|
let path = tools::required_string_param(¶m, "snapshot")?;
|
||||||
let snapshot = BackupDir::parse(path)?;
|
let snapshot = BackupDir::parse(path)?;
|
||||||
|
|
||||||
let mut client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let mut client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
|
let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
|
||||||
|
|
||||||
@ -503,7 +512,7 @@ async fn api_login(param: Value) -> Result<Value, Error> {
|
|||||||
|
|
||||||
let repo = extract_repository_from_value(¶m)?;
|
let repo = extract_repository_from_value(¶m)?;
|
||||||
|
|
||||||
let client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
client.login().await?;
|
client.login().await?;
|
||||||
|
|
||||||
record_repository(&repo);
|
record_repository(&repo);
|
||||||
@ -563,7 +572,7 @@ async fn dump_catalog(param: Value) -> Result<Value, Error> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
let client = BackupReader::start(
|
let client = BackupReader::start(
|
||||||
client,
|
client,
|
||||||
@ -633,7 +642,7 @@ async fn list_snapshot_files(param: Value) -> Result<Value, Error> {
|
|||||||
|
|
||||||
let output_format = param["output-format"].as_str().unwrap_or("text").to_owned();
|
let output_format = param["output-format"].as_str().unwrap_or("text").to_owned();
|
||||||
|
|
||||||
let client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
let path = format!("api2/json/admin/datastore/{}/files", repo.store());
|
let path = format!("api2/json/admin/datastore/{}/files", repo.store());
|
||||||
|
|
||||||
@ -682,7 +691,7 @@ async fn start_garbage_collection(param: Value) -> Result<Value, Error> {
|
|||||||
let repo = extract_repository_from_value(¶m)?;
|
let repo = extract_repository_from_value(¶m)?;
|
||||||
let output_format = param["output-format"].as_str().unwrap_or("text").to_owned();
|
let output_format = param["output-format"].as_str().unwrap_or("text").to_owned();
|
||||||
|
|
||||||
let mut client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let mut client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
let path = format!("api2/json/admin/datastore/{}/gc", repo.store());
|
let path = format!("api2/json/admin/datastore/{}/gc", repo.store());
|
||||||
|
|
||||||
@ -903,7 +912,7 @@ async fn create_backup(
|
|||||||
|
|
||||||
let backup_time = Utc.timestamp(backup_time_opt.unwrap_or_else(|| Utc::now().timestamp()), 0);
|
let backup_time = Utc.timestamp(backup_time_opt.unwrap_or_else(|| Utc::now().timestamp()), 0);
|
||||||
|
|
||||||
let client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
record_repository(&repo);
|
record_repository(&repo);
|
||||||
|
|
||||||
println!("Starting backup: {}/{}/{}", backup_type, backup_id, BackupDir::backup_time_to_string(backup_time));
|
println!("Starting backup: {}/{}/{}", backup_type, backup_id, BackupDir::backup_time_to_string(backup_time));
|
||||||
@ -1161,7 +1170,7 @@ async fn restore(param: Value) -> Result<Value, Error> {
|
|||||||
|
|
||||||
let archive_name = tools::required_string_param(¶m, "archive-name")?;
|
let archive_name = tools::required_string_param(¶m, "archive-name")?;
|
||||||
|
|
||||||
let client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
record_repository(&repo);
|
record_repository(&repo);
|
||||||
|
|
||||||
@ -1328,7 +1337,7 @@ async fn upload_log(param: Value) -> Result<Value, Error> {
|
|||||||
let snapshot = tools::required_string_param(¶m, "snapshot")?;
|
let snapshot = tools::required_string_param(¶m, "snapshot")?;
|
||||||
let snapshot = BackupDir::parse(snapshot)?;
|
let snapshot = BackupDir::parse(snapshot)?;
|
||||||
|
|
||||||
let mut client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let mut client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
let keyfile = param["keyfile"].as_str().map(PathBuf::from);
|
let keyfile = param["keyfile"].as_str().map(PathBuf::from);
|
||||||
|
|
||||||
@ -1388,7 +1397,7 @@ async fn prune(mut param: Value) -> Result<Value, Error> {
|
|||||||
|
|
||||||
let repo = extract_repository_from_value(¶m)?;
|
let repo = extract_repository_from_value(¶m)?;
|
||||||
|
|
||||||
let mut client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let mut client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
let path = format!("api2/json/admin/datastore/{}/prune", repo.store());
|
let path = format!("api2/json/admin/datastore/{}/prune", repo.store());
|
||||||
|
|
||||||
@ -1433,7 +1442,7 @@ async fn status(param: Value) -> Result<Value, Error> {
|
|||||||
|
|
||||||
let output_format = param["output-format"].as_str().unwrap_or("text").to_owned();
|
let output_format = param["output-format"].as_str().unwrap_or("text").to_owned();
|
||||||
|
|
||||||
let client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
let path = format!("api2/json/admin/datastore/{}/status", repo.store());
|
let path = format!("api2/json/admin/datastore/{}/status", repo.store());
|
||||||
|
|
||||||
@ -1463,7 +1472,13 @@ async fn status(param: Value) -> Result<Value, Error> {
|
|||||||
// like get, but simply ignore errors and return Null instead
|
// like get, but simply ignore errors and return Null instead
|
||||||
async fn try_get(repo: &BackupRepository, url: &str) -> Value {
|
async fn try_get(repo: &BackupRepository, url: &str) -> Value {
|
||||||
|
|
||||||
let client = match HttpClient::new(repo.host(), repo.user(), None) {
|
|
||||||
|
let options = HttpClientOptions::new()
|
||||||
|
.verify_cert(false) // fixme: set verify to true, but howto handle fingerprint ??
|
||||||
|
.interactive(false)
|
||||||
|
.ticket_cache(true);
|
||||||
|
|
||||||
|
let client = match HttpClient::new(repo.host(), repo.user(), options) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
_ => return Value::Null,
|
_ => return Value::Null,
|
||||||
};
|
};
|
||||||
@ -1914,7 +1929,7 @@ async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> {
|
|||||||
let repo = extract_repository_from_value(¶m)?;
|
let repo = extract_repository_from_value(¶m)?;
|
||||||
let archive_name = tools::required_string_param(¶m, "archive-name")?;
|
let archive_name = tools::required_string_param(¶m, "archive-name")?;
|
||||||
let target = tools::required_string_param(¶m, "target")?;
|
let target = tools::required_string_param(¶m, "target")?;
|
||||||
let client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
record_repository(&repo);
|
record_repository(&repo);
|
||||||
|
|
||||||
@ -2024,7 +2039,7 @@ async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> {
|
|||||||
/// Shell to interactively inspect and restore snapshots.
|
/// Shell to interactively inspect and restore snapshots.
|
||||||
async fn catalog_shell(param: Value) -> Result<(), Error> {
|
async fn catalog_shell(param: Value) -> Result<(), Error> {
|
||||||
let repo = extract_repository_from_value(¶m)?;
|
let repo = extract_repository_from_value(¶m)?;
|
||||||
let client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
let path = tools::required_string_param(¶m, "snapshot")?;
|
let path = tools::required_string_param(¶m, "snapshot")?;
|
||||||
let archive_name = tools::required_string_param(¶m, "archive-name")?;
|
let archive_name = tools::required_string_param(¶m, "archive-name")?;
|
||||||
|
|
||||||
@ -2159,7 +2174,7 @@ async fn task_list(param: Value) -> Result<Value, Error> {
|
|||||||
|
|
||||||
let output_format = param["output-format"].as_str().unwrap_or("text").to_owned();
|
let output_format = param["output-format"].as_str().unwrap_or("text").to_owned();
|
||||||
let repo = extract_repository_from_value(¶m)?;
|
let repo = extract_repository_from_value(¶m)?;
|
||||||
let client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
let limit = param["limit"].as_u64().unwrap_or(50) as usize;
|
let limit = param["limit"].as_u64().unwrap_or(50) as usize;
|
||||||
|
|
||||||
@ -2208,7 +2223,7 @@ async fn task_log(param: Value) -> Result<Value, Error> {
|
|||||||
let repo = extract_repository_from_value(¶m)?;
|
let repo = extract_repository_from_value(¶m)?;
|
||||||
let upid = tools::required_string_param(¶m, "upid")?;
|
let upid = tools::required_string_param(¶m, "upid")?;
|
||||||
|
|
||||||
let client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
display_task_log(client, upid, true).await?;
|
display_task_log(client, upid, true).await?;
|
||||||
|
|
||||||
@ -2234,7 +2249,7 @@ async fn task_stop(param: Value) -> Result<Value, Error> {
|
|||||||
let repo = extract_repository_from_value(¶m)?;
|
let repo = extract_repository_from_value(¶m)?;
|
||||||
let upid_str = tools::required_string_param(¶m, "upid")?;
|
let upid_str = tools::required_string_param(¶m, "upid")?;
|
||||||
|
|
||||||
let mut client = HttpClient::new(repo.host(), repo.user(), None)?;
|
let mut client = connect(repo.host(), repo.user())?;
|
||||||
|
|
||||||
let path = format!("api2/json/nodes/localhost/tasks/{}", upid_str);
|
let path = format!("api2/json/nodes/localhost/tasks/{}", upid_str);
|
||||||
let _ = client.delete(&path, None).await?;
|
let _ = client.delete(&path, None).await?;
|
||||||
|
@ -34,11 +34,16 @@ fn connect() -> Result<HttpClient, Error> {
|
|||||||
|
|
||||||
let uid = nix::unistd::Uid::current();
|
let uid = nix::unistd::Uid::current();
|
||||||
|
|
||||||
|
let mut options = HttpClientOptions::new()
|
||||||
|
.verify_cert(false); // not required for connection to localhost
|
||||||
|
|
||||||
let client = if uid.is_root() {
|
let client = if uid.is_root() {
|
||||||
let ticket = assemble_rsa_ticket(private_auth_key(), "PBS", Some("root@pam"), None)?;
|
let ticket = assemble_rsa_ticket(private_auth_key(), "PBS", Some("root@pam"), None)?;
|
||||||
HttpClient::new("localhost", "root@pam", Some(ticket))?
|
options = options.password(Some(ticket));
|
||||||
|
HttpClient::new("localhost", "root@pam", options)?
|
||||||
} else {
|
} else {
|
||||||
HttpClient::new("localhost", "root@pam", None)?
|
options = options.ticket_cache(true).interactive(true);
|
||||||
|
HttpClient::new("localhost", "root@pam", options)?
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(client)
|
Ok(client)
|
||||||
@ -473,12 +478,14 @@ pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String
|
|||||||
|
|
||||||
let remote: Remote = remote_config.lookup("remote", &remote)?;
|
let remote: Remote = remote_config.lookup("remote", &remote)?;
|
||||||
|
|
||||||
|
let options = HttpClientOptions::new()
|
||||||
|
.password(Some(remote.password.clone()))
|
||||||
|
.fingerprint(remote.fingerprint.clone());
|
||||||
|
|
||||||
let client = HttpClient::new(
|
let client = HttpClient::new(
|
||||||
&remote.host,
|
&remote.host,
|
||||||
&remote.userid,
|
&remote.userid,
|
||||||
Some(remote.password),
|
options,
|
||||||
remote.fingerprint,
|
|
||||||
false,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut rt = tokio::runtime::Runtime::new().unwrap();
|
let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
@ -9,7 +9,11 @@ async fn upload_speed() -> Result<usize, Error> {
|
|||||||
|
|
||||||
let username = "root@pam";
|
let username = "root@pam";
|
||||||
|
|
||||||
let client = HttpClient::new(host, username, None)?;
|
let options = HttpClientOptions::new()
|
||||||
|
.interactive(true)
|
||||||
|
.ticket_cache(true);
|
||||||
|
|
||||||
|
let client = HttpClient::new(host, username, options)?;
|
||||||
|
|
||||||
let backup_time = chrono::Utc::now();
|
let backup_time = chrono::Utc::now();
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use failure::*;
|
use failure::*;
|
||||||
@ -9,7 +10,7 @@ use http::header::HeaderValue;
|
|||||||
use http::{Request, Response};
|
use http::{Request, Response};
|
||||||
use hyper::Body;
|
use hyper::Body;
|
||||||
use hyper::client::{Client, HttpConnector};
|
use hyper::client::{Client, HttpConnector};
|
||||||
use openssl::ssl::{SslConnector, SslMethod};
|
use openssl::{ssl::{SslConnector, SslMethod}, x509::X509StoreContextRef};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use percent_encoding::percent_encode;
|
use percent_encoding::percent_encode;
|
||||||
use xdg::BaseDirectories;
|
use xdg::BaseDirectories;
|
||||||
@ -29,11 +30,59 @@ pub struct AuthInfo {
|
|||||||
pub token: String,
|
pub token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct HttpClientOptions {
|
||||||
|
password: Option<String>,
|
||||||
|
fingerprint: Option<String>,
|
||||||
|
interactive: bool,
|
||||||
|
ticket_cache: bool,
|
||||||
|
verify_cert: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpClientOptions {
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
password: None,
|
||||||
|
fingerprint: None,
|
||||||
|
interactive: false,
|
||||||
|
ticket_cache: false,
|
||||||
|
verify_cert: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(mut self, password: Option<String>) -> Self {
|
||||||
|
self.password = password;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fingerprint(mut self, fingerprint: Option<String>) -> Self {
|
||||||
|
self.fingerprint = fingerprint;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interactive(mut self, interactive: bool) -> Self {
|
||||||
|
self.interactive = interactive;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ticket_cache(mut self, ticket_cache: bool) -> Self {
|
||||||
|
self.ticket_cache = ticket_cache;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_cert(mut self, verify_cert: bool) -> Self {
|
||||||
|
self.verify_cert = verify_cert;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// HTTP(S) API client
|
/// HTTP(S) API client
|
||||||
pub struct HttpClient {
|
pub struct HttpClient {
|
||||||
client: Client<HttpsConnector>,
|
client: Client<HttpsConnector>,
|
||||||
server: String,
|
server: String,
|
||||||
|
fingerprint: Arc<Mutex<Option<String>>>,
|
||||||
auth: BroadcastFuture<AuthInfo>,
|
auth: BroadcastFuture<AuthInfo>,
|
||||||
|
_options: HttpClientOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete stored ticket data (logout)
|
/// Delete stored ticket data (logout)
|
||||||
@ -116,23 +165,47 @@ fn load_ticket_info(server: &str, username: &str) -> Option<(String, String)> {
|
|||||||
|
|
||||||
impl HttpClient {
|
impl HttpClient {
|
||||||
|
|
||||||
pub fn new(server: &str, username: &str, password: Option<String>) -> Result<Self, Error> {
|
pub fn new(server: &str, username: &str, mut options: HttpClientOptions) -> Result<Self, Error> {
|
||||||
let client = Self::build_client();
|
|
||||||
|
let verified_fingerprint = Arc::new(Mutex::new(None));
|
||||||
|
|
||||||
|
let client = Self::build_client(
|
||||||
|
options.fingerprint.clone(),
|
||||||
|
options.interactive,
|
||||||
|
verified_fingerprint.clone(),
|
||||||
|
options.verify_cert,
|
||||||
|
);
|
||||||
|
|
||||||
|
let password = options.password.take();
|
||||||
|
|
||||||
let password = if let Some(password) = password {
|
let password = if let Some(password) = password {
|
||||||
password
|
password
|
||||||
} else if let Some((ticket, _token)) = load_ticket_info(server, username) {
|
} else {
|
||||||
|
let mut ticket_info = None;
|
||||||
|
if options.ticket_cache {
|
||||||
|
ticket_info = load_ticket_info(server, username);
|
||||||
|
}
|
||||||
|
if let Some((ticket, _token)) = ticket_info {
|
||||||
ticket
|
ticket
|
||||||
} else {
|
} else {
|
||||||
Self::get_password(&username)?
|
Self::get_password(&username, options.interactive)?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let login_future = Self::credentials(client.clone(), server.to_owned(), username.to_owned(), password);
|
let login_future = Self::credentials(
|
||||||
|
client.clone(),
|
||||||
|
server.to_owned(),
|
||||||
|
username.to_owned(),
|
||||||
|
password,
|
||||||
|
options.ticket_cache,
|
||||||
|
);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
client,
|
client,
|
||||||
server: String::from(server),
|
server: String::from(server),
|
||||||
|
fingerprint: verified_fingerprint,
|
||||||
auth: BroadcastFuture::new(Box::new(login_future)),
|
auth: BroadcastFuture::new(Box::new(login_future)),
|
||||||
|
_options: options,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +217,12 @@ impl HttpClient {
|
|||||||
self.auth.listen().await
|
self.auth.listen().await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_password(_username: &str) -> Result<String, Error> {
|
/// Returns the optional fingerprint passed to the new() constructor.
|
||||||
|
pub fn fingerprint(&self) -> Option<String> {
|
||||||
|
(*self.fingerprint.lock().unwrap()).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_password(_username: &str, interactive: bool) -> Result<String, Error> {
|
||||||
use std::env::VarError::*;
|
use std::env::VarError::*;
|
||||||
match std::env::var("PBS_PASSWORD") {
|
match std::env::var("PBS_PASSWORD") {
|
||||||
Ok(p) => return Ok(p),
|
Ok(p) => return Ok(p),
|
||||||
@ -155,18 +233,90 @@ impl HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we're on a TTY, query the user for a password
|
// If we're on a TTY, query the user for a password
|
||||||
if tty::stdin_isatty() {
|
if interactive && tty::stdin_isatty() {
|
||||||
return Ok(String::from_utf8(tty::read_password("Password: ")?)?);
|
return Ok(String::from_utf8(tty::read_password("Password: ")?)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
bail!("no password input mechanism available");
|
bail!("no password input mechanism available");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_client() -> Client<HttpsConnector> {
|
fn verify_callback(
|
||||||
|
valid: bool, ctx:
|
||||||
|
&mut X509StoreContextRef,
|
||||||
|
expected_fingerprint: Option<String>,
|
||||||
|
interactive: bool,
|
||||||
|
verified_fingerprint: Arc<Mutex<Option<String>>>,
|
||||||
|
) -> bool {
|
||||||
|
if valid { return true; }
|
||||||
|
|
||||||
|
let cert = match ctx.current_cert() {
|
||||||
|
Some(cert) => cert,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let depth = ctx.error_depth();
|
||||||
|
if depth != 0 { return false; }
|
||||||
|
|
||||||
|
let fp = match cert.digest(openssl::hash::MessageDigest::sha256()) {
|
||||||
|
Ok(fp) => fp,
|
||||||
|
Err(_) => return false, // 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 == fp_string {
|
||||||
|
*verified_fingerprint.lock().unwrap() = Some(fp_string);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're on a TTY, query the user
|
||||||
|
if interactive && tty::stdin_isatty() {
|
||||||
|
println!("fingerprint: {}", fp_string);
|
||||||
|
loop {
|
||||||
|
print!("Want to trust? (y/n): ");
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
let mut buf = [0u8; 1];
|
||||||
|
use std::io::Read;
|
||||||
|
match std::io::stdin().read_exact(&mut buf) {
|
||||||
|
Ok(()) => {
|
||||||
|
if buf[0] == b'y' || buf[0] == b'Y' {
|
||||||
|
println!("TRUST {}", fp_string);
|
||||||
|
*verified_fingerprint.lock().unwrap() = Some(fp_string);
|
||||||
|
return true;
|
||||||
|
} else if buf[0] == b'n' || buf[0] == b'N' {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_client(
|
||||||
|
fingerprint: Option<String>,
|
||||||
|
interactive: bool,
|
||||||
|
verified_fingerprint: Arc<Mutex<Option<String>>>,
|
||||||
|
verify_cert: bool,
|
||||||
|
) -> Client<HttpsConnector> {
|
||||||
|
|
||||||
let mut ssl_connector_builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
let mut ssl_connector_builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||||
|
|
||||||
ssl_connector_builder.set_verify(openssl::ssl::SslVerifyMode::NONE); // fixme!
|
if verify_cert {
|
||||||
|
ssl_connector_builder.set_verify_callback(openssl::ssl::SslVerifyMode::PEER, move |valid, ctx| {
|
||||||
|
Self::verify_callback(valid, ctx, fingerprint.clone(), interactive, verified_fingerprint.clone())
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ssl_connector_builder.set_verify(openssl::ssl::SslVerifyMode::NONE);
|
||||||
|
}
|
||||||
|
|
||||||
let mut httpc = hyper::client::HttpConnector::new();
|
let mut httpc = hyper::client::HttpConnector::new();
|
||||||
httpc.set_nodelay(true); // important for h2 download performance!
|
httpc.set_nodelay(true); // important for h2 download performance!
|
||||||
@ -339,6 +489,7 @@ impl HttpClient {
|
|||||||
server: String,
|
server: String,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
use_ticket_cache: bool,
|
||||||
) -> Result<AuthInfo, Error> {
|
) -> Result<AuthInfo, Error> {
|
||||||
let data = json!({ "username": username, "password": password });
|
let data = json!({ "username": username, "password": password });
|
||||||
let req = Self::request_builder(&server, "POST", "/api2/json/access/ticket", Some(data)).unwrap();
|
let req = Self::request_builder(&server, "POST", "/api2/json/access/ticket", Some(data)).unwrap();
|
||||||
@ -349,7 +500,9 @@ impl HttpClient {
|
|||||||
token: cred["data"]["CSRFPreventionToken"].as_str().unwrap().to_owned(),
|
token: cred["data"]["CSRFPreventionToken"].as_str().unwrap().to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if use_ticket_cache {
|
||||||
let _ = store_ticket_info(&server, &auth.username, &auth.ticket, &auth.token);
|
let _ = store_ticket_info(&server, &auth.username, &auth.ticket, &auth.token);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(auth)
|
Ok(auth)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user