mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
High-bandwidth transfer system (#6373)
* WebSocket-based data transfer system * Move resource downloads/uploads to the new transfer system Should drastically increase the permitted practical size * Transfer impl for Lidgren * Async impl for receive stream * Use unbounded channel for Lidgren * Add metrics * More comments * Add serverside stream limit to avoid being a DoS vector * Fix tests * Oops forgot to actually implement sequence channels in NetMessage * Doc comment for NetMessage.SequenceChannel * Release notes
This commit is contained in:
committed by
GitHub
parent
48654ac424
commit
dc1464b462
@@ -9,6 +9,7 @@ using Robust.Server.DataMetrics;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Server.Log;
|
||||
using Robust.Server.Network.Transfer;
|
||||
using Robust.Server.Placement;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Server.Scripting;
|
||||
@@ -29,6 +30,7 @@ using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Transfer;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Profiling;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -107,6 +109,8 @@ namespace Robust.Server
|
||||
[Dependency] private readonly UploadedContentManager _uploadedContMan = default!;
|
||||
[Dependency] private readonly NetworkResourceManager _netResMan = default!;
|
||||
[Dependency] private readonly IReflectionManager _refMan = default!;
|
||||
[Dependency] private readonly ITransferManager _transfer = default!;
|
||||
[Dependency] private readonly ServerTransferTestManager _transferTest = default!;
|
||||
|
||||
private readonly Stopwatch _uptimeStopwatch = new();
|
||||
|
||||
@@ -293,6 +297,9 @@ namespace Robust.Server
|
||||
return true;
|
||||
}
|
||||
|
||||
_transfer.Initialize();
|
||||
_transferTest.Initialize();
|
||||
|
||||
var dataDir = Options.LoadConfigAndUserData
|
||||
? _commandLineArgs?.DataDir ?? PathHelpers.ExecutableRelativeFile("data")
|
||||
: null;
|
||||
@@ -773,6 +780,8 @@ namespace Robust.Server
|
||||
|
||||
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
|
||||
|
||||
_transfer.FrameUpdate();
|
||||
|
||||
_metricsManager.FrameUpdate();
|
||||
}
|
||||
|
||||
|
||||
122
Robust.Server/Network/Transfer/ServerTransferImplWebSocket.cs
Normal file
122
Robust.Server/Network/Transfer/ServerTransferImplWebSocket.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Server.ServerStatus;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages.Transfer;
|
||||
using Robust.Shared.Network.Transfer;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.Network.Transfer;
|
||||
|
||||
internal sealed class ServerTransferImplWebSocket : TransferImplWebSocket
|
||||
{
|
||||
private readonly IConfigurationManager _cfg;
|
||||
private readonly INetManager _netManager;
|
||||
private readonly SemaphoreSlim _apiLock = new(1, 1);
|
||||
private readonly TaskCompletionSource _connectTcs = new();
|
||||
|
||||
// To authenticate the client doing the HTTP request,
|
||||
// we ask that they provide a key we gave them via Lidgren traffic.
|
||||
public byte[]? Key;
|
||||
|
||||
public ServerTransferImplWebSocket(
|
||||
ISawmill sawmill,
|
||||
BaseTransferManager parent,
|
||||
IConfigurationManager cfg,
|
||||
INetManager netManager,
|
||||
INetChannel channel)
|
||||
: base(sawmill, parent, channel)
|
||||
{
|
||||
_cfg = cfg;
|
||||
_netManager = netManager;
|
||||
}
|
||||
|
||||
public override Task ServerInit()
|
||||
{
|
||||
Key = RandomNumberGenerator.GetBytes(RandomKeyBytes);
|
||||
|
||||
var uriBuilder = new UriBuilder(string.Concat(
|
||||
_cfg.GetCVar(CVars.TransferHttpEndpoint).TrimEnd("/"),
|
||||
ServerTransferManager.TransferApiUrl));
|
||||
|
||||
uriBuilder.Scheme = uriBuilder.Scheme switch
|
||||
{
|
||||
"http" => "ws",
|
||||
"https" => "wss",
|
||||
_ => throw new InvalidOperationException($"Invalid API endpoint scheme: {uriBuilder.Scheme}")
|
||||
};
|
||||
|
||||
var url = uriBuilder.ToString();
|
||||
|
||||
Sawmill.Verbose($"Transfer API URL is '{url}'");
|
||||
|
||||
var initMsg = new MsgTransferInit();
|
||||
initMsg.HttpInfo = (url, Key);
|
||||
|
||||
_netManager.ServerSendMessage(initMsg, Channel);
|
||||
|
||||
return _connectTcs.Task;
|
||||
}
|
||||
|
||||
public override Task ClientInit(CancellationToken cancel)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public async Task HandleApiRequest(NetUserId userId, IStatusHandlerContext context)
|
||||
{
|
||||
using var _ = await _apiLock.WaitGuardAsync();
|
||||
|
||||
if (Key == null)
|
||||
{
|
||||
Sawmill.Warning($"HTTP request failed: UserID '{userId}' tried to connect twice");
|
||||
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.RequestHeaders.TryGetValue(KeyHeaderName, out var keyValue) || keyValue is not [{ } keyValueStr])
|
||||
{
|
||||
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var buf = new byte[RandomKeyBytes];
|
||||
|
||||
if (!Convert.TryFromBase64String(keyValueStr, buf, out var written) || written != RandomKeyBytes)
|
||||
{
|
||||
Sawmill.Verbose("HTTP request failed: key is not valid base64 or wrong length");
|
||||
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CryptographicOperations.FixedTimeEquals(buf, Key))
|
||||
{
|
||||
Sawmill.Warning("HTTP request failed: key is wrong");
|
||||
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||
return;
|
||||
}
|
||||
|
||||
Sawmill.Debug("Client connect to transfer WS channel: {UserId}", userId);
|
||||
|
||||
WebSocket = await context.AcceptWebSocketAsync();
|
||||
|
||||
// We've connected.
|
||||
// Clear key so this can't be reconnected to.
|
||||
Key = null;
|
||||
|
||||
_connectTcs.TrySetResult();
|
||||
|
||||
ReadThread();
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_connectTcs.TrySetCanceled();
|
||||
}
|
||||
}
|
||||
171
Robust.Server/Network/Transfer/ServerTransferManager.cs
Normal file
171
Robust.Server/Network/Transfer/ServerTransferManager.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Server.ServerStatus;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages.Transfer;
|
||||
using Robust.Shared.Network.Transfer;
|
||||
|
||||
namespace Robust.Server.Network.Transfer;
|
||||
|
||||
internal sealed class ServerTransferManager : BaseTransferManager, ITransferManager
|
||||
{
|
||||
internal const string TransferApiUrl = "/rt_transfer_init";
|
||||
|
||||
private readonly IConfigurationManager _cfg;
|
||||
private readonly IStatusHost _statusHost;
|
||||
private readonly IServerNetManager _netManager;
|
||||
|
||||
private readonly Dictionary<NetUserId, Player> _onlinePlayers = new();
|
||||
|
||||
internal ServerTransferManager(IConfigurationManager cfg, IStatusHost statusHost, IServerNetManager netManager, ILogManager logManager, ITaskManager taskManager)
|
||||
: base(logManager, NetMessageAccept.Server, taskManager)
|
||||
{
|
||||
_cfg = cfg;
|
||||
_statusHost = statusHost;
|
||||
_netManager = netManager;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_netManager.RegisterNetMessage<MsgTransferInit>();
|
||||
_netManager.RegisterNetMessage<MsgTransferData>(RxTransferData, NetMessageAccept.Server | NetMessageAccept.Handshake);
|
||||
_netManager.RegisterNetMessage<MsgTransferAckInit>(RxTransferAckInit, NetMessageAccept.Server | NetMessageAccept.Handshake);
|
||||
|
||||
_statusHost.AddHandler(HandleRequest);
|
||||
|
||||
_netManager.Disconnect += NetManagerOnDisconnect;
|
||||
}
|
||||
|
||||
private void RxTransferData(MsgTransferData message)
|
||||
{
|
||||
if (!_onlinePlayers.TryGetValue(message.MsgChannel.UserId, out var player)
|
||||
|| player.Impl is not TransferImplLidgren lidgren)
|
||||
{
|
||||
message.MsgChannel.Disconnect("Not lidgren");
|
||||
return;
|
||||
}
|
||||
|
||||
lidgren.ReceiveData(message);
|
||||
}
|
||||
|
||||
private void RxTransferAckInit(MsgTransferAckInit message)
|
||||
{
|
||||
if (!_onlinePlayers.TryGetValue(message.MsgChannel.UserId, out var player)
|
||||
|| player.Impl is not TransferImplLidgren lidgren)
|
||||
{
|
||||
message.MsgChannel.Disconnect("Not lidgren");
|
||||
return;
|
||||
}
|
||||
|
||||
lidgren.ReceiveInitAck();
|
||||
}
|
||||
|
||||
public Stream StartTransfer(INetChannel channel, TransferStartInfo startInfo)
|
||||
{
|
||||
if (!_onlinePlayers.TryGetValue(channel.UserId, out var player))
|
||||
throw new InvalidOperationException("Player is not connected yet!");
|
||||
|
||||
return player.Impl.StartTransfer(startInfo);
|
||||
}
|
||||
|
||||
private async Task<bool> HandleRequest(IStatusHandlerContext context)
|
||||
{
|
||||
if (context.Url.AbsolutePath != TransferApiUrl)
|
||||
return false;
|
||||
|
||||
if (!context.IsWebSocketRequest)
|
||||
{
|
||||
Sawmill.Verbose("HTTP request failed: not a websocket request");
|
||||
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!context.RequestHeaders.TryGetValue(TransferImplWebSocket.UserIdHeaderName, out var userIdValue)
|
||||
|| userIdValue.Count != 1)
|
||||
{
|
||||
Sawmill.Verbose("HTTP request failed: missing RT-UserId");
|
||||
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(userIdValue[0], out var userId))
|
||||
{
|
||||
Sawmill.Verbose($"HTTP request failed: UserID '{userId}' invalid");
|
||||
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_onlinePlayers.TryGetValue(new NetUserId(userId), out var player))
|
||||
{
|
||||
Sawmill.Warning($"HTTP request failed: UserID '{userId}' not online");
|
||||
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (player.Impl is not ServerTransferImplWebSocket serverWs)
|
||||
{
|
||||
Sawmill.Warning($"HTTP request failed: UserID '{userId}' is not using websocket transfer");
|
||||
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||
return true;
|
||||
}
|
||||
|
||||
await serverWs.HandleApiRequest(new NetUserId(userId), context);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task ServerHandshake(INetChannel channel)
|
||||
{
|
||||
if (_onlinePlayers.ContainsKey(channel.UserId))
|
||||
throw new InvalidOperationException("We already have data for this user??");
|
||||
|
||||
var transferHttpEnabled = _cfg.GetCVar(CVars.TransferHttp);
|
||||
|
||||
BaseTransferImpl impl;
|
||||
if (transferHttpEnabled)
|
||||
{
|
||||
impl = new ServerTransferImplWebSocket(Sawmill, this, _cfg, _netManager, channel);
|
||||
}
|
||||
else
|
||||
{
|
||||
impl = new TransferImplLidgren(Sawmill, channel, this, _netManager);
|
||||
}
|
||||
|
||||
impl.MaxChannelCount = _cfg.GetCVar(CVars.TransferStreamLimit);
|
||||
|
||||
var datum = new Player
|
||||
{
|
||||
Impl = impl,
|
||||
};
|
||||
|
||||
_onlinePlayers.Add(channel.UserId, datum);
|
||||
|
||||
await impl.ServerInit();
|
||||
}
|
||||
|
||||
public event Action ClientHandshakeComplete
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
private void NetManagerOnDisconnect(object? sender, NetDisconnectedArgs e)
|
||||
{
|
||||
if (!_onlinePlayers.Remove(e.Channel.UserId, out var player))
|
||||
return;
|
||||
|
||||
Sawmill.Debug("Cleaning up connection for channel {Player} that disconnected", e.Channel);
|
||||
player.Impl.Dispose();
|
||||
}
|
||||
|
||||
private sealed class Player
|
||||
{
|
||||
public required BaseTransferImpl Impl;
|
||||
}
|
||||
}
|
||||
23
Robust.Server/Network/Transfer/ServerTransferTestManager.cs
Normal file
23
Robust.Server/Network/Transfer/ServerTransferTestManager.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Robust.Server.Console;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Transfer;
|
||||
|
||||
namespace Robust.Server.Network.Transfer;
|
||||
|
||||
internal sealed class ServerTransferTestManager(
|
||||
ITransferManager manager,
|
||||
ILogManager logManager,
|
||||
IConGroupController controller,
|
||||
IPlayerManager playerManager)
|
||||
: TransferTestManager(manager, logManager)
|
||||
{
|
||||
protected override bool PermissionCheck(INetChannel channel)
|
||||
{
|
||||
if (!playerManager.TryGetSessionByChannel(channel, out var session))
|
||||
return false;
|
||||
|
||||
return controller.CanCommand(session, TransferTestCommand.CommandKey);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Server.Player;
|
||||
@@ -10,4 +11,6 @@ namespace Robust.Server.Player;
|
||||
public interface IPlayerManager : ISharedPlayerManager
|
||||
{
|
||||
BoundKeyMap KeyMap { get; }
|
||||
|
||||
internal void MarkPlayerResourcesSent(INetChannel channel);
|
||||
}
|
||||
|
||||
@@ -120,13 +120,34 @@ namespace Robust.Server.Player
|
||||
private void HandlePlayerListReq(MsgPlayerListReq message)
|
||||
{
|
||||
var channel = message.MsgChannel;
|
||||
var session = (CommonSession) GetSessionByChannel(channel);
|
||||
session.InitialPlayerListReqDone = true;
|
||||
|
||||
if (!session.InitialResourcesDone)
|
||||
return;
|
||||
|
||||
SendPlayerList(channel, session);
|
||||
}
|
||||
|
||||
public void MarkPlayerResourcesSent(INetChannel channel)
|
||||
{
|
||||
var session = (CommonSession) GetSessionByChannel(channel);
|
||||
session.InitialResourcesDone = true;
|
||||
|
||||
if (!session.InitialPlayerListReqDone)
|
||||
return;
|
||||
|
||||
SendPlayerList(channel, session);
|
||||
}
|
||||
|
||||
private void SendPlayerList(INetChannel channel, CommonSession session)
|
||||
{
|
||||
var players = Sessions;
|
||||
var netMsg = new MsgPlayerList();
|
||||
|
||||
// client session is complete, set their status accordingly.
|
||||
// This is done before the packet is built, so that the client
|
||||
// can see themselves Connected.
|
||||
var session = GetSessionByChannel(channel);
|
||||
session.ConnectedTime = DateTime.UtcNow;
|
||||
SetStatus(session, SessionStatus.Connected);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Robust.Server.DataMetrics;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Server.Localization;
|
||||
using Robust.Server.Network.Transfer;
|
||||
using Robust.Server.Placement;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Server.Prototypes;
|
||||
@@ -25,6 +26,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Transfer;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Reflection;
|
||||
@@ -102,6 +104,8 @@ namespace Robust.Server
|
||||
deps.Register<IHWId, DummyHWId>();
|
||||
deps.Register<ILocalizationManager, ServerLocalizationManager>();
|
||||
deps.Register<ILocalizationManagerInternal, ServerLocalizationManager>();
|
||||
deps.Register<ITransferManager, ServerTransferManager>();
|
||||
deps.Register<ServerTransferTestManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
@@ -25,6 +26,8 @@ namespace Robust.Server.ServerStatus
|
||||
IDictionary<string, string> ResponseHeaders { get; }
|
||||
bool KeepAlive { get; set; }
|
||||
|
||||
bool IsWebSocketRequest { get; }
|
||||
|
||||
Task<T?> RequestBodyJsonAsync<T>();
|
||||
|
||||
Task RespondNoContentAsync();
|
||||
@@ -54,5 +57,7 @@ namespace Robust.Server.ServerStatus
|
||||
Task RespondJsonAsync(object jsonData, HttpStatusCode code = HttpStatusCode.OK);
|
||||
|
||||
Task<Stream> RespondStreamAsync(HttpStatusCode code = HttpStatusCode.OK);
|
||||
|
||||
Task<WebSocket> AcceptWebSocketAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -242,6 +243,7 @@ namespace Robust.Server.ServerStatus
|
||||
public Uri Url => _context.Request.Url!;
|
||||
public bool IsGetLike => RequestMethod == HttpMethod.Head || RequestMethod == HttpMethod.Get;
|
||||
public IReadOnlyDictionary<string, StringValues> RequestHeaders { get; }
|
||||
public bool IsWebSocketRequest => _context.Request.IsWebSocketRequest;
|
||||
|
||||
public bool KeepAlive
|
||||
{
|
||||
@@ -353,6 +355,12 @@ namespace Robust.Server.ServerStatus
|
||||
return Task.FromResult(_context.Response.OutputStream);
|
||||
}
|
||||
|
||||
public async Task<WebSocket> AcceptWebSocketAsync()
|
||||
{
|
||||
var context = await _context.AcceptWebSocketAsync(null);
|
||||
return context.WebSocket;
|
||||
}
|
||||
|
||||
private void RespondShared()
|
||||
{
|
||||
foreach (var (header, value) in _responseHeaders)
|
||||
|
||||
@@ -1,77 +1,153 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Robust.Server.Console;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Transfer;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Upload;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Server.Upload;
|
||||
|
||||
public sealed class NetworkResourcesUploadedEvent
|
||||
{
|
||||
public ICommonSession Session { get; }
|
||||
public ImmutableArray<(ResPath Relative, byte[] Data)> Files { get; }
|
||||
|
||||
internal NetworkResourcesUploadedEvent(ICommonSession session, ImmutableArray<(ResPath, byte[])> files)
|
||||
{
|
||||
Session = session;
|
||||
Files = files;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class NetworkResourceManager : SharedNetworkResourceManager
|
||||
{
|
||||
internal const int AckInitial = 1;
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IServerNetManager _serverNetManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
|
||||
[Dependency] private readonly IConGroupController _controller = default!;
|
||||
|
||||
[Obsolete("Use ResourcesUploaded instead")]
|
||||
public event Action<ICommonSession, NetworkResourceUploadMessage>? OnResourceUploaded;
|
||||
public event Action<NetworkResourcesUploadedEvent>? ResourcesUploaded;
|
||||
|
||||
[ViewVariables] public bool Enabled { get; private set; } = true;
|
||||
[ViewVariables] public float SizeLimit { get; private set; }
|
||||
|
||||
public override void Initialize()
|
||||
internal event Action<INetChannel, int>? AckReceived;
|
||||
|
||||
internal override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
TransferManager.RegisterTransferMessage(TransferKeyNetworkDownload);
|
||||
TransferManager.RegisterTransferMessage(TransferKeyNetworkUpload, ReceiveUpload);
|
||||
|
||||
_cfgManager.OnValueChanged(CVars.ResourceUploadingEnabled, value => Enabled = value, true);
|
||||
_cfgManager.OnValueChanged(CVars.ResourceUploadingLimitMb, value => SizeLimit = value, true);
|
||||
|
||||
_serverNetManager.RegisterNetMessage<NetworkResourceAckMessage>(RxAck);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback for when a client attempts to upload a resource.
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
protected override void ResourceUploadMsg(NetworkResourceUploadMessage msg)
|
||||
private void RxAck(NetworkResourceAckMessage message)
|
||||
{
|
||||
AckReceived?.Invoke(message.MsgChannel, message.Key);
|
||||
}
|
||||
|
||||
private async void ReceiveUpload(TransferReceivedEvent transfer)
|
||||
{
|
||||
// Do not allow uploading any new resources if it has been disabled.
|
||||
// Note: Any resources uploaded before being disabled will still be kept and sent.
|
||||
if (!Enabled)
|
||||
{
|
||||
transfer.Channel.Disconnect("Resource upload not enabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_playerManager.TryGetSessionByChannel(msg.MsgChannel, out var session))
|
||||
if (!_playerManager.TryGetSessionByChannel(transfer.Channel, out var session))
|
||||
{
|
||||
transfer.Channel.Disconnect("Not in-game");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_controller.CanCommand(session, "uploadfile"))
|
||||
{
|
||||
transfer.Channel.Disconnect("Not authorized");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the data is under the current size limit, if it's currently enabled.
|
||||
if (SizeLimit > 0f && msg.Data.Length * BytesToMegabytes > SizeLimit)
|
||||
return;
|
||||
Sawmill.Verbose("Ingesting file uploads from {Session}", session);
|
||||
|
||||
base.ResourceUploadMsg(msg);
|
||||
List<(ResPath Relative, byte[] Data)> ingested;
|
||||
await using (var stream = transfer.DataStream)
|
||||
{
|
||||
ingested = await IngestFileStream(stream);
|
||||
}
|
||||
|
||||
Sawmill.Verbose("Ingesting file uploads complete, distributing...");
|
||||
|
||||
// Now we broadcast the message!
|
||||
foreach (var channel in _serverNetManager.Channels)
|
||||
{
|
||||
channel.SendMessage(msg);
|
||||
SendToPlayer(channel, ingested);
|
||||
}
|
||||
|
||||
OnResourceUploaded?.Invoke(session, msg);
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (OnResourceUploaded != null)
|
||||
{
|
||||
foreach (var (relative, data) in ingested)
|
||||
{
|
||||
OnResourceUploaded?.Invoke(session, new NetworkResourceUploadMessage
|
||||
{
|
||||
MsgChannel = session.Channel,
|
||||
Data = data,
|
||||
RelativePath = relative
|
||||
});
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
ResourcesUploaded?.Invoke(new NetworkResourcesUploadedEvent(session, [..ingested]));
|
||||
}
|
||||
|
||||
internal void SendToNewUser(INetChannel channel)
|
||||
protected override void ValidateUpload(uint size)
|
||||
{
|
||||
foreach (var (path, data) in ContentRoot.GetAllFiles())
|
||||
{
|
||||
var msg = new NetworkResourceUploadMessage();
|
||||
msg.RelativePath = path;
|
||||
msg.Data = data;
|
||||
channel.SendMessage(msg);
|
||||
}
|
||||
if (SizeLimit > 0f && size * BytesToMegabytes > SizeLimit)
|
||||
throw new Exception("File upload too large!");
|
||||
}
|
||||
|
||||
internal bool SendToNewUser(INetChannel channel)
|
||||
{
|
||||
var allFiles = ContentRoot.GetAllFiles().ToList();
|
||||
if (allFiles.Count == 0)
|
||||
return false;
|
||||
|
||||
SendToPlayer(channel, allFiles, AckInitial);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async void SendToPlayer(INetChannel channel, List<(ResPath Relative, byte[] Data)> files, int ack = 0)
|
||||
{
|
||||
await using var stream = TransferManager.StartTransfer(channel,
|
||||
new TransferStartInfo
|
||||
{
|
||||
MessageKey = TransferKeyNetworkDownload
|
||||
});
|
||||
|
||||
var ackBytes = new byte[4];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(ackBytes, ack);
|
||||
await stream.WriteAsync(ackBytes);
|
||||
|
||||
await WriteFileStream(stream, files);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Robust.Server.Upload;
|
||||
@@ -9,20 +10,36 @@ namespace Robust.Server.Upload;
|
||||
internal sealed class UploadedContentManager
|
||||
{
|
||||
[Dependency] private readonly IServerNetManager _netManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly GamePrototypeLoadManager _prototypeLoadManager = default!;
|
||||
[Dependency] private readonly NetworkResourceManager _networkResourceManager = default!;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_netManager.Connected += NetManagerOnConnected;
|
||||
_networkResourceManager.AckReceived += OnAckReceived;
|
||||
}
|
||||
|
||||
private void OnAckReceived(INetChannel channel, int ack)
|
||||
{
|
||||
if (ack != NetworkResourceManager.AckInitial)
|
||||
return;
|
||||
|
||||
ResourcesReady(channel);
|
||||
}
|
||||
|
||||
private void NetManagerOnConnected(object? sender, NetChannelArgs e)
|
||||
{
|
||||
// This just shells out to the other managers, ensuring they are ordered properly.
|
||||
// Resources must be done before prototypes.
|
||||
// Note: both net messages sent here are on the same group and are therefore ordered.
|
||||
_networkResourceManager.SendToNewUser(e.Channel);
|
||||
_prototypeLoadManager.SendToNewUser(e.Channel);
|
||||
var sentAny = _networkResourceManager.SendToNewUser(e.Channel);
|
||||
if (!sentAny)
|
||||
ResourcesReady(e.Channel);
|
||||
}
|
||||
|
||||
private void ResourcesReady(INetChannel channel)
|
||||
{
|
||||
_prototypeLoadManager.SendToNewUser(channel);
|
||||
_playerManager.MarkPlayerResourcesSent(channel);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user