Adds Network Resource Uploading for admins. (#6904)

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
This commit is contained in:
Vera Aguilera Puerto
2022-03-26 12:46:37 +01:00
committed by GitHub
parent 5954b7668c
commit eb54f4b224
21 changed files with 2657 additions and 6 deletions

View File

@@ -0,0 +1,62 @@
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Network;
using Robust.Shared.Utility;
namespace Content.Client.Administration.Commands;
public sealed class UploadFile : IConsoleCommand
{
public string Command => "uploadfile";
public string Description => "Uploads a resource to the server.";
public string Help => $"{Command} [relative path for the resource]";
public async void Execute(IConsoleShell shell, string argStr, string[] args)
{
var cfgMan = IoCManager.Resolve<IConfigurationManager>();
if (!cfgMan.GetCVar(CCVars.ResourceUploadingEnabled))
{
shell.WriteError("Network Resource Uploading is currently disabled by the server.");
return;
}
if (args.Length != 1)
{
shell.WriteError("Wrong number of arguments!");
return;
}
var dialog = IoCManager.Resolve<IFileDialogManager>();
var filters = new FileDialogFilters(new FileDialogFilters.Group("*"));
await using var file = await dialog.OpenFile(filters);
if (file == null)
{
shell.WriteError("Error picking file!");
return;
}
var sizeLimit = cfgMan.GetCVar(CCVars.ResourceUploadingLimitMb);
if (sizeLimit > 0f && file.Length * SharedNetworkResourceManager.BytesToMegabytes > sizeLimit)
{
shell.WriteError($"File above the current size limit! It must be smaller than {sizeLimit} MB.");
return;
}
var data = file.CopyToArray();
var netManager = IoCManager.Resolve<INetManager>();
var msg = netManager.CreateNetMessage<NetworkResourceUploadMessage>();
msg.RelativePath = new ResourcePath(args[0]).ToRelativePath();
msg.Data = data;
netManager.ClientSendMessage(msg);
}
}

View File

@@ -0,0 +1,15 @@
using Content.Shared.Administration;
namespace Content.Client.Administration.Managers;
public sealed class NetworkResourceManager : SharedNetworkResourceManager
{
/// <summary>
/// Callback for when the server sends a new resource.
/// </summary>
/// <param name="msg">The network message containing the data.</param>
protected override void ResourceUploadMsg(NetworkResourceUploadMessage msg)
{
ContentRoot.AddOrUpdateFile(msg.RelativePath, msg.Data);
}
}

View File

@@ -194,6 +194,7 @@ namespace Content.Client.Entry
IoCManager.Resolve<EuiManager>().Initialize(); IoCManager.Resolve<EuiManager>().Initialize();
IoCManager.Resolve<IVoteManager>().Initialize(); IoCManager.Resolve<IVoteManager>().Initialize();
IoCManager.Resolve<IGamePrototypeLoadManager>().Initialize(); IoCManager.Resolve<IGamePrototypeLoadManager>().Initialize();
IoCManager.Resolve<NetworkResourceManager>().Initialize();
_baseClient.RunLevelChanged += (_, args) => _baseClient.RunLevelChanged += (_, args) =>
{ {

View File

@@ -15,7 +15,6 @@ using Content.Client.StationEvents.Managers;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Client.Viewport; using Content.Client.Viewport;
using Content.Client.Voting; using Content.Client.Voting;
using Content.Shared.Actions;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Module; using Content.Shared.Module;
@@ -43,6 +42,7 @@ namespace Content.Client.IoC
IoCManager.Register<RulesManager, RulesManager>(); IoCManager.Register<RulesManager, RulesManager>();
IoCManager.Register<ViewportManager, ViewportManager>(); IoCManager.Register<ViewportManager, ViewportManager>();
IoCManager.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>(); IoCManager.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
IoCManager.Register<NetworkResourceManager>();
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
public partial class UploadedResourcesLog : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "uploaded_resource_log",
columns: table => new
{
uploaded_resource_log_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
user_id = table.Column<Guid>(type: "uuid", nullable: false),
path = table.Column<string>(type: "text", nullable: false),
data = table.Column<byte[]>(type: "bytea", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_uploaded_resource_log", x => x.uploaded_resource_log_id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "uploaded_resource_log");
}
}
}

View File

@@ -793,6 +793,39 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("server_unban", (string)null); b.ToTable("server_unban", (string)null);
}); });
modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("uploaded_resource_log_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<byte[]>("Data")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("data");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone")
.HasColumnName("date");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("text")
.HasColumnName("path");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("PK_uploaded_resource_log");
b.ToTable("uploaded_resource_log", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Whitelist", b => modelBuilder.Entity("Content.Server.Database.Whitelist", b =>
{ {
b.Property<Guid>("UserId") b.Property<Guid>("UserId")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
public partial class UploadedResourcesLog : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "uploaded_resource_log",
columns: table => new
{
uploaded_resource_log_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
date = table.Column<DateTime>(type: "TEXT", nullable: false),
user_id = table.Column<Guid>(type: "TEXT", nullable: false),
path = table.Column<string>(type: "TEXT", nullable: false),
data = table.Column<byte[]>(type: "BLOB", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_uploaded_resource_log", x => x.uploaded_resource_log_id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "uploaded_resource_log");
}
}
}

View File

@@ -737,6 +737,37 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("server_unban", (string)null); b.ToTable("server_unban", (string)null);
}); });
modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("uploaded_resource_log_id");
b.Property<byte[]>("Data")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("data");
b.Property<DateTime>("Date")
.HasColumnType("TEXT")
.HasColumnName("date");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<Guid>("UserId")
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("PK_uploaded_resource_log");
b.ToTable("uploaded_resource_log", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Whitelist", b => modelBuilder.Entity("Content.Server.Database.Whitelist", b =>
{ {
b.Property<Guid>("UserId") b.Property<Guid>("UserId")

View File

@@ -33,6 +33,7 @@ namespace Content.Server.Database
public DbSet<ServerBanHit> ServerBanHit { get; set; } = default!; public DbSet<ServerBanHit> ServerBanHit { get; set; } = default!;
public DbSet<ServerRoleBan> RoleBan { get; set; } = default!; public DbSet<ServerRoleBan> RoleBan { get; set; } = default!;
public DbSet<ServerRoleUnban> RoleUnban { get; set; } = default!; public DbSet<ServerRoleUnban> RoleUnban { get; set; } = default!;
public DbSet<UploadedResourceLog> UploadedResourceLog { get; set; } = default!;
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@@ -459,4 +460,19 @@ namespace Content.Server.Database
public DateTime UnbanTime { get; set; } public DateTime UnbanTime { get; set; }
} }
[Table("uploaded_resource_log")]
public sealed class UploadedResourceLog
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime Date { get; set; }
public Guid UserId { get; set; }
public string Path { get; set; } = string.Empty;
public byte[] Data { get; set; } = default!;
}
} }

