05 — Channels API¶
Header: <sshpp/channel.hpp>, <sshpp/channel_stream.hpp>, <sshpp/exec.hpp>,
<sshpp/shell.hpp>, <sshpp/event.hpp>.
5.1 Channel — the Layer-3 primitive¶
namespace sshpp {
enum class Stream { stdout_, stderr_ };
/// Signals accepted by ssh_channel_request_send_signal (RFC 4254 §6.10, no "SIG" prefix).
enum class Signal { abrt, alrm, fpe, hup, ill, int_, kill, pipe, quit, segv, term, usr1, usr2 };
struct ExitState {
std::optional<int> status; // exit-status
std::optional<std::string> signal; // exit-signal, e.g. "TERM"
bool core_dumped = false;
std::string error_message;
};
struct PtySize { int columns = 80; int rows = 24; };
class SSHPP_API Channel {
public:
Channel() = default; // empty
~Channel(); // close + ssh_channel_free
Channel(Channel&&) noexcept;
Channel& operator=(Channel&&) noexcept;
Channel(const Channel&) = delete;
explicit operator bool() const noexcept;
native_channel native_handle() const noexcept;
static Channel from_native(native_channel, const Session&, Ownership);
Session session() const; // strong ref to the owning session
// ---- opening --------------------------------------------------------
Result<void> try_open_session(); // ssh_channel_open_session
bool is_open() const noexcept;
// ---- requests --------------------------------------------------------
Result<void> try_request_pty(std::string_view term = "xterm-256color",
PtySize = {});
Result<void> try_change_pty_size(PtySize);
Result<void> try_request_shell();
Result<void> try_request_exec(std::string_view command);
Result<void> try_request_subsystem(std::string_view name); // e.g. "sftp", "netconf"
Result<void> try_request_env(std::string_view name, std::string_view value);
Result<void> try_request_x11(const X11Request&); // see doc 07
Result<void> try_send_signal(Signal);
Result<void> try_send_break(std::chrono::milliseconds); // ssh_channel_request_send_break
// ---- I/O ---------------------------------------------------------------
/// Blocking read of up to buf.size() bytes. Returns 0 on EOF.
Result<std::size_t> try_read_some(MutableByteView buf, Stream = Stream::stdout_);
Result<std::size_t> try_read_some(MutableByteView buf, Stream,
std::chrono::milliseconds timeout);
/// Never blocks; returns 0 if nothing buffered.
Result<std::size_t> try_read_available(MutableByteView buf, Stream = Stream::stdout_);
/// Reads exactly buf.size() bytes or fails with channel_eof / timed_out.
Result<void> try_read_exact(MutableByteView buf, Stream = Stream::stdout_);
/// Reads until EOF. `limit` guards against unbounded memory use.
Result<std::string> try_read_all(Stream = Stream::stdout_,
std::size_t limit = 64u << 20);
Result<std::size_t> try_write_some(ByteView data); // may write less
Result<void> try_write_all(ByteView data); // loops
Result<void> try_write_all(std::string_view data);
Result<std::size_t> try_write_stderr(ByteView data); // server side only
/// Bytes immediately readable; errc::channel_eof when the peer closed the stream.
Result<std::size_t> try_bytes_available(Stream = Stream::stdout_) const;
Result<bool> try_wait_readable(std::chrono::milliseconds,
Stream = Stream::stdout_);
// ---- closing --------------------------------------------------------------
Result<void> try_send_eof();
bool is_eof() const noexcept; // peer sent EOF
Result<void> try_close();
bool is_closed() const noexcept;
// ---- exit status -----------------------------------------------------------
/// Blocks until the remote command exits (or the channel closes).
Result<ExitState> try_wait_exit(std::chrono::milliseconds timeout =
std::chrono::milliseconds::max());
/// Non-blocking snapshot; `status` is nullopt if not yet received.
ExitState exit_state() const noexcept;
// ---- forwarding factories (see doc 07) ---------------------------------------
static Result<Channel> open_forward(Session&, std::string_view remote_host,
std::uint16_t remote_port,
std::string_view origin_host, std::uint16_t origin_port);
static Result<Channel> open_forward_unix(Session&, std::string_view remote_socket,
std::string_view origin_host, std::uint16_t origin_port);
};
} // namespace sshpp
Design notes¶
exit_state()usesssh_channel_get_exit_state()when the linked libssh is ≥ 0.11 (SSHPP_HAS_CHANNEL_EXIT_STATE), falling back tossh_channel_get_exit_status()otherwise — in the fallback,ExitState::signalis alwaysnulloptand this is documented.try_wait_exit()implements the required drain: libssh only surfaces the exit status once the channel has been read to EOF, sotry_wait_exitloops onssh_channel_read_timeoutfor both streams, discarding data, until EOF or close. A[[nodiscard]]warning plus a runtime diagnostic fires if unread data is discarded, because that is usually a bug — useExec(§5.3) instead.try_read_somewithStream::stderr_maps tossh_channel_read_timeout(..., is_stderr=1).- Interleaving hazard. libssh multiplexes stdout/stderr over one channel; reading only
stdout can stall if the peer fills the stderr window.
ExecandChannelPump(§5.5) read both viassh_channel_select/ssh_event, and theChanneldocs warn about the single-stream loop. try_write_allhandles short writes andSSH_AGAIN.- Destructor order:
try_close()thenssh_channel_free(). Failures go to the destructor error handler.
5.2 <sshpp/channel_stream.hpp> — iostream adapters¶
Thin, opt-in, Layer-4:
namespace sshpp {
class SSHPP_API ChannelStreambuf final : public std::streambuf {
public:
ChannelStreambuf(Channel&, Stream read_from = Stream::stdout_,
std::size_t buffer_size = 16 * 1024);
// underflow/overflow/sync implemented over Channel::try_read_some / try_write_some.
// Errors set the associated stream's badbit AND are retrievable via last_error().
const ErrorInfo& last_error() const noexcept;
};
class SSHPP_API ChannelIStream final : public std::istream { /* owns a streambuf */ };
class SSHPP_API ChannelOStream final : public std::ostream { /* flush() -> write + sync */ };
class SSHPP_API ChannelIOStream final : public std::iostream {};
} // namespace sshpp
Rationale: iostreams cannot report ErrorInfo, so they are explicitly a convenience for
line-oriented protocols (std::getline over a subsystem channel). last_error() preserves the
detail. They are not used internally by any other part of the library.
5.3 <sshpp/exec.hpp> — one-shot remote commands¶
The single most common use case gets a dedicated, hard-to-misuse type.
namespace sshpp {
struct ExecResult {
int exit_code = -1;
std::optional<std::string> exit_signal;
std::string stdout_text;
std::string stderr_text;
bool stdout_truncated = false;
bool stderr_truncated = false;
explicit operator bool() const noexcept { return exit_code == 0; }
/// Throws ChannelError with the captured stderr if exit_code != 0.
const ExecResult& check() const;
};
/// Sink callbacks for streaming instead of buffering.
using OutputSink = std::function<void(Stream, ByteView)>;
class SSHPP_API Exec {
public:
explicit Exec(Session&);
Exec& env(std::string_view name, std::string_view value); // channel "env" requests
Exec& pty(bool enable = true, std::string_view term = "xterm-256color", PtySize = {});
Exec& stdin_data(std::string data); // written then EOF
Exec& stdin_stream(std::istream&);
Exec& timeout(std::chrono::milliseconds);
Exec& max_output(std::size_t bytes); // default 16 MiB per stream
Exec& merge_stderr(bool = true); // stderr appended to stdout_text
Exec& sink(OutputSink); // disables buffering
Result<ExecResult> try_run(std::string_view command);
ExecResult run(std::string_view command);
/// Builds an argv-style command with correct POSIX shell quoting.
Result<ExecResult> try_run(std::vector<std::string> argv);
};
/// Escapes a single argument for a POSIX shell. Exposed because callers need it.
SSHPP_API std::string shell_quote(std::string_view);
} // namespace sshpp
Security note (OWASP A03 — Injection): Exec::run(std::string_view) passes the string to the
remote shell verbatim. The std::vector<std::string> argv overload applies shell_quote to
every element and is the documented default recommendation for any command built from
untrusted input. Documentation and the header both carry this warning; examples use the argv
form.
Exec internally:
- opens a channel, applies
env/ptyrequests, request_exec,- runs a select-driven pump over stdout, stderr and (if provided) stdin,
- sends EOF on stdin, waits for EOF on both output streams,
- closes and collects
ExitState.
This is the loop most users get wrong; it exists once, here.
5.4 <sshpp/shell.hpp> — interactive sessions¶
namespace sshpp {
class SSHPP_API Shell {
public:
struct Options {
std::string term = "xterm-256color";
PtySize size{};
std::vector<std::pair<std::string, std::string>> env;
bool request_pty = true;
};
Shell(Session&, Options = {});
Result<void> try_start(); // pty + shell requests
Channel& channel() noexcept; // full access for advanced use
Result<std::size_t> try_write(std::string_view);
Result<std::string> try_read(std::chrono::milliseconds timeout, Stream = Stream::stdout_);
Result<void> try_resize(PtySize);
Result<void> try_send_signal(Signal);
/// Blocking bidirectional relay between the shell and local stdin/stdout.
/// Handles terminal raw-mode, SIGWINCH-driven resize, and ~. escape.
/// Only available when LIBSSHPP_WITH_CONSOLE=ON.
Result<ExitState> try_interact(InteractOptions = {});
};
} // namespace sshpp
try_interact() is what makes a usable ssh clone; it is deliberately isolated behind a build
option because it needs termios/SetConsoleMode and signal handling that a library embedded
in a server has no business installing.
5.5 <sshpp/event.hpp> — poll integration¶
ssh_event is the seam for integrating with an external event loop (and the basis of the
future async layer).
namespace sshpp {
enum class PollFlags : unsigned { none = 0, in = 1, pri = 2, out = 4, err = 8, hup = 16 };
SSHPP_API PollFlags operator|(PollFlags, PollFlags) noexcept; // + &, ~, etc.
class SSHPP_API Event {
public:
Event(); // ssh_event_new
~Event();
Result<void> try_add_session(Session&);
Result<void> try_remove_session(Session&);
Result<void> try_add_fd(int fd, PollFlags, std::function<PollFlags(int, PollFlags)> cb);
Result<void> try_remove_fd(int fd);
Result<void> try_add_connector(/* ssh_connector, see doc 07 */);
/// SSH_OK / SSH_AGAIN(timeout) / SSH_ERROR.
Result<void> try_poll(std::chrono::milliseconds timeout);
};
/// Copies bytes in both directions between two things (channel<->fd, channel<->channel)
/// using ssh_connector under the hood. The building block of port forwarding.
class SSHPP_API Connector { /* see doc 07 §7.5 */ };
/// Convenience: waits until any of the given channels has data, using ssh_channel_select.
SSHPP_API Result<std::vector<Channel*>> select_channels(
std::vector<Channel*> read_set, std::chrono::milliseconds timeout);
} // namespace sshpp
For users on Asio/libuv/Qt: Session::socket_fd() plus Session::try_set_blocking(false) and
the errc::would_block contract is enough to drive libssh from a foreign loop; a worked example
is shipped in examples/ but no dependency on those libraries is taken.
5.6 Examples¶
Run a command safely:
auto r = sshpp::Exec{session}
.timeout(std::chrono::seconds{30})
.try_run(std::vector<std::string>{"grep", "-r", user_supplied, "/var/log"});
if (!r) return fail(r.error());
if (r->exit_code != 0) std::cerr << r->stderr_text;
Stream a large output without buffering:
std::ofstream out{"dump.bin", std::ios::binary};
sshpp::Exec{session}
.sink([&](sshpp::Stream s, sshpp::ByteView b) {
if (s == sshpp::Stream::stdout_)
out.write(reinterpret_cast<const char*>(b.data()),
static_cast<std::streamsize>(b.size()));
})
.run("cat /var/lib/backup.tar");
Raw channel, subsystem:
auto ch = session.open_channel();
ch.try_request_subsystem("netconf").value();
sshpp::ChannelIOStream io{ch};
io << hello_message << std::flush;
std::string line;
std::getline(io, line);