Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework #6

Merged
merged 1 commit into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:

- name: Push to Nuget
if: ${{ !env.ACT && github.event_name == 'push' }}
run: dotnet nuget push ./nuget/*.nupkg -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json
run: dotnet nuget push ./nuget/*.nupkg -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate

- name: GH Release
uses: softprops/action-gh-release@v1
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea
.vs
bin
obj
obj
Folder.DotSettings.user
129 changes: 74 additions & 55 deletions command/CommandRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using VRisingServerApiPlugin.attributes;
using VRisingServerApiPlugin.attributes.methods;
using VRisingServerApiPlugin.attributes.parameters;
using VRisingServerApiPlugin.http;
using Guid = Il2CppSystem.Guid;

namespace VRisingServerApiPlugin.command;

Expand Down Expand Up @@ -48,59 +46,96 @@ private static void Register(Type type)
}
}

private static void RegisterMethod(object? container, MethodInfo method,
private static void RegisterMethod(object? container, MethodBase method,
HttpHandlerAttribute httpHandlerAttribute)
{
if (method.GetCustomAttribute(typeof(HttpAttribute), true) is not HttpAttribute httpAttribute) return;

var pattern = $"^{httpHandlerAttribute.BasePath}{httpAttribute.Pattern}$";
var paramDictionary = ParseParameters(method);
var pattern =
$"^{httpHandlerAttribute.BasePath}{ParamUtils.ConvertUrlPattern(httpAttribute.Pattern, paramDictionary)}$";

var isProtected = httpAttribute.Protected || httpHandlerAttribute.AllRouteProtected;

var command = new Command(
pattern: pattern,
method: httpAttribute.Method,
commandHandler: GenerateHandler(container, method),
commandHandler: GenerateHandler(container, method, paramDictionary),
isProtected: isProtected
);

Commands.Add(command);
}

private static Func<HttpRequest, object?> GenerateHandler(object? container, MethodBase method)
private static Dictionary<string, HttpParameter> ParseParameters(MethodBase methodBase)
{
return request =>
{
var args = new List<object?>();
var parameters = method.GetParameters();
var result = new Dictionary<string, HttpParameter>();

foreach (var parameter in parameters)
foreach (var parameter in methodBase.GetParameters())
{
var paramAttr = parameter.GetCustomAttribute(typeof(HttpParamAttribute), true);
var httpParameter = new HttpParameter
{
var paramAttr = parameter.GetCustomAttribute(typeof(HttpParamAttribute), true);
ParameterType = parameter.ParameterType,
Attribute = paramAttr as HttpAttribute
};

switch (paramAttr)
{
case null:
throw new HttpException(500, "Param attribute cannot be null !");
case RequestBody:
args.Add(
BodyParserUtils.Deserialize(request.contentType, parameter.ParameterType, request.body));
break;
case UrlParam param:
args.Add(ParseUrlOrQueryArg(request.urlParams, param.Name, parameter));
break;
case QueryParam param:
args.Add(ParseUrlOrQueryArg(request.queryParams, param.Name, parameter));
break;
default:
throw new HttpException(500,
"No default case should happen while checking the parameter attrs");
}
switch (paramAttr)
{
case null:
throw new HttpException(500, "Param attribute cannot be null !");
case RequestBody:
httpParameter.Name = "RequestBody";
httpParameter.Parse = (request) =>
BodyParserUtils.Deserialize(request.contentType, parameter.ParameterType, request.body);
break;
case UrlParam param:
httpParameter.Name = param.Name;
httpParameter.Parse = (request) =>
ParamUtils.ParseUrlOrQueryArg(request.urlParams, param.Name, parameter);
break;
case QueryParam param:
httpParameter.Name = param.Name;
httpParameter.Parse = (request) =>
ParamUtils.ParseUrlOrQueryArg(request.queryParams, param.Name, parameter);
break;
default:
throw new HttpException(500,
"No default case should happen while checking the parameter attrs");
}

if (args.Count != parameters.Length)
result.Add(httpParameter.Name, httpParameter);
}

return result;
}

private static Func<HttpRequest, object?> GenerateHandler(object? container, MethodBase method,
Dictionary<string, HttpParameter> httpParameters)
{
return request =>
{
var args = method.GetParameters()
.Where(info => info.GetCustomAttribute(typeof(HttpParamAttribute), true) != null)
.Select(parameterInfo => parameterInfo.GetCustomAttribute(typeof(HttpParamAttribute), true))
.Select(attribute =>
{
return attribute switch
{
RequestBody => "RequestBody",
UrlParam param => param.Name,
QueryParam param => param.Name,
_ => throw new HttpException(500, "Invalid parameters definition !")
};
})
.Where(httpParameters.ContainsKey)
.Select(name => httpParameters[name])
.Select(parameter => parameter.Parse(request))
.ToList();

if (args.Count != httpParameters.Count)
{
ApiPlugin.Logger?.LogInfo($"parameters are {parameters} and parsed args are {args}");
ApiPlugin.Logger?.LogInfo($"parameters are {httpParameters} and parsed args are {args}");
throw new HttpException(400, "Invalid parameters !");
}

Expand All @@ -119,31 +154,15 @@ private static void RegisterMethod(object? container, MethodInfo method,
};
}

private static object? ParseUrlOrQueryArg(IReadOnlyDictionary<string, string> dictionary, string name,
ParameterInfo parameter)
{
if (!dictionary.TryGetValue(name, out var value)) return parameter.DefaultValue;

object? result;

if (parameter.ParameterType == typeof(System.Guid))
{
result = System.Guid.Parse(value);
}
else if (parameter.ParameterType == typeof(Guid))
{
result = Guid.Parse(value);
}
else
{
result = Convert.ChangeType(value, parameter.ParameterType);
}

return result;
}

private static IEnumerable<Type> ListAllHttpHandlerTypes(Assembly assembly)
{
return assembly.GetTypes().Where(type => type.IsDefined(typeof(HttpHandlerAttribute)));
}

public record struct HttpParameter(
string Name,
HttpAttribute? Attribute,
Type ParameterType,
Func<HttpRequest, object?> Parse
);
}
8 changes: 4 additions & 4 deletions endpoints/clans/ClansEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ public ListClanResponse GetAllClans()
);
}

[HttpGet(@"/(?<id>[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})")]
[HttpGet(@"/{id}")]
public GetClanResponse GetClanById([UrlParam("id")] Guid clanId)
{
var clan = ClanUtils.GetClanById(clanId);
return new GetClanResponse(clan.HasValue ? ClanUtils.Convert(clan.Value) : null);
}

[HttpGet(@"/(?<id>[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})/players")]
[HttpGet(@"/{id}/players")]
public ListClanPlayersResponse GetClanPlayers([UrlParam("id")] Guid clanId)
{
ClanTeam? clan = ServerWorld.GetAllClans()
Expand All @@ -42,7 +42,7 @@ public ListClanPlayersResponse GetClanPlayers([UrlParam("id")] Guid clanId)
}

[HttpPost(pattern:
@"/(?<id>[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",
@"/{id}/updateName",
isProtected: true)]
public GetClanResponse UpdateClanName([UrlParam("id")] Guid clanId, [RequestBody] UpdateClanNameBody? body)
{
Expand Down Expand Up @@ -71,7 +71,7 @@ public GetClanResponse UpdateClanName([UrlParam("id")] Guid clanId, [RequestBody
}

[HttpPost(pattern:
@"/(?<id>[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",
@"/{id}/updateMotto",
isProtected: true)]
public GetClanResponse UpdateClanMotto([UrlParam("id")] Guid clanId, [RequestBody] UpdateClanMottoBody? body)
{
Expand Down
2 changes: 1 addition & 1 deletion endpoints/players/PlayersEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public PlayerListApiResponse GetAllPlayers()
return new PlayerListApiResponse(players);
}

[HttpGet("/(?<id>[0-9]*)")]
[HttpGet("/{id}")]
public PlayerApiResponse GetPlayerDetails([UrlParam("id")] int userIndex)
{
var player = ServerWorld.GetPlayer(userIndex);
Expand Down
8 changes: 4 additions & 4 deletions http/HttpRequestParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ public static HttpRequest ParseHttpRequest(HttpListenerContext context, Command

if (command.Method == "GET")
return new HttpRequest(
queryParams: QueryParamUtils.ParseQueryString(request.url.Query)
queryParams: ParamUtils.ParseQueryString(request.url.Query)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
urlParams: QueryParamUtils.ParseQueryString(request.url.LocalPath, command.Pattern)
urlParams: ParamUtils.ParseQueryString(request.url.LocalPath, command.Pattern)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
url: request.raw_url,
body: body,
Expand All @@ -40,9 +40,9 @@ public static HttpRequest ParseHttpRequest(HttpListenerContext context, Command
body = inputStream.ReadToEnd();

return new HttpRequest(
queryParams: QueryParamUtils.ParseQueryString(request.url.Query)
queryParams: ParamUtils.ParseQueryString(request.url.Query)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
urlParams: QueryParamUtils.ParseQueryString(request.url.LocalPath, command.Pattern)
urlParams: ParamUtils.ParseQueryString(request.url.LocalPath, command.Pattern)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
url: request.raw_url,
body: body,
Expand Down
115 changes: 115 additions & 0 deletions http/ParamUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
using VRisingServerApiPlugin.command;
using Guid = Il2CppSystem.Guid;

namespace VRisingServerApiPlugin.http;

public static class ParamUtils
{
private static readonly Regex QueryStringRegex = new Regex(@"[\?&](?<name>[^&=]+)=(?<value>[^&=]+)");

private static readonly IDictionary<Type, string> TypePatternDictionary = new Dictionary<Type, string>()
{
{
typeof(Guid),
@"(?<$1>[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})"
},
{
typeof(System.Guid),
@"(?<$1>[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})"
},
{ typeof(int), @"(?<$1>[0-9]*)" },
{ typeof(long), @"(?<$1>[0-9]*)" },
{ typeof(string), @"(?<$1>\w*)" },
{ typeof(bool), @"(?<$1>true|false)" }
};

public static IEnumerable<KeyValuePair<string, string>> ParseQueryString(string 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;
yield return new KeyValuePair<string, string>(name, value);
}
}

public static IEnumerable<KeyValuePair<string, string>> ParseQueryString(string url, string pattern)
{
var regex = new Regex(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++)
{
var match = matches[i];
if (match.Groups.Count <= 1) continue;
for (var j = 1; j < match.Groups.Count; j++)
{
var group = match.Groups[j];
var name = group.Name;
var value = group.Value;
yield return new KeyValuePair<string, string>(name, value);
}
}
}

public static object? ParseUrlOrQueryArg(IReadOnlyDictionary<string, string> dictionary, string name,
ParameterInfo parameter)
{
if (!dictionary.TryGetValue(name, out var value)) return parameter.DefaultValue;

object? result;

if (parameter.ParameterType == typeof(Guid))
{
result = Guid.Parse(value);
}
else if (parameter.ParameterType == typeof(System.Guid))
{
result = System.Guid.Parse(value);
}
else
{
result = Convert.ChangeType(value, parameter.ParameterType);
}

return result;
}

public static string ConvertUrlPattern(string urlPattern,
Dictionary<string, CommandRegistry.HttpParameter> parameters)
{
var urlParamPattern = new Regex(@"{(\w+)}");
var matches = urlParamPattern.Matches(urlPattern);
var result = urlPattern;

foreach (Match match in matches)
{
if (!match.Success) continue;

if (match.Groups.Count == 0) continue;

foreach (Group group in match.Groups)
{
if (!group.Success) continue;

var paramName = group.Value;

if (!parameters.TryGetValue(paramName, out var paramType)) continue;

if (!TypePatternDictionary.TryGetValue(paramType.ParameterType, out var pattern)) continue;

result = Regex.Replace(result, "{(" + paramName + ")}", pattern);
}
}

return result;
}
}
Loading