Patched
This commit is contained in:
@@ -369,7 +369,9 @@ Please sign in with this key: <temporary-key>
|
||||
|
||||
Open the printed URL in a browser and sign in with the temporary key.
|
||||
|
||||
The wizard provides guided sections for:
|
||||
The wizard uses a dark control-center UI. It detects whether it is running on Paper/Spigot or BungeeCord/Waterfall and only shows the setup sections that apply to that server type.
|
||||
|
||||
Depending on the server type, it provides guided sections for:
|
||||
|
||||
- server mode and node name
|
||||
- proxy listener settings
|
||||
@@ -379,6 +381,8 @@ The wizard provides guided sections for:
|
||||
- heartbeat and reconnect settings
|
||||
- advanced raw config keys
|
||||
|
||||
You can add or remove proxy endpoints, backend routes, and advanced config keys directly from the web UI.
|
||||
|
||||
The web UI includes:
|
||||
|
||||
- `Save Config`
|
||||
|
||||
Binary file not shown.
@@ -36,4 +36,18 @@ public final class ConfigFileEditor {
|
||||
|
||||
Files.write(path, lines, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static void removeProperty(Path path, String key) throws IOException {
|
||||
if (!Files.exists(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String prefix = key + "=";
|
||||
List<String> lines = new ArrayList<>(Files.readAllLines(path, StandardCharsets.UTF_8));
|
||||
lines.removeIf(line -> {
|
||||
String trimmed = line.trim();
|
||||
return !trimmed.startsWith("#") && trimmed.startsWith(prefix);
|
||||
});
|
||||
Files.write(path, lines, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ public final class PluginRuntime {
|
||||
}
|
||||
|
||||
try {
|
||||
webWizard = WebWizardServer.start(configPath, logger, this::reloadFromConfig);
|
||||
webWizard = WebWizardServer.start(configPath, logger, this::reloadFromConfig, requiredRole);
|
||||
lines.add("Web wizard: running");
|
||||
lines.add("URL: " + webWizard.uri());
|
||||
lines.add("Login key: printed in the server console");
|
||||
|
||||
@@ -2,6 +2,7 @@ package me.proxylink.plugin.web;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import me.proxylink.common.AgentConfig;
|
||||
import me.proxylink.plugin.ConfigFileEditor;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -42,6 +43,7 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
private final Path configPath;
|
||||
private final Logger logger;
|
||||
private final Supplier<String> reloadAction;
|
||||
private final AgentConfig.Role runtimeRole;
|
||||
private final HttpServer server;
|
||||
private final ExecutorService executor;
|
||||
private final String loginKey;
|
||||
@@ -53,6 +55,7 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
Path configPath,
|
||||
Logger logger,
|
||||
Supplier<String> reloadAction,
|
||||
AgentConfig.Role runtimeRole,
|
||||
HttpServer server,
|
||||
ExecutorService executor,
|
||||
String loginKey,
|
||||
@@ -62,6 +65,7 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
this.configPath = configPath;
|
||||
this.logger = logger;
|
||||
this.reloadAction = reloadAction;
|
||||
this.runtimeRole = runtimeRole;
|
||||
this.server = server;
|
||||
this.executor = executor;
|
||||
this.loginKey = loginKey;
|
||||
@@ -69,7 +73,12 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public static WebWizardServer start(Path configPath, Logger logger, Supplier<String> reloadAction) throws IOException {
|
||||
public static WebWizardServer start(
|
||||
Path configPath,
|
||||
Logger logger,
|
||||
Supplier<String> reloadAction,
|
||||
AgentConfig.Role runtimeRole
|
||||
) throws IOException {
|
||||
WizardSettings settings = WizardSettings.load(configPath);
|
||||
IOException lastFailure = null;
|
||||
int firstPort = settings.port();
|
||||
@@ -78,7 +87,7 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
for (int attempt = 0; attempt < attempts; attempt++) {
|
||||
int port = firstPort == 0 ? 0 : firstPort + attempt;
|
||||
try {
|
||||
return bind(configPath, logger, reloadAction, settings.bindHost(), port);
|
||||
return bind(configPath, logger, reloadAction, runtimeRole, settings.bindHost(), port);
|
||||
} catch (BindException e) {
|
||||
lastFailure = e;
|
||||
}
|
||||
@@ -109,6 +118,7 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
Path configPath,
|
||||
Logger logger,
|
||||
Supplier<String> reloadAction,
|
||||
AgentConfig.Role runtimeRole,
|
||||
String bindHost,
|
||||
int port
|
||||
) throws IOException {
|
||||
@@ -118,11 +128,12 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
);
|
||||
String loginKey = randomHex(18);
|
||||
String sessionToken = randomHex(32);
|
||||
URI uri = URI.create("http://" + uriHost(bindHost) + ":" + server.getAddress().getPort() + "/");
|
||||
URI uri = URI.create("http://" + displayUriHost(bindHost) + ":" + server.getAddress().getPort() + "/");
|
||||
WebWizardServer webWizard = new WebWizardServer(
|
||||
configPath,
|
||||
logger,
|
||||
reloadAction,
|
||||
runtimeRole,
|
||||
server,
|
||||
executor,
|
||||
loginKey,
|
||||
@@ -138,6 +149,9 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
server.start();
|
||||
|
||||
logger.warning("DirtSimpleP2P Web Wizard started at " + uri);
|
||||
if (isWildcardHost(bindHost)) {
|
||||
logger.warning("Web Wizard is bound to " + bindHost + ". Use your server IP instead of 127.0.0.1 when connecting from another computer.");
|
||||
}
|
||||
logger.warning("Please sign in with this key: " + loginKey);
|
||||
logger.warning("The Web Wizard can edit config.properties. Stop it with /dsp2p webwizard when finished.");
|
||||
return webWizard;
|
||||
@@ -214,7 +228,7 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
if ("reloadOnly".equals(action)) {
|
||||
message = reloadAction.get();
|
||||
} else {
|
||||
saveWizardForm(form);
|
||||
saveWizardForm(form, action);
|
||||
if ("generateToken".equals(action)) {
|
||||
String token = randomHex(32);
|
||||
ConfigFileEditor.setProperty(configPath, "tunnel.authToken", token);
|
||||
@@ -234,15 +248,14 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private void saveWizardForm(Map<String, List<String>> form) throws IOException {
|
||||
List<String> frontendNames = namesFrom(first(form, "frontends"));
|
||||
String newFrontend = first(form, "newFrontendName").trim();
|
||||
if (!newFrontend.isBlank()) {
|
||||
validateConfigKey(newFrontend);
|
||||
if (!frontendNames.contains(newFrontend)) {
|
||||
frontendNames.add(newFrontend);
|
||||
}
|
||||
}
|
||||
private void saveWizardForm(Map<String, List<String>> form, String action) throws IOException {
|
||||
boolean frontendMode = runtimeRole == AgentConfig.Role.FRONTEND;
|
||||
boolean backendMode = runtimeRole == AgentConfig.Role.BACKEND;
|
||||
String removeFrontend = actionValue(action, "removeFrontend:");
|
||||
String removeRoute = actionValue(action, "removeRoute:");
|
||||
String removeRaw = actionValue(action, "removeRaw:");
|
||||
boolean removedFrontend = false;
|
||||
boolean removedRoute = false;
|
||||
|
||||
List<String> routeNames = namesFrom(first(form, "routes"));
|
||||
String newRoute = first(form, "newRouteName").trim();
|
||||
@@ -252,11 +265,12 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
routeNames.add(newRoute);
|
||||
}
|
||||
}
|
||||
if (!removeRoute.isBlank() && routeNames.size() > 1) {
|
||||
removedRoute = routeNames.remove(removeRoute);
|
||||
}
|
||||
|
||||
saveKey(form, "role");
|
||||
ConfigFileEditor.setProperty(configPath, "role", runtimeRole.name().toLowerCase(java.util.Locale.ROOT));
|
||||
saveKey(form, "node.name");
|
||||
saveKey(form, "tunnel.listenHost");
|
||||
saveKey(form, "tunnel.listenPort");
|
||||
saveKey(form, "tunnel.authToken");
|
||||
saveKey(form, "tunnel.connectTimeoutMillis");
|
||||
saveKey(form, "tunnel.heartbeatIntervalMillis");
|
||||
@@ -266,33 +280,74 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
saveKey(form, "tunnel.reconnectMaxMillis");
|
||||
saveBoolean(form, "tunnel.tls.enabled");
|
||||
saveBoolean(form, "tunnel.tls.allowInsecure");
|
||||
saveBoolean(form, "tunnel.tls.autoGenerate");
|
||||
saveKey(form, "tunnel.tls.keyStore");
|
||||
saveKey(form, "tunnel.tls.keyStorePassword");
|
||||
saveBoolean(form, "tunnel.tls.requireClientAuth");
|
||||
saveBoolean(form, "tunnel.tls.trustOnFirstUse");
|
||||
saveKey(form, "webwizard.bindHost");
|
||||
saveKey(form, "webwizard.port");
|
||||
|
||||
ConfigFileEditor.setProperty(configPath, "frontends", String.join(",", frontendNames));
|
||||
for (String frontend : frontendNames) {
|
||||
saveNamed(form, "frontend." + frontend + ".connectHost");
|
||||
saveNamed(form, "frontend." + frontend + ".connectPort");
|
||||
saveNamed(form, "frontend." + frontend + ".tls.pinnedCertificateSha256");
|
||||
if (frontendMode) {
|
||||
saveKey(form, "tunnel.listenHost");
|
||||
saveKey(form, "tunnel.listenPort");
|
||||
saveBoolean(form, "tunnel.tls.autoGenerate");
|
||||
saveKey(form, "tunnel.tls.keyStore");
|
||||
saveKey(form, "tunnel.tls.keyStorePassword");
|
||||
saveBoolean(form, "tunnel.tls.requireClientAuth");
|
||||
}
|
||||
|
||||
if (backendMode) {
|
||||
saveBoolean(form, "tunnel.tls.trustOnFirstUse");
|
||||
List<String> frontendNames = namesFrom(first(form, "frontends"));
|
||||
String newFrontend = first(form, "newFrontendName").trim();
|
||||
if (!newFrontend.isBlank()) {
|
||||
validateConfigKey(newFrontend);
|
||||
if (!frontendNames.contains(newFrontend)) {
|
||||
frontendNames.add(newFrontend);
|
||||
}
|
||||
}
|
||||
if (!removeFrontend.isBlank() && frontendNames.size() > 1) {
|
||||
removedFrontend = frontendNames.remove(removeFrontend);
|
||||
}
|
||||
|
||||
ConfigFileEditor.setProperty(configPath, "frontends", String.join(",", frontendNames));
|
||||
for (String frontend : frontendNames) {
|
||||
saveNamed(form, "frontend." + frontend + ".connectHost");
|
||||
saveNamed(form, "frontend." + frontend + ".connectPort");
|
||||
saveNamed(form, "frontend." + frontend + ".tls.pinnedCertificateSha256");
|
||||
}
|
||||
if (removedFrontend) {
|
||||
ConfigFileEditor.removeProperty(configPath, "frontend." + removeFrontend + ".connectHost");
|
||||
ConfigFileEditor.removeProperty(configPath, "frontend." + removeFrontend + ".connectPort");
|
||||
ConfigFileEditor.removeProperty(configPath, "frontend." + removeFrontend + ".tls.pinnedCertificateSha256");
|
||||
}
|
||||
}
|
||||
|
||||
ConfigFileEditor.setProperty(configPath, "routes", String.join(",", routeNames));
|
||||
for (String route : routeNames) {
|
||||
saveNamed(form, "route." + route + ".frontendBindHost");
|
||||
saveNamed(form, "route." + route + ".frontendBindPort");
|
||||
saveNamed(form, "route." + route + ".backendNode");
|
||||
saveNamed(form, "route." + route + ".backendTargetHost");
|
||||
saveNamed(form, "route." + route + ".backendTargetPort");
|
||||
if (frontendMode) {
|
||||
saveNamed(form, "route." + route + ".frontendBindHost");
|
||||
saveNamed(form, "route." + route + ".frontendBindPort");
|
||||
saveNamed(form, "route." + route + ".backendNode");
|
||||
}
|
||||
if (backendMode) {
|
||||
saveNamed(form, "route." + route + ".backendTargetHost");
|
||||
saveNamed(form, "route." + route + ".backendTargetPort");
|
||||
}
|
||||
}
|
||||
if (removedRoute) {
|
||||
ConfigFileEditor.removeProperty(configPath, "route." + removeRoute + ".frontendBindHost");
|
||||
ConfigFileEditor.removeProperty(configPath, "route." + removeRoute + ".frontendBindPort");
|
||||
ConfigFileEditor.removeProperty(configPath, "route." + removeRoute + ".backendNode");
|
||||
ConfigFileEditor.removeProperty(configPath, "route." + removeRoute + ".backendTargetHost");
|
||||
ConfigFileEditor.removeProperty(configPath, "route." + removeRoute + ".backendTargetPort");
|
||||
}
|
||||
|
||||
for (String key : form.getOrDefault("rawKeys", List.of())) {
|
||||
validateConfigKey(key);
|
||||
ConfigFileEditor.setProperty(configPath, key, first(form, "raw." + key));
|
||||
if (!key.equals(removeRaw)) {
|
||||
ConfigFileEditor.setProperty(configPath, key, first(form, "raw." + key));
|
||||
}
|
||||
}
|
||||
if (!removeRaw.isBlank()) {
|
||||
validateConfigKey(removeRaw);
|
||||
ConfigFileEditor.removeProperty(configPath, removeRaw);
|
||||
}
|
||||
|
||||
String newRawKey = first(form, "newRawKey").trim();
|
||||
@@ -314,6 +369,17 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
ConfigFileEditor.setProperty(configPath, key, Boolean.toString(form.containsKey(key)));
|
||||
}
|
||||
|
||||
private static String actionValue(String action, String prefix) {
|
||||
if (action == null || !action.startsWith(prefix)) {
|
||||
return "";
|
||||
}
|
||||
String value = action.substring(prefix.length()).trim();
|
||||
if (!value.isBlank()) {
|
||||
validateConfigKey(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String loginPage(String message) {
|
||||
return page("Sign In", """
|
||||
<section class="login-shell">
|
||||
@@ -332,65 +398,284 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
private String wizardPage(String message) throws IOException {
|
||||
Properties properties = loadProperties(configPath);
|
||||
Set<String> guidedKeys = new HashSet<>();
|
||||
String role = value(properties, "role", "backend");
|
||||
List<String> frontendNames = namesFrom(value(properties, "frontends", defaultFrontends(properties)));
|
||||
boolean frontendMode = runtimeRole == AgentConfig.Role.FRONTEND;
|
||||
boolean backendMode = runtimeRole == AgentConfig.Role.BACKEND;
|
||||
String role = runtimeRole.name().toLowerCase(java.util.Locale.ROOT);
|
||||
List<String> frontendNames = backendMode
|
||||
? namesFrom(value(properties, "frontends", defaultFrontends(properties)))
|
||||
: List.of();
|
||||
List<String> routeNames = namesFrom(value(properties, "routes", "minecraft"));
|
||||
addKnownGuidedKeys(properties, guidedKeys);
|
||||
|
||||
StringBuilder frontends = new StringBuilder();
|
||||
for (String frontend : frontendNames) {
|
||||
guidedKeys.add("frontend." + frontend + ".connectHost");
|
||||
guidedKeys.add("frontend." + frontend + ".connectPort");
|
||||
guidedKeys.add("frontend." + frontend + ".tls.pinnedCertificateSha256");
|
||||
frontends.append("""
|
||||
<div class="mini-card">
|
||||
<div class="mini-title">%s</div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(
|
||||
html(frontend),
|
||||
textInput(properties, "frontend." + frontend + ".connectHost", "Proxy IP or domain", "proxy.example.com"),
|
||||
textInput(properties, "frontend." + frontend + ".connectPort", "Tunnel port", "24445"),
|
||||
textInput(properties, "frontend." + frontend + ".tls.pinnedCertificateSha256", "TLS pin", "")
|
||||
));
|
||||
if (backendMode) {
|
||||
for (String frontend : frontendNames) {
|
||||
frontends.append("""
|
||||
<div class="mini-card">
|
||||
<div class="mini-title"><span>%s</span><button type="submit" class="icon-button danger" name="action" value="removeFrontend:%s" title="Remove proxy">X</button></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(
|
||||
html(frontend),
|
||||
attr(frontend),
|
||||
textInput(properties, "frontend." + frontend + ".connectHost", "Proxy IP or domain", "proxy.example.com"),
|
||||
textInput(properties, "frontend." + frontend + ".connectPort", "Tunnel port", "24445"),
|
||||
textInput(properties, "frontend." + frontend + ".tls.pinnedCertificateSha256", "TLS pin", "")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder routes = new StringBuilder();
|
||||
for (String route : routeNames) {
|
||||
guidedKeys.add("route." + route + ".frontendBindHost");
|
||||
guidedKeys.add("route." + route + ".frontendBindPort");
|
||||
guidedKeys.add("route." + route + ".backendNode");
|
||||
guidedKeys.add("route." + route + ".backendTargetHost");
|
||||
guidedKeys.add("route." + route + ".backendTargetPort");
|
||||
routes.append("""
|
||||
<div class="mini-card">
|
||||
<div class="mini-title">%s</div>
|
||||
if (frontendMode) {
|
||||
routes.append("""
|
||||
<div class="mini-card">
|
||||
<div class="mini-title"><span>%s</span><button type="submit" class="icon-button danger" name="action" value="removeRoute:%s" title="Remove route">X</button></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(
|
||||
html(route),
|
||||
attr(route),
|
||||
textInput(properties, "route." + route + ".frontendBindHost", "Proxy local bind host", "127.0.0.1"),
|
||||
textInput(properties, "route." + route + ".frontendBindPort", "Proxy local bind port", "25566"),
|
||||
textInput(properties, "route." + route + ".backendNode", "Backend node name", "paper-backend")
|
||||
));
|
||||
} else {
|
||||
routes.append("""
|
||||
<div class="mini-card">
|
||||
<div class="mini-title"><span>%s</span><button type="submit" class="icon-button danger" name="action" value="removeRoute:%s" title="Remove route">X</button></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(
|
||||
html(route),
|
||||
attr(route),
|
||||
textInput(properties, "route." + route + ".backendTargetHost", "Backend target host", "127.0.0.1"),
|
||||
textInput(properties, "route." + route + ".backendTargetPort", "Backend Minecraft port", "25565")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
String advancedRows = advancedRows(properties, guidedKeys);
|
||||
String roleLabel = frontendMode ? "Proxy / BungeeCord / Waterfall" : "Backend / Paper / Spigot";
|
||||
|
||||
StringBuilder nav = new StringBuilder();
|
||||
nav.append("<a href=\"#identity\">1. Identity</a>");
|
||||
if (frontendMode) {
|
||||
nav.append("<a href=\"#proxy\">2. Proxy Listener</a>");
|
||||
}
|
||||
if (backendMode) {
|
||||
nav.append("<a href=\"#backend\">2. Backend Links</a>");
|
||||
}
|
||||
nav.append("<a href=\"#routes\">3. Routes</a>");
|
||||
nav.append("<a href=\"#security\">4. Security</a>");
|
||||
nav.append("<a href=\"#reliability\">5. Reliability</a>");
|
||||
nav.append("<a href=\"#wizard\">6. Wizard Access</a>");
|
||||
nav.append("<a href=\"#advanced\">7. Advanced</a>");
|
||||
|
||||
StringBuilder cards = new StringBuilder();
|
||||
cards.append("""
|
||||
<section class="panel" id="identity">
|
||||
<div class="section-head"><span>1</span><div><h2>Server Identity</h2><p>This wizard is running in %s mode.</p></div></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</section>
|
||||
""".formatted(
|
||||
html(roleLabel),
|
||||
roleBadge(role, roleLabel),
|
||||
textInput(properties, "node.name", "Node name", frontendMode ? "bungee-frontend" : "paper-backend")
|
||||
));
|
||||
|
||||
if (frontendMode) {
|
||||
cards.append("""
|
||||
<section class="panel" id="proxy">
|
||||
<div class="section-head"><span>2</span><div><h2>Proxy Listener</h2><p>Backend nodes connect to this public tunnel port.</p></div></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
""".formatted(
|
||||
html(route),
|
||||
textInput(properties, "route." + route + ".frontendBindHost", "Proxy local bind host", "127.0.0.1"),
|
||||
textInput(properties, "route." + route + ".frontendBindPort", "Proxy local bind port", "25566"),
|
||||
textInput(properties, "route." + route + ".backendNode", "Backend node name", "paper-backend"),
|
||||
textInput(properties, "route." + route + ".backendTargetHost", "Backend target host", "127.0.0.1"),
|
||||
textInput(properties, "route." + route + ".backendTargetPort", "Backend Minecraft port", "25565")
|
||||
textInput(properties, "tunnel.listenHost", "Tunnel listen host", "0.0.0.0"),
|
||||
textInput(properties, "tunnel.listenPort", "Tunnel listen port", "24445")
|
||||
));
|
||||
}
|
||||
|
||||
if (backendMode) {
|
||||
cards.append("""
|
||||
<section class="panel" id="backend">
|
||||
<div class="section-head"><span>2</span><div><h2>Backend Connections</h2><p>Add every public proxy this backend should connect to.</p></div></div>
|
||||
%s
|
||||
<div class="add-row">
|
||||
<label>New proxy name<input name="newFrontendName" placeholder="proxy2"></label>
|
||||
<button type="submit" name="action" value="addFrontend" class="secondary">Add Proxy</button>
|
||||
</div>
|
||||
%s
|
||||
</section>
|
||||
""".formatted(hiddenText("frontends", String.join(",", frontendNames)), frontends));
|
||||
}
|
||||
|
||||
cards.append("""
|
||||
<section class="panel" id="routes">
|
||||
<div class="section-head"><span>3</span><div><h2>Minecraft Routes</h2><p>%s</p></div></div>
|
||||
%s
|
||||
<div class="add-row">
|
||||
<label>%s<input name="newRouteName" placeholder="lobby"></label>
|
||||
<button type="submit" name="action" value="addRoute" class="secondary">%s</button>
|
||||
</div>
|
||||
%s
|
||||
</section>
|
||||
""".formatted(
|
||||
frontendMode
|
||||
? "Expose local Bungee backend addresses and map them to named backend nodes."
|
||||
: "Point tunnel routes at the local Minecraft server ports on this backend.",
|
||||
hiddenText("routes", String.join(",", routeNames)),
|
||||
frontendMode ? "New backend route name" : "New local route name",
|
||||
frontendMode ? "Add Backend" : "Add Route",
|
||||
routes
|
||||
));
|
||||
|
||||
String frontendSecurity = frontendMode ? """
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
""".formatted(
|
||||
checkbox(properties, "tunnel.tls.autoGenerate", "Auto-generate frontend certificate"),
|
||||
textInput(properties, "tunnel.tls.keyStore", "Frontend TLS keystore", "certs/frontend.p12"),
|
||||
textInput(properties, "tunnel.tls.keyStorePassword", "Keystore password", ""),
|
||||
checkbox(properties, "tunnel.tls.requireClientAuth", "Require TLS client certificates")
|
||||
) : "";
|
||||
String backendSecurity = backendMode
|
||||
? checkbox(properties, "tunnel.tls.trustOnFirstUse", "Trust first TLS certificate")
|
||||
: "";
|
||||
|
||||
cards.append("""
|
||||
<section class="panel" id="security">
|
||||
<div class="section-head"><span>4</span><div><h2>Security</h2><p>Use one shared secret token across the same private tunnel group.</p></div></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</section>
|
||||
""".formatted(
|
||||
textInput(properties, "tunnel.authToken", "Shared tunnel token", "copy the same token to every node"),
|
||||
checkbox(properties, "tunnel.tls.enabled", "Enable TLS"),
|
||||
checkbox(properties, "tunnel.tls.allowInsecure", "Allow insecure mode"),
|
||||
frontendSecurity,
|
||||
backendSecurity
|
||||
));
|
||||
|
||||
cards.append("""
|
||||
<section class="panel" id="reliability">
|
||||
<div class="section-head"><span>5</span><div><h2>Reliability</h2><p>Fast reconnect defaults are already tuned for most Minecraft networks.</p></div></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</section>
|
||||
""".formatted(
|
||||
textInput(properties, "tunnel.connectTimeoutMillis", "Connect timeout", "5000"),
|
||||
textInput(properties, "tunnel.heartbeatIntervalMillis", "Heartbeat interval", "2000"),
|
||||
textInput(properties, "tunnel.heartbeatTimeoutMillis", "Heartbeat timeout", "8000"),
|
||||
textInput(properties, "tunnel.heartbeatMissesBeforeDisconnect", "Missed heartbeats before disconnect", "4"),
|
||||
textInput(properties, "tunnel.reconnectInitialMillis", "First reconnect delay", "250"),
|
||||
textInput(properties, "tunnel.reconnectMaxMillis", "Max reconnect delay", "10000")
|
||||
));
|
||||
|
||||
cards.append("""
|
||||
<section class="panel" id="wizard">
|
||||
<div class="section-head"><span>6</span><div><h2>Web Wizard Access</h2><p>Keep this bound to localhost unless you are putting it behind your own secure tunnel.</p></div></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" id="advanced">
|
||||
<div class="section-head"><span>7</span><div><h2>Advanced Raw Settings</h2><p>Extra custom config keys are preserved here.</p></div></div>
|
||||
%s
|
||||
<div class="field-grid">
|
||||
<label>New key<input name="newRawKey" placeholder="example.setting"></label>
|
||||
<label>New value<input name="newRawValue"></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="action-bar">
|
||||
<button type="submit" name="action" value="save">Save Config</button>
|
||||
<button type="submit" name="action" value="saveReload">Save + Reload DirtSimpleP2P</button>
|
||||
<button type="submit" name="action" value="reloadOnly" class="secondary">Reload Current Config</button>
|
||||
<button type="submit" name="action" value="generateToken" class="secondary">Generate New Token</button>
|
||||
<a href="/logout">Sign out</a>
|
||||
</section>
|
||||
""".formatted(
|
||||
textInput(properties, "webwizard.bindHost", "Wizard bind host", "127.0.0.1"),
|
||||
textInput(properties, "webwizard.port", "Wizard port", "8765"),
|
||||
advancedRows
|
||||
));
|
||||
|
||||
return page("Setup Wizard", """
|
||||
<section class="hero">
|
||||
<div>
|
||||
<div class="eyebrow">Configuration Wizard</div>
|
||||
<h1>%s Control Center</h1>
|
||||
<p>Configure only the settings that apply to this server type, save safely, then reload DirtSimpleP2P without restarting Minecraft.</p>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<span>Detected mode</span>
|
||||
<strong>%s</strong>
|
||||
<span>Config file</span>
|
||||
<small>%s</small>
|
||||
<small>Wizard URL: %s</small>
|
||||
</div>
|
||||
</section>
|
||||
%s
|
||||
<form method="post" action="/save">
|
||||
<section class="layout">
|
||||
<aside class="steps">
|
||||
%s
|
||||
</aside>
|
||||
<div class="cards">
|
||||
%s
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
""".formatted(
|
||||
html(frontendMode ? "Proxy" : "Backend"),
|
||||
html(roleLabel),
|
||||
html(configPath.toString()),
|
||||
html(uri.toString()),
|
||||
alert(message),
|
||||
nav,
|
||||
cards
|
||||
));
|
||||
}
|
||||
|
||||
private static void addKnownGuidedKeys(Properties properties, Set<String> guidedKeys) {
|
||||
guidedKeys.addAll(List.of(
|
||||
"role",
|
||||
"node.name",
|
||||
"tunnel.listenHost",
|
||||
"tunnel.listenPort",
|
||||
"tunnel.connectHost",
|
||||
"tunnel.connectPort",
|
||||
"tunnel.authToken",
|
||||
"tunnel.connectTimeoutMillis",
|
||||
"tunnel.heartbeatIntervalMillis",
|
||||
@@ -405,158 +690,27 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
"tunnel.tls.keyStorePassword",
|
||||
"tunnel.tls.requireClientAuth",
|
||||
"tunnel.tls.trustOnFirstUse",
|
||||
"tunnel.tls.pinnedCertificateSha256",
|
||||
"webwizard.bindHost",
|
||||
"webwizard.port",
|
||||
"frontends",
|
||||
"routes"
|
||||
));
|
||||
|
||||
String advancedRows = advancedRows(properties, guidedKeys);
|
||||
|
||||
return page("Setup Wizard", """
|
||||
<section class="hero">
|
||||
<div>
|
||||
<div class="eyebrow">Configuration Wizard</div>
|
||||
<h1>DirtSimpleP2P Control Center</h1>
|
||||
<p>Configure the tunnel with guided fields, save safely, then reload DirtSimpleP2P without restarting the Minecraft server.</p>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<span>Config file</span>
|
||||
<strong>%s</strong>
|
||||
<small>Wizard URL: %s</small>
|
||||
</div>
|
||||
</section>
|
||||
%s
|
||||
<form method="post" action="/save">
|
||||
<section class="layout">
|
||||
<aside class="steps">
|
||||
<a href="#identity">1. Identity</a>
|
||||
<a href="#proxy">2. Proxy</a>
|
||||
<a href="#backend">3. Backend</a>
|
||||
<a href="#routes">4. Routes</a>
|
||||
<a href="#security">5. Security</a>
|
||||
<a href="#reliability">6. Reliability</a>
|
||||
<a href="#advanced">7. Advanced</a>
|
||||
</aside>
|
||||
<div class="cards">
|
||||
<section class="panel" id="identity">
|
||||
<div class="section-head"><span>1</span><div><h2>Server Identity</h2><p>Choose which side this plugin is running on.</p></div></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" id="proxy">
|
||||
<div class="section-head"><span>2</span><div><h2>Proxy Listener</h2><p>Used on BungeeCord or Waterfall. Backend nodes connect to this public tunnel port.</p></div></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" id="backend">
|
||||
<div class="section-head"><span>3</span><div><h2>Backend Connections</h2><p>Used on Paper or Spigot. Add every public proxy this backend should connect to.</p></div></div>
|
||||
%s
|
||||
<div class="add-row">
|
||||
<label>New proxy name<input name="newFrontendName" placeholder="proxy2"></label>
|
||||
</div>
|
||||
%s
|
||||
</section>
|
||||
<section class="panel" id="routes">
|
||||
<div class="section-head"><span>4</span><div><h2>Minecraft Routes</h2><p>Map Bungee local ports to named backend nodes and backend Minecraft ports.</p></div></div>
|
||||
%s
|
||||
<div class="add-row">
|
||||
<label>New route name<input name="newRouteName" placeholder="lobby"></label>
|
||||
</div>
|
||||
%s
|
||||
</section>
|
||||
<section class="panel" id="security">
|
||||
<div class="section-head"><span>5</span><div><h2>Security</h2><p>Use a shared secret token. Enable TLS for real deployments.</p></div></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" id="reliability">
|
||||
<div class="section-head"><span>6</span><div><h2>Reliability</h2><p>Fast reconnect defaults are already tuned for most Minecraft networks.</p></div></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" id="wizard">
|
||||
<div class="section-head"><span>7</span><div><h2>Web Wizard Access</h2><p>Keep this bound to localhost unless you are putting it behind your own secure tunnel.</p></div></div>
|
||||
<div class="field-grid">
|
||||
%s
|
||||
%s
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" id="advanced">
|
||||
<div class="section-head"><span>8</span><div><h2>Advanced Raw Settings</h2><p>Extra config keys are preserved here for power users.</p></div></div>
|
||||
%s
|
||||
<div class="field-grid">
|
||||
<label>New key<input name="newRawKey" placeholder="example.setting"></label>
|
||||
<label>New value<input name="newRawValue"></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="action-bar">
|
||||
<button type="submit" name="action" value="save">Save Config</button>
|
||||
<button type="submit" name="action" value="saveReload">Save + Reload DirtSimpleP2P</button>
|
||||
<button type="submit" name="action" value="reloadOnly" class="secondary">Reload Current Config</button>
|
||||
<button type="submit" name="action" value="generateToken" class="secondary">Generate New Token</button>
|
||||
<a href="/logout">Sign out</a>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
""".formatted(
|
||||
html(configPath.toString()),
|
||||
html(uri.toString()),
|
||||
alert(message),
|
||||
roleSelect(role),
|
||||
textInput(properties, "node.name", "Node name", role.equals("frontend") ? "bungee-frontend" : "paper-backend"),
|
||||
textInput(properties, "tunnel.listenHost", "Tunnel listen host", "0.0.0.0"),
|
||||
textInput(properties, "tunnel.listenPort", "Tunnel listen port", "24445"),
|
||||
hiddenText("frontends", String.join(",", frontendNames)),
|
||||
frontends,
|
||||
hiddenText("routes", String.join(",", routeNames)),
|
||||
routes,
|
||||
textInput(properties, "tunnel.authToken", "Shared tunnel token", "copy the same token to every node"),
|
||||
checkbox(properties, "tunnel.tls.enabled", "Enable TLS"),
|
||||
checkbox(properties, "tunnel.tls.allowInsecure", "Allow insecure mode"),
|
||||
checkbox(properties, "tunnel.tls.autoGenerate", "Auto-generate frontend certificate"),
|
||||
textInput(properties, "tunnel.tls.keyStore", "Frontend TLS keystore", "certs/frontend.p12"),
|
||||
textInput(properties, "tunnel.tls.keyStorePassword", "Keystore password", ""),
|
||||
checkbox(properties, "tunnel.tls.requireClientAuth", "Require TLS client certificates"),
|
||||
checkbox(properties, "tunnel.tls.trustOnFirstUse", "Trust first TLS certificate"),
|
||||
textInput(properties, "tunnel.connectTimeoutMillis", "Connect timeout", "5000"),
|
||||
textInput(properties, "tunnel.heartbeatIntervalMillis", "Heartbeat interval", "2000"),
|
||||
textInput(properties, "tunnel.heartbeatTimeoutMillis", "Heartbeat timeout", "8000"),
|
||||
textInput(properties, "tunnel.reconnectInitialMillis", "First reconnect delay", "250"),
|
||||
textInput(properties, "tunnel.reconnectMaxMillis", "Max reconnect delay", "10000"),
|
||||
textInput(properties, "webwizard.bindHost", "Wizard bind host", "127.0.0.1"),
|
||||
textInput(properties, "webwizard.port", "Wizard port", "8765"),
|
||||
advancedRows
|
||||
));
|
||||
for (String key : properties.stringPropertyNames()) {
|
||||
if (key.startsWith("frontend.") || key.startsWith("route.")) {
|
||||
guidedKeys.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String roleSelect(String role) {
|
||||
private static String roleBadge(String role, String label) {
|
||||
return """
|
||||
<label>Mode
|
||||
<select name="role">
|
||||
<option value="frontend"%s>Proxy / BungeeCord / Waterfall</option>
|
||||
<option value="backend"%s>Backend / Paper / Spigot</option>
|
||||
</select>
|
||||
<input type="hidden" name="role" value="%s">
|
||||
<input value="%s" readonly>
|
||||
</label>
|
||||
""".formatted(selected(role, "frontend"), selected(role, "backend"));
|
||||
""".formatted(attr(role), attr(label));
|
||||
}
|
||||
|
||||
private static String textInput(Properties properties, String key, String label, String placeholder) {
|
||||
@@ -586,10 +740,12 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
|
||||
StringBuilder rows = new StringBuilder("<div class=\"raw-grid\">");
|
||||
for (String key : keys) {
|
||||
rows.append("<label><code>").append(html(key)).append("</code>")
|
||||
rows.append("<div class=\"raw-item\"><div class=\"mini-title\"><code>").append(html(key)).append("</code>")
|
||||
.append("<button type=\"submit\" class=\"icon-button danger\" name=\"action\" value=\"removeRaw:")
|
||||
.append(attr(key)).append("\" title=\"Remove setting\">X</button></div><label>")
|
||||
.append("<input type=\"hidden\" name=\"rawKeys\" value=\"").append(attr(key)).append("\">")
|
||||
.append("<input name=\"raw.").append(attr(key)).append("\" value=\"")
|
||||
.append(attr(properties.getProperty(key, ""))).append("\"></label>");
|
||||
.append(attr(properties.getProperty(key, ""))).append("\"></label></div>");
|
||||
}
|
||||
rows.append("</div>");
|
||||
return rows.toString();
|
||||
@@ -602,64 +758,75 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>DirtSimpleP2P - %s</title>
|
||||
<title>DirtSimpleP2P - __TITLE__</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
color-scheme: dark;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--ink: #162033;
|
||||
--muted: #637086;
|
||||
--line: #d9e0eb;
|
||||
--panel: #ffffff;
|
||||
--soft: #f5f7fb;
|
||||
--blue: #1f66d1;
|
||||
--teal: #15847a;
|
||||
--gold: #a86c12;
|
||||
--ink: #e8edf7;
|
||||
--muted: #99a7bd;
|
||||
--line: #26354e;
|
||||
--panel: #111a2b;
|
||||
--panel-strong: #162238;
|
||||
--soft: #0d1524;
|
||||
--field: #0a1220;
|
||||
--blue: #5b8cff;
|
||||
--teal: #37d4bd;
|
||||
--danger: #ff6b7a;
|
||||
--shadow: rgba(0,0,0,.34);
|
||||
}
|
||||
body { margin: 0; min-height: 100vh; color: var(--ink); background: linear-gradient(180deg, #edf3fb 0, #f8fafc 280px); }
|
||||
main { width: min(1180px, calc(100%% - 32px)); margin: 28px auto 44px; }
|
||||
body { margin: 0; min-height: 100vh; color: var(--ink); background:
|
||||
radial-gradient(circle at 18% 0%, rgba(91,140,255,.18), transparent 28%),
|
||||
radial-gradient(circle at 84% 8%, rgba(55,212,189,.12), transparent 24%),
|
||||
linear-gradient(180deg, #070d18 0, #0b1220 280px, #080d17 100%);
|
||||
}
|
||||
main { width: min(1180px, calc(100% - 32px)); margin: 28px auto 44px; }
|
||||
.hero { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 18px; align-items: stretch; margin-bottom: 18px; }
|
||||
.hero, .panel, .login-shell, .action-bar { background: rgba(255,255,255,.94); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 18px 40px rgba(22,32,51,.10); }
|
||||
.hero, .panel, .login-shell, .action-bar { background: rgba(17,26,43,.94); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 18px 48px var(--shadow); }
|
||||
.hero { padding: 26px; }
|
||||
.eyebrow { color: var(--teal); font-weight: 800; text-transform: uppercase; letter-spacing: .08em; font-size: 12px; margin-bottom: 8px; }
|
||||
h1, h2 { margin: 0; letter-spacing: 0; }
|
||||
h1 { font-size: 32px; line-height: 1.1; }
|
||||
h2 { font-size: 20px; }
|
||||
p { color: var(--muted); line-height: 1.5; }
|
||||
.status-card { display: grid; gap: 8px; align-content: center; background: #132238; color: white; border-radius: 8px; padding: 18px; min-width: 0; }
|
||||
.status-card span, .status-card small { color: #c5d2e4; }
|
||||
.status-card { display: grid; gap: 8px; align-content: center; background: linear-gradient(180deg, #0c1729, #0a1220); color: white; border: 1px solid #2b3d5b; border-radius: 8px; padding: 18px; min-width: 0; }
|
||||
.status-card span, .status-card small { color: #9db0cb; }
|
||||
.status-card strong, .status-card small { overflow-wrap: anywhere; }
|
||||
.layout { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 18px; align-items: start; }
|
||||
.steps { position: sticky; top: 16px; display: grid; gap: 8px; background: #132238; border-radius: 8px; padding: 12px; }
|
||||
.steps a { color: #d8e5f6; text-decoration: none; padding: 10px 12px; border-radius: 6px; font-weight: 750; }
|
||||
.steps a:hover { background: rgba(255,255,255,.10); }
|
||||
.steps { position: sticky; top: 16px; display: grid; gap: 8px; background: rgba(10,18,32,.92); border: 1px solid var(--line); border-radius: 8px; padding: 12px; }
|
||||
.steps a { color: #c8d5ea; text-decoration: none; padding: 10px 12px; border-radius: 6px; font-weight: 750; }
|
||||
.steps a:hover { background: rgba(91,140,255,.14); color: #ffffff; }
|
||||
.cards { display: grid; gap: 16px; min-width: 0; }
|
||||
.panel { padding: 20px; }
|
||||
.section-head { display: flex; gap: 12px; align-items: start; margin-bottom: 16px; }
|
||||
.section-head > span { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 8px; color: white; background: var(--blue); font-weight: 850; flex: 0 0 auto; }
|
||||
.section-head > span { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 8px; color: white; background: linear-gradient(135deg, var(--blue), #37a7ff); font-weight: 850; flex: 0 0 auto; }
|
||||
.section-head p { margin: 4px 0 0; }
|
||||
.field-grid { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 14px; }
|
||||
.raw-grid { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 12px; margin-bottom: 16px; }
|
||||
.mini-card { border: 1px solid var(--line); background: var(--soft); border-radius: 8px; padding: 14px; margin: 12px 0; }
|
||||
.mini-title { font-weight: 850; margin-bottom: 12px; }
|
||||
.add-row { margin-top: 12px; max-width: 340px; }
|
||||
label { display: grid; gap: 6px; color: #253149; font-weight: 750; }
|
||||
input, select { width: 100%%; box-sizing: border-box; padding: 10px 12px; border: 1px solid #c7d1df; border-radius: 6px; background: white; color: var(--ink); font: inherit; }
|
||||
input:focus, select:focus { outline: 3px solid rgba(31,102,209,.18); border-color: var(--blue); }
|
||||
.check { display: flex; align-items: center; gap: 10px; min-height: 42px; border: 1px solid #c7d1df; border-radius: 6px; padding: 0 12px; background: white; }
|
||||
.mini-card, .raw-item { border: 1px solid var(--line); background: linear-gradient(180deg, var(--panel-strong), var(--soft)); border-radius: 8px; padding: 14px; margin: 12px 0; }
|
||||
.mini-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-weight: 850; margin-bottom: 12px; }
|
||||
.add-row { margin-top: 12px; display: grid; grid-template-columns: minmax(220px, 340px) auto; align-items: end; gap: 12px; }
|
||||
label { display: grid; gap: 6px; color: #d8e2f2; font-weight: 750; }
|
||||
input, select { width: 100%; box-sizing: border-box; padding: 10px 12px; border: 1px solid #334764; border-radius: 6px; background: var(--field); color: var(--ink); font: inherit; }
|
||||
input[readonly] { color: #aebbd0; background: #0d1728; }
|
||||
input::placeholder { color: #62738d; }
|
||||
input:focus, select:focus { outline: 3px solid rgba(91,140,255,.22); border-color: var(--blue); }
|
||||
.check { display: flex; align-items: center; gap: 10px; min-height: 42px; border: 1px solid #334764; border-radius: 6px; padding: 0 12px; background: var(--field); }
|
||||
.check input { width: 18px; height: 18px; }
|
||||
.alert { padding: 13px 15px; border-radius: 8px; margin: 14px 0; border: 1px solid #e3c16c; background: #fff8dd; color: #51370a; font-weight: 700; }
|
||||
.alert { padding: 13px 15px; border-radius: 8px; margin: 14px 0; border: 1px solid #725c27; background: #251d0d; color: #f7d47f; font-weight: 700; }
|
||||
.muted { color: var(--muted); }
|
||||
.action-bar { position: sticky; bottom: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; padding: 14px; }
|
||||
button { border: 0; border-radius: 6px; padding: 11px 15px; font: inherit; font-weight: 850; background: var(--blue); color: white; cursor: pointer; }
|
||||
button.secondary { background: #39465c; }
|
||||
.action-bar a, .login-shell a { color: var(--blue); font-weight: 800; text-decoration: none; }
|
||||
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; color: #33415c; }
|
||||
.action-bar { position: sticky; bottom: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; padding: 14px; backdrop-filter: blur(10px); }
|
||||
button { border: 0; border-radius: 6px; padding: 11px 15px; font: inherit; font-weight: 850; background: linear-gradient(135deg, var(--blue), #37a7ff); color: white; cursor: pointer; }
|
||||
button.secondary { background: #22314a; color: #dbe7fb; border: 1px solid #344865; }
|
||||
button.danger { background: #35141d; color: #ffd8dd; border: 1px solid #653040; }
|
||||
.icon-button { width: 30px; height: 30px; padding: 0; display: grid; place-items: center; border-radius: 6px; flex: 0 0 auto; }
|
||||
.action-bar a, .login-shell a { color: #8db2ff; font-weight: 800; text-decoration: none; }
|
||||
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; color: #b8c8e3; }
|
||||
.login-shell { max-width: 450px; margin: 76px auto; padding: 28px; }
|
||||
.brand-mark { width: 46px; height: 46px; display: grid; place-items: center; background: #132238; color: white; border-radius: 8px; font-weight: 900; margin-bottom: 18px; }
|
||||
.brand-mark { width: 46px; height: 46px; display: grid; place-items: center; background: linear-gradient(135deg, var(--blue), var(--teal)); color: white; border-radius: 8px; font-weight: 900; margin-bottom: 18px; }
|
||||
.stack { display: grid; gap: 14px; }
|
||||
@media (max-width: 880px) {
|
||||
main { width: min(100%% - 20px, 1180px); margin: 10px auto 24px; }
|
||||
main { width: min(100% - 20px, 1180px); margin: 10px auto 24px; }
|
||||
.hero { grid-template-columns: 1fr; padding: 18px; }
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.steps { position: static; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
@@ -668,9 +835,9 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body><main>%s</main></body>
|
||||
<body><main>__BODY__</main></body>
|
||||
</html>
|
||||
""".formatted(html(title), body);
|
||||
""".replace("__TITLE__", html(title)).replace("__BODY__", body);
|
||||
}
|
||||
|
||||
private boolean isAuthenticated(HttpExchange exchange) {
|
||||
@@ -843,6 +1010,22 @@ public final class WebWizardServer implements AutoCloseable {
|
||||
return host;
|
||||
}
|
||||
|
||||
private static String displayUriHost(String host) {
|
||||
if ("0.0.0.0".equals(host)) {
|
||||
return "127.0.0.1";
|
||||
}
|
||||
if ("::".equals(host) || "0:0:0:0:0:0:0:0".equals(host)) {
|
||||
return "[::1]";
|
||||
}
|
||||
return uriHost(host);
|
||||
}
|
||||
|
||||
private static boolean isWildcardHost(String host) {
|
||||
return "0.0.0.0".equals(host)
|
||||
|| "::".equals(host)
|
||||
|| "0:0:0:0:0:0:0:0".equals(host);
|
||||
}
|
||||
|
||||
private record WizardSettings(String bindHost, int port) {
|
||||
private static WizardSettings load(Path configPath) throws IOException {
|
||||
Properties properties = loadProperties(configPath);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user