View File

@@ -1,11 +1,6 @@
using System;
using System.Collections.Generic;
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Shared.Administration; using Content.Shared.Administration;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;

View File

@@ -0,0 +1,90 @@
using Content.Server.Administration.Managers;
using Content.Server.Database;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
namespace Content.Server.Administration;
public sealed class NetworkResourceManager : SharedNetworkResourceManager
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IServerNetManager _serverNetManager = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IServerDbManager _serverDb = default!;
[ViewVariables] public bool Enabled { get; private set; } = true;
[ViewVariables] public float SizeLimit { get; private set; } = 0f;
[ViewVariables] public bool StoreUploaded { get; set; } = true;
public override void Initialize()
{
base.Initialize();
_serverNetManager.Connected += ServerNetManagerOnConnected;
_cfgManager.OnValueChanged(CCVars.ResourceUploadingEnabled, value => Enabled = value, true);
_cfgManager.OnValueChanged(CCVars.ResourceUploadingLimitMb, value => SizeLimit = value, true);
_cfgManager.OnValueChanged(CCVars.ResourceUploadingStoreEnabled, value => StoreUploaded = value, true);
AutoDelete(_cfgManager.GetCVar(CCVars.ResourceUploadingStoreDeletionDays));
}
/// <summary>
/// Callback for when a client attempts to upload a resource.
/// </summary>
/// <param name="msg"></param>
/// <exception cref="NotImplementedException"></exception>
protected override async void ResourceUploadMsg(NetworkResourceUploadMessage msg)
{
// 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)
return;
if (!_playerManager.TryGetSessionByChannel(msg.MsgChannel, out var session))
return;
// +QUERY only for now.
if (!_adminManager.HasAdminFlag(session, AdminFlags.Query))
return;
// Ensure the data is under the current size limit, if it's currently enabled.
if (SizeLimit > 0f && msg.Data.Length * BytesToMegabytes > SizeLimit)
return;
ContentRoot.AddOrUpdateFile(msg.RelativePath, msg.Data);
// Now we broadcast the message!
foreach (var channel in _serverNetManager.Channels)
{
channel.SendMessage(msg);
}
if (!StoreUploaded)
return;
await _serverDb.AddUploadedResourceLogAsync(session.UserId, DateTime.Now, msg.RelativePath.ToString(), msg.Data);
}
private void ServerNetManagerOnConnected(object? sender, NetChannelArgs e)
{
foreach (var (path, data) in ContentRoot.GetAllFiles())
{
var msg = _serverNetManager.CreateNetMessage<NetworkResourceUploadMessage>();
msg.RelativePath = path;
msg.Data = data;
e.Channel.SendMessage(msg);
}
}
private async void AutoDelete(int days)
{
if (days <= 0)
return; // auto-deletion disabled...
await _serverDb.PurgeUploadedResourceLogAsync(days);
}
}

