diff --git a/proxy/src/main/java/com/velocityctd/proxy/connection/profile/GameProfileFetcher.java b/proxy/src/main/java/com/velocityctd/proxy/connection/profile/GameProfileFetcher.java
deleted file mode 100644
index 0b3593f452..0000000000
--- a/proxy/src/main/java/com/velocityctd/proxy/connection/profile/GameProfileFetcher.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * 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 .
- */
-
-package com.velocityctd.proxy.connection.profile;
-
-import static com.google.common.net.UrlEscapers.urlFormParameterEscaper;
-import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON;
-
-import com.google.common.base.Stopwatch;
-import com.velocityctd.proxy.connection.profile.cache.GameProfileCacheStrategy;
-import com.velocitypowered.api.util.GameProfile;
-import com.velocitypowered.proxy.VelocityServer;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-public class GameProfileFetcher {
-
- private static final Logger LOGGER = LogManager.getLogger(GameProfileFetcher.class);
-
- /**
- * The base URL used to verify that a player has joined using Mojang's session server.
- */
- private static final String HASJOINED_BASE_URL =
- System.getProperty("mojang.sessionserver", "https://sessionserver.mojang.com/session/minecraft/hasJoined");
-
- /**
- * Used when {@link com.velocitypowered.proxy.config.VelocityConfiguration#shouldPreventClientProxyConnections()} is {@code false}.
- */
- private static final String HASJOINED_NO_IP_URL = HASJOINED_BASE_URL.concat("?username=%s&serverId=%s");
-
- /**
- * Used when {@link com.velocitypowered.proxy.config.VelocityConfiguration#shouldPreventClientProxyConnections()} is {@code true}.
- */
- private static final String HASJOINED_WITH_IP_URL = HASJOINED_BASE_URL.concat("?username=%s&serverId=%s&ip=%s");
-
- private final List cacheLayers = new ArrayList<>();
-
- private final VelocityServer server;
- private final HttpClient httpClient;
-
- public GameProfileFetcher(VelocityServer server) {
- this.server = server;
-
- httpClient = server.createHttpClient();
- }
-
- /**
- * Gets the mutable cache layer list. May be used to insert caching layers at specific tiers.
- * The cache layers are queried from bottom (index=0) to top (index=len-1). Faster caching
- * layers should be at the bottom of the list, slower layers should be at the top. This
- * may be controlled using {@link List#addFirst} and {@link List#addLast}
- *
- * @return the mutable cache layer list
- */
- public List getCacheLayers() {
- return cacheLayers;
- }
-
- public CompletableFuture fetchProfile(String playerIp, String username, String serverId) {
- return CompletableFuture.supplyAsync(() -> {
- for (int i = 0; i < cacheLayers.size(); i++) {
- var layer = cacheLayers.get(i);
- GameProfile cachedProfile = layer.findByUsername(username).orElse(null);
- if (cachedProfile != null) {
- // Insert to lower-tier cache layers
- for (int j = 0; j < i; j++) {
- cacheLayers.get(j).insert(cachedProfile);
- }
-
- LOGGER.debug("Fetched game profile from cache (hit from {})", layer.getClass().getSimpleName());
- return new GameProfileResponse(cachedProfile, GameProfileResponse.Status.SUCCESS_CACHED);
- }
- }
- return null;
- }).thenCompose(cachedResponse -> {
- if (cachedResponse != null) {
- return CompletableFuture.completedFuture(cachedResponse);
- }
-
- String url;
- if (server.getConfiguration().shouldPreventClientProxyConnections()) {
- url = String.format(HASJOINED_WITH_IP_URL,
- urlFormParameterEscaper().escape(username),
- urlFormParameterEscaper().escape(serverId),
- urlFormParameterEscaper().escape(playerIp));
- } else {
- url = String.format(HASJOINED_NO_IP_URL,
- urlFormParameterEscaper().escape(username),
- urlFormParameterEscaper().escape(serverId));
- }
-
- HttpRequest httpRequest = HttpRequest.newBuilder()
- .setHeader("User-Agent",
- server.getVersion().getName() + "/" + server.getVersion().getVersion())
- .uri(URI.create(url))
- .build();
-
- Stopwatch stopwatch = Stopwatch.createStarted(); // For debug logging
-
- return httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
- .handle((response, throwable) -> {
- if (throwable != null) {
- LOGGER.error("Unable to authenticate player", throwable);
- return new GameProfileResponse(null, GameProfileResponse.Status.ERROR_AUTH_DOWN);
- }
-
- stopwatch.stop();
- LOGGER.debug("Fetched game profile in {}.", stopwatch);
-
- if (response.statusCode() == 200) {
- GameProfile profile = GENERAL_GSON.fromJson(response.body(), GameProfile.class);
-
- // Insert profile into caches
- for (var layer : cacheLayers) {
- layer.insert(profile);
- }
-
- return new GameProfileResponse(profile, GameProfileResponse.Status.SUCCESS);
- } else if (response.statusCode() == 204) {
- // An offline-mode user logged onto this online-mode proxy.
- return new GameProfileResponse(null, GameProfileResponse.Status.ERROR_OFFLINE_USER);
- } else {
- // Something else went wrong
- LOGGER.error(
- "Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
- response.statusCode(), username, playerIp);
- return new GameProfileResponse(null, GameProfileResponse.Status.ERROR_AUTH_DOWN);
- }
- });
- });
- }
-}
diff --git a/proxy/src/main/java/com/velocityctd/proxy/connection/profile/GameProfileResponse.java b/proxy/src/main/java/com/velocityctd/proxy/connection/profile/GameProfileResponse.java
deleted file mode 100644
index e12750c250..0000000000
--- a/proxy/src/main/java/com/velocityctd/proxy/connection/profile/GameProfileResponse.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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 .
- */
-
-package com.velocityctd.proxy.connection.profile;
-
-import com.velocitypowered.api.util.GameProfile;
-import java.util.NoSuchElementException;
-import org.checkerframework.checker.nullness.qual.NonNull;
-import org.checkerframework.checker.nullness.qual.Nullable;
-
-public final class GameProfileResponse {
-
- @Nullable
- private final GameProfile gameProfile;
-
- @NonNull
- private final Status status;
-
- GameProfileResponse(@Nullable GameProfile gameProfile, @NonNull Status status) {
- if (status.success() && gameProfile == null) {
- throw new IllegalArgumentException("Expected a non-null GameProfile for a successful status.");
- }
- if (!status.success() && gameProfile != null) {
- throw new IllegalArgumentException("Expected a null GameProfile for a non-successful status.");
- }
-
- this.gameProfile = gameProfile;
- this.status = status;
- }
-
- @NonNull
- public GameProfile gameProfile() {
- if (gameProfile == null) {
- throw new NoSuchElementException("No game profile fetched.");
- }
-
- return gameProfile;
- }
-
- @NonNull
- public Status status() {
- return status;
- }
-
- public enum Status {
-
- SUCCESS,
- SUCCESS_CACHED,
- ERROR_OFFLINE_USER,
- ERROR_AUTH_DOWN;
-
- public boolean success() {
- return this == SUCCESS || this == SUCCESS_CACHED;
- }
- }
-}
diff --git a/proxy/src/main/java/com/velocityctd/proxy/connection/profile/cache/GameProfileCacheStrategy.java b/proxy/src/main/java/com/velocityctd/proxy/connection/profile/cache/GameProfileCacheStrategy.java
deleted file mode 100644
index 1d0ad3d5a7..0000000000
--- a/proxy/src/main/java/com/velocityctd/proxy/connection/profile/cache/GameProfileCacheStrategy.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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 .
- */
-
-package com.velocityctd.proxy.connection.profile.cache;
-
-import com.velocitypowered.api.util.GameProfile;
-import java.util.Optional;
-
-public interface GameProfileCacheStrategy {
-
- Optional findByUsername(String username);
-
- void insert(GameProfile profile);
-}
diff --git a/proxy/src/main/java/com/velocityctd/proxy/connection/profile/cache/MemoryGameProfileCache.java b/proxy/src/main/java/com/velocityctd/proxy/connection/profile/cache/MemoryGameProfileCache.java
deleted file mode 100644
index 1f59d5b064..0000000000
--- a/proxy/src/main/java/com/velocityctd/proxy/connection/profile/cache/MemoryGameProfileCache.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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 .
- */
-
-package com.velocityctd.proxy.connection.profile.cache;
-
-import com.github.benmanes.caffeine.cache.Cache;
-import com.github.benmanes.caffeine.cache.Caffeine;
-import com.velocitypowered.api.util.GameProfile;
-import java.time.Duration;
-import java.util.Optional;
-
-public class MemoryGameProfileCache implements GameProfileCacheStrategy {
-
- private final Cache cache;
-
- public MemoryGameProfileCache(Duration cacheExpiry, int maximumSize) {
- this.cache = Caffeine.newBuilder()
- .expireAfterWrite(cacheExpiry)
- .maximumSize(maximumSize)
- .build();
- }
-
- @Override
- public Optional findByUsername(String username) {
- return Optional.ofNullable(cache.getIfPresent(username));
- }
-
- @Override
- public void insert(GameProfile profile) {
- cache.put(profile.getName(), profile);
- }
-}
diff --git a/proxy/src/main/java/com/velocityctd/proxy/redis/profilecache/RedisGameProfileCache.java b/proxy/src/main/java/com/velocityctd/proxy/redis/profilecache/RedisGameProfileCache.java
deleted file mode 100644
index 250603b04f..0000000000
--- a/proxy/src/main/java/com/velocityctd/proxy/redis/profilecache/RedisGameProfileCache.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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 .
- */
-
-package com.velocityctd.proxy.redis.profilecache;
-
-import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON;
-
-import com.velocityctd.proxy.connection.profile.cache.GameProfileCacheStrategy;
-import com.velocityctd.proxy.redis.provider.RedisProvider;
-import com.velocitypowered.api.util.GameProfile;
-import java.time.Duration;
-import java.util.Optional;
-
-public class RedisGameProfileCache implements GameProfileCacheStrategy {
-
- private static final String KEY_PREFIX = "velocity:profile-cache:";
-
- private final RedisProvider provider;
- private final long ttlSeconds;
-
- public RedisGameProfileCache(RedisProvider provider, Duration cacheExpiry) {
- this.provider = provider;
- this.ttlSeconds = cacheExpiry.toSeconds();
- }
-
- @Override
- public Optional findByUsername(String username) {
- return Optional.ofNullable(provider.get(KEY_PREFIX + username))
- .map(json -> GENERAL_GSON.fromJson(json, GameProfile.class));
- }
-
- @Override
- public void insert(GameProfile profile) {
- String json = GENERAL_GSON.toJson(profile);
- provider.setWithExpiry(KEY_PREFIX + profile.getName(), json, ttlSeconds);
- }
-}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
index 5acfdb6dea..e8b7aa42f0 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
@@ -35,7 +35,6 @@
import com.velocityctd.proxy.command.builtin.QueueAdminCommand;
import com.velocityctd.proxy.command.builtin.SlashServerCommand;
import com.velocityctd.proxy.command.builtin.TransferCommand;
-import com.velocityctd.proxy.connection.profile.GameProfileFetcher;
import com.velocityctd.proxy.queue.RedisVelocityQueueManager;
import com.velocityctd.proxy.queue.VelocityQueueManager;
import com.velocityctd.proxy.redis.VelocityRedis;
@@ -333,11 +332,6 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
*/
private @MonotonicNonNull VelocityRedis redis;
- /**
- * The global {@link GameProfileFetcher} used by {@link com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler}.
- */
- private @MonotonicNonNull GameProfileFetcher gameProfileFetcher;
-
VelocityServer(final ProxyOptions options) {
pluginManager = new VelocityPluginManager(this);
eventManager = new VelocityEventManager(pluginManager);
@@ -384,10 +378,6 @@ public VelocityRedis getRedis() {
return redis;
}
- public @MonotonicNonNull GameProfileFetcher getGameProfileFetcher() {
- return gameProfileFetcher;
- }
-
@Override
public final VelocityConfiguration getConfiguration() {
return this.configuration;
@@ -524,8 +514,6 @@ void start() {
}
}
- gameProfileFetcher = new GameProfileFetcher(this);
-
if (configuration.getRedis().isEnabled()) {
redis = new VelocityRedis(this);
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java
index 6f4fd5fd9f..0756e446fe 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java
@@ -42,6 +42,7 @@
import io.netty.buffer.ByteBuf;
import java.net.InetAddress;
import java.net.InetSocketAddress;
+import java.net.http.HttpClient;
import java.util.Optional;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
@@ -74,6 +75,12 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
*/
private final VelocityServer server;
+ /**
+ * The {@link HttpClient} that will be passed to the {@link InitialLoginSessionHandler}.
+ * May be a shared instance.
+ */
+ private final HttpClient httpClient;
+
/**
* The configured minimum version string used to validate connecting clients.
*/
@@ -94,9 +101,12 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
* and event handling.
* @throws NullPointerException if either {@code connection} or {@code server} is {@code null}.
*/
- public HandshakeSessionHandler(final MinecraftConnection connection, final VelocityServer server) {
+ public HandshakeSessionHandler(final MinecraftConnection connection,
+ final VelocityServer server,
+ final HttpClient httpClient) {
this.connection = Preconditions.checkNotNull(connection, "connection");
this.server = Preconditions.checkNotNull(server, "server");
+ this.httpClient = Preconditions.checkNotNull(httpClient, "httpClient");
this.minimumVersion = server.getConfiguration().getMinimumVersion();
this.maximumVersion = server.getConfiguration().getMaximumVersion()
.orElse(ProtocolVersion.MAXIMUM_VERSION.getMostRecentSupportedVersion());
@@ -226,7 +236,8 @@ private void handleLogin(final HandshakePacket handshake, final InitialInboundCo
final LoginInboundConnection lic = new LoginInboundConnection(ic);
server.getEventManager().fireAndForget(new ConnectionHandshakeEvent(lic, handshake.getIntent()));
- connection.setActiveSessionHandler(StateRegistry.LOGIN, new InitialLoginSessionHandler(server, connection, lic));
+ connection.setActiveSessionHandler(StateRegistry.LOGIN,
+ new InitialLoginSessionHandler(server, connection, lic, httpClient));
}
private ConnectionType getHandshakeConnectionType(final HandshakePacket handshake) {
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java
index 1a60c71d16..e2435d02ac 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java
@@ -17,6 +17,8 @@
package com.velocitypowered.proxy.connection.client;
+import static com.google.common.net.UrlEscapers.urlFormParameterEscaper;
+import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON;
import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
import static com.velocitypowered.proxy.crypto.EncryptionUtils.decryptRsa;
import static com.velocitypowered.proxy.crypto.EncryptionUtils.generateServerId;
@@ -41,6 +43,10 @@
import com.velocitypowered.proxy.util.VelocityProperties;
import io.netty.buffer.ByteBuf;
import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.MessageDigest;
@@ -68,6 +74,14 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
*/
private static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current();
+ /**
+ * The URL used to verify that a player has joined using Mojang's session server.
+ */
+ private static final String MOJANG_HASJOINED_URL =
+ System.getProperty("mojang.sessionserver",
+ "https://sessionserver.mojang.com/session/minecraft/hasJoined")
+ .concat("?username=%s&serverId=%s");
+
/**
* The Velocity server instance.
*/
@@ -83,6 +97,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
*/
private final LoginInboundConnection inbound;
+ /**
+ * The {@link HttpClient} used to fetch {@link #MOJANG_HASJOINED_URL}.
+ * May be a shared instance.
+ */
+ private final HttpClient httpClient;
+
/**
* The login packet sent by the client. May be {@code null} if not yet received.
*/
@@ -104,12 +124,13 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
private final boolean forceKeyAuthentication;
InitialLoginSessionHandler(final VelocityServer server, final MinecraftConnection mcConnection,
- final LoginInboundConnection inbound) {
+ final LoginInboundConnection inbound, final HttpClient httpClient) {
this.server = Preconditions.checkNotNull(server, "server");
this.mcConnection = Preconditions.checkNotNull(mcConnection, "mcConnection");
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
+ this.httpClient = Preconditions.checkNotNull(httpClient, "httpClient");
this.forceKeyAuthentication = VelocityProperties.readBoolean(
- "auth.forceSecureProfiles", server.getConfiguration().isForceKeyAuthentication());
+ "auth.forceSecureProfiles", server.getConfiguration().isForceKeyAuthentication());
}
/**
@@ -263,41 +284,57 @@ public boolean handle(final EncryptionResponsePacket packet) {
String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString();
+ String url = String.format(MOJANG_HASJOINED_URL, urlFormParameterEscaper().escape(login.getUsername()), serverId);
- server.getGameProfileFetcher().fetchProfile(playerIp, login.getUsername(), serverId)
- .whenCompleteAsync((response, throwable) -> {
- if (mcConnection.isClosed()) {
- // The player disconnected after we authenticated them.
- return;
- }
-
- if (throwable != null || !response.status().success()) {
- if (throwable != null) {
- LOGGER.error("Exception while fetching profile", throwable);
- }
-
- switch (response.status()) {
- case ERROR_AUTH_DOWN -> inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
- case ERROR_OFFLINE_USER -> inbound.disconnect(Component.translatable("velocity.error.online-mode-only", NamedTextColor.RED));
- default -> throw new IllegalStateException("Unhandled error '" + response.status() + "'.");
- }
- return;
- }
-
- GameProfile profile = response.gameProfile();
+ if (server.getConfiguration().shouldPreventClientProxyConnections()) {
+ url += "&ip=" + urlFormParameterEscaper().escape(playerIp);
+ }
- // Not so fast, now we verify the public key for 1.19.1+
- if (inbound.getIdentifiedKey() != null
- && inbound.getIdentifiedKey().getKeyRevision() == IdentifiedKey.Revision.LINKED_V2
- && inbound.getIdentifiedKey() instanceof final IdentifiedKeyImpl key) {
- if (!key.internalAddHolder(profile.getId())) {
- inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_public_key"));
- }
+ final HttpRequest httpRequest = HttpRequest.newBuilder()
+ .setHeader("User-Agent",
+ server.getVersion().getName() + "/" + server.getVersion().getVersion())
+ .uri(URI.create(url))
+ .build();
+
+ httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
+ .whenCompleteAsync((response, throwable) -> {
+ if (mcConnection.isClosed()) {
+ // The player disconnected after we authenticated them.
+ return;
+ }
+
+ if (throwable != null) {
+ LOGGER.error("Unable to authenticate player", throwable);
+ inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
+ return;
+ }
+
+ if (response.statusCode() == 200) {
+ final GameProfile profile = GENERAL_GSON.fromJson(response.body(), GameProfile.class);
+
+ // Not so fast, now we verify the public key for 1.19.1+
+ if (inbound.getIdentifiedKey() != null
+ && inbound.getIdentifiedKey().getKeyRevision() == IdentifiedKey.Revision.LINKED_V2
+ && inbound.getIdentifiedKey() instanceof final IdentifiedKeyImpl key) {
+ if (!key.internalAddHolder(profile.getId())) {
+ inbound.disconnect(
+ Component.translatable("multiplayer.disconnect.invalid_public_key"));
}
-
- // All went well, initialize the session.
- mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, new AuthSessionHandler(server, inbound, profile, true, serverId));
- }, mcConnection.eventLoop());
+ }
+ // All went well, initialize the session.
+ mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, new AuthSessionHandler(server, inbound, profile, true, serverId));
+ } else if (response.statusCode() == 204) {
+ // Apparently, an offline-mode user logged onto this online-mode proxy.
+ inbound.disconnect(
+ Component.translatable("velocity.error.online-mode-only", NamedTextColor.RED));
+ } else {
+ // Something else went wrong
+ LOGGER.error(
+ "Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
+ response.statusCode(), login.getUsername(), playerIp);
+ inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
+ }
+ }, mcConnection.eventLoop());
} catch (GeneralSecurityException e) {
LOGGER.error("Unable to enable encryption", e);
mcConnection.close(true);
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java
index c15962c8a5..c28b578bee 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java
@@ -40,7 +40,10 @@
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
import io.netty.handler.timeout.ReadTimeoutHandler;
+import java.net.http.HttpClient;
import java.util.concurrent.TimeUnit;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Server channel initializer.
@@ -52,6 +55,11 @@ public class ServerChannelInitializer extends ChannelInitializer {
*/
private final VelocityServer server;
+ /**
+ * The shared {@link HttpClient} amongst all {@link HandshakeSessionHandler}s this class creates.
+ */
+ private volatile @MonotonicNonNull HttpClient httpClient;
+
/**
* Constructs a new {@link ServerChannelInitializer}.
*
@@ -82,7 +90,16 @@ public ServerChannelInitializer(final VelocityServer server) {
* @param ch the Netty channel to initialize
*/
@Override
+ @EnsuresNonNull("httpClient")
protected void initChannel(final Channel ch) {
+ if (this.httpClient == null) {
+ synchronized (this) {
+ if (this.httpClient == null) {
+ this.httpClient = server.createHttpClient();
+ }
+ }
+ }
+
ch.pipeline()
.addLast(LEGACY_PING_DECODER, new LegacyPingDecoder())
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder(ProtocolUtils.Direction.SERVERBOUND))
@@ -93,7 +110,8 @@ protected void initChannel(final Channel ch) {
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolUtils.Direction.CLIENTBOUND));
final MinecraftConnection connection = new MinecraftConnection(ch, this.server);
- connection.setActiveSessionHandler(StateRegistry.HANDSHAKE, new HandshakeSessionHandler(connection, this.server));
+ connection.setActiveSessionHandler(StateRegistry.HANDSHAKE,
+ new HandshakeSessionHandler(connection, this.server, this.httpClient));
ch.pipeline().addLast(Connections.HANDLER, connection);
if (this.server.getConfiguration().isProxyProtocol()) {