Skip to content

Commit 4544b00

Browse files
committed
feat: Implement PTY/shell session support for server (#129)
Add Unix PTY handling for interactive shell sessions in bssh-server: - Create pty.rs module for PTY master/slave pair management with async I/O - Create shell.rs module for shell process spawning and I/O forwarding - Implement shell_request handler to start interactive shell sessions - Implement window_change_request handler for terminal resizing - Update data handler to forward SSH data to active shell sessions - Add shell session state tracking to ChannelState - Enable nix crate 'term' and 'fs' features for PTY operations
1 parent 879b3fa commit 4544b00

6 files changed

Lines changed: 1157 additions & 13 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ regex = "1.12.2"
4343
lazy_static = "1.5"
4444
ctrlc = "3.5.1"
4545
signal-hook = "0.4.1"
46-
nix = { version = "0.30", features = ["poll", "process", "signal"] }
46+
nix = { version = "0.30", features = ["fs", "poll", "process", "signal", "term"] }
4747
atty = "0.2.14"
4848
arrayvec = "0.7.6"
4949
smallvec = "1.15.1"

src/server/handler.rs

Lines changed: 195 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ use zeroize::Zeroizing;
3131
use super::auth::AuthProvider;
3232
use super::config::ServerConfig;
3333
use super::exec::CommandExecutor;
34+
use super::pty::PtyConfig as PtyMasterConfig;
3435
use super::session::{ChannelState, PtyConfig, SessionId, SessionInfo, SessionManager};
3536
use super::sftp::SftpHandler;
37+
use super::shell::ShellSession;
3638
use crate::shared::rate_limit::RateLimiter;
3739

3840
/// SSH handler for a single client connection.
@@ -716,22 +718,124 @@ impl russh::server::Handler for SshHandler {
716718

717719
/// Handle shell request.
718720
///
719-
/// Placeholder implementation - will be implemented in a future issue.
721+
/// Starts an interactive shell session for the authenticated user.
720722
fn shell_request(
721723
&mut self,
722724
channel_id: ChannelId,
723725
session: &mut Session,
724726
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
725-
tracing::debug!("Shell request");
727+
tracing::debug!(channel = ?channel_id, "Shell request");
726728

727-
if let Some(channel_state) = self.channels.get_mut(&channel_id) {
728-
channel_state.set_shell();
729-
}
729+
// Get authenticated user info
730+
let username = match self.session_info.as_ref().and_then(|s| s.user.clone()) {
731+
Some(user) => user,
732+
None => {
733+
tracing::warn!(
734+
channel = ?channel_id,
735+
"Shell request without authenticated user"
736+
);
737+
let _ = session.channel_failure(channel_id);
738+
return async { Ok(()) }.boxed();
739+
}
740+
};
730741

731-
// Placeholder - reject for now
732-
// Will be implemented in #129
733-
let _ = session.channel_failure(channel_id);
734-
async { Ok(()) }
742+
// Get PTY configuration (if set during pty_request)
743+
let pty_config = self
744+
.channels
745+
.get(&channel_id)
746+
.and_then(|state| state.pty.as_ref())
747+
.map(|pty| {
748+
PtyMasterConfig::new(
749+
pty.term.clone(),
750+
pty.col_width,
751+
pty.row_height,
752+
pty.pix_width,
753+
pty.pix_height,
754+
)
755+
})
756+
.unwrap_or_default();
757+
758+
// Clone what we need for the async block
759+
let auth_provider = Arc::clone(&self.auth_provider);
760+
let handle = session.handle();
761+
let peer_addr = self.peer_addr;
762+
763+
// Get mutable reference to channel state
764+
let channels = &mut self.channels;
765+
766+
// Signal success before starting shell
767+
let _ = session.channel_success(channel_id);
768+
769+
async move {
770+
// Get user info from auth provider
771+
let user_info = match auth_provider.get_user_info(&username).await {
772+
Ok(Some(info)) => info,
773+
Ok(None) => {
774+
tracing::error!(
775+
user = %username,
776+
"User not found after authentication for shell"
777+
);
778+
let _ = handle.close(channel_id).await;
779+
return Ok(());
780+
}
781+
Err(e) => {
782+
tracing::error!(
783+
user = %username,
784+
error = %e,
785+
"Failed to get user info for shell"
786+
);
787+
let _ = handle.close(channel_id).await;
788+
return Ok(());
789+
}
790+
};
791+
792+
tracing::info!(
793+
user = %username,
794+
peer = ?peer_addr,
795+
term = %pty_config.term,
796+
size = %format!("{}x{}", pty_config.col_width, pty_config.row_height),
797+
"Starting shell session"
798+
);
799+
800+
// Create shell session
801+
let mut shell_session = match ShellSession::new(channel_id, pty_config) {
802+
Ok(session) => session,
803+
Err(e) => {
804+
tracing::error!(
805+
user = %username,
806+
error = %e,
807+
"Failed to create shell session"
808+
);
809+
let _ = handle.close(channel_id).await;
810+
return Ok(());
811+
}
812+
};
813+
814+
// Start shell session
815+
if let Err(e) = shell_session.start(&user_info, handle.clone()).await {
816+
tracing::error!(
817+
user = %username,
818+
error = %e,
819+
"Failed to start shell session"
820+
);
821+
let _ = handle.close(channel_id).await;
822+
return Ok(());
823+
}
824+
825+
// Store shell session in channel state
826+
if let Some(channel_state) = channels.get_mut(&channel_id) {
827+
channel_state.set_shell_session(shell_session);
828+
}
829+
830+
tracing::info!(
831+
user = %username,
832+
peer = ?peer_addr,
833+
"Shell session started"
834+
);
835+
836+
Ok(())
837+
}
838+
.boxed()
735839
}
736840

737841
/// Handle subsystem request.
@@ -849,19 +953,98 @@ impl russh::server::Handler for SshHandler {
849953
}
850954

851955
/// Handle incoming data from the client.
956+
///
957+
/// Forwards data to the active shell session if one exists.
852958
fn data(
853959
&mut self,
854-
_channel_id: ChannelId,
960+
channel_id: ChannelId,
855961
data: &[u8],
856962
_session: &mut Session,
857963
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
858964
tracing::trace!(
965+
channel = ?channel_id,
859966
bytes = %data.len(),
860967
"Received data"
861968
);
862969

863-
// Placeholder - data handling will be implemented with exec/shell/sftp
864-
async { Ok(()) }
970+
// Get the data sender if there's an active shell session
971+
let data_sender = self
972+
.channels
973+
.get(&channel_id)
974+
.and_then(|state| state.shell_session.as_ref())
975+
.and_then(|shell| shell.data_sender());
976+
977+
if let Some(tx) = data_sender {
978+
let data = data.to_vec();
979+
return async move {
980+
if let Err(e) = tx.send(data).await {
981+
tracing::debug!(
982+
channel = ?channel_id,
983+
error = %e,
984+
"Error forwarding data to shell"
985+
);
986+
}
987+
Ok(())
988+
}
989+
.boxed();
990+
}
991+
992+
async { Ok(()) }.boxed()
993+
}
994+
995+
/// Handle window size change request.
996+
///
997+
/// Updates the PTY window size for active shell sessions.
998+
#[allow(clippy::too_many_arguments)]
999+
fn window_change_request(
1000+
&mut self,
1001+
channel_id: ChannelId,
1002+
col_width: u32,
1003+
row_height: u32,
1004+
pix_width: u32,
1005+
pix_height: u32,
1006+
_session: &mut Session,
1007+
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
1008+
tracing::debug!(
1009+
channel = ?channel_id,
1010+
cols = col_width,
1011+
rows = row_height,
1012+
"Window change request"
1013+
);
1014+
1015+
// Update stored PTY config
1016+
if let Some(state) = self.channels.get_mut(&channel_id) {
1017+
if let Some(ref mut pty) = state.pty {
1018+
pty.col_width = col_width;
1019+
pty.row_height = row_height;
1020+
pty.pix_width = pix_width;
1021+
pty.pix_height = pix_height;
1022+
}
1023+
}
1024+
1025+
// Get the PTY mutex if there's an active shell session
1026+
let pty_mutex = self
1027+
.channels
1028+
.get(&channel_id)
1029+
.and_then(|state| state.shell_session.as_ref())
1030+
.map(|shell| Arc::clone(shell.pty()));
1031+
1032+
if let Some(pty) = pty_mutex {
1033+
return async move {
1034+
let mut pty_guard = pty.lock().await;
1035+
if let Err(e) = pty_guard.resize(col_width, row_height) {
1036+
tracing::debug!(
1037+
channel = ?channel_id,
1038+
error = %e,
1039+
"Error resizing shell PTY"
1040+
);
1041+
}
1042+
Ok(())
1043+
}
1044+
.boxed();
1045+
}
1046+
1047+
async { Ok(()) }.boxed()
8651048
}
8661049

8671050
/// Handle channel EOF from the client.

src/server/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ pub mod auth;
4848
pub mod config;
4949
pub mod exec;
5050
pub mod handler;
51+
pub mod pty;
5152
pub mod session;
5253
pub mod sftp;
54+
pub mod shell;
5355

5456
use std::net::SocketAddr;
5557
use std::path::Path;
@@ -66,9 +68,11 @@ use crate::shared::rate_limit::RateLimiter;
6668
pub use self::config::{ServerConfig, ServerConfigBuilder};
6769
pub use self::exec::{CommandExecutor, ExecConfig};
6870
pub use self::handler::SshHandler;
71+
pub use self::pty::{PtyConfig as PtyMasterConfig, PtyMaster};
6972
pub use self::session::{
7073
ChannelMode, ChannelState, PtyConfig, SessionId, SessionInfo, SessionManager,
7174
};
75+
pub use self::shell::ShellSession;
7276

7377
/// The main SSH server struct.
7478
///

0 commit comments

Comments
 (0)