Skip to content

Commit 5a6ceaf

Browse files
committed
feat: Implement SFTP server handler with path traversal prevention
Implement the SFTP server subsystem using russh-sftp library for file transfer operations. The handler provides secure file operations with chroot-like path resolution that prevents clients from accessing files outside their designated root directory.
1 parent fb7759b commit 5a6ceaf

4 files changed

Lines changed: 1307 additions & 9 deletions

File tree

src/server/handler.rs

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use super::auth::AuthProvider;
3131
use super::config::ServerConfig;
3232
use super::exec::CommandExecutor;
3333
use super::session::{ChannelState, PtyConfig, SessionId, SessionInfo, SessionManager};
34+
use super::sftp::SftpHandler;
3435
use crate::shared::rate_limit::RateLimiter;
3536

3637
/// SSH handler for a single client connection.
@@ -186,11 +187,13 @@ impl russh::server::Handler for SshHandler {
186187
let channel_id = channel.id();
187188
tracing::debug!(
188189
peer = ?self.peer_addr,
190+
channel = ?channel_id,
189191
"Channel opened for session"
190192
);
191193

194+
// Store the channel itself so we can use it for subsystems like SFTP
192195
self.channels
193-
.insert(channel_id, ChannelState::new(channel_id));
196+
.insert(channel_id, ChannelState::with_channel(channel));
194197
async { Ok(true) }
195198
}
196199

