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" commonsCodecVersion = "1.15"
caffeineVersion = "3.1.1" caffeineVersion = "3.1.1"
mysqlVersion = "8.0.29" mysqlVersion = "8.0.29"
jettyVersion = "11.0.6"
caffeineVersion = "2.9.2"
mysqlVersion = "8.0.26"
sqliteVersion = "3.36.0.3" sqliteVersion = "3.36.0.3"
hikariVersion = "5.0.1" hikariVersion = "5.0.1"
slf4jVersion = "1.7.36" slf4jVersion = "1.7.36"

View File

@ -51,6 +51,9 @@ dependencies {
} }
mysqlDriver "mysql:mysql-connector-java:$mysqlVersion" mysqlDriver "mysql:mysql-connector-java:$mysqlVersion"
sqliteDriver "org.xerial:sqlite-jdbc:$sqliteVersion" 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 project(":api")
testImplementation "com.google.code.gson:gson:$gsonVersion" testImplementation "com.google.code.gson:gson:$gsonVersion"
@ -166,4 +169,6 @@ shadowJar {
exclude "org/jayway/**/*" exclude "org/jayway/**/*"
exclude "google/protobuf/**/*" exclude "google/protobuf/**/*"
exclude "jargs/gnu/**/*" exclude "jargs/gnu/**/*"
mergeServiceFiles()
} }

View File

@ -16,6 +16,7 @@
*/ */
package com.djrapitops.plan.delivery.webserver; 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.Server;
import com.djrapitops.plan.identification.properties.ServerProperties; import com.djrapitops.plan.identification.properties.ServerProperties;
import com.djrapitops.plan.settings.config.PlanConfig; import com.djrapitops.plan.settings.config.PlanConfig;

View File

@ -16,6 +16,7 @@
*/ */
package com.djrapitops.plan.delivery.webserver; 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.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings; import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings; import com.djrapitops.plan.settings.config.paths.WebserverSettings;

View File

