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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,5 @@ nb-configuration.xml
# Don't exclude the .gitignore itself
!.gitignore
/samples/

.factorypath
13 changes: 13 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
<pluginDescription.authors>sgdc3, games647, Hex3l, krusic22</pluginDescription.authors>

<!-- Java libraries -->
<dependencies.junit.version>6.1.0</dependencies.junit.version>
<dependencies.injector.version>1.0</dependencies.injector.version>
<dependencies.string-similarity.version>1.0.0</dependencies.string-similarity.version>
<dependencies.geoip2.version>4.2.0</dependencies.geoip2.version>
Expand Down Expand Up @@ -174,6 +175,11 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.5</version>
</plugin>
<!-- Clean the target folder content -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down Expand Up @@ -552,6 +558,13 @@
</repositories>

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${dependencies.junit.version}</version>
<scope>test</scope>
</dependency>

<!-- Java Libraries -->
<dependency>
<groupId>org.geysermc.floodgate</groupId>
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public String getPluginVersion() {
* @return true if the player is authenticated
*/
public boolean isAuthenticated(Player player) {
return playerCache.isAuthenticated(player.getName());
return playerCache.isAuthenticated(player);
}

/**
Expand Down
283 changes: 243 additions & 40 deletions src/main/java/fr/xephi/authme/data/auth/PlayerCache.java
Original file line number Diff line number Diff line change
@@ -1,74 +1,277 @@
package fr.xephi.authme.data.auth;

import org.bukkit.entity.Player;

import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
* Used to manage player's Authenticated status
* Connection-bound authentication state machine.
* <p>
* Live authentication is owned by one exact {@link Player} object representing one server connection.
* Only that connection may transition itself {@code CONNECTED_UNAUTHENTICATED -> AUTHENTICATED}.
* Only that connection may transition itself {@code AUTHENTICATED -> CONNECTED_UNAUTHENTICATED} for logout.
* Only that connection may transition itself to disconnected on quit.
* <p>
* Name-based APIs are preserved for compatibility/offline/admin paths only.
* They must not authorize a live {@link Player}.
*/
public class PlayerCache {

private final Map<String, PlayerAuth> cache = new ConcurrentHashMap<>();
private final ConcurrentMap<String, ConnectionState> connections = new ConcurrentHashMap<>();

PlayerCache() {
}

/**
* Adds the given auth object to the player cache (for the name defined in the PlayerAuth).
*
* @param auth the player auth object to save
*/
public void registerConnection(Player player) {
Objects.requireNonNull(player, "player must not be null");
String name = normalizeName(player.getName());
UUID uuid = player.getUniqueId();
connections.compute(name, (key, oldState) ->
ConnectionState.unauthenticated(player, uuid));
}

public boolean authenticate(Player player, PlayerAuth auth) {
Objects.requireNonNull(player, "player must not be null");
Objects.requireNonNull(auth, "auth must not be null");
String name = normalizeName(player.getName());
UUID uuid = player.getUniqueId();

for (;;) {
ConnectionState current = connections.get(name);
if (current == null) {
return false;
}
if (!current.belongsTo(player)) {
return false;
}
if (current.isAuthenticated()) {
return true;
}
ConnectionState updated = current.withAuth(auth);
if (connections.replace(name, current, updated)) {
return true;
}
}
}

public Optional<PlayerAuth> deauthenticate(Player player) {
Objects.requireNonNull(player, "player must not be null");
String name = normalizeName(player.getName());

for (;;) {
ConnectionState current = connections.get(name);
if (current == null) {
return Optional.empty();
}
if (!current.belongsTo(player)) {
return Optional.empty();
}
if (!current.isAuthenticated()) {
return Optional.empty();
}
ConnectionState updated = current.withoutAuth();
if (connections.replace(name, current, updated)) {
return Optional.ofNullable(current.getAuth().orElse(null));
}
}
}

public Optional<ConnectionSnapshot> disconnect(Player player) {
Objects.requireNonNull(player, "player must not be null");
String name = normalizeName(player.getName());

for (;;) {
ConnectionState current = connections.get(name);
if (current == null) {
return Optional.empty();
}
if (!current.belongsTo(player)) {
return Optional.empty();
}
if (connections.remove(name, current)) {
return Optional.of(current.snapshot(name));
}
}
}

public AuthenticationState getAuthenticationState(Player player) {
Objects.requireNonNull(player, "player must not be null");
String name = normalizeName(player.getName());
ConnectionState state = connections.get(name);
if (state == null) {
return AuthenticationState.NO_CONNECTION;
}
if (!state.belongsTo(player)) {
return AuthenticationState.CONNECTION_MISMATCH;
}
return state.isAuthenticated()
? AuthenticationState.AUTHENTICATED
: AuthenticationState.CONNECTED_UNAUTHENTICATED;
}

public boolean isAuthenticated(Player player) {
return getAuthenticationState(player) == AuthenticationState.AUTHENTICATED;
}

public Optional<PlayerAuth> getAuth(Player player) {
Objects.requireNonNull(player, "player must not be null");
String name = normalizeName(player.getName());
ConnectionState state = connections.get(name);
if (state == null || !state.belongsTo(player)) {
return Optional.empty();
}
return state.getAuth();
}

public void updatePlayer(PlayerAuth auth) {
cache.put(auth.getNickname().toLowerCase(Locale.ROOT), auth);
if (auth == null) {
return;
}
String name = normalizeName(auth.getNickname());
connections.computeIfPresent(name, (key, state) -> {
if (state.isAuthenticated()) {
return state.withAuth(auth);
}
return state;
});
}

/**
* Removes a player from the player cache.
*
* @param user name of the player to remove
*/
public void removePlayer(String user) {
cache.remove(user.toLowerCase(Locale.ROOT));
connections.remove(normalizeName(user));
}

/**
* Get whether a player is authenticated (i.e. whether he is present in the player cache).
*
* @param user player's name
*
* @return true if player is logged in, false otherwise.
*/
public boolean isAuthenticated(String user) {
return cache.containsKey(user.toLowerCase(Locale.ROOT));
String name = normalizeName(user);
ConnectionState state = connections.get(name);
return state != null && state.isAuthenticated();
}

/**
* Returns the PlayerAuth associated with the given user, if available.
*
* @param user name of the player
*
* @return the associated auth object, or null if not available
*/
public PlayerAuth getAuth(String user) {
return cache.get(user.toLowerCase(Locale.ROOT));
String name = normalizeName(user);
ConnectionState state = connections.get(name);
if (state == null) {
return null;
}
return state.getAuth().orElse(null);
}

/**
* @return number of logged in players
*/
public int getLogged() {
return cache.size();
return (int) connections.values().stream().filter(ConnectionState::isAuthenticated).count();
}

/**
* Returns the player cache data.
*
* @return all player auths inside the player cache
*/
public Map<String, PlayerAuth> getCache() {
return this.cache;
Map<String, PlayerAuth> snapshot = new HashMap<>();
connections.forEach((name, state) -> {
state.getAuth().ifPresent(auth -> snapshot.put(name, auth));
});
return Collections.unmodifiableMap(snapshot);
}

private static String normalizeName(String name) {
return name.toLowerCase(Locale.ROOT);
}

public enum AuthenticationState {
NO_CONNECTION,
CONNECTED_UNAUTHENTICATED,
AUTHENTICATED,
CONNECTION_MISMATCH
}

public static final class ConnectionSnapshot {
private final String normalizedName;
private final UUID uniqueId;
private final PlayerAuth auth;

ConnectionSnapshot(String normalizedName, UUID uniqueId, PlayerAuth auth) {
this.normalizedName = normalizedName;
this.uniqueId = uniqueId;
this.auth = auth;
}

public String getNormalizedName() {
return normalizedName;
}

public UUID getUniqueId() {
return uniqueId;
}

public Optional<PlayerAuth> getAuth() {
return Optional.ofNullable(auth);
}

public boolean wasAuthenticated() {
return auth != null;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof ConnectionSnapshot)) {
return false;
}
ConnectionSnapshot that = (ConnectionSnapshot) o;
return Objects.equals(normalizedName, that.normalizedName)
&& Objects.equals(uniqueId, that.uniqueId)
&& Objects.equals(auth, that.auth);
}

@Override
public int hashCode() {
return Objects.hash(normalizedName, uniqueId, auth);
}

@Override
public String toString() {
return "ConnectionSnapshot{name=" + normalizedName + ",uuid=" + uniqueId + ",auth=" + (auth != null) + "}";
}
}

