feat: add Bukkit backend agent and shared CI publishing

This commit is contained in:
layue13 committed 2026-09-22 03:42:25 +08:00
1 parent cfa3c8bc51
commit 71b15cd180
45 files changed
+798 -173

No files matched your search

+9 -25
View File
@@ -1,4 +1,4 @@
name: ci
name: Build and publish
on:
push:
@@ -8,27 +8,11 @@ on:
workflow_dispatch:
jobs:
verify:
name: Java 25 verification
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Java 25
uses: actions/setup-java@v4
with:
distribution: zulu
java-version: "25"
cache: gradle
- name: Make Gradle wrapper executable
run: chmod +x ./gradlew
- name: Verify build, tests, release gates, and installed distributions
run: ./gradlew --no-daemon --configuration-cache check installDist :proxy-admin-cli:installDist :proxy-query:installDist
- name: Build release bundle
run: ./gradlew --no-daemon --configuration-cache release
build:
uses: https://git.nest.potatolab.uk:8443/TDLM/ci-templates/.gitea/workflows/build-publish.yml@main
with:
java-version: '25'
gradle-tasks: 'check release'
secrets:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
+5 -5
View File
@@ -1,14 +1,14 @@
# StrataProxy
StrataProxy 是 Java 25 Minecraft 后端代理。它只做连接接入、静态后端路由、健康检查、认证/转发协议、Minecraft zlib 压缩和必要的 Forge/Bungee 兼容。
StrataProxy 是 Java 25 Minecraft 后端代理。它负责连接接入、后端路由、健康检查、认证/转发协议、Minecraft zlib 压缩和必要的 Forge/Bungee 兼容;运行时后端可由代理插件或受限的 Bukkit 后端适配器注册。
没有 Admin HTTP API、管理 CLI、数据包分析/抓取、Prometheus/Grafana 资源或动态运维控制面。
没有 Admin HTTP API、管理 CLI、数据包分析/抓取或 Prometheus/Grafana 资源。Bukkit 适配器仅拥有后端注册、心跳与下线权限,不是动态运维控制面。
```powershell
.\gradlew.bat --no-daemon check :proxy-app:installDist
.\proxy-app\build\install\strataproxy\bin\strataproxy.bat --validate-config .\proxy-app\build\install\strataproxy\config\strataproxy.yml
.\gradlew.bat --no-daemon check :proxy-core:installDist
.\proxy-core\build\install\strataproxy\bin\strataproxy.bat --validate-config .\proxy-core\build\install\strataproxy\config\strataproxy.yml
```
第三方稳定边界只有 `proxy-plugin-api`。运行时由 `proxy-core`、`proxy-plugin-api`、`proxy-plugin` 与 `proxy-app` 组成。
工程只保留两个模块:`proxy-core` 是完整的代理运行时(启动、配置、插件加载与网络转发),`proxy-plugin-api` 是唯一稳定的第三方插件契约。
中文文档从 [docs/README.md](docs/README.md) 开始。
+32 -2
View File
@@ -9,6 +9,7 @@ import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.Sync
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.bundling.Zip
import java.security.MessageDigest
@@ -30,6 +31,35 @@ tasks.register("check") {
description = "Runs root-level StrataProxy verification tasks."
}
val bukkitAgentDirectory = layout.projectDirectory.dir("integrations/bukkit-backend-agent").asFile
val rootGradleWrapper = layout.projectDirectory.file(
if (System.getProperty("os.name").lowercase().contains("windows")) "gradlew.bat" else "gradlew"
).asFile
val buildBukkitBackendAgent = tasks.register<Exec>("buildBukkitBackendAgent") {
group = "verification"
description = "Builds the standalone Java 8-compatible Bukkit backend agent."
workingDir = projectDir
commandLine(rootGradleWrapper.absolutePath, "-p", bukkitAgentDirectory.absolutePath, "clean", "jar", "--no-daemon")
}
val publishBukkitBackendAgent = tasks.register<Exec>("publishBukkitBackendAgent") {
group = "publishing"
description = "Publishes the standalone Bukkit backend agent through its native Gradle publication."
workingDir = projectDir
commandLine(rootGradleWrapper.absolutePath, "-p", bukkitAgentDirectory.absolutePath, "publish", "--no-daemon")
}
tasks.named("check") {
dependsOn(buildBukkitBackendAgent)
}
tasks.register("publish") {
group = "publishing"
description = "Publishes StrataProxy plugin contracts and official Bukkit integration."
dependsOn(":proxy-plugin-api:publish", publishBukkitBackendAgent)
}
gradle.projectsEvaluated {
val releaseName = "strataproxy-${project.version}"
val releaseStaging = layout.buildDirectory.dir("release/staging/$releaseName")
@@ -37,7 +67,7 @@ gradle.projectsEvaluated {
val releaseChecksum = layout.buildDirectory.file("release/$releaseName.zip.sha256")
val releaseSbom = layout.buildDirectory.file("release/$releaseName.sbom.cdx.json")
val releaseMetadata = layout.buildDirectory.file("release/$releaseName.metadata.json")
val app = project(":proxy-app")
val app = project(":proxy-core")
val pluginApi = project(":proxy-plugin-api")
val appDistZip = app.layout.buildDirectory.file("distributions/strataproxy-${project.version}.zip")
val pluginApiJar = pluginApi.layout.buildDirectory.file("libs/proxy-plugin-api-${project.version}.jar")
@@ -107,7 +137,7 @@ gradle.projectsEvaluated {
from("examples") {
into("examples")
}
from("proxy-app/src/main/resources/config") {
from("proxy-core/src/main/resources/config") {
into("config")
}
from(layout.buildDirectory.file("release/RELEASE-MANIFEST.txt"))
+2 -2
View File
@@ -1,9 +1,9 @@
# 部署
将 `proxy-app` 的 `installDist` 产物和 YAML 配置部署到目标主机即可。示例 systemd 与容器文件只暴露 Minecraft 监听端口 `25577`,注册表持久化路径通过 `STRATAPROXY_DATA_DIR` 控制。
将 `proxy-core` 的 `installDist` 产物和 YAML 配置部署到目标主机即可。示例 systemd 与容器文件只暴露 Minecraft 监听端口 `25577`,注册表持久化路径通过 `STRATAPROXY_DATA_DIR` 控制。
```bash
./gradlew --no-daemon :proxy-app:installDist
./gradlew --no-daemon :proxy-core:installDist
```
启动前执行 `strataproxy --validate-config /path/to/strataproxy.yml`。生产环境不包含管理端口、指标端点或数据包抓取功能。
+2 -2
View File
@@ -4,12 +4,12 @@ ARG RUNTIME_IMAGE=eclipse-temurin:25-jre
FROM ${JAVA_IMAGE} AS build
WORKDIR /workspace
COPY . .
RUN ./gradlew --no-daemon :proxy-app:installDist
RUN ./gradlew --no-daemon :proxy-core:installDist
FROM ${RUNTIME_IMAGE}
RUN useradd --system --home-dir /opt/strataproxy --shell /usr/sbin/nologin strataproxy
WORKDIR /opt/strataproxy
COPY --from=build /workspace/proxy-app/build/install/strataproxy/ /opt/strataproxy/
COPY --from=build /workspace/proxy-core/build/install/strataproxy/ /opt/strataproxy/
RUN mkdir -p /opt/strataproxy/data \
&& cp /opt/strataproxy/config/strataproxy-production.yml /opt/strataproxy/config/strataproxy.yml \
&& chown -R strataproxy:strataproxy /opt/strataproxy
@@ -6,13 +6,13 @@ $ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
$Repo = Split-Path -Parent (Split-Path -Parent $Root)
$Config = Join-Path $Root "strataproxy-1.7.10-local.yml"
$Proxy = Join-Path $Repo "proxy-app\build\install\strataproxy\bin\strataproxy.bat"
$Proxy = Join-Path $Repo "proxy-core\build\install\strataproxy\bin\strataproxy.bat"
if (-not (Test-Path -LiteralPath $Proxy)) {
Push-Location $Repo
try {
$env:JAVA_HOME = $JavaHome
& .\gradlew.bat --no-daemon :proxy-app:installDist
& .\gradlew.bat --no-daemon :proxy-core:installDist
} finally {
Pop-Location
}
+1
View File
@@ -6,6 +6,7 @@
| YAML 参数 | [配置说明](configuration.md) |
| 部署与检查 | [运维说明](operations-manual.md) |
| 插件与游戏内命令 | [插件开发](plugins.md) |
| Bukkit 后端自动注册 | [Bukkit 适配器](../integrations/bukkit-backend-agent/README.md) |
| 模块边界 | [架构](architecture.md) |
| 协议支持范围 | [协议矩阵](protocol-version-matrix.md) |
+5 -5
View File
@@ -3,14 +3,14 @@
```text
第三方插件 ──> proxy-plugin-api
│
proxy-app ──> proxy-plugin ──> proxy-core
proxy-core
```
| 工程 | 职责 |
| --- | --- |
| `proxy-core` | Minecraft 协议、Netty 转发、路由、注册表、健康检查与兼容处理 |
| `proxy-core` | 完整代理运行时:启动、YAML 配置、插件加载、Minecraft 协议、Netty 转发、路由、注册表、健康检查与兼容处理 |
| `proxy-plugin-api` | 唯一稳定的第三方插件契约 |
| `proxy-plugin` | 插件装载、生命周期与服务实现 |
| `proxy-app` | YAML 配置、进程启动、静态后端与本地注册表持久化 |
模块边界按独立部署与稳定契约划分,不按每一种内部实现策略拆分。没有 `proxy-admin-cli`;运行中后端不通过 HTTP 修改,配置变更后重启代理生效。
模块边界只按稳定契约划分,不按内部实现策略拆分。没有 `proxy-admin-cli` 或 HTTP 管理面;运行时后端只由已加载插件通过公开 API 管理。
`integrations/bukkit-backend-agent/` 是独立的 Java 8 Bukkit 适配器工程,不属于代理运行时模块。它只能通过受限的 `backendAgent` 端点注册、续租和下线自己的后端,不能作为管理 API 使用。
+15 -1
View File
@@ -33,4 +33,18 @@ servers:
`network` 控制监听、帧上限和连接限制;`registry` 控制静态后端、健康检查和本地持久化;`compression` 控制 Minecraft 压缩协商与重写;`observability.flushIntervalSeconds` 仅控制后端已观测负载写回间隔。
`status` 控制服务器列表响应,`auth` 控制 online-mode 登录校验,`forwarding` 可选 `none`、`velocity-modern`、`bungee-legacy` 或 `bungee-guard`,`native` 控制 Netty 原生传输偏好。完整默认值见 [strataproxy.yml](../proxy-app/src/main/resources/config/strataproxy.yml)。
`status` 控制服务器列表响应,`auth` 控制 online-mode 登录校验,`forwarding` 可选 `none`、`velocity-modern`、`bungee-legacy` 或 `bungee-guard`,`native` 控制 Netty 原生传输偏好。完整默认值见 [strataproxy.yml](../proxy-core/src/main/resources/config/strataproxy.yml)。
## Bukkit 后端自动注册
`backendAgent` 默认关闭,且不是 Admin API。启用后只允许持有共享密钥的后端适配器注册、发送心跳或下线自己的服务:
```yaml
backendAgent:
enabled: true
bind: "127.0.0.1:25578"
sharedSecret: "至少32字符的随机密钥"
heartbeatTimeout: "30s"
```
若 Bukkit 服务在另一台受保护的主机,可把 `bind` 改为内网地址,并通过防火墙只允许该主机访问。适配器位于 [`integrations/bukkit-backend-agent`](../integrations/bukkit-backend-agent),其 `config.yml` 的密钥必须完全一致;心跳超时后代理会移除该适配器注册的后端。
+1 -4
View File
@@ -12,7 +12,6 @@
- `proxy-core/build/docs/javadoc`
- `proxy-plugin-api/build/docs/javadoc`
- `proxy-app/build/docs/javadoc`
大规模修改注释后,建议强制重跑,避免 Gradle up-to-date 状态隐藏文档问题:
@@ -24,10 +23,8 @@
本项目的 JavaDoc 重点覆盖公开契约:
- `proxy-core`:后端服务器描述、路由、协议元数据、Minecraft codec、Netty 生命周期、指标、诊断和运行时策略。这些包是代理内部实现,不单独作为 API 发布。
- `proxy-core`:代理启动、YAML 配置、插件加载、后端服务器描述、路由、Minecraft codec、Netty 生命周期和运行时策略。这些包是代理内部实现,不单独作为 API 发布。
- `proxy-plugin-api`:插件生命周期、命令、事件、调度器、玩家服务和服务器服务契约。
- `proxy-plugin`:运行时插件加载和生命周期管理。
- `proxy-app`:YAML 加载、配置校验、注册表持久化、管理 HTTP 视图、指标和诊断 DTO。
包私有 relay handler 和底层实现辅助类只在行为不直观、或影响公开契约时补充注释。
+1 -1
View File
@@ -2,7 +2,7 @@
StrataProxy 会从当前配置文件旁边的 `plugins/` 目录加载代理插件。插件 jar 是可选的;目录不存在时代理照常启动。
公开插件接口是 `dev.strataproxy:proxy-plugin-api` artifact。插件项目只应该依赖这个 API;`proxy-core`、`proxy-app` 和其他运行时模块都是内部实现细节。
公开插件接口是 `dev.strataproxy:proxy-plugin-api` artifact。插件项目只应该依赖这个 API;`proxy-core` 是内部实现细节。
在本仓库本地开发时,先发布 API 到本机 Maven:
+6 -6
View File
@@ -5,19 +5,19 @@ StrataProxy 是面向 Minecraft 的静态后端代理:监听客户端连接、
## 构建与校验
```powershell
.\gradlew.bat --no-daemon check :proxy-app:installDist
.\gradlew.bat --no-daemon check :proxy-core:installDist
```
代理程序在 `proxy-app\build\install\strataproxy\`。
代理程序在 `proxy-core\build\install\strataproxy\`。
## 配置并启动
```powershell
Copy-Item proxy-app\src\main\resources\config\strataproxy.yml .\strataproxy.yml
.\proxy-app\build\install\strataproxy\bin\strataproxy.bat --config .\strataproxy.yml --validate-config
.\proxy-app\build\install\strataproxy\bin\strataproxy.bat --config .\strataproxy.yml
Copy-Item proxy-core\src\main\resources\config\strataproxy.yml .\strataproxy.yml
.\proxy-core\build\install\strataproxy\bin\strataproxy.bat --config .\strataproxy.yml --validate-config
.\proxy-core\build\install\strataproxy\bin\strataproxy.bat --config .\strataproxy.yml
```
至少配置一个 `servers` 后端。修改 YAML 后重启代理生效;没有 HTTP 管理端口、管理令牌或动态后端 API。
至少配置一个 `servers` 后端。修改 YAML 后重启代理生效;没有 HTTP 管理端口或管理令牌。动态后端仅由已加载插件通过 API 注册、下线或切换 drain 状态。
详见[配置说明](configuration.md)和[运维说明](operations-manual.md)。
@@ -0,0 +1,9 @@
# StrataProxy Bukkit 后端适配器
这是独立的 Java 8 Bukkit 插件,不属于 StrataProxy 的 Gradle 多模块工程。它以 Bukkit/Spigot 1.8 API 编译,但只调用 Bukkit 1.7.10 已有的基础 API,因此可运行在 1.7.10 Bukkit 派生端。它在启动时向代理的 `backendAgent` 端点注册本服务器,随后发送心跳;关闭时可主动下线。
先在代理配置启用 `backendAgent`,并设置至少 32 字符的随机 `sharedSecret`。然后将同一密钥写入本插件 `config.yml`,按实际服务器修改 `backend.name`、`backend.address`、标签、容量和持久化选项。
构建:`gradle jar`。将生成的 `build/libs/strataproxy-bukkit-backend-agent-*.jar` 放入 Bukkit 的 `plugins/` 目录后重启。
该端点只接受注册、心跳和下线,不能读取玩家、执行命令或修改其他代理设置。
@@ -0,0 +1,51 @@
plugins {
id 'java'
id 'maven-publish'
}
group = 'dev.strataproxy'
version = '0.1.0-SNAPSHOT'
repositories {
maven { url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
mavenCentral()
}
dependencies {
// The implementation deliberately uses only API methods already present in Bukkit 1.7.10.
compileOnly('org.spigotmc:spigot-api:1.8-R0.1-SNAPSHOT') {
// This historical snapshot declares a now-unavailable chat artifact.
// The backend agent does not use Bukkit chat APIs.
exclude group: 'net.md-5', module: 'bungeecord-chat'
}
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}
jar {
archiveBaseName = 'strataproxy-bukkit-backend-agent'
}
publishing {
publications {
bukkitAgent(MavenPublication) {
from components.java
artifactId = 'strataproxy-bukkit-backend-agent'
}
}
repositories {
maven {
name = 'giteaPackages'
url = uri(System.getenv('MAVEN_URL') ?: 'https://git.nest.potatolab.uk:8443/api/packages/layue13/maven')
credentials {
username = System.getenv('MAVEN_USER') ?: 'NONE'
password = System.getenv('MAVEN_PASSWORD') ?: 'NONE'
}
}
}
}
@@ -0,0 +1 @@
rootProject.name = 'strataproxy-bukkit-backend-agent'
@@ -0,0 +1,170 @@
package dev.strataproxy.bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.java.JavaPlugin;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/** Bukkit-side adapter for the restricted StrataProxy backend-agent protocol. */
public final class StrataProxyBackendAgentPlugin extends JavaPlugin {
private BackendAgentClient client;
private int heartbeatTask = -1;
private boolean unregisterOnDisable;
@Override
public void onEnable() {
saveDefaultConfig();
try {
client = BackendAgentClient.from(getConfig());
} catch (IllegalArgumentException exception) {
getLogger().severe("Invalid StrataProxy backend-agent configuration: " + exception.getMessage());
getServer().getPluginManager().disablePlugin(this);
return;
}
unregisterOnDisable = getConfig().getBoolean("unregisterOnDisable", true);
runAsync("register");
long heartbeatTicks = Math.max(20L, getConfig().getLong("heartbeatSeconds", 10L) * 20L);
heartbeatTask = getServer().getScheduler().scheduleAsyncRepeatingTask(this, new Runnable() {
@Override
public void run() {
runAsync("heartbeat");
}
}, heartbeatTicks, heartbeatTicks);
}
@Override
public void onDisable() {
if (heartbeatTask >= 0) {
getServer().getScheduler().cancelTask(heartbeatTask);
}
if (client != null && unregisterOnDisable) {
runAsync("unregister");
}
}
private void runAsync(final String operation) {
getServer().getScheduler().runTaskAsynchronously(this, new Runnable() {
@Override
public void run() {
try {
String response = client.call(operation);
if (!response.contains("\"success\":true")) {
getLogger().warning("StrataProxy backend-agent " + operation + " failed: " + response);
}
} catch (Exception exception) {
getLogger().warning("StrataProxy backend-agent " + operation + " failed: " + exception.getMessage());
}
}
});
}
private static final class BackendAgentClient {
private final String proxyHost;
private final int proxyPort;
private final String secret;
private final String backendJson;
private final String backendName;
private BackendAgentClient(String proxyHost, int proxyPort, String secret, String backendJson, String backendName) {
this.proxyHost = proxyHost;
this.proxyPort = proxyPort;
this.secret = secret;
this.backendJson = backendJson;
this.backendName = backendName;
}
static BackendAgentClient from(org.bukkit.configuration.file.FileConfiguration config) {
String proxyHost = required(config.getString("proxy.host"), "proxy.host");
int proxyPort = config.getInt("proxy.port", 25578);
String secret = required(config.getString("proxy.sharedSecret"), "proxy.sharedSecret");
if (secret.length() < 32) throw new IllegalArgumentException("proxy.sharedSecret must be at least 32 characters");
String name = required(config.getString("backend.name"), "backend.name");
String[] address = required(config.getString("backend.address"), "backend.address").split(":", 2);
if (address.length != 2) throw new IllegalArgumentException("backend.address must be host:port");
int backendPort = Integer.parseInt(address[1]);
if (proxyPort < 1 || proxyPort > 65535 || backendPort < 1 || backendPort > 65535) throw new IllegalArgumentException("ports must be between 1 and 65535");
ConfigurationSection metadata = config.getConfigurationSection("backend.metadata");
StringBuilder json = new StringBuilder();
json.append('{').append(field("name", name)).append(',').append(field("host", address[0])).append(',')
.append("\"port\":").append(backendPort).append(',')
.append(array("tags", config.getStringList("backend.tags"))).append(',')
.append(array("capabilities", config.getStringList("backend.capabilities"))).append(',')
.append(field("protocolName", config.getString("backend.protocolName", "any"))).append(',')
.append("\"minProtocol\":").append(config.getInt("backend.minProtocol", 0)).append(',')
.append("\"maxProtocol\":").append(config.getInt("backend.maxProtocol", Integer.MAX_VALUE)).append(',')
.append("\"weight\":").append(config.getInt("backend.weight", 100)).append(',')
.append("\"softCapacity\":").append(config.getInt("backend.softCapacity", 0)).append(',')
.append("\"hardCapacity\":").append(config.getInt("backend.hardCapacity", 0)).append(',')
.append("\"drainMode\":").append(config.getBoolean("backend.drainMode", false)).append(',')
.append("\"persistent\":").append(config.getBoolean("backend.persistent", false)).append(',')
.append(map("metadata", metadata));
json.append('}');
return new BackendAgentClient(proxyHost, proxyPort, secret, json.toString(), name);
}
String call(String operation) throws Exception {
String payload = operation.equals("register") ? backendJsonWithOperation(operation) : "{\"operation\":\"" + operation + "\",\"name\":\"" + escape(backendName) + "\"}";
String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(payload.getBytes(StandardCharsets.UTF_8));
long timestamp = System.currentTimeMillis();
String nonce = UUID.randomUUID().toString().replace("-", "");
String signature = hex(hmac(timestamp + "\n" + nonce + "\n" + encoded));
String envelope = "{\"timestamp\":" + timestamp + ",\"nonce\":\"" + nonce + "\",\"payload\":\"" + encoded + "\",\"signature\":\"" + signature + "\"}";
Socket socket = new Socket(proxyHost, proxyPort);
try {
socket.setSoTimeout(5000);
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
out.write(envelope); out.newLine(); out.flush();
String response = in.readLine();
if (response == null) throw new IllegalStateException("proxy closed the registration connection");
return response;
} finally {
socket.close();
}
}
private String backendJsonWithOperation(String operation) {
return "{\"operation\":\"" + operation + "\"," + backendJson.substring(1);
}
private byte[] hmac(String input) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return mac.doFinal(input.getBytes(StandardCharsets.UTF_8));
}
private static String required(String value, String key) {
if (value == null || value.trim().isEmpty()) throw new IllegalArgumentException(key + " must not be blank");
return value.trim();
}
private static String field(String key, String value) { return "\"" + key + "\":\"" + escape(value) + "\""; }
private static String array(String key, List<String> values) {
List<String> escaped = new ArrayList<String>();
for (String value : values) escaped.add("\"" + escape(value) + "\"");
return "\"" + key + "\":[" + join(escaped) + "]";
}
private static String map(String key, ConfigurationSection values) {
List<String> entries = new ArrayList<String>();
if (values != null) for (String name : values.getKeys(false)) entries.add(field(name, String.valueOf(values.get(name))));
return "\"" + key + "\":{" + join(entries) + "}";
}
private static String join(List<String> values) { return values.isEmpty() ? "" : joinValues(values); }
private static String joinValues(List<String> values) { StringBuilder out = new StringBuilder(); for (String value : values) { if (out.length() > 0) out.append(','); out.append(value); } return out.toString(); }
private static String escape(String value) { return value.replace("\\", "\\\\").replace("\"", "\\\""); }
private static String hex(byte[] bytes) { StringBuilder out = new StringBuilder(); for (byte value : bytes) out.append(String.format("%02x", value & 0xff)); return out.toString(); }
}
}
@@ -0,0 +1,26 @@
proxy:
host: "127.0.0.1"
port: 25578
# Must match proxy backendAgent.sharedSecret. Generate a random value with at least 32 characters.
sharedSecret: "replace-with-a-random-secret"
backend:
name: "survival-1"
address: "127.0.0.1:25565"
tags: ["survival"]
capabilities: []
protocolName: "any"
minProtocol: 0
maxProtocol: 2147483647
weight: 100
softCapacity: 0
hardCapacity: 0
drainMode: false
persistent: false
metadata:
group: "survival"
# The proxy removes this backend after backendAgent.heartbeatTimeout if it stops receiving heartbeats.
heartbeatSeconds: 10
# When true, disable/restart unregisters immediately rather than waiting for lease expiry.
unregisterOnDisable: true
@@ -0,0 +1,5 @@
name: StrataProxyBackendAgent
main: dev.strataproxy.bukkit.StrataProxyBackendAgentPlugin
version: 0.1.0-SNAPSHOT
author: StrataProxy
description: Registers this Bukkit backend with a StrataProxy backend-agent endpoint.
-100
View File
@@ -1,100 +0,0 @@
import org.gradle.api.tasks.Exec
plugins {
id("strataproxy.application-conventions")
}
dependencies {
implementation(project(":proxy-core"))
implementation(project(":proxy-plugin"))
implementation(libs.findLibrary("jackson-databind").get())
implementation(libs.findLibrary("jackson-yaml").get())
runtimeOnly(libs.findLibrary("slf4j-simple").get())
}
val installedDistSmokeTest = tasks.register<Exec>("installedDistSmokeTest") {
group = "verification"
description = "Runs the installed StrataProxy distribution in --validate-config mode."
dependsOn(tasks.named("installDist"))
val installDir = layout.buildDirectory.dir("install/strataproxy")
inputs.dir(installDir)
doFirst {
val windows = System.getProperty("os.name").lowercase().contains("windows")
val script = installDir.get().file("bin/" + if (windows) "strataproxy.bat" else "strataproxy").asFile
val config = installDir.get().file("config/strataproxy.yml").asFile
commandLine(script.absolutePath, "--validate-config", config.absolutePath)
}
}
val installedDistProductionConfigSmokeTest = tasks.register<Exec>("installedDistProductionConfigSmokeTest") {
group = "verification"
description = "Validates the installed production StrataProxy config."
dependsOn(tasks.named("installDist"))
val installDir = layout.buildDirectory.dir("install/strataproxy")
inputs.dir(installDir)
doFirst {
val windows = System.getProperty("os.name").lowercase().contains("windows")
val script = installDir.get().file("bin/" + if (windows) "strataproxy.bat" else "strataproxy").asFile
val config = installDir.get().file("config/strataproxy-production.yml").asFile
commandLine(script.absolutePath, "--validate-config", config.absolutePath)
}
}
val installedDistHelpSmokeTest = tasks.register<Exec>("installedDistHelpSmokeTest") {
group = "verification"
description = "Runs the installed StrataProxy distribution help command."
dependsOn(tasks.named("installDist"))
val installDir = layout.buildDirectory.dir("install/strataproxy")
inputs.dir(installDir)
doFirst {
val windows = System.getProperty("os.name").lowercase().contains("windows")
val script = installDir.get().file("bin/" + if (windows) "strataproxy.bat" else "strataproxy").asFile
commandLine(script.absolutePath, "--help")
}
}
val installedDistVersionSmokeTest = tasks.register<Exec>("installedDistVersionSmokeTest") {
group = "verification"
description = "Runs the installed StrataProxy distribution version command."
dependsOn(tasks.named("installDist"))
val installDir = layout.buildDirectory.dir("install/strataproxy")
inputs.dir(installDir)
doFirst {
val windows = System.getProperty("os.name").lowercase().contains("windows")
val script = installDir.get().file("bin/" + if (windows) "strataproxy.bat" else "strataproxy").asFile
commandLine(script.absolutePath, "--version")
}
}
tasks.named("check") {
dependsOn(installedDistSmokeTest)
dependsOn(installedDistProductionConfigSmokeTest)
dependsOn(installedDistHelpSmokeTest)
dependsOn(installedDistVersionSmokeTest)
}
application {
mainClass.set("dev.strataproxy.app.StrataProxyLauncher")
applicationName = "strataproxy"
}
distributions {
main {
contents {
from("src/main/resources/config") {
into("config")
}
from("../deployment") {
into("deployment")
}
}
}
}
+93 -1
View File
@@ -1,5 +1,7 @@
import org.gradle.api.tasks.Exec
plugins {
id("strataproxy.java-library-conventions")
id("strataproxy.application-conventions")
}
val nettyVersion = libs.findVersion("netty").get().requiredVersion
@@ -18,4 +20,94 @@ dependencies {
runtimeOnly("io.netty:netty-transport-native-epoll:$nettyVersion:linux-aarch_64")
runtimeOnly("io.netty:netty-transport-native-kqueue:$nettyVersion:osx-x86_64")
runtimeOnly("io.netty:netty-transport-native-kqueue:$nettyVersion:osx-aarch_64")
implementation(libs.findLibrary("jackson-databind").get())
implementation(libs.findLibrary("jackson-yaml").get())
runtimeOnly(libs.findLibrary("slf4j-simple").get())
}
val installedDistSmokeTest = tasks.register<Exec>("installedDistSmokeTest") {
group = "verification"
description = "Runs the installed StrataProxy distribution in --validate-config mode."
dependsOn(tasks.named("installDist"))
val installDir = layout.buildDirectory.dir("install/strataproxy")
inputs.dir(installDir)
doFirst {
val windows = System.getProperty("os.name").lowercase().contains("windows")
val script = installDir.get().file("bin/" + if (windows) "strataproxy.bat" else "strataproxy").asFile
val config = installDir.get().file("config/strataproxy.yml").asFile
commandLine(script.absolutePath, "--validate-config", config.absolutePath)
}
}
val installedDistProductionConfigSmokeTest = tasks.register<Exec>("installedDistProductionConfigSmokeTest") {
group = "verification"
description = "Validates the installed production StrataProxy config."
dependsOn(tasks.named("installDist"))
val installDir = layout.buildDirectory.dir("install/strataproxy")
inputs.dir(installDir)
doFirst {
val windows = System.getProperty("os.name").lowercase().contains("windows")
val script = installDir.get().file("bin/" + if (windows) "strataproxy.bat" else "strataproxy").asFile
val config = installDir.get().file("config/strataproxy-production.yml").asFile
commandLine(script.absolutePath, "--validate-config", config.absolutePath)
}
}
val installedDistHelpSmokeTest = tasks.register<Exec>("installedDistHelpSmokeTest") {
group = "verification"
description = "Runs the installed StrataProxy distribution help command."
dependsOn(tasks.named("installDist"))
val installDir = layout.buildDirectory.dir("install/strataproxy")
inputs.dir(installDir)
doFirst {
val windows = System.getProperty("os.name").lowercase().contains("windows")
val script = installDir.get().file("bin/" + if (windows) "strataproxy.bat" else "strataproxy").asFile
commandLine(script.absolutePath, "--help")
}
}
val installedDistVersionSmokeTest = tasks.register<Exec>("installedDistVersionSmokeTest") {
group = "verification"
description = "Runs the installed StrataProxy distribution version command."
dependsOn(tasks.named("installDist"))
val installDir = layout.buildDirectory.dir("install/strataproxy")
inputs.dir(installDir)
doFirst {
val windows = System.getProperty("os.name").lowercase().contains("windows")
val script = installDir.get().file("bin/" + if (windows) "strataproxy.bat" else "strataproxy").asFile
commandLine(script.absolutePath, "--version")
}
}
tasks.named("check") {
dependsOn(installedDistSmokeTest)
dependsOn(installedDistProductionConfigSmokeTest)
dependsOn(installedDistHelpSmokeTest)
dependsOn(installedDistVersionSmokeTest)
}
application {
mainClass.set("dev.strataproxy.app.StrataProxyLauncher")
applicationName = "strataproxy"
}
distributions {
main {
contents {
from("src/main/resources/config") {
into("config")
}
from("../deployment") {
into("deployment")
}
}
}
}
@@ -0,0 +1,250 @@
package dev.strataproxy.agent;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.strataproxy.bootstrap.ProxyConfig;
import dev.strataproxy.plugin.service.ServerMutationResult;
import dev.strataproxy.plugin.service.ServerPersistence;
import dev.strataproxy.plugin.service.ServerProtocolRange;
import dev.strataproxy.plugin.service.ServerRegistration;
import dev.strataproxy.plugin.service.ServerRemoval;
import dev.strataproxy.plugin.service.ServerService;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.Base64;
import java.util.HexFormat;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Narrow authenticated endpoint for backend agents. It deliberately exposes only
* backend registration, lease renewal and removal; it is not an administration API.
*/
public final class BackendAgentServer implements AutoCloseable {
private static final int MAX_LINE_LENGTH = 64 * 1024;
private static final long MAX_CLOCK_SKEW_MILLIS = 60_000L;
private final ProxyConfig.BackendAgentConfig config;
private final ServerService servers;
private final ObjectMapper mapper = new ObjectMapper();
private final Map<String, Long> nonces = new ConcurrentHashMap<>();
private final Map<String, Lease> leases = new ConcurrentHashMap<>();
private final ExecutorService connections = Executors.newCachedThreadPool(runnable -> new Thread(runnable, "strataproxy-backend-agent"));
private final ScheduledExecutorService maintenance = Executors.newSingleThreadScheduledExecutor(runnable -> new Thread(runnable, "strataproxy-backend-agent-lease"));
private volatile ServerSocket socket;
private volatile boolean closed;
public BackendAgentServer(ProxyConfig.BackendAgentConfig config, ServerService servers) {
this.config = config;
this.servers = servers;
}
/** Starts the endpoint and the lease sweeper. */
public void start() throws IOException {
socket = new ServerSocket();
socket.bind(config.bindAddress());
connections.execute(this::acceptLoop);
var delay = Math.max(1_000L, config.heartbeatTimeout().toMillis() / 2L);
maintenance.scheduleWithFixedDelay(this::expireLeases, delay, delay, TimeUnit.MILLISECONDS);
}
private void acceptLoop() {
while (!closed) {
try {
var connection = socket.accept();
connections.execute(() -> handle(connection));
} catch (IOException exception) {
if (!closed) {
// The next accept is not useful after a listener failure.
close();
}
}
}
}
private void handle(Socket connection) {
try (connection;
var reader = new BufferedReader(new java.io.InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
var writer = new BufferedWriter(new java.io.OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8))) {
connection.setSoTimeout(5_000);
var line = reader.readLine();
var response = line == null || line.length() > MAX_LINE_LENGTH
? Response.failure("invalid_request", "request is missing or too large")
: process(line);
writer.write(mapper.writeValueAsString(response));
writer.newLine();
writer.flush();
} catch (IOException ignored) {
// A disconnected backend agent has no state to clean up here; lease expiry handles it.
}
}
private Response process(String line) {
try {
var envelope = mapper.readValue(line, Envelope.class);
if (envelope == null || envelope.nonce == null || envelope.payload == null || envelope.signature == null) {
return Response.failure("invalid_request", "missing authentication fields");
}
var now = System.currentTimeMillis();
if (Math.abs(now - envelope.timestamp) > MAX_CLOCK_SKEW_MILLIS) {
return Response.failure("unauthorized", "request timestamp is outside the allowed clock skew");
}
if (nonces.putIfAbsent(envelope.nonce, now) != null) {
return Response.failure("unauthorized", "request nonce was already used");
}
var signed = envelope.timestamp + "\n" + envelope.nonce + "\n" + envelope.payload;
var expected = hmac(signed);
if (!MessageDigest.isEqual(expected.getBytes(StandardCharsets.US_ASCII), envelope.signature.getBytes(StandardCharsets.US_ASCII))) {
return Response.failure("unauthorized", "invalid request signature");
}
var command = mapper.readValue(Base64.getUrlDecoder().decode(envelope.payload), Command.class);
if (command == null || command.operation == null || command.name == null || command.name.isBlank()) {
return Response.failure("invalid_request", "operation and backend name are required");
}
return switch (command.operation) {
case "register" -> register(command);
case "heartbeat" -> heartbeat(command);
case "unregister" -> unregister(command);
default -> Response.failure("invalid_request", "unsupported operation");
};
} catch (IllegalArgumentException exception) {
return Response.failure("invalid_request", exception.getMessage());
} catch (Exception exception) {
return Response.failure("request_failed", rootMessage(exception));
} finally {
var cutoff = System.currentTimeMillis() - MAX_CLOCK_SKEW_MILLIS;
nonces.entrySet().removeIf(entry -> entry.getValue() < cutoff);
}
}
private Response register(Command command) throws Exception {
if (command.host == null || command.host.isBlank() || command.port < 1 || command.port > 65535) {
return Response.failure("invalid_request", "backend host and port are required");
}
var persistence = command.persistent ? ServerPersistence.PERSISTENT : ServerPersistence.EPHEMERAL;
var registration = new ServerRegistration(
command.name,
new java.net.InetSocketAddress(command.host, command.port),
command.tags == null ? Set.of() : Set.copyOf(command.tags),
command.capabilities == null ? Set.of() : Set.copyOf(command.capabilities),
command.minProtocol == null && command.maxProtocol == null
? ServerProtocolRange.any()
: new ServerProtocolRange(command.minProtocol == null ? 0 : command.minProtocol,
command.maxProtocol == null ? Integer.MAX_VALUE : command.maxProtocol,
command.protocolName == null ? "configured" : command.protocolName),
command.weight == null ? 100 : command.weight,
command.softCapacity == null ? 0 : command.softCapacity,
command.hardCapacity == null ? 0 : command.hardCapacity,
command.drainMode,
command.metadata == null ? Map.of() : Map.copyOf(command.metadata),
persistence);
var result = servers.register(registration).toCompletableFuture().get(5, TimeUnit.SECONDS);
if (result.success()) {
leases.put(command.name.trim(), new Lease(System.currentTimeMillis(), persistence));
}
return response(result);
}
private Response heartbeat(Command command) {
var lease = leases.get(command.name.trim());
if (lease == null) {
return Response.failure("not_registered", "backend must register before sending heartbeats");
}
leases.put(command.name.trim(), new Lease(System.currentTimeMillis(), lease.persistence));
return Response.success("heartbeat", "");
}
private Response unregister(Command command) throws Exception {
var lease = leases.get(command.name.trim());
var persistence = lease == null ? (command.persistent ? ServerPersistence.PERSISTENT : ServerPersistence.EPHEMERAL) : lease.persistence;
var result = servers.unregister(command.name, new ServerRemoval(true, false, Duration.ZERO, persistence))
.toCompletableFuture().get(5, TimeUnit.SECONDS);
if (result.success()) {
leases.remove(command.name.trim());
}
return response(result);
}
private void expireLeases() {
var cutoff = System.currentTimeMillis() - config.heartbeatTimeout().toMillis();
leases.forEach((name, lease) -> {
if (lease.lastSeen < cutoff && leases.remove(name, lease)) {
servers.unregister(name, new ServerRemoval(true, false, Duration.ZERO, lease.persistence));
}
});
}
private String hmac(String value) throws Exception {
var mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(config.sharedSecret().getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return HexFormat.of().formatHex(mac.doFinal(value.getBytes(StandardCharsets.UTF_8)));
}
private static Response response(ServerMutationResult result) {
return new Response(result.success(), result.outcome(), result.message());
}
private static String rootMessage(Throwable exception) {
var cause = exception;
while (cause.getCause() != null) {
cause = cause.getCause();
}
return cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage();
}
@Override
public void close() {
closed = true;
try {
if (socket != null) socket.close();
} catch (IOException ignored) {
}
maintenance.shutdownNow();
connections.shutdownNow();
}
private record Lease(long lastSeen, ServerPersistence persistence) { }
public static final class Envelope {
public long timestamp;
public String nonce;
public String payload;
public String signature;
}
public static final class Command {
public String operation;
public String name;
public String host;
public int port;
public Set<String> tags;
public Set<String> capabilities;
public Integer minProtocol;
public Integer maxProtocol;
public String protocolName;
public Integer weight;
public Integer softCapacity;
public Integer hardCapacity;
public boolean drainMode;
public Map<String, String> metadata;
public boolean persistent;
}
public record Response(boolean success, String outcome, String message) {
static Response success(String outcome, String message) { return new Response(true, outcome, message); }
static Response failure(String outcome, String message) { return new Response(false, outcome, message == null ? "" : message); }
}
}
@@ -4,6 +4,7 @@ import dev.strataproxy.app.registry.RegistryPersistenceService;
import dev.strataproxy.app.registry.JsonRegistryStore;
import dev.strataproxy.app.registry.NoopRegistryStore;
import dev.strataproxy.app.registry.RegistryStore;
import dev.strataproxy.agent.BackendAgentServer;
import dev.strataproxy.bootstrap.ConfigLoader;
import dev.strataproxy.bootstrap.ConfigValidationResult;
import dev.strataproxy.bootstrap.ConfigValidator;
@@ -256,6 +257,16 @@ public final class StrataProxyLauncher {
started.add(loadReporter);
out.println("StrataProxy observed load flush interval " + config.observability().flushInterval());
if (config.backendAgent().enabled()) {
var backendAgent = new BackendAgentServer(
config.backendAgent(),
new PluginServerService(registry, registryPersistence, scheduler, "bukkit-agent"));
backendAgent.start();
started.add(backendAgent);
out.println("StrataProxy backend-agent bound on " + config.backendAgent().bindAddress()
+ " heartbeatTimeout=" + config.backendAgent().heartbeatTimeout());
}
var server = new NettyProxyNetworkServer(
config.resolvedWorkerThreads(),
resolver,
@@ -126,6 +126,8 @@ public final class ConfigLoader {
/** Public field for native runtime. */
@JsonProperty("native")
public NativeFile nativeRuntime = new NativeFile();
/** YAML value for the authenticated backend-agent endpoint. */
public BackendAgentFile backendAgent = new BackendAgentFile();
/** YAML value for servers. */
/** Public field for servers. */
public List<ServerFile> servers = List.of();
@@ -133,6 +135,7 @@ public final class ConfigLoader {
LoadedProxyConfig toLoadedConfig(Path configDirectory) {
var nativeFile = nativeRuntime == null ? new NativeFile() : nativeRuntime;
var statusFile = status == null ? new StatusFile() : status;
var backendAgentFile = backendAgent == null ? new BackendAgentFile() : backendAgent;
var bind = parseAddress(network.bind, 25577);
var proxy = new ProxyConfig(
bind,
@@ -192,7 +195,12 @@ public final class ConfigLoader {
nativeFile.preferOpenSslTls,
nativeFile.preferNativeCompression,
nativeFile.disabledFeatures,
nativeFile.forcedFeatures));
nativeFile.forcedFeatures),
new ProxyConfig.BackendAgentConfig(
backendAgentFile.enabled,
parseAddress(backendAgentFile.bind, 25578),
backendAgentFile.sharedSecret,
java.time.Duration.ofMillis(parseDurationMillis(backendAgentFile.heartbeatTimeout, 30_000))));
var descriptors = registry.staticServers
? servers.stream().map(ServerFile::toDescriptor).toList()
: List.<ServerDescriptor>of();
@@ -222,6 +230,20 @@ public final class ConfigLoader {
}
}
/**
* YAML configuration for the narrow backend-agent registration endpoint.
*/
public static final class BackendAgentFile {
/** Enables the endpoint. */
public boolean enabled = false;
/** Bind address; keep loopback unless backend agents are on another protected host. */
public String bind = "127.0.0.1:25578";
/** Shared secret used to authenticate each request. */
public String sharedSecret = "";
/** Maximum interval without a heartbeat before an agent-owned backend is removed. */
public String heartbeatTimeout = "30s";
}
/**
* YAML network section.
*/
@@ -130,6 +130,7 @@ public final class ConfigValidator {
validateStatus(config.status(), errors);
validateForwarding(config.forwarding(), errors);
validateNative(config.nativeRuntime(), errors, warnings);
validateBackendAgent(config.backendAgent(), errors);
}
@@ -229,6 +230,17 @@ public final class ConfigValidator {
}
}
private static void validateBackendAgent(ProxyConfig.BackendAgentConfig backendAgent, ArrayList<String> errors) {
if (backendAgent == null || !backendAgent.enabled()) {
return;
}
validateAddress("backendAgent.bind", backendAgent.bindAddress(), errors);
if (backendAgent.sharedSecret().trim().length() < 32) {
errors.add("backendAgent.sharedSecret must be at least 32 characters when backendAgent is enabled");
}
validatePositive("backendAgent.heartbeatTimeout", backendAgent.heartbeatTimeout(), errors);
}
private static void validateFeatureSet(String field, java.util.Set<String> features, ArrayList<String> errors) {
if (features == null) {
return;
@@ -263,7 +275,7 @@ public final class ConfigValidator {
warnings.add("server." + server.name() + " starts in drain mode");
}
}
if (count == 0) {
if (count == 0 && (config.backendAgent() == null || !config.backendAgent().enabled())) {
errors.add("at least one server must be configured");
}
}
@@ -31,7 +31,25 @@ public record ProxyConfig(
StatusConfig status,
AuthConfig auth,
ForwardingConfig forwarding,
NativeConfig nativeRuntime) {
NativeConfig nativeRuntime,
BackendAgentConfig backendAgent) {
/**
* Compatibility constructor for callers that do not configure the backend-agent endpoint.
*/
public ProxyConfig(
InetSocketAddress bindAddress,
int workerThreads,
boolean nativeTransport,
NetworkConfig network,
RegistryConfig registry,
CompressionConfig compression,
ObservabilityConfig observability,
StatusConfig status,
AuthConfig auth,
ForwardingConfig forwarding,
NativeConfig nativeRuntime) {
this(bindAddress, workerThreads, nativeTransport, network, registry, compression, observability, status, auth, forwarding, nativeRuntime, BackendAgentConfig.defaults());
}
/**
* Validates and normalizes record components.
*/
@@ -47,6 +65,7 @@ public record ProxyConfig(
auth = auth == null ? AuthConfig.defaults() : auth;
forwarding = forwarding == null ? ForwardingConfig.defaults() : forwarding;
nativeRuntime = nativeRuntime == null ? NativeConfig.defaults() : nativeRuntime;
backendAgent = backendAgent == null ? BackendAgentConfig.defaults() : backendAgent;
}
/**
@@ -75,7 +94,28 @@ public record ProxyConfig(
StatusConfig.defaults(),
AuthConfig.defaults(),
ForwardingConfig.defaults(),
NativeConfig.defaults());
NativeConfig.defaults(),
BackendAgentConfig.defaults());
}
/**
* Dedicated, authenticated endpoint used only by backend agents such as the Bukkit adapter.
* It is disabled by default and is not a general-purpose administration API.
*/
public record BackendAgentConfig(
boolean enabled,
InetSocketAddress bindAddress,
String sharedSecret,
Duration heartbeatTimeout) {
public BackendAgentConfig {
bindAddress = bindAddress == null ? new InetSocketAddress("127.0.0.1", 25578) : bindAddress;
sharedSecret = sharedSecret == null ? "" : sharedSecret;
heartbeatTimeout = heartbeatTimeout == null ? Duration.ofSeconds(30) : heartbeatTimeout;
}
public static BackendAgentConfig defaults() {
return new BackendAgentConfig(false, new InetSocketAddress("127.0.0.1", 25578), "", Duration.ofSeconds(30));
}
}
/**
@@ -25,6 +25,13 @@ registry:
# Use minecraft-status when backends should only receive traffic after a valid server-list response.
healthCheckMode: "minecraft-status"
# Enable only when using an authenticated backend agent such as integrations/bukkit-backend-agent.
backendAgent:
enabled: false
bind: "127.0.0.1:25578"
sharedSecret: "${STRATAPROXY_BACKEND_AGENT_SECRET:}"
heartbeatTimeout: "30s"
compression:
mode: adaptive
minThreshold: 256
@@ -25,6 +25,14 @@ registry:
# tcp keeps compatibility; minecraft-status verifies the backend speaks the Minecraft status protocol.
healthCheckMode: "tcp"
# Dedicated backend-agent endpoint for the official Bukkit adapter. This is not an Admin API.
# Keep it disabled unless a backend agent is installed; use a random secret of at least 32 characters.
backendAgent:
enabled: false
bind: "127.0.0.1:25578"
sharedSecret: ""
heartbeatTimeout: "30s"
compression:
mode: adaptive
minThreshold: 256
+8 -2
View File
@@ -25,8 +25,14 @@ publishing {
}
repositories {
maven {
name = "localPluginApi"
url = uri(layout.buildDirectory.dir("repo"))
name = "giteaPackages"
url = uri(providers.environmentVariable("MAVEN_URL")
.orElse("https://git.nest.potatolab.uk:8443/api/packages/layue13/maven")
.get())
credentials {
username = providers.environmentVariable("MAVEN_USER").orElse("NONE").get()
password = providers.environmentVariable("MAVEN_PASSWORD").orElse("NONE").get()
}
}
}
}
-9
View File
@@ -1,9 +0,0 @@
plugins {
id("strataproxy.java-library-conventions")
}
dependencies {
api(project(":proxy-plugin-api"))
api(libs.findLibrary("slf4j-api").get())
testImplementation(project(":proxy-core"))
}
-2
View File
@@ -22,6 +22,4 @@ rootProject.name = "strataproxy"
include(
"proxy-core",
"proxy-plugin-api",
"proxy-plugin",
"proxy-app",
)