using System.Collections.Immutable;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Connection;
using Content.Server.Database;
using Content.Shared.Database;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.Server.Administration
{
///
/// Contains data resolved via .
///
/// The ID of the located user.
/// The last known IP address that the user connected with.
///
/// The last known HWID that the user connected with.
/// This should be used for placing new records involving HWIDs, such as bans.
/// For looking up data based on HWID, use combined and .
///
/// The last known username for the user connected with.
///
/// The last known legacy HWID value this user connected with. Only use for old lookups!
///
///
/// The set of last known modern HWIDs the user connected with.
///
public sealed record LocatedPlayerData(
NetUserId UserId,
IPAddress? LastAddress,
ImmutableTypedHwid? LastHWId,
string Username,
ImmutableArray? LastLegacyHWId,
ImmutableArray> LastModernHWIds);
///
/// Utilities for finding user IDs that extend to more than the server database.
///
///
/// Methods in this class will check connected clients, server database
/// AND the authentication server for lookups, in that order.
///
public interface IPlayerLocator
{
///
/// Look up a user ID by name globally.
///
/// Null if the player does not exist.
Task LookupIdByNameAsync(string playerName, CancellationToken cancel = default);
///
/// If passed a GUID, looks up the ID and tries to find HWId for it.
/// If passed a player name, returns .
///
Task LookupIdByNameOrIdAsync(string playerName, CancellationToken cancel = default);
///
/// Look up a user by globally.
///
/// Null if the player does not exist.
Task LookupIdAsync(NetUserId userId, CancellationToken cancel = default);
}
internal sealed class PlayerLocator : IPlayerLocator, IDisposable, IPostInjectInit
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly HttpClient _httpClient = new();
private ISawmill _sawmill = default!;
public PlayerLocator()
{
if (typeof(PlayerLocator).Assembly.GetName().Version is { } version)
{
_httpClient.DefaultRequestHeaders.UserAgent.Add(
new ProductInfoHeaderValue("SpaceStation14", version.ToString()));
}
}
public async Task LookupIdByNameAsync(string playerName, CancellationToken cancel = default)
{
// Check people currently on the server, the easiest case.
if (_playerManager.TryGetSessionByUsername(playerName, out var session))
return ReturnForSession(session);
// Check database for past players.
var record = await _db.GetPlayerRecordByUserName(playerName, cancel);
if (record != null)
return ReturnForPlayerRecord(record);
// If all else fails, ask the auth server.
var authServer = _configurationManager.GetCVar(CVars.AuthServer);
var requestUri = $"{authServer}api/query/name?name={WebUtility.UrlEncode(playerName)}";
using var resp = await _httpClient.GetAsync(requestUri, cancel);
return await HandleAuthServerResponse(resp, cancel);
}
public async Task LookupIdAsync(NetUserId userId, CancellationToken cancel = default)
{
// Check people currently on the server, the easiest case.
if (_playerManager.TryGetSessionById(userId, out var session))
return ReturnForSession(session);
// Check database for past players.
var record = await _db.GetPlayerRecordByUserId(userId, cancel);
if (record != null)
return ReturnForPlayerRecord(record);
// If all else fails, ask the auth server.
var authServer = _configurationManager.GetCVar(CVars.AuthServer);
var requestUri = $"{authServer}api/query/userid?userid={WebUtility.UrlEncode(userId.UserId.ToString())}";
using var resp = await _httpClient.GetAsync(requestUri, cancel);
return await HandleAuthServerResponse(resp, cancel);
}
private async Task HandleAuthServerResponse(HttpResponseMessage resp, CancellationToken cancel)
{
if (resp.StatusCode == HttpStatusCode.NotFound)
return null;
if (!resp.IsSuccessStatusCode)
{
_sawmill.Error("Auth server returned bad response {StatusCode}!", resp.StatusCode);
return null;
}
var responseData = await resp.Content.ReadFromJsonAsync(cancellationToken: cancel);
if (responseData == null)
{
_sawmill.Error("Auth server returned null response!");
return null;
}
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName, null, []);
}
private static LocatedPlayerData ReturnForSession(ICommonSession session)
{
var userId = session.UserId;
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.GetModernHwid();
return new LocatedPlayerData(
userId,
address,
hwId,
session.Name,
session.Channel.UserData.HWId,
session.Channel.UserData.ModernHWIds);
}
private static LocatedPlayerData ReturnForPlayerRecord(PlayerRecord record)
{
var hwid = record.HWId;
return new LocatedPlayerData(
record.UserId,
record.LastSeenAddress,
hwid,
record.LastSeenUserName,
hwid is { Type: HwidType.Legacy } ? hwid.Hwid : null,
hwid is { Type: HwidType.Modern } ? [hwid.Hwid] : []);
}
public async Task LookupIdByNameOrIdAsync(string playerName, CancellationToken cancel = default)
{
if (Guid.TryParse(playerName, out var guid))
{
var userId = new NetUserId(guid);
return await LookupIdAsync(userId, cancel);
}
return await LookupIdByNameAsync(playerName, cancel);
}
[UsedImplicitly]
private sealed record UserDataResponse(string UserName, Guid UserId)
{
}
void IDisposable.Dispose()
{
_httpClient.Dispose();
}
void IPostInjectInit.PostInject()
{
_sawmill = _logManager.GetSawmill("PlayerLocate");
}
}
}