@@ -626,7 +629,8 @@ impl russh::server::Handler for SshHandler {
626629

627630
/// Handle subsystem request.
628631
///
629-
/// Placeholder implementation - will be implemented in a future issue.
632+
/// Handles SFTP subsystem requests by creating an SftpHandler and running
633+
/// the SFTP server on the channel stream.
630634
fn subsystem_request(
631635
&mut self,
632636
channel_id: ChannelId,
@@ -635,19 +639,106 @@ impl russh::server::Handler for SshHandler {
635639
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
636640
tracing::debug!(
637641
subsystem = %name,
642+
channel = ?channel_id,
643+
peer = ?self.peer_addr,
638644
"Subsystem request"
639645
);
640646

647+
// Handle SFTP subsystem
641648
if name == "sftp" {
642-
if let Some(channel_state) = self.channels.get_mut(&channel_id) {
643-
channel_state.set_sftp();
649+
// Check if SFTP is enabled (default: enabled)
650+
// In future, this should check config.sftp.enabled
651+
652+
// Get the channel from our stored channels
653+
let channel = self.channels.get_mut(&channel_id).and_then(|state| {
654+
state.set_sftp();
655+
state.take_channel()
656+
});
657+
658+
let channel = match channel {
659+
Some(ch) => ch,
660+
None => {
661+
tracing::warn!(
662+
channel = ?channel_id,
663+
"SFTP request but channel not found or already taken"
664+
);
665+
let _ = session.channel_failure(channel_id);
666+
return async { Ok(()) }.boxed();
667+
}
668+
};
669+
670+
// Get authenticated user info
671+
let username = match self.session_info.as_ref().and_then(|s| s.user.clone()) {
672+
Some(user) => user,
673+
None => {
674+
tracing::warn!(
675+
channel = ?channel_id,
676+
"SFTP request without authenticated user"
677+
);
678+
let _ = session.channel_failure(channel_id);
679+
return async { Ok(()) }.boxed();
680+
}
681+
};
682+
683+
// Clone what we need for the async block
684+
let auth_provider = Arc::clone(&self.auth_provider);
685+
let peer_addr = self.peer_addr;
686+
687+
// Signal success before spawning the SFTP handler
688+
let _ = session.channel_success(channel_id);
689+
690+
return async move {
691+
// Get user info from auth provider
692+
let user_info = match auth_provider.get_user_info(&username).await {
693+
Ok(Some(info)) => info,
694+
Ok(None) => {
695+
tracing::error!(
696+
user = %username,
697+
"User not found after authentication for SFTP"
698+
);
699+
return Ok(());
700+
}
701+
Err(e) => {
702+
tracing::error!(
703+
user = %username,
704+
error = %e,
705+
"Failed to get user info for SFTP"
706+
);
707+
return Ok(());
708+
}
709+
};
710+
711+
tracing::info!(
712+
user = %username,
713+
peer = ?peer_addr,
714+
home = %user_info.home_dir.display(),
715+
"Starting SFTP session"
716+
);
717+
718+
// Create SFTP handler with user's home directory as root
719+
let sftp_handler = SftpHandler::new(user_info.clone(), Some(user_info.home_dir));
720+
721+
// Run SFTP server on the channel stream
722+
russh_sftp::server::run(channel.into_stream(), sftp_handler).await;
723+
724+
tracing::info!(
725+
user = %username,
726+
peer = ?peer_addr,
727+
"SFTP session ended"
728+
);
729+
730+
Ok(())
644731
}
732+
.boxed();
645733
}
646734

647-
// Placeholder - reject for now
648-
// Will be implemented in #132 for SFTP
735+
// Unknown subsystem - reject
736+
tracing::debug!(
737+
subsystem = %name,
738+
"Unknown subsystem, rejecting"
739+
);
649740
let _ = session.channel_failure(channel_id);
650-
async { Ok(()) }
741+
async { Ok(()) }.boxed()
651742
}
652743

653744
/// Handle incoming data from the client.

src/server/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub mod config;
4949
pub mod exec;
5050
pub mod handler;
5151
pub mod session;
52+
pub mod sftp;
5253

5354
use std::net::SocketAddr;
5455
use std::path::Path;

src/server/session.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ use std::net::SocketAddr;
3030
use std::sync::atomic::{AtomicU64, Ordering};
3131
use std::time::Instant;
3232

33-
use russh::ChannelId;
33+
use russh::server::Msg;
34+
use russh::{Channel, ChannelId};
3435

3536
/// Unique identifier for an SSH session.
3637
///
@@ -184,11 +185,13 @@ impl PtyConfig {
184185
/// State of an SSH channel.
185186
///
186187
/// Tracks the current mode and configuration of a channel.
187-
#[derive(Debug)]
188188
pub struct ChannelState {
189189
/// The channel ID.
190190
pub channel_id: ChannelId,
191191

192+
/// The underlying channel for subsystem communication.
193+
channel: Option<Channel<Msg>>,
194+
192195
/// Current operation mode.
193196
pub mode: ChannelMode,
194197

@@ -199,17 +202,46 @@ pub struct ChannelState {
199202
pub eof_received: bool,
200203
}
201204

205+
impl std::fmt::Debug for ChannelState {
206+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207+
f.debug_struct("ChannelState")
208+
.field("channel_id", &self.channel_id)
209+
.field("has_channel", &self.channel.is_some())
210+
.field("mode", &self.mode)
211+
.field("pty", &self.pty)
212+
.field("eof_received", &self.eof_received)
213+
.finish()
214+
}
215+
}
216+
202217
impl ChannelState {
203218
/// Create a new channel state.
204219
pub fn new(channel_id: ChannelId) -> Self {
205220
Self {
206221
channel_id,
222+
channel: None,
207223
mode: ChannelMode::Idle,
208224
pty: None,
209225
eof_received: false,
210226
}
211227
}
212228

229+
/// Create a new channel state with the underlying channel.
230+
pub fn with_channel(channel: Channel<Msg>) -> Self {
231+
Self {
232+
channel_id: channel.id(),
233+
channel: Some(channel),
234+
mode: ChannelMode::Idle,
235+
pty: None,
236+
eof_received: false,
237+
}
238+
}
239+
240+
/// Take the underlying channel (consumes it for use with subsystems).
241+
pub fn take_channel(&mut self) -> Option<Channel<Msg>> {
242+
self.channel.take()
243+
}
244+
213245
/// Check if the channel has a PTY attached.
214246
pub fn has_pty(&self) -> bool {
215247
self.pty.is_some()

0 commit comments

Comments
 (0)