1use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9use std::collections::BTreeMap;
10use std::path::{Path, PathBuf};
11use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
12
13pub mod capabilities;
14pub mod flow_control;
15pub use capabilities::{
16 RequiredTransferCapabilities, TransferCapabilities, MAX_CAPABILITY_BYTES, MAX_SAFE_FILE_BYTES,
17 REQUIRED_CAPABILITIES_METADATA_KEY,
18};
19
20const ACK_OK: u8 = 0x01;
21const ACK_REJECTED: u8 = 0x02;
22pub const DEFAULT_CHUNK_BYTES: usize = 64 * 1024;
23pub const DEFAULT_MAX_HEADER_BYTES: usize = 64 * 1024;
24pub const DEFAULT_MAX_FILE_BYTES: u64 = 256 * 1024 * 1024;
25pub const ROUTED_FRAME_MAGIC: &[u8; 4] = b"OFT2";
26pub const DEFAULT_ROUTED_CHUNK_BYTES: usize = 48 * 1024;
27pub const MAX_ROUTED_TRANSFER_ID_BYTES: usize = 512;
28
29const ROUTED_KIND_OFFER: u8 = 0x01;
30const ROUTED_KIND_RESUME: u8 = 0x02;
31const ROUTED_KIND_CHUNK: u8 = 0x03;
32const ROUTED_KIND_ACK: u8 = 0x04;
33const ROUTED_KIND_COMPLETE: u8 = 0x05;
34const ROUTED_KIND_COMPLETED: u8 = 0x06;
35const ROUTED_KIND_REJECT: u8 = 0x07;
36const ROUTED_KIND_WAITING: u8 = 0x08;
37const ROUTED_KIND_CAPABILITIES_REQUEST: u8 = 0x09;
38const ROUTED_KIND_CAPABILITIES: u8 = 0x0a;
39pub const RECEIVER_WAIT_METADATA_KEY: &str = "openrtc.receive-wait";
40pub const MAX_RECEIVER_WAIT_LEASE_MS: u32 = 60_000;
41
42#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum RoutedTransferFrame {
47 Offer(FileTransferHeader),
48 CapabilitiesRequest {
50 transfer_id: String,
51 header: Option<FileTransferHeader>,
52 },
53 Capabilities {
54 transfer_id: String,
55 capabilities: TransferCapabilities,
56 },
57 Resume {
58 transfer_id: String,
59 offset: u64,
60 },
61 Chunk {
62 transfer_id: String,
63 offset: u64,
64 ack_requested: bool,
65 bytes: Vec<u8>,
66 },
67 Ack {
68 transfer_id: String,
69 offset: u64,
70 },
71 Waiting {
73 transfer_id: String,
74 offset: u64,
75 lease_ms: u32,
76 },
77 Complete {
78 transfer_id: String,
79 size: u64,
80 },
81 Completed {
82 transfer_id: String,
83 size: u64,
84 },
85 Reject {
86 transfer_id: String,
87 reason: String,
88 },
89}
90
91impl RoutedTransferFrame {
92 pub fn transfer_id(&self) -> &str {
93 match self {
94 Self::Offer(header) => &header.transfer_id,
95 Self::Resume { transfer_id, .. }
96 | Self::Chunk { transfer_id, .. }
97 | Self::Ack { transfer_id, .. }
98 | Self::Waiting { transfer_id, .. }
99 | Self::Complete { transfer_id, .. }
100 | Self::Completed { transfer_id, .. }
101 | Self::Reject { transfer_id, .. }
102 | Self::CapabilitiesRequest { transfer_id, .. }
103 | Self::Capabilities { transfer_id, .. } => transfer_id,
104 }
105 }
106
107 pub fn encode(&self, limits: TransferLimits) -> Result<Vec<u8>, TransferError> {
108 let (kind, transfer_id) = match self {
109 Self::Offer(header) => (ROUTED_KIND_OFFER, header.transfer_id.as_str()),
110 Self::Resume { transfer_id, .. } => (ROUTED_KIND_RESUME, transfer_id.as_str()),
111 Self::Chunk { transfer_id, .. } => (ROUTED_KIND_CHUNK, transfer_id.as_str()),
112 Self::Ack { transfer_id, .. } => (ROUTED_KIND_ACK, transfer_id.as_str()),
113 Self::Waiting { transfer_id, .. } => (ROUTED_KIND_WAITING, transfer_id.as_str()),
114 Self::Complete { transfer_id, .. } => (ROUTED_KIND_COMPLETE, transfer_id.as_str()),
115 Self::Completed { transfer_id, .. } => (ROUTED_KIND_COMPLETED, transfer_id.as_str()),
116 Self::Reject { transfer_id, .. } => (ROUTED_KIND_REJECT, transfer_id.as_str()),
117 Self::CapabilitiesRequest { transfer_id, .. } => {
118 (ROUTED_KIND_CAPABILITIES_REQUEST, transfer_id.as_str())
119 }
120 Self::Capabilities { transfer_id, .. } => {
121 (ROUTED_KIND_CAPABILITIES, transfer_id.as_str())
122 }
123 };
124 let id = transfer_id.as_bytes();
125 if id.is_empty() || id.len() > MAX_ROUTED_TRANSFER_ID_BYTES || id.len() > u16::MAX as usize
126 {
127 return Err(TransferError::Limit(
128 "routed transferId length is invalid".into(),
129 ));
130 }
131 let mut output = Vec::with_capacity(7 + id.len() + 16);
132 output.extend_from_slice(ROUTED_FRAME_MAGIC);
133 output.push(kind);
134 output.extend_from_slice(&(id.len() as u16).to_be_bytes());
135 output.extend_from_slice(id);
136 match self {
137 Self::CapabilitiesRequest { header, .. } => {
138 let encoded = if let Some(header) = header {
139 header.validate(TransferLimits {
140 max_file_bytes: MAX_SAFE_FILE_BYTES,
141 ..limits
142 })?;
143 if header.transfer_id != transfer_id {
144 return Err(TransferError::Protocol(
145 "routed capability transferId mismatch".into(),
146 ));
147 }
148 serde_json::to_vec(header)?
149 } else {
150 Vec::new()
151 };
152 if encoded.len() > limits.max_header_bytes || encoded.len() > u32::MAX as usize {
153 return Err(TransferError::Limit(
154 "routed capability header is too large".into(),
155 ));
156 }
157 output.extend_from_slice(&(encoded.len() as u32).to_be_bytes());
158 output.extend_from_slice(&encoded);
159 }
160 Self::Capabilities { capabilities, .. } => {
161 let encoded = serde_json::to_vec(&capabilities.clone().normalized()?)?;
162 if encoded.len() > MAX_CAPABILITY_BYTES {
163 return Err(TransferError::Limit(
164 "routed capabilities are too large".into(),
165 ));
166 }
167 output.extend_from_slice(&(encoded.len() as u32).to_be_bytes());
168 output.extend_from_slice(&encoded);
169 }
170 Self::Offer(header) => {
171 header.validate(limits)?;
172 let encoded = serde_json::to_vec(header)?;
173 if encoded.is_empty()
174 || encoded.len() > limits.max_header_bytes
175 || encoded.len() > u32::MAX as usize
176 {
177 return Err(TransferError::Limit(
178 "routed offer header is too large".into(),
179 ));
180 }
181 output.extend_from_slice(&(encoded.len() as u32).to_be_bytes());
182 output.extend_from_slice(&encoded);
183 }
184 Self::Resume { offset, .. } | Self::Ack { offset, .. } => {
185 output.extend_from_slice(&offset.to_be_bytes());
186 }
187 Self::Waiting {
188 offset, lease_ms, ..
189 } => {
190 if *offset > limits.max_file_bytes
191 || *lease_ms == 0
192 || *lease_ms > MAX_RECEIVER_WAIT_LEASE_MS
193 {
194 return Err(TransferError::Limit(
195 "invalid receiver wait lease or offset".into(),
196 ));
197 }
198 output.extend_from_slice(&offset.to_be_bytes());
199 output.extend_from_slice(&lease_ms.to_be_bytes());
200 }
201 Self::Chunk {
202 offset,
203 ack_requested,
204 bytes,
205 ..
206 } => {
207 if bytes.is_empty() || bytes.len() > limits.chunk_bytes {
208 return Err(TransferError::Limit(format!(
209 "routed chunk must contain 1..={} bytes",
210 limits.chunk_bytes
211 )));
212 }
213 output.extend_from_slice(&offset.to_be_bytes());
214 output.push(u8::from(*ack_requested));
215 output.extend_from_slice(bytes);
216 }
217 Self::Complete { size, .. } | Self::Completed { size, .. } => {
218 output.extend_from_slice(&size.to_be_bytes());
219 }
220 Self::Reject { reason, .. } => {
221 let bytes = reason.as_bytes();
222 if bytes.len() > limits.max_header_bytes || bytes.len() > u16::MAX as usize {
223 return Err(TransferError::Limit(
224 "routed rejection reason is too large".into(),
225 ));
226 }
227 output.extend_from_slice(&(bytes.len() as u16).to_be_bytes());
228 output.extend_from_slice(bytes);
229 }
230 }
231 Ok(output)
232 }
233
234 pub fn decode(input: &[u8], limits: TransferLimits) -> Result<Self, TransferError> {
235 if input.len() < 7 || &input[..4] != ROUTED_FRAME_MAGIC {
236 return Err(TransferError::Protocol(
237 "not a routed file-transfer frame".into(),
238 ));
239 }
240 let kind = input[4];
241 let id_len = u16::from_be_bytes([input[5], input[6]]) as usize;
242 if id_len == 0 || id_len > MAX_ROUTED_TRANSFER_ID_BYTES || input.len() < 7 + id_len {
243 return Err(TransferError::Protocol("invalid routed transferId".into()));
244 }
245 let transfer_id = std::str::from_utf8(&input[7..7 + id_len])
246 .map_err(|_| TransferError::Protocol("routed transferId is not UTF-8".into()))?
247 .to_string();
248 let body = &input[7 + id_len..];
249 let read_u64 = |bytes: &[u8]| -> Result<u64, TransferError> {
250 let bytes: [u8; 8] = bytes
251 .get(..8)
252 .ok_or_else(|| TransferError::Protocol("truncated routed frame".into()))?
253 .try_into()
254 .map_err(|_| TransferError::Protocol("truncated routed frame".into()))?;
255 Ok(u64::from_be_bytes(bytes))
256 };
257 match kind {
258 ROUTED_KIND_CAPABILITIES_REQUEST | ROUTED_KIND_CAPABILITIES => {
259 let bytes: [u8; 4] = body
260 .get(..4)
261 .ok_or_else(|| TransferError::Protocol("truncated routed capabilities".into()))?
262 .try_into()
263 .unwrap();
264 let len = u32::from_be_bytes(bytes) as usize;
265 let bound = if kind == ROUTED_KIND_CAPABILITIES {
266 MAX_CAPABILITY_BYTES
267 } else {
268 limits.max_header_bytes
269 };
270 if len > bound || body.len() != 4 + len {
271 return Err(TransferError::Limit(
272 "invalid routed capability length".into(),
273 ));
274 }
275 if kind == ROUTED_KIND_CAPABILITIES {
276 let capabilities: TransferCapabilities = serde_json::from_slice(&body[4..])?;
277 return Ok(Self::Capabilities {
278 transfer_id,
279 capabilities: capabilities.normalized()?,
280 });
281 }
282 let header = if len == 0 {
283 None
284 } else {
285 let header: FileTransferHeader = serde_json::from_slice(&body[4..])?;
286 header.validate(TransferLimits {
287 max_file_bytes: MAX_SAFE_FILE_BYTES,
288 ..limits
289 })?;
290 if header.transfer_id != transfer_id {
291 return Err(TransferError::Protocol(
292 "routed capability transferId mismatch".into(),
293 ));
294 }
295 Some(header)
296 };
297 Ok(Self::CapabilitiesRequest {
298 transfer_id,
299 header,
300 })
301 }
302 ROUTED_KIND_OFFER => {
303 let len_bytes: [u8; 4] = body
304 .get(..4)
305 .ok_or_else(|| TransferError::Protocol("truncated routed offer".into()))?
306 .try_into()
307 .map_err(|_| TransferError::Protocol("truncated routed offer".into()))?;
308 let len = u32::from_be_bytes(len_bytes) as usize;
309 if len == 0 || len > limits.max_header_bytes || body.len() != 4 + len {
310 return Err(TransferError::Limit(
311 "invalid routed offer header length".into(),
312 ));
313 }
314 let header: FileTransferHeader = serde_json::from_slice(&body[4..])?;
315 header.validate(limits)?;
316 if header.transfer_id != transfer_id {
317 return Err(TransferError::Protocol(
318 "routed offer transferId mismatch".into(),
319 ));
320 }
321 Ok(Self::Offer(header))
322 }
323 ROUTED_KIND_RESUME if body.len() == 8 => Ok(Self::Resume {
324 transfer_id,
325 offset: read_u64(body)?,
326 }),
327 ROUTED_KIND_ACK if body.len() == 8 => Ok(Self::Ack {
328 transfer_id,
329 offset: read_u64(body)?,
330 }),
331 ROUTED_KIND_COMPLETE if body.len() == 8 => Ok(Self::Complete {
332 transfer_id,
333 size: read_u64(body)?,
334 }),
335 ROUTED_KIND_COMPLETED if body.len() == 8 => Ok(Self::Completed {
336 transfer_id,
337 size: read_u64(body)?,
338 }),
339 ROUTED_KIND_WAITING if body.len() == 12 => {
340 let offset = read_u64(body)?;
341 let lease_ms = u32::from_be_bytes(body[8..12].try_into().unwrap());
342 if offset > limits.max_file_bytes
343 || lease_ms == 0
344 || lease_ms > MAX_RECEIVER_WAIT_LEASE_MS
345 {
346 return Err(TransferError::Limit(
347 "invalid receiver wait lease or offset".into(),
348 ));
349 }
350 Ok(Self::Waiting {
351 transfer_id,
352 offset,
353 lease_ms,
354 })
355 }
356 ROUTED_KIND_CHUNK if body.len() > 9 => {
357 let offset = read_u64(body)?;
358 let ack_requested = match body[8] {
359 0 => false,
360 1 => true,
361 _ => {
362 return Err(TransferError::Protocol(
363 "invalid routed chunk ACK flag".into(),
364 ))
365 }
366 };
367 let bytes = body[9..].to_vec();
368 if bytes.len() > limits.chunk_bytes {
369 return Err(TransferError::Limit(
370 "routed chunk exceeds configured limit".into(),
371 ));
372 }
373 Ok(Self::Chunk {
374 transfer_id,
375 offset,
376 ack_requested,
377 bytes,
378 })
379 }
380 ROUTED_KIND_REJECT => {
381 let len_bytes: [u8; 2] = body
382 .get(..2)
383 .ok_or_else(|| TransferError::Protocol("truncated routed rejection".into()))?
384 .try_into()
385 .map_err(|_| TransferError::Protocol("truncated routed rejection".into()))?;
386 let len = u16::from_be_bytes(len_bytes) as usize;
387 if body.len() != 2 + len {
388 return Err(TransferError::Protocol(
389 "invalid routed rejection length".into(),
390 ));
391 }
392 let reason = std::str::from_utf8(&body[2..])
393 .map_err(|_| TransferError::Protocol("routed rejection is not UTF-8".into()))?
394 .to_string();
395 Ok(Self::Reject {
396 transfer_id,
397 reason,
398 })
399 }
400 _ => Err(TransferError::Protocol(
401 "invalid routed file-transfer frame".into(),
402 )),
403 }
404 }
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
408#[serde(rename_all = "camelCase")]
409pub struct FileTransferHeader {
410 pub version: u8,
411 pub transfer_id: String,
412 pub name: String,
413 pub size: u64,
414 #[serde(default, skip_serializing_if = "is_zero")]
415 pub offset: u64,
416 pub mime_type: String,
417 #[serde(skip_serializing_if = "Option::is_none")]
418 pub sha256: Option<String>,
419 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
420 pub metadata: BTreeMap<String, String>,
421}
422
423impl FileTransferHeader {
424 pub fn new(name: impl Into<String>, size: u64, mime_type: impl Into<String>) -> Self {
425 Self {
426 version: 1,
427 transfer_id: uuid::Uuid::new_v4().to_string(),
428 name: name.into(),
429 size,
430 offset: 0,
431 mime_type: mime_type.into(),
432 sha256: None,
433 metadata: BTreeMap::new(),
434 }
435 }
436
437 pub fn validate(&self, limits: TransferLimits) -> Result<(), TransferError> {
438 if self.version != 1 {
439 return Err(TransferError::Protocol(
440 "unsupported file-transfer version".into(),
441 ));
442 }
443 if self.transfer_id.trim().is_empty() {
444 return Err(TransferError::Protocol("transferId is required".into()));
445 }
446 if self.name.trim().is_empty() || self.name.contains('\0') {
447 return Err(TransferError::Protocol("file name is invalid".into()));
448 }
449 if self.size > limits.max_file_bytes {
450 return Err(TransferError::Limit(format!(
451 "declared file size {} exceeds {} bytes",
452 self.size, limits.max_file_bytes
453 )));
454 }
455 if self.offset > self.size {
456 return Err(TransferError::Protocol(
457 "resume offset exceeds declared file size".into(),
458 ));
459 }
460 if let Some(digest) = &self.sha256 {
461 if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) {
462 return Err(TransferError::Protocol("invalid SHA-256 digest".into()));
463 }
464 }
465 self.required_capabilities()?;
466 Ok(())
467 }
468}
469
470#[derive(Debug, Clone, Copy)]
471pub struct TransferLimits {
472 pub max_header_bytes: usize,
473 pub max_file_bytes: u64,
474 pub chunk_bytes: usize,
475}
476
477impl Default for TransferLimits {
478 fn default() -> Self {
479 Self {
480 max_header_bytes: DEFAULT_MAX_HEADER_BYTES,
481 max_file_bytes: DEFAULT_MAX_FILE_BYTES,
482 chunk_bytes: DEFAULT_CHUNK_BYTES,
483 }
484 }
485}
486
487#[derive(Debug)]
488pub enum TransferError {
489 Io(std::io::Error),
490 Json(serde_json::Error),
491 Protocol(String),
492 Limit(String),
493 Rejected,
494 Integrity,
495}
496
497impl std::fmt::Display for TransferError {
498 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
499 match self {
500 Self::Io(error) => write!(f, "file-transfer IO error: {error}"),
501 Self::Json(error) => write!(f, "file-transfer JSON error: {error}"),
502 Self::Protocol(error) => write!(f, "file-transfer protocol error: {error}"),
503 Self::Limit(error) => write!(f, "file-transfer limit error: {error}"),
504 Self::Rejected => write!(f, "file transfer rejected by receiver"),
505 Self::Integrity => write!(f, "file-transfer SHA-256 verification failed"),
506 }
507 }
508}
509
510impl std::error::Error for TransferError {}
511impl From<std::io::Error> for TransferError {
512 fn from(value: std::io::Error) -> Self {
513 Self::Io(value)
514 }
515}
516impl From<serde_json::Error> for TransferError {
517 fn from(value: serde_json::Error) -> Self {
518 Self::Json(value)
519 }
520}
521impl From<flow_control::BulkFlowError> for TransferError {
522 fn from(value: flow_control::BulkFlowError) -> Self {
523 Self::Protocol(value.to_string())
524 }
525}
526
527#[derive(Clone, Copy)]
530pub struct BodyFlow<'a> {
531 pub controller: &'a flow_control::BulkFlowController,
532 pub peer: &'a str,
533}
534
535#[derive(Debug, Clone)]
536pub struct ReceivedFile {
537 pub header: FileTransferHeader,
538 pub path: PathBuf,
539}
540
541pub async fn send_path<S, F>(
542 stream: &mut S,
543 path: &Path,
544 header: FileTransferHeader,
545 limits: TransferLimits,
546 on_progress: F,
547) -> Result<(), TransferError>
548where
549 S: AsyncRead + AsyncWrite + Unpin,
550 F: FnMut(u64, u64),
551{
552 send_path_with_flow(stream, path, header, limits, None, on_progress).await
553}
554
555pub async fn send_path_with_flow<S, F>(
556 stream: &mut S,
557 path: &Path,
558 mut header: FileTransferHeader,
559 limits: TransferLimits,
560 flow: Option<BodyFlow<'_>>,
561 mut on_progress: F,
562) -> Result<(), TransferError>
563where
564 S: AsyncRead + AsyncWrite + Unpin,
565 F: FnMut(u64, u64),
566{
567 if limits.chunk_bytes == 0 {
568 return Err(TransferError::Limit("chunk size must be positive".into()));
569 }
570 if header.required_capabilities()?.is_some() {
571 return Err(TransferError::Protocol(
572 "transfer_capability_required: stream/v1 cannot negotiate required transfer features"
573 .into(),
574 ));
575 }
576 let chunk_bytes = limits.chunk_bytes.min(DEFAULT_CHUNK_BYTES);
577 let mut file = tokio::fs::File::open(path).await?;
578 let size = file.metadata().await?.len();
579 header.size = size;
580 if header.name.trim().is_empty() {
581 header.name = path
582 .file_name()
583 .and_then(|name| name.to_str())
584 .unwrap_or("transfer.bin")
585 .to_string();
586 }
587 if header.sha256.is_some() {
588 header.sha256 = Some(hash_file(&mut file, chunk_bytes).await?);
589 file.rewind().await?;
590 }
591 header.validate(limits)?;
592 write_header(stream, &header, limits.max_header_bytes).await?;
593
594 file.seek(std::io::SeekFrom::Start(header.offset)).await?;
595 let mut buffer = vec![0_u8; chunk_bytes];
596 let mut sent = header.offset;
597 on_progress(sent, size);
598 while sent < size {
599 let remaining = (size - sent).min(buffer.len() as u64) as usize;
600 let read = file.read(&mut buffer[..remaining]).await?;
601 if read == 0 {
602 return Err(TransferError::Protocol(
603 "source file ended before its metadata size".into(),
604 ));
605 }
606 if let Some(flow) = flow {
607 flow.controller
608 .acquire(flow_control::BulkDirection::Upload, flow.peer, read as u64)
609 .await?;
610 }
611 stream.write_all(&buffer[..read]).await?;
612 sent += read as u64;
613 on_progress(sent, size);
614 }
615 stream.flush().await?;
616 let ack = stream.read_u8().await?;
617 match ack {
618 ACK_OK => Ok(()),
619 ACK_REJECTED => Err(TransferError::Rejected),
620 _ => Err(TransferError::Protocol(
621 "invalid file-transfer acknowledgement".into(),
622 )),
623 }
624}
625
626pub async fn receive_to_directory<S, F>(
627 stream: &mut S,
628 directory: &Path,
629 limits: TransferLimits,
630 on_progress: F,
631) -> Result<ReceivedFile, TransferError>
632where
633 S: AsyncRead + AsyncWrite + Unpin,
634 F: FnMut(u64, u64),
635{
636 receive_to_directory_with_flow(stream, directory, limits, None, on_progress).await
637}
638
639pub async fn receive_to_directory_with_flow<S, F>(
640 stream: &mut S,
641 directory: &Path,
642 limits: TransferLimits,
643 flow: Option<BodyFlow<'_>>,
644 mut on_progress: F,
645) -> Result<ReceivedFile, TransferError>
646where
647 S: AsyncRead + AsyncWrite + Unpin,
648 F: FnMut(u64, u64),
649{
650 let result =
651 receive_to_directory_inner(stream, directory, limits, flow, &mut on_progress).await;
652 if result.is_err() {
653 let _ = stream.write_u8(ACK_REJECTED).await;
654 let _ = stream.flush().await;
655 }
656 result
657}
658
659async fn receive_to_directory_inner<S, F>(
660 stream: &mut S,
661 directory: &Path,
662 limits: TransferLimits,
663 flow: Option<BodyFlow<'_>>,
664 on_progress: &mut F,
665) -> Result<ReceivedFile, TransferError>
666where
667 S: AsyncRead + AsyncWrite + Unpin,
668 F: FnMut(u64, u64),
669{
670 if limits.chunk_bytes == 0 {
671 return Err(TransferError::Limit("chunk size must be positive".into()));
672 }
673 let chunk_bytes = limits.chunk_bytes.min(DEFAULT_CHUNK_BYTES);
674 let header = read_header(stream, limits).await?;
675 tokio::fs::create_dir_all(directory).await?;
676 let safe_name = sanitize_file_name(&header.name);
677 let destination = if header.offset > 0 {
678 directory.join(&safe_name)
679 } else {
680 unique_destination(directory, &safe_name).await?
681 };
682 let mut hasher = header.sha256.as_ref().map(|_| Sha256::new());
683 if header.offset > 0 {
684 let metadata = tokio::fs::metadata(&destination)
685 .await
686 .map_err(|_| TransferError::Protocol("resume destination does not exist".into()))?;
687 if metadata.len() != header.offset {
688 return Err(TransferError::Protocol(format!(
689 "resume destination length {} does not match offset {}",
690 metadata.len(),
691 header.offset
692 )));
693 }
694 if let Some(hasher) = hasher.as_mut() {
695 let mut existing = tokio::fs::File::open(&destination).await?;
696 let mut prefix = vec![0_u8; chunk_bytes];
697 loop {
698 let read = existing.read(&mut prefix).await?;
699 if read == 0 {
700 break;
701 }
702 hasher.update(&prefix[..read]);
703 }
704 }
705 }
706 let resume_stage = if header.offset > 0 {
710 Some(unique_destination(directory, &format!(".{}.openrtc-resume", safe_name)).await?)
711 } else {
712 None
713 };
714 let write_path = resume_stage.as_ref().unwrap_or(&destination);
715 let mut file = tokio::fs::OpenOptions::new()
716 .write(true)
717 .create_new(true)
718 .open(write_path)
719 .await?;
720 let mut buffer = vec![0_u8; chunk_bytes];
721 let mut received = header.offset;
722 on_progress(received, header.size);
723 while received < header.size {
724 let remaining = (header.size - received).min(buffer.len() as u64) as usize;
725 if let Some(flow) = flow {
726 flow.controller
727 .acquire(
728 flow_control::BulkDirection::Download,
729 flow.peer,
730 remaining as u64,
731 )
732 .await?;
733 }
734 stream.read_exact(&mut buffer[..remaining]).await?;
735 file.write_all(&buffer[..remaining]).await?;
736 if let Some(hasher) = hasher.as_mut() {
737 hasher.update(&buffer[..remaining]);
738 }
739 received += remaining as u64;
740 on_progress(received, header.size);
741 }
742 file.flush().await?;
743 if let (Some(expected), Some(hasher)) = (&header.sha256, hasher) {
744 let actual = hex_digest(hasher.finalize().as_slice());
745 if !actual.eq_ignore_ascii_case(expected) {
746 drop(file);
747 let _ = tokio::fs::remove_file(write_path).await;
750 return Err(TransferError::Integrity);
751 }
752 }
753 if let Some(stage_path) = resume_stage {
754 drop(file);
755 let mut stage = tokio::fs::File::open(&stage_path).await?;
756 let mut destination_file = tokio::fs::OpenOptions::new()
757 .append(true)
758 .open(&destination)
759 .await?;
760 tokio::io::copy(&mut stage, &mut destination_file).await?;
761 destination_file.flush().await?;
762 let _ = tokio::fs::remove_file(stage_path).await;
763 }
764 stream.write_u8(ACK_OK).await?;
765 stream.flush().await?;
766 Ok(ReceivedFile {
767 header,
768 path: destination,
769 })
770}
771
772async fn write_header<S: AsyncWrite + Unpin>(
773 stream: &mut S,
774 header: &FileTransferHeader,
775 max_header_bytes: usize,
776) -> Result<(), TransferError> {
777 let encoded = serde_json::to_vec(header)?;
778 if encoded.is_empty() || encoded.len() > max_header_bytes || encoded.len() > u32::MAX as usize {
779 return Err(TransferError::Limit(format!(
780 "header exceeds {max_header_bytes} bytes"
781 )));
782 }
783 stream.write_u32(encoded.len() as u32).await?;
784 stream.write_all(&encoded).await?;
785 Ok(())
786}
787
788async fn read_header<S: AsyncRead + Unpin>(
789 stream: &mut S,
790 limits: TransferLimits,
791) -> Result<FileTransferHeader, TransferError> {
792 let length = stream.read_u32().await? as usize;
793 if length == 0 || length > limits.max_header_bytes {
794 return Err(TransferError::Limit(format!(
795 "invalid header length {length}"
796 )));
797 }
798 let mut encoded = vec![0_u8; length];
799 stream.read_exact(&mut encoded).await?;
800 let header: FileTransferHeader = serde_json::from_slice(&encoded)?;
801 header.validate(limits)?;
802 if header.required_capabilities()?.is_some() {
803 return Err(TransferError::Protocol(
804 "transfer_capability_required: stream/v1 cannot negotiate required transfer features"
805 .into(),
806 ));
807 }
808 Ok(header)
809}
810
811async fn hash_file(
812 file: &mut tokio::fs::File,
813 chunk_bytes: usize,
814) -> Result<String, TransferError> {
815 let mut hasher = Sha256::new();
816 let mut buffer = vec![0_u8; chunk_bytes];
817 loop {
818 let read = file.read(&mut buffer).await?;
819 if read == 0 {
820 break;
821 }
822 hasher.update(&buffer[..read]);
823 }
824 Ok(hex_digest(hasher.finalize().as_slice()))
825}
826
827fn hex_digest(bytes: &[u8]) -> String {
828 let mut output = String::with_capacity(bytes.len() * 2);
829 for byte in bytes {
830 use std::fmt::Write as _;
831 let _ = write!(output, "{byte:02x}");
832 }
833 output
834}
835
836fn is_zero(value: &u64) -> bool {
837 *value == 0
838}
839
840pub fn sanitize_file_name(value: &str) -> String {
841 let candidate = value.replace('\\', "/");
842 let name = candidate.rsplit('/').next().unwrap_or("").trim();
843 let sanitized: String = name
844 .chars()
845 .map(|character| {
846 if character.is_control() || character == '\0' {
847 '_'
848 } else {
849 character
850 }
851 })
852 .take(240)
853 .collect();
854 match sanitized.as_str() {
855 "" | "." | ".." => "transfer.bin".to_string(),
856 _ => sanitized,
857 }
858}
859
860async fn unique_destination(directory: &Path, file_name: &str) -> Result<PathBuf, TransferError> {
861 let original = Path::new(file_name);
862 let stem = original
863 .file_stem()
864 .and_then(|value| value.to_str())
865 .unwrap_or("transfer");
866 let extension = original.extension().and_then(|value| value.to_str());
867 for suffix in 0..10_000_u32 {
868 let name = if suffix == 0 {
869 file_name.to_string()
870 } else if let Some(extension) = extension {
871 format!("{stem} ({suffix}).{extension}")
872 } else {
873 format!("{stem} ({suffix})")
874 };
875 let candidate = directory.join(name);
876 if !tokio::fs::try_exists(&candidate).await? {
877 return Ok(candidate);
878 }
879 }
880 Err(TransferError::Limit(
881 "unable to allocate a unique destination name".into(),
882 ))
883}
884
885#[cfg(test)]
886mod tests {
887 use super::*;
888
889 #[tokio::test(start_paused = true)]
890 async fn paced_parallel_streams_obey_directional_rates_and_preserve_exact_files() {
891 use flow_control::{BulkDirection, BulkFlowController, BulkRates};
892 use std::sync::{Arc, Mutex};
893 use tokio::time::Instant;
894 let source_dir = tempfile::tempdir().unwrap();
895 let source = source_dir.path().join("source.bin");
896 let payload: Vec<u8> = (0..3 * DEFAULT_CHUNK_BYTES)
897 .map(|index| (index % 251) as u8)
898 .collect();
899 std::fs::write(&source, &payload).unwrap();
900 let flow = BulkFlowController::new(BulkRates {
901 upload_bytes_per_second: Some(65536),
902 download_bytes_per_second: Some(131072),
903 })
904 .unwrap();
905 let events = Arc::new(Mutex::new(Vec::new()));
906 let start = Instant::now();
907 let mut jobs = Vec::new();
908 for peer in ["peer-a", "peer-b"] {
909 let (source, payload, flow, events) = (
910 source.clone(),
911 payload.clone(),
912 flow.clone(),
913 events.clone(),
914 );
915 jobs.push(tokio::spawn(async move {
916 let destination = tempfile::tempdir().unwrap();
917 let (mut sender, mut receiver) = tokio::io::duplex(DEFAULT_CHUNK_BYTES);
918 let limits = TransferLimits {
921 chunk_bytes: 16 * 1024 * 1024,
922 ..TransferLimits::default()
923 };
924 let mut header =
925 FileTransferHeader::new("received.bin", 0, "application/octet-stream");
926 header.sha256 = Some(String::new());
927 let (mut sent_last, mut received_last) = (0, 0);
928 let sent_events = events.clone();
929 let send = send_path_with_flow(
930 &mut sender,
931 &source,
932 header,
933 limits,
934 Some(BodyFlow {
935 controller: &flow,
936 peer,
937 }),
938 |bytes, _| {
939 if bytes > sent_last {
940 sent_events.lock().unwrap().push((
941 BulkDirection::Upload,
942 bytes - sent_last,
943 Instant::now().duration_since(start),
944 ));
945 sent_last = bytes;
946 }
947 },
948 );
949 let receive = receive_to_directory_with_flow(
950 &mut receiver,
951 destination.path(),
952 limits,
953 Some(BodyFlow {
954 controller: &flow,
955 peer,
956 }),
957 |bytes, _| {
958 if bytes > received_last {
959 events.lock().unwrap().push((
960 BulkDirection::Download,
961 bytes - received_last,
962 Instant::now().duration_since(start),
963 ));
964 received_last = bytes;
965 }
966 },
967 );
968 let (sent, received) = tokio::join!(send, receive);
969 sent.unwrap();
970 assert_eq!(std::fs::read(received.unwrap().path).unwrap(), payload);
971 }));
972 }
973 for job in jobs {
974 job.await.unwrap();
975 }
976 let events = events.lock().unwrap();
977 for (direction, rate) in [
978 (BulkDirection::Upload, 65536_u128),
979 (BulkDirection::Download, 131072),
980 ] {
981 let mut total = 0_u128;
982 for (_, bytes, elapsed) in events.iter().filter(|(value, _, _)| *value == direction) {
983 assert!(*bytes <= DEFAULT_CHUNK_BYTES as u64);
984 total += u128::from(*bytes);
985 assert!(
986 total
987 <= DEFAULT_CHUNK_BYTES as u128 + elapsed.as_nanos() * rate / 1_000_000_000
988 );
989 }
990 assert_eq!(total, (2 * payload.len()) as u128);
991 }
992 }
993
994 #[tokio::test]
995 async fn round_trip_streams_and_verifies_a_file() {
996 let source_dir = tempfile::tempdir().unwrap();
997 let destination_dir = tempfile::tempdir().unwrap();
998 let source = source_dir.path().join("plane.webp");
999 tokio::fs::write(&source, [1_u8, 2, 3, 4, 5]).await.unwrap();
1000 let (mut sender, mut receiver) = tokio::io::duplex(256 * 1024);
1001 let mut header = FileTransferHeader::new("plane.webp", 0, "image/webp");
1002 header.transfer_id = "transfer-1".to_string();
1003 header.sha256 = Some(String::new());
1004 let limits = TransferLimits::default();
1005
1006 let send = send_path(&mut sender, &source, header, limits, |_, _| {});
1007 let receive =
1008 receive_to_directory(&mut receiver, destination_dir.path(), limits, |_, _| {});
1009 let (sent, received) = tokio::join!(send, receive);
1010 sent.unwrap();
1011 let received = received.unwrap();
1012 assert_eq!(received.header.transfer_id, "transfer-1");
1013 assert_eq!(
1014 tokio::fs::read(received.path).await.unwrap(),
1015 [1, 2, 3, 4, 5]
1016 );
1017 }
1018
1019 #[test]
1020 fn sanitizes_cross_platform_path_traversal() {
1021 assert_eq!(sanitize_file_name("../../secret.txt"), "secret.txt");
1022 assert_eq!(
1023 sanitize_file_name(r"C:\\Users\\name\\secret.txt"),
1024 "secret.txt"
1025 );
1026 assert_eq!(sanitize_file_name(".."), "transfer.bin");
1027 }
1028
1029 #[tokio::test]
1030 async fn rejects_legacy_headers_before_creating_a_file() {
1031 for required_feature in [false, true] {
1032 let destination = tempfile::tempdir().unwrap();
1033 let (mut sender, mut receiver) = tokio::io::duplex(1024);
1034 let limits = if required_feature {
1035 let header = br#"{"version":1,"transferId":"legacy-required","name":"empty.bin","size":0,"mimeType":"application/octet-stream","metadata":{"openrtc.required-capabilities.v1":"{\"version\":1,\"durableResume\":false,\"completionReceipt\":false,\"extensions\":[\"example.destination.v1\"]}"}}"#;
1036 sender.write_u32(header.len() as u32).await.unwrap();
1037 sender.write_all(header).await.unwrap();
1038 TransferLimits::default()
1039 } else {
1040 sender.write_u32(1024).await.unwrap();
1041 TransferLimits {
1042 max_header_bytes: 16,
1043 ..TransferLimits::default()
1044 }
1045 };
1046 sender.flush().await.unwrap();
1047 let error = receive_to_directory(&mut receiver, destination.path(), limits, |_, _| {})
1048 .await
1049 .unwrap_err();
1050 if required_feature {
1051 assert!(error.to_string().contains("transfer_capability_required"));
1052 } else {
1053 assert!(matches!(error, TransferError::Limit(_)));
1054 }
1055 assert_eq!(std::fs::read_dir(destination.path()).unwrap().count(), 0);
1056 }
1057 }
1058
1059 #[tokio::test]
1060 async fn resumes_an_existing_verified_prefix() {
1061 let source_dir = tempfile::tempdir().unwrap();
1062 let destination_dir = tempfile::tempdir().unwrap();
1063 let source = source_dir.path().join("resume.bin");
1064 let destination = destination_dir.path().join("resume.bin");
1065 tokio::fs::write(&source, [1_u8, 2, 3, 4, 5]).await.unwrap();
1066 tokio::fs::write(&destination, [1_u8, 2]).await.unwrap();
1067 let (mut sender, mut receiver) = tokio::io::duplex(256 * 1024);
1068 let mut header = FileTransferHeader::new("resume.bin", 0, "application/octet-stream");
1069 header.transfer_id = "resume-1".to_string();
1070 header.offset = 2;
1071 header.sha256 = Some(String::new());
1072 let limits = TransferLimits::default();
1073
1074 let send = send_path(&mut sender, &source, header, limits, |_, _| {});
1075 let receive =
1076 receive_to_directory(&mut receiver, destination_dir.path(), limits, |_, _| {});
1077 let (sent, received) = tokio::join!(send, receive);
1078 sent.unwrap();
1079 assert_eq!(received.unwrap().header.offset, 2);
1080 assert_eq!(tokio::fs::read(destination).await.unwrap(), [1, 2, 3, 4, 5]);
1081 }
1082
1083 #[tokio::test]
1084 async fn corrupt_resume_preserves_the_existing_destination() {
1085 let destination_dir = tempfile::tempdir().unwrap();
1086 let destination = destination_dir.path().join("resume.bin");
1087 tokio::fs::write(&destination, [1_u8, 2]).await.unwrap();
1088 let (mut sender, mut receiver) = tokio::io::duplex(256 * 1024);
1089 let mut header = FileTransferHeader::new("resume.bin", 5, "application/octet-stream");
1090 header.transfer_id = "corrupt-resume".to_string();
1091 header.offset = 2;
1092 header.sha256 = Some("00".repeat(32));
1093 let limits = TransferLimits::default();
1094
1095 let send = async {
1096 write_header(&mut sender, &header, limits.max_header_bytes)
1097 .await
1098 .unwrap();
1099 sender.write_all(&[9_u8, 9, 9]).await.unwrap();
1100 sender.flush().await.unwrap();
1101 sender.read_u8().await.unwrap()
1102 };
1103 let receive =
1104 receive_to_directory(&mut receiver, destination_dir.path(), limits, |_, _| {});
1105 let (ack, received) = tokio::join!(send, receive);
1106
1107 assert_eq!(ack, ACK_REJECTED);
1108 assert!(matches!(received, Err(TransferError::Integrity)));
1109 assert_eq!(
1110 tokio::fs::read(&destination).await.unwrap(),
1111 [1, 2],
1112 "a corrupt resume must not delete or append to existing receiver data"
1113 );
1114 assert_eq!(
1115 std::fs::read_dir(destination_dir.path()).unwrap().count(),
1116 1,
1117 "a rejected resume must clean only its private staging file"
1118 );
1119 }
1120
1121 #[test]
1122 fn routed_frames_round_trip_offsets_chunks_and_completion() {
1123 let limits = TransferLimits {
1124 chunk_bytes: DEFAULT_ROUTED_CHUNK_BYTES,
1125 ..TransferLimits::default()
1126 };
1127 let mut header = FileTransferHeader::new("route.bin", 99, "application/octet-stream");
1128 header.transfer_id = "route-transfer-1".to_string();
1129 let frames = vec![
1130 RoutedTransferFrame::Offer(header),
1131 RoutedTransferFrame::Resume {
1132 transfer_id: "route-transfer-1".to_string(),
1133 offset: 24,
1134 },
1135 RoutedTransferFrame::Chunk {
1136 transfer_id: "route-transfer-1".to_string(),
1137 offset: 24,
1138 ack_requested: true,
1139 bytes: vec![1, 2, 3],
1140 },
1141 RoutedTransferFrame::Ack {
1142 transfer_id: "route-transfer-1".to_string(),
1143 offset: 27,
1144 },
1145 RoutedTransferFrame::Complete {
1146 transfer_id: "route-transfer-1".to_string(),
1147 size: 99,
1148 },
1149 RoutedTransferFrame::Completed {
1150 transfer_id: "route-transfer-1".to_string(),
1151 size: 99,
1152 },
1153 RoutedTransferFrame::Reject {
1154 transfer_id: "route-transfer-1".to_string(),
1155 reason: "ACL changed".to_string(),
1156 },
1157 ];
1158
1159 for frame in frames {
1160 let encoded = frame.encode(limits).unwrap();
1161 assert_eq!(
1162 RoutedTransferFrame::decode(&encoded, limits).unwrap(),
1163 frame
1164 );
1165 }
1166 }
1167
1168 #[test]
1169 fn receiver_wait_uses_independent_bounded_wire_contract() {
1170 let wire = [
1171 0x4f, 0x46, 0x54, 0x32, 8, 0, 1, 0x78, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 40,
1172 ];
1173 let limits = TransferLimits::default();
1174 let expected = RoutedTransferFrame::Waiting {
1175 transfer_id: "x".into(),
1176 offset: 2,
1177 lease_ms: 40,
1178 };
1179 assert_eq!(
1180 RoutedTransferFrame::decode(&wire, limits).unwrap(),
1181 expected
1182 );
1183 assert_eq!(expected.encode(limits).unwrap(), wire);
1184 for lease_ms in [0_u32, 60_001] {
1185 let mut malformed = wire;
1186 malformed[16..20].copy_from_slice(&lease_ms.to_be_bytes());
1187 assert!(RoutedTransferFrame::decode(&malformed, limits).is_err());
1188 assert!(RoutedTransferFrame::Waiting {
1189 transfer_id: "x".into(),
1190 offset: 2,
1191 lease_ms
1192 }
1193 .encode(limits)
1194 .is_err());
1195 }
1196 assert!(RoutedTransferFrame::decode(&wire[..19], limits).is_err());
1197 let small = TransferLimits {
1198 max_file_bytes: 1,
1199 ..limits
1200 };
1201 assert!(RoutedTransferFrame::decode(&wire, small).is_err());
1202 assert!(expected.encode(small).is_err());
1203 }
1204
1205 #[test]
1206 fn capabilities_use_independent_bounded_wire_contract() {
1207 let payload = br#"{"version":1,"durableResume":false,"completionReceipt":true,"extensions":["example.destination.v1"],"receiveAvailable":true,"maxFileBytes":107374182400}"#;
1208 let mut wire = vec![0x4f, 0x46, 0x54, 0x32, 10, 0, 1, 0x78, 0, 0, 0, 152];
1209 wire.extend_from_slice(payload);
1210 let limits = TransferLimits {
1211 max_file_bytes: 1,
1212 ..TransferLimits::default()
1213 };
1214 let expected = RoutedTransferFrame::Capabilities {
1215 transfer_id: "x".into(),
1216 capabilities: TransferCapabilities {
1217 version: 1,
1218 durable_resume: false,
1219 completion_receipt: true,
1220 extensions: vec!["example.destination.v1".into()],
1221 receive_available: true,
1222 max_file_bytes: 100 * 1024_u64.pow(3),
1223 },
1224 };
1225 assert_eq!(
1226 RoutedTransferFrame::decode(&wire, limits).unwrap(),
1227 expected
1228 );
1229 assert_eq!(expected.encode(limits).unwrap(), wire);
1230 let request = [0x4f, 0x46, 0x54, 0x32, 9, 0, 1, 0x78, 0, 0, 0, 0];
1231 let generic = RoutedTransferFrame::CapabilitiesRequest {
1232 transfer_id: "x".into(),
1233 header: None,
1234 };
1235 assert_eq!(
1236 RoutedTransferFrame::decode(&request, limits).unwrap(),
1237 generic
1238 );
1239 assert_eq!(generic.encode(limits).unwrap(), request);
1240 let text = std::str::from_utf8(payload).unwrap();
1241 for invalid in [
1242 text.replace("\"version\":1", "\"version\":2"),
1243 text.replace("\"durableResume\":false", "\"durableResume\":0"),
1244 text.replace("\"receiveAvailable\":true,", ""),
1245 text.replace(
1246 "\"extensions\":[\"example.destination.v1\"]",
1247 "\"extensions\":[\"x\",\"x\"]",
1248 ),
1249 text.replace("107374182400", "9007199254740992"),
1250 text.replace("\"version\":1", "\"unexpected\":true,\"version\":1"),
1251 ] {
1252 let mut malformed = wire[..8].to_vec();
1253 malformed.extend_from_slice(&(invalid.len() as u32).to_be_bytes());
1254 malformed.extend_from_slice(invalid.as_bytes());
1255 assert!(RoutedTransferFrame::decode(&malformed, limits).is_err());
1256 }
1257 assert!(RoutedTransferFrame::decode(&wire[..wire.len() - 1], limits).is_err());
1258 let mut trailing = request.to_vec();
1259 trailing.push(0);
1260 assert!(RoutedTransferFrame::decode(&trailing, limits).is_err());
1261 let mut header = FileTransferHeader::new(
1262 "large.bin",
1263 100 * 1024_u64.pow(3),
1264 "application/octet-stream",
1265 );
1266 header.transfer_id = "x".into();
1267 let probe = RoutedTransferFrame::CapabilitiesRequest {
1268 transfer_id: "x".into(),
1269 header: Some(header),
1270 };
1271 let mut encoded = probe.encode(limits).unwrap();
1272 assert_eq!(
1273 RoutedTransferFrame::decode(&encoded, limits).unwrap(),
1274 probe
1275 );
1276 encoded[7] = b'y';
1277 assert!(RoutedTransferFrame::decode(&encoded, limits).is_err());
1278 }
1279
1280 #[test]
1281 fn routed_chunk_rejects_oversized_payloads() {
1282 let limits = TransferLimits {
1283 chunk_bytes: 4,
1284 ..TransferLimits::default()
1285 };
1286 let frame = RoutedTransferFrame::Chunk {
1287 transfer_id: "route-transfer-1".to_string(),
1288 offset: 0,
1289 ack_requested: true,
1290 bytes: vec![0; 5],
1291 };
1292 assert!(matches!(frame.encode(limits), Err(TransferError::Limit(_))));
1293 }
1294}