Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
import com.velocitypowered.proxy.tablist.KeyedVelocityTabList;
import com.velocitypowered.proxy.tablist.VelocityTabList;
import com.velocitypowered.proxy.tablist.VelocityTabListLegacy;
import com.velocitypowered.proxy.util.AddressUtil;
import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
import com.velocitypowered.proxy.util.DurationUtils;
import com.velocitypowered.proxy.util.TranslatableMapper;
Expand Down Expand Up @@ -890,8 +891,7 @@ private Optional<RegisteredServer> getNextServerToTry(@Nullable RegisteredServer
String virtualHostStr = getVirtualHost().map(InetSocketAddress::getHostString)
.orElse("")
.toLowerCase(Locale.ROOT);
serversToTry = server.getConfiguration().getForcedHosts().getOrDefault(virtualHostStr,
Collections.emptyList());
serversToTry = AddressUtil.resolveForcedHostServers(server, virtualHostStr).orElseGet(Collections::emptyList);
}

if (serversToTry.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.velocitypowered.proxy.config.PingPassthroughMode;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
import com.velocitypowered.proxy.util.AddressUtil;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -174,8 +175,8 @@ public CompletableFuture<ServerPing> getInitialPing(VelocityInboundConnection co
String virtualHostStr = connection.getVirtualHost().map(InetSocketAddress::getHostString)
.map(str -> str.toLowerCase(Locale.ROOT))
.orElse("");
List<String> serversToTry = server.getConfiguration().getForcedHosts().getOrDefault(
virtualHostStr, server.getConfiguration().getAttemptConnectionOrder());
List<String> serversToTry = AddressUtil.resolveForcedHostServers(server, virtualHostStr)
.orElseGet(() -> server.getConfiguration().getAttemptConnectionOrder());
Comment thread
lllincoln marked this conversation as resolved.
Outdated
return attemptPingPassthrough(connection, passthroughMode, serversToTry, shownVersion, virtualHostStr);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@

import com.google.common.base.Preconditions;
import com.google.common.net.InetAddresses;
import com.velocitypowered.proxy.VelocityServer;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;

/**
* Utilities to parse addresses.
Expand Down Expand Up @@ -74,4 +79,69 @@ public static InetSocketAddress parseAndResolveAddress(String ip) {
int port = uri.getPort() == -1 ? DEFAULT_MINECRAFT_PORT : uri.getPort();
return new InetSocketAddress(uri.getHost(), port);
}

/**
* Tests whether a host matches a pattern whose labels may be the
* wildcard {@code "*"}. Each {@code "*"} matches exactly one label; all other
* labels match case-insensitively.
*
* @param pattern the pattern to match against, for example, {@code *.example.com}
* @param host the virtual host to test, for example, {@code play.example.com}
* @return true if the host matches the pattern, false otherwise
*/
public static boolean isHostMatchingPattern(@Nullable String pattern, @Nullable String host) {
if (host == null || pattern == null) {
return false;
}

String[] patternDomains = pattern.split("\\.");
String[] strDomains = host.split("\\.");

if (patternDomains.length != strDomains.length) {
return false;
}

for (int i = 0; i < patternDomains.length; i++) {
String patternDomain = patternDomains[i];
String strDomain = strDomains[i];

if (!patternDomain.equals("*") && !strDomain.equalsIgnoreCase(patternDomain)) {
return false;
}
}

return true;
}

/**
* Resolves the list of servers configured for a given virtual host via forced hosts. An exact
* match on the virtual host is preferred. Then, if none exist, the configured forced host patterns are
* checked in turn and the first matching pattern's servers are returned.
*
* @param server the proxy server providing the forced host configuration
* @param virtualHostStr the virtual host the client connected with
* @return the servers for the matching forced host, or {@link Optional#empty()} if none match
*/
public static Optional<List<String>> resolveForcedHostServers(VelocityServer server, String virtualHostStr) {
Map<String, List<String>> forcedHosts = server.getConfiguration().getForcedHosts();

// Check for exact match
List<String> exactMatch = forcedHosts.get(virtualHostStr);

if (exactMatch != null) {
return Optional.of(exactMatch);
}

// Check for pattern match
for (Map.Entry<String, List<String>> entry : forcedHosts.entrySet()) {
String virtualHostPattern = entry.getKey();

if (AddressUtil.isHostMatchingPattern(virtualHostPattern, virtualHostStr)) {
return Optional.of(entry.getValue());
}
}

// No match
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2018-2026 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.velocitypowered.proxy.protocol.util;

import static com.velocitypowered.proxy.util.AddressUtil.isHostMatchingPattern;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

class AddressUtilTest {
@Test
void testOneWildcardMatches() {
assertTrue(isHostMatchingPattern("*.example.com", "play.example.com"));
assertTrue(isHostMatchingPattern("*.example.com", "a.example.com"));
assertTrue(isHostMatchingPattern("b.*.example.com", "b.a.example.com"));
}

@Test
void testMultipleWildcardsMatch() {
assertTrue(isHostMatchingPattern("*.*.example.com", "a.b.example.com"));
}

@Test
void testDifferentNumberOfLabelsDoNotMatch() {
assertFalse(isHostMatchingPattern("*.example.com", "a.b.example.com"));
}

@Test
void testWildcardsDoNotMatchApexDomain() {
assertFalse(isHostMatchingPattern("*.example.com", "example.com"));
}

@Test
void testExactMatchesMatch() {
assertTrue(isHostMatchingPattern("example.com", "example.com"));
assertTrue(isHostMatchingPattern("play.example.com", "play.example.com"));
}

@Test
void testDifferentDomainsDoNotMatch() {
assertFalse(isHostMatchingPattern("otherdomain.com", "example.com"));
assertFalse(isHostMatchingPattern("a.otherdomain.com", "a.example.com"));
}

@Test
void testMalformedArgumentsDoNotMatch() {
assertFalse(isHostMatchingPattern(null, "example.com"));
assertFalse(isHostMatchingPattern("example.com", null));
assertFalse(isHostMatchingPattern(null, null));
}

@Test
void testCaseInsensitivityMatches() {
assertTrue(isHostMatchingPattern("Example.COM", "example.com"));
assertTrue(isHostMatchingPattern("*.Example.com", "play.EXAMPLE.com"));
}
}