View File

@@ -776,6 +776,34 @@ namespace Content.Server.Database
#endregion #endregion
#region Uploaded Resources Logs
public async Task AddUploadedResourceLogAsync(NetUserId user, DateTime date, string path, byte[] data)
{
await using var db = await GetDb();
db.DbContext.UploadedResourceLog.Add(new UploadedResourceLog() { UserId = user, Date = date, Path = path, Data = data });
await db.DbContext.SaveChangesAsync();
}
public async Task PurgeUploadedResourceLogAsync(int days)
{
await using var db = await GetDb();
var date = DateTime.Now.Subtract(TimeSpan.FromDays(days));
await foreach (var log in db.DbContext.UploadedResourceLog
.Where(l => date > l.Date)
.AsAsyncEnumerable())
{
db.DbContext.UploadedResourceLog.Remove(log);
}
await db.DbContext.SaveChangesAsync();
}
#endregion
protected abstract Task<DbGuard> GetDb(); protected abstract Task<DbGuard> GetDb();
protected abstract class DbGuard : IAsyncDisposable protected abstract class DbGuard : IAsyncDisposable

View File

@@ -185,6 +185,14 @@ namespace Content.Server.Database
Task RemoveFromWhitelistAsync(NetUserId player); Task RemoveFromWhitelistAsync(NetUserId player);
#endregion #endregion
#region Uploaded Resources Logs
Task AddUploadedResourceLogAsync(NetUserId user, DateTime date, string path, byte[] data);
Task PurgeUploadedResourceLogAsync(int days);
#endregion
} }
public sealed class ServerDbManager : IServerDbManager public sealed class ServerDbManager : IServerDbManager
@@ -455,6 +463,16 @@ namespace Content.Server.Database
return _db.RemoveFromWhitelistAsync(player); return _db.RemoveFromWhitelistAsync(player);
} }
public Task AddUploadedResourceLogAsync(NetUserId user, DateTime date, string path, byte[] data)
{
return _db.AddUploadedResourceLogAsync(user, date, path, data);
}
public Task PurgeUploadedResourceLogAsync(int days)
{
return _db.PurgeUploadedResourceLogAsync(days);
}
private DbContextOptions<PostgresServerDbContext> CreatePostgresOptions() private DbContextOptions<PostgresServerDbContext> CreatePostgresOptions()
{ {
var host = _cfg.GetCVar(CCVars.DatabasePgHost); var host = _cfg.GetCVar(CCVars.DatabasePgHost);

View File

@@ -1,4 +1,5 @@
using System.IO; using System.IO;
using Content.Server.Administration;
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Server.Afk; using Content.Server.Afk;
using Content.Server.AI.Utility; using Content.Server.AI.Utility;
@@ -86,6 +87,7 @@ namespace Content.Server.Entry
IoCManager.Resolve<IServerPreferencesManager>().Init(); IoCManager.Resolve<IServerPreferencesManager>().Init();
IoCManager.Resolve<INodeGroupFactory>().Initialize(); IoCManager.Resolve<INodeGroupFactory>().Initialize();
IoCManager.Resolve<IGamePrototypeLoadManager>().Initialize(); IoCManager.Resolve<IGamePrototypeLoadManager>().Initialize();
IoCManager.Resolve<NetworkResourceManager>().Initialize();
_voteManager.Initialize(); _voteManager.Initialize();
} }
} }

View File

