Jetty support (#2132)

- Refactored Webserver request handling code to be easier to read
- Adds support for larger range of cipher suites and protocols for HTTPS
- Adds support for HTTP/2 and ALPN and as result more concurrent users than before
- APIs are fully compatible with previous code

Thanks to Kopo for assistance

Affects issues:
- Close #1987
This commit is contained in:
Aurora Lahtela 2022-06-19 18:38:40 +03:00 committed by GitHub
parent 862815a537
commit 2aa189798d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1538 additions and 357 deletions

View File

@ -81,6 +81,9 @@ subprojects {
commonsCodecVersion = "1.15"
caffeineVersion = "3.1.1"
mysqlVersion = "8.0.29"
jettyVersion = "11.0.6"
caffeineVersion = "2.9.2"
mysqlVersion = "8.0.26"
sqliteVersion = "3.36.0.3"
hikariVersion = "5.0.1"
slf4jVersion = "1.7.36"

View File

@ -51,6 +51,9 @@ dependencies {
}
mysqlDriver "mysql:mysql-connector-java:$mysqlVersion"
sqliteDriver "org.xerial:sqlite-jdbc:$sqliteVersion"
implementation "org.eclipse.jetty:jetty-server:$jettyVersion"
implementation "org.eclipse.jetty:jetty-alpn-java-server:$jettyVersion"
implementation "org.eclipse.jetty.http2:http2-server:$jettyVersion"
testImplementation project(":api")
testImplementation "com.google.code.gson:gson:$gsonVersion"
@ -166,4 +169,6 @@ shadowJar {
exclude "org/jayway/**/*"
exclude "google/protobuf/**/*"
exclude "jargs/gnu/**/*"
mergeServiceFiles()
}

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.delivery.webserver;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.properties.ServerProperties;
import com.djrapitops.plan.settings.config.PlanConfig;

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.delivery.webserver;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;

View File

@ -35,14 +35,7 @@ public class RequestBodyConverter {
* @return {@link URIQuery}.
*/
public static URIQuery formBody(Request request) {
if (
"POST".equalsIgnoreCase(request.getMethod()) &&
"application/x-www-form-urlencoded".equalsIgnoreCase(request.getHeader("Content-type").orElse(""))
) {
return new URIQuery(new String(request.getRequestBody(), StandardCharsets.UTF_8));
} else {
return new URIQuery("");
}
return new URIQuery(new String(request.getRequestBody(), StandardCharsets.UTF_8));
}
public static <T> T bodyJson(Request request, Gson gson, Class<T> ofType) {

View File

@ -1,257 +0,0 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.URIPath;
import com.djrapitops.plan.delivery.web.resolver.request.URIQuery;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.auth.*;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.PluginLang;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import net.playeranalytics.plugin.server.PluginLogger;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.TextStringBuilder;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* HttpHandler for WebServer request management.
*
* @author AuroraLS3
*/
@Singleton
public class RequestHandler implements HttpHandler {
private final Locale locale;
private final PlanConfig config;
private final DBSystem dbSystem;
private final Addresses addresses;
private final ResponseResolver responseResolver;
private final ResponseFactory responseFactory;
private final PluginLogger logger;
private final ErrorLogger errorLogger;
private final ActiveCookieStore activeCookieStore;
private final PassBruteForceGuard bruteForceGuard;
private List<String> ipWhitelist = null;
private final AtomicBoolean warnedAboutXForwardedSecurityIssue = new AtomicBoolean(false);
@Inject
RequestHandler(
Locale locale,
PlanConfig config,
DBSystem dbSystem,
Addresses addresses,
ResponseResolver responseResolver,
ResponseFactory responseFactory,
ActiveCookieStore activeCookieStore,
PluginLogger logger,
ErrorLogger errorLogger
) {
this.locale = locale;
this.config = config;
this.dbSystem = dbSystem;
this.addresses = addresses;
this.responseResolver = responseResolver;
this.responseFactory = responseFactory;
this.activeCookieStore = activeCookieStore;
this.logger = logger;
this.errorLogger = errorLogger;
bruteForceGuard = new PassBruteForceGuard();
}
@Override
public void handle(HttpExchange exchange) {
try {
Response response = getResponse(exchange);
response.getHeaders().putIfAbsent("Access-Control-Allow-Origin", config.get(WebserverSettings.CORS_ALLOW_ORIGIN));
response.getHeaders().putIfAbsent("Access-Control-Allow-Methods", "GET, OPTIONS");
response.getHeaders().putIfAbsent("Access-Control-Allow-Credentials", "true");
response.getHeaders().putIfAbsent("X-Robots-Tag", "noindex, nofollow");
ResponseSender sender = new ResponseSender(addresses, exchange, response);
sender.send();
} catch (Exception e) {
if (config.isTrue(PluginSettings.DEV_MODE)) {
logger.warn("THIS ERROR IS ONLY LOGGED IN DEV MODE:");
errorLogger.warn(e, ErrorContext.builder()
.whatToDo("THIS ERROR IS ONLY LOGGED IN DEV MODE")
.related(exchange.getRequestMethod(), exchange.getRemoteAddress(), exchange.getRequestHeaders(), exchange.getResponseHeaders(), exchange.getRequestURI())
.build());
}
} finally {
exchange.close();
}
}
public Response getResponse(HttpExchange exchange) {
if (ipWhitelist == null) {
ipWhitelist = config.isTrue(WebserverSettings.IP_WHITELIST)
? config.get(WebserverSettings.WHITELIST)
: Collections.emptyList();
}
String accessor = getAccessorAddress(exchange);
Request request = null;
Response response;
try {
request = buildRequest(exchange);
if (bruteForceGuard.shouldPreventRequest(accessor)) {
response = responseFactory.failedLoginAttempts403();
} else if (!ipWhitelist.isEmpty() && !ipWhitelist.contains(accessor)) {
response = responseFactory.ipWhitelist403(accessor);
logger.info(locale.getString(PluginLang.WEB_SERVER_NOTIFY_IP_WHITELIST_BLOCK, accessor, exchange.getRequestURI().toString()));
} else {
response = responseResolver.getResponse(request);
}
} catch (WebUserAuthException thrownByAuthentication) {
FailReason failReason = thrownByAuthentication.getFailReason();
if (failReason == FailReason.USER_PASS_MISMATCH) {
bruteForceGuard.increaseAttemptCountOnFailedLogin(accessor);
response = responseFactory.badRequest(failReason.getReason(), "/auth/login");
} else {
String from = exchange.getRequestURI().toASCIIString();
String directTo = StringUtils.startsWithAny(from, "/auth/", "/login") ? "/login" : "/login?from=." + from;
response = Response.builder()
.redirectTo(directTo)
.setHeader("Set-Cookie", "auth=expired; Path=/; Max-Age=0; SameSite=Lax; Secure;")
.build();
}
}
if (bruteForceGuard.shouldPreventRequest(accessor)) {
response = responseFactory.failedLoginAttempts403();
}
if (response.getCode() != 401 // Not failed
&& response.getCode() != 403 // Not blocked
&& request != null && request.getUser().isPresent() // Logged in
) {
bruteForceGuard.resetAttemptCount(accessor);
}
return response;
}
private String getAccessorAddress(HttpExchange exchange) {
if (config.isTrue(WebserverSettings.IP_WHITELIST_X_FORWARDED)) {
String header = exchange.getRequestHeaders().getFirst("X-Forwarded-For");
if (header == null) {
warnAboutXForwardedForSecurityIssue();
} else {
return header;
}
}
return exchange.getRemoteAddress().getAddress().getHostAddress();
}
private void warnAboutXForwardedForSecurityIssue() {
if (!warnedAboutXForwardedSecurityIssue.get()) {
logger.warn("Security Vulnerability due to misconfiguration: X-Forwarded-For header was not present in a request & '" +
WebserverSettings.IP_WHITELIST_X_FORWARDED.getPath() + "' is 'true'!");
logger.warn("This could mean non-reverse-proxy access is not blocked & someone can use IP Spoofing to bypass security!");
logger.warn("Make sure you can only access Plan panel from your reverse-proxy or disable this setting.");
}
warnedAboutXForwardedSecurityIssue.set(true);
}
private Request buildRequest(HttpExchange exchange) {
String requestMethod = exchange.getRequestMethod();
URIPath path = new URIPath(exchange.getRequestURI().getPath());
URIQuery query = new URIQuery(exchange.getRequestURI().getRawQuery());
byte[] requestBody = readRequestBody(exchange);
WebUser user = getWebUser(exchange);
Map<String, String> headers = getRequestHeaders(exchange);
return new Request(requestMethod, path, query, user, headers, requestBody);
}
private byte[] readRequestBody(HttpExchange exchange) {
try (ByteArrayOutputStream buf = new ByteArrayOutputStream(512)) {
int b;
while ((b = exchange.getRequestBody().read()) != -1) {
buf.write((byte) b);
}
return buf.toByteArray();
} catch (IOException ignored) {
// requestBody stays empty
return new byte[0];
}
}
private WebUser getWebUser(HttpExchange exchange) {
return getAuthentication(exchange.getRequestHeaders())
.map(Authentication::getUser) // Can throw WebUserAuthException
.map(User::toWebUser)
.orElse(null);
}
private Map<String, String> getRequestHeaders(HttpExchange exchange) {
Map<String, String> headers = new HashMap<>();
for (Map.Entry<String, List<String>> e : exchange.getRequestHeaders().entrySet()) {
List<String> value = e.getValue();
headers.put(e.getKey(), new TextStringBuilder().appendWithSeparators(value, ";").build());
}
return headers;
}
private Optional<Authentication> getAuthentication(Headers requestHeaders) {
if (config.isTrue(WebserverSettings.DISABLED_AUTHENTICATION)) {
return Optional.empty();
}
List<String> cookies = requestHeaders.get("Cookie");
if (cookies != null && !cookies.isEmpty()) {
for (String cookie : new TextStringBuilder().appendWithSeparators(cookies, ";").build().split(";")) {
String[] split = cookie.trim().split("=", 2);
String name = split[0];
String value = split[1];
if ("auth".equals(name)) {
return Optional.of(new CookieAuthentication(activeCookieStore, value));
}
}
}
List<String> authorization = requestHeaders.get("Authorization");
if (authorization == null || authorization.isEmpty()) return Optional.empty();
String authLine = authorization.get(0);
if (StringUtils.contains(authLine, "Basic ")) {
return Optional.of(new BasicAuthentication(StringUtils.split(authLine, ' ')[1], dbSystem.getDatabase()));
}
return Optional.empty();
}
public ResponseResolver getResponseResolver() {
return responseResolver;
}
}

View File

@ -26,6 +26,7 @@ import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.delivery.webserver.resolver.*;
import com.djrapitops.plan.delivery.webserver.resolver.auth.*;
import com.djrapitops.plan.delivery.webserver.resolver.json.RootJSONResolver;

View File

@ -19,6 +19,7 @@ package com.djrapitops.plan.delivery.webserver;
import com.djrapitops.plan.SubSystem;
import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import javax.inject.Inject;
import javax.inject.Singleton;

View File

@ -0,0 +1,54 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.auth;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
@Singleton
public class AllowedIpList {
private final PlanConfig config;
private final AtomicReference<List<String>> allowList = new AtomicReference<>(null);
@Inject
public AllowedIpList(PlanConfig config) {
this.config = config;
}
private synchronized void prepare() {
if (allowList.get() == null) {
allowList.set(config.isTrue(WebserverSettings.IP_WHITELIST)
? config.get(WebserverSettings.WHITELIST)
: Collections.emptyList());
}
}
public boolean isAllowed(String accessAddress) {
prepare();
List<String> ips = allowList.get();
return ips.isEmpty() || ips.contains(accessAddress);
}
}

View File

@ -0,0 +1,48 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.auth;
import com.djrapitops.plan.delivery.webserver.http.InternalRequest;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.List;
import java.util.Optional;
@Singleton
public class AuthenticationExtractor {
private final ActiveCookieStore activeCookieStore;
@Inject
public AuthenticationExtractor(ActiveCookieStore activeCookieStore) {
this.activeCookieStore = activeCookieStore;
}
public Optional<Authentication> extractAuthentication(InternalRequest internalRequest) {
return getCookieAuthentication(internalRequest.getCookies());
}
private Optional<Authentication> getCookieAuthentication(List<Cookie> cookies) {
for (Cookie cookie : cookies) {
if ("auth".equals(cookie.getName())) {
return Optional.of(new CookieAuthentication(activeCookieStore, cookie.getValue()));
}
}
return Optional.empty();
}
}

View File

@ -1,78 +0,0 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.auth;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.exceptions.PassEncryptException;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.exceptions.database.DBOpException;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries;
import com.djrapitops.plan.utilities.Base64Util;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
/**
* Authentication handling for Basic Auth.
* <p>
* Basic access authentication (Wikipedia):
* https://en.wikipedia.org/wiki/Basic_access_authentication
*
* @author AuroraLS3
*/
public class BasicAuthentication implements Authentication {
private final String authenticationString;
private final Database database;
public BasicAuthentication(String authenticationString, Database database) {
this.authenticationString = authenticationString;
this.database = database;
}
@Override
public User getUser() {
String decoded = Base64Util.decode(authenticationString);
String[] userInfo = StringUtils.split(decoded, ':');
if (userInfo.length != 2) {
throw new WebUserAuthException(FailReason.USER_AND_PASS_NOT_SPECIFIED, Arrays.toString(userInfo));
}
String username = userInfo[0];
String passwordRaw = userInfo[1];
Database.State dbState = database.getState();
if (dbState != Database.State.OPEN) {
throw new WebUserAuthException(FailReason.DATABASE_NOT_OPEN, "State was: " + dbState.name());
}
try {
User user = database.query(WebUserQueries.fetchUser(username))
.orElseThrow(() -> new WebUserAuthException(FailReason.USER_DOES_NOT_EXIST, username));
boolean correctPass = user.doesPasswordMatch(passwordRaw);
if (!correctPass) {
throw new WebUserAuthException(FailReason.USER_PASS_MISMATCH, username);
}
return user;
} catch (DBOpException | PassEncryptException e) {
throw new WebUserAuthException(e);
}
}
}

View File

@ -0,0 +1,44 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.auth;
import org.apache.commons.lang3.StringUtils;
public class Cookie {
private final String name;
private final String value;
public Cookie(String rawRepresentation) {
String[] split = StringUtils.split(rawRepresentation, "=", 2);
name = split[0];
value = split[1];
}
public Cookie(String name, String value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
}

View File

@ -0,0 +1,110 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.configuration;
import com.djrapitops.plan.delivery.webserver.auth.AllowedIpList;
import com.djrapitops.plan.delivery.webserver.http.AccessAddressPolicy;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.storage.file.PlanFiles;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
@Singleton
public class WebserverConfiguration {
private final PlanFiles files;
private final PlanConfig config;
private final AllowedIpList allowedIpList;
private final WebserverLogMessages webserverLogMessages;
@Inject
public WebserverConfiguration(PlanFiles files, PlanConfig config, AllowedIpList allowedIpList, WebserverLogMessages webserverLogMessages) {
this.files = files;
this.config = config;
this.allowedIpList = allowedIpList;
this.webserverLogMessages = webserverLogMessages;
}
public WebserverLogMessages getWebserverLogMessages() {
return webserverLogMessages;
}
public boolean isAuthenticationDisabled() {
return config.isTrue(WebserverSettings.DISABLED_AUTHENTICATION);
}
public boolean isAuthenticationEnabled() {
return config.isFalse(WebserverSettings.DISABLED_AUTHENTICATION);
}
public AccessAddressPolicy getAccessAddressPolicy() {
return config.isTrue(WebserverSettings.IP_USE_X_FORWARDED_FOR)
? AccessAddressPolicy.X_FORWARDED_FOR_HEADER
: AccessAddressPolicy.SOCKET_IP;
}
public AllowedIpList getAllowedIpList() {
return allowedIpList;
}
public String getAllowedCorsOrigin() {
return config.get(WebserverSettings.CORS_ALLOW_ORIGIN);
}
public int getPort() {
return config.get(WebserverSettings.PORT);
}
public boolean isWebserverDisabled() {
return config.isTrue(WebserverSettings.DISABLED);
}
public String getKeyStorePath() {
String keyStorePath = config.get(WebserverSettings.CERTIFICATE_PATH);
if ("proxy".equalsIgnoreCase(keyStorePath)) {
return keyStorePath;
}
try {
if (!Paths.get(keyStorePath).isAbsolute()) {
keyStorePath = files.getDataFolder() + File.separator + keyStorePath;
}
} catch (InvalidPathException e) {
webserverLogMessages.keystoreNotFoundError(e, keyStorePath);
}
return keyStorePath;
}
public String getKeyStorePassword() {
return config.get(WebserverSettings.CERTIFICATE_STOREPASS);
}
public String getKeyManagerPassword() {
return config.get(WebserverSettings.CERTIFICATE_KEYPASS);
}
public String getAlias() {
return config.get(WebserverSettings.CERTIFICATE_ALIAS);
}
}

View File

@ -0,0 +1,98 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.configuration;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.PluginLang;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import net.playeranalytics.plugin.server.PluginLogger;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.InvalidPathException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Singleton
public class WebserverLogMessages {
private final PluginLogger logger;
private final ErrorLogger errorLogger;
private final Locale locale;
private final Addresses addresses;
private final AtomicLong warnedAboutXForwardedSecurityIssue = new AtomicLong(0L);
@Inject
public WebserverLogMessages(PluginLogger logger, ErrorLogger errorLogger, Locale locale, Addresses addresses) {
this.logger = logger;
this.errorLogger = errorLogger;
this.locale = locale;
this.addresses = addresses;
}
public void warnAboutXForwardedForSecurityIssue() {
if (System.currentTimeMillis() - warnedAboutXForwardedSecurityIssue.get() > TimeUnit.MINUTES.toMillis(2L)) {
logger.warn("Security Vulnerability due to misconfiguration: X-Forwarded-For header was not present in a request & '" +
WebserverSettings.IP_USE_X_FORWARDED_FOR.getPath() + "' is 'true'!");
logger.warn("This could mean non-reverse-proxy access is not blocked & someone can use IP Spoofing to bypass security!");
logger.warn("Make sure you can only access Plan panel from your reverse-proxy or disable this setting.");
warnedAboutXForwardedSecurityIssue.set(System.currentTimeMillis());
}
}
public void warnAboutWhitelistBlock(String accessAddress, String requestedURIString) {
logger.info(locale.getString(PluginLang.WEB_SERVER_NOTIFY_IP_WHITELIST_BLOCK, accessAddress, requestedURIString));
}
public void infoWebserverEnabled(int port) {
String address = addresses.getAccessAddress().orElse(addresses.getFallbackLocalhostAddress());
logger.info(locale.getString(PluginLang.ENABLED_WEB_SERVER, port, address));
}
public void warnWebserverDisabledByConfig() {
logger.warn(locale.getString(PluginLang.ENABLE_NOTIFY_WEB_SERVER_DISABLED));
}
public void keystoreNotFoundError(InvalidPathException error, String keyStorePath) {
String errorMessage = error.getMessage();
logger.error("WebServer: Could not find Keystore: " + errorMessage);
errorLogger.error(error, ErrorContext.builder()
.whatToDo(errorMessage + ", Fix this path to point to a valid keystore file: " + keyStorePath)
.related(keyStorePath).build());
}
public void authenticationNotPossible() {
logger.info(locale.getString(PluginLang.WEB_SERVER_NOTIFY_HTTP));
logger.info(locale.getString(PluginLang.WEB_SERVER_NOTIFY_HTTP_USER_AUTH));
}
public void authenticationUsingProxy() {
logger.info(locale.getString(PluginLang.WEB_SERVER_NOTIFY_USING_PROXY_MODE));
}
public void invalidCertificate() {
logger.warn(locale.getString(PluginLang.WEB_SERVER_FAIL_STORE_LOAD));
}
public void keystoreFileNotFound() {
logger.info(locale.getString(PluginLang.WEB_SERVER_NOTIFY_NO_CERT_FILE));
}
}

View File

@ -0,0 +1,24 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.http;
public enum AccessAddressPolicy {
SOCKET_IP,
X_FORWARDED_FOR_HEADER
}

View File

@ -0,0 +1,74 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.auth.Authentication;
import com.djrapitops.plan.delivery.webserver.auth.AuthenticationExtractor;
import com.djrapitops.plan.delivery.webserver.auth.Cookie;
import com.djrapitops.plan.delivery.webserver.configuration.WebserverConfiguration;
import java.util.List;
import java.util.Optional;
/**
* Represents a HTTP request.
*
* @see com.djrapitops.plan.delivery.web.resolver.request.Request for API based request, as this interface is for internal use.
*/
public interface InternalRequest {
default String getAccessAddress(WebserverConfiguration webserverConfiguration) {
AccessAddressPolicy accessAddressPolicy = webserverConfiguration.getAccessAddressPolicy();
if (accessAddressPolicy == AccessAddressPolicy.X_FORWARDED_FOR_HEADER) {
String fromHeader = getAccessAddressFromHeader();
if (fromHeader == null) {
webserverConfiguration.getWebserverLogMessages().warnAboutXForwardedForSecurityIssue();
return getAccessAddressFromSocketIp();
} else {
return fromHeader;
}
}
return getAccessAddressFromSocketIp();
}
Request toRequest();
List<Cookie> getCookies();
String getAccessAddressFromSocketIp();
String getAccessAddressFromHeader();
String getRequestedURIString();
default WebUser getWebUser(WebserverConfiguration webserverConfiguration, AuthenticationExtractor authenticationExtractor) {
return getAuthentication(webserverConfiguration, authenticationExtractor)
.map(Authentication::getUser) // Can throw WebUserAuthException
.map(User::toWebUser)
.orElse(null);
}
default Optional<Authentication> getAuthentication(WebserverConfiguration webserverConfiguration, AuthenticationExtractor authenticationExtractor) {
if (webserverConfiguration.isAuthenticationDisabled()) {
return Optional.empty();
}
return authenticationExtractor.extractAuthentication(this);
}
}

View File

@ -0,0 +1,133 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.delivery.web.resolver.request.URIPath;
import com.djrapitops.plan.delivery.web.resolver.request.URIQuery;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.auth.AuthenticationExtractor;
import com.djrapitops.plan.delivery.webserver.auth.Cookie;
import com.djrapitops.plan.delivery.webserver.configuration.WebserverConfiguration;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.text.TextStringBuilder;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.server.Request;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Spliterators;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class JettyInternalRequest implements InternalRequest {
private final Request baseRequest;
private final HttpServletRequest request;
private final WebserverConfiguration webserverConfiguration;
private final AuthenticationExtractor authenticationExtractor;
public JettyInternalRequest(Request baseRequest, HttpServletRequest request, WebserverConfiguration webserverConfiguration, AuthenticationExtractor authenticationExtractor) {
this.baseRequest = baseRequest;
this.request = request;
this.webserverConfiguration = webserverConfiguration;
this.authenticationExtractor = authenticationExtractor;
}
@Override
public String getAccessAddressFromSocketIp() {
return baseRequest.getRemoteAddr();
}
@Override
public String getAccessAddressFromHeader() {
return baseRequest.getHeader(HttpHeader.X_FORWARDED_FOR.asString());
}
@Override
public com.djrapitops.plan.delivery.web.resolver.request.Request toRequest() {
String requestMethod = baseRequest.getMethod();
URIPath path = new URIPath(baseRequest.getHttpURI().getDecodedPath());
URIQuery query = new URIQuery(baseRequest.getHttpURI().getQuery());
byte[] requestBody = readRequestBody();
WebUser user = getWebUser(webserverConfiguration, authenticationExtractor);
Map<String, String> headers = getRequestHeaders();
return new com.djrapitops.plan.delivery.web.resolver.request.Request(requestMethod, path, query, user, headers, requestBody);
}
private Map<String, String> getRequestHeaders() {
return streamHeaderNames()
.collect(Collectors.toMap(Function.identity(), baseRequest::getHeader,
(one, two) -> one + ';' + two));
}
private Stream<String> streamHeaderNames() {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(baseRequest.getHeaderNames().asIterator(), 0), false);
}
private byte[] readRequestBody() {
try (BufferedReader reader = request.getReader();
ByteArrayOutputStream buf = new ByteArrayOutputStream(512)) {
int b;
while ((b = reader.read()) != -1) {
buf.write((byte) b);
}
return buf.toByteArray();
} catch (IOException ignored) {
// requestBody stays empty
return new byte[0];
}
}
@Override
public List<Cookie> getCookies() {
List<String> textCookies = getCookieHeaders();
List<Cookie> cookies = new ArrayList<>();
if (!textCookies.isEmpty()) {
String[] separated = new TextStringBuilder().appendWithSeparators(textCookies, ";").build().split(";");
for (String textCookie : separated) {
cookies.add(new Cookie(textCookie));
}
}
return cookies;
}
private List<String> getCookieHeaders() {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(request.getHeaders(HttpHeader.COOKIE.asString()).asIterator(), 0), false)
.collect(Collectors.toList());
}
@Override
public String getRequestedURIString() {
return baseRequest.getRequestURI();
}
@Override
public String toString() {
return "JettyInternalRequest{" +
"baseRequest=" + baseRequest +
", request=" + request +
", webserverConfiguration=" + webserverConfiguration +
", authenticationExtractor=" + authenticationExtractor +
'}';
}
}

View File

@ -0,0 +1,78 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.djrapitops.plan.delivery.webserver.auth.AuthenticationExtractor;
import com.djrapitops.plan.delivery.webserver.configuration.WebserverConfiguration;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import net.playeranalytics.plugin.server.PluginLogger;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
@Singleton
public class JettyRequestHandler extends AbstractHandler {
private final WebserverConfiguration webserverConfiguration;
private final AuthenticationExtractor authenticationExtractor;
private final Addresses addresses;
private final RequestHandler requestHandler;
private final PlanConfig config;
private final PluginLogger logger;
private final ErrorLogger errorLogger;
@Inject
public JettyRequestHandler(WebserverConfiguration webserverConfiguration, AuthenticationExtractor authenticationExtractor, Addresses addresses, RequestHandler requestHandler, PlanConfig config, PluginLogger logger, ErrorLogger errorLogger) {
this.webserverConfiguration = webserverConfiguration;
this.authenticationExtractor = authenticationExtractor;
this.addresses = addresses;
this.requestHandler = requestHandler;
this.config = config;
this.logger = logger;
this.errorLogger = errorLogger;
}
@Override
public void handle(String target, Request baseRequest, HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException {
try {
InternalRequest internalRequest = new JettyInternalRequest(baseRequest, servletRequest, webserverConfiguration, authenticationExtractor);
Response response = requestHandler.getResponse(internalRequest);
new JettyResponseSender(response, servletRequest, servletResponse, addresses).send();
baseRequest.setHandled(true);
} catch (Exception e) {
if (config.isTrue(PluginSettings.DEV_MODE)) {
logger.warn("THIS ERROR IS ONLY LOGGED IN DEV MODE:");
errorLogger.warn(e, ErrorContext.builder()
.whatToDo("THIS ERROR IS ONLY LOGGED IN DEV MODE")
.related(baseRequest.getMethod(), baseRequest.getRemoteAddr(), target, baseRequest.getRequestURI())
.build());
}
}
}
}

View File

@ -0,0 +1,118 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.webserver.Addresses;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpHeader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
import java.util.zip.GZIPOutputStream;
public class JettyResponseSender {
private final Response response;
private final HttpServletRequest servletRequest;
private final HttpServletResponse servletResponse;
private final Addresses addresses;
public JettyResponseSender(Response response, HttpServletRequest servletRequest, HttpServletResponse servletResponse, Addresses addresses) {
this.response = response;
this.servletRequest = servletRequest;
this.servletResponse = servletResponse;
this.addresses = addresses;
}
public void send() throws IOException {
setResponseHeaders();
if ("HEAD".equals(servletRequest.getMethod()) || response.getCode() == 204) {
sendHeadResponse();
} else if ("bytes".equalsIgnoreCase(response.getHeaders().get(HttpHeader.ACCEPT_RANGES.asString()))) {
sendRawBytes();
} else {
sendCompressed();
}
}
public void sendHeadResponse() throws IOException {
try {
response.getHeaders().remove(HttpHeader.CONTENT_LENGTH.asString());
beginSend();
} finally {
servletResponse.getOutputStream().close();
}
}
private void setResponseHeaders() {
Map<String, String> responseHeaders = response.getHeaders();
correctRedirect(responseHeaders);
for (Map.Entry<String, String> header : responseHeaders.entrySet()) {
servletResponse.setHeader(header.getKey(), header.getValue());
}
}
private void correctRedirect(Map<String, String> responseHeaders) {
String redirect = responseHeaders.get("Location");
if (redirect != null) {
if (redirect.startsWith("http") || !redirect.startsWith("/")) return;
addresses.getAccessAddress().ifPresent(address -> responseHeaders.put("Location", address + redirect));
}
}
private void sendCompressed() throws IOException {
servletResponse.setHeader(HttpHeader.CONTENT_ENCODING.asString(), "gzip");
beginSend();
try (OutputStream out = new GZIPOutputStream(servletResponse.getOutputStream())) {
send(out);
}
}
private void beginSend() throws IOException {
String length = response.getHeaders().get(HttpHeader.CONTENT_LENGTH.asString());
if (length == null || "0".equals(length) || response.getCode() == 204 || "HEAD".equals(servletRequest.getMethod())) {
servletResponse.setHeader(HttpHeader.CONTENT_LENGTH.asString(), null);
}
// Return a content length of -1 for HTTP code 204 (No content)
// and HEAD requests to avoid warning messages.
servletResponse.setStatus(response.getCode());
}
private void sendRawBytes() throws IOException {
beginSend();
try (OutputStream out = servletResponse.getOutputStream()) {
send(out);
}
}
private void send(OutputStream out) throws IOException {
try (
ByteArrayInputStream bis = new ByteArrayInputStream(response.getBytes())
) {
byte[] buffer = new byte[2048];
int count;
while ((count = bis.read(buffer)) != -1) {
out.write(buffer, 0, count);
}
}
}
}

View File

@ -0,0 +1,222 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.delivery.webserver.ResponseResolver;
import com.djrapitops.plan.delivery.webserver.configuration.WebserverConfiguration;
import com.djrapitops.plan.delivery.webserver.configuration.WebserverLogMessages;
import com.djrapitops.plan.exceptions.EnableException;
import net.playeranalytics.plugin.server.PluginLogger;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.SecuredRedirectHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.util.Optional;
@Singleton
public class JettyWebserver implements WebServer {
private final PluginLogger logger;
private final WebserverConfiguration webserverConfiguration;
private final LegacyJettySSLContextLoader legacyJettySSLContextLoader;
private final JettyRequestHandler jettyRequestHandler;
private final ResponseResolver responseResolver;
private final WebserverLogMessages webserverLogMessages;
private int port;
private boolean usingHttps;
private Server webserver;
@Inject
public JettyWebserver(PluginLogger logger, WebserverConfiguration webserverConfiguration, LegacyJettySSLContextLoader legacyJettySSLContextLoader, JettyRequestHandler jettyRequestHandler, ResponseResolver responseResolver) {
this.logger = logger;
this.webserverConfiguration = webserverConfiguration;
webserverLogMessages = webserverConfiguration.getWebserverLogMessages();
this.legacyJettySSLContextLoader = legacyJettySSLContextLoader;
this.jettyRequestHandler = jettyRequestHandler;
this.responseResolver = responseResolver;
}
@Override
public void enable() {
if (isEnabled()) return;
if (webserverConfiguration.isWebserverDisabled()) {
webserverLogMessages.warnWebserverDisabledByConfig();
return;
}
webserver = new Server();
this.port = webserverConfiguration.getPort();
HttpConfiguration configuration = new HttpConfiguration();
Optional<SslContextFactory.Server> sslContext = getSslContextFactory();
sslContext.ifPresent(ssl -> {
configuration.setSecureScheme("https");
configuration.setSecurePort(port);
SecureRequestCustomizer serverNameIdentifierCheckSkipper = new SecureRequestCustomizer();
serverNameIdentifierCheckSkipper.setSniHostCheck(false);
serverNameIdentifierCheckSkipper.setSniRequired(false);
configuration.addCustomizer(serverNameIdentifierCheckSkipper);
usingHttps = true;
});
HttpConnectionFactory httpConnector = new HttpConnectionFactory(configuration);
HTTP2CServerConnectionFactory http2CConnector = new HTTP2CServerConnectionFactory(configuration);
http2CConnector.setConnectProtocolEnabled(true);
ServerConnector connector = sslContext
.map(sslContextFactory -> {
HTTP2ServerConnectionFactory http2Connector = new HTTP2ServerConnectionFactory(configuration);
http2Connector.setConnectProtocolEnabled(true);
ALPNServerConnectionFactory alpn = getAlpnServerConnectionFactory(httpConnector.getProtocol());
return new ServerConnector(webserver, sslContextFactory, alpn, httpConnector, http2Connector, http2CConnector);
})
.orElseGet(() -> {
webserverLogMessages.authenticationNotPossible();
return new ServerConnector(webserver, httpConnector, http2CConnector);
});
connector.setPort(port);
webserver.addConnector(connector);
if (usingHttps) {
webserver.setHandler(new HandlerList(new SecuredRedirectHandler(), jettyRequestHandler));
} else {
webserver.setHandler(jettyRequestHandler);
}
try {
webserver.start();
} catch (Exception e) {
throw new EnableException("Failed to start Jetty webserver: " + e.toString(), e);
}
webserverLogMessages.infoWebserverEnabled(getPort());
responseResolver.registerPages();
}
private ALPNServerConnectionFactory getAlpnServerConnectionFactory(String protocol) {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
try {
ClassLoader pluginClassLoader = getClass().getClassLoader();
// Jetty uses Thread context classloader, so we need to change to plugin classloader where the ALPNProcessor is.
Thread.currentThread().setContextClassLoader(pluginClassLoader);
Class.forName("org.eclipse.jetty.alpn.java.server.JDK9ServerALPNProcessor");
// ALPN is protocol upgrade protocol required for upgrading http 1.1 connections to 2
ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory("h2", "h2c", "http/1.1");
alpn.setDefaultProtocol(protocol);
return alpn;
} catch (ClassNotFoundException ignored) {
logger.warn("JDK9ServerALPNProcessor not found. ALPN is not available.");
return null;
} finally {
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
}
private Optional<SslContextFactory.Server> getSslContextFactory() {
String keyStorePath = webserverConfiguration.getKeyStorePath();
if ("proxy".equals(keyStorePath)) {
webserverLogMessages.authenticationUsingProxy();
return Optional.empty();
}
if (!new File(keyStorePath).exists()) {
webserverLogMessages.keystoreFileNotFound();
return Optional.empty();
}
String storepass = webserverConfiguration.getKeyStorePassword();
String keypass = webserverConfiguration.getKeyManagerPassword();
String alias = webserverConfiguration.getAlias();
if (keyStorePath.endsWith(".jks") && "DefaultPlanCert".equals(alias)) {
logger.warn("You're using self-signed PlanCert.jks certificate included with Plan.jar (Considered legacy since 5.5), it has expired and can cause issues.");
logger.info("Create new self-signed certificate using openssl:");
logger.info(" openssl req -x509 -newkey rsa:4096 -keyout myKey.pem -out cert.pem -days 3650");
logger.info(" openssl pkcs12 -export -out keyStore.p12 -inkey myKey.pem -in cert.pem -name alias -passout pass:<password> -passin pass:<password>");
logger.info("Then change config settings to match.");
logger.info(" SSL_certificate:");
logger.info(" KeyStore_path: keyStore.p12");
logger.info(" Key_pass: <password>");
logger.info(" Store_pass: <password>");
logger.info(" Alias: alias");
return legacyJettySSLContextLoader.load(keyStorePath, storepass, keypass, alias);
}
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setSniRequired(false);
sslContextFactory.setKeyStorePath(keyStorePath);
sslContextFactory.setKeyStorePassword(storepass);
sslContextFactory.setKeyManagerPassword(keypass);
sslContextFactory.setCertAlias(alias);
return Optional.of(sslContextFactory);
}
@Override
public boolean isEnabled() {
return webserver != null && (webserver.isStarting() || webserver.isStarted());
}
@Override
public void disable() {
try {
if (webserver != null) webserver.stop();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
@Override
public String getProtocol() {
return isUsingHTTPS() ? "https" : "http";
}
@Override
public boolean isUsingHTTPS() {
return usingHttps;
}
@Override
public boolean isAuthRequired() {
return isUsingHTTPS() && webserverConfiguration.isAuthenticationEnabled();
}
@Override
public int getPort() {
return port;
}
}

View File

@ -0,0 +1,102 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.PluginLang;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import net.playeranalytics.plugin.server.PluginLogger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Optional;
@Singleton
public class LegacyJettySSLContextLoader {
private final Locale locale;
private final PluginLogger logger;
private final ErrorLogger errorLogger;
@Inject
public LegacyJettySSLContextLoader(Locale locale, PluginLogger logger, ErrorLogger errorLogger) {
this.locale = locale;
this.logger = logger;
this.errorLogger = errorLogger;
}
public Optional<SslContextFactory.Server> load(String keyStorePath, String storepass, String keypass, String alias) {
String keyStoreKind = keyStorePath.endsWith(".p12") ? "PKCS12" : "JKS";
try (FileInputStream fIn = new FileInputStream(keyStorePath)) {
KeyStore keystore = KeyStore.getInstance(keyStoreKind);
keystore.load(fIn, storepass.toCharArray());
Certificate cert = keystore.getCertificate(alias);
if (cert == null) {
throw new IllegalStateException("Alias: '" + alias + "' was not found in file " + keyStorePath + ".");
}
logger.info("Certificate: " + cert.getType());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keystore, keypass.toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
trustManagerFactory.init(keystore);
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(keyManagerFactory.getKeyManagers(), null/*trustManagerFactory.getTrustManagers()*/, null);
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setSslContext(sslContext);
return Optional.of(sslContextFactory);
} catch (IllegalStateException e) {
logger.error(e.getMessage());
} catch (KeyManagementException | NoSuchAlgorithmException e) {
logger.error(locale.getString(PluginLang.WEB_SERVER_FAIL_SSL_CONTEXT));
errorLogger.error(e, ErrorContext.builder().related(keyStoreKind).build());
} catch (EOFException e) {
logger.error(locale.getString(PluginLang.WEB_SERVER_FAIL_EMPTY_FILE));
} catch (FileNotFoundException e) {
logger.info(locale.getString(PluginLang.WEB_SERVER_NOTIFY_NO_CERT_FILE, keyStorePath));
logger.info(locale.getString(PluginLang.WEB_SERVER_NOTIFY_HTTP));
} catch (IOException e) {
errorLogger.error(e, ErrorContext.builder().related(keyStorePath).build());
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException e) {
logger.error(locale.getString(PluginLang.WEB_SERVER_FAIL_STORE_LOAD));
errorLogger.error(e, ErrorContext.builder()
.whatToDo("Make sure the Certificate settings are correct / You can try remaking the keystore without -passin or -passout parameters.")
.related(keyStorePath).build());
}
return Optional.empty();
}
}

View File

@ -0,0 +1,134 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.webserver.PassBruteForceGuard;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import com.djrapitops.plan.delivery.webserver.ResponseResolver;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
import com.djrapitops.plan.delivery.webserver.configuration.WebserverConfiguration;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.http.HttpHeader;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class RequestHandler {
private final WebserverConfiguration webserverConfiguration;
private final ResponseFactory responseFactory;
private final ResponseResolver responseResolver;
private final PassBruteForceGuard bruteForceGuard;
@Inject
public RequestHandler(WebserverConfiguration webserverConfiguration, ResponseFactory responseFactory, ResponseResolver responseResolver) {
this.webserverConfiguration = webserverConfiguration;
this.responseFactory = responseFactory;
this.responseResolver = responseResolver;
bruteForceGuard = new PassBruteForceGuard();
}
public Response getResponse(InternalRequest internalRequest) {
String accessAddress = internalRequest.getAccessAddress(webserverConfiguration);
if (bruteForceGuard.shouldPreventRequest(accessAddress)) {
return responseFactory.failedLoginAttempts403();
}
if (!webserverConfiguration.getAllowedIpList().isAllowed(accessAddress)) {
webserverConfiguration.getWebserverLogMessages()
.warnAboutWhitelistBlock(accessAddress, internalRequest.getRequestedURIString());
return responseFactory.ipWhitelist403(accessAddress);
}
Response response = attemptToResolve(internalRequest);
response.getHeaders().putIfAbsent("Access-Control-Allow-Origin", webserverConfiguration.getAllowedCorsOrigin());
response.getHeaders().putIfAbsent("Access-Control-Allow-Methods", "GET, OPTIONS");
response.getHeaders().putIfAbsent("Access-Control-Allow-Credentials", "true");
response.getHeaders().putIfAbsent("X-Robots-Tag", "noindex, nofollow");
return response;
}
private Response attemptToResolve(InternalRequest internalRequest) {
String accessAddress = internalRequest.getAccessAddress(webserverConfiguration);
try {
Request request = internalRequest.toRequest();
Response response = protocolUpgradeResponse(request)
.orElseGet(() -> responseResolver.getResponse(request));
request.getUser().ifPresent(user -> processSuccessfulLogin(response.getCode(), accessAddress));
return response;
} catch (WebUserAuthException thrownByAuthentication) {
return processFailedAuthentication(internalRequest, accessAddress, thrownByAuthentication);
}
}
private Optional<Response> protocolUpgradeResponse(Request request) {
Optional<String> upgrade = request.getHeader(HttpHeader.UPGRADE.asString());
if (upgrade.isPresent()) {
String value = upgrade.get();
if ("h2c".equals(value) || "h2".equals(value)) {
return Optional.of(Response.builder()
.setStatus(101)
.setHeader("Connection", HttpHeader.UPGRADE.asString())
.setHeader(HttpHeader.UPGRADE.asString(), value)
.build());
}
}
return Optional.empty();
}
private Response processFailedAuthentication(InternalRequest internalRequest, String accessAddress, WebUserAuthException thrownByAuthentication) {
FailReason failReason = thrownByAuthentication.getFailReason();
if (failReason == FailReason.USER_PASS_MISMATCH) {
return processWrongPassword(accessAddress, failReason);
} else {
String from = internalRequest.getRequestedURIString();
String directTo = StringUtils.startsWithAny(from, "/auth/", "/login") ? "/login" : "/login?from=." + from;
return Response.builder()
.redirectTo(directTo)
.setHeader("Set-Cookie", "auth=expired; Path=/; Max-Age=0; SameSite=Lax; Secure;")
.build();
}
}
private Response processWrongPassword(String accessAddress, FailReason failReason) {
bruteForceGuard.increaseAttemptCountOnFailedLogin(accessAddress);
if (bruteForceGuard.shouldPreventRequest(accessAddress)) {
return responseFactory.failedLoginAttempts403();
} else {
return responseFactory.badRequest(failReason.getReason(), "/auth/login");
}
}
private void processSuccessfulLogin(int responseCode, String accessAddress) {
boolean successfulLogin = responseCode != 401;
boolean notForbidden = responseCode != 403;
if (successfulLogin && notForbidden) {
bruteForceGuard.resetAttemptCount(accessAddress);
}
}
}

View File

@ -0,0 +1,112 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.URIPath;
import com.djrapitops.plan.delivery.web.resolver.request.URIQuery;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.auth.AuthenticationExtractor;
import com.djrapitops.plan.delivery.webserver.auth.Cookie;
import com.djrapitops.plan.delivery.webserver.configuration.WebserverConfiguration;
import com.sun.net.httpserver.HttpExchange;
import org.apache.commons.text.TextStringBuilder;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class SunInternalRequest implements InternalRequest {
private final HttpExchange exchange;
private final WebserverConfiguration webserverConfiguration;
private final AuthenticationExtractor authenticationExtractor;
public SunInternalRequest(HttpExchange exchange, WebserverConfiguration webserverConfiguration, AuthenticationExtractor authenticationExtractor) {
this.exchange = exchange;
this.webserverConfiguration = webserverConfiguration;
this.authenticationExtractor = authenticationExtractor;
}
@Override
public String getAccessAddressFromSocketIp() {
return exchange.getRemoteAddress().getAddress().getHostAddress();
}
@Override
public String getAccessAddressFromHeader() {
return exchange.getRequestHeaders().getFirst("X-Forwarded-For");
}
@Override
public String getRequestedURIString() {
return exchange.getRequestURI().toASCIIString();
}
@Override
public Request toRequest() {
return buildRequest(exchange);
}
private Request buildRequest(HttpExchange exchange) {
String requestMethod = exchange.getRequestMethod();
URIPath path = new URIPath(exchange.getRequestURI().getPath());
URIQuery query = new URIQuery(exchange.getRequestURI().getRawQuery());
byte[] requestBody = readRequestBody(exchange);
WebUser user = getWebUser(webserverConfiguration, authenticationExtractor);
Map<String, String> headers = getRequestHeaders(exchange);
return new Request(requestMethod, path, query, user, headers, requestBody);
}
private byte[] readRequestBody(HttpExchange exchange) {
try (ByteArrayOutputStream buf = new ByteArrayOutputStream(512)) {
int b;
while ((b = exchange.getRequestBody().read()) != -1) {
buf.write((byte) b);
}
return buf.toByteArray();
} catch (IOException ignored) {
// requestBody stays empty
return new byte[0];
}
}
private Map<String, String> getRequestHeaders(HttpExchange exchange) {
Map<String, String> headers = new HashMap<>();
for (Map.Entry<String, List<String>> e : exchange.getRequestHeaders().entrySet()) {
List<String> value = e.getValue();
headers.put(e.getKey(), new TextStringBuilder().appendWithSeparators(value, ";").build());
}
return headers;
}
@Override
public List<Cookie> getCookies() {
List<String> textCookies = exchange.getRequestHeaders().get("Cookie");
List<Cookie> cookies = new ArrayList<>();
if (textCookies != null && !textCookies.isEmpty()) {
String[] separated = new TextStringBuilder().appendWithSeparators(textCookies, ";").build().split(";");
for (String textCookie : separated) {
cookies.add(new Cookie(textCookie));
}
}
return cookies;
}
}

View File

@ -0,0 +1,95 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.djrapitops.plan.delivery.webserver.ResponseResolver;
import com.djrapitops.plan.delivery.webserver.auth.AuthenticationExtractor;
import com.djrapitops.plan.delivery.webserver.configuration.WebserverConfiguration;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import net.playeranalytics.plugin.server.PluginLogger;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* HttpHandler for WebServer request management.
*
* @author AuroraLS3
*/
@Singleton
public class SunRequestHandler implements HttpHandler {
private final PlanConfig config;
private final Addresses addresses;
private final WebserverConfiguration webserverConfiguration;
private final AuthenticationExtractor authenticationExtractor;
private final ResponseResolver responseResolver;
private final RequestHandler requestHandler;
private final PluginLogger logger;
private final ErrorLogger errorLogger;
@Inject
public SunRequestHandler(
PlanConfig config,
Addresses addresses,
WebserverConfiguration webserverConfiguration,
AuthenticationExtractor authenticationExtractor,
ResponseResolver responseResolver,
RequestHandler requestHandler,
PluginLogger logger,
ErrorLogger errorLogger
) {
this.config = config;
this.addresses = addresses;
this.webserverConfiguration = webserverConfiguration;
this.authenticationExtractor = authenticationExtractor;
this.responseResolver = responseResolver;
this.requestHandler = requestHandler;
this.logger = logger;
this.errorLogger = errorLogger;
}
@Override
public void handle(HttpExchange exchange) {
try {
InternalRequest internalRequest = new SunInternalRequest(exchange, webserverConfiguration, authenticationExtractor);
Response response = requestHandler.getResponse(internalRequest);
new SunResponseSender(addresses, exchange, response).send();
} catch (Exception e) {
if (config.isTrue(PluginSettings.DEV_MODE)) {
logger.warn("THIS ERROR IS ONLY LOGGED IN DEV MODE:");
errorLogger.warn(e, ErrorContext.builder()
.whatToDo("THIS ERROR IS ONLY LOGGED IN DEV MODE")
.related(exchange.getRequestMethod(), exchange.getRemoteAddress(), exchange.getRequestHeaders(), exchange.getResponseHeaders(), exchange.getRequestURI())
.build());
}
} finally {
exchange.close();
}
}
public ResponseResolver getResponseResolver() {
return responseResolver;
}
}

View File

@ -14,9 +14,10 @@
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver;
package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
@ -31,13 +32,13 @@ import java.util.zip.GZIPOutputStream;
*
* @author AuroraLS3
*/
public class ResponseSender {
public class SunResponseSender {
private final Addresses addresses;
private final HttpExchange exchange;
private final Response response;
public ResponseSender(Addresses addresses, HttpExchange exchange, Response response) {
public SunResponseSender(Addresses addresses, HttpExchange exchange, Response response) {
this.addresses = addresses;
this.exchange = exchange;
this.response = response;

View File

@ -14,9 +14,9 @@
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver;
package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.SubSystem;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
@ -49,14 +49,14 @@ import java.util.concurrent.*;
* @author AuroraLS3
*/
@Singleton
public class WebServer implements SubSystem {
public class SunWebServer implements WebServer {
private final Locale locale;
private final PlanFiles files;
private final PlanConfig config;
private final Addresses addresses;
private final RequestHandler requestHandler;
private final SunRequestHandler requestHandler;
private final PluginLogger logger;
private final ErrorLogger errorLogger;
@ -68,14 +68,14 @@ public class WebServer implements SubSystem {
private boolean usingHttps = false;
@Inject
public WebServer(
public SunWebServer(
Locale locale,
PlanFiles files,
PlanConfig config,
Addresses addresses,
PluginLogger logger,
ErrorLogger errorLogger,
RequestHandler requestHandler
SunRequestHandler requestHandler
) {
this.locale = locale;
this.files = files;
@ -258,6 +258,7 @@ public class WebServer implements SubSystem {
/**
* @return if the WebServer is enabled
*/
@Override
public boolean isEnabled() {
return enabled;
}
@ -291,18 +292,22 @@ public class WebServer implements SubSystem {
}
}
@Override
public String getProtocol() {
return usingHttps ? "https" : "http";
}
@Override
public boolean isUsingHTTPS() {
return usingHttps;
}
@Override
public boolean isAuthRequired() {
return isUsingHTTPS() && config.isFalse(WebserverSettings.DISABLED_AUTHENTICATION);
}
@Override
public int getPort() {
return port;
}

View File

@ -0,0 +1,37 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.SubSystem;
public interface WebServer extends SubSystem {
@Override
void enable();
boolean isEnabled();
@Override
void disable();
String getProtocol();
boolean isUsingHTTPS();
boolean isAuthRequired();
int getPort();
}

View File

@ -22,8 +22,8 @@ import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import com.djrapitops.plan.delivery.webserver.WebServer;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.ServerInfo;

View File

@ -21,7 +21,7 @@ import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import com.djrapitops.plan.delivery.webserver.WebServer;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import dagger.Lazy;
import javax.inject.Inject;

View File

@ -21,7 +21,7 @@ import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import com.djrapitops.plan.delivery.webserver.WebServer;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import dagger.Lazy;
import javax.inject.Inject;

View File

@ -20,7 +20,7 @@ import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.WebServer;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.utilities.java.Maps;
import dagger.Lazy;

View File

@ -21,6 +21,8 @@ import com.djrapitops.plan.DataSvc;
import com.djrapitops.plan.delivery.webserver.cache.JSONFileStorage;
import com.djrapitops.plan.delivery.webserver.cache.JSONMemoryStorageShim;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.delivery.webserver.http.JettyWebserver;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.gathering.importing.importers.Importer;
import com.djrapitops.plan.settings.config.ExtensionSettings;
import com.djrapitops.plan.settings.config.PlanConfig;
@ -50,6 +52,12 @@ import java.util.function.Predicate;
@Module
public class SystemObjectProvidingModule {
@Provides
@Singleton
WebServer provideWebserver(JettyWebserver webServer) {
return webServer;
}
@Provides
@Singleton
Gson provideGson() {

View File

@ -37,7 +37,7 @@ public class WebserverSettings {
public static final Setting<String> CERTIFICATE_KEYPASS = new StringSetting("Webserver.Security.SSL_certificate.Key_pass");
public static final Setting<String> CERTIFICATE_STOREPASS = new StringSetting("Webserver.Security.SSL_certificate.Store_pass");
public static final Setting<String> CERTIFICATE_ALIAS = new StringSetting("Webserver.Security.SSL_certificate.Alias");
public static final Setting<Boolean> IP_WHITELIST_X_FORWARDED = new BooleanSetting("Webserver.Security.Use_X-Forwarded-For_Header");
public static final Setting<Boolean> IP_USE_X_FORWARDED_FOR = new BooleanSetting("Webserver.Security.Use_X-Forwarded-For_Header");
public static final Setting<Boolean> IP_WHITELIST = new BooleanSetting("Webserver.Security.IP_whitelist");
public static final Setting<List<String>> WHITELIST = new StringListSetting("Webserver.Security.IP_whitelist.Whitelist");
public static final Setting<Boolean> DISABLED = new BooleanSetting("Webserver.Disable_Webserver");

View File

@ -115,6 +115,9 @@ class AccessControlTest {
.orElseThrow(AssertionError::new);
caller.updatePlayerData(TestConstants.PLAYER_ONE_UUID, TestConstants.PLAYER_ONE_NAME);
assertTrue(system.getWebServerSystem().getWebServer().isUsingHTTPS());
assertTrue(system.getWebServerSystem().getWebServer().isAuthRequired());
address = "https://localhost:" + TEST_PORT_NUMBER;
cookieLevel0 = login(address, userLevel0.getUsername());
cookieLevel1 = login(address, userLevel1.getUsername());

View File

@ -18,6 +18,7 @@ package com.djrapitops.plan.delivery.webserver;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.exceptions.connection.ForbiddenException;
import com.djrapitops.plan.exceptions.connection.WebException;
import org.apache.commons.compress.utils.IOUtils;

View File

@ -18,6 +18,7 @@ package com.djrapitops.plan.delivery.webserver;
import com.djrapitops.plan.PlanSystem;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.storage.database.transactions.commands.RegisterWebUserTransaction;

View File

@ -18,6 +18,7 @@ package com.djrapitops.plan.delivery.webserver;
import com.djrapitops.plan.PlanSystem;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.changes.ConfigUpdater;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;

View File

@ -16,6 +16,8 @@
*/
package utilities.dagger;
import com.djrapitops.plan.delivery.webserver.http.JettyWebserver;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.settings.config.ExtensionSettings;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.locale.Locale;
@ -38,6 +40,12 @@ import java.util.function.Predicate;
@Module
public class TestSystemObjectProvidingModule {
@Provides
@Singleton
WebServer provideWebserver(JettyWebserver webServer) {
return webServer;
}
@Provides
@Singleton
Gson provideGson() {