Skip to content

Commit 8e5d209

Browse files
committed
Add more changes from March 2025
1 parent 21f4845 commit 8e5d209

File tree

5 files changed

+368
-127
lines changed

5 files changed

+368
-127
lines changed

sftp/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
[package]
22
name = "sunset-sftp"
33
version = "0.1.0"
4-
edition = "2021"
4+
edition = "2024"
55

66
[dependencies]
7+
sunset = { version = "0.2.0", path = "../" }
8+
sunset-sshwire-derive = { version = "0.2", path = "../sshwire-derive" }

sftp/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
mod sftp_proto;
1+
mod proto;
2+
mod sftpserver;

sftp/src/proto.rs

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
use core::marker::PhantomData;
2+
3+
use sshwire::{BinString, TextString, SSHEncode, SSHDecode, SSHSource, SSHSink, WireResult, WireError};
4+
use sunset::{error, Result};
5+
6+
// TODO is utf8 enough, or does this need to be an opaque binstring?
7+
#[derive(Debug)]
8+
pub struct Filename<'a>(TextString<'a>);
9+
10+
#[derive(Debug, SSHEncode, SSHDecode)]
11+
struct FileHandle<'a>(pub BinString<'a>);
12+
13+
#[derive(Debug, SSHEncode, SSHDecode)]
14+
pub struct InitVersion<'a> {
15+
// No ReqId for SSH_FXP_INIT
16+
pub version: u32,
17+
// TODO variable number of ExtPair
18+
pub _ext: &'a PhantomData<()>,
19+
}
20+
21+
#[derive(Debug, SSHEncode, SSHDecode)]
22+
pub struct Open<'a> {
23+
pub filename: Filename<'a>,
24+
pub pflags: u32,
25+
pub attrs: Attrs<'a>,
26+
}
27+
28+
#[derive(Debug, SSHEncode, SSHDecode)]
29+
pub struct Close<'a> {
30+
pub handle: FileHandle<'a>,
31+
}
32+
33+
#[derive(Debug, SSHEncode, SSHDecode)]
34+
pub struct Read<'a> {
35+
pub handle: FileHandle<'a>,
36+
pub offset: u64,
37+
pub len: u32,
38+
}
39+
40+
#[derive(Debug, SSHEncode, SSHDecode)]
41+
pub struct Write<'a> {
42+
pub handle: FileHandle<'a>,
43+
pub offset: u64,
44+
pub data: BinString<'a>,
45+
}
46+
47+
// Responses
48+
49+
#[derive(Debug, SSHEncode, SSHDecode)]
50+
pub struct Status<'a> {
51+
pub code: StatusCode,
52+
pub message: TextString<'a>,
53+
pub lang: TextString<'a>,
54+
}
55+
56+
#[derive(Debug, SSHEncode, SSHDecode)]
57+
pub struct Handle<'a> {
58+
pub handle: FileHandle<'a>,
59+
}
60+
61+
#[derive(Debug, SSHEncode, SSHDecode)]
62+
pub struct Data<'a> {
63+
pub handle: FileHandle<'a>,
64+
pub offset: u64,
65+
pub data: BinString<'a>,
66+
}
67+
68+
#[derive(Debug, SSHEncode, SSHDecode)]
69+
pub struct Name<'a> {
70+
pub count: u32,
71+
// TODO repeat NameEntry
72+
}
73+
74+
#[derive(Debug, SSHEncode, SSHDecode)]
75+
pub struct NameEntry<'a> {
76+
pub filename: Filename<'a>,
77+
/// longname is an undefined text line like "ls -l",
78+
/// SHOULD NOT be used.
79+
pub _longname: Filename<'a>,
80+
pub attrs: Attrs<'a>,
81+
}
82+
83+
#[derive(Debug, SSHEncode, SSHDecode, Clone, Copy)]
84+
pub struct ReqId(pub u32);
85+
86+
#[derive(Debug, SSHEncode, SSHDecode)]
87+
#[repr(u8)]
88+
#[allow(non_camel_case_types)]
89+
enum StatusCode {
90+
SSH_FX_OK = 0,
91+
SSH_FX_EOF = 1,
92+
SSH_FX_NO_SUCH_FILE = 2,
93+
SSH_FX_PERMISSION_DENIED = 3,
94+
SSH_FX_FAILURE = 4,
95+
SSH_FX_BAD_MESSAGE = 5,
96+
SSH_FX_NO_CONNECTION = 6,
97+
SSH_FX_CONNECTION_LOST = 7,
98+
SSH_FX_OP_UNSUPPORTED = 8,
99+
Other(u8),
100+
}
101+
102+
#[derive(Debug, SSHEncode, SSHDecode)]
103+
pub struct ExtPair<'a> {
104+
pub name: &'a str,
105+
pub data: BinString<'a>,
106+
}
107+
108+
#[derive(Debug)]
109+
pub struct Attrs<'a> {
110+
// flags: u32, defines used attributes
111+
pub size: Option<u64>,
112+
pub uid: Option<u32>,
113+
pub gid: Option<u32>,
114+
pub permissions: Option<u32>,
115+
pub atime: Option<u32>,
116+
pub mtime: Option<u32>,
117+
pub ext_count: Option<u32>,
118+
// TODO extensions
119+
}
120+
121+
enum Error {
122+
UnknownPacket { number: u8 },
123+
}
124+
125+
macro_rules! sftpmessages {
126+
(
127+
$( ( $message_num:literal,
128+
$SpecificPacketVariant:ident,
129+
$SpecificPacketType:ty,
130+
$SSH_FXP_NAME:ident
131+
),
132+
)*
133+
) => {
134+
135+
136+
#[derive(Debug)]
137+
#[repr(u8)]
138+
#[allow(non_camel_case_types)]
139+
pub enum SftpNum {
140+
// variants are eg
141+
// SSH_FXP_OPEN = 3,
142+
$(
143+
$SSH_FXP_NAME = $message_num,
144+
)*
145+
}
146+
147+
impl SftpNum {
148+
fn is_request(&self) -> bool {
149+
// TODO SSH_FXP_EXTENDED
150+
(2..=99).contains(self as u8)
151+
}
152+
153+
fn is_response(&self) -> bool {
154+
// TODO SSH_FXP_EXTENDED_REPLY
155+
(100..=199).contains(self as u8)
156+
}
157+
}
158+
159+
impl TryFrom<u8> for SftpNum {
160+
type Error = Error;
161+
fn try_from(v: u8) -> Result<Self> {
162+
match v {
163+
// eg
164+
// 3 => Ok(SftpNum::SSH_FXP_OPEN)
165+
$(
166+
$message_num => Ok(SftpNum::$SSH_FXP_NAME),
167+
)*
168+
_ => {
169+
Err(Error::UnknownPacket { number: v })
170+
}
171+
}
172+
}
173+
}
174+
175+
impl SSHEncode for SftpPacket<'_> {
176+
fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> {
177+
let t = self.message_num() as u8;
178+
t.enc(s)?;
179+
match self {
180+
// eg
181+
// Packet::KexInit(p) => {
182+
// ...
183+
$(
184+
Packet::$SpecificPacketVariant(p) => {
185+
p.enc(s)?
186+
}
187+
)*
188+
};
189+
Ok(())
190+
}
191+
}
192+
193+
impl<'de: 'a, 'a> SSHDecode<'de> for SftpPacket<'a> {
194+
fn dec<S>(s: &mut S) -> WireResult<Self>
195+
where S: SSHSource<'de> {
196+
let msg_num = u8::dec(s)?;
197+
let ty = MessageNumber::try_from(msg_num);
198+
let ty = match ty {
199+
Ok(t) => t,
200+
Err(_) => return Err(WireError::UnknownPacket { number: msg_num })
201+
};
202+
203+
// Decode based on the message number
204+
let p = match ty {
205+
// eg
206+
// MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit(
207+
// ...
208+
$(
209+
MessageNumber::$SSH_MESSAGE_NAME => Packet::$SpecificPacketVariant(SSHDecode::dec(s)?),
210+
)*
211+
};
212+
Ok(p)
213+
}
214+
}
215+
216+
/// Top level SSH packet enum
217+
#[derive(Debug)]
218+
pub enum SftpPacket<'a> {
219+
// eg Open(Open<'a>),
220+
$(
221+
$SpecificPacketVariant($SpecificPacketType),
222+
)*
223+
}
224+
225+
impl<'a> SftpPacket<'a> {
226+
pub fn sftp_num(&self) -> SftpNum {
227+
match self {
228+
// eg
229+
// SftpPacket::Open(_) => {
230+
// ..
231+
$(
232+
SftpPacket::$SpecificPacketVariant(_) => {
233+
MessageNumber::$SSH_FXP_NAME
234+
}
235+
)*
236+
}
237+
}
238+
239+
/// Encode a request.
240+
///
241+
/// Used by a SFTP client. Does not include the length field.
242+
pub fn encode_request(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> {
243+
if !self.sftp_num().is_request() {
244+
return Err(Error::bug())
245+
}
246+
247+
// packet type
248+
self.sftp_num().enc(s)?;
249+
// request ID
250+
id.0.enc(s)?;
251+
// contents
252+
self.enc(s)
253+
}
254+
255+
/// Decode a response.
256+
///
257+
/// Used by a SFTP client. Does not include the length field.
258+
pub fn decode_response(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> {
259+
let num = SftpNum::try_from(u8::dec(s)?)?;
260+
261+
if !num.is_response() {
262+
return error::SSHProto.fail();
263+
}
264+
265+
let id = ReqId(u32::dec(s)?);
266+
Ok((id, Self::dec(s)))
267+
}
268+
269+
/// Decode a request.
270+
///
271+
/// Used by a SFTP server. Does not include the length field.
272+
pub fn decode_request(s: &mut dyn SSHSource) -> WireResult<(ReqId, Self)> {
273+
let num = SftpNum::try_from(u8::dec(s)?)?;
274+
275+
if !num.is_request() {
276+
return error::SSHProto.fail();
277+
}
278+
279+
let id = ReqId(u32::dec(s)?);
280+
Ok((id, Self::dec(s)))
281+
}
282+
283+
/// Encode a response.
284+
///
285+
/// Used by a SFTP server. Does not include the length field.
286+
pub fn encode_response(&self, id: ReqId, s: &mut dyn SSHSink) -> Result<()> {
287+
if !self.sftp_num().is_response() {
288+
return Err(Error::bug())
289+
}
290+
291+
// packet type
292+
self.sftp_num().enc(s)?;
293+
// request ID
294+
id.0.enc(s)?;
295+
// contents
296+
self.enc(s)
297+
}
298+
}
299+
300+
$(
301+
impl<'a> From<$SpecificPacketType> for SftpPacket<'a> {
302+
fn from(s: $SpecificPacketType) -> SftpPacket<'a> {
303+
SftpPacket::$SpecificPacketVariant(s)
304+
}
305+
}
306+
)*
307+
308+
} } // macro
309+
310+
sftpmessages![
311+
312+
// Message number ranges are also used by Sftpnum::is_request and is_response.
313+
314+
(1, Init, InitVersion<'a>, SSH_FXP_INIT),
315+
(2, Version, InitVersion<'a>, SSH_FXP_VERSION),
316+
317+
// Requests
318+
(3, Open, Open<'a>, SSH_FXP_OPEN),
319+
(4, Close, Close<'a>, SSH_FXP_CLOSE),
320+
(5, Read, Read<'a>, SSH_FXP_READ),
321+
322+
// Responses
323+
(101, Status, Status<'a>, SSH_FXP_STATUS),
324+
(102, Handle, Handle<'a>, SSH_FXP_HANDLE),
325+
(103, Data, Data<'a>, SSH_FXP_DATA),
326+
(104, Name, Name<'a>, SSH_FXP_NAME),
327+
328+
];

0 commit comments

Comments
 (0)