Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
0.25.0 - RegEx name validation

- Add RegEx name validation
- Allow to retrieve the plugin version from LibreLoginProvider
- Allow to retrieve the plugin version from LibreLoginProvider
- Skip the login flow for players already authenticated by Minekube Connect
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package xyz.kyngs.librelogin.bungeecord;

import io.netty.channel.Channel;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
Expand All @@ -15,6 +16,7 @@
import net.md_5.bungee.event.EventPriority;
import xyz.kyngs.librelogin.api.event.exception.EventCancelledException;
import xyz.kyngs.librelogin.common.config.ConfigurationKeys;
import xyz.kyngs.librelogin.common.integration.ConnectIntegration;
import xyz.kyngs.librelogin.common.listener.AuthenticListeners;
import xyz.kyngs.librelogin.common.util.GeneralUtil;

Expand Down Expand Up @@ -56,6 +58,12 @@ public void onDisconnect(PlayerDisconnectEvent event) {
public void onPreLogin(PreLoginEvent event) {
if (plugin.fromFloodgate(event.getConnection().getUniqueId())) return;

// Minekube Connect authenticated the player at its own edge, there is no Mojang session left
// for this proxy to verify. Setting online mode would make the proxy send an encryption
// request that can never be answered, so the login flow has to be skipped, just like it is
// for Floodgate players above.
if (fromConnect(event.getConnection())) return;

runAsyncEvent(event, () -> {
var result = onPreLogin(event.getConnection().getName(), event.getConnection().getAddress().getAddress());

Expand Down Expand Up @@ -93,10 +101,51 @@ private void setField(PendingConnection connection, String fieldName, Object val
}
}

/**
* Reads the connection channel out of the {@link PendingConnection} implementation, so that the
* attributes other systems put on it can be looked at. BungeeCord exposes neither the field nor
* its type through its API, hence the reflection.
*
* @return the channel, or null if it could not be read
*/
private Channel getChannel(PendingConnection connection) {
try {
Field field = connection.getClass().getDeclaredField("ch");
field.setAccessible(true);

Object wrapper = field.get(connection);
if (wrapper == null) return null;

return (Channel) wrapper.getClass().getMethod("getHandle").invoke(wrapper);
} catch (ReflectiveOperationException | ClassCastException e) {
plugin.getLogger().debug("Failed to read the channel of a pending connection.", e);
return null;
}
}

/**
* Checks whether the connection was already authenticated by Minekube Connect, which marks such
* connections with the {@code connect-player} channel attribute before any login event fires.
* <p>
* This fails open: if the channel cannot be read the connection is treated as if Connect was not
* installed, which is exactly what happened before this check existed.
*/
private boolean fromConnect(PendingConnection connection) {
return ConnectIntegration.isConnectChannel(getChannel(connection));
}

@EventHandler(priority = EventPriority.LOWEST)
public void onProfileRequest(LoginEvent event) {
if (plugin.fromFloodgate(event.getConnection().getUniqueId())) return;

if (fromConnect(event.getConnection())) {
// Connect has already put the player's real uuid on the connection, rewriting it here
// would replace it with LibreLogin's own. Remember the uuid, as the channel is no longer
// reachable once the player is online.
plugin.getConnectIntegration().addPlayer(event.getConnection().getUniqueId());
return;
}

// Note to future self: NEVER EVER RUN THIS ASYNC, IT WILL BREAK PLUGINS

var profile = plugin.getDatabaseProvider().getByName(event.getConnection().getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import xyz.kyngs.librelogin.common.database.provider.LibreLoginSQLiteDatabaseProvider;
import xyz.kyngs.librelogin.common.event.AuthenticEventProvider;
import xyz.kyngs.librelogin.common.image.AuthenticImageProjector;
import xyz.kyngs.librelogin.common.integration.ConnectIntegration;
import xyz.kyngs.librelogin.common.integration.FloodgateIntegration;
import xyz.kyngs.librelogin.common.integration.luckperms.LuckPermsIntegration;
import xyz.kyngs.librelogin.common.listener.LoginTryListener;
Expand Down Expand Up @@ -98,6 +99,7 @@ public abstract class AuthenticLibreLogin<P, S> implements LibreLoginPlugin<P, S
private final Multimap<P, CancellableTask> cancelOnExit;
private final PlatformHandle<P, S> platformHandle;
private final Set<String> forbiddenPasswords;
private final ConnectIntegration connectApi;
protected Logger logger;
private AuthenticPremiumProvider premiumProvider;
private AuthenticEventProvider<P, S> eventProvider;
Expand All @@ -123,6 +125,7 @@ protected AuthenticLibreLogin() {
platformHandle = providePlatformHandle();
forbiddenPasswords = new HashSet<>();
cancelOnExit = HashMultimap.create();
connectApi = new ConnectIntegration();
}

public Map<Class<?>, DatabaseConnectorRegistration<?, ?>> getDatabaseConnectors() {
Expand Down Expand Up @@ -814,6 +817,26 @@ public boolean fromFloodgate(UUID uuid) {
return floodgateApi != null && uuid != null && floodgateApi.isFloodgateId(uuid);
}

public ConnectIntegration getConnectIntegration() {
return connectApi;
}

public boolean fromConnect(UUID uuid) {
return connectApi.isConnectId(uuid);
}

/**
* Whether the player has already been authenticated before the connection reached this proxy,
* either by Floodgate or by Minekube Connect. LibreLogin must not run its own login flow for
* such players.
*
* @param uuid the player's uuid
* @return whether the player has been authenticated externally
*/
public boolean externallyAuthenticated(UUID uuid) {
return fromFloodgate(uuid) || fromConnect(uuid);
}

protected void shutdownProxy(int code) {
//noinspection finally
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package xyz.kyngs.librelogin.common.integration;

import io.netty.channel.Channel;
import io.netty.util.AttributeKey;

import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
* Recognizes players that have already been authenticated by Minekube Connect.
* <p>
* Connect terminates the player's connection at its own edge, performs the Mojang handshake there,
* and then relays the player into the proxy over a local channel which it marks with the
* {@code connect-player} attribute. There is no second Mojang session left for the proxy to verify,
* so forcing online mode on such a connection makes the proxy send an encryption request that can
* never be answered and the login never completes.
* <p>
* Reading the marker needs no compile time dependency on Connect: Netty interns attribute keys by
* name, so the attribute is simply absent when Connect is not installed. This is the same approach
* the listeners already use for Floodgate's {@code floodgate-player} attribute.
* <p>
* The channel is only reachable while the player is logging in, so the UUIDs recognized during
* login are remembered here and dropped again when the player disconnects. That gives the rest of
* the plugin a UUID based check with the same shape as {@link FloodgateIntegration#isFloodgateId}.
*/
public class ConnectIntegration {

private static final AttributeKey<?> CONNECT_ATTR = AttributeKey.valueOf("connect-player");

private final Set<UUID> players = ConcurrentHashMap.newKeySet();

/**
* Checks whether the given connection channel has been marked by Connect.
*
* @param channel the connection channel, may be null if it could not be read
* @return whether the player behind the channel has already been authenticated by Connect
*/
public static boolean isConnectChannel(Channel channel) {
return channel != null && channel.hasAttr(CONNECT_ATTR) && channel.attr(CONNECT_ATTR).get() != null;
}

public void addPlayer(UUID uuid) {
if (uuid != null) players.add(uuid);
}

public void removePlayer(UUID uuid) {
if (uuid != null) players.remove(uuid);
}

public boolean isConnectId(UUID uuid) {
return uuid != null && players.contains(uuid);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public AuthenticListeners(Plugin plugin) {
protected void onPostLogin(P player, User user) {
var ip = platformHandle.getIP(player);
var uuid = platformHandle.getUUIDForPlayer(player);
if (plugin.fromFloodgate(uuid)) return;
if (plugin.externallyAuthenticated(uuid)) return;

if (user == null) {
user = plugin.getDatabaseProvider().getByUUID(uuid);
Expand All @@ -63,6 +63,7 @@ protected void onPostLogin(P player, User user) {
}

protected void onPlayerDisconnect(P player) {
plugin.getConnectIntegration().removePlayer(platformHandle.getUUIDForPlayer(player));
plugin.onExit(player);
plugin.getAuthorizationProvider().onExit(player);
}
Expand Down Expand Up @@ -268,11 +269,11 @@ private User checkAndValidateByName(String username, @Nullable PremiumUser premi

protected BiHolder<Boolean, S> chooseServer(P player, @Nullable String ip, @Nullable User user) {
var id = platformHandle.getUUIDForPlayer(player);
var fromFloodgate = plugin.fromFloodgate(id);
var externallyAuthenticated = plugin.externallyAuthenticated(id);

var sessionTime = Duration.ofSeconds(plugin.getConfiguration().get(ConfigurationKeys.SESSION_TIMEOUT));

if (fromFloodgate) {
if (externallyAuthenticated) {
user = null;
} else if (user == null) {
user = plugin.getDatabaseProvider().getByUUID(id);
Expand All @@ -282,7 +283,7 @@ protected BiHolder<Boolean, S> chooseServer(P player, @Nullable String ip, @Null
ip = platformHandle.getIP(player);
}

if (fromFloodgate || user.autoLoginEnabled() || (sessionTime != null && user.getLastAuthentication() != null && ip.equals(user.getIp()) && user.getLastAuthentication().toLocalDateTime().plus(sessionTime).isAfter(LocalDateTime.now()))) {
if (externallyAuthenticated || user.autoLoginEnabled() || (sessionTime != null && user.getLastAuthentication() != null && ip.equals(user.getIp()) && user.getLastAuthentication().toLocalDateTime().plus(sessionTime).isAfter(LocalDateTime.now()))) {
return new BiHolder<>(true, plugin.getServerHandler().chooseLobbyServer(user, player, true, false));
} else {
return new BiHolder<>(false, plugin.getServerHandler().chooseLimboServer(user, player));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import net.kyori.adventure.text.Component;
import xyz.kyngs.librelogin.api.event.exception.EventCancelledException;
import xyz.kyngs.librelogin.common.config.ConfigurationKeys;
import xyz.kyngs.librelogin.common.integration.ConnectIntegration;
import xyz.kyngs.librelogin.common.listener.AuthenticListeners;
import xyz.kyngs.librelogin.common.util.GeneralUtil;

Expand Down Expand Up @@ -78,6 +79,33 @@ public VelocityListeners(VelocityLibreLogin plugin) {
super(plugin);
}

private static Channel getChannel(InboundConnection connection) throws IllegalAccessException {
if (INITIAL_CONNECTION_DELEGATE != null) {
connection = (InboundConnection) INITIAL_CONNECTION_DELEGATE.get(connection);
}

Object mcConnection = INITIAL_MINECRAFT_CONNECTION.get(connection);

return (Channel) CHANNEL.get(mcConnection);
}

/**
* Checks whether the connection was already authenticated by Minekube Connect, which marks such
* connections with the {@code connect-player} channel attribute before any login event fires.
* <p>
* Unlike the Floodgate check this one fails open: if the channel cannot be read we simply
* proceed as if Connect was not installed, which is exactly what happened before this check
* existed.
*/
private boolean fromConnect(InboundConnection connection) {
try {
return ConnectIntegration.isConnectChannel(getChannel(connection));
} catch (Exception e) {
plugin.getLogger().debug("Failed to check if player is coming from Connect.", e);
return false;
}
}

@Subscribe(order = PostOrder.LAST)
public void onPostLogin(PostLoginEvent event) {
onPostLogin(event.getPlayer(), null);
Expand All @@ -94,6 +122,14 @@ public void onProfileRequest(GameProfileRequestEvent event) {

if (existing != null && plugin.fromFloodgate(existing.getId())) return;

if (existing != null && fromConnect(event.getConnection())) {
// Connect has already put the player's real uuid and skin properties into the profile,
// rebuilding it from the original one would throw both away. Remember the uuid, as the
// channel is no longer reachable once the player is online.
plugin.getConnectIntegration().addPlayer(existing.getId());
return;
}

var profile = plugin.getDatabaseProvider().getByName(event.getUsername());

var gProfile = event.getOriginalProfile();
Expand All @@ -109,15 +145,8 @@ public void onPreLogin(PreLoginEvent event) {

// If floodgate is present, attempt to extract the floodgate player from the connection channel.
if (plugin.floodgateEnabled()) {
Channel channel;
InboundConnection connection = event.getConnection();
try {
if (INITIAL_CONNECTION_DELEGATE != null) {
connection = (InboundConnection) INITIAL_CONNECTION_DELEGATE.get(connection);
}

Object mcConnection = INITIAL_MINECRAFT_CONNECTION.get(connection);
channel = (Channel) CHANNEL.get(mcConnection);
Channel channel = getChannel(event.getConnection());

if (channel.attr(FLOODGATE_ATTR).get() != null) {
return; // Player is coming from Floodgate
Expand All @@ -130,6 +159,14 @@ public void onPreLogin(PreLoginEvent event) {
}
}

// Minekube Connect authenticated the player at its own edge, there is no Mojang session
// left for this proxy to verify. Forcing online mode would make the proxy send an encryption
// request that can never be answered, so the login flow has to be skipped, just like it is
// for Floodgate players above.
if (fromConnect(event.getConnection())) {
return; // Player has already been authenticated by Connect
}

var result = onPreLogin(event.getUsername(), event.getConnection().getRemoteAddress().getAddress());

event.setResult(
Expand Down