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()) {