@@ -50,6 +50,7 @@ namespace Content.Server.IoC
IoCManager.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>(); IoCManager.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
IoCManager.Register<RulesManager, RulesManager>(); IoCManager.Register<RulesManager, RulesManager>();
IoCManager.Register<RoleBanManager, RoleBanManager>(); IoCManager.Register<RoleBanManager, RoleBanManager>();
IoCManager.Register<NetworkResourceManager>();
} }
} }
} }

View File

@@ -0,0 +1,39 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Utility;
namespace Content.Shared.Administration;
public sealed class NetworkResourceUploadMessage : NetMessage
{
public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableUnordered;
public override MsgGroups MsgGroup => MsgGroups.Command;
public byte[] Data { get; set; } = Array.Empty<byte>();
public ResourcePath RelativePath { get; set; } = ResourcePath.Self;
public NetworkResourceUploadMessage()
{
}
public NetworkResourceUploadMessage(byte[] data, ResourcePath relativePath)
{
Data = data;
RelativePath = relativePath;
}
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
var dataLength = buffer.ReadVariableInt32();
Data = buffer.ReadBytes(dataLength);
RelativePath = new ResourcePath(buffer.ReadString(), buffer.ReadString());
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.WriteVariableInt32(Data.Length);
buffer.Write(Data);
buffer.Write(RelativePath.ToString());
buffer.Write(RelativePath.Separator);
}
}

View File

@@ -0,0 +1,41 @@
using Robust.Shared.ContentPack;
using Robust.Shared.Network;
using Robust.Shared.Utility;
namespace Content.Shared.Administration;
/// <summary>
/// Manager that allows resources to be added at runtime by admins.
/// They will be sent to all clients automatically.
/// </summary>
public abstract class SharedNetworkResourceManager : IDisposable
{
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] protected readonly IResourceManager ResourceManager = default!;
public const double BytesToMegabytes = 0.000001d;
/// <summary>
/// The prefix for any and all downloaded network resources.
/// </summary>
private static readonly ResourcePath Prefix = ResourcePath.Root / "Uploaded";
protected readonly MemoryContentRoot ContentRoot = new();
public virtual void Initialize()
{
_netManager.RegisterNetMessage<NetworkResourceUploadMessage>(ResourceUploadMsg);
// Add our content root to the resource manager.
ResourceManager.AddRoot(Prefix, ContentRoot);
}
protected abstract void ResourceUploadMsg(NetworkResourceUploadMessage msg);
public void Dispose()
{
// This is called automatically when the IoCManager's dependency collection is cleared.
// MemoryContentRoot uses a ReaderWriterLockSlim, which we need to dispose of.
ContentRoot.Dispose();
}
}

View File

@@ -737,5 +737,37 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<string> DestinationFile = public static readonly CVarDef<string> DestinationFile =
CVarDef.Create("autogen.destination_file", "", CVar.SERVER | CVar.SERVERONLY); CVarDef.Create("autogen.destination_file", "", CVar.SERVER | CVar.SERVERONLY);
/*
* Network Resource Manager
*/
/// <summary>
/// Controls whether new resources can be uploaded by admins.
/// Does not prevent already uploaded resources from being sent.
/// </summary>
public static readonly CVarDef<bool> ResourceUploadingEnabled =
CVarDef.Create("netres.enabled", true, CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Controls the data size limit in megabytes for uploaded resources. If they're too big, they will be dropped.
/// Set to zero or a negative value to disable limit.
/// </summary>
public static readonly CVarDef<float> ResourceUploadingLimitMb =
CVarDef.Create("netres.limit", 3f, CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Whether uploaded files will be stored in the server's database.
/// This is useful to keep "logs" on what files admins have uploaded in the past.
/// </summary>
public static readonly CVarDef<bool> ResourceUploadingStoreEnabled =
CVarDef.Create("netres.store_enabled", true, CVar.SERVER | CVar.SERVERONLY);
/// <summary>
/// Numbers of days before stored uploaded files are deleted. Set to zero or negative to disable auto-delete.
/// This is useful to free some space automatically. Auto-deletion runs only on server boot.
/// </summary>
public static readonly CVarDef<int> ResourceUploadingStoreDeletionDays =
CVarDef.Create("netres.store_deletion_days", 30, CVar.SERVER | CVar.SERVERONLY);
} }
} }

View File

@@ -28,3 +28,7 @@
- Flags: ADMIN - Flags: ADMIN
Commands: Commands:
- togglehealthoverlay - togglehealthoverlay
- Flags: QUERY
Commands:
- uploadfile