From 1f5aa7b79ca9b1f1b42709dedc67ef13bb9a52cb Mon Sep 17 00:00:00 2001 From: ehko Date: Tue, 26 May 2026 13:30:22 -0400 Subject: [PATCH] bruh --- docs/03_Configuration.md | 21 +++++----- docs/08_Commands.md | 18 +++++++-- .../networkcore/NetworkCorePermissions.java | 12 ++++++ .../networkcore/bus/RedisMessageBus.java | 38 ++++++++++++------- .../command/NetworkCoreCommand.java | 24 ++++++++++++ .../networkcore/config/ConfigLoader.java | 4 ++ .../networkcore/config/CoreConfig.java | 7 ++++ .../registry/RedisServerRegistry.java | 8 ++-- src/main/resources/manifest.json | 2 +- 9 files changed, 103 insertions(+), 31 deletions(-) create mode 100644 src/main/java/net/kewwbec/networkcore/NetworkCorePermissions.java diff --git a/docs/03_Configuration.md b/docs/03_Configuration.md index d267149..aefb940 100644 --- a/docs/03_Configuration.md +++ b/docs/03_Configuration.md @@ -13,6 +13,7 @@ "role": "lobby" }, "redis": { + "url": null, "host": "127.0.0.1", "port": 6379, "password": null, @@ -63,13 +64,14 @@ | Field | Default | Meaning | |---|---|---| -| `host` | `"127.0.0.1"` | Redis host | -| `port` | `6379` | Redis port | -| `password` | `null` | Redis AUTH password. Prefer the env var (below). | -| `database` | `0` | Redis logical database (0-15 on stock Redis) | -| `key_prefix` | `"network:"` | All keys NetworkCore writes start with this prefix. Server registry keys end up at `network:servers:`. Change this only if you're sharing a Redis with something unrelated. | -| `tls` | `false` | Enables TLS for the Redis connection (Lettuce `rediss://` equivalent). **Set to true whenever Redis is on a different host than the Hytale server**, since the connection crosses untrusted network. | -| `verify_peer` | `true` | When `tls` is on, verifies the Redis server's certificate against the JVM's default trust store. Set to false only in dev / against self-signed certs. Logs a warning when disabled. | +| `url` | `null` | Full Redis connection URI. Supports both `redis://` (plain) and `rediss://` (TLS). When set, **all other redis fields are ignored** (host, port, password, database, tls). This is what you want when your provider hands you a one-line connection string. Prefer the env var (below) over putting credentials in the file. | +| `host` | `"127.0.0.1"` | Redis host. Only used when `url` is null. | +| `port` | `6379` | Redis port. Only used when `url` is null. | +| `password` | `null` | Redis AUTH password. Only used when `url` is null. Prefer the env var. | +| `database` | `0` | Redis logical database (0-15 on stock Redis). Only used when `url` is null. | +| `key_prefix` | `"network:"` | All keys NetworkCore writes start with this prefix. Server registry keys end up at `network:servers:`. Change this only if you're sharing a Redis with something unrelated. **Applies regardless of `url`.** | +| `tls` | `false` | Enables TLS for the Redis connection. Only used when `url` is null (the URI scheme picks TLS on/off when `url` is set). **Set to true whenever Redis is on a different host than the Hytale server**, since the connection crosses untrusted network. | +| `verify_peer` | `true` | When TLS is on (via `tls: true` or a `rediss://` url), verifies the Redis server's certificate against the JVM's default trust store. Set to false only in dev / against self-signed certs. Logs a warning when disabled. | ### network @@ -115,6 +117,7 @@ These take precedence over the config file at boot time. Use them for secrets so | Env var | Overrides | |---|---| +| `REDIS_URL` | `redis.url` (full connection URI) | | `REDIS_PASSWORD` | `redis.password` | | `NETWORK_AUTH_TOKEN` | `network.auth_token` | | `MYSQL_PASSWORD` | `mysql.password` | @@ -127,7 +130,7 @@ These take precedence over the config file at boot time. Use them for secrets so **Multi-server staging or production:** - `server.id` left null is fine if you're OK with the id changing on every boot. Set it to something stable like `lobby-prod-01` if you want logs and dashboards to keep continuity across restarts. - `server.role` set explicitly per server. This is what dashboards and routing use. -- `redis.host` pointed at your shared Redis. `redis.password` not in the file; set `REDIS_PASSWORD` env var. -- `redis.tls` **on** whenever Redis is on a different host than the Hytale server. `verify_peer` stays true unless Redis uses a self-signed cert you haven't loaded into the JVM trust store. +- **Easiest path for hosted Redis**: set `REDIS_URL` env var to the `rediss://user:pass@host:port/0` string your provider gives you. Leave the host/port/password/database fields at defaults. The `rediss://` scheme already implies TLS - no need to touch `tls`. +- **Alternative**: set `redis.host` / `redis.port` explicitly, put the password in `REDIS_PASSWORD`, and set `redis.tls: true`. Works the same; just more fields to keep in sync across servers. - `network.auth_token` not in the file; set `NETWORK_AUTH_TOKEN` env var to the same value on every server. - `metrics.bind` stays `127.0.0.1`. Run Prometheus on the same host (or use an SSH tunnel / reverse proxy with auth to expose it elsewhere). diff --git a/docs/08_Commands.md b/docs/08_Commands.md index aa26677..23189fa 100644 --- a/docs/08_Commands.md +++ b/docs/08_Commands.md @@ -4,6 +4,20 @@ Currently one in-game admin command: `/networkcore`. All subcommands are run by an in-world player. Console support isn't wired up yet. +## Permissions + +Every subcommand is gated by a permission node, checked via Hytale's `PermissionsModule`. Assign these to your staff/admin groups in the Hytale server's own permission config. + +| Subcommand | Required permission | +|---|---| +| `/networkcore status` | `networkcore.status` | +| `/networkcore servers` | `networkcore.servers` | +| `/networkcore publish` | `networkcore.publish` | + +You can grant the whole namespace with `networkcore.*` (Hytale's permission system handles wildcards). A player without the relevant node sees Hytale's standard `You do not have permission for this command.` message - the command system itself blocks execution before our code runs. + +Permissions are wired via `AbstractCommand.requirePermission(node)` in each subcommand constructor, and `canGeneratePermission()` is overridden to false on every command (including the root) so Hytale doesn't auto-generate a different one and clash with ours. + ## /networkcore status Prints local server identity, Redis state, and the metrics endpoint. @@ -49,10 +63,6 @@ The JSON is parsed and wrapped in the standard envelope (with your auth token if This is dangerous in production - it lets anyone with command access spoof any message on any channel. Lock it down with permissions before exposing to non-admins. -## Permissions - -Subcommand permissions aren't enforced yet. If your server runs HytalePerms, gate `/networkcore` behind an admin group at the command-routing layer. - ## Things this command does *not* do (and why) - **Restart Redis / reload config.** Restart the server. Reload is fragile - half-loaded state is worse than no state. diff --git a/src/main/java/net/kewwbec/networkcore/NetworkCorePermissions.java b/src/main/java/net/kewwbec/networkcore/NetworkCorePermissions.java new file mode 100644 index 0000000..fd5ccf3 --- /dev/null +++ b/src/main/java/net/kewwbec/networkcore/NetworkCorePermissions.java @@ -0,0 +1,12 @@ +package net.kewwbec.networkcore; + +public final class NetworkCorePermissions { + + public static final String ROOT = "networkcore"; + public static final String STATUS = ROOT + ".status"; + public static final String SERVERS = ROOT + ".servers"; + public static final String PUBLISH = ROOT + ".publish"; + + private NetworkCorePermissions() { + } +} diff --git a/src/main/java/net/kewwbec/networkcore/bus/RedisMessageBus.java b/src/main/java/net/kewwbec/networkcore/bus/RedisMessageBus.java index c808942..7e9b281 100644 --- a/src/main/java/net/kewwbec/networkcore/bus/RedisMessageBus.java +++ b/src/main/java/net/kewwbec/networkcore/bus/RedisMessageBus.java @@ -55,20 +55,32 @@ public final class RedisMessageBus implements MessageBus { this.serverId = serverId; this.authToken = (authToken == null || authToken.isEmpty()) ? null : authToken; - RedisURI.Builder b = RedisURI.Builder.redis(redis.host, redis.port) - .withDatabase(redis.database) - .withTimeout(Duration.ofSeconds(5)); - if (redis.password != null && !redis.password.isEmpty()) { - b.withPassword(redis.password.toCharArray()); - } - if (redis.tls) { - b.withSsl(true); - b.withVerifyPeer(redis.verify_peer); - if (!redis.verify_peer) { + RedisURI uri; + if (redis.url != null && !redis.url.isBlank()) { + uri = RedisURI.create(redis.url.trim()); + if (uri.getTimeout() == null) { + uri.setTimeout(Duration.ofSeconds(5)); + } + if (uri.isSsl() && !redis.verify_peer) { + uri.setVerifyPeer(io.lettuce.core.SslVerifyMode.NONE); LOGGER.at(Level.WARNING).log("Redis TLS is on but peer verification is OFF. Only acceptable in dev."); } + } else { + RedisURI.Builder b = RedisURI.Builder.redis(redis.host, redis.port) + .withDatabase(redis.database) + .withTimeout(Duration.ofSeconds(5)); + if (redis.password != null && !redis.password.isEmpty()) { + b.withPassword(redis.password.toCharArray()); + } + if (redis.tls) { + b.withSsl(true); + b.withVerifyPeer(redis.verify_peer); + if (!redis.verify_peer) { + LOGGER.at(Level.WARNING).log("Redis TLS is on but peer verification is OFF. Only acceptable in dev."); + } + } + uri = b.build(); } - RedisURI uri = b.build(); this.client = RedisClient.create(uri); this.commands = client.connect(); @@ -99,8 +111,8 @@ public final class RedisMessageBus implements MessageBus { this.rejectedCounter = (metrics == null) ? null : metrics.counter("messagebus_rejected_total", Map.of()); LOGGER.at(Level.INFO).log("Connected to Redis at %s:%d (db %d, tls=%s, auth=%s)", - redis.host, redis.port, redis.database, - redis.tls ? "on" : "off", + uri.getHost(), uri.getPort(), uri.getDatabase(), + uri.isSsl() ? "on" : "off", this.authToken != null ? "on" : "off"); } diff --git a/src/main/java/net/kewwbec/networkcore/command/NetworkCoreCommand.java b/src/main/java/net/kewwbec/networkcore/command/NetworkCoreCommand.java index c3f8b81..2834f1c 100644 --- a/src/main/java/net/kewwbec/networkcore/command/NetworkCoreCommand.java +++ b/src/main/java/net/kewwbec/networkcore/command/NetworkCoreCommand.java @@ -15,6 +15,7 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import net.kewwbec.networkcore.NetworkCore; +import net.kewwbec.networkcore.NetworkCorePermissions; import net.kewwbec.networkcore.api.DatabaseService; import net.kewwbec.networkcore.api.MessageBus; import net.kewwbec.networkcore.api.ServerInfo; @@ -33,6 +34,11 @@ public final class NetworkCoreCommand extends AbstractCommandCollection { addSubCommand(new PublishCommand(plugin)); } + @Override + protected boolean canGeneratePermission() { + return false; + } + public static final class StatusCommand extends AbstractPlayerCommand { private final NetworkCore plugin; @@ -40,6 +46,12 @@ public final class NetworkCoreCommand extends AbstractCommandCollection { public StatusCommand(@Nonnull NetworkCore plugin) { super("status", "Show local server status"); this.plugin = plugin; + requirePermission(NetworkCorePermissions.STATUS); + } + + @Override + protected boolean canGeneratePermission() { + return false; } @Override @@ -89,6 +101,12 @@ public final class NetworkCoreCommand extends AbstractCommandCollection { public ServersCommand(@Nonnull NetworkCore plugin) { super("servers", "List known servers in the network"); this.plugin = plugin; + requirePermission(NetworkCorePermissions.SERVERS); + } + + @Override + protected boolean canGeneratePermission() { + return false; } @Override @@ -122,6 +140,12 @@ public final class NetworkCoreCommand extends AbstractCommandCollection { public PublishCommand(@Nonnull NetworkCore plugin) { super("publish", "Publish a raw JSON payload to a channel"); this.plugin = plugin; + requirePermission(NetworkCorePermissions.PUBLISH); + } + + @Override + protected boolean canGeneratePermission() { + return false; } @Override diff --git a/src/main/java/net/kewwbec/networkcore/config/ConfigLoader.java b/src/main/java/net/kewwbec/networkcore/config/ConfigLoader.java index 6a43dea..210f65c 100644 --- a/src/main/java/net/kewwbec/networkcore/config/ConfigLoader.java +++ b/src/main/java/net/kewwbec/networkcore/config/ConfigLoader.java @@ -41,6 +41,10 @@ public final class ConfigLoader { @Nonnull private static CoreConfig applyEnvOverrides(@Nonnull CoreConfig config) { + String envUrl = System.getenv("REDIS_URL"); + if (envUrl != null && !envUrl.isEmpty()) { + config.redis.url = envUrl; + } String envPw = System.getenv("REDIS_PASSWORD"); if (envPw != null && !envPw.isEmpty()) { config.redis.password = envPw; diff --git a/src/main/java/net/kewwbec/networkcore/config/CoreConfig.java b/src/main/java/net/kewwbec/networkcore/config/CoreConfig.java index 091309b..c4c77a8 100644 --- a/src/main/java/net/kewwbec/networkcore/config/CoreConfig.java +++ b/src/main/java/net/kewwbec/networkcore/config/CoreConfig.java @@ -18,6 +18,13 @@ public final class CoreConfig { } public static final class RedisSection { + /** + * Full Redis URI ("redis://..." or "rediss://..."). When set, host/port/password/database/tls + * fields below are ignored. Use this when your provider gives you a one-line connection string. + */ + @Nullable + public String url = null; + public String host = "127.0.0.1"; public int port = 6379; @Nullable diff --git a/src/main/java/net/kewwbec/networkcore/registry/RedisServerRegistry.java b/src/main/java/net/kewwbec/networkcore/registry/RedisServerRegistry.java index 878694b..217d299 100644 --- a/src/main/java/net/kewwbec/networkcore/registry/RedisServerRegistry.java +++ b/src/main/java/net/kewwbec/networkcore/registry/RedisServerRegistry.java @@ -115,7 +115,7 @@ public final class RedisServerRegistry implements ServerRegistry { ServerInfo previous = cache.put(p.id, info); if (previous == null) { try { - eventBus.dispatchFor(ServerJoinedNetworkEvent.class).dispatch((IBaseEvent) new ServerJoinedNetworkEvent(info)); + eventBus.dispatchFor(ServerJoinedNetworkEvent.class).dispatch(new ServerJoinedNetworkEvent(info)); } catch (RuntimeException ignored) { } } @@ -123,7 +123,7 @@ public final class RedisServerRegistry implements ServerRegistry { ServerInfo removed = cache.remove(p.id); if (removed != null) { try { - eventBus.dispatchFor(ServerLeftNetworkEvent.class).dispatch((IBaseEvent) new ServerLeftNetworkEvent(removed)); + eventBus.dispatchFor(ServerLeftNetworkEvent.class).dispatch(new ServerLeftNetworkEvent(removed)); } catch (RuntimeException ignored) { } } @@ -183,7 +183,7 @@ public final class RedisServerRegistry implements ServerRegistry { ServerInfo info = latest.get(j); if (info != null) { try { - eventBus.dispatchFor(ServerJoinedNetworkEvent.class).dispatch((IBaseEvent) new ServerJoinedNetworkEvent(info)); + eventBus.dispatchFor(ServerJoinedNetworkEvent.class).dispatch(new ServerJoinedNetworkEvent(info)); } catch (RuntimeException ignored) { } } @@ -191,7 +191,7 @@ public final class RedisServerRegistry implements ServerRegistry { for (String l : left) { ServerInfo info = new ServerInfo(l, "unknown", null, 0, 0L, 0L); try { - eventBus.dispatchFor(ServerLeftNetworkEvent.class).dispatch((IBaseEvent) new ServerLeftNetworkEvent(info)); + eventBus.dispatchFor(ServerLeftNetworkEvent.class).dispatch(new ServerLeftNetworkEvent(info)); } catch (RuntimeException ignored) { } } diff --git a/src/main/resources/manifest.json b/src/main/resources/manifest.json index 3f03142..6e207b1 100644 --- a/src/main/resources/manifest.json +++ b/src/main/resources/manifest.json @@ -3,7 +3,7 @@ "Name": "NetworkCore", "Version": "0.1.0", "Description": "Cross-server messaging and metrics core", - "IncludesAssetsPack": false, + "IncludesAssetPack": false, "Authors": [ {"Name": "kewwbec"} ],