Skip to content

Commit b82e9fa

Browse files
committed
Add API endpoints for server management
1 parent 7ea76a0 commit b82e9fa

8 files changed

Lines changed: 192 additions & 24 deletions

File tree

src/BF2WebAdmin.Server/BF2WebAdmin.Server.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
</ItemGroup>
4040

4141
<ItemGroup>
42+
<PackageReference Include="AspNetCore.Authentication.ApiKey" Version="8.0.1" />
4243
<PackageReference Include="Discord.Net" Version="2.3.0" />
4344
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
4445
<PackageReference Include="Mapster" Version="7.3.0" />
@@ -98,7 +99,6 @@
9899

99100
<ItemGroup>
100101
<Folder Include="Hubs\" />
101-
<Folder Include="Controllers\" />
102102
<Folder Include="Data\" />
103103
<Folder Include="Services\" />
104104
<Folder Include="Properties\PublishProfiles\" />

src/BF2WebAdmin.Server/Configuration/appsettings.template.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
"Username": "admin",
4848
"Password": "test"
4949
}
50-
]
50+
],
51+
"ApiKey": "testkey"
5152
},
5253
"RabbitMQ": {
5354
"Host": "",

src/BF2WebAdmin.Server/Controllers/AuthController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public async Task<IActionResult> Test()
8585
public class AuthSettings
8686
{
8787
public IEnumerable<AdminUser> Admins { get; set; }
88+
public string ApiKey { get; set; }
8889
}
8990

9091
public class AdminUser
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using AspNetCore.Authentication.ApiKey;
2+
using BF2WebAdmin.Common.Communication;
3+
using BF2WebAdmin.Shared.Communication.DTOs;
4+
using BF2WebAdmin.Shared.Communication.Events;
5+
using Mapster;
6+
using Microsoft.AspNetCore.Authentication.Cookies;
7+
using Microsoft.AspNetCore.Authorization;
8+
using Microsoft.AspNetCore.Mvc;
9+
10+
namespace BF2WebAdmin.Server.Controllers;
11+
12+
[Authorize(AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme},{ApiKeyDefaults.AuthenticationScheme}")]
13+
[Route("/api/v1/servers")]
14+
public class ServerController : Controller
15+
{
16+
private readonly ISocketServer _socketServer;
17+
18+
public ServerController(ISocketServer socketServer)
19+
{
20+
_socketServer = socketServer;
21+
}
22+
23+
[HttpGet]
24+
public async Task<IActionResult> GetServers()
25+
{
26+
var servers = _socketServer.GetGameServers().Select(s => new Shared.Communication.Events.ServerUpdateEvent
27+
{
28+
Id = s.Id,
29+
Name = s.Name,
30+
IpAddress = s.IpAddress.ToString(),
31+
GamePort = s.GamePort,
32+
QueryPort = s.QueryPort,
33+
Map = s.Map?.Name,
34+
Players = s.Players.Count(),
35+
MaxPlayers = s.MaxPlayers,
36+
GameState = s.State,
37+
SocketState = s.SocketState,
38+
ServerGroup = s.ModManager.ServerSettings.ServerGroup
39+
});
40+
return Ok(servers);
41+
}
42+
43+
[HttpGet("{serverId}")]
44+
public async Task<IActionResult> GetServer(string serverId)
45+
{
46+
var server = _socketServer.GetGameServer(serverId);
47+
var result = new ServerSnapshotEvent
48+
{
49+
Server = new ServerDto
50+
{
51+
Id = server.Id,
52+
Name = server.Name,
53+
IpAddress = server.IpAddress.ToString(),
54+
GamePort = server.GamePort,
55+
QueryPort = server.QueryPort,
56+
Map = server.Map?.Name,
57+
Players = server.Players.Count(),
58+
MaxPlayers = server.MaxPlayers,
59+
GameState = server.State,
60+
SocketState = server.SocketState,
61+
ServerGroup = server.ModManager.ServerSettings.ServerGroup
62+
},
63+
Data = new ServerDataDto
64+
{
65+
ServerId = $"{server.ServerInfo.IpAddress}:{server.ServerInfo.GamePort}",
66+
ServerGroup = server.ServerInfo.ServerGroup,
67+
IpAddress = server.ServerInfo.IpAddress,
68+
GamePort = server.ServerInfo.GamePort,
69+
QueryPort = server.ServerInfo.QueryPort,
70+
RconPort = server.ServerInfo.RconPort,
71+
RconPassword = server.ServerInfo.RconPassword,
72+
DiscordBotToken = server.ServerInfo.DiscordBot.Token,
73+
DiscordAdminChannel = server.ServerInfo.DiscordBot.AdminChannel,
74+
DiscordNotificationChannel = server.ServerInfo.DiscordBot.NotificationChannel,
75+
DiscordMatchResultChannel = server.ServerInfo.DiscordBot.MatchResultChannel
76+
},
77+
Maps = server.Maps.Select(m => m.ToDto()),
78+
Teams = server.Teams.Select(t => t.ToDto()),
79+
Players = server.Players.Select(p => p.ToDto()).ToList(),
80+
EventLog = server.Events.Select(e => new EventLogDto { Message = e.Message, Timestamp = e.Time }),
81+
ChatLog = server.Messages.Select(m => new ChatLogDto { Message = m.Message, Timestamp = m.Time })
82+
};
83+
84+
return Ok(result);
85+
}
86+
87+
[HttpPost("{serverId}")]
88+
public async Task<IActionResult> CreateServer(string serverId, [FromBody] Data.Entities.Server server)
89+
{
90+
if (serverId != server.ServerId)
91+
return BadRequest();
92+
93+
var serverEntity = server.Adapt<Data.Entities.Server>();
94+
await _socketServer.AddOrUpdateServerAsync(serverEntity);
95+
return Ok();
96+
}
97+
98+
[HttpPatch("{serverId}")]
99+
public async Task<IActionResult> UpdateServer(string serverId, [FromBody] Data.Entities.Server server)
100+
{
101+
if (serverId != server.ServerId)
102+
return BadRequest();
103+
104+
var serverEntity = server.Adapt<Data.Entities.Server>();
105+
await _socketServer.AddOrUpdateServerAsync(serverEntity);
106+
return Ok();
107+
}
108+
109+
[HttpDelete("{serverId}")]
110+
public async Task<IActionResult> DeleteServer(string serverId)
111+
{
112+
await _socketServer.RemoveServerAsync(serverId);
113+
return Ok();
114+
}
115+
}

src/BF2WebAdmin.Server/Hubs/ServerHub.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,16 +162,14 @@ public async Task SetServer(ServerDataDto server)
162162
{
163163
using var _ = Telemetry.StartRootActivity();
164164
var serverEntity = server.Adapt<Data.Entities.Server>();
165-
await _serverSettingsRepository.SetServerAsync(serverEntity);
166-
await _socketServer.HandleServerUpdateAsync(server.ServerId, ServerUpdateType.AddOrUpdate);
165+
await _socketServer.AddOrUpdateServerAsync(serverEntity);
167166
await SendServerUpdatesAsync();
168167
}
169168

170169
public async Task RemoveServer(string serverId)
171170
{
172171
using var _ = Telemetry.StartRootActivity();
173-
await _serverSettingsRepository.RemoveServerAsync(serverId);
174-
await _socketServer.HandleServerUpdateAsync(serverId, ServerUpdateType.Remove);
172+
await _socketServer.RemoveServerAsync(serverId);
175173
await Clients.All.ServerRemoveEvent(new ServerRemoveEvent { ServerId = serverId });
176174
}
177175

@@ -245,4 +243,4 @@ private static async Task ReloadModulesAsync(IGameServer gameServer)
245243
// TODO: disable whatever the old modmanager is doing, dispose and finish all long-running tasks
246244
await gameServer.CreateModManagerAsync(true);
247245
}
248-
}
246+
}

