mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-12-27 03:27:37 +01:00
Restructured WebServer Response serving
This commit is contained in:
parent
5e9624cba6
commit
3ffc5dd95d
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Licence is provided in the jar as license.yml also here:
|
||||
* https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml
|
||||
*/
|
||||
package main.java.com.djrapitops.plan.api.exceptions;
|
||||
|
||||
/**
|
||||
* Thrown when WebUser can not be authorized (WebServer).
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
public class WebUserAuthException extends Exception {
|
||||
public WebUserAuthException() {
|
||||
}
|
||||
|
||||
public WebUserAuthException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public WebUserAuthException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public WebUserAuthException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
@ -185,7 +185,10 @@ public class UserInfoTable extends UserIDTable {
|
||||
boolean banned = set.getBoolean(columnBanned);
|
||||
String name = set.getString("name");
|
||||
UUID uuid = UUID.fromString(set.getString("uuid"));
|
||||
userInfo.add(new UserInfo(uuid, name, registered, opped, banned));
|
||||
UserInfo info = new UserInfo(uuid, name, registered, opped, banned);
|
||||
if (!userInfo.contains(info)) {
|
||||
userInfo.add(info);
|
||||
}
|
||||
}
|
||||
return userInfo;
|
||||
} finally {
|
||||
|
@ -6,7 +6,7 @@ import com.maxmind.geoip2.DatabaseReader;
|
||||
import com.maxmind.geoip2.exception.GeoIp2Exception;
|
||||
import com.maxmind.geoip2.model.CountryResponse;
|
||||
import com.maxmind.geoip2.record.Country;
|
||||
import main.java.com.djrapitops.plan.Plan;
|
||||
import main.java.com.djrapitops.plan.utilities.MiscUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@ -29,7 +29,7 @@ import java.util.zip.GZIPInputStream;
|
||||
*/
|
||||
public class GeolocationCache {
|
||||
|
||||
private static File geolocationDB = new File(Plan.getInstance().getDataFolder(), "GeoIP.dat");
|
||||
private static File geolocationDB = new File(MiscUtils.getIPlan().getDataFolder(), "GeoIP.dat");
|
||||
|
||||
private static final Cache<String, String> geolocationCache = CacheBuilder.newBuilder()
|
||||
.build();
|
||||
@ -51,7 +51,7 @@ public class GeolocationCache {
|
||||
* <p>
|
||||
* An exception from that rule is when the country is unknown or the retrieval of the country failed in any way,
|
||||
* if that happens, "Not Known" will be returned.
|
||||
* @see #getUncachedCountry(String)
|
||||
* @see #getUnCachedCountry(String)
|
||||
*/
|
||||
public static String getCountry(String ipAddress) {
|
||||
String country = getCachedCountry(ipAddress);
|
||||
@ -59,7 +59,7 @@ public class GeolocationCache {
|
||||
if (country != null) {
|
||||
return country;
|
||||
} else {
|
||||
country = getUncachedCountry(ipAddress);
|
||||
country = getUnCachedCountry(ipAddress);
|
||||
geolocationCache.put(ipAddress, country);
|
||||
|
||||
return country;
|
||||
@ -71,6 +71,7 @@ public class GeolocationCache {
|
||||
* <p>
|
||||
* This product includes GeoLite2 data created by MaxMind, available from
|
||||
* <a href="http://www.maxmind.com">http://www.maxmind.com</a>.
|
||||
*
|
||||
* @param ipAddress The IP Address from which the country is retrieved
|
||||
* @return The name of the country in full length.
|
||||
* <p>
|
||||
@ -79,7 +80,7 @@ public class GeolocationCache {
|
||||
* @see <a href="http://maxmind.com">http://maxmind.com</a>
|
||||
* @see #getCountry(String)
|
||||
*/
|
||||
private static String getUncachedCountry(String ipAddress) {
|
||||
private static String getUnCachedCountry(String ipAddress) {
|
||||
try {
|
||||
checkDB();
|
||||
|
||||
|
@ -12,20 +12,20 @@ import java.util.Map;
|
||||
*/
|
||||
public class WebAPIManager {
|
||||
|
||||
private static final Map<String, WebAPI> registry = new HashMap<>();
|
||||
private final Map<String, WebAPI> registry;
|
||||
|
||||
/**
|
||||
* Constructor used to hide the public constructor
|
||||
*/
|
||||
private WebAPIManager() {
|
||||
throw new IllegalStateException("Utility class");
|
||||
public WebAPIManager() {
|
||||
registry = new HashMap<>();
|
||||
}
|
||||
|
||||
public static void registerNewAPI(String method, WebAPI api) {
|
||||
public void registerNewAPI(String method, WebAPI api) {
|
||||
registry.put(method.toLowerCase(), api);
|
||||
}
|
||||
|
||||
public static WebAPI getAPI(String method) {
|
||||
public WebAPI getAPI(String method) {
|
||||
return registry.get(method.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Licence is provided in the jar as license.yml also here:
|
||||
* https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml
|
||||
*/
|
||||
package main.java.com.djrapitops.plan.systems.webserver;
|
||||
|
||||
import main.java.com.djrapitops.plan.Plan;
|
||||
import main.java.com.djrapitops.plan.systems.webapi.WebAPI;
|
||||
import main.java.com.djrapitops.plan.systems.webapi.WebAPIManager;
|
||||
import main.java.com.djrapitops.plan.systems.webserver.response.ForbiddenResponse;
|
||||
import main.java.com.djrapitops.plan.systems.webserver.response.Response;
|
||||
import main.java.com.djrapitops.plan.systems.webserver.response.api.BadRequestResponse;
|
||||
import main.java.com.djrapitops.plan.utilities.MiscUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* //TODO Class Javadoc Comment
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
public class APIResponseHandler {
|
||||
|
||||
private final WebAPIManager webAPI;
|
||||
|
||||
public APIResponseHandler(WebAPIManager webAPI) {
|
||||
this.webAPI = webAPI;
|
||||
}
|
||||
|
||||
Response getAPIResponse(Request request) throws IOException {
|
||||
String target = request.getTarget();
|
||||
String[] args = target.split("/");
|
||||
|
||||
if (args.length < 3) {
|
||||
String error = "API Method not specified";
|
||||
return PageCache.loadPage(error, () -> new BadRequestResponse(error));
|
||||
}
|
||||
|
||||
String method = args[2];
|
||||
String response = null;
|
||||
try (InputStream inputStream = request.getRequestBody()) {
|
||||
response = readPOSTRequest(inputStream);
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
String error = "Error at reading the POST request." +
|
||||
"Note that the Encoding must be ISO-8859-1.";
|
||||
return PageCache.loadPage(error, () -> new BadRequestResponse(error));
|
||||
}
|
||||
|
||||
Map<String, String> variables = readVariables(response);
|
||||
String key = variables.get("key");
|
||||
|
||||
if (!checkKey(key)) {
|
||||
String error = "Server Key not given or invalid";
|
||||
return PageCache.loadPage(error, () -> {
|
||||
ForbiddenResponse forbidden = new ForbiddenResponse();
|
||||
forbidden.setContent(error);
|
||||
return forbidden;
|
||||
});
|
||||
}
|
||||
|
||||
WebAPI api = webAPI.getAPI(method);
|
||||
|
||||
if (api == null) {
|
||||
String error = "API Method not found";
|
||||
return PageCache.loadPage(error, () -> new BadRequestResponse(error));
|
||||
}
|
||||
|
||||
return api.onResponse(Plan.getInstance(), variables);
|
||||
}
|
||||
|
||||
private String readPOSTRequest(InputStream in) throws IOException {
|
||||
byte[] bytes;
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
for (int n = in.read(buf); n > 0; n = in.read(buf)) {
|
||||
out.write(buf, 0, n);
|
||||
}
|
||||
|
||||
bytes = out.toByteArray();
|
||||
|
||||
try {
|
||||
return new String(bytes, StandardCharsets.ISO_8859_1);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkKey(String key) {
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UUID uuid = MiscUtils.getIPlan().getServerInfoManager().getServerUUID();
|
||||
UUID keyUUID;
|
||||
try {
|
||||
keyUUID = UUID.fromString(key);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return uuid.equals(keyUUID);
|
||||
}
|
||||
|
||||
private Map<String, String> readVariables(String response) {
|
||||
String[] variables = response.split("&");
|
||||
|
||||
return Arrays.stream(variables)
|
||||
.map(variable -> variable.split("=", 2))
|
||||
.filter(splittedVariables -> splittedVariables.length == 2)
|
||||
.collect(Collectors.toMap(splittedVariables -> splittedVariables[0], splittedVariables -> splittedVariables[1], (a, b) -> b));
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Licence is provided in the jar as license.yml also here:
|
||||
* https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml
|
||||
*/
|
||||
package main.java.com.djrapitops.plan.systems.webserver;
|
||||
|
||||
import com.djrapitops.plugin.utilities.Verify;
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* //TODO Class Javadoc Comment
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
public class Request {
|
||||
private String auth;
|
||||
private final String requestMethod;
|
||||
private final String target;
|
||||
|
||||
private final HttpExchange exchange;
|
||||
|
||||
public Request(HttpExchange exchange) {
|
||||
this.requestMethod = exchange.getRequestMethod();
|
||||
this.target = exchange.getRequestURI().toString();
|
||||
|
||||
this.exchange = exchange;
|
||||
setAuth(exchange.getRequestHeaders());
|
||||
}
|
||||
|
||||
public String getAuth() {
|
||||
return auth;
|
||||
}
|
||||
|
||||
public void setAuth(Headers requestHeaders) {
|
||||
List<String> authorization = requestHeaders.get("Authorization");
|
||||
if (Verify.isEmpty(authorization)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String authLine = authorization.get(0);
|
||||
if (authLine.contains("Basic ")) {
|
||||
auth = authLine.split(" ")[1];
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAuth() {
|
||||
return auth != null;
|
||||
}
|
||||
|
||||
public String getRequestMethod() {
|
||||
return requestMethod;
|
||||
}
|
||||
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public boolean isAPIRequest() {
|
||||
return "POST".equals(requestMethod);
|
||||
}
|
||||
|
||||
public InputStream getRequestBody() {
|
||||
return exchange.getRequestBody();
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Licence is provided in the jar as license.yml also here:
|
||||
* https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml
|
||||
*/
|
||||
package main.java.com.djrapitops.plan.systems.webserver;
|
||||
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import main.java.com.djrapitops.plan.api.IPlan;
|
||||
import main.java.com.djrapitops.plan.systems.webserver.response.PromptAuthorizationResponse;
|
||||
import main.java.com.djrapitops.plan.systems.webserver.response.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* HttpHandler for webserver request management.
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
public class RequestHandler implements HttpHandler {
|
||||
|
||||
|
||||
private final ResponseHandler responseHandler;
|
||||
|
||||
RequestHandler(IPlan plugin, WebServer webServer) {
|
||||
responseHandler = new ResponseHandler(plugin, webServer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
Headers responseHeaders = exchange.getResponseHeaders();
|
||||
Request request = new Request(exchange);
|
||||
|
||||
try {
|
||||
Response response = responseHandler.getResponse(request);
|
||||
if (response instanceof PromptAuthorizationResponse) {
|
||||
responseHeaders.set("WWW-Authenticate", "Basic realm=\"/\";");
|
||||
}
|
||||
response.setResponseHeaders(responseHeaders);
|
||||
response.send(exchange);
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Licence is provided in the jar as license.yml also here:
|
||||
* https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml
|
||||
*/
|
||||
package main.java.com.djrapitops.plan.systems.webserver;
|
||||
|
||||
import main.java.com.djrapitops.plan.api.IPlan;
|
||||
import main.java.com.djrapitops.plan.api.exceptions.WebUserAuthException;
|
||||
import main.java.com.djrapitops.plan.data.WebUser;
|
||||
import main.java.com.djrapitops.plan.database.tables.SecurityTable;
|
||||
import main.java.com.djrapitops.plan.systems.webserver.response.*;
|
||||
import main.java.com.djrapitops.plan.utilities.PassEncryptUtil;
|
||||
import main.java.com.djrapitops.plan.utilities.html.HtmlUtils;
|
||||
import main.java.com.djrapitops.plan.utilities.uuid.UUIDUtility;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* //TODO Class Javadoc Comment
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
public class ResponseHandler extends APIResponseHandler {
|
||||
|
||||
private final IPlan plugin;
|
||||
private final WebServer webServer;
|
||||
|
||||
private final boolean usingHttps;
|
||||
|
||||
public ResponseHandler(IPlan plugin, WebServer webServer) {
|
||||
super(webServer.getWebAPI());
|
||||
this.plugin = plugin;
|
||||
this.webServer = webServer;
|
||||
this.usingHttps = webServer.isUsingHTTPS();
|
||||
}
|
||||
|
||||
public Response getResponse(Request request) {
|
||||
String target = request.getTarget();
|
||||
String[] args = target.split("/");
|
||||
try {
|
||||
if ("/favicon.ico".equals(target)) {
|
||||
return PageCache.loadPage("Redirect: favicon", () -> new RedirectResponse("https://puu.sh/tK0KL/6aa2ba141b.ico"));
|
||||
}
|
||||
if (request.isAPIRequest()) {
|
||||
return getAPIResponse(request);
|
||||
}
|
||||
if (target.endsWith(".css")) {
|
||||
return PageCache.loadPage(target + "css", () -> new CSSResponse("main.css"));
|
||||
}
|
||||
|
||||
if (target.endsWith(".js")) {
|
||||
String fileName = args[args.length - 1];
|
||||
return PageCache.loadPage(target + "js", () -> new JavaScriptResponse(fileName));
|
||||
}
|
||||
if (usingHttps) {
|
||||
if (!request.hasAuth()) {
|
||||
throw new WebUserAuthException("No Authorization");
|
||||
}
|
||||
|
||||
WebUser user = getUser(request.getAuth());
|
||||
int required = getRequiredPermLevel(target, user.getName());
|
||||
int permLevel = user.getPermLevel();
|
||||
|
||||
if (!isAuthorized(required, permLevel)) {
|
||||
return forbiddenResponse(required, permLevel);
|
||||
}
|
||||
if (args.length < 2) {
|
||||
return rootPageResponse(user);
|
||||
}
|
||||
}
|
||||
|
||||
String page = args[1];
|
||||
switch (page) {
|
||||
case "players":
|
||||
return PageCache.loadPage("players", PlayersPageResponse::new);
|
||||
case "player":
|
||||
return playerResponse(args);
|
||||
case "server":
|
||||
return serverResponse();
|
||||
default:
|
||||
return notFoundResponse();
|
||||
}
|
||||
|
||||
} catch (WebUserAuthException e) {
|
||||
return PageCache.loadPage("promptAuthorization", PromptAuthorizationResponse::new);
|
||||
} catch (Exception e) {
|
||||
return new InternalErrorResponse(e, request.getTarget());
|
||||
}
|
||||
}
|
||||
|
||||
private Response forbiddenResponse(int required, int permLevel) {
|
||||
return PageCache.loadPage("forbidden", () ->
|
||||
new ForbiddenResponse("Unauthorized User.<br>"
|
||||
+ "Make sure your user has the correct access level.<br>"
|
||||
+ "This page requires permission level of " + required + ",<br>"
|
||||
+ "This user has permission level of " + permLevel));
|
||||
}
|
||||
|
||||
private boolean isAuthorized(int requiredPermLevel, int permLevel) throws Exception {
|
||||
return permLevel <= requiredPermLevel;
|
||||
}
|
||||
|
||||
private WebUser getUser(String auth) throws SQLException, PassEncryptUtil.InvalidHashException, PassEncryptUtil.CannotPerformOperationException, WebUserAuthException {
|
||||
Base64.Decoder decoder = Base64.getDecoder();
|
||||
byte[] decoded = decoder.decode(auth);
|
||||
String[] userInfo = new String(decoded).split(":");
|
||||
if (userInfo.length != 2) {
|
||||
throw new WebUserAuthException("User and Password not specified");
|
||||
}
|
||||
|
||||
String user = userInfo[0];
|
||||
String passwordRaw = userInfo[1];
|
||||
|
||||
SecurityTable securityTable = plugin.getDB().getSecurityTable();
|
||||
if (!securityTable.userExists(user)) {
|
||||
throw new WebUserAuthException("User Doesn't exist");
|
||||
}
|
||||
|
||||
WebUser webUser = securityTable.getWebUser(user);
|
||||
|
||||
boolean correctPass = PassEncryptUtil.verifyPassword(passwordRaw, webUser.getSaltedPassHash());
|
||||
if (!correctPass) {
|
||||
throw new WebUserAuthException("User and Password do not match");
|
||||
}
|
||||
return webUser;
|
||||
}
|
||||
|
||||
private int getRequiredPermLevel(String target, String user) {
|
||||
String[] t = target.split("/");
|
||||
if (t.length < 2) {
|
||||
return 100;
|
||||
}
|
||||
if (t.length > 3) {
|
||||
return 0;
|
||||
}
|
||||
String page = t[1];
|
||||
switch (page) {
|
||||
case "players":
|
||||
return 1;
|
||||
case "player":
|
||||
// /player/ - 404 for perm lvl 1
|
||||
if (t.length < 3) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
final String wantedUser = t[2].toLowerCase().trim();
|
||||
final String theUser = user.trim().toLowerCase();
|
||||
|
||||
return wantedUser.equals(theUser) ? 2 : 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private Response rootPageResponse(WebUser user) {
|
||||
if (user == null) {
|
||||
return notFoundResponse();
|
||||
}
|
||||
|
||||
switch (user.getPermLevel()) {
|
||||
case 0:
|
||||
return serverResponse();
|
||||
case 1:
|
||||
return PageCache.loadPage("players", PlayersPageResponse::new);
|
||||
case 2:
|
||||
return playerResponse(new String[]{"", "", user.getName()});
|
||||
default:
|
||||
return forbiddenResponse(user.getPermLevel(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
private Response serverResponse() {
|
||||
if (!plugin.getInfoManager().isAnalysisCached()) {
|
||||
String error = "Analysis Data was not cached.<br>Use /plan analyze to cache the Data.";
|
||||
PageCache.loadPage("notFound: " + error, () -> new NotFoundResponse(error));
|
||||
}
|
||||
|
||||
return PageCache.loadPage("analysisPage", () -> new AnalysisPageResponse(plugin.getInfoManager()));
|
||||
}
|
||||
|
||||
private Response playerResponse(String[] args) {
|
||||
if (args.length < 3) {
|
||||
return PageCache.loadPage("notFound", NotFoundResponse::new);
|
||||
}
|
||||
|
||||
String playerName = args[2].trim();
|
||||
UUID uuid = UUIDUtility.getUUIDOf(playerName);
|
||||
|
||||
if (uuid == null) {
|
||||
String error = "Player has no UUID";
|
||||
return PageCache.loadPage("notFound: " + error, () -> new NotFoundResponse(error));
|
||||
}
|
||||
|
||||
plugin.getInfoManager().cachePlayer(uuid);
|
||||
return PageCache.loadPage("inspectPage: " + uuid, () -> new InspectPageResponse(plugin.getInfoManager(), uuid));
|
||||
}
|
||||
|
||||
private Response notFoundResponse() {
|
||||
String error = "404 Not Found";
|
||||
return PageCache.loadPage("notFound: " + error, () ->
|
||||
new NotFoundResponse("Make sure you're accessing a link given by a command, Examples:</p>"
|
||||
+ "<p>" + webServer.getProtocol() + ":" + HtmlUtils.getInspectUrl("<player>") + " or<br>"
|
||||
+ webServer.getProtocol() + ":" + HtmlUtils.getServerAnalysisUrl())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,43 +1,33 @@
|
||||
package main.java.com.djrapitops.plan.systems.webserver;
|
||||
|
||||
import com.djrapitops.plugin.utilities.Verify;
|
||||
import com.sun.net.httpserver.*;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import com.sun.net.httpserver.HttpsConfigurator;
|
||||
import com.sun.net.httpserver.HttpsParameters;
|
||||
import com.sun.net.httpserver.HttpsServer;
|
||||
import main.java.com.djrapitops.plan.Log;
|
||||
import main.java.com.djrapitops.plan.Plan;
|
||||
import main.java.com.djrapitops.plan.Settings;
|
||||
import main.java.com.djrapitops.plan.api.IPlan;
|
||||
import main.java.com.djrapitops.plan.data.WebUser;
|
||||
import main.java.com.djrapitops.plan.database.tables.SecurityTable;
|
||||
import main.java.com.djrapitops.plan.locale.Locale;
|
||||
import main.java.com.djrapitops.plan.locale.Msg;
|
||||
import main.java.com.djrapitops.plan.systems.info.InformationManager;
|
||||
import main.java.com.djrapitops.plan.systems.webapi.WebAPI;
|
||||
import main.java.com.djrapitops.plan.systems.webapi.WebAPIManager;
|
||||
import main.java.com.djrapitops.plan.systems.webapi.bukkit.*;
|
||||
import main.java.com.djrapitops.plan.systems.webapi.universal.PingWebAPI;
|
||||
import main.java.com.djrapitops.plan.systems.webserver.response.*;
|
||||
import main.java.com.djrapitops.plan.systems.webserver.response.api.BadRequestResponse;
|
||||
import main.java.com.djrapitops.plan.systems.webserver.response.api.JsonResponse;
|
||||
import main.java.com.djrapitops.plan.utilities.Benchmark;
|
||||
import main.java.com.djrapitops.plan.utilities.PassEncryptUtil;
|
||||
import main.java.com.djrapitops.plan.utilities.html.HtmlUtils;
|
||||
import main.java.com.djrapitops.plan.utilities.uuid.UUIDUtility;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import java.io.*;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.*;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
/**
|
||||
* @author Rsl1122
|
||||
@ -46,6 +36,7 @@ public class WebServer {
|
||||
|
||||
private final IPlan plugin;
|
||||
private InformationManager infoManager;
|
||||
private final WebAPIManager webAPI;
|
||||
|
||||
private final int port;
|
||||
private boolean enabled = false;
|
||||
@ -59,7 +50,7 @@ public class WebServer {
|
||||
public WebServer(IPlan plugin) {
|
||||
this.plugin = plugin;
|
||||
this.port = Settings.WEBSERVER_PORT.getNumber();
|
||||
|
||||
webAPI = new WebAPIManager();
|
||||
registerWebAPIs();
|
||||
}
|
||||
|
||||
@ -68,16 +59,16 @@ public class WebServer {
|
||||
}
|
||||
|
||||
private void registerWebAPIs() {
|
||||
WebAPIManager.registerNewAPI("analytics", new AnalyticsWebAPI());
|
||||
WebAPIManager.registerNewAPI("analyze", new AnalyzeWebAPI());
|
||||
WebAPIManager.registerNewAPI("configure", new ConfigureWebAPI());
|
||||
WebAPIManager.registerNewAPI("inspect", new InspectWebAPI());
|
||||
WebAPIManager.registerNewAPI("onlineplayers", new OnlinePlayersWebAPI());
|
||||
WebAPIManager.registerNewAPI("ping", new PingWebAPI());
|
||||
webAPI.registerNewAPI("analytics", new AnalyticsWebAPI());
|
||||
webAPI.registerNewAPI("analyze", new AnalyzeWebAPI());
|
||||
webAPI.registerNewAPI("configure", new ConfigureWebAPI());
|
||||
webAPI.registerNewAPI("inspect", new InspectWebAPI());
|
||||
webAPI.registerNewAPI("onlineplayers", new OnlinePlayersWebAPI());
|
||||
webAPI.registerNewAPI("ping", new PingWebAPI());
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts up the Webserver in a Asynchronous thread.
|
||||
* Starts up the WebServer in a new Thread Pool.
|
||||
*/
|
||||
public void initServer() {
|
||||
//Server is already enabled stop code
|
||||
@ -96,53 +87,7 @@ public class WebServer {
|
||||
server = HttpServer.create(new InetSocketAddress(port), 10);
|
||||
}
|
||||
|
||||
server.createContext("/", exchange -> {
|
||||
Headers responseHeaders = exchange.getResponseHeaders();
|
||||
URI uri = exchange.getRequestURI();
|
||||
String target = uri.toString();
|
||||
try {
|
||||
boolean apiRequest = "POST".equals(exchange.getRequestMethod());
|
||||
Response response = null;
|
||||
|
||||
String type = "text/html;";
|
||||
|
||||
if (apiRequest) {
|
||||
response = getAPIResponse(target, exchange);
|
||||
|
||||
if (response instanceof JsonResponse) {
|
||||
type = "application/json;";
|
||||
}
|
||||
}
|
||||
|
||||
responseHeaders.set("Content-Type", type);
|
||||
|
||||
if (apiRequest) {
|
||||
sendData(responseHeaders, exchange, response);
|
||||
return;
|
||||
}
|
||||
|
||||
WebUser user = null;
|
||||
|
||||
if (usingHttps) {
|
||||
user = getUser(exchange.getRequestHeaders());
|
||||
|
||||
// Prompt authorization
|
||||
if (user == null) {
|
||||
responseHeaders.set("WWW-Authenticate", "Basic realm=\"/\";");
|
||||
}
|
||||
}
|
||||
|
||||
response = getResponse(target, user);
|
||||
if (response instanceof CSSResponse) {
|
||||
responseHeaders.set("Content-Type", "text/css");
|
||||
}
|
||||
sendData(responseHeaders, exchange, response);
|
||||
} catch (Exception e) {
|
||||
internalErrorResponse(exchange, responseHeaders, target, e);
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
});
|
||||
server.createContext("/", new RequestHandler(plugin, this));
|
||||
|
||||
server.setExecutor(new ThreadPoolExecutor(4, 8, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100)));
|
||||
server.start();
|
||||
@ -156,77 +101,6 @@ public class WebServer {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendData(Headers header, HttpExchange exchange, Response response) throws IOException {
|
||||
header.set("Content-Encoding", "gzip");
|
||||
exchange.sendResponseHeaders(response.getCode(), 0);
|
||||
|
||||
try (GZIPOutputStream out = new GZIPOutputStream(exchange.getResponseBody());
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(response.getContent().getBytes())) {
|
||||
byte[] buffer = new byte[2048];
|
||||
int count;
|
||||
while ((count = bis.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void internalErrorResponse(HttpExchange exchange, Headers responseHeaders, String target, Exception e) {
|
||||
try {
|
||||
Log.toLog(target, e);
|
||||
sendData(responseHeaders, exchange, new InternalErrorResponse(e, target));
|
||||
} catch (IOException e1) {
|
||||
Log.toLog(this.getClass().getName(), e1);
|
||||
}
|
||||
}
|
||||
|
||||
private WebUser getUser(Headers requestHeaders) {
|
||||
Benchmark.start("getUser");
|
||||
try {
|
||||
List<String> authorization = requestHeaders.get("Authorization");
|
||||
if (Verify.isEmpty(authorization)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String auth = authorization.get(0);
|
||||
if (auth.contains("Basic ")) {
|
||||
auth = auth.split(" ")[1];
|
||||
} else {
|
||||
throw new IllegalArgumentException("Wrong format of Auth");
|
||||
}
|
||||
|
||||
Base64.Decoder decoder = Base64.getDecoder();
|
||||
byte[] decoded = decoder.decode(auth);
|
||||
String[] userInfo = new String(decoded).split(":");
|
||||
if (userInfo.length != 2) {
|
||||
throw new IllegalArgumentException("User and Password not specified");
|
||||
}
|
||||
|
||||
String user = userInfo[0];
|
||||
String passwordRaw = userInfo[1];
|
||||
|
||||
SecurityTable securityTable = plugin.getDB().getSecurityTable();
|
||||
if (!securityTable.userExists(user)) {
|
||||
throw new IllegalArgumentException("User Doesn't exist");
|
||||
}
|
||||
|
||||
WebUser webUser = securityTable.getWebUser(user);
|
||||
|
||||
boolean correctPass = PassEncryptUtil.verifyPassword(passwordRaw, webUser.getSaltedPassHash());
|
||||
if (!correctPass) {
|
||||
throw new IllegalArgumentException("User and Password do not match");
|
||||
}
|
||||
|
||||
Benchmark.stop("getUser: " + requestHeaders);
|
||||
return webUser;
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.debug("WebServer: " + e.getMessage());
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
Log.toLog(this.getClass().getName(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean startHttpsServer() {
|
||||
String keyStorePath = Settings.WEBSERVER_CERTIFICATE_PATH.toString();
|
||||
if (!Paths.get(keyStorePath).isAbsolute()) {
|
||||
@ -286,212 +160,6 @@ public class WebServer {
|
||||
return startSuccessful;
|
||||
}
|
||||
|
||||
private String readPOSTRequest(HttpExchange exchange) throws IOException {
|
||||
byte[] bytes;
|
||||
|
||||
try (InputStream in = exchange.getRequestBody()) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
for (int n = in.read(buf); n > 0; n = in.read(buf)) {
|
||||
out.write(buf, 0, n);
|
||||
}
|
||||
|
||||
bytes = out.toByteArray();
|
||||
}
|
||||
|
||||
try {
|
||||
return new String(bytes, StandardCharsets.ISO_8859_1);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Response getAPIResponse(String target, HttpExchange exchange) throws IOException {
|
||||
String[] args = target.split("/");
|
||||
|
||||
if (args.length < 3) {
|
||||
String error = "API Method not specified";
|
||||
return PageCache.loadPage(error, () -> new BadRequestResponse(error));
|
||||
}
|
||||
|
||||
String method = args[2];
|
||||
String response = readPOSTRequest(exchange);
|
||||
|
||||
if (response == null) {
|
||||
String error = "Error at reading the POST request." +
|
||||
"Note that the Encoding must be ISO-8859-1.";
|
||||
return PageCache.loadPage(error, () -> new BadRequestResponse(error));
|
||||
}
|
||||
|
||||
Map<String, String> variables = readVariables(response);
|
||||
String key = variables.get("key");
|
||||
|
||||
if (!checkKey(key)) {
|
||||
String error = "Server Key not given or invalid";
|
||||
return PageCache.loadPage(error, () -> {
|
||||
ForbiddenResponse forbidden = new ForbiddenResponse();
|
||||
forbidden.setContent(error);
|
||||
return forbidden;
|
||||
});
|
||||
}
|
||||
|
||||
WebAPI api = WebAPIManager.getAPI(method);
|
||||
|
||||
if (api == null) {
|
||||
String error = "API Method not found";
|
||||
return PageCache.loadPage(error, () -> new BadRequestResponse(error));
|
||||
}
|
||||
|
||||
try {
|
||||
return api.onResponse(Plan.getInstance(), variables);
|
||||
} catch (Exception ex) {
|
||||
Log.toLog("WebServer.getAPIResponse", ex);
|
||||
return new InternalErrorResponse(ex, "An error while processing the request happened");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkKey(String key) {
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UUID uuid = Plan.getServerUUID();
|
||||
UUID keyUUID;
|
||||
try {
|
||||
keyUUID = UUID.fromString(key);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return uuid.equals(keyUUID);
|
||||
}
|
||||
|
||||
private Map<String, String> readVariables(String response) {
|
||||
String[] variables = response.split("&");
|
||||
|
||||
return Arrays.stream(variables)
|
||||
.map(variable -> variable.split("=", 2))
|
||||
.filter(splittedVariables -> splittedVariables.length == 2)
|
||||
.collect(Collectors.toMap(splittedVariables -> splittedVariables[0], splittedVariables -> splittedVariables[1], (a, b) -> b));
|
||||
}
|
||||
|
||||
private Response getResponse(String target, WebUser user) {
|
||||
if ("/favicon.ico".equals(target)) {
|
||||
return PageCache.loadPage("Redirect: favicon", () -> new RedirectResponse("https://puu.sh/tK0KL/6aa2ba141b.ico"));
|
||||
}
|
||||
|
||||
if (usingHttps) {
|
||||
if (user == null) {
|
||||
return PageCache.loadPage("promptAuthorization", PromptAuthorizationResponse::new);
|
||||
}
|
||||
|
||||
int permLevel = user.getPermLevel(); // Lower number has higher clearance.
|
||||
int required = getRequiredPermLevel(target, user.getName());
|
||||
if (permLevel > required) {
|
||||
return forbiddenResponse(permLevel, required);
|
||||
}
|
||||
}
|
||||
|
||||
boolean javaScriptRequest = target.endsWith(".js");
|
||||
boolean cssRequest = target.endsWith(".css");
|
||||
|
||||
String[] args = target.split("/");
|
||||
if (args.length < 2) {
|
||||
return rootPageResponse(user);
|
||||
}
|
||||
|
||||
if (javaScriptRequest) {
|
||||
return getJSResponse(args[args.length - 1]);
|
||||
}
|
||||
if (cssRequest) {
|
||||
try {
|
||||
return new CSSResponse("main.css");
|
||||
} catch (Exception e) {
|
||||
return new InternalErrorResponse(e, target);
|
||||
}
|
||||
}
|
||||
|
||||
String page = args[1];
|
||||
switch (page) {
|
||||
case "players":
|
||||
return PageCache.loadPage("players", PlayersPageResponse::new);
|
||||
case "player":
|
||||
return playerResponse(args);
|
||||
case "server":
|
||||
return serverResponse();
|
||||
default:
|
||||
return notFoundResponse();
|
||||
}
|
||||
}
|
||||
|
||||
private Response getJSResponse(String fileName) {
|
||||
try {
|
||||
return new JavaScriptResponse(fileName);
|
||||
} catch (Exception e) {
|
||||
return new InternalErrorResponse(e, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
private Response forbiddenResponse(int permLevel, int required) {
|
||||
return PageCache.loadPage("forbidden", () ->
|
||||
new ForbiddenResponse("Unauthorized User.<br>"
|
||||
+ "Make sure your user has the correct access level.<br>"
|
||||
+ "This page requires permission level of " + required + ",<br>"
|
||||
+ "This user has permission level of " + permLevel));
|
||||
}
|
||||
|
||||
private Response rootPageResponse(WebUser user) {
|
||||
if (user == null) {
|
||||
return notFoundResponse();
|
||||
}
|
||||
|
||||
switch (user.getPermLevel()) {
|
||||
case 0:
|
||||
return serverResponse();
|
||||
case 1:
|
||||
return PageCache.loadPage("players", PlayersPageResponse::new);
|
||||
case 2:
|
||||
return playerResponse(new String[]{"", "", user.getName()});
|
||||
default:
|
||||
return forbiddenResponse(user.getPermLevel(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
private Response serverResponse() {
|
||||
if (!infoManager.isAnalysisCached()) {
|
||||
String error = "Analysis Data was not cached.<br>Use /plan analyze to cache the Data.";
|
||||
PageCache.loadPage("notFound: " + error, () -> new NotFoundResponse(error));
|
||||
}
|
||||
|
||||
return PageCache.loadPage("analysisPage", () -> new AnalysisPageResponse(infoManager));
|
||||
}
|
||||
|
||||
private Response playerResponse(String[] args) {
|
||||
if (args.length < 3) {
|
||||
return PageCache.loadPage("notFound", NotFoundResponse::new);
|
||||
}
|
||||
|
||||
String playerName = args[2].trim();
|
||||
UUID uuid = UUIDUtility.getUUIDOf(playerName);
|
||||
|
||||
if (uuid == null) {
|
||||
String error = "Player has no UUID";
|
||||
return PageCache.loadPage("notFound: " + error, () -> new NotFoundResponse(error));
|
||||
}
|
||||
|
||||
plugin.getInfoManager().cachePlayer(uuid);
|
||||
return PageCache.loadPage("inspectPage: " + uuid, () -> new InspectPageResponse(infoManager, uuid));
|
||||
}
|
||||
|
||||
private Response notFoundResponse() {
|
||||
String error = "404 Not Found";
|
||||
return PageCache.loadPage("notFound: " + error, () ->
|
||||
new NotFoundResponse("Make sure you're accessing a link given by a command, Examples:</p>"
|
||||
+ "<p>" + getProtocol() + ":" + HtmlUtils.getInspectUrl("<player>") + " or<br>"
|
||||
+ getProtocol() + ":" + HtmlUtils.getServerAnalysisUrl())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the WebServer is enabled
|
||||
*/
|
||||
@ -509,33 +177,6 @@ public class WebServer {
|
||||
}
|
||||
}
|
||||
|
||||
private int getRequiredPermLevel(String target, String user) {
|
||||
String[] t = target.split("/");
|
||||
if (t.length < 2) {
|
||||
return 100;
|
||||
}
|
||||
if (t.length > 3) {
|
||||
return 0;
|
||||
}
|
||||
String page = t[1];
|
||||
switch (page) {
|
||||
case "players":
|
||||
return 1;
|
||||
case "player":
|
||||
// /player/ - 404 for perm lvl 1
|
||||
if (t.length < 3) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
final String wantedUser = t[2].toLowerCase().trim();
|
||||
final String theUser = user.trim().toLowerCase();
|
||||
|
||||
return wantedUser.equals(theUser) ? 2 : 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String getProtocol() {
|
||||
return usingHttps ? "https" : "http";
|
||||
}
|
||||
@ -551,4 +192,8 @@ public class WebServer {
|
||||
public String getAccessAddress() {
|
||||
return getProtocol() + ":/" + HtmlUtils.getIP();
|
||||
}
|
||||
|
||||
public WebAPIManager getWebAPI() {
|
||||
return webAPI;
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ public class CSSResponse extends FileResponse {
|
||||
|
||||
public CSSResponse(String fileName) {
|
||||
super(fileName);
|
||||
super.setType(ResponseType.CSS);
|
||||
setContent(Theme.replaceColors(getContent()));
|
||||
}
|
||||
}
|
||||
|
@ -8,5 +8,6 @@ public class JavaScriptResponse extends FileResponse {
|
||||
|
||||
public JavaScriptResponse(String fileName) {
|
||||
super(fileName);
|
||||
super.setType(ResponseType.JAVASCRIPT);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,12 @@
|
||||
package main.java.com.djrapitops.plan.systems.webserver.response;
|
||||
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
/**
|
||||
* @author Rsl1122
|
||||
@ -8,13 +14,21 @@ import java.util.Objects;
|
||||
*/
|
||||
public abstract class Response {
|
||||
|
||||
private String type;
|
||||
private String header;
|
||||
private String content;
|
||||
|
||||
private Headers responseHeaders;
|
||||
|
||||
/**
|
||||
* Class Constructor.
|
||||
*/
|
||||
public Response(ResponseType type) {
|
||||
this.type = type.get();
|
||||
}
|
||||
|
||||
public Response() {
|
||||
this.type = ResponseType.HTML.get();
|
||||
}
|
||||
|
||||
public String getResponse() {
|
||||
@ -54,4 +68,27 @@ public abstract class Response {
|
||||
public int hashCode() {
|
||||
return Objects.hash(header, content);
|
||||
}
|
||||
|
||||
protected void setType(ResponseType type) {
|
||||
this.type = type.get();
|
||||
}
|
||||
|
||||
public void setResponseHeaders(Headers responseHeaders) {
|
||||
this.responseHeaders = responseHeaders;
|
||||
}
|
||||
|
||||
public void send(HttpExchange exchange) throws IOException {
|
||||
responseHeaders.set("Content-Type", type);
|
||||
responseHeaders.set("Content-Encoding", "gzip");
|
||||
exchange.sendResponseHeaders(getCode(), 0);
|
||||
|
||||
try (GZIPOutputStream out = new GZIPOutputStream(exchange.getResponseBody());
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(content.getBytes())) {
|
||||
byte[] buffer = new byte[2048];
|
||||
int count;
|
||||
while ((count = bis.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Licence is provided in the jar as license.yml also here:
|
||||
* https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml
|
||||
*/
|
||||
package main.java.com.djrapitops.plan.systems.webserver.response;
|
||||
|
||||
/**
|
||||
* //TODO Class Javadoc Comment
|
||||
*
|
||||
* @author Rsl1122
|
||||
*/
|
||||
public enum ResponseType {
|
||||
HTML("text/html"),
|
||||
CSS("text/css"),
|
||||
JSON("application/json"),
|
||||
JAVASCRIPT("application/javascript");
|
||||
|
||||
private final String type;
|
||||
|
||||
ResponseType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String get() {
|
||||
return type;
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ package main.java.com.djrapitops.plan.systems.webserver.response.api;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import main.java.com.djrapitops.plan.systems.webserver.response.Response;
|
||||
import main.java.com.djrapitops.plan.systems.webserver.response.ResponseType;
|
||||
|
||||
/**
|
||||
* @author Fuzzlemann
|
||||
@ -13,6 +14,7 @@ import main.java.com.djrapitops.plan.systems.webserver.response.Response;
|
||||
public class JsonResponse extends Response {
|
||||
|
||||
public <T> JsonResponse(T object) {
|
||||
super(ResponseType.JSON);
|
||||
Gson gson = new Gson();
|
||||
|
||||
super.setHeader("HTTP/1.1 200 OK");
|
||||
|
Loading…
Reference in New Issue
Block a user