258 lines
11 KiB
Java
258 lines
11 KiB
Java
/*
|
|
* 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=1; 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;
|
|
}
|
|
}
|