From 76bbb3c268c1522e291d2e7282ff4a816825bb8c Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Mon, 10 Jul 2023 01:23:14 +0100 Subject: [PATCH] Hack together SFTP/modern SCP auditing support --- Cargo.lock | 20 ++++++++++++++++++++ pisshoff-server/Cargo.toml | 4 ++++ pisshoff-server/src/main.rs | 1 + pisshoff-server/src/server.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------- pisshoff-server/src/subsystem/mod.rs | 19 +++++++++++++++++++ pisshoff-server/src/subsystem/sftp.rs | 618 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pisshoff-types/src/audit.rs | 13 +++++++++++++ 7 files changed, 741 insertions(+), 17 deletions(-) create mode 100644 pisshoff-server/src/subsystem/mod.rs create mode 100644 pisshoff-server/src/subsystem/sftp.rs diff --git a/Cargo.lock b/Cargo.lock index b7b9ab3..e213c04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -840,6 +840,12 @@ dependencies = [ ] [[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] name = "miniz_oxide" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -874,6 +880,16 @@ dependencies = [ ] [[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1038,18 +1054,22 @@ name = "pisshoff-server" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "bitflags 2.3.3", + "bytes", "clap", "fastrand", "futures", "insta", "itertools", "nix", + "nom", "parking_lot", "pisshoff-types", "serde", "serde_json", "shlex", + "strum", "test-case", "thrussh", "thrussh-keys", diff --git a/pisshoff-server/Cargo.toml b/pisshoff-server/Cargo.toml index 700c0f4..3400422 100644 --- a/pisshoff-server/Cargo.toml +++ b/pisshoff-server/Cargo.toml @@ -9,15 +9,19 @@ edition = "2021" pisshoff-types = { path = "../pisshoff-types" } anyhow = "1.0" +async-trait = "0.1" bitflags = "2.3" +bytes = "1.4" clap = { version = "4.3", features = ["derive", "env", "cargo"] } futures = "0.3" parking_lot = "0.12" fastrand = "1.9" itertools = "0.10" +nom = "7.1" nix = { version = "0.26", features = ["hostname"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +strum = { version = "0.24", features = ["derive"] } shlex = "1.1" thrussh = "0.34" thrussh-keys = "0.22" diff --git a/pisshoff-server/src/main.rs b/pisshoff-server/src/main.rs index 71af86d..ae975d3 100644 --- a/pisshoff-server/src/main.rs +++ b/pisshoff-server/src/main.rs @@ -20,6 +20,7 @@ mod config; mod file_system; mod server; mod state; +mod subsystem; #[tokio::main] async fn main() { diff --git a/pisshoff-server/src/server.rs b/pisshoff-server/src/server.rs index fdeb143..04109a2 100644 --- a/pisshoff-server/src/server.rs +++ b/pisshoff-server/src/server.rs @@ -1,16 +1,18 @@ -use crate::audit::{ - ExecCommandEvent, SignalEvent, SubsystemRequestEvent, TcpIpForwardEvent, WindowAdjustedEvent, - WindowChangeRequestEvent, -}; -use crate::file_system::FileSystem; use crate::{ audit::{ AuditLog, AuditLogAction, LoginAttemptEvent, OpenDirectTcpIpEvent, OpenX11Event, PtyRequestEvent, X11RequestEvent, }, + audit::{ + ExecCommandEvent, SignalEvent, SubsystemRequestEvent, TcpIpForwardEvent, + WindowAdjustedEvent, WindowChangeRequestEvent, + }, command::run_command, config::Config, + file_system::FileSystem, state::State, + subsystem, + subsystem::Subsystem as SubsystemTrait, }; use futures::{ future::{BoxFuture, InspectErr}, @@ -18,6 +20,7 @@ use futures::{ }; use std::{ borrow::Cow, + collections::HashMap, future::Future, net::SocketAddr, pin::Pin, @@ -30,6 +33,7 @@ use thrussh::{ }; use thrussh_keys::key::PublicKey; use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::Mutex; use tracing::{debug, error, info, info_span, instrument::Instrumented, Instrument, Span}; pub static KEYBOARD_INTERACTIVE_PROMPT: &[(Cow<'static, str>, bool)] = @@ -76,6 +80,7 @@ impl thrussh::server::Server for Server { }, username: None, file_system: None, + subsystem: HashMap::new(), } } } @@ -86,6 +91,7 @@ pub struct Connection { audit_log: AuditLog, username: Option, file_system: Option, + subsystem: HashMap>>, } impl Connection { @@ -244,13 +250,21 @@ impl thrussh::server::Handler for Connection { self.finished(session).boxed().wrap(Span::current()) } - fn channel_eof(self, channel: ChannelId, mut session: Session) -> Self::FutureUnit { + fn channel_eof(mut self, channel: ChannelId, mut session: Session) -> Self::FutureUnit { let span = info_span!(parent: &self.span, "channel_eof"); let _entered = span.enter(); - info!("In here"); + if self.subsystem.remove(&channel).is_some() { + session.channel_success(channel); + } else { + session.channel_failure(channel); + } + + if self.subsystem.is_empty() { + session.exit_status_request(channel, 0); + session.close(channel); + } - session.channel_success(channel); self.finished(session).boxed().wrap(Span::current()) } @@ -310,18 +324,33 @@ impl thrussh::server::Handler for Connection { let span = info_span!(parent: &self.span, "data"); let _entered = span.enter(); - let data = shlex::split(String::from_utf8_lossy(data).as_ref()); + // TODO: don't unwrap + let subsystem = self.subsystem.get(&channel).unwrap().clone(); + 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), - })); + 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::Sftp(ref mut inner) => { + inner + .data(&mut self.audit_log, channel, &data, &mut session) + .await; + } } - session.data(channel, SHELL_PROMPT.to_string().into()); self.finished(session).await } .boxed() @@ -448,6 +477,8 @@ impl thrussh::server::Handler for Connection { self.audit_log.push_action(AuditLogAction::ShellRequested); session.data(channel, SHELL_PROMPT.to_string().into()); + self.subsystem + .insert(channel, Arc::new(Mutex::new(Subsystem::Shell))); session.channel_success(channel); self.finished(session).boxed().wrap(Span::current()) @@ -497,7 +528,19 @@ impl thrussh::server::Handler for Connection { name: Box::from(name), })); - session.channel_failure(channel); + let subsystem = match name { + subsystem::sftp::Sftp::NAME => Some(Subsystem::Sftp(subsystem::sftp::Sftp::default())), + _ => None, + }; + + if let Some(subsystem) = subsystem { + self.subsystem + .insert(channel, Arc::new(Mutex::new(subsystem))); + session.channel_success(channel); + } else { + session.channel_failure(channel); + } + self.finished(session).boxed().wrap(Span::current()) } @@ -594,6 +637,12 @@ impl Drop for Connection { } } +#[derive(Debug, Clone)] +pub enum Subsystem { + Shell, + Sftp(subsystem::sftp::Sftp), +} + type HandlerResult = Result::Error>; type HandlerFuture = ServerFuture< ::Error, diff --git a/pisshoff-server/src/subsystem/mod.rs b/pisshoff-server/src/subsystem/mod.rs new file mode 100644 index 0000000..55e9bda --- /dev/null +++ b/pisshoff-server/src/subsystem/mod.rs @@ -0,0 +1,19 @@ +use async_trait::async_trait; +use pisshoff_types::audit::AuditLog; +use thrussh::server::Session; +use thrussh::ChannelId; + +pub mod sftp; + +#[async_trait] +pub trait Subsystem { + const NAME: &'static str; + + async fn data( + &mut self, + audit_log: &mut AuditLog, + channel: ChannelId, + data: &[u8], + session: &mut Session, + ); +} diff --git a/pisshoff-server/src/subsystem/sftp.rs b/pisshoff-server/src/subsystem/sftp.rs new file mode 100644 index 0000000..9c12069 --- /dev/null +++ b/pisshoff-server/src/subsystem/sftp.rs @@ -0,0 +1,618 @@ +use crate::subsystem::Subsystem; +use async_trait::async_trait; +use nom::{ + bytes::complete::take, + combinator::{map_res, opt}, + error::ErrorKind, + number::complete::{be_u32, be_u64, be_u8}, + IResult, +}; +use pisshoff_types::audit::{AuditLog, AuditLogAction, MkdirEvent, WriteFileEvent}; +use std::{collections::HashMap, io::Write, mem::size_of, str::FromStr}; +use strum::FromRepr; +use thrussh::{server::Session, ChannelId}; +use tracing::{debug, error, trace, warn}; +use uuid::Uuid; + +// https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13 +#[derive(Default, Clone, Debug)] +pub struct Sftp { + open_files: HashMap, + pending_data: bytes::BytesMut, +} + +#[async_trait] +impl Subsystem for Sftp { + const NAME: &'static str = "sftp"; + + #[allow(clippy::too_many_lines)] + async fn data( + &mut self, + audit_log: &mut AuditLog, + channel: ChannelId, + data: &[u8], + session: &mut Session, + ) { + self.pending_data.extend_from_slice(data); + + loop { + let data = self.pending_data.split(); + + let packet = match WirePacket::parse(&data) { + Ok((rest, packet)) => { + self.pending_data.extend_from_slice(rest); + packet + } + Err(e) if e.is_incomplete() => { + self.pending_data.unsplit(data); + break; + } + Err(nom::Err::Error(nom::error::Error { + code: ErrorKind::Eof, + .. + })) => { + self.pending_data.unsplit(data); + break; + } + Err(e) => { + error!("Bad SFTP packet {e:?}"); + break; + } + }; + + match packet.typ { + PacketType::Init => { + // the version the client sent us is in `request_id`, lets just echo it back + // to them, bounded by the version of the rfc we developed this barebones + // implementation against + session.data( + channel, + WirePacket::new(PacketType::Version, packet.request_id.min(6), &[]) + .to_bytes() + .into(), + ); + } + PacketType::Stat | PacketType::Lstat => { + let (_data, stat) = StatPacket::parse(packet.data).unwrap(); + + trace!("SFTP stat packet: {stat:?}"); + + session.data( + channel, + StatusResponse { + code: StatusCode::NoSuchFile, + message: "No such file or directory", + } + .to_packet(packet.request_id) + .into(), + ); + } + PacketType::Open => { + let (_data, open) = OpenPacket::parse(packet.data).unwrap(); + + trace!("SFTP open packet: {open:?}"); + + let uuid = Uuid::new_v4(); + self.open_files.insert(uuid, open.path.to_string()); + + session.data( + channel, + HandleResponse(uuid).to_packet(packet.request_id).into(), + ); + } + PacketType::FSetStat | PacketType::SetStat => { + let (_data, set_stat) = FSetStatPacket::parse(packet.data).unwrap(); + + trace!("SFTP fsetstat packet: {set_stat:?}"); + + session.data( + channel, + StatusResponse { + code: StatusCode::Ok, + message: "", + } + .to_packet(packet.request_id) + .into(), + ); + } + PacketType::Write => { + let (_data, write_packet) = WritePacket::parse(packet.data).unwrap(); + + let path = self + .open_files + .get(&Uuid::from_str(write_packet.handle).unwrap()) + .unwrap(); + + debug!( + "Received write for {path} at offset {}: {:?}", + 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(), + })); + + session.data( + channel, + StatusResponse { + code: StatusCode::Ok, + message: "", + } + .to_packet(packet.request_id) + .into(), + ); + } + PacketType::Close => { + let (_data, close_packet) = ClosePacket::parse(packet.data).unwrap(); + + trace!("SFTP close packet: {close_packet:?}"); + + self.open_files + .remove(&Uuid::from_str(close_packet.handle).unwrap()) + .unwrap(); + + session.data( + channel, + StatusResponse { + code: StatusCode::Ok, + message: "", + } + .to_packet(packet.request_id) + .into(), + ); + } + PacketType::RealPath => { + let (_data, real_path) = RealPathPacket::parse(packet.data).unwrap(); + + trace!("SFTP realpath packet: {real_path:?}"); + + #[allow(clippy::wildcard_in_or_patterns)] + match real_path.control { + // SSH_FXP_REALPATH_STAT_ALWAYS + Some(2) => { + session.data( + channel, + StatusResponse { + code: StatusCode::NoSuchFile, + message: "No such file or directory", + } + .to_packet(packet.request_id) + .into(), + ); + } + // SSH_FXP_REALPATH_NO_CHECK | SSH_FXP_REALPATH_STAT_IF + Some(0 | 1) | _ => { + session.data( + channel, + NameResponse { + files: &[NameResponseFile { + name: real_path.path, + long_name: real_path.path, + attrs: FileAttrs { + typ: FileType::Unknown, + }, + }], + } + .to_packet(packet.request_id) + .into(), + ); + } + } + } + PacketType::Mkdir => { + let (_data, mkdir) = MkdirPacket::parse(packet.data).unwrap(); + + trace!("SFTP mkdir packet: {mkdir:?}"); + + audit_log.push_action(AuditLogAction::Mkdir(MkdirEvent { + path: mkdir.path.to_string().into_boxed_str(), + })); + + session.data( + channel, + StatusResponse { + code: StatusCode::Ok, + message: "", + } + .to_packet(packet.request_id) + .into(), + ); + } + _ => { + // TODO: return SSH_FX_OP_UNSUPPORTED + warn!("Unknown SFTP packet {packet:?}"); + } + } + } + + session.channel_success(channel); + session.flush_pending(channel); + } +} + +fn take_length_delimited_string(rest: &[u8]) -> IResult<&[u8], &str> { + let (rest, length) = be_u32(rest)?; + map_res(take(length), std::str::from_utf8)(rest) +} + +#[derive(Debug)] +struct MkdirPacket<'a> { + path: &'a str, + // TODO: fileattrs +} + +impl<'a> MkdirPacket<'a> { + fn parse(rest: &'a [u8]) -> IResult<&'a [u8], Self> { + let (rest, path) = take_length_delimited_string(rest)?; + + Ok((rest, Self { path })) + } +} + +#[derive(Debug)] +struct RealPathPacket<'a> { + path: &'a str, + control: Option, +} + +impl<'a> RealPathPacket<'a> { + fn parse(rest: &'a [u8]) -> IResult<&'a [u8], Self> { + let (rest, path) = take_length_delimited_string(rest)?; + let (rest, control) = opt(be_u8)(rest)?; + + Ok((rest, Self { path, control })) + } +} + +#[derive(Debug)] +struct WritePacket<'a> { + handle: &'a str, + offset: u64, + data: &'a str, +} + +impl<'a> WritePacket<'a> { + fn parse(rest: &'a [u8]) -> IResult<&'a [u8], Self> { + let (rest, handle) = take_length_delimited_string(rest)?; + let (rest, offset) = be_u64(rest)?; + let (rest, data) = take_length_delimited_string(rest)?; + + Ok(( + rest, + Self { + handle, + offset, + data, + }, + )) + } +} + +#[derive(Debug)] +struct ClosePacket<'a> { + handle: &'a str, +} + +impl<'a> ClosePacket<'a> { + fn parse(rest: &'a [u8]) -> IResult<&'a [u8], Self> { + let (rest, handle) = take_length_delimited_string(rest)?; + + Ok((rest, Self { handle })) + } +} + +#[derive(Debug)] +#[allow(dead_code)] +struct OpenPacket<'a> { + path: &'a str, + desired_access: u32, + flags: u32, +} + +impl<'a> OpenPacket<'a> { + fn parse(rest: &'a [u8]) -> IResult<&'a [u8], Self> { + let (rest, path) = take_length_delimited_string(rest)?; + let (rest, desired_access) = be_u32(rest)?; + let (rest, flags) = be_u32(rest)?; + + Ok(( + rest, + Self { + path, + desired_access, + flags, + }, + )) + } +} + +#[derive(Debug)] +#[allow(dead_code)] +struct FSetStatPacket<'a> { + handle: &'a str, +} + +impl<'a> FSetStatPacket<'a> { + fn parse(rest: &'a [u8]) -> IResult<&'a [u8], Self> { + let (rest, handle) = take_length_delimited_string(rest)?; + + Ok((rest, Self { handle })) + } +} + +#[derive(Debug)] +#[allow(dead_code)] +struct StatPacket<'a> { + path: &'a str, + flags: u32, +} + +impl<'a> StatPacket<'a> { + fn parse(rest: &'a [u8]) -> IResult<&'a [u8], Self> { + let (rest, path) = take_length_delimited_string(rest)?; + let (rest, flags) = opt(be_u32)(rest)?; + + Ok(( + rest, + Self { + path, + flags: flags.unwrap_or(0), + }, + )) + } +} + +#[derive(Debug)] +struct WirePacket<'a> { + length: u32, + typ: PacketType, + request_id: u32, + data: &'a [u8], +} + +impl<'a> WirePacket<'a> { + fn new(typ: PacketType, request_id: u32, data: &'a [u8]) -> Self { + Self { + length: u32::try_from(size_of::() + size_of::() + data.len()).unwrap(), + typ, + request_id, + data, + } + } + + fn to_bytes(&self) -> Vec { + let mut out = Vec::with_capacity( + size_of::() + size_of::() + size_of::() + self.data.len(), + ); + out.extend_from_slice(&self.length.to_be_bytes()); + out.push(self.typ as u8); + out.extend_from_slice(&self.request_id.to_be_bytes()); + out.extend_from_slice(self.data); + out + } + + fn parse(rest: &'a [u8]) -> IResult<&'a [u8], Self> { + let (rest, length) = be_u32(rest)?; + let (rest, typ) = be_u8(rest)?; + let (rest, request_id) = be_u32(rest)?; + let (rest, data) = take( + length - u32::try_from(size_of::() + size_of::()).unwrap_or(u32::MAX), + )(rest)?; + + let Some(typ) = PacketType::from_repr(typ) else { + return Err(nom::Err::Failure(nom::error::Error::new(rest, nom::error::ErrorKind::Verify))); + }; + + Ok(( + rest, + Self { + length, + typ, + request_id, + data, + }, + )) + } +} + +#[derive(Copy, Clone, Debug, FromRepr)] +#[repr(u8)] +pub enum PacketType { + Init = 1, + Version = 2, + Open = 3, + Close = 4, + Read = 5, + Write = 6, + Lstat = 7, + Fstat = 8, + SetStat = 9, + FSetStat = 10, + OpenDir = 11, + ReadDir = 12, + Remove = 13, + Mkdir = 14, + Rmdir = 15, + RealPath = 16, + Stat = 17, + Rename = 18, + ReadLink = 19, + Link = 21, + Block = 22, + Unblock = 23, + Status = 101, + Handle = 102, + Data = 103, + Name = 104, + Attrs = 105, + Extended = 200, + ExtendedReply = 201, +} + +pub struct StatusResponse<'a> { + code: StatusCode, + message: &'a str, + // language_tag: &'a str, +} + +impl Response for StatusResponse<'_> { + const TYPE: PacketType = PacketType::Status; + + fn to_bytes(&self) -> Vec { + let mut out = Vec::with_capacity(size_of::() + size_of::() + self.message.len()); + out.extend_from_slice(&(self.code as u32).to_be_bytes()); + out.extend_from_slice( + &u32::try_from(self.message.len()) + .unwrap_or(u32::MAX) + .to_be_bytes(), + ); + out.extend_from_slice(self.message.as_bytes()); + out + } +} + +pub struct HandleResponse(Uuid); + +impl Response for HandleResponse { + const TYPE: PacketType = PacketType::Handle; + + fn to_bytes(&self) -> Vec { + let mut out = Vec::with_capacity(size_of::() + 36); + out.extend_from_slice(&36_u32.to_be_bytes()); + write!(out, "{}", self.0).unwrap(); + out + } +} + +pub struct NameResponse<'a> { + files: &'a [NameResponseFile<'a>], +} + +impl Response for NameResponse<'_> { + const TYPE: PacketType = PacketType::Name; + + fn to_bytes(&self) -> Vec { + // TODO: include nameresponsefile size + let mut out = Vec::with_capacity(size_of::()); + out.extend_from_slice( + &u32::try_from(self.files.len()) + .unwrap_or(u32::MAX) + .to_be_bytes(), + ); + + for file in self.files { + out.extend_from_slice(&file.to_bytes()); + } + + out.push(1); + + out + } +} + +pub struct NameResponseFile<'a> { + name: &'a str, + long_name: &'a str, + attrs: FileAttrs, +} + +impl NameResponseFile<'_> { + fn to_bytes(&self) -> Vec { + // TODO: include FileAttrs size + let mut out = Vec::with_capacity( + size_of::() + self.name.len() + size_of::() + self.long_name.len(), + ); + out.extend_from_slice( + &u32::try_from(self.name.len()) + .unwrap_or(u32::MAX) + .to_be_bytes(), + ); + out.extend_from_slice(self.name.as_bytes()); + out.extend_from_slice( + &u32::try_from(self.long_name.len()) + .unwrap_or(u32::MAX) + .to_be_bytes(), + ); + out.extend_from_slice(self.long_name.as_bytes()); + out.extend_from_slice(&self.attrs.to_bytes()); + out + } +} + +#[derive(Copy, Clone, Debug)] +#[repr(u8)] +#[allow(dead_code)] +enum FileType { + Regular = 1, + Directory = 2, + Symlink = 3, + Special = 4, + Unknown = 5, + Socket = 6, + CharDevice = 7, + BlockDevice = 8, + Fifo = 9, +} + +#[derive(Copy, Clone, Debug)] +struct FileAttrs { + typ: FileType, +} + +impl FileAttrs { + fn to_bytes(self) -> Vec { + let mut out = Vec::with_capacity(size_of::() + size_of::()); + out.extend_from_slice(&0_u32.to_be_bytes()); + out.push(self.typ as u8); + out + } +} + +#[derive(Copy, Clone, Debug)] +#[repr(u32)] +#[allow(dead_code)] +enum StatusCode { + Ok = 0, + Eof = 1, + NoSuchFile = 2, + PermissionDenied = 3, + Failure = 4, + BadMessage = 5, + NoConnection = 6, + ConnectionLost = 7, + OpUnsupported = 8, + InvalidHandle = 9, + NoSuchPath = 10, + FileAlreadyExists = 11, + WriteProtect = 12, + NoMedia = 13, + NoSpaceOnFilesystem = 14, + QuotaExceeded = 15, + UnknownPrincipal = 16, + LockConflict = 17, + DirNotEmpty = 18, + NotADirectory = 19, + InvalidFilename = 20, + LinkLoop = 21, + CannotDelete = 22, + InvalidParameter = 23, + FileIsADirectory = 24, + ByteRangeLockConflict = 25, + ByteRangeLockRefused = 26, + DeletePending = 27, + FileCorrupt = 28, + OwnerInvalid = 29, + GroupInvalid = 30, + NoMatchingByteRangeLock = 31, +} + +trait Response { + const TYPE: PacketType; + + fn to_bytes(&self) -> Vec; + + fn to_packet(&self, request_id: u32) -> Vec { + WirePacket::new(Self::TYPE, request_id, &self.to_bytes()).to_bytes() + } +} diff --git a/pisshoff-types/src/audit.rs b/pisshoff-types/src/audit.rs index 392d01e..834abbc 100644 --- a/pisshoff-types/src/audit.rs +++ b/pisshoff-types/src/audit.rs @@ -80,6 +80,19 @@ pub enum AuditLogAction { Signal(SignalEvent), TcpIpForward(TcpIpForwardEvent), CancelTcpIpForward(TcpIpForwardEvent), + Mkdir(MkdirEvent), + WriteFile(WriteFileEvent), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct MkdirEvent { + pub path: Box, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct WriteFileEvent { + pub path: Box, + pub content: Box, } #[derive(Debug, Serialize, Deserialize)] -- libgit2 1.7.2