-
Notifications
You must be signed in to change notification settings - Fork 1
Closed
Labels
priority:mediumMedium priority issueMedium priority issuestatus:doneCompletedCompletedtype:enhancementNew feature or requestNew feature or request
Description
Summary
Implement PTY (pseudo-terminal) and interactive shell session support for bssh-server. This enables users to get an interactive login shell through SSH.
Parent Epic
- Implement bssh-server with SFTP/SCP support #123 - bssh-server 추가 구현
- Depends on: Implement command execution handler for server #128 (command execution handler)
Implementation Details
1. PTY Manager
// src/server/pty.rs
use std::os::unix::io::{AsRawFd, RawFd};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
/// PTY configuration from pty_request
#[derive(Debug, Clone)]
pub struct PtyConfig {
pub term: String,
pub col_width: u32,
pub row_height: u32,
pub pix_width: u32,
pub pix_height: u32,
pub modes: Vec<(russh::Pty, u32)>,
}
/// Manages a pseudo-terminal session
pub struct PtySession {
config: PtyConfig,
master_fd: RawFd,
slave_path: PathBuf,
child: Option<Child>,
}
impl PtySession {
/// Create a new PTY session
pub fn new(config: PtyConfig) -> Result<Self> {
// Open PTY master/slave pair
let (master_fd, slave_path) = Self::open_pty()?;
// Set terminal size
Self::set_window_size(master_fd, config.col_width, config.row_height)?;
Ok(Self {
config,
master_fd,
slave_path,
child: None,
})
}
/// Open a new PTY pair
fn open_pty() -> Result<(RawFd, PathBuf)> {
use nix::pty::{openpty, OpenptyResult};
use nix::unistd::close;
let OpenptyResult { master, slave } = openpty(None, None)
.context("Failed to open PTY")?;
// Get slave path
let slave_path = nix::unistd::ttyname(slave)
.context("Failed to get slave path")?;
// Close slave fd (will be reopened by child)
close(slave).ok();
Ok((master, slave_path))
}
/// Set terminal window size
fn set_window_size(fd: RawFd, cols: u32, rows: u32) -> Result<()> {
use nix::pty::Winsize;
use nix::libc::{ioctl, TIOCSWINSZ};
let winsize = Winsize {
ws_row: rows as u16,
ws_col: cols as u16,
ws_xpixel: 0,
ws_ypixel: 0,
};
unsafe {
if ioctl(fd, TIOCSWINSZ, &winsize) < 0 {
anyhow::bail!("Failed to set window size");
}
}
Ok(())
}
/// Spawn shell process
pub async fn spawn_shell(&mut self, user_info: &UserInfo) -> Result<()> {
use std::os::unix::process::CommandExt;
let slave_path = self.slave_path.clone();
let shell = user_info.shell.clone();
let home_dir = user_info.home_dir.clone();
let username = user_info.username.clone();
let env = user_info.env.clone();
let term = self.config.term.clone();
// Spawn in separate thread due to pre_exec
let child = tokio::task::spawn_blocking(move || -> Result<Child> {
let slave_fd = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&slave_path)?;
let mut cmd = std::process::Command::new(&shell);
cmd.arg("-l"); // Login shell
// Set environment
cmd.env_clear();
cmd.env("HOME", &home_dir);
cmd.env("USER", &username);
cmd.env("SHELL", &shell);
cmd.env("TERM", &term);
cmd.env("PATH", "/usr/local/bin:/usr/bin:/bin");
for (key, value) in &env {
cmd.env(key, value);
}
cmd.current_dir(&home_dir);
// Set up stdio to use PTY slave
cmd.stdin(slave_fd.try_clone()?);
cmd.stdout(slave_fd.try_clone()?);
cmd.stderr(slave_fd);
// Create new session and set controlling terminal
unsafe {
cmd.pre_exec(|| {
nix::unistd::setsid()?;
// TIOCSCTTY - set controlling terminal
nix::libc::ioctl(0, nix::libc::TIOCSCTTY, 0);
Ok(())
});
}
let child = cmd.spawn()?;
Ok(Child::from_std(child)?)
}).await??;
self.child = Some(child);
Ok(())
}
/// Handle window size change
pub fn resize(&self, cols: u32, rows: u32) -> Result<()> {
Self::set_window_size(self.master_fd, cols, rows)
}
/// Get async reader for master fd
pub fn reader(&self) -> Result<impl AsyncRead + '_> {
use tokio::io::unix::AsyncFd;
// Implementation using AsyncFd
todo!()
}
/// Get async writer for master fd
pub fn writer(&self) -> Result<impl AsyncWrite + '_> {
todo!()
}
}
impl Drop for PtySession {
fn drop(&mut self) {
// Kill child process if still running
if let Some(ref mut child) = self.child {
let _ = child.start_kill();
}
// Close master fd
let _ = nix::unistd::close(self.master_fd);
}
}2. Shell Session Handler
// src/server/shell.rs
pub struct ShellSession {
pty: PtySession,
channel_id: ChannelId,
}
impl ShellSession {
pub fn new(pty_config: PtyConfig, channel_id: ChannelId) -> Result<Self> {
let pty = PtySession::new(pty_config)?;
Ok(Self { pty, channel_id })
}
/// Start the shell session
pub async fn start(
&mut self,
user_info: &UserInfo,
handle: Handle<SshHandler>,
) -> Result<()> {
self.pty.spawn_shell(user_info).await?;
// Start I/O forwarding tasks
let channel_id = self.channel_id;
// Forward PTY output to SSH channel
let pty_reader = self.pty.reader()?;
let handle_clone = handle.clone();
tokio::spawn(async move {
let mut buf = [0u8; 4096];
loop {
match pty_reader.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
if handle_clone.data(channel_id, CryptoVec::from_slice(&buf[..n])).await.is_err() {
break;
}
}
Err(_) => break,
}
}
});
Ok(())
}
/// Handle data from SSH channel (forward to PTY)
pub async fn handle_data(&mut self, data: &[u8]) -> Result<()> {
let mut writer = self.pty.writer()?;
writer.write_all(data).await?;
Ok(())
}
/// Handle window resize
pub fn resize(&self, cols: u32, rows: u32) -> Result<()> {
self.pty.resize(cols, rows)
}
}3. Integrate with SSH Handler
// Update src/server/handler.rs
impl Handler for SshHandler {
async fn pty_request(
&mut self,
channel_id: ChannelId,
term: &str,
col_width: u32,
row_height: u32,
pix_width: u32,
pix_height: u32,
modes: &[(russh::Pty, u32)],
session: &mut Session,
) -> Result<(), Self::Error> {
tracing::debug!(
"PTY request: term={}, size={}x{}",
term, col_width, row_height
);
let pty_config = PtyConfig {
term: term.to_string(),
col_width,
row_height,
pix_width,
pix_height,
modes: modes.to_vec(),
};
// Store PTY config for when shell_request comes
if let Some(channel_state) = self.channels.get_mut(&channel_id) {
channel_state.pty_config = Some(pty_config);
}
session.channel_success(channel_id)?;
Ok(())
}
async fn shell_request(
&mut self,
channel_id: ChannelId,
session: &mut Session,
) -> Result<(), Self::Error> {
tracing::debug!("Shell request on channel {}", channel_id);
// Get user info
let user = self.user.as_ref()
.ok_or_else(|| anyhow::anyhow!("No authenticated user"))?;
let user_info = self.config.auth_provider
.get_user_info(user).await?
.ok_or_else(|| anyhow::anyhow!("User not found"))?;
// Get PTY config
let channel_state = self.channels.get_mut(&channel_id)
.ok_or_else(|| anyhow::anyhow!("Channel not found"))?;
let pty_config = channel_state.pty_config.take()
.unwrap_or_else(|| PtyConfig::default());
// Create and start shell session
let mut shell_session = ShellSession::new(pty_config, channel_id)?;
shell_session.start(&user_info, session.handle()).await?;
channel_state.shell_session = Some(shell_session);
channel_state.mode = ChannelMode::Shell;
session.channel_success(channel_id)?;
Ok(())
}
async fn window_change_request(
&mut self,
channel_id: ChannelId,
col_width: u32,
row_height: u32,
pix_width: u32,
pix_height: u32,
session: &mut Session,
) -> Result<(), Self::Error> {
if let Some(channel_state) = self.channels.get(&channel_id) {
if let Some(ref shell_session) = channel_state.shell_session {
shell_session.resize(col_width, row_height)?;
}
}
Ok(())
}
async fn data(
&mut self,
channel_id: ChannelId,
data: &[u8],
session: &mut Session,
) -> Result<(), Self::Error> {
if let Some(channel_state) = self.channels.get_mut(&channel_id) {
match channel_state.mode {
ChannelMode::Shell => {
if let Some(ref mut shell) = channel_state.shell_session {
shell.handle_data(data).await?;
}
}
_ => {}
}
}
Ok(())
}
}Platform Considerations
- This implementation uses POSIX PTY APIs (Unix-specific)
- Windows support would require ConPTY (future enhancement)
Files to Create/Modify
| File | Action |
|---|---|
src/server/pty.rs |
Create - PTY management |
src/server/shell.rs |
Create - Shell session handler |
src/server/handler.rs |
Modify - Implement PTY/shell handlers |
src/server/session.rs |
Modify - Add shell session state |
src/server/mod.rs |
Modify - Add pty, shell modules |
Testing Requirements
- Unit test: PTY creation
- Unit test: Window size setting
- Integration test: Interactive shell session
- Integration test: Window resize
- Integration test: Signal handling (Ctrl+C, Ctrl+D)
# Test interactive shell
ssh -p 2222 testuser@localhost
# Should get login shell prompt
# Test window resize
# Test Ctrl+C interrupts command
# Test Ctrl+D exits shellAcceptance Criteria
- PTY pair creation using nix crate
- Shell process spawning with PTY slave
- Terminal environment setup (TERM, etc.)
- Bidirectional I/O forwarding
- Window size change handling
- Proper process cleanup on disconnect
- Signal propagation (Ctrl+C, etc.)
- Login shell flag support
- Tests passing
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
priority:mediumMedium priority issueMedium priority issuestatus:doneCompletedCompletedtype:enhancementNew feature or requestNew feature or request