put: degrade to the anonymous FTP queue when the SSH transport fails
pkh put only spoke SFTP to the PPA queue, so a failure of the SSH transport itself (TCP, banner exchange) failed the upload even though dput happily pushes the same files: its plain ppa: profile goes over the anonymous FTP queue of ppa.launchpad.net, the same destination over another port. Classify the SSH connection failures: Transport (the connection never came up: resolution, TCP, banner or key exchange) degrades to that FTP queue — the upload order (payload first, .changes last), the reverse-order DELE cleanup of a failed upload and the per-chunk progress reporting all mirror the SFTP path, sharing cleanup_list. Refused failures (host key not accepted, no matching authentication) stay errors: silently switching transport would bypass the refusal. The FTP client is suppaftp's blocking stream, with the time bounds it does not carry by itself: the control channel's reads and writes, the data channel's writes and connect (through a custom passive stream builder), and the NAT workaround for PASV replies announcing an unroutable address. The queue endpoints (host, port) join data/launchpad.yml next to the SFTP ones, and the FTP transport is covered by unit tests against an in-process fake queue plus a live control-channel handshake with the real server (ignored, network).
This commit is contained in:
+46
-8
@@ -253,7 +253,9 @@ fn duration_ms(timeout: Duration) -> u32 {
|
||||
/// address in order (like `TcpStream::connect` does) with
|
||||
/// [`TCP_CONNECT_TIMEOUT`] per attempt instead of blocking indefinitely.
|
||||
/// Fails with a message naming the target and every per-address error.
|
||||
fn tcp_connect(host: &str, port: u16) -> Result<TcpStream, String> {
|
||||
/// Also the TCP layer of the anonymous FTP fallback transport
|
||||
/// ([`super::ftp`]), whose connection semantics are identical.
|
||||
pub(crate) fn tcp_connect(host: &str, port: u16) -> Result<TcpStream, String> {
|
||||
let addrs: Vec<SocketAddr> = (host, port)
|
||||
.to_socket_addrs()
|
||||
.map_err(|e| format!("cannot resolve {host}:{port}: {e}"))?
|
||||
@@ -292,6 +294,39 @@ fn connect_failed_message(
|
||||
attempts.len()
|
||||
)
|
||||
}
|
||||
/// Why establishing the SSH session failed.
|
||||
///
|
||||
/// [`Transport`] failures mean the connection never came up (name
|
||||
/// resolution, TCP, banner or key exchange): the target may still be
|
||||
/// reachable over another transport, so `pkh put` degrades to the
|
||||
/// anonymous FTP queue — dput's default for PPAs ([`super::ftp`]).
|
||||
/// [`Refused`] failures mean the server answered but rejected the
|
||||
/// upload (host key not accepted, no matching authentication): silently
|
||||
/// switching to anonymous FTP would bypass a refusal, so they stay
|
||||
/// errors.
|
||||
#[derive(Debug)]
|
||||
pub enum ConnectFailure {
|
||||
/// The connection itself never came up.
|
||||
Transport(Box<dyn std::error::Error>),
|
||||
/// The server answered but rejected the upload.
|
||||
Refused(Box<dyn std::error::Error>),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConnectFailure {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ConnectFailure::Transport(e) | ConnectFailure::Refused(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConnectFailure {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
ConnectFailure::Transport(e) | ConnectFailure::Refused(e) => Some(e.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to `host:port`, verify the server host key and authenticate as
|
||||
/// `login`: every ssh-agent identity first, then the configured and default
|
||||
@@ -303,10 +338,13 @@ pub fn connect(
|
||||
login: &str,
|
||||
config: &SshConfig,
|
||||
prompter: &dyn Prompter,
|
||||
) -> Result<Session, Box<dyn std::error::Error>> {
|
||||
let tcp = tcp_connect(host, port)?;
|
||||
) -> Result<Session, ConnectFailure> {
|
||||
use ConnectFailure::*;
|
||||
|
||||
let mut session = Session::new()?;
|
||||
let tcp = tcp_connect(host, port).map_err(|e| Transport(e.into()))?;
|
||||
|
||||
let mut session = Session::new()
|
||||
.map_err(|e| Transport(format!("cannot initialize the SSH session: {e}").into()))?;
|
||||
// In blocking mode (the libssh2 default), a call that would block loops
|
||||
// in `_libssh2_wait_socket` (via the `BLOCK_ADJUST` macros of
|
||||
// session.h in the vendored libssh2-sys sources), which bounds the
|
||||
@@ -321,14 +359,14 @@ pub fn connect(
|
||||
session.set_tcp_stream(tcp);
|
||||
session
|
||||
.handshake()
|
||||
.map_err(|e| format!("SSH handshake with {host} failed: {e}"))?;
|
||||
.map_err(|e| Transport(format!("SSH handshake with {host} failed: {e}").into()))?;
|
||||
|
||||
let (key, key_type) = session
|
||||
.host_key()
|
||||
.ok_or_else(|| format!("{host} offered no host key"))?;
|
||||
verify_host_key(host, port, key, key_type, prompter)?;
|
||||
.ok_or_else(|| Transport(format!("{host} offered no host key").into()))?;
|
||||
verify_host_key(host, port, key, key_type, prompter).map_err(Refused)?;
|
||||
|
||||
authenticate(&session, host, login, config)?;
|
||||
authenticate(&session, host, login, config).map_err(Refused)?;
|
||||
|
||||
// Only SFTP open/data calls remain on this session: switch from the
|
||||
// connection-phase budget to the generous per-call transfer one
|
||||
|
||||
Reference in New Issue
Block a user