diff --git a/Subsurface/Barotrauma.csproj b/Subsurface/Barotrauma.csproj
index e50021540..058e5ecea 100644
--- a/Subsurface/Barotrauma.csproj
+++ b/Subsurface/Barotrauma.csproj
@@ -155,6 +155,7 @@
+
diff --git a/Subsurface/Source/Networking/FileTransfer/FileReceiver.cs b/Subsurface/Source/Networking/FileTransfer/FileReceiver.cs
new file mode 100644
index 000000000..82f1241b2
--- /dev/null
+++ b/Subsurface/Source/Networking/FileTransfer/FileReceiver.cs
@@ -0,0 +1,343 @@
+using Lidgren.Network;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace Barotrauma.Networking
+{
+ class FileReceiver
+ {
+ public class FileTransferIn : IDisposable
+ {
+ public delegate void OnFinishedDelegate(FileTransferIn fileStreamReceiver);
+ public OnFinishedDelegate OnFinished;
+
+ public string FileName
+ {
+ get;
+ private set;
+ }
+
+ public string FilePath
+ {
+ get;
+ private set;
+ }
+
+ public ulong FileSize
+ {
+ get;
+ private set;
+ }
+
+ public ulong Received
+ {
+ get;
+ private set;
+ }
+
+ public FileTransferType FileType
+ {
+ get;
+ private set;
+ }
+
+ public FileTransferStatus Status
+ {
+ get;
+ set;
+ }
+
+ 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 int SequenceChannel;
+
+ public FileTransferIn(string filePath, FileTransferType fileType, OnFinishedDelegate onFinished)
+ {
+ FilePath = filePath;
+ FileName = Path.GetFileName(FilePath);
+ FileType = fileType;
+
+ this.OnFinished = onFinished;
+
+ WriteStream = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None);
+ TimeStarted = Environment.TickCount;
+
+ Status = FileTransferStatus.NotStarted;
+ }
+
+ public void ReadBytes(NetIncomingMessage inc)
+ {
+ byte[] all = inc.ReadBytes(inc.LengthBytes - inc.PositionInBytes);
+ Received += (ulong)all.Length;
+ WriteStream.Write(all, 0, all.Length);
+
+ int passed = Environment.TickCount - TimeStarted;
+ float psec = passed / 1000.0f;
+
+ BytesPerSecond = Received / psec;
+
+ Status = Received >= FileSize ? FileTransferStatus.Finished : FileTransferStatus.Receiving;
+ }
+
+ private bool disposed = false;
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposed) return;
+
+ if (disposing)
+ {
+ if (WriteStream != null)
+ {
+ WriteStream.Flush();
+ WriteStream.Close();
+ WriteStream.Dispose();
+ WriteStream = null;
+ }
+ }
+ disposed = true;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+ }
+
+ const int MaxFileSize = 1000000;
+
+ private List activeTransfers;
+
+ private string downloadFolder;
+
+ public FileReceiver(string downloadFolder)
+ {
+ activeTransfers = new List();
+
+ this.downloadFolder = downloadFolder;
+ }
+
+ private void ReadMessage(NetIncomingMessage 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:
+ var existingTransfer = activeTransfers.Find(t => t.SequenceChannel == inc.SequenceChannel);
+ if (existingTransfer != null)
+ {
+ DebugConsole.ThrowError("File transfer error: file transfer initiated on a sequence channel that's already in use");
+ return;
+ }
+
+ byte fileType = inc.ReadByte();
+ ushort chunkLen = inc.ReadUInt16();
+ ulong fileSize = inc.ReadUInt64();
+ string fileName = inc.ReadString();
+
+ string errorMsg;
+ if (!ValidateInitialData(fileType, fileName, fileSize, out errorMsg))
+ {
+ DebugConsole.ThrowError("File transfer failed ("+errorMsg+")");
+ return;
+ }
+
+ var newTransfer = new FileTransferIn(Path.Combine(downloadFolder, fileName), (FileTransferType)fileType, null);
+ newTransfer.SequenceChannel = inc.SequenceChannel;
+ newTransfer.Status = FileTransferStatus.Receiving;
+
+ activeTransfers.Add(newTransfer);
+
+ break;
+ case (byte)FileTransferMessageType.Data:
+ var activeTransfer = activeTransfers.Find(t => t.SequenceChannel == inc.SequenceChannel);
+ if (activeTransfer == null)
+ {
+ DebugConsole.ThrowError("File transfer error: received data without a transfer initiation message");
+ return;
+ }
+
+ if (activeTransfer.Received + (ulong)inc.LengthBytes > activeTransfer.FileSize * 1.1f)
+ {
+ DebugConsole.ThrowError("File transfer error: Received more data than expected");
+ activeTransfer.Status = FileTransferStatus.Error;
+ return;
+ }
+
+ try
+ {
+ activeTransfer.ReadBytes(inc);
+ }
+ catch (Exception e)
+ {
+ DebugConsole.ThrowError("File transfer error: "+e.Message);
+ activeTransfer.Status = FileTransferStatus.Error;
+ StopTransfer(activeTransfer, true);
+ return;
+ }
+
+ if (activeTransfer.Status == FileTransferStatus.Finished)
+ {
+ string errorMessage = "";
+ if (ValidateReceivedData(activeTransfer, out errorMessage))
+ {
+ activeTransfer.OnFinished(activeTransfer);
+ StopTransfer(activeTransfer);
+ }
+ else
+ {
+ activeTransfer.Status = FileTransferStatus.Error;
+ StopTransfer(activeTransfer, true);
+ }
+ }
+
+ break;
+ }
+ }
+
+ private bool ValidateInitialData(byte type, string fileName, ulong fileSize, out string errorMessage)
+ {
+ errorMessage = "";
+
+ if (fileSize > MaxFileSize)
+ {
+ errorMessage = "File too large (" + MathUtils.GetBytesReadable((long)fileSize) + ")";
+ return false;
+ }
+
+ if (!Enum.IsDefined(typeof(FileTransferType), type))
+ {
+ errorMessage = "Unknown file type";
+ return false;
+ }
+
+ if (!Regex.Match(fileName, @"^[\w\- ]+[\w\-. ]*$").Success)
+ {
+ 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;
+ }
+
+ return true;
+ }
+
+ private bool ValidateReceivedData(FileTransferIn fileTransfer, out string ErrorMessage)
+ {
+ ErrorMessage = "";
+ switch (fileTransfer.FileType)
+ {
+ case FileTransferType.Submarine:
+ Stream stream = null;
+
+ 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();
+ settings.DtdProcessing = DtdProcessing.Prohibit;
+ settings.IgnoreProcessingInstructions = true;
+
+ using (var reader = XmlReader.Create(stream, settings))
+ {
+ while (reader.Read());
+ }
+ }
+ catch
+ {
+ stream.Close();
+ stream.Dispose();
+
+ ErrorMessage = "Parsing file ''" + fileTransfer.FilePath + "'' failed! The file may not be a valid submarine file.";
+ return false;
+ }
+
+ stream.Close();
+ stream.Dispose();
+ break;
+ }
+
+ return true;
+ }
+
+ public void StopTransfer(FileTransferIn transfer, bool deleteFile = false)
+ {
+ if (deleteFile && File.Exists(transfer.FilePath))
+ {
+ try
+ {
+ File.Delete(transfer.FilePath);
+ }
+ catch (Exception e)
+ {
+ DebugConsole.ThrowError("Failed to delete file \""+transfer.FilePath+"\" ("+e.Message+")");
+ }
+ }
+
+ if (transfer.Status != FileTransferStatus.Finished &&
+ transfer.Status != FileTransferStatus.Error)
+ {
+ transfer.Status = FileTransferStatus.Canceled;
+ }
+
+ if (activeTransfers.Contains(transfer)) activeTransfers.Remove(transfer);
+ transfer.Dispose();
+ }
+ }
+}
diff --git a/Subsurface/Source/Networking/FileTransfer/FileSender.cs b/Subsurface/Source/Networking/FileTransfer/FileSender.cs
index 6c4173532..e020575c5 100644
--- a/Subsurface/Source/Networking/FileTransfer/FileSender.cs
+++ b/Subsurface/Source/Networking/FileTransfer/FileSender.cs
@@ -10,7 +10,7 @@ namespace Barotrauma.Networking
{
enum FileTransferStatus
{
- NotStarted, Sending, Receiving, Finished, Canceled
+ NotStarted, Sending, Receiving, Finished, Canceled, Error
}
enum FileTransferMessageType
@@ -27,7 +27,6 @@ namespace Barotrauma.Networking
{
public class FileTransferOut
{
- private byte[] tempBuffer;
private byte[] data;
private DateTime startingTime;
@@ -56,7 +55,7 @@ namespace Barotrauma.Networking
public float Progress
{
- get { return 0.0f; }//inputStream == null ? 0.0f : (float)sentOffset / (float)inputStream.Length; }
+ get { return SentOffset / (float)Data.Length; }
}
public float WaitTimer
@@ -81,6 +80,8 @@ namespace Barotrauma.Networking
get { return connection; }
}
+ public int SequenceChannel;
+
public FileTransferOut(NetConnection recipient, FileTransferType fileType, string filePath)
{
connection = recipient;
@@ -115,6 +116,7 @@ namespace Barotrauma.Networking
public FileTransferOut StartTransfer(NetConnection recipient, FileTransferType fileType, string filePath)
{
+ //TODO: set a limit on the amount of active transfers
if (!File.Exists(filePath))
{
DebugConsole.ThrowError("Failed to initiate file transfer (file \""+filePath+"\" not found.");
@@ -125,6 +127,11 @@ namespace Barotrauma.Networking
try
{
transfer = new FileTransferOut(recipient, fileType, filePath);
+ transfer.SequenceChannel = 1;
+ while (activeTransfers.Any(t => t.Connection == recipient && t.SequenceChannel == transfer.SequenceChannel))
+ {
+ transfer.SequenceChannel++;
+ }
}
catch (Exception e)
{
@@ -136,6 +143,8 @@ namespace Barotrauma.Networking
public void Update(float deltaTime)
{
+ activeTransfers.RemoveAll(t => t.Connection.Status != NetConnectionStatus.Connected);
+
foreach (FileTransferOut transfer in activeTransfers)
{
transfer.WaitTimer -= deltaTime;
@@ -161,9 +170,10 @@ namespace Barotrauma.Networking
message.Write((ushort)chunkLen);
message.Write((ulong)transfer.Data.Length);
message.Write(transfer.FileName);
- transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, 1);
+ transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel);
transfer.Status = FileTransferStatus.Sending;
+ continue;
}
message = peer.CreateMessage(sendByteCount + 8 + 1);
@@ -175,7 +185,7 @@ namespace Barotrauma.Networking
message.Write(sendBytes);
- transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, 1);
+ transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel);
transfer.SentOffset += sendByteCount;
if (remaining - sendByteCount <= 0)
@@ -193,5 +203,33 @@ namespace Barotrauma.Networking
activeTransfers.Remove(transfer);
}
+ public void ReadFileRequest(NetIncomingMessage inc)
+ {
+ byte messageType = inc.ReadByte();
+
+ if (messageType == (byte)FileTransferMessageType.Cancel)
+ {
+ byte sequenceChannel = inc.ReadByte();
+ var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.SenderConnection && t.SequenceChannel == sequenceChannel);
+ if (matchingTransfer != null) CancelTransfer(matchingTransfer);
+
+ return;
+ }
+
+ byte fileType = inc.ReadByte();
+ switch (fileType)
+ {
+ case (byte)FileTransferType.Submarine:
+ string fileName = inc.ReadString();
+ var requestedSubmarine = Submarine.SavedSubmarines.Find(s => s.Name == fileName);
+
+ if (requestedSubmarine != null)
+ {
+ StartTransfer(inc.SenderConnection, FileTransferType.Submarine, requestedSubmarine.FilePath);
+ }
+ break;
+ }
+ }
+
}
}
diff --git a/Subsurface/Source/Networking/GameServer.cs b/Subsurface/Source/Networking/GameServer.cs
index fd5c6840d..0007d8198 100644
--- a/Subsurface/Source/Networking/GameServer.cs
+++ b/Subsurface/Source/Networking/GameServer.cs
@@ -45,6 +45,8 @@ namespace Barotrauma.Networking
private ServerEntityEventManager entityEventManager;
+ private FileSender fileSender;
+
public override List ConnectedClients
{
get
@@ -132,6 +134,8 @@ namespace Barotrauma.Networking
entityEventManager = new ServerEntityEventManager(this);
+ fileSender = new FileSender(this);
+
whitelist = new WhiteList();
banList = new BanList();
@@ -341,7 +345,6 @@ namespace Barotrauma.Networking
if (settingsFrame != null) settingsFrame.Update(deltaTime);
if (log.LogFrame != null) log.LogFrame.Update(deltaTime);
-
if (!started) return;
base.Update(deltaTime);
@@ -355,7 +358,9 @@ namespace Barotrauma.Networking
}
}
- unauthenticatedClients.RemoveAll(uc => uc.AuthTimer <= 0.0f);
+ unauthenticatedClients.RemoveAll(uc => uc.AuthTimer <= 0.0f);
+
+ fileSender.Update(deltaTime);
if (gameStarted)
{
@@ -550,6 +555,12 @@ namespace Barotrauma.Networking
case ClientPacketHeader.SERVER_COMMAND:
ClientReadServerCommand(inc);
break;
+ case ClientPacketHeader.FILE_REQUEST:
+ if (AllowFileTransfers)
+ {
+
+ }
+ break;
}
}
@@ -1902,7 +1913,7 @@ namespace Barotrauma.Networking
Log("Shutting down server...", Color.Cyan);
log.Save();
}
-
+
server.Shutdown("The server has been shut down");
}
}
diff --git a/Subsurface/Source/Networking/NetworkMember.cs b/Subsurface/Source/Networking/NetworkMember.cs
index 6002da628..bfa7b1339 100644
--- a/Subsurface/Source/Networking/NetworkMember.cs
+++ b/Subsurface/Source/Networking/NetworkMember.cs
@@ -15,6 +15,8 @@ namespace Barotrauma.Networking
REQUEST_INIT, //ask the server to give you initialization
UPDATE_LOBBY, //update state in lobby
UPDATE_INGAME, //update state ingame
+
+ FILE_REQUEST, //request a (submarine) file from the server
RESPONSE_STARTGAME, //tell the server whether you're ready to start
SERVER_COMMAND //tell the server to end a round or kick/ban someone (special permissions required)