Add token auth and POST endpoints (#6)

Add token auth and POST endpoints

Signed-off-by: Etzelia <etzelia@hotmail.com>

Reviewed-on: https://git.etztech.xyz/Minecraft/ServerAPI/pulls/6
Reviewed-by: ZeroHD <joey@ahines.net>
mcm
Etzelia 2020-10-07 01:20:02 +02:00
parent 055672468d
commit fbf285fb4f
13 changed files with 332 additions and 14 deletions

View File

@ -3,7 +3,7 @@
<groupId>xyz.etztech</groupId> <groupId>xyz.etztech</groupId>
<artifactId>ServerAPI</artifactId> <artifactId>ServerAPI</artifactId>
<!-- Version is used in plugin.yml --> <!-- Version is used in plugin.yml -->
<version>0.0.2</version> <version>0.0.3</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<!-- Plugin Information --> <!-- Plugin Information -->

View File

@ -3,11 +3,13 @@ package xyz.etztech.serverapi;
import org.bukkit.BanEntry; import org.bukkit.BanEntry;
import org.bukkit.BanList; import org.bukkit.BanList;
import org.bukkit.Bukkit;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import xyz.etztech.serverapi.commands.MainCommand; import xyz.etztech.serverapi.commands.MainCommand;
import xyz.etztech.serverapi.token.TokenList;
import xyz.etztech.serverapi.tps.TPS; import xyz.etztech.serverapi.tps.TPS;
import xyz.etztech.serverapi.web.IProvider; import xyz.etztech.serverapi.web.IProvider;
import xyz.etztech.serverapi.web.Web; import xyz.etztech.serverapi.web.Web;
@ -49,7 +51,8 @@ public class ServerAPI extends JavaPlugin implements IProvider {
web.stop(); web.stop();
web.start( web.start(
getConfig().getInt("port", 8080), getConfig().getInt("port", 8080),
getConfig().getString("password", "") new TokenList(getConfig().getConfigurationSection("auth")),
getConfig().getStringList("custom")
); );
} }
@ -107,6 +110,44 @@ public class ServerAPI extends JavaPlugin implements IProvider {
return bans; return bans;
} }
@Override
public void kick(BanAPI kick) {
Player player = Bukkit.getPlayerExact(kick.getTarget());
if (player != null) {
player.kickPlayer("You have been kicked: " + kick.getReason());
}
}
@Override
public void ban(BanAPI ban) {
Date expires = null;
if (ban.getExpiration() != 0) {
expires = new Date(ban.getExpiration());
}
Bukkit.getBanList(BanList.Type.NAME).addBan(ban.getTarget(), ban.getReason(), expires, "ServerAPI");
Player player = Bukkit.getPlayerExact(ban.getTarget());
if (player != null) {
player.kickPlayer("You have been banned: " + ban.getReason());
}
}
@Override
public void unban(BanAPI ban) {
Bukkit.getBanList(BanList.Type.NAME).pardon(ban.getTarget());
}
@Override
public void broadcast(BroadcastAPI broadcast) {
Bukkit.broadcastMessage(String.format("%s > %s", broadcast.getFrom(), broadcast.getMessage()));
}
@Override
public void custom(CustomAPI custom) {
getServer().getScheduler().runTask(this, () -> {
getServer().dispatchCommand(getServer().getConsoleSender(), custom.build());
});
}
@Override @Override
public PingAPI ping() { public PingAPI ping() {
return PingAPI.fromMinecraft(getServer()); return PingAPI.fromMinecraft(getServer());

View File

@ -0,0 +1,27 @@
package xyz.etztech.serverapi.token;
public class Token {
private final String token;
private final TokenScope scope;
public Token(String token, TokenScope scope) {
this.token = token;
this.scope = scope;
}
public String getToken() {
return token;
}
public TokenScope getScope() {
return scope;
}
public boolean canGET() {
return scope == TokenScope.GET || scope == TokenScope.ALL;
}
public boolean canPOST() {
return scope == TokenScope.POST || scope == TokenScope.ALL;
}
}

View File

@ -0,0 +1,49 @@
package xyz.etztech.serverapi.token;
import org.bukkit.configuration.ConfigurationSection;
import java.util.ArrayList;
import java.util.List;
public class TokenList {
private final boolean protectGET;
private final boolean protectPOST;
private final List<Token> tokens;
public TokenList(boolean protectGET, boolean protectPOST, List<Token> tokens) {
this.protectGET = protectGET;
this.protectPOST = protectPOST;
this.tokens = tokens;
}
public TokenList(ConfigurationSection auth) {
if (auth == null) {
this.protectGET = false;
this.protectPOST = false;
this.tokens = null;
return;
}
this.protectGET = auth.getBoolean("get", true);
this.protectPOST = auth.getBoolean("post", true);
this.tokens = new ArrayList<>();
ConfigurationSection tokenSection = auth.getConfigurationSection("tokens");
if (tokenSection == null) {
return;
}
for (String token : tokenSection.getKeys(false)) {
this.tokens.add(new Token(token, TokenScope.parseScope(tokenSection.getString(token, "none"))));
}
}
public boolean isProtectGET() {
return protectGET;
}
public boolean isProtectPOST() {
return protectPOST;
}
public List<Token> getTokens() {
return tokens;
}
}

View File

@ -0,0 +1,24 @@
package xyz.etztech.serverapi.token;
public enum TokenScope {
NONE,
GET,
POST,
ALL;
public static TokenScope parseScope(String scope) {
if (scope == null) {
return NONE;
}
switch (scope.toLowerCase()) {
case "get":
return GET;
case "post":
return POST;
case "all":
return ALL;
default:
return NONE;
}
}
}

View File

@ -6,6 +6,8 @@ import java.util.List;
import java.util.Set; import java.util.Set;
public interface IProvider { public interface IProvider {
// GET
Set<BanAPI> bans(); Set<BanAPI> bans();
Set<PlayerAPI> players(); Set<PlayerAPI> players();
PingAPI ping(); PingAPI ping();
@ -14,5 +16,13 @@ public interface IProvider {
WorldAPI world(String name); WorldAPI world(String name);
List<PluginAPI> plugins(); List<PluginAPI> plugins();
// POST
void kick(BanAPI kick);
void ban(BanAPI ban);
void unban(BanAPI ban);
void broadcast(BroadcastAPI broadcast);
void custom(CustomAPI custom);
// MISC
void log(String message); void log(String message);
} }

View File

@ -4,6 +4,7 @@ import io.javalin.Javalin;
import io.javalin.core.event.EventListener; import io.javalin.core.event.EventListener;
import io.javalin.core.plugin.Plugin; import io.javalin.core.plugin.Plugin;
import io.javalin.core.util.RouteOverviewPlugin; import io.javalin.core.util.RouteOverviewPlugin;
import io.javalin.http.BadRequestResponse;
import io.javalin.http.Context; import io.javalin.http.Context;
import io.javalin.http.UnauthorizedResponse; import io.javalin.http.UnauthorizedResponse;
import io.javalin.plugin.graphql.GraphQLOptions; import io.javalin.plugin.graphql.GraphQLOptions;
@ -12,18 +13,25 @@ import io.javalin.plugin.json.JavalinJson;
import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.helpers.NOPLogger; import org.slf4j.helpers.NOPLogger;
import xyz.etztech.serverapi.ServerAPI; import xyz.etztech.serverapi.ServerAPI;
import xyz.etztech.serverapi.token.TokenList;
import xyz.etztech.serverapi.web.api.BanAPI;
import xyz.etztech.serverapi.web.api.BroadcastAPI;
import xyz.etztech.serverapi.web.api.CustomAPI;
import xyz.etztech.serverapi.web.api.ErrorAPI; import xyz.etztech.serverapi.web.api.ErrorAPI;
import java.util.List;
public class Web { public class Web {
private final IProvider provider; private final IProvider provider;
private String password; private TokenList tokens;
private List<String> custom;
private Javalin app; private Javalin app;
public Web(IProvider provider) { public Web(IProvider provider) {
this.provider = provider; this.provider = provider;
} }
public void start(int port, String password) { public void start(int port, TokenList tokens, List<String> custom) {
Javalin.log = NOPLogger.NOP_LOGGER; Javalin.log = NOPLogger.NOP_LOGGER;
app = Javalin.create(config -> { app = Javalin.create(config -> {
config.registerPlugin(graphql()); config.registerPlugin(graphql());
@ -31,8 +39,14 @@ public class Web {
config.enableCorsForAllOrigins(); config.enableCorsForAllOrigins();
}).events(this::events).exception(Exception.class, this::exception); }).events(this::events).exception(Exception.class, this::exception);
this.password = password; this.custom = custom;
if (!"".equals(password)) {
this.tokens = tokens;
if (tokens != null) {
provider.log(String.format("Loaded %d tokens", tokens.getTokens().size()));
if (!tokens.isProtectPOST()) {
provider.log("WARNING: You have disabled POST protection, which can enable users to arbitrarily run actions against your server.");
}
app.before(this::access); app.before(this::access);
} }
@ -46,6 +60,12 @@ public class Web {
app.get("/worlds", this::worlds); app.get("/worlds", this::worlds);
app.get("/worlds/:name", this::world); app.get("/worlds/:name", this::world);
app.post("/kick", this::kick);
app.post("/ban", this::ban);
app.post("/unban", this::unban);
app.post("/broadcast", this::broadcast);
app.post("/custom", this::custom);
// What in the actual fuck... https://github.com/tipsy/javalin/issues/358 // What in the actual fuck... https://github.com/tipsy/javalin/issues/358
ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(ServerAPI.class.getClassLoader()); Thread.currentThread().setContextClassLoader(ServerAPI.class.getClassLoader());
@ -77,13 +97,22 @@ public class Web {
} }
public void access(Context ctx) { public void access(Context ctx) {
String pw = ctx.header("X-ServerAPI-Password"); String token = ctx.header("X-ServerAPI-Token");
if (pw == null) { if (token == null) {
pw = ctx.queryParam("password"); token = ctx.queryParam("token");
} }
boolean isGET = "GET".equalsIgnoreCase(ctx.method());
boolean isPOST = "POST".equalsIgnoreCase(ctx.method());
if (!password.equals(pw)) { if (isGET && !tokens.isProtectGET()) return;
throw new UnauthorizedResponse(JavalinJson.toJson(new ErrorAPI(401, "Unauthorized"))); if (isPOST && !tokens.isProtectPOST()) return;
String finalToken = token;
boolean pass = tokens.getTokens().stream().anyMatch(t -> t.getToken().equals(finalToken) &&
((isGET && t.canGET()) || (isPOST && t.canPOST())));
if (!pass) {
throw new UnauthorizedResponse();
} }
} }
@ -121,4 +150,33 @@ public class Web {
public void ping(Context ctx) { public void ping(Context ctx) {
ctx.json(provider.ping()); ctx.json(provider.ping());
} }
public void kick(Context ctx) {
provider.kick(JavalinJson.fromJson(ctx.body(), BanAPI.class));
ctx.status(200);
}
public void ban(Context ctx) {
provider.ban(JavalinJson.fromJson(ctx.body(), BanAPI.class));
ctx.status(200);
}
public void unban(Context ctx) {
provider.unban(JavalinJson.fromJson(ctx.body(), BanAPI.class));
ctx.status(200);
}
public void broadcast(Context ctx) {
provider.broadcast(JavalinJson.fromJson(ctx.body(), BroadcastAPI.class));
ctx.status(200);
}
public void custom(Context ctx) {
CustomAPI capi = JavalinJson.fromJson(ctx.body(), CustomAPI.class);
if (custom.stream().noneMatch(c -> c.equalsIgnoreCase(capi.getCommand()))) {
throw new BadRequestResponse();
}
provider.custom(capi);
ctx.status(200);
}
} }

View File

@ -13,6 +13,14 @@ public class BanAPI {
private final long created; private final long created;
private final long expiration; private final long expiration;
public BanAPI() {
this.target = "";
this.source = "";
this.reason = "";
this.created = 0;
this.expiration = 0;
}
public BanAPI(String target, String source, String reason, long created, long expiration) { public BanAPI(String target, String source, String reason, long created, long expiration) {
this.target = target; this.target = target;
this.source = source; this.source = source;

View File

@ -0,0 +1,25 @@
package xyz.etztech.serverapi.web.api;
public class BroadcastAPI {
private final String from;
private final String message;
public BroadcastAPI() {
this.from = "";
this.message = "";
}
public BroadcastAPI(String from, String message) {
this.from = from;
this.message = message;
}
public String getFrom() {
return from;
}
public String getMessage() {
return message;
}
}

View File

@ -0,0 +1,32 @@
package xyz.etztech.serverapi.web.api;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class CustomAPI {
private final String command;
private final List<String> args;
public CustomAPI() {
this.command = "";
this.args = new ArrayList<>();
}
public CustomAPI(String command, List<String> args) {
this.command = command;
this.args = args;
}
public String getCommand() {
return command;
}
public List<String> getArgs() {
return args;
}
public String build() {
return command + " " + String.join(" ", args);
}
}

View File

@ -1,5 +1,15 @@
# The port to run on # The port to run on
port: 8080 port: 8080
# (Optional) password # Authentication
password: '' auth:
# Protect GET routes. If false, GET routes are public.
get: true
# Protect POST routes. If false, POST routes are public.
post: true
tokens:
token: access
# Custom commands (POST)
custom:
- say

View File

@ -46,6 +46,21 @@ public class MockProvider implements IProvider {
)); ));
} }
@Override
public void kick(BanAPI kick) {}
@Override
public void ban(BanAPI ban) {}
@Override
public void unban(BanAPI ban) {}
@Override
public void broadcast(BroadcastAPI broadcast) {}
@Override
public void custom(CustomAPI custom) {}
@Override @Override
public Set<PlayerAPI> players() { public Set<PlayerAPI> players() {
return new HashSet<>(Arrays.asList( return new HashSet<>(Arrays.asList(

View File

@ -1,11 +1,30 @@
package xyz.etztech.serverapi; package xyz.etztech.serverapi;
import xyz.etztech.serverapi.token.Token;
import xyz.etztech.serverapi.token.TokenList;
import xyz.etztech.serverapi.token.TokenScope;
import xyz.etztech.serverapi.web.Web; import xyz.etztech.serverapi.web.Web;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ServerRunner { public class ServerRunner {
static TokenList tokens = new TokenList(true, true, Arrays.asList(
new Token("testGET", TokenScope.GET),
new Token("testPOST", TokenScope.POST),
new Token("testAll", TokenScope.ALL),
new Token("testNone", TokenScope.NONE)
));
static List<String> custom = Arrays.asList(
"test",
"say"
);
public static void main(String[] args) { public static void main(String[] args) {
Web web = new Web(new MockProvider()); Web web = new Web(new MockProvider());
web.start(8080, ""); web.start(8080, tokens, custom);
} }
} }