src/BF2WebAdmin.Server/Program.cs

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
using BF2WebAdmin.Data;
1+
using System.Security.Claims;
2+
using AspNetCore.Authentication.ApiKey;
3+
using BF2WebAdmin.Data;
24
using BF2WebAdmin.Data.Abstractions;
35
using BF2WebAdmin.Data.Repositories;
46
using BF2WebAdmin.Server;
57
using BF2WebAdmin.Server.Controllers;
68
using BF2WebAdmin.Server.Extensions;
79
using BF2WebAdmin.Server.Hubs;
810
using BF2WebAdmin.Server.Services;
11+
using Google.Protobuf.WellKnownTypes;
912
using MassTransit;
1013
using Microsoft.AspNetCore.Authentication.Cookies;
1114
using Microsoft.AspNetCore.ResponseCompression;
@@ -50,12 +53,15 @@
5053

5154
// Add services to the container.
5255

53-
builder.Services.Configure<ServerSettings>(builder.Configuration.GetSection("ServerSettings"));
54-
builder.Services.Configure<AuthSettings>(builder.Configuration.GetSection("Authentication"));
56+
var authSettings = builder.Configuration.GetSection("Authentication").Get<AuthSettings>();
57+
ArgumentNullException.ThrowIfNull(authSettings);
5558

