Client LuaCsForBarotrauma
FileReceiver.cs
1 using Microsoft.Xna.Framework;
2 using System;
3 using System.Collections.Generic;
4 using System.Collections.Immutable;
5 using Barotrauma.IO;
6 using System.Linq;
7 using System.Threading;
8 using System.Xml;
9 
10 namespace Barotrauma.Networking
11 {
13  {
14  public class FileTransferIn : IDisposable
15  {
16  public string FileName
17  {
18  get;
19  private set;
20  }
21 
22  public string FilePath
23  {
24  get;
25  private set;
26  }
27 
28  public int FileSize
29  {
30  get;
31  set;
32  }
33 
34  public int Received
35  {
36  get;
37  private set;
38  }
39 
40  public int LastSeen { get; set; }
41 
43  {
44  get;
45  private set;
46  }
47 
49  {
50  get;
51  set;
52  }
53 
54  public DateTime LastOffsetAckTime
55  {
56  get;
57  private set;
58  }
59 
60  public void RecordOffsetAckTime()
61  {
62  LastOffsetAckTime = DateTime.Now;
63  }
64 
65  public float BytesPerSecond
66  {
67  get;
68  private set;
69  }
70 
71  public float Progress
72  {
73  get { return Received / (float)FileSize; }
74  }
75 
77  {
78  get;
79  private set;
80  }
81 
82  public int TimeStarted
83  {
84  get;
85  private set;
86  }
87 
89  {
90  get;
91  private set;
92  }
93 
94  public int ID;
95 
96  public const int DataBufferSize = 50;
100  public readonly Dictionary<int, byte[]> DataBuffer = new Dictionary<int, byte[]>();
101 
102  public FileTransferIn(NetworkConnection connection, string filePath, FileTransferType fileType)
103  {
104  FilePath = filePath;
105  FileName = Path.GetFileName(FilePath);
106  FileType = fileType;
107 
108  Connection = connection;
109 
110  Status = FileTransferStatus.NotStarted;
111 
112  LastOffsetAckTime = DateTime.Now - new TimeSpan(days: 0, hours: 0, minutes: 5, seconds: 0);
113  }
114 
115  public void OpenStream()
116  {
117  if (WriteStream != null)
118  {
119  WriteStream.Flush();
120  WriteStream.Close();
122  WriteStream = null;
123  }
124 
125  WriteStream = File.Open(FilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write);
126  TimeStarted = Environment.TickCount;
127  }
128 
129  public void ReadBytes(IReadMessage inc, int bytesToRead)
130  {
131  if (Received + bytesToRead > FileSize)
132  {
133  //strip out excess bytes
134  bytesToRead -= Received + bytesToRead - FileSize;
135  }
136 
137  ReadBytes(inc.ReadBytes(bytesToRead));
138  }
139 
140  public void ReadBytes(byte[] data)
141  {
142  Received += data.Length;
143  WriteStream.Write(data, 0, data.Length);
144 
145  int passed = Environment.TickCount - TimeStarted;
146  float psec = passed / 1000.0f;
147 
148  BytesPerSecond = Received / psec;
149 
150  var outdatedKeys = DataBuffer.Keys.Where(k => k < Received).ToList();
151  foreach (int key in outdatedKeys)
152  {
153  DataBuffer.Remove(key);
154  }
155 
156  Status = Received >= FileSize ? FileTransferStatus.Finished : FileTransferStatus.Receiving;
157  }
158 
159  private bool disposed = false;
160 
161  public void Dispose()
162  {
163  if (disposed) { return; }
164 
165  if (WriteStream != null)
166  {
167  WriteStream.Flush();
168  WriteStream.Close();
170  WriteStream = null;
171  }
172  disposed = true;
173  }
174  }
175 
176  private static int GetMaxFileSizeInBytes(FileTransferType fileTransferType) =>
177  fileTransferType switch
178  {
179  FileTransferType.Mod => 500 * 1024 * 1024, //500 MiB should be good enough, right?
180  _ => 50 * 1024 * 1024 //50 MiB for everything other than mods
181  };
182 
183  public delegate void TransferInDelegate(FileTransferIn fileStreamReceiver);
186 
187  private readonly List<FileTransferIn> activeTransfers;
188  private readonly List<(int transferId, double finishedTime)> finishedTransfers;
189 
190  private readonly ImmutableDictionary<FileTransferType, string> downloadFolders = new Dictionary<FileTransferType, string>()
191  {
192  { FileTransferType.Submarine, SaveUtil.SubmarineDownloadFolder },
193  { FileTransferType.CampaignSave, SaveUtil.CampaignDownloadFolder },
194  { FileTransferType.Mod, ModReceiver.DownloadFolder }
195  }.ToImmutableDictionary();
196 
197  public IReadOnlyList<FileTransferIn> ActiveTransfers => activeTransfers;
198  public bool HasActiveTransfers => ActiveTransfers.Any();
199 
200  public FileReceiver()
201  {
202  activeTransfers = new List<FileTransferIn>();
203  finishedTransfers = new List<(int transferId, double finishedTime)>();
204  }
205 
206  public void ReadMessage(IReadMessage inc)
207  {
208  System.Diagnostics.Debug.Assert(!activeTransfers.Any(t =>
209  t.Status == FileTransferStatus.Error ||
210  t.Status == FileTransferStatus.Canceled ||
211  t.Status == FileTransferStatus.Finished), "List of active file transfers contains entires that should have been removed");
212 
213  byte transferMessageType = inc.ReadByte();
214 
215  switch (transferMessageType)
216  {
217  case (byte)FileTransferMessageType.Initiate:
218  {
219  byte transferId = inc.ReadByte();
220  var existingTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.Endpoint) && t.ID == transferId);
221  finishedTransfers.RemoveAll(t => t.transferId == transferId);
222  byte fileType = inc.ReadByte();
223  //ushort chunkLen = inc.ReadUInt16();
224  int fileSize = inc.ReadInt32();
225  string fileName = inc.ReadString();
226 
227  if (existingTransfer != null)
228  {
229  if (fileType != (byte)existingTransfer.FileType ||
230  fileSize != existingTransfer.FileSize ||
231  fileName != existingTransfer.FileName)
232  {
233  GameMain.Client.CancelFileTransfer(transferId);
234  DebugConsole.AddWarning("File transfer error: file transfer initiated with an ID that's already in use");
235  }
236  else //resend acknowledgement packet
237  {
238  GameMain.Client.UpdateFileTransfer(existingTransfer, existingTransfer.Received, existingTransfer.LastSeen);
239  }
240  return;
241  }
242 
243  if (!ValidateInitialData(fileType, fileName, fileSize, out string errorMsg))
244  {
245  GameMain.Client.CancelFileTransfer(transferId);
246  DebugConsole.ThrowError("File transfer failed (" + errorMsg + ")");
247  return;
248  }
249 
250  if (GameSettings.CurrentConfig.VerboseLogging)
251  {
252  DebugConsole.Log("Received file transfer initiation message: ");
253  DebugConsole.Log(" File: " + fileName);
254  DebugConsole.Log(" Size: " + fileSize);
255  DebugConsole.Log(" ID: " + transferId);
256  }
257 
258  string downloadFolder = downloadFolders[(FileTransferType)fileType];
259  if (!Directory.Exists(downloadFolder))
260  {
261  try
262  {
263  Directory.CreateDirectory(downloadFolder, catchUnauthorizedAccessExceptions: false);
264  }
265  catch (Exception e)
266  {
267  DebugConsole.ThrowError("Could not start a file transfer: failed to create the folder \"" + downloadFolder + "\".", e);
268  return;
269  }
270  }
271 
272  FileTransferIn newTransfer = new FileTransferIn(inc.Sender, Path.Combine(downloadFolder, fileName), (FileTransferType)fileType)
273  {
274  ID = transferId,
275  Status = FileTransferStatus.Receiving,
276  FileSize = fileSize
277  };
278 
279  int maxRetries = 4;
280  for (int i = 0; i <= maxRetries; i++)
281  {
282  try
283  {
284  newTransfer.OpenStream();
285  }
286  catch (System.IO.IOException e)
287  {
288  if (i < maxRetries)
289  {
290  DebugConsole.NewMessage("Failed to initiate a file transfer {" + e.Message + "}, retrying in 250 ms...", Color.Red);
291  Thread.Sleep(250);
292  }
293  else
294  {
295  DebugConsole.NewMessage("Failed to initiate a file transfer {" + e.Message + "}", Color.Red);
296  GameMain.Client.CancelFileTransfer(transferId);
297  newTransfer.Status = FileTransferStatus.Error;
298  OnTransferFailed(newTransfer);
299  return;
300  }
301  }
302  }
303  activeTransfers.Add(newTransfer);
304 
305  GameMain.Client.UpdateFileTransfer(newTransfer, 0, 0); //send acknowledgement packet
306  }
307  break;
308  case (byte)FileTransferMessageType.TransferOnSameMachine:
309  {
310  byte transferId = inc.ReadByte();
311  byte fileType = inc.ReadByte();
312  string filePath = inc.ReadString();
313 
314  if (GameSettings.CurrentConfig.VerboseLogging)
315  {
316  DebugConsole.Log("Received file transfer message on the same machine: ");
317  DebugConsole.Log(" File: " + filePath);
318  DebugConsole.Log(" ID: " + transferId);
319  }
320 
321  if (!File.Exists(filePath))
322  {
323  DebugConsole.ThrowError("File transfer on the same machine failed, file \"" + filePath + "\" not found.");
324  GameMain.Client.CancelFileTransfer(transferId);
325  return;
326  }
327 
328  FileTransferIn directTransfer = new FileTransferIn(inc.Sender, filePath, (FileTransferType)fileType)
329  {
330  ID = transferId,
331  Status = FileTransferStatus.Finished,
332  FileSize = 0
333  };
334 
335  OnFinished(directTransfer);
336  }
337  break;
338  case (byte)FileTransferMessageType.Data:
339  {
340  byte transferId = inc.ReadByte();
341 
342  var activeTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.Endpoint) && t.ID == transferId);
343  if (activeTransfer == null)
344  {
345  //it's possible for the server to send some extra data
346  //before it acknowledges that the download is finished,
347  //so let's suppress the error message in that case
348  finishedTransfers.RemoveAll(t => t.finishedTime + 5.0 < Timing.TotalTime);
349  if (!finishedTransfers.Any(t => t.transferId == transferId))
350  {
351  GameMain.Client.CancelFileTransfer(transferId);
352  DebugConsole.AddWarning("File transfer error: received data without a transfer initiation message");
353  }
354  return;
355  }
356 
357  int offset = inc.ReadInt32();
358  int bytesToRead = inc.ReadUInt16();
359  if (offset != activeTransfer.Received)
360  {
361  activeTransfer.LastSeen = Math.Max(offset, activeTransfer.LastSeen);
362  if (!activeTransfer.DataBuffer.ContainsKey(offset) && activeTransfer.DataBuffer.Count < FileTransferIn.DataBufferSize)
363  {
364  activeTransfer.DataBuffer.Add(offset, inc.ReadBytes(bytesToRead));
365  }
366  DebugConsole.Log($"Received {bytesToRead} bytes of the file {activeTransfer.FileName} (ignoring: offset {offset}, waiting for {activeTransfer.Received})");
367  GameMain.Client.UpdateFileTransfer(activeTransfer, activeTransfer.Received, activeTransfer.LastSeen);
368  return;
369  }
370  activeTransfer.LastSeen = offset;
371 
372  if (activeTransfer.Received + bytesToRead > activeTransfer.FileSize)
373  {
374  GameMain.Client.CancelFileTransfer(transferId);
375  DebugConsole.ThrowError("File transfer error: Received more data than expected (total received: " + activeTransfer.Received +
376  ", msg received: " + (inc.LengthBytes - inc.BytePosition) +
377  ", msg length: " + inc.LengthBytes +
378  ", msg read: " + inc.BytePosition +
379  ", filesize: " + activeTransfer.FileSize);
380  activeTransfer.Status = FileTransferStatus.Error;
381  StopTransfer(activeTransfer);
382  return;
383  }
384 
385  try
386  {
387  activeTransfer.ReadBytes(inc, bytesToRead);
388  if (GameSettings.CurrentConfig.VerboseLogging)
389  {
390  DebugConsole.Log($"Received {bytesToRead} bytes of the file {activeTransfer.FileName} ({activeTransfer.Received / 1000}/{activeTransfer.FileSize / 1000} kB received)");
391  }
392  while (activeTransfer.DataBuffer.TryGetValue(activeTransfer.Received, out byte[] data))
393  {
394  activeTransfer.ReadBytes(data);
395  DebugConsole.Log($"Read {data.Length} bytes of buffer data of the file {activeTransfer.FileName} ({activeTransfer.Received / 1000}/{activeTransfer.FileSize / 1000} kB received)");
396  }
397  }
398  catch (Exception e)
399  {
400  GameMain.Client.CancelFileTransfer(transferId);
401  DebugConsole.ThrowError("File transfer error: " + e.Message);
402  activeTransfer.Status = FileTransferStatus.Error;
403  StopTransfer(activeTransfer, true);
404  return;
405  }
406 
407  GameMain.Client.UpdateFileTransfer(activeTransfer, activeTransfer.Received, activeTransfer.LastSeen, reliable: activeTransfer.Status == FileTransferStatus.Finished);
408  if (activeTransfer.Status == FileTransferStatus.Finished)
409  {
410  activeTransfer.Dispose();
411 
412  if (ValidateReceivedData(activeTransfer, out string errorMessage))
413  {
414  finishedTransfers.Add((transferId, Timing.TotalTime));
415  StopTransfer(activeTransfer);
416  OnFinished(activeTransfer);
417  }
418  else
419  {
420  new GUIMessageBox("File transfer aborted", errorMessage);
421 
422  activeTransfer.Status = FileTransferStatus.Error;
423  StopTransfer(activeTransfer, true);
424  }
425  }
426  }
427  break;
428  case (byte)FileTransferMessageType.Cancel:
429  {
430  byte transferId = inc.ReadByte();
431  var matchingTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.Endpoint) && t.ID == transferId);
432  if (matchingTransfer != null)
433  {
434  new GUIMessageBox("File transfer cancelled", "The server has cancelled the transfer of the file \"" + matchingTransfer.FileName + "\".");
435  StopTransfer(matchingTransfer);
436  }
437  break;
438  }
439  }
440  }
441 
442  private bool ValidateInitialData(byte type, string fileName, int fileSize, out string errorMessage)
443  {
444  errorMessage = "";
445 
446  if (!Enum.IsDefined(typeof(FileTransferType), (int)type))
447  {
448  errorMessage = "Unknown file type";
449  return false;
450  }
451 
452  if (fileSize > GetMaxFileSizeInBytes((FileTransferType)type))
453  {
454  errorMessage = $"File too large ({MathUtils.GetBytesReadable(fileSize)} > {MathUtils.GetBytesReadable(GetMaxFileSizeInBytes((FileTransferType)type))})";
455  return false;
456  }
457 
458  if (string.IsNullOrEmpty(fileName) ||
459  fileName.IndexOfAny(Path.GetInvalidFileNameCharsCrossPlatform().ToArray()) > -1)
460  {
461  errorMessage = "Illegal characters in file name ''" + fileName + "''";
462  return false;
463  }
464 
465  switch (type)
466  {
467  case (byte)FileTransferType.Submarine:
468  if (Path.GetExtension(fileName) != ".sub")
469  {
470  errorMessage = "Wrong file extension ''" + Path.GetExtension(fileName) + "''! (Expected .sub)";
471  return false;
472  }
473  break;
474  case (byte)FileTransferType.CampaignSave:
475  if (Path.GetExtension(fileName) != ".save")
476  {
477  errorMessage = "Wrong file extension ''" + Path.GetExtension(fileName) + "''! (Expected .save)";
478  return false;
479  }
480  break;
481  }
482 
483  return true;
484  }
485 
486  private bool ValidateReceivedData(FileTransferIn fileTransfer, out string ErrorMessage)
487  {
488  ErrorMessage = "";
489  switch (fileTransfer.FileType)
490  {
491  case FileTransferType.Submarine:
492  System.IO.Stream stream;
493  try
494  {
495  stream = SaveUtil.DecompressFileToStream(fileTransfer.FilePath);
496  }
497  catch (Exception e)
498  {
499  ErrorMessage = "Loading received submarine \"" + fileTransfer.FileName + "\" failed! {" + e.Message + "}";
500  return false;
501  }
502 
503  if (stream == null)
504  {
505  ErrorMessage = "Decompressing received submarine file \"" + fileTransfer.FilePath + "\" failed!";
506  return false;
507  }
508 
509  try
510  {
511  stream.Position = 0;
512 
513  XmlReaderSettings settings = new XmlReaderSettings
514  {
515  DtdProcessing = DtdProcessing.Prohibit,
516  IgnoreProcessingInstructions = true
517  };
518 
519  using (var reader = XmlReader.Create(stream, settings))
520  {
521  while (reader.Read());
522  }
523  }
524  catch
525  {
526  stream?.Close();
527  ErrorMessage = "Parsing file \"" + fileTransfer.FilePath + "\" failed! The file may not be a valid submarine file.";
528  return false;
529  }
530 
531  stream?.Close();
532  break;
533  case FileTransferType.CampaignSave:
534  try
535  {
536  var files = SaveUtil.EnumerateContainedFiles(fileTransfer.FilePath);
537  foreach (var file in files)
538  {
539  string extension = Path.GetExtension(file);
540  if ((!extension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
541  && !file.Equals(SaveUtil.GameSessionFileName))
542  || file.CleanUpPathCrossPlatform(correctFilenameCase: false).Contains('/'))
543  {
544  ErrorMessage = $"Found unexpected file in \"{fileTransfer.FileName}\"! ({file})";
545  return false;
546  }
547  }
548  }
549  catch (Exception e)
550  {
551  ErrorMessage = $"Loading received campaign save \"{fileTransfer.FileName}\" failed! {{{e.Message}}}";
552  return false;
553  }
554  break;
555  }
556 
557  return true;
558  }
559 
560  public void StopTransfer(FileTransferIn transfer, bool deleteFile = false)
561  {
562  if (transfer.Status != FileTransferStatus.Finished &&
563  transfer.Status != FileTransferStatus.Error)
564  {
565  transfer.Status = FileTransferStatus.Canceled;
566  }
567 
568  if (activeTransfers.Contains(transfer)) { activeTransfers.Remove(transfer); }
569  transfer.Dispose();
570 
571  if (deleteFile && File.Exists(transfer.FilePath))
572  {
573  try
574  {
575  File.Delete(transfer.FilePath, catchUnauthorizedAccessExceptions: false);
576  }
577  catch (Exception e)
578  {
579  DebugConsole.ThrowError("Failed to delete file \"" + transfer.FilePath + "\" (" + e.Message + ")");
580  }
581  }
582  }
583  }
584 }
static GameClient Client
Definition: GameMain.cs:188
override void Write(byte[] buffer, int offset, int count)
Definition: SafeIO.cs:706
override void Flush()
Definition: SafeIO.cs:728
override void Dispose(bool notCalledByFinalizer)
Definition: SafeIO.cs:733
void ReadBytes(IReadMessage inc, int bytesToRead)
readonly Dictionary< int, byte[]> DataBuffer
Data that we've ignored because we're waiting for some earlier data. Key = byte offset,...
FileTransferIn(NetworkConnection connection, string filePath, FileTransferType fileType)
TransferInDelegate OnTransferFailed
delegate void TransferInDelegate(FileTransferIn fileStreamReceiver)
void ReadMessage(IReadMessage inc)
IReadOnlyList< FileTransferIn > ActiveTransfers
void StopTransfer(FileTransferIn transfer, bool deleteFile=false)
void CancelFileTransfer(FileReceiver.FileTransferIn transfer)
Definition: GameClient.cs:2546
void UpdateFileTransfer(FileReceiver.FileTransferIn transfer, int expecting, int lastSeen, bool reliable=false)
Definition: GameClient.cs:2551
byte[] ReadBytes(int numberOfBytes)