using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.IO;
using System.Linq;
using System.Threading;
using System.Xml;
namespace Barotrauma.Networking
{
class FileReceiver
{
public class FileTransferIn : IDisposable
{
public string FileName
{
get;
private set;
}
public string FilePath
{
get;
private set;
}
public int FileSize
{
get;
set;
}
public int Received
{
get;
private set;
}
public int LastSeen { get; set; }
public FileTransferType FileType
{
get;
private set;
}
public FileTransferStatus Status
{
get;
set;
}
public DateTime LastOffsetAckTime
{
get;
private set;
}
public void RecordOffsetAckTime()
{
LastOffsetAckTime = DateTime.Now;
}
public float BytesPerSecond
{
get;
private set;
}
public float Progress
{
get { return Received / (float)FileSize; }
}
public FileStream WriteStream
{
get;
private set;
}
public int TimeStarted
{
get;
private set;
}
public NetworkConnection Connection
{
get;
private set;
}
public int ID;
public const int DataBufferSize = 50;
///
/// Data that we've ignored because we're waiting for some earlier data. Key = byte offset, value = the actual data
///
public readonly Dictionary DataBuffer = new Dictionary();
public FileTransferIn(NetworkConnection connection, string filePath, FileTransferType fileType)
{
FilePath = filePath;
FileName = Path.GetFileName(FilePath);
FileType = fileType;
Connection = connection;
Status = FileTransferStatus.NotStarted;
LastOffsetAckTime = DateTime.Now - new TimeSpan(days: 0, hours: 0, minutes: 5, seconds: 0);
}
public void OpenStream()
{
if (WriteStream != null)
{
WriteStream.Flush();
WriteStream.Close();
WriteStream.Dispose();
WriteStream = null;
}
WriteStream = File.Open(FilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write);
TimeStarted = Environment.TickCount;
}
public void ReadBytes(IReadMessage inc, int bytesToRead)
{
if (Received + bytesToRead > FileSize)
{
//strip out excess bytes
bytesToRead -= Received + bytesToRead - FileSize;
}
ReadBytes(inc.ReadBytes(bytesToRead));
}
public void ReadBytes(byte[] data)
{
Received += data.Length;
WriteStream.Write(data, 0, data.Length);
int passed = Environment.TickCount - TimeStarted;
float psec = passed / 1000.0f;
BytesPerSecond = Received / psec;
var outdatedKeys = DataBuffer.Keys.Where(k => k < Received).ToList();
foreach (int key in outdatedKeys)
{
DataBuffer.Remove(key);
}
Status = Received >= FileSize ? FileTransferStatus.Finished : FileTransferStatus.Receiving;
}
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
if (WriteStream != null)
{
WriteStream.Flush();
WriteStream.Close();
WriteStream.Dispose();
WriteStream = null;
}
disposed = true;
}
}
private static int GetMaxFileSizeInBytes(FileTransferType fileTransferType) =>
fileTransferType switch
{
FileTransferType.Mod => 500 * 1024 * 1024, //500 MiB should be good enough, right?
_ => 50 * 1024 * 1024 //50 MiB for everything other than mods
};
public delegate void TransferInDelegate(FileTransferIn fileStreamReceiver);
public TransferInDelegate OnFinished;
public TransferInDelegate OnTransferFailed;
private readonly List activeTransfers;
private readonly List<(int transferId, double finishedTime)> finishedTransfers;
private readonly ImmutableDictionary downloadFolders = new Dictionary()
{
{ FileTransferType.Submarine, SaveUtil.SubmarineDownloadFolder },
{ FileTransferType.CampaignSave, SaveUtil.CampaignDownloadFolder },
{ FileTransferType.Mod, ModReceiver.DownloadFolder }
}.ToImmutableDictionary();
public IReadOnlyList ActiveTransfers => activeTransfers;
public bool HasActiveTransfers => ActiveTransfers.Any();
public FileReceiver()
{
activeTransfers = new List();
finishedTransfers = new List<(int transferId, double finishedTime)>();
}
public void ReadMessage(IReadMessage inc)
{
System.Diagnostics.Debug.Assert(!activeTransfers.Any(t =>
t.Status == FileTransferStatus.Error ||
t.Status == FileTransferStatus.Canceled ||
t.Status == FileTransferStatus.Finished), "List of active file transfers contains entires that should have been removed");
byte transferMessageType = inc.ReadByte();
switch (transferMessageType)
{
case (byte)FileTransferMessageType.Initiate:
{
byte transferId = inc.ReadByte();
var existingTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.Endpoint) && t.ID == transferId);
finishedTransfers.RemoveAll(t => t.transferId == transferId);
byte fileType = inc.ReadByte();
//ushort chunkLen = inc.ReadUInt16();
int fileSize = inc.ReadInt32();
string fileName = inc.ReadString();
if (existingTransfer != null)
{
if (fileType != (byte)existingTransfer.FileType ||
fileSize != existingTransfer.FileSize ||
fileName != existingTransfer.FileName)
{
GameMain.Client.CancelFileTransfer(transferId);
DebugConsole.AddWarning("File transfer error: file transfer initiated with an ID that's already in use");
}
else //resend acknowledgement packet
{
GameMain.Client.UpdateFileTransfer(existingTransfer, existingTransfer.Received, existingTransfer.LastSeen);
}
return;
}
if (!ValidateInitialData(fileType, fileName, fileSize, out string errorMsg))
{
GameMain.Client.CancelFileTransfer(transferId);
DebugConsole.ThrowError("File transfer failed (" + errorMsg + ")");
return;
}
if (GameSettings.CurrentConfig.VerboseLogging)
{
DebugConsole.Log("Received file transfer initiation message: ");
DebugConsole.Log(" File: " + fileName);
DebugConsole.Log(" Size: " + fileSize);
DebugConsole.Log(" ID: " + transferId);
}
string downloadFolder = downloadFolders[(FileTransferType)fileType];
if (!Directory.Exists(downloadFolder))
{
try
{
Directory.CreateDirectory(downloadFolder);
}
catch (Exception e)
{
DebugConsole.ThrowError("Could not start a file transfer: failed to create the folder \"" + downloadFolder + "\".", e);
return;
}
}
FileTransferIn newTransfer = new FileTransferIn(inc.Sender, Path.Combine(downloadFolder, fileName), (FileTransferType)fileType)
{
ID = transferId,
Status = FileTransferStatus.Receiving,
FileSize = fileSize
};
int maxRetries = 4;
for (int i = 0; i <= maxRetries; i++)
{
try
{
newTransfer.OpenStream();
}
catch (System.IO.IOException e)
{
if (i < maxRetries)
{
DebugConsole.NewMessage("Failed to initiate a file transfer {" + e.Message + "}, retrying in 250 ms...", Color.Red);
Thread.Sleep(250);
}
else
{
DebugConsole.NewMessage("Failed to initiate a file transfer {" + e.Message + "}", Color.Red);
GameMain.Client.CancelFileTransfer(transferId);
newTransfer.Status = FileTransferStatus.Error;
OnTransferFailed(newTransfer);
return;
}
}
}
activeTransfers.Add(newTransfer);
GameMain.Client.UpdateFileTransfer(newTransfer, 0, 0); //send acknowledgement packet
}
break;
case (byte)FileTransferMessageType.TransferOnSameMachine:
{
byte transferId = inc.ReadByte();
byte fileType = inc.ReadByte();
string filePath = inc.ReadString();
if (GameSettings.CurrentConfig.VerboseLogging)
{
DebugConsole.Log("Received file transfer message on the same machine: ");
DebugConsole.Log(" File: " + filePath);
DebugConsole.Log(" ID: " + transferId);
}
if (!File.Exists(filePath))
{
DebugConsole.ThrowError("File transfer on the same machine failed, file \"" + filePath + "\" not found.");
GameMain.Client.CancelFileTransfer(transferId);
return;
}
FileTransferIn directTransfer = new FileTransferIn(inc.Sender, filePath, (FileTransferType)fileType)
{
ID = transferId,
Status = FileTransferStatus.Finished,
FileSize = 0
};
OnFinished(directTransfer);
}
break;
case (byte)FileTransferMessageType.Data:
{
byte transferId = inc.ReadByte();
var activeTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.Endpoint) && t.ID == transferId);
if (activeTransfer == null)
{
//it's possible for the server to send some extra data
//before it acknowledges that the download is finished,
//so let's suppress the error message in that case
finishedTransfers.RemoveAll(t => t.finishedTime + 5.0 < Timing.TotalTime);
if (!finishedTransfers.Any(t => t.transferId == transferId))
{
GameMain.Client.CancelFileTransfer(transferId);
DebugConsole.AddWarning("File transfer error: received data without a transfer initiation message");
}
return;
}
int offset = inc.ReadInt32();
int bytesToRead = inc.ReadUInt16();
if (offset != activeTransfer.Received)
{
activeTransfer.LastSeen = Math.Max(offset, activeTransfer.LastSeen);
if (!activeTransfer.DataBuffer.ContainsKey(offset) && activeTransfer.DataBuffer.Count < FileTransferIn.DataBufferSize)
{
activeTransfer.DataBuffer.Add(offset, inc.ReadBytes(bytesToRead));
}
DebugConsole.Log($"Received {bytesToRead} bytes of the file {activeTransfer.FileName} (ignoring: offset {offset}, waiting for {activeTransfer.Received})");
GameMain.Client.UpdateFileTransfer(activeTransfer, activeTransfer.Received, activeTransfer.LastSeen);
return;
}
activeTransfer.LastSeen = offset;
if (activeTransfer.Received + bytesToRead > activeTransfer.FileSize)
{
GameMain.Client.CancelFileTransfer(transferId);
DebugConsole.ThrowError("File transfer error: Received more data than expected (total received: " + activeTransfer.Received +
", msg received: " + (inc.LengthBytes - inc.BytePosition) +
", msg length: " + inc.LengthBytes +
", msg read: " + inc.BytePosition +
", filesize: " + activeTransfer.FileSize);
activeTransfer.Status = FileTransferStatus.Error;
StopTransfer(activeTransfer);
return;
}
try
{
activeTransfer.ReadBytes(inc, bytesToRead);
if (GameSettings.CurrentConfig.VerboseLogging)
{
DebugConsole.Log($"Received {bytesToRead} bytes of the file {activeTransfer.FileName} ({activeTransfer.Received / 1000}/{activeTransfer.FileSize / 1000} kB received)");
}
while (activeTransfer.DataBuffer.TryGetValue(activeTransfer.Received, out byte[] data))
{
activeTransfer.ReadBytes(data);
DebugConsole.Log($"Read {data.Length} bytes of buffer data of the file {activeTransfer.FileName} ({activeTransfer.Received / 1000}/{activeTransfer.FileSize / 1000} kB received)");
}
}
catch (Exception e)
{
GameMain.Client.CancelFileTransfer(transferId);
DebugConsole.ThrowError("File transfer error: " + e.Message);
activeTransfer.Status = FileTransferStatus.Error;
StopTransfer(activeTransfer, true);
return;
}
GameMain.Client.UpdateFileTransfer(activeTransfer, activeTransfer.Received, activeTransfer.LastSeen, reliable: activeTransfer.Status == FileTransferStatus.Finished);
if (activeTransfer.Status == FileTransferStatus.Finished)
{
activeTransfer.Dispose();
if (ValidateReceivedData(activeTransfer, out string errorMessage))
{
finishedTransfers.Add((transferId, Timing.TotalTime));
StopTransfer(activeTransfer);
OnFinished(activeTransfer);
}
else
{
new GUIMessageBox("File transfer aborted", errorMessage);
activeTransfer.Status = FileTransferStatus.Error;
StopTransfer(activeTransfer, true);
}
}
}
break;
case (byte)FileTransferMessageType.Cancel:
{
byte transferId = inc.ReadByte();
var matchingTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.Endpoint) && t.ID == transferId);
if (matchingTransfer != null)
{
new GUIMessageBox("File transfer cancelled", "The server has cancelled the transfer of the file \"" + matchingTransfer.FileName + "\".");
StopTransfer(matchingTransfer);
}
break;
}
}
}
private bool ValidateInitialData(byte type, string fileName, int fileSize, out string errorMessage)
{
errorMessage = "";
if (!Enum.IsDefined(typeof(FileTransferType), (int)type))
{
errorMessage = "Unknown file type";
return false;
}
if (fileSize > GetMaxFileSizeInBytes((FileTransferType)type))
{
errorMessage = $"File too large ({MathUtils.GetBytesReadable(fileSize)} > {MathUtils.GetBytesReadable(GetMaxFileSizeInBytes((FileTransferType)type))})";
return false;
}
if (string.IsNullOrEmpty(fileName) ||
fileName.IndexOfAny(Path.GetInvalidFileNameCharsCrossPlatform().ToArray()) > -1)
{
errorMessage = "Illegal characters in file name ''" + fileName + "''";
return false;
}
switch (type)
{
case (byte)FileTransferType.Submarine:
if (Path.GetExtension(fileName) != ".sub")
{
errorMessage = "Wrong file extension ''" + Path.GetExtension(fileName) + "''! (Expected .sub)";
return false;
}
break;
case (byte)FileTransferType.CampaignSave:
if (Path.GetExtension(fileName) != ".save")
{
errorMessage = "Wrong file extension ''" + Path.GetExtension(fileName) + "''! (Expected .save)";
return false;
}
break;
}
return true;
}
private bool ValidateReceivedData(FileTransferIn fileTransfer, out string ErrorMessage)
{
ErrorMessage = "";
switch (fileTransfer.FileType)
{
case FileTransferType.Submarine:
System.IO.Stream stream;
try
{
stream = SaveUtil.DecompressFileToStream(fileTransfer.FilePath);
}
catch (Exception e)
{
ErrorMessage = "Loading received submarine \"" + fileTransfer.FileName + "\" failed! {" + e.Message + "}";
return false;
}
if (stream == null)
{
ErrorMessage = "Decompressing received submarine file \"" + fileTransfer.FilePath + "\" failed!";
return false;
}
try
{
stream.Position = 0;
XmlReaderSettings settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
IgnoreProcessingInstructions = true
};
using (var reader = XmlReader.Create(stream, settings))
{
while (reader.Read());
}
}
catch
{
stream?.Close();
ErrorMessage = "Parsing file \"" + fileTransfer.FilePath + "\" failed! The file may not be a valid submarine file.";
return false;
}
stream?.Close();
break;
case FileTransferType.CampaignSave:
try
{
var files = SaveUtil.EnumerateContainedFiles(fileTransfer.FilePath);
foreach (var file in files)
{
string extension = Path.GetExtension(file);
if ((!extension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
&& !file.Equals("gamesession.xml"))
|| file.CleanUpPathCrossPlatform(correctFilenameCase: false).Contains('/'))
{
ErrorMessage = $"Found unexpected file in \"{fileTransfer.FileName}\"! ({file})";
return false;
}
}
}
catch (Exception e)
{
ErrorMessage = $"Loading received campaign save \"{fileTransfer.FileName}\" failed! {{{e.Message}}}";
return false;
}
break;
}
return true;
}
public void StopTransfer(FileTransferIn transfer, bool deleteFile = false)
{
if (transfer.Status != FileTransferStatus.Finished &&
transfer.Status != FileTransferStatus.Error)
{
transfer.Status = FileTransferStatus.Canceled;
}
if (activeTransfers.Contains(transfer)) { activeTransfers.Remove(transfer); }
transfer.Dispose();
if (deleteFile && File.Exists(transfer.FilePath))
{
try
{
File.Delete(transfer.FilePath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to delete file \"" + transfer.FilePath + "\" (" + e.Message + ")");
}
}
}
}
}