client/remote: add support to specify port number

this adds the ability to add port numbers in the backup repo spec
as well as remotes, so that user that are behind a
NAT/Firewall/Reverse proxy can still use it

also adds some explanation and examples to the docs to make it clearer
for h2 client i left the localhost:8007 part, since it is not
configurable where we bind to

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
This commit is contained in:
Dominik Csapak 2020-09-29 16:18:58 +02:00 committed by Dietmar Maurer
parent 729d41fe6a
commit ba20987ae7
16 changed files with 108 additions and 51 deletions

View File

@ -732,11 +732,14 @@ Repository Locations
The client uses the following notation to specify a datastore repository The client uses the following notation to specify a datastore repository
on the backup server. on the backup server.
[[username@]server:]datastore [[username@]server[:port]:]datastore
The default value for ``username`` ist ``root@pam``. If no server is specified, The default value for ``username`` ist ``root@pam``. If no server is specified,
the default is the local host (``localhost``). the default is the local host (``localhost``).
You can specify a port if your backup server is only reachable on a different
port (e.g. with NAT and port forwarding).
Note that if the server is an IPv6 address, you have to write it with Note that if the server is an IPv6 address, you have to write it with
square brackets (e.g. [fe80::01]). square brackets (e.g. [fe80::01]).
@ -744,6 +747,18 @@ You can pass the repository with the ``--repository`` command
line option, or by setting the ``PBS_REPOSITORY`` environment line option, or by setting the ``PBS_REPOSITORY`` environment
variable. variable.
Here some examples of valid repositories and the real values
================================ ============ ================== ===========
Example User Host:Port Datastore
================================ ============ ================== ===========
mydatastore ``root@pam`` localhost:8007 mydatastore
myhostname:mydatastore ``root@pam`` myhostname:8007 mydatastore
user@pbs@myhostname:mydatastore ``user@pbs`` myhostname:8007 mydatastore
192.168.55.55:1234:mydatastore ``root@pam`` 192.168.55.55:1234 mydatastore
[ff80::51]:mydatastore ``root@pam`` [ff80::51]:8007 mydatastore
[ff80::51]:1234:mydatastore ``root@pam`` [ff80::51]:1234 mydatastore
================================ ============ ================== ===========
Environment Variables Environment Variables
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~

View File

@ -60,6 +60,12 @@ pub fn list_remotes(
host: { host: {
schema: DNS_NAME_OR_IP_SCHEMA, schema: DNS_NAME_OR_IP_SCHEMA,
}, },
port: {
description: "The (optional) port.",
type: u16,
optional: true,
default: 8007,
},
userid: { userid: {
type: Userid, type: Userid,
}, },
@ -136,6 +142,8 @@ pub enum DeletableProperty {
comment, comment,
/// Delete the fingerprint property. /// Delete the fingerprint property.
fingerprint, fingerprint,
/// Delete the port property.
port,
} }
#[api( #[api(
@ -153,6 +161,11 @@ pub enum DeletableProperty {
optional: true, optional: true,
schema: DNS_NAME_OR_IP_SCHEMA, schema: DNS_NAME_OR_IP_SCHEMA,
}, },
port: {
description: "The (optional) port.",
type: u16,
optional: true,
},
userid: { userid: {
optional: true, optional: true,
type: Userid, type: Userid,
@ -188,6 +201,7 @@ pub fn update_remote(
name: String, name: String,
comment: Option<String>, comment: Option<String>,
host: Option<String>, host: Option<String>,
port: Option<u16>,
userid: Option<Userid>, userid: Option<Userid>,
password: Option<String>, password: Option<String>,
fingerprint: Option<String>, fingerprint: Option<String>,
@ -211,6 +225,7 @@ pub fn update_remote(
match delete_prop { match delete_prop {
DeletableProperty::comment => { data.comment = None; }, DeletableProperty::comment => { data.comment = None; },
DeletableProperty::fingerprint => { data.fingerprint = None; }, DeletableProperty::fingerprint => { data.fingerprint = None; },
DeletableProperty::port => { data.port = None; },
} }
} }
} }
@ -224,6 +239,7 @@ pub fn update_remote(
} }
} }
if let Some(host) = host { data.host = host; } if let Some(host) = host { data.host = host; }
if port.is_some() { data.port = port; }
if let Some(userid) = userid { data.userid = userid; } if let Some(userid) = userid { data.userid = userid; }
if let Some(password) = password { data.password = password; } if let Some(password) = password { data.password = password; }

