Skip to content

Commit

Permalink
Reduce session server lookups (#509)
Browse files Browse the repository at this point in the history
* fix: add default skin to gameprofiles

* fix: add signatures by default to prevent issues

* cleanup

* no longer apply empty textures

* revert formatting change

* fix(spigot): linked player textures

* fix(velocity): linked player textures

* fix(bungeecord): apply linked textures

* Made the MojangUtils class instance based, removed some unneeded code

* Don't block Velocity event threads, made the Bungee variant work

* Add some comments

---------

Co-authored-by: bridge <[email protected]>
Co-authored-by: Tim203 <[email protected]>
  • Loading branch information
3 people authored May 18, 2024
1 parent f8c8418 commit 00b8b1b
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,18 @@
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import net.md_5.bungee.netty.ChannelWrapper;
import org.geysermc.floodgate.api.ProxyFloodgateApi;
import org.geysermc.floodgate.api.logger.FloodgateLogger;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
import org.geysermc.floodgate.config.ProxyFloodgateConfig;
import org.geysermc.floodgate.skin.SkinApplier;
import org.geysermc.floodgate.skin.SkinDataImpl;
import org.geysermc.floodgate.util.LanguageManager;
import org.geysermc.floodgate.util.MojangUtils;
import org.geysermc.floodgate.util.ReflectionUtils;

@SuppressWarnings("ConstantConditions")
Expand All @@ -66,7 +67,7 @@ public final class BungeeListener implements Listener {
checkNotNull(PLAYER_NAME, "Initial name field cannot be null");
}

@Inject private ProxyFloodgateConfig config;
@Inject private Plugin plugin;
@Inject private ProxyFloodgateApi api;
@Inject private LanguageManager languageManager;
@Inject private FloodgateLogger logger;
Expand All @@ -80,6 +81,8 @@ public final class BungeeListener implements Listener {
@Named("kickMessageAttribute")
private AttributeKey<String> kickMessageAttribute;

@Inject private MojangUtils mojangUtils;

@EventHandler(priority = EventPriority.LOWEST)
public void onPreLogin(PreLoginEvent event) {
// well, no reason to check if the player will be kicked anyway
Expand Down Expand Up @@ -127,13 +130,28 @@ public void onLogin(LoginEvent event) {

@EventHandler(priority = EventPriority.LOWEST)
public void onPostLogin(PostLoginEvent event) {
// To fix the February 2 2022 Mojang authentication changes
if (!config.isSendFloodgateData()) {
FloodgatePlayer player = api.getPlayer(event.getPlayer().getUniqueId());
if (player != null && !player.isLinked()) {
skinApplier.applySkin(player, new SkinDataImpl("", ""));
}
FloodgatePlayer player = api.getPlayer(event.getPlayer().getUniqueId());

// Skin look up (on Spigot and friends) would result in it failing, so apply a default skin
if (!player.isLinked()) {
skinApplier.applySkin(player, SkinDataImpl.DEFAULT_SKIN);
return;
}

// Floodgate players are seen as offline mode players, meaning we have to look up
// the linked player's textures ourselves

event.registerIntent(plugin);

mojangUtils.skinFor(player.getJavaUniqueId())
.exceptionally(exception -> {
logger.debug("Unexpected skin fetch error for " + player.getJavaUniqueId(), exception);
return SkinDataImpl.DEFAULT_SKIN;
})
.thenAccept(skin -> {
skinApplier.applySkin(player, skin);
event.completeIntent(plugin);
});
}

@EventHandler(priority = EventPriority.HIGHEST)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ public void applySkin(@NonNull FloodgatePlayer floodgatePlayer, @NonNull SkinDat
SkinData currentSkin = currentSkin(properties);

SkinApplyEvent event = new SkinApplyEventImpl(floodgatePlayer, currentSkin, skinData);
event.setCancelled(floodgatePlayer.isLinked());

eventBus.fire(event);

if (event.isCancelled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@
import java.util.Objects;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData;
import org.geysermc.floodgate.util.Constants;

public class SkinDataImpl implements SkinData {
public static final SkinData DEFAULT_SKIN = new SkinDataImpl(
Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE,
Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE
);

private final String value;
private final String signature;

Expand Down
110 changes: 110 additions & 0 deletions core/src/main/java/org/geysermc/floodgate/util/MojangUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Floodgate
*/

package org.geysermc.floodgate.util;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import lombok.NonNull;
import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData;
import org.geysermc.floodgate.skin.SkinDataImpl;
import org.geysermc.floodgate.util.HttpClient.HttpResponse;

@Singleton
public class MojangUtils {
private final Cache<UUID, SkinData> SKIN_CACHE = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(500)
.build();

@Inject private HttpClient httpClient;
@Inject
@Named("commonPool")
private ExecutorService commonPool;

public CompletableFuture<@NonNull SkinData> skinFor(UUID playerId) {
return CompletableFuture.supplyAsync(() -> {
try {
return SKIN_CACHE.get(playerId, () -> fetchSkinFor(playerId));
} catch (ExecutionException exception) {
throw new RuntimeException(exception.getCause());
}
}, commonPool);
}

private @NonNull SkinData fetchSkinFor(UUID playerId) {
HttpResponse<JsonObject> httpResponse = httpClient.get(
String.format(Constants.PROFILE_WITH_PROPERTIES_URL, playerId.toString()));

if (httpResponse.getHttpCode() != 200) {
return SkinDataImpl.DEFAULT_SKIN;
}

JsonObject response = httpResponse.getResponse();

if (response == null) {
return SkinDataImpl.DEFAULT_SKIN;
}

JsonArray properties = response.getAsJsonArray("properties");

if (properties.size() == 0) {
return SkinDataImpl.DEFAULT_SKIN;
}

for (JsonElement property : properties) {
if (!property.isJsonObject()) {
continue;
}

JsonObject propertyObject = property.getAsJsonObject();

if (!propertyObject.has("name")
|| !propertyObject.has("value")
|| !propertyObject.has("signature")
|| !propertyObject.get("name").getAsString().equals("textures")) {
continue;
}

return new SkinDataImpl(
propertyObject.get("value").getAsString(),
propertyObject.get("signature").getAsString()
);
}

return SkinDataImpl.DEFAULT_SKIN;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ public final class Constants {
public static final String LATEST_VERSION_URL =
"https://download.geysermc.org/v2/projects/%s/versions/latest/builds/latest";

public static final String PROFILE_WITH_PROPERTIES_URL =
"https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false";


public static final String NTP_SERVER = "time.cloudflare.com";
public static final String INTERNAL_ERROR_MESSAGE =
Expand All @@ -70,4 +73,7 @@ public final class Constants {
public static final int HANDSHAKE_PACKET_ID = 0;
public static final int LOGIN_SUCCESS_PACKET_ID = 2;
public static final int SET_COMPRESSION_PACKET_ID = 3;

public static final String DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE = "ewogICJ0aW1lc3RhbXAiIDogMTcxNTcxNzM1NTI2MywKICAicHJvZmlsZUlkIiA6ICIyMWUzNjdkNzI1Y2Y0ZTNiYjI2OTJjNGEzMDBhNGRlYiIsCiAgInByb2ZpbGVOYW1lIiA6ICJHZXlzZXJNQyIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS8zMWY0NzdlYjFhN2JlZWU2MzFjMmNhNjRkMDZmOGY2OGZhOTNhMzM4NmQwNDQ1MmFiMjdmNDNhY2RmMWI2MGNiIgogICAgfQogIH0KfQ";
public static final String DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE = "dFKIZ5d6vNqCSe1IFGiVLjt3cnW8qh4qNP2umg9zqkX9bvAQawuR1iuO1kCD/+ye8A6GQFv2wRCdxdrjp5+Vrr0SsWqMnsYDN8cEg6CD18mAnaKI1TYDuGbdJaqLyGqN5wqSMdHxchs9iovFkde5ir4aYdvHkA11vOTi11L4kUzETGzJ4iKVuZOv4dq+B7wFAWqp4n8QZfhixyvemFazQHlLmxnuhU+jhpZMvYY9MAaRAJonfy/wJe9LymbTe0EJ8N+NwZQDrEUzgfBFo4OIGDqRZwvydInCqkjhPMtHCSL25VOKwcFocYpRYbk4eIKM4CLjYlBiQGki+XKsPaljwjVhnT0jUupSf7yraGb3T0CsVBjhDbIIIp9nytlbO0GvxHu0TzYjkr4Iji0do5jlCKQ/OasXcL21wd6ozw0t1QZnnzxi9ewSuyYVY9ErmWdkww1OtCIgJilceEBwNAB8+mhJ062WFaYPgJQAmOREM8InW33dbbeENMFhQi4LIO5P7p9ye3B4Lrwm20xtd9wJk3lewzcs8ezh0LUF6jPSDQDivgSKU49mLCTmOi+WZh8zKjjxfVEtNZON2W+3nct0LiWBVsQ55HzlvF0FFxuRVm6pxi6MQK2ernv3DQl0hUqyQ1+RV9nfZXTQOAUzwLjKx3t2zKqyZIiNEKLE+iAXrsE=";
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import static org.geysermc.floodgate.util.ReflectionUtils.setValue;

import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import io.netty.channel.Channel;
import io.netty.util.AttributeKey;
import java.lang.reflect.InvocationTargetException;
Expand All @@ -38,9 +39,16 @@
import org.geysermc.floodgate.player.FloodgateHandshakeHandler;
import org.geysermc.floodgate.player.FloodgateHandshakeHandler.HandshakeResult;
import org.geysermc.floodgate.util.ClassNames;
import org.geysermc.floodgate.util.Constants;
import org.geysermc.floodgate.util.ProxyUtils;

public final class SpigotDataHandler extends CommonDataHandler {
private static final Property DEFAULT_TEXTURE_PROPERTY = new Property(
"textures",
Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE,
Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE
);

private Object networkManager;
private FloodgatePlayer player;
private boolean proxyData;
Expand Down Expand Up @@ -171,6 +179,13 @@ private boolean checkAndHandleLogin(Object packet) throws Exception {
player.getCorrectUniqueId(), player.getCorrectUsername()
);

if (!player.isLinked()) {
// Otherwise game server will try to fetch the skin from Mojang.
// No need to worry that this overrides proxy data, because those won't reach this
// method / are already removed (in the case of username validation)
gameProfile.getProperties().put("textures", DEFAULT_TEXTURE_PROPERTY);
}

// we have to fake the offline player (login) cycle

if (ClassNames.IS_PRE_1_20_2) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,24 @@
package org.geysermc.floodgate.listener;

import com.destroystokyo.paper.event.profile.PreFillProfileEvent;
import com.destroystokyo.paper.profile.PlayerProfile;
import com.destroystokyo.paper.profile.ProfileProperty;
import com.google.inject.Inject;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.geysermc.floodgate.api.SimpleFloodgateApi;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
import org.geysermc.floodgate.util.Constants;

public final class PaperProfileListener implements Listener {
private static final ProfileProperty DEFAULT_TEXTURE_PROPERTY = new ProfileProperty(
"textures",
Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE,
Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE
);

@Inject private SimpleFloodgateApi api;

@EventHandler
Expand All @@ -62,26 +66,8 @@ public void onFill(PreFillProfileEvent event) {
}

Set<ProfileProperty> properties = new HashSet<>(event.getPlayerProfile().getProperties());
properties.add(new ProfileProperty("textures", "", ""));
event.setProperties(properties);
}

@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player bukkitPlayer = event.getPlayer();
FloodgatePlayer player = api.getPlayer(bukkitPlayer.getUniqueId());
if (player == null || player.isLinked()) {
return;
}

PlayerProfile profile = bukkitPlayer.getPlayerProfile();
if (profile.getProperties().stream().noneMatch(
prop -> "textures".equals(prop.getName()) && prop.getValue().isEmpty()
&& prop.getSignature() != null && prop.getSignature().isEmpty())) {
return;
}
properties.add(DEFAULT_TEXTURE_PROPERTY);

profile.removeProperty("textures");
bukkitPlayer.setPlayerProfile(profile);
event.setProperties(properties);
}
}
Loading

0 comments on commit 00b8b1b

Please sign in to comment.