diff --git a/.github/release.yaml b/.github/release.yaml new file mode 100644 index 0000000..aafd5d2 --- /dev/null +++ b/.github/release.yaml @@ -0,0 +1,16 @@ +changelog: + exclude: + labels: + - ignore-for-release + categories: + - title: Breaking Changes 🛠 + labels: + - Semver-Major + - breaking-change + - title: Exciting New Features 🎉 + labels: + - Semver-Minor + - enhancement + - title: Other Changes + labels: + - "*" \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 20cdcd7..afda1c9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -65,5 +65,6 @@ jobs: name: v${{ env.GitVersion_SemVer }} files: ./bin/Release/net6.0/VRisingServerApiPlugin.dll fail_on_unmatched_files: true + generate_release_notes: true prerelease: true tag_name: v${{ env.GitVersion_SemVer }} \ No newline at end of file diff --git a/ApiPlugin.cs b/ApiPlugin.cs new file mode 100644 index 0000000..f574fa4 --- /dev/null +++ b/ApiPlugin.cs @@ -0,0 +1,100 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using BepInEx; +using BepInEx.Configuration; +using BepInEx.Logging; +using BepInEx.Unity.IL2CPP; +using HarmonyLib; +using Il2CppInterop.Runtime.Injection; +using UnityEngine; +using VRisingServerApiPlugin.command; +using VRisingServerApiPlugin.query; + +namespace VRisingServerApiPlugin; + +[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)] +public class ApiPlugin : BasePlugin +{ + private Harmony? _harmony; + private Component? _queryDispatcher; + +# nullable disable + internal static ManualLogSource Logger { get; private set; } + public static ApiPlugin Instance { get; private set; } +#nullable enable + + private ConfigEntry _authorizedUsers; + + public ApiPlugin() : base() + { + Instance = this; + Logger = Log; + + _authorizedUsers = Config.Bind("Authentication", "AuthorizedUsers", "", + "A list of comma separated username:password entries that defines the accounts allowed to query the API"); + } + + public override void Load() + { + if (!ServerWorld.IsServer) + { + Log.LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} must be installed server side"); + return; + } + + CommandRegistry.RegisterAll(); + + ClassInjector.RegisterTypeInIl2Cpp(); + _queryDispatcher = AddComponent(); + + _harmony = new Harmony(MyPluginInfo.PLUGIN_GUID); + _harmony.PatchAll(Assembly.GetExecutingAssembly()); + + Log.LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!"); + } + + public override bool Unload() + { + _harmony?.UnpatchSelf(); + if (_queryDispatcher != null) + { + Object.Destroy(_queryDispatcher); + } + + return true; + } + + public List GetAuthorizedUserList() + { + return AuthorizedUser.ParseConfig(this._authorizedUsers.Value); + } + + public bool CheckAuthenticationOfUser(string username, string password) + { + return GetAuthorizedUserList() + .Count(user => user.Username.Equals(username) && user.Password.Equals(password)) == 1; + } + + public class AuthorizedUser + { + public string Username { get; set; } + public string Password { get; set; } + + private AuthorizedUser(string username, string password) + { + Username = username; + Password = password; + } + + public static List ParseConfig(string authorizedUsers) + { + return (from user in authorizedUsers.Split(",") + select user.Split(':') + into parts + where parts.Length == 2 + select new AuthorizedUser(parts[0].Trim(), parts[1].Trim())).ToList(); + } + } +} \ No newline at end of file diff --git a/Plugin.cs b/Plugin.cs deleted file mode 100644 index 9d06e47..0000000 --- a/Plugin.cs +++ /dev/null @@ -1,52 +0,0 @@ -#nullable enable -using System.Reflection; -using BepInEx; -using BepInEx.Logging; -using BepInEx.Unity.IL2CPP; -using HarmonyLib; -using Il2CppInterop.Runtime.Injection; -using UnityEngine; -using VRisingServerApiPlugin.command; -using VRisingServerApiPlugin.query; - -namespace VRisingServerApiPlugin; - -[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)] -public class Plugin : BasePlugin -{ - internal static ManualLogSource? Logger; - private Harmony? _harmony; - private Component? _queryDispatcher; - - public override void Load() - { - if (!ServerWorld.IsServer) - { - Log.LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} must be installed server side"); - return; - } - - Logger = Log; - - CommandRegistry.RegisterAll(); - - ClassInjector.RegisterTypeInIl2Cpp(); - _queryDispatcher = AddComponent(); - - _harmony = new Harmony(MyPluginInfo.PLUGIN_GUID); - _harmony.PatchAll(Assembly.GetExecutingAssembly()); - - Log.LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!"); - } - - public override bool Unload() - { - _harmony?.UnpatchSelf(); - if (_queryDispatcher != null) - { - Object.Destroy(_queryDispatcher); - } - - return true; - } -} diff --git a/ServerWorld.cs b/ServerWorld.cs index bd9a45a..f30dff0 100644 --- a/ServerWorld.cs +++ b/ServerWorld.cs @@ -21,7 +21,7 @@ public static class ServerWorld public static EntityManager EntityManager = Server.EntityManager; public static GameDataSystem GameDataSystem = Server.GetExistingSystem(); - + public static bool IsServer => Application.productName == "VRisingServer"; private static World? GetWorld(string name) @@ -40,39 +40,47 @@ public static class ServerWorld private static Player ConvertEntityToPlayer(Entity userEntity) { var user = EntityManager.GetComponentData(userEntity); - return new Player(user, + + PlayerCharacter? playerCharacter = EntityManager.HasComponent(user.LocalCharacter._Entity) + ? EntityManager.GetComponentData(user.LocalCharacter._Entity) + : null; + + return new Player(user, userEntity, - EntityManager.GetComponentData(user.LocalCharacter._Entity), - user.LocalCharacter._Entity); + playerCharacter, + user.LocalCharacter._Entity + ); } public static IEnumerable GetAllPlayerCharacters() { return ListUtils.Convert( - EntityManager.CreateEntityQuery(ComponentType.ReadOnly()) - .ToEntityArray(Allocator.Temp) + EntityManager.CreateEntityQuery(ComponentType.ReadOnly()) + .ToEntityArray(Allocator.Temp) ) .Where(userEntity => EntityManager.GetComponentData(userEntity).LocalCharacter._Entity != Entity.Null) .Select(ConvertEntityToPlayer) - .ToList(); + .ToList(); } public static Player? GetPlayer(int userIndex) { - Entity? userEntity = ListUtils + Entity? entity = ListUtils .Convert(EntityManager.CreateEntityQuery(ComponentType.ReadOnly()) .ToEntityArray(Allocator.Temp)) - .FirstOrDefault(userEntity => EntityManager.GetComponentData(userEntity).Index == userIndex); + .FirstOrDefault(e => EntityManager.GetComponentData(e).Index == userIndex); - return userEntity.HasValue ? ConvertEntityToPlayer(userEntity.Value) : null; + return entity.HasValue + ? ConvertEntityToPlayer(entity.Value) + : null; } public static IEnumerable GetAllClans() { return ListUtils.Convert( - EntityManager.CreateEntityQuery(ComponentType.ReadOnly()) - .ToEntityArray(Allocator.Temp) - ) + EntityManager.CreateEntityQuery(ComponentType.ReadOnly()) + .ToEntityArray(Allocator.Temp) + ) .Select(clanEntity => EntityManager.GetComponentData(clanEntity)) .ToList(); } @@ -88,7 +96,7 @@ public static IEnumerable GetAllClanEntities() public readonly record struct Player( User User, Entity UserEntity, - PlayerCharacter Character, + PlayerCharacter? Character, Entity CharacterEntity ); } \ No newline at end of file diff --git a/VRisingServerApiPlugin.csproj b/VRisingServerApiPlugin.csproj index f808011..1edb600 100644 --- a/VRisingServerApiPlugin.csproj +++ b/VRisingServerApiPlugin.csproj @@ -23,9 +23,10 @@ + - + diff --git a/attributes/HttpHandlerAttribute.cs b/attributes/HttpHandlerAttribute.cs index 609b483..0cadba6 100644 --- a/attributes/HttpHandlerAttribute.cs +++ b/attributes/HttpHandlerAttribute.cs @@ -6,9 +6,12 @@ namespace VRisingServerApiPlugin.attributes; public class HttpHandlerAttribute : Attribute { public string BasePath { get; set; } - - public HttpHandlerAttribute(string basePath = "/") + + public bool AllRouteProtected { get; set; } + + public HttpHandlerAttribute(string basePath = "/", bool allRouteProtected = false) { BasePath = basePath; + AllRouteProtected = allRouteProtected; } } \ No newline at end of file diff --git a/attributes/methods/HttpAttribute.cs b/attributes/methods/HttpAttribute.cs index e593ffe..ed0e58e 100644 --- a/attributes/methods/HttpAttribute.cs +++ b/attributes/methods/HttpAttribute.cs @@ -8,10 +8,13 @@ public class HttpAttribute : Attribute public string Pattern { get; set; } public string Method { get; set; } - - public HttpAttribute(string pattern, string method) + + public bool Protected { get; set; } + + public HttpAttribute(string pattern, string method, bool isProtected = false) { Pattern = pattern; Method = method; + Protected = isProtected; } } \ No newline at end of file diff --git a/attributes/methods/HttpGet.cs b/attributes/methods/HttpGet.cs index fd659c6..2b06fc8 100644 --- a/attributes/methods/HttpGet.cs +++ b/attributes/methods/HttpGet.cs @@ -5,4 +5,8 @@ public class HttpGet : HttpAttribute public HttpGet(string pattern) : base(pattern, "GET") { } + + public HttpGet(string pattern, bool isProtected) : base(pattern, "GET", isProtected) + { + } } \ No newline at end of file diff --git a/attributes/methods/HttpPost.cs b/attributes/methods/HttpPost.cs index 49e2c50..bba3725 100644 --- a/attributes/methods/HttpPost.cs +++ b/attributes/methods/HttpPost.cs @@ -5,4 +5,8 @@ public class HttpPost : HttpAttribute public HttpPost(string pattern) : base(pattern, "POST") { } + + public HttpPost(string pattern, bool isProtected) : base(pattern, "POST", isProtected) + { + } } \ No newline at end of file diff --git a/command/Command.cs b/command/Command.cs index 48be09e..36d21df 100644 --- a/command/Command.cs +++ b/command/Command.cs @@ -9,11 +9,14 @@ public class Command public string Pattern { get; } public string Method { get; } + public bool IsProtected { get; } + public Func CommandHandler { get; } - public Command(string pattern, string method, Func commandHandler){ + public Command(string pattern, string method, Func commandHandler, bool isProtected = false){ Pattern = pattern; Method = method; CommandHandler = commandHandler; + IsProtected = isProtected; } } \ No newline at end of file diff --git a/command/CommandRegistry.cs b/command/CommandRegistry.cs index 90c7ced..332be11 100644 --- a/command/CommandRegistry.cs +++ b/command/CommandRegistry.cs @@ -53,7 +53,22 @@ private static void RegisterMethod(object? container, MethodInfo method, if (method.GetCustomAttribute(typeof(HttpAttribute), true) is not HttpAttribute httpAttribute) return; var pattern = $"^{httpHandlerAttribute.BasePath}{httpAttribute.Pattern}$"; - var command = new Command(pattern: pattern, method: httpAttribute.Method, request => + + var isProtected = httpAttribute.Protected || httpHandlerAttribute.AllRouteProtected; + + var command = new Command( + pattern: pattern, + method: httpAttribute.Method, + commandHandler: GenerateHandler(container, method), + isProtected: isProtected + ); + + Commands.Add(command); + } + + private static Func GenerateHandler(object? container, MethodBase method) + { + return request => { var args = new List(); var parameters = method.GetParameters(); @@ -84,7 +99,7 @@ private static void RegisterMethod(object? container, MethodInfo method, if (args.Count != parameters.Length) { - Plugin.Logger?.LogInfo($"parameters are {parameters} and parsed args are {args}"); + ApiPlugin.Logger?.LogInfo($"parameters are {parameters} and parsed args are {args}"); throw new HttpException(400, "Invalid parameters !"); } @@ -96,13 +111,11 @@ private static void RegisterMethod(object? container, MethodInfo method, { if (ex.InnerException == null) throw; - Plugin.Logger?.LogError( + ApiPlugin.Logger?.LogError( $"Inner Exception {ex.InnerException?.Message}, stack: {ex.InnerException?.StackTrace}"); throw ex.InnerException!; } - }); - - Commands.Add(command); + }; } private static object? ParseUrlOrQueryArg(Dictionary dictionary, string name, diff --git a/command/ServerWebAPISystemPatches.cs b/command/ServerWebAPISystemPatches.cs index 4ff297d..6ad826e 100644 --- a/command/ServerWebAPISystemPatches.cs +++ b/command/ServerWebAPISystemPatches.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Linq; using System.Net.Mime; using System.Text.Json; using System.Text.Json.Serialization; @@ -19,7 +20,7 @@ namespace VRisingServerApiPlugin.command; [HarmonyPatch(typeof(ServerWebAPISystem))] public class ServerWebAPISystemPatches { - private static readonly JsonSerializerOptions _serializerOptions = new() + private static readonly JsonSerializerOptions SerializerOptions = new() { DefaultIgnoreCondition = JsonIgnoreCondition.Never, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, @@ -33,13 +34,13 @@ public static void OnCreate(ServerWebAPISystem __instance) { if (!SettingsManager.ServerHostSettings.API.Enabled) { - Plugin.Logger?.LogInfo($"HTTP API is not enabled !"); + ApiPlugin.Logger?.LogInfo($"HTTP API is not enabled !"); return; } foreach (var command in CommandRegistry.GetCommands()) { - Plugin.Logger?.LogInfo($"Registering route with pattern {command.Pattern}"); + ApiPlugin.Logger?.LogInfo($"Registering route with pattern {command.Pattern}"); __instance._HttpReceiveService.AddRoute(new HttpServiceReceiveThread.Route( new Regex(command.Pattern), command.Method, @@ -54,23 +55,41 @@ private static HttpServiceReceiveThread.RequestHandler BuildAdapter( return DelegateSupport.ConvertDelegate( new Action(context => { - var request = HttpRequestParser.ParseHttpRequest(context.request, command); - Plugin.Logger?.LogInfo( - $"Http Request parsed is {JsonSerializer.Serialize(request, _serializerOptions)}"); - var commandResponse = QueryDispatcher.Instance.Dispatch(() => command.CommandHandler(request)); - while (commandResponse.Status == Status.PENDING) + object? responseData; + + var request = HttpRequestParser.ParseHttpRequest(context, command); + + try { - Thread.Sleep(25); - } + if (command.IsProtected && request.user is not { IsAuthorized: true }) + { + throw new HttpUnAuthorizeException(); + } - object? responseData; + var commandResponse = QueryDispatcher.Instance.Dispatch(() => command.CommandHandler(request)); + while (commandResponse.Status == Status.PENDING) + { + Thread.Sleep(25); + } - if (commandResponse.Status is Status.FAILURE or Status.PENDING) + if (commandResponse.Status is Status.FAILURE or Status.PENDING) + { + if (commandResponse.Exception == null) + { + throw new RequestTimeout(); + } + + throw commandResponse.Exception; + } + + responseData = commandResponse.Data; + } + catch (Exception e) { - Plugin.Logger?.LogError( - $"Request with url '{context.Request.Url.ToString()}' failed with message : {commandResponse.Exception?.Message}"); + ApiPlugin.Logger?.LogError( + $"Request with url '{context.Request.Url.ToString()}' failed with message : {e.Message}"); - if (commandResponse.Exception is HttpException httpException) + if (e is HttpException httpException) { context.Response.StatusCode = httpException.Status; responseData = @@ -82,18 +101,21 @@ private static HttpServiceReceiveThread.RequestHandler BuildAdapter( responseData = new InternalServerError("about:blank", "Internal Server Error"); } } - else - { - responseData = commandResponse.Data; - } context.Response.ContentType = MediaTypeNames.Application.Json; var responseWriter = new StreamWriter(context.Response.OutputStream); - responseWriter.Write(JsonSerializer.Serialize(responseData, _serializerOptions)); + responseWriter.Write(JsonSerializer.Serialize(responseData, SerializerOptions)); responseWriter.Flush(); }) )!; } + + private class RequestTimeout : HttpException + { + public RequestTimeout() : base(408, "Request Timeout") + { + } + } } \ No newline at end of file diff --git a/endpoints/clans/ClansEndpoints.cs b/endpoints/clans/ClansEndpoints.cs index 76365bf..497e6ae 100644 --- a/endpoints/clans/ClansEndpoints.cs +++ b/endpoints/clans/ClansEndpoints.cs @@ -41,8 +41,9 @@ public ListClanPlayersResponse GetClanPlayers([UrlParam("id")] Guid clanId) id: clan?.ClanGuid.ToString()); } - [HttpPost( - @"/(?[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})/updateName")] + [HttpPost(pattern: + @"/(?[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})/updateName", + isProtected: true)] public GetClanResponse UpdateClanName([UrlParam("id")] Guid clanId, [RequestBody] UpdateClanNameBody? body) { if (body.HasValue) @@ -57,7 +58,7 @@ public GetClanResponse UpdateClanName([UrlParam("id")] Guid clanId, [RequestBody } var clanTeam = clan.Value.ClanTeam; - Plugin.Logger?.LogInfo( + ApiPlugin.Logger?.LogInfo( $"Updating clan name from '{clanTeam.Name.ToString()}' to '{body.Value.Name}'"); clanTeam.Name = new FixedString64(body.Value.Name); ServerWorld.EntityManager.SetComponentData(clan.Value.ClanEntity, clanTeam); @@ -69,11 +70,12 @@ public GetClanResponse UpdateClanName([UrlParam("id")] Guid clanId, [RequestBody return new GetClanResponse(result.HasValue ? ClanUtils.Convert(result.Value) : null); } - [HttpPost( - @"/(?[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})/updateMotto")] + [HttpPost(pattern: + @"/(?[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})/updateMotto", + isProtected: true)] public GetClanResponse UpdateClanMotto([UrlParam("id")] Guid clanId, [RequestBody] UpdateClanMottoBody? body) { - Plugin.Logger?.LogInfo($"Clan GUID is {clanId.ToString()}"); + ApiPlugin.Logger?.LogInfo($"Clan GUID is {clanId.ToString()}"); if (body.HasValue) { var clan = ClanUtils.GetClanWithEntityById(clanId); @@ -86,7 +88,7 @@ public GetClanResponse UpdateClanMotto([UrlParam("id")] Guid clanId, [RequestBod } var clanTeam = clan.Value.ClanTeam; - Plugin.Logger?.LogInfo( + ApiPlugin.Logger?.LogInfo( $"Updating clan motto from '{clanTeam.Motto.ToString()}' to '{body.Value.Motto}'"); clanTeam.Motto = new FixedString64(body.Value.Motto); ServerWorld.EntityManager.SetComponentData(clan.Value.ClanEntity, clanTeam); diff --git a/endpoints/players/ApiPlayer.cs b/endpoints/players/ApiPlayer.cs index 69fdaeb..3868df2 100644 --- a/endpoints/players/ApiPlayer.cs +++ b/endpoints/players/ApiPlayer.cs @@ -6,27 +6,47 @@ namespace VRisingServerApiPlugin.endpoints.players; public class ApiUserDetails { - private long TimeLastConnected { get; set; } - private bool IsBot { get; set; } - private bool IsAdmin { get; set; } - private bool IsConnected { get; set; } + public int UserIndex { get; set; } + public long TimeLastConnected { get; set; } + public bool IsBot { get; set; } + public bool IsAdmin { get; set; } + public bool IsConnected { get; set; } + + protected ApiUserDetails(int userIndex, long timeLastConnected, bool isBot, bool isAdmin, bool isConnected) + { + UserIndex = userIndex; + TimeLastConnected = timeLastConnected; + IsBot = isBot; + IsAdmin = isAdmin; + IsConnected = isConnected; + } } -public class ApiPlayer +public class ApiPlayer : ApiUserDetails { - public int UserIndex { get; set; } - public string CharacterName { get; set; } + public string? CharacterName { get; set; } public string SteamID { get; set; } public string? ClanId { get; set; } public int GearLevel { get; set; } + public float? LastValidPositionX { get; set; } + public float? LastValidPositionY { get; set; } - public ApiPlayer(int userIndex, string characterName, string steamID, string? clanId, int gearLevel) + public bool HasLocalCharacter { get; set; } + + public bool ShouldCreateCharacter { get; set; } + + public ApiPlayer(int userIndex, string? characterName, string steamID, string? clanId, int gearLevel, + float? lastValidPositionX, float? lastValidPositionY, long timeLastConnected, bool isBot, bool isAdmin, + bool isConnected) : base(userIndex, timeLastConnected, isBot, isAdmin, isConnected) { - UserIndex = userIndex; CharacterName = characterName; SteamID = steamID; ClanId = clanId; GearLevel = gearLevel; + LastValidPositionX = lastValidPositionX; + LastValidPositionY = lastValidPositionY; + HasLocalCharacter = characterName != null; + ShouldCreateCharacter = !HasLocalCharacter; } } @@ -35,7 +55,15 @@ public class ApiPlayerDetails : ApiPlayer public object Stats { get; set; } public List Gears { get; set; } - public ApiPlayerDetails(int userIndex, string characterName, string steamID, string? clanId, int gearLevel, object stats, List gears) : base(userIndex, characterName, steamID, clanId, gearLevel) + public ApiPlayerDetails( + int userIndex, string? characterName, string steamID, string? clanId, int gearLevel, + float? lastValidPositionX, float? lastValidPositionY, + long timeLastConnected, bool isBot, bool isAdmin, bool isConnected, + object stats, + List gears + ) + : base(userIndex, characterName, steamID, clanId, gearLevel, lastValidPositionX, lastValidPositionY, + timeLastConnected, isBot, isAdmin, isConnected) { Stats = stats; Gears = gears; @@ -58,8 +86,8 @@ public ApiPlayerStats(float armorLevel, float weaponLevel, float spellLevel) public class EmptyGear { - public bool IsEmpty {get; set;} - + public bool IsEmpty { get; set; } + public EquipmentSlot Slot { get; set; } protected EmptyGear(bool isEmpty, EquipmentSlot slot) @@ -79,7 +107,7 @@ public class Gear : EmptyGear { public string Name { get; set; } public string Description { get; set; } - + public string PrefabName { get; set; } public Gear(string name, string prefabName, string description, EquipmentSlot slot) : base(false, slot) diff --git a/endpoints/players/PlayerUtils.cs b/endpoints/players/PlayerUtils.cs index b48b9c6..0a467ce 100644 --- a/endpoints/players/PlayerUtils.cs +++ b/endpoints/players/PlayerUtils.cs @@ -8,19 +8,6 @@ namespace VRisingServerApiPlugin.endpoints.players; public static class PlayerUtils { - private static ApiClan? ResolveClan(ServerWorld.Player player) - { - var clanEntity = player.User.ClanEntity._Entity; - ClanTeam? clan = null; - - if (ServerWorld.EntityManager.HasComponent(clanEntity)) - { - clan = ServerWorld.EntityManager.GetComponentData(clanEntity); - } - - return clan.HasValue ? ClanUtils.Convert(clan.Value) : null; - } - private static string? ResolveClanId(ServerWorld.Player player) { var clanEntity = player.User.ClanEntity._Entity; @@ -33,15 +20,21 @@ public static class PlayerUtils return clan.HasValue ? ClanUtils.Convert(clan.Value).Id : null; } - + public static ApiPlayer Convert(ServerWorld.Player player) { return new ApiPlayer( player.User.Index, - player.Character.Name.ToString(), + player.Character?.Name.ToString(), player.User.PlatformId.ToString(), ResolveClanId(player), - (int) ServerWorld.EntityManager.GetComponentData(player.CharacterEntity).GetFullLevel() + (int)ServerWorld.EntityManager.GetComponentData(player.CharacterEntity).GetFullLevel(), + player.Character?.LastValidPosition.x, + player.Character?.LastValidPosition.y, + player.User.TimeLastConnected, + player.User.IsBot, + player.User.IsAdmin, + player.User.IsConnected ); } @@ -56,22 +49,25 @@ private static EmptyGear ResolveItem(PrefabGUID guid, Entity entity, EquipmentSl var data = ServerWorld.GameDataSystem.ManagedDataRegistry.GetOrDefault(guid); if (data == null) return new EmptyGear(slot); - + var name = data.Name.GetGuid().ToGuid().ToString(); var description = data.Description.Key.GetGuid().ToGuid().ToString(); return new Gear(name, data.PrefabName, description, slot); } - private static List resolveGear(Equipment equipment) + private static List ResolveGear(Equipment equipment) { return new List { ResolveItem(equipment.ArmorChestSlotId, equipment.ArmorChestSlotEntity._Entity, EquipmentSlot.ARMOR_CHEST), ResolveItem(equipment.CloakSlotId, equipment.CloakSlotEntity._Entity, EquipmentSlot.CLOACK), - ResolveItem(equipment.ArmorHeadgearSlotId, equipment.ArmorHeadgearSlotEntity._Entity, EquipmentSlot.ARMOR_HEADGEAR), - ResolveItem(equipment.ArmorFootgearSlotId, equipment.ArmorFootgearSlotEntity._Entity, EquipmentSlot.ARMOR_FOOTGEAR), + ResolveItem(equipment.ArmorHeadgearSlotId, equipment.ArmorHeadgearSlotEntity._Entity, + EquipmentSlot.ARMOR_HEADGEAR), + ResolveItem(equipment.ArmorFootgearSlotId, equipment.ArmorFootgearSlotEntity._Entity, + EquipmentSlot.ARMOR_FOOTGEAR), ResolveItem(equipment.ArmorLegsSlotId, equipment.ArmorLegsSlotEntity._Entity, EquipmentSlot.ARMOR_LEGS), - ResolveItem(equipment.ArmorGlovesSlotId, equipment.ArmorGlovesSlotEntity._Entity, EquipmentSlot.ARMOR_GLOVES), + ResolveItem(equipment.ArmorGlovesSlotId, equipment.ArmorGlovesSlotEntity._Entity, + EquipmentSlot.ARMOR_GLOVES), ResolveItem(equipment.GrimoireSlotId, equipment.GrimoireSlotEntity._Entity, EquipmentSlot.GRIMOIRE), ResolveItem(equipment.WeaponSlotId, equipment.WeaponSlotEntity._Entity, EquipmentSlot.WEAPON) }; @@ -80,15 +76,21 @@ private static List resolveGear(Equipment equipment) public static ApiPlayerDetails ConvertDetails(ServerWorld.Player player) { var equipment = ServerWorld.EntityManager.GetComponentData(player.CharacterEntity); - + return new ApiPlayerDetails( player.User.Index, - player.Character.Name.ToString(), + player.Character?.Name.ToString(), player.User.PlatformId.ToString(), ResolveClanId(player), - (int) equipment.GetFullLevel(), + (int)equipment.GetFullLevel(), + player.Character?.LastValidPosition.x, + player.Character?.LastValidPosition.y, + player.User.TimeLastConnected, + player.User.IsBot, + player.User.IsAdmin, + player.User.IsConnected, ResolveStats(equipment), - resolveGear(equipment) - ); + ResolveGear(equipment) + ); } } \ No newline at end of file diff --git a/endpoints/players/PlayersEndpoints.cs b/endpoints/players/PlayersEndpoints.cs index 3ca279b..712fc7c 100644 --- a/endpoints/players/PlayersEndpoints.cs +++ b/endpoints/players/PlayersEndpoints.cs @@ -2,6 +2,7 @@ using VRisingServerApiPlugin.attributes; using VRisingServerApiPlugin.attributes.methods; using VRisingServerApiPlugin.attributes.parameters; +using VRisingServerApiPlugin.http; namespace VRisingServerApiPlugin.endpoints.players; @@ -31,8 +32,12 @@ public PlayerListApiResponse GetAllPlayers() public PlayerApiResponse GetPlayerDetails([UrlParam("id")] int userIndex) { var player = ServerWorld.GetPlayer(userIndex); - return player.HasValue - ? new PlayerApiResponse(PlayerUtils.ConvertDetails(player.Value)) - : new PlayerApiResponse(); + + if (!player.HasValue) + { + throw new HttpException(404, $"Player with id {userIndex} not found."); + } + + return new PlayerApiResponse(PlayerUtils.ConvertDetails(player.Value)); } } \ No newline at end of file diff --git a/http/BodyParserUtils.cs b/http/BodyParserUtils.cs index 6a8c5e5..ec04bfa 100644 --- a/http/BodyParserUtils.cs +++ b/http/BodyParserUtils.cs @@ -20,19 +20,19 @@ public static class BodyParserUtils { if (body == null) { - Plugin.Logger?.LogWarning($"Provided body is null, cannot deserialize."); + ApiPlugin.Logger?.LogWarning($"Provided body is null, cannot deserialize."); return null; } try { var rawText = ((JsonDocument)body).RootElement.GetRawText(); - Plugin.Logger?.LogDebug($"Deserializing to {typeof(T)} from body {rawText}"); + ApiPlugin.Logger?.LogDebug($"Deserializing to {typeof(T)} from body {rawText}"); return JsonSerializer.Deserialize(rawText, SerializerOptions); } catch (JsonException ex) { - Plugin.Logger?.LogWarning($"Exception Deserializing from body : {ex.Message}"); + ApiPlugin.Logger?.LogWarning($"Exception Deserializing from body : {ex.Message}"); return null; } } @@ -47,7 +47,7 @@ public static class BodyParserUtils } catch (JsonException ex) { - Plugin.Logger?.LogWarning($"Exception Deserializing from body : {ex.Message}"); + ApiPlugin.Logger?.LogWarning($"Exception Deserializing from body : {ex.Message}"); return null; } } diff --git a/http/HttpException.cs b/http/HttpException.cs index ee6dcdb..2f16ffc 100644 --- a/http/HttpException.cs +++ b/http/HttpException.cs @@ -20,4 +20,11 @@ public HttpException(int status, string message, Exception inner) : base(message { Status = status; } +} + +public class HttpUnAuthorizeException : HttpException +{ + public HttpUnAuthorizeException() : base(401, "You are not authorized to access this endpoint") + { + } } \ No newline at end of file diff --git a/http/HttpRequest.cs b/http/HttpRequest.cs index 14be7b7..258d414 100644 --- a/http/HttpRequest.cs +++ b/http/HttpRequest.cs @@ -8,5 +8,6 @@ public readonly record struct HttpRequest( string url, string contentType, Dictionary urlParams, - Dictionary queryParams + Dictionary queryParams, + HttpRequestParser.AuthenticatedUser? user = null ); \ No newline at end of file diff --git a/http/HttpRequestParser.cs b/http/HttpRequestParser.cs index a0b7be5..3ff9d62 100644 --- a/http/HttpRequestParser.cs +++ b/http/HttpRequestParser.cs @@ -1,16 +1,25 @@ #nullable enable using System.Linq; -using System.Net.Mime; -using System.Text.Json; using Il2CppSystem.Net; +using Il2CppSystem.Security.Principal; using VRisingServerApiPlugin.command; namespace VRisingServerApiPlugin.http; public static class HttpRequestParser { - public static HttpRequest ParseHttpRequest(HttpListenerRequest request, Command command) + public static HttpRequest ParseHttpRequest(HttpListenerContext context, Command command) { + var request = context.request; + + var authenticatedUser = ParseAuthenticatedUser(context); + + if (authenticatedUser.HasValue) + { + ApiPlugin.Logger?.LogInfo( + $"Authenticated user with name {authenticatedUser?.Username} and IsAuthorized {authenticatedUser?.IsAuthorized}"); + } + var body = ""; var contentType = request.Headers["Content-Type"]; @@ -22,7 +31,8 @@ public static HttpRequest ParseHttpRequest(HttpListenerRequest request, Command .ToDictionary(kvp => kvp.Key, kvp => kvp.Value), url: request.raw_url, body: body, - contentType: contentType + contentType: contentType, + user: authenticatedUser ); var inputStream = new Il2CppSystem.IO.StreamReader(request.InputStream); @@ -35,7 +45,34 @@ public static HttpRequest ParseHttpRequest(HttpListenerRequest request, Command .ToDictionary(kvp => kvp.Key, kvp => kvp.Value), url: request.raw_url, body: body, - contentType: contentType + contentType: contentType, + user: authenticatedUser ); } + + private static AuthenticatedUser? ParseAuthenticatedUser(HttpListenerContext context) + { + context.ParseAuthentication(AuthenticationSchemes.Basic); + + if (context.user == null) return null; + + var principal = context.user.TryCast(); + var identity = principal?.m_identity.TryCast(); + + if (identity == null) return null; + + var username = identity.Name; + var password = identity.password; + + var isAuthorized = ApiPlugin.Instance.CheckAuthenticationOfUser(username, password); + + return new AuthenticatedUser(Username: identity.Name, Password: identity.password, + IsAuthorized: isAuthorized); + } + + public readonly record struct AuthenticatedUser( + string Username, + string Password, + bool IsAuthorized + ); } \ No newline at end of file diff --git a/http/QueryParamUtils.cs b/http/QueryParamUtils.cs index 737fcc6..976a6ba 100644 --- a/http/QueryParamUtils.cs +++ b/http/QueryParamUtils.cs @@ -14,14 +14,13 @@ static QueryParamUtils() } public static IEnumerable> ParseQueryString(string url) { - Plugin.Logger?.LogDebug($"Parsing query params from {url}"); + ApiPlugin.Logger?.LogDebug($"Parsing query params from {url}"); var matches = queryStringRegex.Matches(url); for (var i = 0; i < matches.Count; i++) { var match = matches[i]; var name = match.Groups["name"].Value; var value = match.Groups["value"].Value; - Plugin.Logger?.LogDebug($"Found Match {name}={value}"); yield return new KeyValuePair(name, value); } } @@ -29,7 +28,7 @@ public static IEnumerable> ParseQueryString(string public static IEnumerable> ParseQueryString(string url, string pattern) { var regex = new Regex(pattern); - Plugin.Logger?.LogDebug($"Parsing query params from {url} with Pattern {pattern}"); + ApiPlugin.Logger?.LogDebug($"Parsing query params from {url} with Pattern {pattern}"); var matches = regex.Matches(url); for (var i = 0; i < matches.Count; i++) { @@ -40,7 +39,6 @@ public static IEnumerable> ParseQueryString(string var group = match.Groups[j]; var name = group.Name; var value = group.Value; - Plugin.Logger?.LogDebug($"Found Match {name}={value}"); yield return new KeyValuePair(name, value); } }