View File

@ -55,12 +55,12 @@ pub async fn get_pull_parameters(
.password(Some(remote.password.clone())) .password(Some(remote.password.clone()))
.fingerprint(remote.fingerprint.clone()); .fingerprint(remote.fingerprint.clone());
let client = HttpClient::new(&remote.host, &remote.userid, options)?; let client = HttpClient::new(&remote.host, remote.port.unwrap_or(8007), &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))?;
let src_repo = BackupRepository::new(Some(remote.userid), Some(remote.host), remote_store.to_string()); let src_repo = BackupRepository::new(Some(remote.userid), Some(remote.host), remote.port, remote_store.to_string());
Ok((client, src_repo, tgt_store)) Ok((client, src_repo, tgt_store))
} }

View File

@ -65,7 +65,7 @@ const_regex!{
pub DNS_NAME_OR_IP_REGEX = concat!(r"^", DNS_NAME!(), "|", IPRE!(), r"$"); pub DNS_NAME_OR_IP_REGEX = concat!(r"^", DNS_NAME!(), "|", IPRE!(), r"$");
pub BACKUP_REPO_URL_REGEX = concat!(r"^^(?:(?:(", USER_ID_REGEX_STR!(), ")@)?(", DNS_NAME!(), "|", IPRE_BRACKET!() ,"):)?(", PROXMOX_SAFE_ID_REGEX_STR!(), r")$"); pub BACKUP_REPO_URL_REGEX = concat!(r"^^(?:(?:(", USER_ID_REGEX_STR!(), ")@)?(", DNS_NAME!(), "|", IPRE_BRACKET!() ,"):)?(?:([0-9]{1,5}):)?(", PROXMOX_SAFE_ID_REGEX_STR!(), r")$");
pub CERT_FINGERPRINT_SHA256_REGEX = r"^(?:[0-9a-fA-F][0-9a-fA-F])(?::[0-9a-fA-F][0-9a-fA-F]){31}$"; pub CERT_FINGERPRINT_SHA256_REGEX = r"^(?:[0-9a-fA-F][0-9a-fA-F])(?::[0-9a-fA-F][0-9a-fA-F]){31}$";

View File

@ -192,7 +192,7 @@ pub fn complete_repository(_arg: &str, _param: &HashMap<String, String>) -> Vec<
result result
} }
fn connect(server: &str, userid: &Userid) -> Result<HttpClient, Error> { fn connect(server: &str, port: u16, userid: &Userid) -> Result<HttpClient, Error> {
let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok(); let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();
@ -211,7 +211,7 @@ fn connect(server: &str, userid: &Userid) -> Result<HttpClient, Error> {
.fingerprint_cache(true) .fingerprint_cache(true)
.ticket_cache(true); .ticket_cache(true);
HttpClient::new(server, userid, options) HttpClient::new(server, port, userid, options)
} }
async fn view_task_result( async fn view_task_result(
@ -365,7 +365,7 @@ async fn list_backup_groups(param: Value) -> Result<Value, Error> {
let repo = extract_repository_from_value(&param)?; let repo = extract_repository_from_value(&param)?;
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
let path = format!("api2/json/admin/datastore/{}/groups", repo.store()); let path = format!("api2/json/admin/datastore/{}/groups", repo.store());
@ -438,7 +438,7 @@ async fn list_snapshots(param: Value) -> Result<Value, Error> {
let output_format = get_output_format(&param); let output_format = get_output_format(&param);
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
let group: Option<BackupGroup> = if let Some(path) = param["group"].as_str() { let group: Option<BackupGroup> = if let Some(path) = param["group"].as_str() {
Some(path.parse()?) Some(path.parse()?)
@ -503,7 +503,7 @@ async fn forget_snapshots(param: Value) -> Result<Value, Error> {
let path = tools::required_string_param(&param, "snapshot")?; let path = tools::required_string_param(&param, "snapshot")?;
let snapshot: BackupDir = path.parse()?; let snapshot: BackupDir = path.parse()?;
let mut client = connect(repo.host(), repo.user())?; let mut client = connect(repo.host(), repo.port(), repo.user())?;
let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store()); let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
@ -533,7 +533,7 @@ async fn api_login(param: Value) -> Result<Value, Error> {
let repo = extract_repository_from_value(&param)?; let repo = extract_repository_from_value(&param)?;
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
client.login().await?; client.login().await?;
record_repository(&repo); record_repository(&repo);
@ -590,7 +590,7 @@ async fn api_version(param: Value) -> Result<(), Error> {
let repo = extract_repository_from_value(&param); let repo = extract_repository_from_value(&param);
if let Ok(repo) = repo { if let Ok(repo) = repo {
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
match client.get("api2/json/version", None).await { match client.get("api2/json/version", None).await {
Ok(mut result) => version_info["server"] = result["data"].take(), Ok(mut result) => version_info["server"] = result["data"].take(),
@ -640,7 +640,7 @@ async fn list_snapshot_files(param: Value) -> Result<Value, Error> {
let output_format = get_output_format(&param); let output_format = get_output_format(&param);
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
let path = format!("api2/json/admin/datastore/{}/files", repo.store()); let path = format!("api2/json/admin/datastore/{}/files", repo.store());
@ -684,7 +684,7 @@ async fn start_garbage_collection(param: Value) -> Result<Value, Error> {
let output_format = get_output_format(&param); let output_format = get_output_format(&param);
let mut client = connect(repo.host(), repo.user())?; let mut client = connect(repo.host(), repo.port(), repo.user())?;
let path = format!("api2/json/admin/datastore/{}/gc", repo.store()); let path = format!("api2/json/admin/datastore/{}/gc", repo.store());
@ -996,7 +996,7 @@ async fn create_backup(
let backup_time = backup_time_opt.unwrap_or_else(|| epoch_i64()); let backup_time = backup_time_opt.unwrap_or_else(|| epoch_i64());
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), 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)?);
@ -1299,7 +1299,7 @@ async fn restore(param: Value) -> Result<Value, Error> {
let archive_name = tools::required_string_param(&param, "archive-name")?; let archive_name = tools::required_string_param(&param, "archive-name")?;
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
record_repository(&repo); record_repository(&repo);
@ -1472,7 +1472,7 @@ async fn upload_log(param: Value) -> Result<Value, Error> {
let snapshot = tools::required_string_param(&param, "snapshot")?; let snapshot = tools::required_string_param(&param, "snapshot")?;
let snapshot: BackupDir = snapshot.parse()?; let snapshot: BackupDir = snapshot.parse()?;
let mut client = connect(repo.host(), repo.user())?; let mut client = connect(repo.host(), repo.port(), repo.user())?;
let (keydata, crypt_mode) = keyfile_parameters(&param)?; let (keydata, crypt_mode) = keyfile_parameters(&param)?;
@ -1543,7 +1543,7 @@ fn prune<'a>(
async fn prune_async(mut param: Value) -> Result<Value, Error> { async fn prune_async(mut param: Value) -> Result<Value, Error> {
let repo = extract_repository_from_value(&param)?; let repo = extract_repository_from_value(&param)?;
let mut client = connect(repo.host(), repo.user())?; let mut client = connect(repo.host(), repo.port(), repo.user())?;
let path = format!("api2/json/admin/datastore/{}/prune", repo.store()); let path = format!("api2/json/admin/datastore/{}/prune", repo.store());
@ -1626,7 +1626,7 @@ async fn status(param: Value) -> Result<Value, Error> {
let output_format = get_output_format(&param); let output_format = get_output_format(&param);
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
let path = format!("api2/json/admin/datastore/{}/status", repo.store()); let path = format!("api2/json/admin/datastore/{}/status", repo.store());
@ -1671,7 +1671,7 @@ async fn try_get(repo: &BackupRepository, url: &str) -> Value {
.fingerprint_cache(true) .fingerprint_cache(true)
.ticket_cache(true); .ticket_cache(true);
let client = match HttpClient::new(repo.host(), repo.user(), options) { let client = match HttpClient::new(repo.host(), repo.port(), repo.user(), options) {
Ok(v) => v, Ok(v) => v,
_ => return Value::Null, _ => return Value::Null,
}; };

View File

@ -62,10 +62,10 @@ fn connect() -> Result<HttpClient, Error> {
let ticket = Ticket::new("PBS", Userid::root_userid())? let ticket = Ticket::new("PBS", Userid::root_userid())?
.sign(private_auth_key(), None)?; .sign(private_auth_key(), None)?;
options = options.password(Some(ticket)); options = options.password(Some(ticket));
HttpClient::new("localhost", Userid::root_userid(), options)? HttpClient::new("localhost", 8007, Userid::root_userid(), options)?
} else { } else {
options = options.ticket_cache(true).interactive(true); options = options.ticket_cache(true).interactive(true);
HttpClient::new("localhost", Userid::root_userid(), options)? HttpClient::new("localhost", 8007, Userid::root_userid(), options)?
}; };
Ok(client) Ok(client)
@ -410,6 +410,7 @@ pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String
let client = HttpClient::new( let client = HttpClient::new(
&remote.host, &remote.host,
remote.port.unwrap_or(8007),
&remote.userid, &remote.userid,
options, options,
)?; )?;

View File

@ -225,7 +225,7 @@ async fn test_upload_speed(
let backup_time = proxmox::tools::time::epoch_i64(); let backup_time = proxmox::tools::time::epoch_i64();
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
record_repository(&repo); record_repository(&repo);
if verbose { eprintln!("Connecting to backup server"); } if verbose { eprintln!("Connecting to backup server"); }

View File

@ -79,7 +79,7 @@ async fn dump_catalog(param: Value) -> Result<Value, Error> {
} }
}; };
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
let client = BackupReader::start( let client = BackupReader::start(
client, client,
@ -153,7 +153,7 @@ async fn dump_catalog(param: Value) -> 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(&param)?; let repo = extract_repository_from_value(&param)?;
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
let path = tools::required_string_param(&param, "snapshot")?; let path = tools::required_string_param(&param, "snapshot")?;
let archive_name = tools::required_string_param(&param, "archive-name")?; let archive_name = tools::required_string_param(&param, "archive-name")?;

View File

@ -101,7 +101,7 @@ async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> {
let repo = extract_repository_from_value(&param)?; let repo = extract_repository_from_value(&param)?;
let archive_name = tools::required_string_param(&param, "archive-name")?; let archive_name = tools::required_string_param(&param, "archive-name")?;
let target = tools::required_string_param(&param, "target")?; let target = tools::required_string_param(&param, "target")?;
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
record_repository(&repo); record_repository(&repo);

View File

@ -48,7 +48,7 @@ async fn task_list(param: Value) -> Result<Value, Error> {
let output_format = get_output_format(&param); let output_format = get_output_format(&param);
let repo = extract_repository_from_value(&param)?; let repo = extract_repository_from_value(&param)?;
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
let limit = param["limit"].as_u64().unwrap_or(50) as usize; let limit = param["limit"].as_u64().unwrap_or(50) as usize;
let running = !param["all"].as_bool().unwrap_or(false); let running = !param["all"].as_bool().unwrap_or(false);
@ -96,7 +96,7 @@ async fn task_log(param: Value) -> Result<Value, Error> {
let repo = extract_repository_from_value(&param)?; let repo = extract_repository_from_value(&param)?;
let upid = tools::required_string_param(&param, "upid")?; let upid = tools::required_string_param(&param, "upid")?;
let client = connect(repo.host(), repo.user())?; let client = connect(repo.host(), repo.port(), repo.user())?;
display_task_log(client, upid, true).await?; display_task_log(client, upid, true).await?;
@ -122,7 +122,7 @@ async fn task_stop(param: Value) -> Result<Value, Error> {
let repo = extract_repository_from_value(&param)?; let repo = extract_repository_from_value(&param)?;
let upid_str = tools::required_string_param(&param, "upid")?; let upid_str = tools::required_string_param(&param, "upid")?;
let mut client = connect(repo.host(), repo.user())?; let mut client = connect(repo.host(), repo.port(), 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?;

View File

@ -54,7 +54,7 @@ impl BackupReader {
"store": datastore, "store": datastore,
"debug": debug, "debug": debug,
}); });
let req = HttpClient::request_builder(client.server(), "GET", "/api2/json/reader", Some(param)).unwrap(); let req = HttpClient::request_builder(client.server(), client.port(), "GET", "/api2/json/reader", Some(param)).unwrap();
let (h2, abort) = client.start_h2_connection(req, String::from(PROXMOX_BACKUP_READER_PROTOCOL_ID_V1!())).await?; let (h2, abort) = client.start_h2_connection(req, String::from(PROXMOX_BACKUP_READER_PROTOCOL_ID_V1!())).await?;

View File

@ -19,14 +19,16 @@ pub struct BackupRepository {
user: Option<Userid>, user: Option<Userid>,
/// The host name or IP address /// The host name or IP address
host: Option<String>, host: Option<String>,
/// The port
port: Option<u16>,
/// The name of the datastore /// The name of the datastore
store: String, store: String,
} }
impl BackupRepository { impl BackupRepository {
pub fn new(user: Option<Userid>, host: Option<String>, store: String) -> Self { pub fn new(user: Option<Userid>, host: Option<String>, port: Option<u16>, store: String) -> Self {
Self { user, host, store } Self { user, host, port, store }
} }
pub fn user(&self) -> &Userid { pub fn user(&self) -> &Userid {
@ -43,6 +45,13 @@ impl BackupRepository {
"localhost" "localhost"
} }
pub fn port(&self) -> u16 {
if let Some(port) = self.port {
return port;
}
8007
}
pub fn store(&self) -> &str { pub fn store(&self) -> &str {
&self.store &self.store
} }
@ -50,12 +59,11 @@ impl BackupRepository {
impl fmt::Display for BackupRepository { impl fmt::Display for BackupRepository {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(ref user) = self.user { match (&self.user, &self.host, self.port) {
write!(f, "{}@{}:{}", user, self.host(), self.store) (Some(user), _, _) => write!(f, "{}@{}:{}:{}", user, self.host(), self.port(), self.store),
} else if let Some(ref host) = self.host { (None, Some(host), None) => write!(f, "{}:{}", host, self.store),
write!(f, "{}:{}", host, self.store) (None, _, Some(port)) => write!(f, "{}:{}:{}", self.host(), port, self.store),
} else { (None, None, None) => write!(f, "{}", self.store),
write!(f, "{}", self.store)
} }
} }
} }
@ -76,7 +84,8 @@ impl std::str::FromStr for BackupRepository {
Ok(Self { Ok(Self {
user: cap.get(1).map(|m| Userid::try_from(m.as_str().to_owned())).transpose()?, user: cap.get(1).map(|m| Userid::try_from(m.as_str().to_owned())).transpose()?,
host: cap.get(2).map(|m| m.as_str().to_owned()), host: cap.get(2).map(|m| m.as_str().to_owned()),
store: cap[3].to_owned(), port: cap.get(3).map(|m| m.as_str().parse::<u16>()).transpose()?,
store: cap[4].to_owned(),
}) })
} }
} }

View File

@ -65,7 +65,7 @@ impl BackupWriter {
}); });
let req = HttpClient::request_builder( let req = HttpClient::request_builder(
client.server(), "GET", "/api2/json/backup", Some(param)).unwrap(); client.server(), client.port(), "GET", "/api2/json/backup", Some(param)).unwrap();
let (h2, abort) = client.start_h2_connection(req, String::from(PROXMOX_BACKUP_PROTOCOL_ID_V1!())).await?; let (h2, abort) = client.start_h2_connection(req, String::from(PROXMOX_BACKUP_PROTOCOL_ID_V1!())).await?;

View File

@ -99,6 +99,7 @@ impl HttpClientOptions {
pub struct HttpClient { pub struct HttpClient {
client: Client<HttpsConnector>, client: Client<HttpsConnector>,
server: String, server: String,
port: u16,
fingerprint: Arc<Mutex<Option<String>>>, fingerprint: Arc<Mutex<Option<String>>>,
first_auth: BroadcastFuture<()>, first_auth: BroadcastFuture<()>,
auth: Arc<RwLock<AuthInfo>>, auth: Arc<RwLock<AuthInfo>>,
@ -250,6 +251,7 @@ fn load_ticket_info(prefix: &str, server: &str, userid: &Userid) -> Option<(Stri
impl HttpClient { impl HttpClient {
pub fn new( pub fn new(
server: &str, server: &str,
port: u16,
userid: &Userid, userid: &Userid,
mut options: HttpClientOptions, mut options: HttpClientOptions,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
@ -338,7 +340,7 @@ impl HttpClient {
let authinfo = auth2.read().unwrap().clone(); let authinfo = auth2.read().unwrap().clone();
(authinfo.userid, authinfo.ticket) (authinfo.userid, authinfo.ticket)
}; };
match Self::credentials(client2.clone(), server2.clone(), userid, ticket).await { match Self::credentials(client2.clone(), server2.clone(), port, userid, ticket).await {
Ok(auth) => { 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.userid.to_string(), &auth.ticket, &auth.token); let _ = store_ticket_info(prefix2.as_ref().unwrap(), &server2, &auth.userid.to_string(), &auth.ticket, &auth.token);
@ -358,6 +360,7 @@ impl HttpClient {
let login_future = Self::credentials( let login_future = Self::credentials(
client.clone(), client.clone(),
server.to_owned(), server.to_owned(),
port,
userid.to_owned(), userid.to_owned(),
password.to_owned(), password.to_owned(),
).map_ok({ ).map_ok({
@ -377,6 +380,7 @@ impl HttpClient {
Ok(Self { Ok(Self {
client, client,
server: String::from(server), server: String::from(server),
port,
fingerprint: verified_fingerprint, fingerprint: verified_fingerprint,
auth, auth,
ticket_abort, ticket_abort,
@ -486,7 +490,7 @@ impl HttpClient {
path: &str, path: &str,
data: Option<Value>, data: Option<Value>,
) -> Result<Value, Error> { ) -> Result<Value, Error> {
let req = Self::request_builder(&self.server, "GET", path, data).unwrap(); let req = Self::request_builder(&self.server, self.port, "GET", path, data)?;
self.request(req).await self.request(req).await
} }
@ -495,7 +499,7 @@ impl HttpClient {
path: &str, path: &str,
data: Option<Value>, data: Option<Value>,
) -> Result<Value, Error> { ) -> Result<Value, Error> {
let req = Self::request_builder(&self.server, "DELETE", path, data).unwrap(); let req = Self::request_builder(&self.server, self.port, "DELETE", path, data)?;
self.request(req).await self.request(req).await
} }
@ -504,7 +508,7 @@ impl HttpClient {
path: &str, path: &str,
data: Option<Value>, data: Option<Value>,
) -> Result<Value, Error> { ) -> Result<Value, Error> {
let req = Self::request_builder(&self.server, "POST", path, data).unwrap(); let req = Self::request_builder(&self.server, self.port, "POST", path, data)?;
self.request(req).await self.request(req).await
} }
@ -513,7 +517,7 @@ impl HttpClient {
path: &str, path: &str,
output: &mut (dyn Write + Send), output: &mut (dyn Write + Send),
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut req = Self::request_builder(&self.server, "GET", path, None).unwrap(); let mut req = Self::request_builder(&self.server, self.port, "GET", path, None)?;
let client = self.client.clone(); let client = self.client.clone();
@ -549,7 +553,7 @@ impl HttpClient {
) -> Result<Value, Error> { ) -> Result<Value, Error> {
let path = path.trim_matches('/'); let path = path.trim_matches('/');
let mut url = format!("https://{}:8007/{}", &self.server, path); let mut url = format!("https://{}:{}/{}", &self.server, self.port, path);
if let Some(data) = data { if let Some(data) = data {
let query = tools::json_object_to_query(data).unwrap(); let query = tools::json_object_to_query(data).unwrap();
@ -624,11 +628,12 @@ impl HttpClient {
async fn credentials( async fn credentials(
client: Client<HttpsConnector>, client: Client<HttpsConnector>,
server: String, server: String,
port: u16,
username: Userid, username: Userid,
password: String, password: String,
) -> 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, port, "POST", "/api2/json/access/ticket", Some(data))?;
let cred = Self::api_request(client, req).await?; let cred = Self::api_request(client, req).await?;
let auth = AuthInfo { let auth = AuthInfo {
userid: cred["data"]["username"].as_str().unwrap().parse()?, userid: cred["data"]["username"].as_str().unwrap().parse()?,
@ -672,9 +677,13 @@ impl HttpClient {
&self.server &self.server
} }
pub fn request_builder(server: &str, method: &str, path: &str, data: Option<Value>) -> Result<Request<Body>, Error> { pub fn port(&self) -> u16 {
self.port
}
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 path = path.trim_matches('/');
let url: Uri = format!("https://{}:8007/{}", server, path).parse()?; let url: Uri = format!("https://{}:{}/{}", server, port, path).parse()?;
if let Some(data) = data { if let Some(data) = data {
if method == "POST" { if method == "POST" {
@ -687,7 +696,7 @@ impl HttpClient {
return Ok(request); return Ok(request);
} else { } else {
let query = tools::json_object_to_query(data)?; let query = tools::json_object_to_query(data)?;
let url: Uri = format!("https://{}:8007/{}?{}", server, path, query).parse()?; let url: Uri = format!("https://{}:{}/{}?{}", server, port, path, query).parse()?;
let request = Request::builder() let request = Request::builder()
.method(method) .method(method)
.uri(url) .uri(url)

View File

@ -439,7 +439,7 @@ pub async fn pull_group(
.password(Some(auth_info.ticket.clone())) .password(Some(auth_info.ticket.clone()))
.fingerprint(fingerprint.clone()); .fingerprint(fingerprint.clone());
let new_client = HttpClient::new(src_repo.host(), src_repo.user(), options)?; let new_client = HttpClient::new(src_repo.host(), src_repo.port(), src_repo.user(), options)?;
let reader = BackupReader::start( let reader = BackupReader::start(
new_client, new_client,

View File

@ -39,6 +39,11 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t
host: { host: {
schema: DNS_NAME_OR_IP_SCHEMA, schema: DNS_NAME_OR_IP_SCHEMA,
}, },
port: {
optional: true,
description: "The (optional) port",
type: u16,
},
userid: { userid: {
type: Userid, type: Userid,
}, },
@ -58,6 +63,8 @@ pub struct Remote {
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if="Option::is_none")]
pub comment: Option<String>, pub comment: Option<String>,
pub host: String, pub host: String,
#[serde(skip_serializing_if="Option::is_none")]
pub port: Option<u16>,
pub userid: Userid, pub userid: Userid,
#[serde(skip_serializing_if="String::is_empty")] #[serde(skip_serializing_if="String::is_empty")]
#[serde(with = "proxmox::tools::serde::string_as_base64")] #[serde(with = "proxmox::tools::serde::string_as_base64")]