Hack together support for legacy scp protocol
Diff
Cargo.lock | 4 +-
pisshoff-server/src/command.rs | 55 ++++++-
pisshoff-server/src/command/scp.rs | 276 ++++++++++++++++++++++++++++++++++-
pisshoff-server/src/server.rs | 61 ++------
pisshoff-server/src/subsystem/mod.rs | 5 +-
pisshoff-server/src/subsystem/sftp.rs | 25 +--
pisshoff-server/src/subsystem/shell.rs | 82 ++++++++++-
pisshoff-types/Cargo.toml | 1 +-
pisshoff-types/src/audit.rs | 3 +-
9 files changed, 456 insertions(+), 56 deletions(-)
@@ -198,6 +198,9 @@ name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
dependencies = [
"serde",
]
[[package]]
name = "cc"
@@ -1104,6 +1107,7 @@ dependencies = [
name = "pisshoff-types"
version = "0.1.0"
dependencies = [
"bytes",
"serde",
"strum",
"time",
@@ -1,6 +1,8 @@
pub mod scp;
pub mod uname;
use crate::server::Connection;
use crate::{command::scp::Scp, server::Connection};
use async_trait::async_trait;
use itertools::{Either, Itertools};
use std::{f32, fmt::Write, str::FromStr, time::Duration};
use thrussh::{server::Session, ChannelId};
@@ -10,9 +12,9 @@ pub async fn run_command(
channel: ChannelId,
session: &mut Session,
conn: &mut Connection,
) {
) -> Option<ConcreteLongRunningCommand> {
let Some(command) = args.get(0) else {
return;
return None;
};
match command.as_str() {
@@ -61,7 +63,7 @@ pub async fn run_command(
channel,
"-bash: cd: too many arguments\n".to_string().into(),
);
return;
return None;
}
conn.file_system().cd(args.get(1).map(String::as_str));
@@ -85,6 +87,10 @@ pub async fn run_command(
let out = uname::execute(&args[1..]);
session.data(channel, out.into());
}
"scp" => match Scp::new(&args[1..], channel, session) {
Ok(v) => return Some(ConcreteLongRunningCommand::Scp(v)),
Err(e) => session.data(channel, e.to_string().into()),
},
other => {
session.data(
@@ -93,6 +99,47 @@ pub async fn run_command(
);
}
}
None
}
#[async_trait]
pub trait LongRunningCommand: Sized {
fn new(
params: &[String],
channel: ChannelId,
session: &mut Session,
) -> Result<Self, &'static str>;
async fn data(
self,
connection: &mut Connection,
channel: ChannelId,
data: &[u8],
session: &mut Session,
) -> Option<Self>;
}
#[derive(Debug, Clone)]
pub enum ConcreteLongRunningCommand {
Scp(Scp),
}
impl ConcreteLongRunningCommand {
pub async fn data(
self,
connection: &mut Connection,
channel: ChannelId,
data: &[u8],
session: &mut Session,
) -> Option<Self> {
match self {
Self::Scp(cmd) => cmd
.data(connection, channel, data, session)
.await
.map(Self::Scp),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -0,0 +1,276 @@
use crate::{
command::{Arg, LongRunningCommand},
server::Connection,
};
use async_trait::async_trait;
use bytes::{Buf, BytesMut};
use nom::{
bytes::complete::{tag, take, take_until},
character::complete::{digit1, u64},
combinator::{map, map_res},
IResult,
};
use pisshoff_types::audit::{AuditLogAction, WriteFileEvent};
use std::{path::PathBuf, str::FromStr};
use thrussh::{server::Session, ChannelId};
use tracing::warn;
const HELP: &str = "usage: scp [-346ABCOpqRrsTv] [-c cipher] [-D sftp_server_path] [-F ssh_config]
[-i identity_file] [-J destination] [-l limit] [-o ssh_option]
[-P port] [-S program] [-X sftp_option] source ... target\n";
const AMBIGUOUS_TARGET: &str = "scp: ambiguous target\n";
const SUCCESS: &str = "\0";
#[derive(Debug, Clone)]
pub struct Scp {
path: PathBuf,
pending_data: BytesMut,
state: State,
}
#[async_trait]
impl LongRunningCommand for Scp {
fn new(
params: &[String],
channel: ChannelId,
session: &mut Session,
) -> Result<Self, &'static str> {
let mut path = None;
let mut transfer = false;
for param in super::argparse(params) {
match param {
Arg::Short('t') => {
transfer = true;
}
Arg::Short('r' | 'v') => {
}
Arg::Operand(p) => {
path = Some(p);
}
_ => {
return Err(HELP);
}
}
}
let Some(path) = path else {
return Err(AMBIGUOUS_TARGET);
};
if !transfer {
return Err(HELP);
}
session.data(channel, SUCCESS.to_string().into());
Ok(Self {
path: PathBuf::new().join(path),
pending_data: BytesMut::new(),
state: State::Waiting,
})
}
async fn data(
mut self,
connection: &mut Connection,
channel: ChannelId,
data: &[u8],
session: &mut Session,
) -> Option<Self> {
self.pending_data.extend_from_slice(data);
let mut exit = false;
while !self.pending_data.is_empty() && !exit {
let next_state = match self.state {
State::Waiting => {
match Receive::parse(&self.pending_data) {
Ok((rest, res)) => {
let mut state = State::Waiting;
match res {
Receive::FileCopy {
length, file_name, ..
} => {
state = State::ReceivingFile(length, self.path.join(file_name));
}
Receive::DirectoryCopy { directory_name, .. } => {
self.path.push(directory_name);
}
Receive::EndDirectory => {
self.path.pop();
}
Receive::AccessTime { .. } => {}
}
self.pending_data
.advance(self.pending_data.len() - rest.len());
session.data(channel, SUCCESS.to_string().into());
state
}
Err(error) => {
warn!(%error, "Rejecting scp modes payload");
return None;
}
}
}
State::ReceivingFile(length, path) => {
if self.pending_data.len() < length {
exit = true;
State::ReceivingFile(length, path)
} else {
let data = self.pending_data.split_to(length);
connection
.audit_log()
.push_action(AuditLogAction::WriteFile(WriteFileEvent {
path: Box::from(path.to_string_lossy().into_owned()),
content: data.freeze(),
}));
State::AwaitingSeparator
}
}
State::AwaitingSeparator => {
if self.pending_data.starts_with(&[0]) {
self.pending_data.advance(1);
session.data(channel, SUCCESS.to_string().into());
}
State::Waiting
}
};
self.state = next_state;
}
Some(self)
}
}
#[derive(Clone, Debug)]
enum State {
Waiting,
ReceivingFile(usize, PathBuf),
AwaitingSeparator,
}
#[derive(Debug)]
#[allow(dead_code)]
enum Receive<'a> {
FileCopy {
mode: &'a str,
length: usize,
file_name: &'a str,
},
DirectoryCopy {
mode: &'a str,
length: u64,
directory_name: &'a str,
},
EndDirectory,
AccessTime {
modified_time: u64,
modified_time_micros: u64,
access_time: u64,
access_time_micros: u64,
},
}
enum ReceiveType {
FileCopy,
DirectoryCopy,
EndDirectory,
AccessTime,
}
impl<'a> Receive<'a> {
fn parse(rest: &'a [u8]) -> IResult<&'a [u8], Receive<'a>> {
let (rest, typ) = nom::branch::alt((
map(tag("C"), |_| ReceiveType::FileCopy),
map(tag("D"), |_| ReceiveType::DirectoryCopy),
map(tag("E"), |_| ReceiveType::EndDirectory),
map(tag("T"), |_| ReceiveType::AccessTime),
))(rest)?;
match typ {
ReceiveType::FileCopy => {
let (rest, mode) = map_res(take(4_usize), std::str::from_utf8)(rest)?;
let (rest, _) = tag(" ")(rest)?;
let (rest, length) =
map_res(map_res(digit1, std::str::from_utf8), usize::from_str)(rest)?;
let (rest, _) = tag(" ")(rest)?;
let (rest, file_name) = map_res(take_until("\n"), std::str::from_utf8)(rest)?;
let (rest, _) = tag("\n")(rest)?;
Ok((
rest,
Receive::FileCopy {
mode,
length,
file_name,
},
))
}
ReceiveType::DirectoryCopy => {
let (rest, mode) = map_res(take(4_usize), std::str::from_utf8)(rest)?;
let (rest, _) = tag(" ")(rest)?;
let (rest, length) = u64(rest)?;
let (rest, _) = tag(" ")(rest)?;
let (rest, directory_name) = map_res(take_until("\n"), std::str::from_utf8)(rest)?;
let (rest, _) = tag("\n")(rest)?;
Ok((
rest,
Receive::DirectoryCopy {
mode,
length,
directory_name,
},
))
}
ReceiveType::EndDirectory => {
let (rest, _) = tag("\n")(rest)?;
Ok((rest, Receive::EndDirectory))
}
ReceiveType::AccessTime => {
let (rest, modified_time) =
map_res(map_res(digit1, std::str::from_utf8), u64::from_str)(rest)?;
let (rest, _) = tag(" ")(rest)?;
let (rest, modified_time_micros) =
map_res(map_res(digit1, std::str::from_utf8), u64::from_str)(rest)?;
let (rest, _) = tag(" ")(rest)?;
let (rest, access_time) =
map_res(map_res(digit1, std::str::from_utf8), u64::from_str)(rest)?;
let (rest, _) = tag(" ")(rest)?;
let (rest, access_time_micros) =
map_res(map_res(digit1, std::str::from_utf8), u64::from_str)(rest)?;
let (rest, _) = tag("\n")(rest)?;
Ok((
rest,
Receive::AccessTime {
modified_time,
modified_time_micros,
access_time,
access_time_micros,
},
))
}
}
}
}
@@ -4,15 +4,13 @@ use crate::{
PtyRequestEvent, X11RequestEvent,
},
audit::{
ExecCommandEvent, SignalEvent, SubsystemRequestEvent, TcpIpForwardEvent,
WindowAdjustedEvent, WindowChangeRequestEvent,
SignalEvent, SubsystemRequestEvent, TcpIpForwardEvent, WindowAdjustedEvent,
WindowChangeRequestEvent,
},
command::run_command,
config::Config,
file_system::FileSystem,
state::State,
subsystem,
subsystem::Subsystem as SubsystemTrait,
subsystem::{self, shell::Shell, Subsystem as SubsystemTrait},
};
use futures::{
future::{BoxFuture, InspectErr},
@@ -38,7 +36,6 @@ use tracing::{debug, error, info, info_span, instrument::Instrumented, Instrumen
pub static KEYBOARD_INTERACTIVE_PROMPT: &[(Cow<'static, str>, bool)] =
&[(Cow::Borrowed("Password: "), false)];
pub const SHELL_PROMPT: &str = "bash-5.1$ ";
#[derive(Clone)]
pub struct Server {
@@ -107,6 +104,10 @@ impl Connection {
self.file_system.as_mut().unwrap()
}
pub fn audit_log(&mut self) -> &mut AuditLog {
&mut self.audit_log
}
fn try_login(&mut self, user: &str, password: &str) -> bool {
self.username = Some(user.to_string());
@@ -255,15 +256,13 @@ impl thrussh::server::Handler for Connection {
let _entered = span.enter();
if self.subsystem.remove(&channel).is_some() {
session.exit_status_request(channel, 0);
session.channel_success(channel);
} else {
session.channel_failure(channel);
}
if self.subsystem.is_empty() {
session.exit_status_request(channel, 0);
session.close(channel);
}
session.close(channel);
self.finished(session).boxed().wrap(Span::current())
}
@@ -332,22 +331,11 @@ impl thrussh::server::Handler for Connection {
let mut subsystem = subsystem.lock().await;
match &mut *subsystem {
Subsystem::Shell => {
let data = shlex::split(String::from_utf8_lossy(&data).as_ref());
if let Some(args) = data {
run_command(&args, channel, &mut session, &mut self).await;
self.audit_log
.push_action(AuditLogAction::ExecCommand(ExecCommandEvent {
args: Box::from(args),
}));
}
session.data(channel, SHELL_PROMPT.to_string().into());
Subsystem::Shell(ref mut inner) => {
inner.data(&mut self, channel, &data, &mut session).await;
}
Subsystem::Sftp(ref mut inner) => {
inner
.data(&mut self.audit_log, channel, &data, &mut session)
.await;
inner.data(&mut self, channel, &data, &mut session).await;
}
}
@@ -476,9 +464,9 @@ impl thrussh::server::Handler for Connection {
self.audit_log.push_action(AuditLogAction::ShellRequested);
session.data(channel, SHELL_PROMPT.to_string().into());
let shell = Shell::new(true, channel, &mut session);
self.subsystem
.insert(channel, Arc::new(Mutex::new(Subsystem::Shell)));
.insert(channel, Arc::new(Mutex::new(Subsystem::Shell(shell))));
session.channel_success(channel);
self.finished(session).boxed().wrap(Span::current())
@@ -493,21 +481,16 @@ impl thrussh::server::Handler for Connection {
let span = info_span!(parent: &self.span, "exec_request");
let _entered = span.enter();
let data = shlex::split(String::from_utf8_lossy(data).as_ref());
let data = data.to_vec();
async move {
if let Some(args) = data {
run_command(&args, channel, &mut session, &mut self).await;
self.audit_log
.push_action(AuditLogAction::ExecCommand(ExecCommandEvent {
args: Box::from(args),
}));
session.channel_success(channel);
} else {
session.channel_failure(channel);
}
let mut shell = Shell::new(false, channel, &mut session);
shell.data(&mut self, channel, &data, &mut session).await;
self.subsystem
.insert(channel, Arc::new(Mutex::new(Subsystem::Shell(shell))));
session.channel_success(channel);
self.finished(session).await
}
.boxed()
@@ -639,7 +622,7 @@ impl Drop for Connection {
#[derive(Debug, Clone)]
pub enum Subsystem {
Shell,
Shell(subsystem::shell::Shell),
Sftp(subsystem::sftp::Sftp),
}
@@ -1,9 +1,10 @@
use crate::server::Connection;
use async_trait::async_trait;
use pisshoff_types::audit::AuditLog;
use thrussh::server::Session;
use thrussh::ChannelId;
pub mod sftp;
pub mod shell;
#[async_trait]
pub trait Subsystem {
@@ -11,7 +12,7 @@ pub trait Subsystem {
async fn data(
&mut self,
audit_log: &mut AuditLog,
connection: &mut Connection,
channel: ChannelId,
data: &[u8],
session: &mut Session,
@@ -1,5 +1,6 @@
use crate::subsystem::Subsystem;
use crate::{server::Connection, subsystem::Subsystem};
use async_trait::async_trait;
use bytes::Bytes;
use nom::{
bytes::complete::take,
combinator::{map_res, opt},
@@ -7,7 +8,7 @@ use nom::{
number::complete::{be_u32, be_u64, be_u8},
IResult,
};
use pisshoff_types::audit::{AuditLog, AuditLogAction, MkdirEvent, WriteFileEvent};
use pisshoff_types::audit::{AuditLogAction, MkdirEvent, WriteFileEvent};
use std::{collections::HashMap, io::Write, mem::size_of, str::FromStr};
use strum::FromRepr;
use thrussh::{server::Session, ChannelId};
@@ -28,7 +29,7 @@ impl Subsystem for Sftp {
#[allow(clippy::too_many_lines)]
async fn data(
&mut self,
audit_log: &mut AuditLog,
connection: &mut Connection,
channel: ChannelId,
data: &[u8],
session: &mut Session,
@@ -128,10 +129,12 @@ impl Subsystem for Sftp {
write_packet.offset, write_packet.data
);
audit_log.push_action(AuditLogAction::WriteFile(WriteFileEvent {
path: path.to_string().into_boxed_str(),
content: write_packet.data.to_string().into_boxed_str(),
}));
connection
.audit_log()
.push_action(AuditLogAction::WriteFile(WriteFileEvent {
path: path.to_string().into_boxed_str(),
content: Bytes::copy_from_slice(write_packet.data.as_bytes()),
}));
session.data(
channel,
@@ -205,9 +208,11 @@ impl Subsystem for Sftp {
trace!("SFTP mkdir packet: {mkdir:?}");
audit_log.push_action(AuditLogAction::Mkdir(MkdirEvent {
path: mkdir.path.to_string().into_boxed_str(),
}));
connection
.audit_log()
.push_action(AuditLogAction::Mkdir(MkdirEvent {
path: mkdir.path.to_string().into_boxed_str(),
}));
session.data(
channel,
@@ -0,0 +1,82 @@
use crate::{
command::{run_command, ConcreteLongRunningCommand},
server::Connection,
subsystem::Subsystem,
};
use async_trait::async_trait;
use pisshoff_types::audit::{AuditLogAction, ExecCommandEvent};
use thrussh::{server::Session, ChannelId};
pub const SHELL_PROMPT: &str = "bash-5.1$ ";
#[derive(Clone, Debug)]
pub struct Shell {
interactive: bool,
state: State,
}
impl Shell {
pub fn new(interactive: bool, channel: ChannelId, session: &mut Session) -> Self {
if interactive {
session.data(channel, SHELL_PROMPT.to_string().into());
}
Self {
interactive,
state: State::Prompt,
}
}
}
#[async_trait]
impl Subsystem for Shell {
const NAME: &'static str = "shell";
async fn data(
&mut self,
connection: &mut Connection,
channel: ChannelId,
data: &[u8],
session: &mut Session,
) {
let next = match std::mem::take(&mut self.state) {
State::Prompt => {
let Some(args) = shlex::split(String::from_utf8_lossy(data).as_ref()) else {
return;
};
connection
.audit_log()
.push_action(AuditLogAction::ExecCommand(ExecCommandEvent {
args: Box::from(args.clone()),
}));
run_command(&args, channel, session, connection)
.await
.map_or(State::Prompt, State::Running)
}
State::Running(command) => command
.data(connection, channel, data, session)
.await
.map_or(State::Prompt, State::Running),
};
if matches!(next, State::Prompt) {
if self.interactive {
session.data(channel, SHELL_PROMPT.to_string().into());
} else {
session.exit_status_request(channel, 0);
session.close(channel);
}
}
self.state = next;
}
}
#[derive(Debug, Clone, Default)]
enum State {
#[default]
Prompt,
Running(ConcreteLongRunningCommand),
}
@@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
bytes = { version = "1.4", features = ["serde"] }
uuid = "1.3"
time = { version = "0.3", features = ["serde", "formatting", "parsing"] }
serde = { version = "1.0", features = ["derive"] }
@@ -1,3 +1,4 @@
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::{
@@ -92,7 +93,7 @@ pub struct MkdirEvent {
#[derive(Debug, Serialize, Deserialize)]
pub struct WriteFileEvent {
pub path: Box<str>,
pub content: Box<str>,
pub content: Bytes,
}
#[derive(Debug, Serialize, Deserialize)]