@ -35,14 +35,7 @@ public class RequestBodyConverter {
* @return {@link URIQuery}. * @return {@link URIQuery}.
*/ */
public static URIQuery formBody(Request request) { public static URIQuery formBody(Request request) {
if ( return new URIQuery(new String(request.getRequestBody(), StandardCharsets.UTF_8));
"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("");
}
} }
public static <T> T bodyJson(Request request, Gson gson, Class<T> ofType) { 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.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser; import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.auth.FailReason; 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.*;
import com.djrapitops.plan.delivery.webserver.resolver.auth.*; import com.djrapitops.plan.delivery.webserver.resolver.auth.*;
import com.djrapitops.plan.delivery.webserver.resolver.json.RootJSONResolver; 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.SubSystem;
import com.djrapitops.plan.delivery.web.ResourceService; import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore; import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; 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 * You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>. * 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.web.resolver.Response;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
@ -31,13 +32,13 @@ import java.util.zip.GZIPOutputStream;
* *
* @author AuroraLS3 * @author AuroraLS3
*/ */
public class ResponseSender { public class SunResponseSender {
private final Addresses addresses; private final Addresses addresses;
private final HttpExchange exchange; private final HttpExchange exchange;
private final Response response; private final Response response;
public ResponseSender(Addresses addresses, HttpExchange exchange, Response response) { public SunResponseSender(Addresses addresses, HttpExchange exchange, Response response) {
this.addresses = addresses; this.addresses = addresses;
this.exchange = exchange; this.exchange = exchange;
this.response = response; this.response = response;

View File

@ -14,9 +14,9 @@
* You should have received a copy of the GNU Lesser General Public License * You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>. * 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.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings; import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings; import com.djrapitops.plan.settings.config.paths.WebserverSettings;
@ -49,14 +49,14 @@ import java.util.concurrent.*;
* @author AuroraLS3 * @author AuroraLS3
*/ */
@Singleton @Singleton
public class WebServer implements SubSystem { public class SunWebServer implements WebServer {
private final Locale locale; private final Locale locale;
private final PlanFiles files; private final PlanFiles files;
private final PlanConfig config; private final PlanConfig config;
private final Addresses addresses; private final Addresses addresses;
private final RequestHandler requestHandler; private final SunRequestHandler requestHandler;
private final PluginLogger logger; private final PluginLogger logger;
private final ErrorLogger errorLogger; private final ErrorLogger errorLogger;
@ -68,14 +68,14 @@ public class WebServer implements SubSystem {
private boolean usingHttps = false; private boolean usingHttps = false;
@Inject @Inject
public WebServer( public SunWebServer(
Locale locale, Locale locale,
PlanFiles files, PlanFiles files,
PlanConfig config, PlanConfig config,
Addresses addresses, Addresses addresses,
PluginLogger logger, PluginLogger logger,
ErrorLogger errorLogger, ErrorLogger errorLogger,
RequestHandler requestHandler SunRequestHandler requestHandler
) { ) {
this.locale = locale; this.locale = locale;
this.files = files; this.files = files;
@ -258,6 +258,7 @@ public class WebServer implements SubSystem {
/** /**
* @return if the WebServer is enabled * @return if the WebServer is enabled
*/ */
@Override
public boolean isEnabled() { public boolean isEnabled() {
return enabled; return enabled;
} }
@ -291,18 +292,22 @@ public class WebServer implements SubSystem {
} }
} }
@Override
public String getProtocol() { public String getProtocol() {
return usingHttps ? "https" : "http"; return usingHttps ? "https" : "http";
} }
@Override
public boolean isUsingHTTPS() { public boolean isUsingHTTPS() {
return usingHttps; return usingHttps;
} }
@Override
public boolean isAuthRequired() { public boolean isAuthRequired() {
return isUsingHTTPS() && config.isFalse(WebserverSettings.DISABLED_AUTHENTICATION); return isUsingHTTPS() && config.isFalse(WebserverSettings.DISABLED_AUTHENTICATION);
} }
@Override
public int getPort() { public int getPort() {
return port; 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.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser; import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.ResponseFactory; 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.auth.FailReason;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.exceptions.WebUserAuthException; import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.identification.Server; import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.ServerInfo; 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.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser; import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.ResponseFactory; 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 dagger.Lazy;
import javax.inject.Inject; 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.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser; import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.ResponseFactory; 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 dagger.Lazy;
import javax.inject.Inject; 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.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request; import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser; 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 com.djrapitops.plan.utilities.java.Maps;
import dagger.Lazy; 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.JSONFileStorage;
import com.djrapitops.plan.delivery.webserver.cache.JSONMemoryStorageShim; import com.djrapitops.plan.delivery.webserver.cache.JSONMemoryStorageShim;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage; 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.gathering.importing.importers.Importer;
import com.djrapitops.plan.settings.config.ExtensionSettings; import com.djrapitops.plan.settings.config.ExtensionSettings;
import com.djrapitops.plan.settings.config.PlanConfig; import com.djrapitops.plan.settings.config.PlanConfig;
@ -50,6 +52,12 @@ import java.util.function.Predicate;
@Module @Module
public class SystemObjectProvidingModule { public class SystemObjectProvidingModule {
@Provides
@Singleton
WebServer provideWebserver(JettyWebserver webServer) {
return webServer;
}
@Provides @Provides
@Singleton @Singleton
Gson provideGson() { 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_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_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<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<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<List<String>> WHITELIST = new StringListSetting("Webserver.Security.IP_whitelist.Whitelist");
public static final Setting<Boolean> DISABLED = new BooleanSetting("Webserver.Disable_Webserver"); public static final Setting<Boolean> DISABLED = new BooleanSetting("Webserver.Disable_Webserver");

View File

@ -115,6 +115,9 @@ class AccessControlTest {
.orElseThrow(AssertionError::new); .orElseThrow(AssertionError::new);
caller.updatePlayerData(TestConstants.PLAYER_ONE_UUID, TestConstants.PLAYER_ONE_NAME); 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; address = "https://localhost:" + TEST_PORT_NUMBER;
cookieLevel0 = login(address, userLevel0.getUsername()); cookieLevel0 = login(address, userLevel0.getUsername());
cookieLevel1 = login(address, userLevel1.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.BadRequestException;
import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException; 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.ForbiddenException;
import com.djrapitops.plan.exceptions.connection.WebException; import com.djrapitops.plan.exceptions.connection.WebException;
import org.apache.commons.compress.utils.IOUtils; 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.PlanSystem;
import com.djrapitops.plan.delivery.domain.auth.User; 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.PlanConfig;
import com.djrapitops.plan.settings.config.paths.WebserverSettings; import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.storage.database.transactions.commands.RegisterWebUserTransaction; 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.PlanSystem;
import com.djrapitops.plan.delivery.domain.auth.User; 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.PlanConfig;
import com.djrapitops.plan.settings.config.changes.ConfigUpdater; import com.djrapitops.plan.settings.config.changes.ConfigUpdater;
import com.djrapitops.plan.settings.config.paths.WebserverSettings; import com.djrapitops.plan.settings.config.paths.WebserverSettings;

View File

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