mirror of
https://github.com/Sanae6/SmoOnlineServer.git
synced 2024-11-22 03:05:16 +00:00
JSON API
This commit is contained in:
parent
47505dbdd5
commit
87cec91941
14 changed files with 619 additions and 2 deletions
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
/Server/**/.gitignore
|
||||
/Server/**/*.env
|
||||
/Server/**/*.md
|
||||
/Server/**/*.sh
|
1
Server/JsonApi/.gitignore
vendored
Normal file
1
Server/JsonApi/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/test.env
|
47
Server/JsonApi/ApiPacket.cs
Normal file
47
Server/JsonApi/ApiPacket.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
using System.Net.Sockets;
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using Shared;
|
||||
|
||||
namespace Server.JsonApi;
|
||||
|
||||
public class ApiPacket {
|
||||
public const ushort MAX_PACKET_SIZE = 512; // in bytes (including 20 byte header)
|
||||
|
||||
|
||||
public ApiRequest? API_JSON_REQUEST { get; set; }
|
||||
|
||||
|
||||
public static async Task<ApiPacket?> Read(Context ctx, string header) {
|
||||
string reqStr = header + await ApiPacket.GetRequestStr(ctx);
|
||||
|
||||
ApiPacket? p = null;
|
||||
try { p = JsonSerializer.Deserialize<ApiPacket>(reqStr); }
|
||||
catch {
|
||||
JsonApi.Logger.Warn($"Invalid packet deserialize from {ctx.socket.RemoteEndPoint}: {reqStr}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (p == null) {
|
||||
JsonApi.Logger.Warn($"Invalid packet from {ctx.socket.RemoteEndPoint}: {reqStr}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (p.API_JSON_REQUEST == null) {
|
||||
JsonApi.Logger.Warn($"Invalid request from {ctx.socket.RemoteEndPoint}: {reqStr}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
private static async Task<string> GetRequestStr(Context ctx) {
|
||||
byte[] buffer = new byte[ApiPacket.MAX_PACKET_SIZE - Constants.HeaderSize];
|
||||
int size = await ctx.socket.ReceiveAsync(buffer, SocketFlags.None);
|
||||
return Encoding.UTF8.GetString(buffer, 0, size);
|
||||
}
|
||||
}
|
||||
|
68
Server/JsonApi/ApiRequest.cs
Normal file
68
Server/JsonApi/ApiRequest.cs
Normal file
|
@ -0,0 +1,68 @@
|
|||
namespace Server.JsonApi;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using TypesDictionary = Dictionary<string, Func<Context, Task<bool>>>;
|
||||
|
||||
public class ApiRequest {
|
||||
public string? Token { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public JsonNode? Data { get; set; }
|
||||
|
||||
|
||||
private static TypesDictionary Types = new TypesDictionary() {
|
||||
["Status"] = async (Context ctx) => await ApiRequestStatus.Send(ctx),
|
||||
["Command"] = async (Context ctx) => await ApiRequestCommand.Send(ctx),
|
||||
["Permissions"] = async (Context ctx) => await ApiRequestPermissions.Send(ctx),
|
||||
};
|
||||
|
||||
|
||||
public dynamic? GetData() {
|
||||
if (this.Data == null) { return null; }
|
||||
if (this.Data is JsonArray) { return this.Data.AsArray(); } // TODO: better way?
|
||||
if (this.Data is JsonObject) { return this.Data.AsObject(); } // TODO: better way?
|
||||
if (this.Data is JsonValue) {
|
||||
JsonElement val = this.Data.GetValue<JsonElement>();
|
||||
JsonValueKind kind = val.ValueKind;
|
||||
if (kind == JsonValueKind.String) { return val.GetString(); }
|
||||
if (kind == JsonValueKind.Number) { return val.GetInt64(); } // TODO: floats
|
||||
if (kind == JsonValueKind.False) { return false; }
|
||||
if (kind == JsonValueKind.True) { return true; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> Process(Context ctx) {
|
||||
if (this.Type != null) {
|
||||
return await ApiRequest.Types[this.Type](ctx);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public bool IsValid(Context ctx) {
|
||||
if (this.Token == null) {
|
||||
JsonApi.Logger.Warn($"Invalid request missing Token from {ctx.socket.RemoteEndPoint}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.Type == null) {
|
||||
JsonApi.Logger.Warn($"Invalid request missing Type from {ctx.socket.RemoteEndPoint}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ApiRequest.Types.ContainsKey(this.Type)) {
|
||||
JsonApi.Logger.Warn($"Invalid Type \"{this.Type}\" from {ctx.socket.RemoteEndPoint}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Settings.Instance.JsonApi.Tokens.ContainsKey(this.Token)) {
|
||||
JsonApi.Logger.Warn($"Invalid Token from {ctx.socket.RemoteEndPoint}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
74
Server/JsonApi/ApiRequestCommand.cs
Normal file
74
Server/JsonApi/ApiRequestCommand.cs
Normal file
|
@ -0,0 +1,74 @@
|
|||
namespace Server.JsonApi;
|
||||
|
||||
public static class ApiRequestCommand {
|
||||
public static async Task<bool> Send(Context ctx) {
|
||||
if (!ctx.HasPermission("Commands")) {
|
||||
await Response.Send(ctx, "Error: Missing Commands permission.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ApiRequestCommand.IsValid(ctx)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
string input = ctx.request!.GetData()!;
|
||||
string command = input.Split(" ")[0];
|
||||
|
||||
// help doesn't need permissions and is invidualized to the token
|
||||
if (command == "help") {
|
||||
List<string> commands = new List<string>();
|
||||
commands.Add("help");
|
||||
commands.AddRange(
|
||||
ctx.Permissions
|
||||
.Where(str => str.StartsWith("Commands/"))
|
||||
.Select(str => str.Substring(9))
|
||||
.Where(cmd => CommandHandler.Handlers.ContainsKey(cmd))
|
||||
);
|
||||
string commandsStr = string.Join(", ", commands);
|
||||
|
||||
await Response.Send(ctx, $"Valid commands: {commandsStr}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// no permissions
|
||||
if (! ctx.HasPermission($"Commands/{command}")) {
|
||||
await Response.Send(ctx, $"Error: Missing Commands/{command} permission.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// execute command
|
||||
JsonApi.Logger.Info($"[Commands] " + input);
|
||||
await Response.Send(ctx, CommandHandler.GetResult(input));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static bool IsValid(Context ctx) {
|
||||
var command = ctx.request!.GetData();
|
||||
|
||||
if (command == null) {
|
||||
JsonApi.Logger.Warn($"[Commands] Invalid request Data is \"null\" or missing and not a \"System.String\" from {ctx.socket.RemoteEndPoint}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (command.GetType() != typeof(string)) {
|
||||
JsonApi.Logger.Warn($"[Commands] Invalid request Data is \"{command.GetType()}\" and not a \"System.String\" from {ctx.socket.RemoteEndPoint}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private class Response {
|
||||
public string[]? Output { get; set; }
|
||||
|
||||
|
||||
public static async Task Send(Context ctx, CommandHandler.Response response)
|
||||
{
|
||||
Response resp = new Response();
|
||||
resp.Output = response.ReturnStrings;
|
||||
await ctx.Send(resp);
|
||||
}
|
||||
}
|
||||
}
|
21
Server/JsonApi/ApiRequestPermissions.cs
Normal file
21
Server/JsonApi/ApiRequestPermissions.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
namespace Server.JsonApi;
|
||||
|
||||
public static class ApiRequestPermissions {
|
||||
public static async Task<bool> Send(Context ctx) {
|
||||
await Response.Send(ctx);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private class Response {
|
||||
public string[]? Permissions { get; set; }
|
||||
|
||||
|
||||
public static async Task Send(Context ctx)
|
||||
{
|
||||
Response resp = new Response();
|
||||
resp.Permissions = ctx.Permissions.ToArray();
|
||||
await ctx.Send(resp);
|
||||
}
|
||||
}
|
||||
}
|
125
Server/JsonApi/ApiRequestStatus.cs
Normal file
125
Server/JsonApi/ApiRequestStatus.cs
Normal file
|
@ -0,0 +1,125 @@
|
|||
using Shared.Packet.Packets;
|
||||
using System.Dynamic;
|
||||
using System.Net;
|
||||
|
||||
namespace Server.JsonApi;
|
||||
|
||||
using Mutators = Dictionary<string, Action<dynamic, Client>>;
|
||||
|
||||
public static class ApiRequestStatus {
|
||||
public static async Task<bool> Send(Context ctx) {
|
||||
StatusResponse resp = new StatusResponse {
|
||||
Settings = ApiRequestStatus.GetSettings(ctx),
|
||||
Players = Player.GetPlayers(ctx),
|
||||
};
|
||||
await ctx.Send(resp);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static dynamic GetSettings(Context ctx)
|
||||
{
|
||||
// output object
|
||||
dynamic settings = new ExpandoObject();
|
||||
|
||||
// all permissions for Settings
|
||||
var allowedSettings = ctx.Permissions
|
||||
.Where(str => str.StartsWith("Status/Settings/"))
|
||||
.Select(str => str.Substring(16))
|
||||
;
|
||||
|
||||
// copy all allowed Settings
|
||||
foreach (string allowedSetting in allowedSettings) {
|
||||
string lastKey = "";
|
||||
dynamic? next = settings;
|
||||
dynamic input = Settings.Instance;
|
||||
IDictionary<string, object> output = settings;
|
||||
|
||||
// recursively go down the path
|
||||
foreach (string key in allowedSetting.Split("/")) {
|
||||
lastKey = key;
|
||||
|
||||
if (next == null) { break; }
|
||||
output = (IDictionary<string, object>) next;
|
||||
|
||||
// create the sublayer
|
||||
if (!output.ContainsKey(key)) { output.Add(key, new ExpandoObject()); }
|
||||
|
||||
// traverse down the output object
|
||||
output.TryGetValue(key, out next);
|
||||
|
||||
// traverse down the Settings object
|
||||
input = input.GetType().GetProperty(key).GetValue(input, null);
|
||||
}
|
||||
|
||||
if (lastKey != "") {
|
||||
// copy key with the actual value
|
||||
output.Remove(lastKey);
|
||||
output.Add(lastKey, input);
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
|
||||
private class StatusResponse {
|
||||
public dynamic? Settings { get; set; }
|
||||
public dynamic[]? Players { get; set; }
|
||||
}
|
||||
|
||||
|
||||
private static class Player {
|
||||
private static Mutators Mutators = new Mutators {
|
||||
["Status/Players/Name"] = (dynamic p, Client c) => p.Name = c.Name,
|
||||
["Status/Players/Stage"] = (dynamic p, Client c) => p.Stage = Player.GetGamePacket(c)?.Stage ?? null,
|
||||
["Status/Players/Scenario"] = (dynamic p, Client c) => p.Scenario = Player.GetGamePacket(c)?.ScenarioNum ?? null,
|
||||
["Status/Players/Costume"] = (dynamic p, Client c) => p.Costume = Costume.FromClient(c),
|
||||
["Status/Players/IPv4"] = (dynamic p, Client c) => p.IPv4 = (c.Socket?.RemoteEndPoint as IPEndPoint)?.Address.ToString(),
|
||||
};
|
||||
|
||||
|
||||
public static dynamic[]? GetPlayers(Context ctx) {
|
||||
if (!ctx.HasPermission("Status/Players")) { return null; }
|
||||
return ctx.server.ClientsConnected.Select((Client c) => Player.FromClient(ctx, c)).ToArray();
|
||||
}
|
||||
|
||||
|
||||
private static dynamic FromClient(Context ctx, Client c) {
|
||||
dynamic player = new ExpandoObject();
|
||||
foreach (var (perm, mutate) in Mutators) {
|
||||
if (ctx.HasPermission(perm)) {
|
||||
mutate(player, c);
|
||||
}
|
||||
}
|
||||
return player;
|
||||
}
|
||||
|
||||
|
||||
private static GamePacket? GetGamePacket(Client c) {
|
||||
object? lastGamePacket = null;
|
||||
c.Metadata.TryGetValue("lastGamePacket", out lastGamePacket);
|
||||
if (lastGamePacket == null) { return null; }
|
||||
return (GamePacket) lastGamePacket;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class Costume {
|
||||
public string Cap { get; private set; }
|
||||
public string Body { get; private set; }
|
||||
|
||||
|
||||
private Costume(CostumePacket p) {
|
||||
this.Cap = p.CapName;
|
||||
this.Body = p.BodyName;
|
||||
}
|
||||
|
||||
|
||||
public static Costume? FromClient(Client c) {
|
||||
if (c.CurrentCostume == null) { return null; }
|
||||
CostumePacket p = (CostumePacket) c.CurrentCostume!;
|
||||
return new Costume(p);
|
||||
}
|
||||
}
|
||||
}
|
46
Server/JsonApi/BlockClients.cs
Normal file
46
Server/JsonApi/BlockClients.cs
Normal file
|
@ -0,0 +1,46 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Server.JsonApi;
|
||||
|
||||
public static class BlockClients
|
||||
{
|
||||
private const int MAX_TRIES = 5;
|
||||
|
||||
|
||||
private static ConcurrentDictionary<IPAddress, int> Failures = new ConcurrentDictionary<IPAddress, int>();
|
||||
|
||||
|
||||
public static bool IsBlocked(Context ctx) {
|
||||
if (ctx.socket.RemoteEndPoint == null) { return true; }
|
||||
|
||||
IPAddress ip = (ctx.socket.RemoteEndPoint as IPEndPoint)!.Address;
|
||||
|
||||
int failures = BlockClients.Failures.GetValueOrDefault(ip, 0);
|
||||
return failures >= BlockClients.MAX_TRIES;
|
||||
}
|
||||
|
||||
|
||||
public static void Fail(Context ctx) {
|
||||
if (ctx.socket.RemoteEndPoint == null) { return; }
|
||||
|
||||
IPAddress ip = (ctx.socket.RemoteEndPoint as IPEndPoint)!.Address;
|
||||
|
||||
int failures = 1;
|
||||
BlockClients.Failures.AddOrUpdate(ip, 1, (k, v) => failures = v + 1);
|
||||
|
||||
if (failures == BlockClients.MAX_TRIES) {
|
||||
JsonApi.Logger.Warn($"Block client {ctx.socket.RemoteEndPoint} because of too many failed requests.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void Redeem(Context ctx) {
|
||||
if (ctx.socket.RemoteEndPoint == null) { return; }
|
||||
|
||||
IPAddress ip = (ctx.socket.RemoteEndPoint as IPEndPoint)!.Address;
|
||||
|
||||
BlockClients.Failures.Remove(ip, out int val);
|
||||
}
|
||||
}
|
42
Server/JsonApi/Context.cs
Normal file
42
Server/JsonApi/Context.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
using Server;
|
||||
using Shared;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Server.JsonApi;
|
||||
|
||||
public class Context {
|
||||
public Server server;
|
||||
public Socket socket;
|
||||
public ApiRequest? request;
|
||||
public Logger? logger;
|
||||
|
||||
|
||||
public Context(
|
||||
Server server,
|
||||
Socket socket
|
||||
) {
|
||||
this.server = server;
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
|
||||
public bool HasPermission(string perm) {
|
||||
if (this.request == null) { return false; }
|
||||
return Settings.Instance.JsonApi.Tokens[this.request!.Token!].Contains(perm);
|
||||
}
|
||||
|
||||
|
||||
public List<string> Permissions {
|
||||
get {
|
||||
if (this.request == null) { return new List<string>(); }
|
||||
return Settings.Instance.JsonApi.Tokens[this.request!.Token!];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task Send(object data) {
|
||||
byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(data);
|
||||
await this.socket.SendAsync(bytes, SocketFlags.None);
|
||||
}
|
||||
}
|
77
Server/JsonApi/JsonApi.cs
Normal file
77
Server/JsonApi/JsonApi.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
using System.Buffers;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
using Server;
|
||||
|
||||
using Shared;
|
||||
using Shared.Packet;
|
||||
|
||||
namespace Server.JsonApi;
|
||||
|
||||
|
||||
public static class JsonApi {
|
||||
public const ushort PACKET_TYPE = 0x5453; // ascii "ST" (0x53 0x54) from preamble, but swapped because of endianness
|
||||
public const string PREAMBLE = "{\"API_JSON_REQUEST\":";
|
||||
|
||||
|
||||
public static readonly Logger Logger = new Logger("JsonApi");
|
||||
|
||||
|
||||
public static async Task<bool> HandleAPIRequest(
|
||||
Server server,
|
||||
Socket socket,
|
||||
PacketHeader header,
|
||||
IMemoryOwner<byte> memory
|
||||
) {
|
||||
// check if it is enabled
|
||||
if (!Settings.Instance.JsonApi.Enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check packet type
|
||||
if ((ushort) header.Type != JsonApi.PACKET_TYPE) {
|
||||
server.Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// check entire header length
|
||||
string headerStr = Encoding.UTF8.GetString(memory.Memory.Span[..Constants.HeaderSize].ToArray());
|
||||
if (headerStr != JsonApi.PREAMBLE) {
|
||||
server.Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Context ctx = new Context(server, socket);
|
||||
|
||||
// not if there were too many failed attempts in the past
|
||||
if (BlockClients.IsBlocked(ctx)) {
|
||||
JsonApi.Logger.Info($"Rejected blocked client {socket.RemoteEndPoint}.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// receive & parse JSON
|
||||
ApiPacket? p = await ApiPacket.Read(ctx, headerStr);
|
||||
if (p == null) {
|
||||
BlockClients.Fail(ctx);
|
||||
return true;
|
||||
}
|
||||
|
||||
// verify basic request structure & token
|
||||
ApiRequest req = p.API_JSON_REQUEST!;
|
||||
ctx.request = req;
|
||||
if (!req.IsValid(ctx)) {
|
||||
BlockClients.Fail(ctx);
|
||||
return true;
|
||||
}
|
||||
|
||||
// process request
|
||||
if (!await req.Process(ctx)) {
|
||||
BlockClients.Fail(ctx);
|
||||
return true;
|
||||
}
|
||||
|
||||
BlockClients.Redeem(ctx);
|
||||
return true;
|
||||
}
|
||||
}
|
78
Server/JsonApi/README.md
Normal file
78
Server/JsonApi/README.md
Normal file
|
@ -0,0 +1,78 @@
|
|||
The API runs on the same port as the normal game server. This is easier to deploy instead of a dedicated port, but has some limitations.
|
||||
|
||||
To use the API the client sends only one texual JSON object to the server and might get a JSON object back (if the request is valid).
|
||||
|
||||
The first 20 bytes of the request JSON are constant `{"API_JSON_REQUEST":`,
|
||||
to fill up and exactly match a complete normal game packet header (to identify and separate it from other server traffic).
|
||||
|
||||
A complete request can have a size of up to 512 characters (arbitrary limit that could be increased if needed).
|
||||
|
||||
The first 22 bytes of binary data returned from the server (`InitPacket`) need to be ignored to parse the rest as valid JSON.
|
||||
|
||||
---
|
||||
|
||||
Every request to the server needs to be authorized by containing a secret token.
|
||||
The token and its permissions are configured in the `settings.json`.
|
||||
There can be several tokens with different permission sets.
|
||||
|
||||
IP addresses that provide invalid requests or token values, are automatically blocked after 5 such requests until the next server restart.
|
||||
(This is mainly there to prevent agains brute force attacks that try to guess the token).
|
||||
|
||||
---
|
||||
|
||||
Currently available `Type` of requests:
|
||||
- `Permissions`: lists all permissions the token in use has (this request is always possible and doesn't require an extra permission).
|
||||
- `Status`: outputs all Settings, Players and Player properties the token has explicit permissions for.
|
||||
- `Command`: passes an command to the CommandHandler and returns its output. Every command needs to be permitted individually.
|
||||
|
||||
Specific settings and commands aren't hardcoded, but the API should automatically work for future extensions on both.
|
||||
The server operator only needs to add the new permissions for the new commands or settings that they want to whitelist to the `settings.json`.
|
||||
|
||||
The possible player status permissions are hardcoded though:
|
||||
- `Status/Players`
|
||||
- `Status/Players/Name`
|
||||
- `Status/Players/Stage`
|
||||
- `Status/Players/Scenario`
|
||||
- `Status/Players/Costume`
|
||||
- `Status/Players/IPv4`
|
||||
|
||||
---
|
||||
|
||||
Example for the `settings.json`:
|
||||
```json
|
||||
"JsonApi": {
|
||||
"Enabled": true,
|
||||
"Tokens": {
|
||||
"SECRET_TOKEN_12345": [
|
||||
"Status/Settings/Server/MaxPlayers",
|
||||
"Status/Settings/Scenario/MergeEnabled",
|
||||
"Status/Settings/Shines/Enabled",
|
||||
"Status/Settings/PersistShines/Enabled",
|
||||
"Status/Players",
|
||||
"Status/Players/Name",
|
||||
"Status/Players/Stage",
|
||||
"Status/Players/Costume",
|
||||
"Commands",
|
||||
"Commands/list",
|
||||
"Commands/sendall"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Example request (e.g. with `./test.sh Command sendall mush`):
|
||||
```json
|
||||
{"API_JSON_REQUEST":{"Token":"SECRET_TOKEN_12345","Type":"Command","Data":"sendall mush"}}
|
||||
```
|
||||
|
||||
Example `hexdump -C` response:
|
||||
```
|
||||
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
|
||||
00000010 01 00 02 00 04 00 7b 22 4f 75 74 70 75 74 22 3a |......{"Output":|
|
||||
00000020 5b 22 53 65 6e 74 20 70 6c 61 79 65 72 73 20 74 |["Sent players t|
|
||||
00000030 6f 20 50 65 61 63 68 57 6f 72 6c 64 48 6f 6d 65 |o PeachWorldHome|
|
||||
00000040 53 74 61 67 65 3a 2d 31 22 5d 7d |Stage:-1"]}|
|
||||
0000004b
|
||||
```
|
22
Server/JsonApi/test.sh
Executable file
22
Server/JsonApi/test.sh
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/bin/bash
|
||||
|
||||
TOKEN="SECRET_TOKEN_12345"
|
||||
HOST="localhost"
|
||||
PORT="1027"
|
||||
|
||||
DIR=`dirname "$0"`
|
||||
[ -f "$DIR/test.env" ] && source "$DIR/test.env"
|
||||
|
||||
TYPE="$1"
|
||||
|
||||
DATA=""
|
||||
if [ $# -gt 1 ] ; then
|
||||
DATA=",\"Data\":\"${@:2}\""
|
||||
fi
|
||||
|
||||
echo -n "{\"API_JSON_REQUEST\":{\"Token\":\"${TOKEN}\",\"Type\":\"$TYPE\"$DATA}}" \
|
||||
| timeout 5.0 nc $HOST $PORT \
|
||||
| tail -c+23 \
|
||||
;
|
||||
|
||||
echo ""
|
|
@ -29,7 +29,9 @@ public class Server {
|
|||
Socket socket = token.HasValue ? await serverSocket.AcceptAsync(token.Value) : await serverSocket.AcceptAsync();
|
||||
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);
|
||||
|
||||
if (! Settings.Instance.JsonApi.Enabled) {
|
||||
Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}");
|
||||
}
|
||||
|
||||
try {
|
||||
#pragma warning disable CS4014
|
||||
|
@ -157,6 +159,8 @@ public class Server {
|
|||
if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0))
|
||||
break;
|
||||
PacketHeader header = GetHeader(memory.Memory.Span[..Constants.HeaderSize]);
|
||||
if (first && await JsonApi.JsonApi.HandleAPIRequest(this, socket, header, memory)) { goto close; }
|
||||
|
||||
Range packetRange = Constants.HeaderSize..(Constants.HeaderSize + header.PacketSize);
|
||||
if (header.PacketSize > 0) {
|
||||
IMemoryOwner<byte> memTemp = memory; // header to copy to new memory
|
||||
|
@ -294,6 +298,7 @@ public class Server {
|
|||
Logger.Info($"Client {remote} disconnected from the server");
|
||||
}
|
||||
|
||||
close:
|
||||
// Clients.Remove(client)
|
||||
client.Connected = false;
|
||||
try {
|
||||
|
|
|
@ -47,6 +47,7 @@ public class Settings {
|
|||
public DiscordTable Discord { get; set; } = new DiscordTable();
|
||||
public ShineTable Shines { get; set; } = new ShineTable();
|
||||
public PersistShinesTable PersistShines { get; set; } = new PersistShinesTable();
|
||||
public JsonApiTable JsonApi { get; set; } = new JsonApiTable();
|
||||
|
||||
public class ServerTable {
|
||||
public string Address { get; set; } = IPAddress.Any.ToString();
|
||||
|
@ -86,4 +87,10 @@ public class Settings {
|
|||
public bool Enabled { get; set; } = false;
|
||||
public string Filename { get; set; } = "./moons.json";
|
||||
}
|
||||
|
||||
public class JsonApiTable
|
||||
{
|
||||
public bool Enabled { get; set; } = false;
|
||||
public Dictionary<string, List<string>> Tokens { get; set; } = new Dictionary<string, List<string>>();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue