feat: velocity routing, server registry and RampartVelocity tests
This commit is contained in:
parent
fa6de281fb
commit
29ce6fb8e9
41 changed files with 1239 additions and 532 deletions
|
|
@ -8,8 +8,6 @@ import org.bukkit.event.player.PlayerLoginEvent;
|
|||
|
||||
public class HmacLoginListener implements Listener {
|
||||
|
||||
private static final int HEX_SIG_LENGTH = 64;
|
||||
|
||||
private final RampartPaper plugin;
|
||||
|
||||
public HmacLoginListener(RampartPaper plugin) {
|
||||
|
|
@ -21,28 +19,10 @@ public class HmacLoginListener implements Listener {
|
|||
String secretEnv = System.getenv("RAMPART_HMAC_SECRET");
|
||||
if (secretEnv == null || secretEnv.isEmpty()) return;
|
||||
|
||||
byte[] secret = secretEnv.getBytes();
|
||||
String raw = event.getHostname();
|
||||
if (raw == null || raw.isEmpty()) return;
|
||||
|
||||
int sepIdx = raw.indexOf(RampartPaper.SHIELD_SEPARATOR);
|
||||
if (sepIdx < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
String domain = raw.substring(0, sepIdx);
|
||||
String sig = raw.substring(sepIdx + RampartPaper.SHIELD_SEPARATOR.length());
|
||||
|
||||
if (sig.length() != HEX_SIG_LENGTH) {
|
||||
plugin.getLogger().warning("Invalid sig length from " + event.getAddress() +
|
||||
": got " + sig.length() + ", expected " + HEX_SIG_LENGTH);
|
||||
event.disallow(PlayerLoginEvent.Result.KICK_OTHER,
|
||||
Component.text("Connection rejected: invalid signature"));
|
||||
return;
|
||||
}
|
||||
|
||||
String expected = plugin.hmacHex(domain, secret);
|
||||
if (expected == null || !plugin.constantTimeEquals(sig, expected)) {
|
||||
if (!plugin.verifyHostname(raw)) {
|
||||
plugin.getLogger().warning("HMAC verification failed for " + event.getAddress());
|
||||
event.disallow(PlayerLoginEvent.Result.KICK_OTHER,
|
||||
Component.text("Connection rejected: invalid signature"));
|
||||
|
|
|
|||
|
|
@ -2,14 +2,25 @@ package me.rampart.paper;
|
|||
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class RampartPaper extends JavaPlugin {
|
||||
|
||||
static final String SHIELD_SEPARATOR = "\0shield\0";
|
||||
static final String HMAC_ALGO = "HmacSHA256";
|
||||
static final int HEX_SIG_LENGTH = 64;
|
||||
|
||||
private ShieldAgent shieldAgent;
|
||||
private long rotationSecs = 3600;
|
||||
private long ttlSecs = 60;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
rotationSecs = envLong("RAMPART_HMAC_ROTATION_SECS", 3600);
|
||||
ttlSecs = envLong("RAMPART_HMAC_TTL_SECS", 60);
|
||||
|
||||
String secret = System.getenv("RAMPART_HMAC_SECRET");
|
||||
if (secret == null || secret.isEmpty()) {
|
||||
getLogger().warning("RAMPART_HMAC_SECRET not set — HMAC verification disabled");
|
||||
|
|
@ -35,22 +46,66 @@ public class RampartPaper extends JavaPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
String hmacHex(String data, byte[] secret) {
|
||||
boolean verifyHostname(String raw) {
|
||||
String secretEnv = System.getenv("RAMPART_HMAC_SECRET");
|
||||
if (secretEnv == null || secretEnv.isEmpty()) return false;
|
||||
return verifyHostname(raw, secretEnv.getBytes(StandardCharsets.UTF_8), rotationSecs, ttlSecs);
|
||||
}
|
||||
|
||||
private boolean verifyHostname(String raw, byte[] secret, long rotation, long ttl) {
|
||||
int sepIdx = raw.indexOf(SHIELD_SEPARATOR);
|
||||
if (sepIdx < 0) return false;
|
||||
|
||||
String domain = raw.substring(0, sepIdx);
|
||||
String rest = raw.substring(sepIdx + SHIELD_SEPARATOR.length());
|
||||
int tsSep = rest.indexOf('\0');
|
||||
if (tsSep < 0) return false;
|
||||
String tsStr = rest.substring(0, tsSep);
|
||||
String sig = rest.substring(tsSep + 1);
|
||||
|
||||
if (sig.length() != HEX_SIG_LENGTH || !sig.matches("[0-9a-f]+")) return false;
|
||||
|
||||
long ts;
|
||||
try {
|
||||
var mac = javax.crypto.Mac.getInstance("HmacSHA256");
|
||||
mac.init(new javax.crypto.spec.SecretKeySpec(secret, "HmacSHA256"));
|
||||
byte[] raw = mac.doFinal(data.getBytes());
|
||||
StringBuilder sb = new StringBuilder(raw.length * 2);
|
||||
for (byte b : raw) {
|
||||
sb.append(String.format("%02x", b & 0xFF));
|
||||
ts = Long.parseLong(tsStr);
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis() / 1000;
|
||||
if (now < ts || now - ts > ttl) return false;
|
||||
|
||||
long tsBucket = ts / rotation;
|
||||
for (long bucket : new long[]{tsBucket, tsBucket - 1}) {
|
||||
byte[] derivedKey = hmacBytes(secret, ("rampart-key-" + bucket).getBytes(StandardCharsets.UTF_8));
|
||||
if (derivedKey == null) continue;
|
||||
String expected = toHex(hmacBytes(derivedKey, (domain + "|" + ts).getBytes(StandardCharsets.UTF_8)));
|
||||
if (expected != null && constantTimeEquals(sig, expected)) {
|
||||
return true;
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private byte[] hmacBytes(byte[] key, byte[] data) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_ALGO);
|
||||
mac.init(new SecretKeySpec(key, HMAC_ALGO));
|
||||
return mac.doFinal(data);
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("HMAC error: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String toHex(byte[] raw) {
|
||||
StringBuilder sb = new StringBuilder(raw.length * 2);
|
||||
for (byte b : raw) {
|
||||
sb.append(String.format("%02x", b & 0xFF));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
boolean constantTimeEquals(String a, String b) {
|
||||
if (a.length() != b.length()) return false;
|
||||
int result = 0;
|
||||
|
|
@ -59,4 +114,14 @@ public class RampartPaper extends JavaPlugin {
|
|||
}
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
private static long envLong(String name, long def) {
|
||||
String value = System.getenv(name);
|
||||
if (value == null || value.isEmpty()) return def;
|
||||
try {
|
||||
return Long.parseLong(value.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ public class ShieldAgent {
|
|||
private final RampartPaper plugin;
|
||||
private final Jedis jedis;
|
||||
private final String serverName;
|
||||
private final String serverDomain;
|
||||
private final String serverIp;
|
||||
private final int serverPort;
|
||||
private BukkitRunnable task;
|
||||
|
|
@ -35,6 +36,9 @@ public class ShieldAgent {
|
|||
}
|
||||
this.serverName = name;
|
||||
|
||||
String domain = System.getenv("RAMPART_SERVER_DOMAIN");
|
||||
this.serverDomain = (domain == null) ? "" : domain;
|
||||
|
||||
String ip = System.getenv("RAMPART_SERVER_IP");
|
||||
if (ip == null || ip.isEmpty()) {
|
||||
try {
|
||||
|
|
@ -108,7 +112,8 @@ public class ShieldAgent {
|
|||
}
|
||||
|
||||
private String buildJson(String status, int online, int maxPlayers, double tps) {
|
||||
return "{\"name\":\"" + serverName + "\",\"type\":\"paper\",\"ip\":\"" + serverIp
|
||||
return "{\"name\":\"" + serverName + "\",\"type\":\"paper\",\"domain\":\"" + serverDomain
|
||||
+ "\",\"ip\":\"" + serverIp
|
||||
+ "\",\"port\":" + serverPort + ",\"status\":\"" + status
|
||||
+ "\",\"online\":" + online + ",\"max_players\":" + maxPlayers
|
||||
+ ",\"tps\":" + tps
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ dependencies {
|
|||
|
||||
implementation("redis.clients:jedis:5.2.0")
|
||||
|
||||
testImplementation("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
|
||||
testImplementation("org.slf4j:slf4j-api:2.0.16")
|
||||
testRuntimeOnly("org.slf4j:slf4j-simple:2.0.16")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package me.rampart.velocity;
|
|||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
||||
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||
import com.velocitypowered.api.event.connection.LoginEvent;
|
||||
import com.velocitypowered.api.event.player.PlayerChatEvent;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
|
|
@ -14,7 +13,6 @@ import java.util.Map;
|
|||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class CaptchaHandler {
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import org.slf4j.Logger;
|
|||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Map;
|
||||
|
|
@ -23,10 +24,14 @@ public class HmacCheckListener {
|
|||
|
||||
private final Logger logger;
|
||||
private final byte[] secret;
|
||||
private final long rotationSecs;
|
||||
private final long ttlSecs;
|
||||
|
||||
public HmacCheckListener(Logger logger, String secret) {
|
||||
public HmacCheckListener(Logger logger, String secret, long rotationSecs, long ttlSecs) {
|
||||
this.logger = logger;
|
||||
this.secret = secret.getBytes();
|
||||
this.secret = secret.getBytes(StandardCharsets.UTF_8);
|
||||
this.rotationSecs = rotationSecs;
|
||||
this.ttlSecs = ttlSecs;
|
||||
}
|
||||
|
||||
public static void markVerified(UUID uuid) {
|
||||
|
|
@ -34,6 +39,10 @@ public class HmacCheckListener {
|
|||
PhysicsCheckListener.clearSuspicion(uuid);
|
||||
}
|
||||
|
||||
public static boolean isVerified(UUID uuid) {
|
||||
return verifiedPlayers.containsKey(uuid);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onLogin(LoginEvent event) {
|
||||
var player = event.getPlayer();
|
||||
|
|
@ -53,60 +62,83 @@ public class HmacCheckListener {
|
|||
return;
|
||||
}
|
||||
|
||||
int sepIdx = raw.indexOf(SHIELD_SEPARATOR);
|
||||
if (sepIdx < 0) {
|
||||
if (raw.indexOf(SHIELD_SEPARATOR) < 0) {
|
||||
event.setResult(LoginEvent.ComponentResult.denied(
|
||||
Component.text("Connection rejected: unsigned connection")
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
String domain = raw.substring(0, sepIdx);
|
||||
String sig = raw.substring(sepIdx + SHIELD_SEPARATOR.length());
|
||||
|
||||
if (sig.length() != HEX_SIG_LENGTH) {
|
||||
logger.warn("Invalid HMAC signature length from {}: got {}, expected {}",
|
||||
player.getRemoteAddress(), sig.length(), HEX_SIG_LENGTH);
|
||||
event.setResult(LoginEvent.ComponentResult.denied(
|
||||
Component.text("Connection rejected: invalid signature")
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
String expected = hmacHex(domain);
|
||||
if (expected == null) {
|
||||
event.setResult(LoginEvent.ComponentResult.denied(
|
||||
Component.text("Connection rejected: internal error")
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!constantTimeEquals(sig, expected)) {
|
||||
logger.warn("HMAC verification failed for {} (domain: {})",
|
||||
player.getRemoteAddress(), domain);
|
||||
if (!verifyHostname(raw, secret, rotationSecs, ttlSecs)) {
|
||||
logger.warn("HMAC verification failed for {} (hostname: {})",
|
||||
player.getRemoteAddress(), raw);
|
||||
event.setResult(LoginEvent.ComponentResult.denied(
|
||||
Component.text("Connection rejected: invalid signature")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private String hmacHex(String data) {
|
||||
static boolean verifyHostname(String raw, byte[] secret, long rotationSecs, long ttlSecs) {
|
||||
if (raw == null) return false;
|
||||
int sepIdx = raw.indexOf(SHIELD_SEPARATOR);
|
||||
if (sepIdx < 0) return false;
|
||||
|
||||
String domain = raw.substring(0, sepIdx);
|
||||
String rest = raw.substring(sepIdx + SHIELD_SEPARATOR.length());
|
||||
int tsSep = rest.indexOf('\0');
|
||||
if (tsSep < 0) return false;
|
||||
String tsStr = rest.substring(0, tsSep);
|
||||
String sig = rest.substring(tsSep + 1);
|
||||
|
||||
if (sig.length() != HEX_SIG_LENGTH || !sig.matches("[0-9a-f]+")) return false;
|
||||
|
||||
long ts;
|
||||
try {
|
||||
ts = Long.parseLong(tsStr);
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis() / 1000;
|
||||
if (now < ts || now - ts > ttlSecs) return false;
|
||||
|
||||
long tsBucket = ts / rotationSecs;
|
||||
for (long bucket : new long[]{tsBucket, tsBucket - 1}) {
|
||||
String expected = sign(domain, ts, bucket, secret);
|
||||
if (expected != null && constantTimeEquals(sig, expected)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String sign(String domain, long ts, long bucket, byte[] masterSecret) {
|
||||
byte[] derivedKey = hmacRaw(masterSecret, ("rampart-key-" + bucket).getBytes(StandardCharsets.UTF_8));
|
||||
if (derivedKey == null) return null;
|
||||
byte[] sig = hmacRaw(derivedKey, (domain + "|" + ts).getBytes(StandardCharsets.UTF_8));
|
||||
if (sig == null) return null;
|
||||
return toHex(sig);
|
||||
}
|
||||
|
||||
private static byte[] hmacRaw(byte[] key, byte[] data) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_ALGO);
|
||||
mac.init(new SecretKeySpec(secret, HMAC_ALGO));
|
||||
byte[] raw = mac.doFinal(data.getBytes());
|
||||
StringBuilder sb = new StringBuilder(raw.length * 2);
|
||||
for (byte b : raw) {
|
||||
sb.append(String.format("%02x", b & 0xFF));
|
||||
}
|
||||
return sb.toString();
|
||||
mac.init(new SecretKeySpec(key, HMAC_ALGO));
|
||||
return mac.doFinal(data);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
logger.error("HMAC error", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean constantTimeEquals(String a, String b) {
|
||||
private static String toHex(byte[] raw) {
|
||||
StringBuilder sb = new StringBuilder(raw.length * 2);
|
||||
for (byte b : raw) {
|
||||
sb.append(String.format("%02x", b & 0xFF));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static boolean constantTimeEquals(String a, String b) {
|
||||
if (a.length() != b.length()) return false;
|
||||
int result = 0;
|
||||
for (int i = 0; i < a.length(); i++) {
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ public class PhysicsCheckListener {
|
|||
|
||||
private void triggerReVerify(Player player, PlayerTracker tracker) {
|
||||
if (tracker.reVerificationTriggered) return;
|
||||
if (HmacCheckListener.isVerified(player.getUniqueId())) return;
|
||||
tracker.reVerificationTriggered = true;
|
||||
player.disconnect(Component.text("Re-verification required. Please reconnect."));
|
||||
logger.info("Triggered re-verification for {}", player.getUsername());
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ package me.rampart.velocity;
|
|||
|
||||
import com.google.inject.Inject;
|
||||
import com.velocitypowered.api.event.EventManager;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.player.ServerPostConnectEvent;
|
||||
import com.velocitypowered.api.plugin.Plugin;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
|
|
@ -38,8 +41,10 @@ public class RampartVelocity {
|
|||
}
|
||||
|
||||
if (secret != null && !secret.isEmpty()) {
|
||||
logger.info("HMAC verification enabled");
|
||||
em.register(this, new HmacCheckListener(logger, secret));
|
||||
long rotationSecs = envLong("RAMPART_HMAC_ROTATION_SECS", 3600);
|
||||
long ttlSecs = envLong("RAMPART_HMAC_TTL_SECS", 60);
|
||||
logger.info("HMAC verification enabled (rotation={}s, ttl={}s)", rotationSecs, ttlSecs);
|
||||
em.register(this, new HmacCheckListener(logger, secret, rotationSecs, ttlSecs));
|
||||
} else {
|
||||
logger.warn("RAMPART_HMAC_SECRET not set — HMAC verification disabled");
|
||||
}
|
||||
|
|
@ -61,11 +66,39 @@ public class RampartVelocity {
|
|||
|
||||
CaptchaHandler captchaHandler = new CaptchaHandler(logger, server);
|
||||
em.register(this, captchaHandler);
|
||||
em.register(this, new CaptchaChallengeListener(captchaHandler));
|
||||
logger.info("CAPTCHA handler enabled");
|
||||
|
||||
logger.info("Server registry + load balancer started with Redis at {}", redisUrl);
|
||||
}
|
||||
|
||||
private static class CaptchaChallengeListener {
|
||||
|
||||
private final CaptchaHandler captchaHandler;
|
||||
|
||||
CaptchaChallengeListener(CaptchaHandler captchaHandler) {
|
||||
this.captchaHandler = captchaHandler;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onServerPostConnect(ServerPostConnectEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (player.isActive()) {
|
||||
captchaHandler.challenge(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long envLong(String name, long def) {
|
||||
String value = System.getenv(name);
|
||||
if (value == null || value.isEmpty()) return def;
|
||||
try {
|
||||
return Long.parseLong(value.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> loadDomainWhitelist() {
|
||||
String env = System.getenv("RAMPART_ALLOWED_DOMAINS");
|
||||
if (env == null || env.isEmpty()) return Collections.emptyList();
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ public class ServerRegistry {
|
|||
private final String redisUrl;
|
||||
private final AtomicInteger counter = new AtomicInteger(0);
|
||||
private final ConcurrentHashMap<String, Double> tpsCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, String> serverDomains = new ConcurrentHashMap<>();
|
||||
private volatile List<RegisteredServer> cachedServers = new ArrayList<>();
|
||||
|
||||
public ServerRegistry(ProxyServer proxyServer, Logger logger, String redisUrl) {
|
||||
|
|
@ -63,6 +64,7 @@ public class ServerRegistry {
|
|||
if (!redisNames.contains(name)) {
|
||||
proxyServer.getServer(name).ifPresent(s ->
|
||||
proxyServer.unregisterServer(s.getServerInfo()));
|
||||
serverDomains.remove(name);
|
||||
unregistered++;
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +97,12 @@ public class ServerRegistry {
|
|||
if (!"online".equals(status)) continue;
|
||||
double tps = extractJsonDouble(json, "tps");
|
||||
tpsCache.put(name, tps);
|
||||
String domain = extractJsonString(json, "domain");
|
||||
if (domain != null && !domain.isEmpty()) {
|
||||
serverDomains.put(name, domain.trim());
|
||||
} else {
|
||||
serverDomains.remove(name);
|
||||
}
|
||||
servers.add(new ServerInfo(name, InetSocketAddress.createUnresolved(ip, port)));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to parse server data for key {}: {}", key, e.getMessage());
|
||||
|
|
@ -121,6 +129,10 @@ public class ServerRegistry {
|
|||
return tpsCache.getOrDefault(name, 20.0);
|
||||
}
|
||||
|
||||
public String getServerDomain(String name) {
|
||||
return serverDomains.get(name);
|
||||
}
|
||||
|
||||
private static String extractJsonString(String json, String key) {
|
||||
String search = "\"" + key + "\":\"";
|
||||
int start = json.indexOf(search);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@ public class ServerRouter {
|
|||
}
|
||||
|
||||
public Optional<RegisteredServer> routeServer(String domain) {
|
||||
String cleanDomain = domain == null ? "" : domain.trim();
|
||||
if (!cleanDomain.isEmpty()) {
|
||||
for (RegisteredServer server : registry.getCachedServers()) {
|
||||
String name = server.getServerInfo().getName();
|
||||
if (cleanDomain.equals(registry.getServerDomain(name))
|
||||
&& registry.getServerTps(name) >= TPS_DEAD) {
|
||||
return Optional.of(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (RegisteredServer server : registry.getCachedServers()) {
|
||||
double tps = registry.getServerTps(server.getServerInfo().getName());
|
||||
if (tps >= TPS_DEAD) {
|
||||
|
|
|
|||
|
|
@ -113,6 +113,101 @@ public class RampartVelocityTest {
|
|||
assertTrue(DomainCheckUtil.isDomainAllowed("192.168.1.1", List.of()));
|
||||
}
|
||||
|
||||
// --- Dual-key hostname verification (mirrors HmacCheckListener.verifyHostname) ---
|
||||
|
||||
@Test
|
||||
void hostnameVerifyAcceptsValidSignature() {
|
||||
long ts = System.currentTimeMillis() / 1000;
|
||||
String raw = signHostname("play.example.com", ts, 3600, "test_secret");
|
||||
assertTrue(HmacCheckListener.verifyHostname(raw, "test_secret".getBytes(), 3600, 60));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hostnameVerifyRejectsWrongSecret() {
|
||||
long ts = System.currentTimeMillis() / 1000;
|
||||
String raw = signHostname("play.example.com", ts, 3600, "secret_a");
|
||||
assertFalse(HmacCheckListener.verifyHostname(raw, "secret_b".getBytes(), 3600, 60));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hostnameVerifyRejectsTamperedSig() {
|
||||
long ts = System.currentTimeMillis() / 1000;
|
||||
String raw = signHostname("play.example.com", ts, 3600, "test_secret");
|
||||
char flip = raw.charAt(raw.length() - 1) == '0' ? '1' : '0';
|
||||
String tampered = raw.substring(0, raw.length() - 1) + flip;
|
||||
assertFalse(HmacCheckListener.verifyHostname(tampered, "test_secret".getBytes(), 3600, 60));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hostnameVerifyRejectsUnsigned() {
|
||||
assertFalse(HmacCheckListener.verifyHostname("play.example.com", "test_secret".getBytes(), 3600, 60));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hostnameVerifyRejectsMalformedTs() {
|
||||
String raw = "play.example.com\0shield\0notanumber\0" + "a".repeat(64);
|
||||
assertFalse(HmacCheckListener.verifyHostname(raw, "test_secret".getBytes(), 3600, 60));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hostnameVerifyRejectsBadSigLength() {
|
||||
long ts = System.currentTimeMillis() / 1000;
|
||||
String raw = "play.example.com\0shield\0" + ts + "\0" + "a".repeat(63);
|
||||
assertFalse(HmacCheckListener.verifyHostname(raw, "test_secret".getBytes(), 3600, 60));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hostnameVerifyRejectsExpired() {
|
||||
long ts = System.currentTimeMillis() / 1000 - 120;
|
||||
String raw = signHostname("play.example.com", ts, 3600, "test_secret");
|
||||
assertFalse(HmacCheckListener.verifyHostname(raw, "test_secret".getBytes(), 3600, 60));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hostnameVerifyRejectsFutureTimestamp() {
|
||||
long ts = System.currentTimeMillis() / 1000 + 3600;
|
||||
String raw = signHostname("play.example.com", ts, 3600, "test_secret");
|
||||
assertFalse(HmacCheckListener.verifyHostname(raw, "test_secret".getBytes(), 3600, 60));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hostnameVerifyAcceptsPreviousRotationBucket() {
|
||||
long ts = System.currentTimeMillis() / 1000;
|
||||
long bucket = ts / 3600 - 1;
|
||||
String raw = "play.example.com\0shield\0" + ts + "\0"
|
||||
+ sign("play.example.com", ts, bucket, "test_secret");
|
||||
assertTrue(HmacCheckListener.verifyHostname(raw, "test_secret".getBytes(), 3600, 60));
|
||||
}
|
||||
|
||||
private static String signHostname(String domain, long ts, long rotationSecs, String secret) {
|
||||
long bucket = ts / rotationSecs;
|
||||
return domain + "\0shield\0" + ts + "\0" + sign(domain, ts, bucket, secret);
|
||||
}
|
||||
|
||||
private static String sign(String domain, long ts, long bucket, String secret) {
|
||||
byte[] derived = hmacRaw(secret.getBytes(), ("rampart-key-" + bucket).getBytes());
|
||||
byte[] sig = hmacRaw(derived, (domain + "|" + ts).getBytes());
|
||||
return toHex(sig);
|
||||
}
|
||||
|
||||
private static byte[] hmacRaw(byte[] key, byte[] data) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||
return mac.doFinal(data);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String toHex(byte[] raw) {
|
||||
StringBuilder sb = new StringBuilder(raw.length * 2);
|
||||
for (byte b : raw) {
|
||||
sb.append(String.format("%02x", b & 0xFF));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// --- HMAC utility (mirrors HmacCheckListener) ---
|
||||
|
||||
private String hmacHex(String data, String secret) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue