Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,15 @@ private String getExportBasePath() {
}

private String getWebserverBasePath() {
String address = addresses.getMainAddress()
.orElseGet(addresses::getFallbackLocalhostAddress);
return addresses.getBasePath(address);
// Prefer External_Webserver_address base path for reverse proxy subpath support
return addresses.getExternalAddress()
.map(addresses::getBasePath)
.filter(basePath -> !basePath.isEmpty())
.orElseGet(() -> {
String address = addresses.getMainAddress()
.orElseGet(addresses::getFallbackLocalhostAddress);
return addresses.getBasePath(address);
});
}

public String correctAddressForWebserver(String content, String fileName) {
Expand Down Expand Up @@ -97,6 +103,13 @@ private String correctAddressInCss(String content, String basePath) {
}

private String correctAddressInJavascript(String content, String basePath) {
// Vite's __vitePreload base URL function: return"/"+l
// Needs to include the base path so preloaded assets resolve correctly.
if (!basePath.isEmpty()) {
String endingSlash = basePath.endsWith("/") ? "" : "/";
content = Strings.CS.replace(content, "return\"/\"+", "return\"" + basePath + endingSlash + "\"+");
}

int lastIndex = 0;
StringBuilder output = new StringBuilder();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ public class Contributors {
new Contributor("YannicHock", CODE),
new Contributor("SaolGhra", CODE),
new Contributor("Jsinco", CODE),
new Contributor("julianvdhogen", LANG)
new Contributor("julianvdhogen", LANG),
new Contributor("Leolebleis", CODE)
};

private Contributors() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ public Optional<String> getAnyValidServerAddress() {
.findAny();
}

/**
* Get the configured External_Webserver_address if it is set to a non-default value.
* <p>
* This is useful for reverse proxy setups where the external address includes
* the correct protocol and subpath that the internal webserver address lacks.
*
* @return External address if configured, empty otherwise.
*/
public Optional<String> getExternalAddress() {
String externalLink = getFallbackExternalAddress();
return isValidAddress(externalLink) ? Optional.of(externalLink) : Optional.empty();
}

private boolean isValidAddress(String address) {
return address != null
&& !address.isEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,12 @@ public Response javaScriptResponse(@Untrusted String fileName) {
}

private String replaceMainAddressPlaceholder(String resource) {
String address = addresses.get().getAccessAddress()
.orElseGet(addresses.get()::getFallbackLocalhostAddress);
Addresses addr = addresses.get();
// Prefer External_Webserver_address when configured — it includes
// the correct protocol and subpath for reverse proxy setups.
String address = addr.getExternalAddress()
.orElseGet(() -> addr.getAccessAddress()
.orElseGet(addr::getFallbackLocalhostAddress));
return Strings.CS.replace(resource, "PLAN_BASE_ADDRESS", address);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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.rendering;

import com.djrapitops.plan.delivery.webserver.Addresses;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;

/**
* Tests for {@link BundleAddressCorrection} reverse proxy subpath support.
*/
@ExtendWith(MockitoExtension.class)
class BundleAddressCorrectionTest {

@Mock
PlanConfig config;
@Mock
Addresses addresses;
@InjectMocks
BundleAddressCorrection bundleAddressCorrection;

@Test
@DisplayName("HTML paths are corrected when External_Webserver_address has subpath")
void htmlPathsCorrectedWithExternalSubpath() {
when(addresses.getExternalAddress()).thenReturn(Optional.of("https://example.com/plan"));
when(addresses.getBasePath("https://example.com/plan")).thenReturn("/plan");

String html = "<script src=\"/static/index-abc123.js\"></script>"
+ "<link href=\"/static/index-abc123.css\">";

String result = bundleAddressCorrection.correctAddressForWebserver(html, "index.html");

assertTrue(result.contains("src=\"/plan/static/index-abc123.js\""),
"JS src should be corrected to /plan/static/..., got: " + result);
assertTrue(result.contains("href=\"/plan/static/index-abc123.css\""),
"CSS href should be corrected to /plan/static/..., got: " + result);
}

@Test
@DisplayName("HTML paths unchanged when External_Webserver_address has no subpath")
void htmlPathsUnchangedWithoutSubpath() {
when(addresses.getExternalAddress()).thenReturn(Optional.of("https://example.com"));
when(addresses.getBasePath("https://example.com")).thenReturn("");
when(addresses.getMainAddress()).thenReturn(Optional.of("http://localhost:8804"));
when(addresses.getBasePath("http://localhost:8804")).thenReturn("");

String html = "<script src=\"/static/index-abc123.js\"></script>";

String result = bundleAddressCorrection.correctAddressForWebserver(html, "index.html");

assertTrue(result.contains("src=\"/static/index-abc123.js\""),
"JS src should stay at root when no subpath, got: " + result);
}

@Test
@DisplayName("HTML paths corrected using main address when no external address")
void htmlPathsCorrectedFromMainAddress() {
when(addresses.getExternalAddress()).thenReturn(Optional.empty());
when(addresses.getMainAddress()).thenReturn(Optional.of("http://example.com/plan"));
when(addresses.getBasePath("http://example.com/plan")).thenReturn("/plan");

String html = "<script src=\"/static/index-abc123.js\"></script>";

String result = bundleAddressCorrection.correctAddressForWebserver(html, "index.html");

assertTrue(result.contains("src=\"/plan/static/index-abc123.js\""),
"Should fall back to main address base path, got: " + result);
}

@Test
@DisplayName("HTML paths unchanged when no external address and main address has no subpath")
void htmlPathsUnchangedNoSubpathAnywhere() {
when(addresses.getExternalAddress()).thenReturn(Optional.empty());
when(addresses.getMainAddress()).thenReturn(Optional.of("http://localhost:8804"));
when(addresses.getBasePath("http://localhost:8804")).thenReturn("");

String html = "<script src=\"/static/index-abc123.js\"></script>";

String result = bundleAddressCorrection.correctAddressForWebserver(html, "index.html");

assertTrue(result.contains("src=\"/static/index-abc123.js\""),
"Should be unchanged when no subpath anywhere, got: " + result);
}

@Test
@DisplayName("Vite preload base URL is corrected with subpath")
void vitePreloadCorrectedWithSubpath() {
when(addresses.getExternalAddress()).thenReturn(Optional.of("https://example.com/plan"));
when(addresses.getBasePath("https://example.com/plan")).thenReturn("/plan");

String js = "GN=function(l){return\"/\"+l}";

String result = bundleAddressCorrection.correctAddressForWebserver(js, "index.js");

assertTrue(result.contains("return\"/plan/\"+l"),
"Vite preload should include base path, got: " + result);
}

@Test
@DisplayName("Vite preload base URL unchanged at root")
void vitePreloadUnchangedAtRoot() {
when(addresses.getExternalAddress()).thenReturn(Optional.empty());
when(addresses.getMainAddress()).thenReturn(Optional.of("http://localhost:8804"));
when(addresses.getBasePath("http://localhost:8804")).thenReturn("");

String js = "GN=function(l){return\"/\"+l}";

String result = bundleAddressCorrection.correctAddressForWebserver(js, "index.js");

assertTrue(result.contains("return\"/\"+l"),
"Vite preload should be unchanged at root, got: " + result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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.webserver.http.WebServer;
import com.djrapitops.plan.identification.properties.ServerProperties;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.storage.database.DBSystem;
import dagger.Lazy;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

/**
* Tests for {@link Addresses#getExternalAddress()} and {@link Addresses#getBasePath(String)}.
*/
@ExtendWith(MockitoExtension.class)
class AddressesExternalAddressTest {

@Mock
PlanConfig config;
@Mock
DBSystem dbSystem;
@Mock
Lazy<ServerProperties> serverProperties;
@Mock
Lazy<WebServer> webserver;
@InjectMocks
Addresses addresses;

@Test
@DisplayName("getExternalAddress returns configured address with subpath")
void externalAddressWithSubpath() {
when(config.get(WebserverSettings.EXTERNAL_LINK)).thenReturn("https://example.com/plan");
assertEquals(Optional.of("https://example.com/plan"), addresses.getExternalAddress());
}

@Test
@DisplayName("getExternalAddress returns configured address without subpath")
void externalAddressWithoutSubpath() {
when(config.get(WebserverSettings.EXTERNAL_LINK)).thenReturn("https://example.com");
assertEquals(Optional.of("https://example.com"), addresses.getExternalAddress());
}

@Test
@DisplayName("getExternalAddress returns empty for default placeholder")
void externalAddressDefaultPlaceholder() {
when(config.get(WebserverSettings.EXTERNAL_LINK)).thenReturn("https://www.example.address");
assertEquals(Optional.empty(), addresses.getExternalAddress());
}

@Test
@DisplayName("getExternalAddress returns empty for http placeholder")
void externalAddressHttpPlaceholder() {
when(config.get(WebserverSettings.EXTERNAL_LINK)).thenReturn("http://www.example.address");
assertEquals(Optional.empty(), addresses.getExternalAddress());
}

@Test
@DisplayName("getExternalAddress returns empty for empty string")
void externalAddressEmpty() {
when(config.get(WebserverSettings.EXTERNAL_LINK)).thenReturn("");
assertEquals(Optional.empty(), addresses.getExternalAddress());
}

@Test
@DisplayName("getBasePath extracts subpath from address")
void basePathExtraction() {
assertEquals("/minecraft/stats", addresses.getBasePath("https://disqt.com/minecraft/stats"));
}

@Test
@DisplayName("getBasePath returns empty for root address")
void basePathRoot() {
assertEquals("", addresses.getBasePath("https://example.com"));
}

@Test
@DisplayName("getBasePath extracts subpath from http address")
void basePathHttp() {
assertEquals("/plan", addresses.getBasePath("http://example.com/plan"));
}
}
Loading