5659
var serverSettings = builder.Configuration.GetSection("ServerSettings").Get<ServerSettings>();
5760
ArgumentNullException.ThrowIfNull(serverSettings);
5861

62+
builder.Services.Configure<AuthSettings>(builder.Configuration.GetSection("Authentication"));
63+
builder.Services.Configure<ServerSettings>(builder.Configuration.GetSection("ServerSettings"));
64+
5965
var mqSettings = builder.Configuration.GetSection("RabbitMQ").Get<RabbitMqSettings>();
6066
if (!string.IsNullOrWhiteSpace(mqSettings?.Host))
6167
{
@@ -107,18 +113,48 @@
107113

108114
// TODO: DataProtection to persist logins
109115
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
110-
.AddCookie(options =>
116+
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
111117
{
112118
options.ExpireTimeSpan = TimeSpan.FromDays(30);
113119
options.SlidingExpiration = true;
114120
options.AccessDeniedPath = "/Forbidden/";
121+
})
122+
.AddApiKeyInHeader(ApiKeyDefaults.AuthenticationScheme, options =>
123+
{
124+
options.Realm = "bf2-webadmin";
125+
options.KeyName = "X-API-KEY";
126+
options.Events = new ApiKeyEvents
127+
{
128+
OnValidateKey = async (context) =>
129+
{
130+
var apiKey = authSettings.ApiKey;
131+
if (string.IsNullOrWhiteSpace(apiKey))
132+
{
133+
context.NoResult();
134+
return;
135+
}
136+
137+
var isValid = apiKey.Equals(context.ApiKey, StringComparison.OrdinalIgnoreCase);
138+
if (isValid)
139+
{
140+
var claims = new[]
141+
{
142+
new Claim(ClaimTypes.NameIdentifier, "api-user", ClaimValueTypes.String, context.Options.ClaimsIssuer),
143+
new Claim(ClaimTypes.Name, "api-user", ClaimValueTypes.String, context.Options.ClaimsIssuer),
144+
};
145+
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
146+
context.Success();
147+
}
148+
else
149+
{
150+
context.NoResult();
151+
}
152+
},
153+
};
115154
});
116155

117156
builder.Services.AddRazorPages();
118-
builder.Services.AddResponseCompression(opts =>
119-
{
120-
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/octet-stream" });
121-
});
157+
builder.Services.AddResponseCompression(opts => { opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/octet-stream" }); });
122158

123159
// Normal MediatR doesn't work with the singleton SocketServer, but we can setup it up differently
124160
// This is not very nice though...
@@ -199,4 +235,4 @@
199235
// finally
200236
// {
201237
// Log.CloseAndFlush();
202-
// }
238+
// }

src/BF2WebAdmin.Server/SocketServer.cs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ namespace BF2WebAdmin.Server;
1515
public interface ISocketServer
1616
{
1717
IGameServer? GetGameServer(string serverId);
18-
IEnumerable<IGameServer> GetGameServers(string? serverGroup);
18+
IEnumerable<IGameServer> GetGameServers(string? serverGroup = null);
1919
Task<IEnumerable<ServerInfo>> GetDisconnectedServersAsync();
2020
Task StartAsync(CancellationToken cancellationToken);
21-
Task HandleServerUpdateAsync(string serverId, ServerUpdateType type);
21+
Task AddOrUpdateServerAsync(Data.Entities.Server server);
22+
Task RemoveServerAsync(string serverId);
2223
}
2324

2425
public class SocketServer : ISocketServer
@@ -38,7 +39,7 @@ public class SocketServer : ISocketServer
3839

3940
public IPAddress GetIpAddress() => _ipAddress;
4041
public IGameServer? GetGameServer(string serverId) => _servers.TryGetValue(serverId, out var serverContext) ? serverContext.GameServer : null;
41-
public IEnumerable<IGameServer> GetGameServers(string? serverGroup) => _servers.Values.Where(s => s.GameServer.ModManager?.ServerSettings?.ServerGroup == serverGroup || serverGroup is null).Select(c => c.GameServer).ToList();
42+
public IEnumerable<IGameServer> GetGameServers(string? serverGroup = null) => _servers.Values.Where(s => s.GameServer.ModManager?.ServerSettings?.ServerGroup == serverGroup || serverGroup is null).Select(c => c.GameServer).ToList();
4243
public async Task<IEnumerable<ServerInfo>> GetDisconnectedServersAsync() => (await GetServerInfoAsync()).Where(si => !_servers.ContainsKey($"{si.IpAddress}:{si.GamePort}"));
4344

4445
public SocketServer(IPAddress ipAddress, int port, IEnumerable<ServerInfo>? serverInfoFromConfig, IServiceProvider globalServices, bool logSend, bool logRecv)
@@ -65,7 +66,7 @@ private async ValueTask<IEnumerable<ServerInfo>> GetServerInfoAsync()
6566
{
6667
using var serviceScope = _globalServices.CreateScope();
6768
var serverRepository = serviceScope.ServiceProvider.GetRequiredService<IServerSettingsRepository>();
68-
69+
6970
// Combine server info from config and DB
7071
entry.AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(5);
7172

@@ -163,7 +164,7 @@ private async Task ListenAsync(CancellationToken cancellationToken)
163164
_logger.LogError(ex, "Game server TCP error");
164165
}
165166
}
166-
167+
167168
if (cancellationToken.IsCancellationRequested)
168169
server?.Server?.Close();
169170

@@ -651,8 +652,23 @@ private Task RetryConnectionAsync(IPAddress ipAddress, int port, ServerInfo serv
651652
}, cancellationToken);
652653
}
653654