private static final class ConnectionState {
private final Player owner;
private final UUID uniqueId;
private final PlayerAuth auth;

ConnectionState(Player owner, UUID uniqueId, PlayerAuth auth) {
this.owner = owner;
this.uniqueId = uniqueId;
this.auth = auth;
}

static ConnectionState unauthenticated(Player owner, UUID uniqueId) {
return new ConnectionState(owner, uniqueId, null);
}

ConnectionState withAuth(PlayerAuth auth) {
return new ConnectionState(this.owner, this.uniqueId, auth);
}

ConnectionState withoutAuth() {
return new ConnectionState(this.owner, this.uniqueId, null);
}

boolean belongsTo(Player candidate) {
return this.owner == candidate;
}

boolean isAuthenticated() {
return auth != null;
}

Optional<PlayerAuth> getAuth() {
return Optional.ofNullable(auth);
}

ConnectionSnapshot snapshot(String normalizedName) {
return new ConnectionSnapshot(normalizedName, uniqueId, auth);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import fr.xephi.authme.data.captcha.RegistrationCaptchaManager;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.process.login.ForceLoginRequestService;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.RegistrationSettings;
Expand Down Expand Up @@ -34,6 +35,9 @@ class LimboPlayerTaskManager {
@Inject
private PlayerCache playerCache;

@Inject
private ForceLoginRequestService forceLoginRequestService;

@Inject
private RegistrationCaptchaManager registrationCaptchaManager;

Expand All @@ -52,7 +56,7 @@ void registerMessageTask(Player player, LimboPlayer limbo, LimboMessageType mess
MessageResult result = getMessageKey(player.getName(), messageType);
if (interval > 0) {
String[] joinMessage = messages.retrieveSingle(player, result.messageKey, result.args).split("\n");
MessageTask messageTask = new MessageTask(player, joinMessage);
MessageTask messageTask = new MessageTask(player, joinMessage, playerCache, forceLoginRequestService);
bukkitService.runTaskTimer(messageTask, 2 * TICKS_PER_SECOND, (long) interval * TICKS_PER_SECOND);
limbo.setMessageTask(messageTask);
}
Expand Down
Loading