aboutsummaryrefslogtreecommitdiff
path: root/src/tftp.rs
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-10-07 10:34:12 +0100
committerdiogo464 <[email protected]>2025-10-07 10:34:12 +0100
commitc0148cb62800789e94ef41e34bee53e58fac02f2 (patch)
tree8c2c5588cb075b4ef94f6ca81f8f2e3228c1097e /src/tftp.rs
parent5bf4135847954bec6b8c90ef2996439783ae8056 (diff)
split some code into modules
Diffstat (limited to 'src/tftp.rs')
-rw-r--r--src/tftp.rs365
1 files changed, 365 insertions, 0 deletions
diff --git a/src/tftp.rs b/src/tftp.rs
new file mode 100644
index 0000000..becaa65
--- /dev/null
+++ b/src/tftp.rs
@@ -0,0 +1,365 @@
1use std::{
2 io::{Cursor, Read as _, Result, Write},
3 net::UdpSocket,
4 path::{Path, PathBuf},
5 str::FromStr,
6};
7
8use crate::wire;
9
10pub const PORT: u16 = 69;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct InvalidTftpOp(u16);
14
15impl std::fmt::Display for InvalidTftpOp {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 write!(f, "invalid tftp opcode '{}'", self.0)
18 }
19}
20
21impl std::error::Error for InvalidTftpOp {}
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum TftpOp {
24 ReadRequest,
25 WriteRequest,
26 Data,
27 Ack,
28 Error,
29 Oack,
30}
31
32impl Into<u16> for TftpOp {
33 fn into(self) -> u16 {
34 match self {
35 TftpOp::ReadRequest => 1,
36 TftpOp::WriteRequest => 2,
37 TftpOp::Data => 3,
38 TftpOp::Ack => 4,
39 TftpOp::Error => 5,
40 TftpOp::Oack => 6,
41 }
42 }
43}
44
45impl TryFrom<u16> for TftpOp {
46 type Error = InvalidTftpOp;
47
48 fn try_from(value: u16) -> std::result::Result<Self, InvalidTftpOp> {
49 match value {
50 1 => Ok(Self::ReadRequest),
51 2 => Ok(Self::WriteRequest),
52 3 => Ok(Self::Data),
53 4 => Ok(Self::Ack),
54 5 => Ok(Self::Error),
55 6 => Ok(Self::Oack),
56 unknown => Err(InvalidTftpOp(unknown)),
57 }
58 }
59}
60
61#[derive(Debug)]
62pub struct InvalidTftpMode(String);
63
64impl std::fmt::Display for InvalidTftpMode {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "invalid tftp mode '{}'", self.0)
67 }
68}
69
70impl std::error::Error for InvalidTftpMode {}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73pub enum TftpMode {
74 NetAscii,
75 Octet,
76 Mail,
77}
78
79impl FromStr for TftpMode {
80 type Err = InvalidTftpMode;
81
82 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
83 match s.to_lowercase().as_str() {
84 "netascii" => Ok(Self::NetAscii),
85 "octet" => Ok(Self::Octet),
86 "mail" => Ok(Self::Mail),
87 _ => Err(InvalidTftpMode(s.to_string())),
88 }
89 }
90}
91
92#[derive(Debug)]
93pub enum TftpPacket {
94 Request(TftpRequestPacket),
95 Data(TftpDataPacket),
96 Ack(TftpAckPacket),
97 OAck(TftpOAckPacket),
98 Error(TftpErrorPacket),
99}
100
101impl TftpPacket {
102 pub fn write<W: Write>(&self, writer: W) -> Result<()> {
103 match self {
104 TftpPacket::Request(p) => p.write(writer),
105 TftpPacket::Data(p) => p.write(writer),
106 TftpPacket::Ack(p) => p.write(writer),
107 TftpPacket::OAck(p) => p.write(writer),
108 TftpPacket::Error(p) => p.write(writer),
109 }
110 }
111}
112
113#[derive(Debug)]
114pub struct TftpRequestPacket {
115 pub filename: String,
116 pub mode: TftpMode,
117 pub tsize: Option<u64>,
118 pub blksize: Option<u64>,
119}
120
121impl TftpRequestPacket {
122 pub fn write<W: Write>(&self, mut writer: W) -> Result<()> {
123 todo!()
124 }
125}
126
127#[derive(Debug)]
128pub struct TftpDataPacket {
129 pub block: u16,
130 pub data: Vec<u8>,
131}
132
133impl TftpDataPacket {
134 pub fn write<W: Write>(&self, mut writer: W) -> Result<()> {
135 wire::write_u16(&mut writer, TftpOp::Data.into())?;
136 wire::write_u16(&mut writer, self.block)?;
137 wire::write(&mut writer, &self.data)?;
138 Ok(())
139 }
140}
141
142#[derive(Debug)]
143pub struct TftpAckPacket {
144 pub block: u16,
145}
146
147impl TftpAckPacket {
148 pub fn write<W: Write>(&self, mut writer: W) -> Result<()> {
149 wire::write_u16(&mut writer, TftpOp::Data.into())?;
150 wire::write_u16(&mut writer, self.block)?;
151 Ok(())
152 }
153}
154
155#[derive(Debug)]
156pub struct TftpOAckPacket {
157 pub tsize: Option<u64>,
158 pub blksize: Option<u64>,
159}
160
161impl TftpOAckPacket {
162 pub fn write<W: Write>(&self, mut writer: W) -> Result<()> {
163 wire::write_u16(&mut writer, TftpOp::Oack.into())?;
164
165 // Only include options that were requested by the client
166 if let Some(blksize_val) = self.blksize {
167 wire::write(&mut writer, b"blksize")?;
168 wire::write_u8(&mut writer, 0)?; // null terminator
169 let blksize_str = blksize_val.to_string();
170 wire::write(&mut writer, blksize_str.as_bytes())?;
171 wire::write_u8(&mut writer, 0)?; // null terminator
172 }
173
174 if let Some(tsize_val) = self.tsize {
175 wire::write(&mut writer, b"tsize")?;
176 wire::write_u8(&mut writer, 0)?; // null terminator
177 let tsize_str = tsize_val.to_string();
178 wire::write(&mut writer, tsize_str.as_bytes())?;
179 wire::write_u8(&mut writer, 0)?; // null terminator
180 }
181
182 Ok(())
183 }
184}
185
186#[derive(Debug)]
187pub struct TftpErrorPacket {
188 pub code: u16,
189 pub message: String,
190}
191
192impl TftpErrorPacket {
193 pub fn write<W: Write>(&self, mut writer: W) -> Result<()> {
194 wire::write_u16(&mut writer, TftpOp::Error.into())?;
195 wire::write_u16(&mut writer, self.code)?;
196 wire::write_null_terminated_string(&mut writer, &self.message)?;
197 Ok(())
198 }
199}
200
201pub fn parse_packet(buf: &[u8]) -> Result<TftpPacket> {
202 let mut cursor = Cursor::new(buf);
203 let op = TftpOp::try_from(wire::read_u16(&mut cursor)?).unwrap();
204
205 match op {
206 TftpOp::ReadRequest => {
207 let filename = wire::read_null_terminated_string(&mut cursor)?;
208 let mode = wire::read_null_terminated_string(&mut cursor)?
209 .parse::<TftpMode>()
210 .unwrap();
211
212 let mut tsize = None;
213 let mut blksize = None;
214
215 while let Ok(opt_name) = wire::read_null_terminated_string(&mut cursor) {
216 if opt_name.is_empty() {
217 break;
218 }
219 let opt_data = wire::read_null_terminated_string(&mut cursor)?;
220 match opt_name.as_str() {
221 "tsize" => tsize = Some(opt_data.parse::<u64>().unwrap()),
222 "blksize" => blksize = Some(opt_data.parse::<u64>().unwrap()),
223 _ => eprintln!("unknown tftp request option '{opt_name}'"),
224 }
225 }
226
227 Ok(TftpPacket::Request(TftpRequestPacket {
228 filename,
229 mode,
230 tsize,
231 blksize,
232 }))
233 }
234 TftpOp::WriteRequest => unimplemented!(),
235 TftpOp::Data => {
236 let block = wire::read_u16(&mut cursor)?;
237 let mut data = Vec::new();
238 cursor.read_to_end(&mut data)?;
239 Ok(TftpPacket::Data(TftpDataPacket { block, data }))
240 }
241 TftpOp::Ack => {
242 let block = wire::read_u16(&mut cursor)?;
243 Ok(TftpPacket::Ack(TftpAckPacket { block }))
244 }
245 TftpOp::Error => {
246 let code = wire::read_u16(&mut cursor)?;
247 let message = wire::read_null_terminated_string(&mut cursor)?;
248 Ok(TftpPacket::Error(TftpErrorPacket { code, message }))
249 }
250 TftpOp::Oack => {
251 let mut tsize = None;
252 let mut blksize = None;
253
254 while let Ok(opt_name) = wire::read_null_terminated_string(&mut cursor) {
255 if opt_name.is_empty() {
256 break;
257 }
258 let opt_data = wire::read_null_terminated_string(&mut cursor)?;
259 match opt_name.as_str() {
260 "tsize" => tsize = Some(opt_data.parse::<u64>().unwrap()),
261 "blksize" => blksize = Some(opt_data.parse::<u64>().unwrap()),
262 _ => eprintln!("unknown tftp ack option '{opt_name}'"),
263 }
264 }
265 Ok(TftpPacket::OAck(TftpOAckPacket { tsize, blksize }))
266 }
267 }
268}
269
270pub fn serve(dir: &Path) -> Result<()> {
271 let socket = UdpSocket::bind(format!("0.0.0.0:{PORT}"))?;
272
273 // TODO: this needs to be done per addr
274 let mut last_blksize = 512u64;
275 let mut current_file = PathBuf::default();
276
277 loop {
278 let mut buf = [0u8; 1500];
279 let (n, addr) = socket.recv_from(&mut buf)?;
280 let packet = parse_packet(&buf[..n]).unwrap();
281
282 let response = match packet {
283 TftpPacket::Request(req) => {
284 println!(
285 "Request options: tsize={:?}, blksize={:?}",
286 req.tsize, req.blksize
287 );
288
289 let filepath = dir.join(req.filename);
290 current_file = filepath.clone();
291 let meta = std::fs::metadata(&filepath).unwrap();
292 let actual_file_size = meta.len();
293
294 // Only send OACK if client sent options
295 if req.tsize.is_some() || req.blksize.is_some() {
296 if let Some(blksize) = req.blksize {
297 last_blksize = blksize;
298 }
299
300 let tsize_response = if req.tsize.is_some() {
301 Some(actual_file_size)
302 } else {
303 None
304 };
305
306 Some(TftpPacket::OAck(TftpOAckPacket {
307 tsize: req.tsize,
308 blksize: req.blksize,
309 }))
310 } else {
311 // No options, send first data block directly
312 let contents = std::fs::read(&filepath).unwrap();
313 let block_size = 512;
314 let first_block = if contents.len() > block_size {
315 contents[..block_size].to_vec()
316 } else {
317 contents
318 };
319
320 Some(TftpPacket::Data(TftpDataPacket {
321 block: 1,
322 data: first_block,
323 }))
324 }
325 }
326 TftpPacket::Data(dat) => unimplemented!(),
327 TftpPacket::Ack(ack) => {
328 println!("Received ACK packet: block {}", ack.block);
329
330 let contents = std::fs::read(&current_file).unwrap();
331 let next_block = ack.block + 1;
332 let start_offset = (next_block - 1) as u64 * last_blksize;
333 let end_offset = next_block as u64 * last_blksize;
334 let prev_start_offset = (next_block.saturating_sub(2)) as u64 * last_blksize;
335 let prev_remain = contents.len() - prev_start_offset as usize;
336 if prev_remain as u64 >= last_blksize || ack.block == 0 {
337 let end = std::cmp::min(end_offset as usize, contents.len());
338 let block_data = contents[start_offset as usize..end].to_vec();
339 println!("sending tftp data packet with {} bytes", block_data.len());
340 Some(TftpPacket::Data(TftpDataPacket {
341 block: next_block,
342 data: block_data,
343 }))
344 } else {
345 None
346 }
347 }
348 TftpPacket::OAck(ack) => todo!(),
349 TftpPacket::Error(err) => {
350 println!(
351 "Received ERROR packet: code {}, message: {}",
352 err.code, err.message
353 );
354 None
355 }
356 };
357
358 if let Some(response) = response {
359 let mut writer = Cursor::new(&mut buf[..]);
360 response.write(&mut writer).unwrap();
361 let (response, _) = writer.split();
362 socket.send_to(&response, addr).unwrap();
363 }
364 }
365}