654-
// public async Task Handle(ServerUpdated notification, CancellationToken cancellationToken)
655-
public async Task HandleServerUpdateAsync(string serverId, ServerUpdateType type)
655+
public async Task AddOrUpdateServerAsync(Data.Entities.Server server)
656+
{
657+
using var serviceScope = _globalServices.CreateScope();
658+
var serverRepository = serviceScope.ServiceProvider.GetRequiredService<IServerSettingsRepository>();
659+
await serverRepository.SetServerAsync(server);
660+
await HandleServerUpdateAsync(server.ServerId, ServerUpdateType.AddOrUpdate);
661+
}
662+
663+
public async Task RemoveServerAsync(string serverId)
664+
{
665+
using var serviceScope = _globalServices.CreateScope();
666+
var serverRepository = serviceScope.ServiceProvider.GetRequiredService<IServerSettingsRepository>();
667+
await serverRepository.RemoveServerAsync(serverId);
668+
await HandleServerUpdateAsync(serverId, ServerUpdateType.Remove);
669+
}
670+
671+
private async Task HandleServerUpdateAsync(string serverId, ServerUpdateType type)
656672
{
657673
_logger.LogDebug("Handling server {UpdateType} for {ServerId}", type, serverId);
658674

@@ -683,4 +699,4 @@ public enum ServerUpdateType
683699
{
684700
AddOrUpdate,
685701
Remove
686-
}
702+
}

src/BF2WebAdmin.Shared/Communication/Events/ServerSnapshotEvent.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public class ServerSnapshotEvent : IMessagePayload
88
{
99
// TODO: unnecessary?
1010
public ServerDto Server { get; set; }
11+
public ServerDataDto Data { get; set; }
1112
public IEnumerable<MapDto> Maps { get; set; }
1213
public IEnumerable<TeamDto> Teams { get; set; }
1314
public IEnumerable<PlayerDto> Players { get; set; }

0 commit comments

Comments
 (0)