Improve the way player uuid data is saved/stored. Add a warning message to catch ip forwarding issues

This commit is contained in:
Luck 2018-03-29 21:46:34 +01:00
parent 8f30176edd
commit e4e93b1af1
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
25 changed files with 485 additions and 223 deletions

View File

@ -254,7 +254,7 @@ public class ApiStorage implements me.lucko.luckperms.api.Storage {
public CompletableFuture<Boolean> saveUUIDData(@Nonnull String username, @Nonnull UUID uuid) {
Objects.requireNonNull(username, "username");
Objects.requireNonNull(uuid, "uuid");
return this.handle.noBuffer().saveUUIDData(uuid, checkUsername(username))
return this.handle.noBuffer().savePlayerData(uuid, checkUsername(username))
.thenApply(r -> true)
.exceptionally(consumeExceptionToFalse());
}
@ -263,13 +263,13 @@ public class ApiStorage implements me.lucko.luckperms.api.Storage {
@Override
public CompletableFuture<UUID> getUUID(@Nonnull String username) {
Objects.requireNonNull(username, "username");
return this.handle.noBuffer().getUUID(checkUsername(username));
return this.handle.noBuffer().getPlayerUuid(checkUsername(username));
}
@Nonnull
@Override
public CompletableFuture<String> getName(@Nonnull UUID uuid) {
Objects.requireNonNull(uuid, "uuid");
return this.handle.noBuffer().getName(uuid);
return this.handle.noBuffer().getPlayerName(uuid);
}
}

View File

@ -92,7 +92,7 @@ public class GroupListMembers extends SubCommand<Group> {
if (!matchedUsers.isEmpty()) {
LoadingCache<UUID, String> uuidLookups = Caffeine.newBuilder()
.build(u -> {
String s = plugin.getStorage().getName(u).join();
String s = plugin.getStorage().getPlayerName(u).join();
if (s == null || s.isEmpty() || s.equals("null")) {
s = u.toString();
}

View File

@ -86,7 +86,7 @@ public class LogRecent extends SubCommand<Log> {
}
}
uuid = plugin.getStorage().getUUID(target.toLowerCase()).join();
uuid = plugin.getStorage().getPlayerUuid(target.toLowerCase()).join();
if (uuid == null) {
if (!plugin.getConfiguration().get(ConfigKeys.USE_SERVER_UUID_CACHE)) {
Message.USER_NOT_FOUND.send(sender, target);

View File

@ -81,7 +81,7 @@ public class LogUserHistory extends SubCommand<Log> {
}
}
uuid = plugin.getStorage().getUUID(target.toLowerCase()).join();
uuid = plugin.getStorage().getPlayerUuid(target.toLowerCase()).join();
if (uuid == null) {
if (!plugin.getConfiguration().get(ConfigKeys.USE_SERVER_UUID_CACHE)) {
Message.USER_NOT_FOUND.send(sender, target);

View File

@ -86,7 +86,7 @@ public class SearchCommand extends SingleCommand {
if (!matchedUsers.isEmpty()) {
LoadingCache<UUID, String> uuidLookups = Caffeine.newBuilder()
.build(u -> {
String s = plugin.getStorage().getName(u).join();
String s = plugin.getStorage().getPlayerName(u).join();
if (s == null || s.isEmpty() || s.equals("null")) {
s = u.toString();
}

View File

@ -73,7 +73,7 @@ public class UserClone extends SubCommand<User> {
}
}
uuid = plugin.getStorage().getUUID(target.toLowerCase()).join();
uuid = plugin.getStorage().getPlayerUuid(target.toLowerCase()).join();
if (uuid == null) {
if (!plugin.getConfiguration().get(ConfigKeys.USE_SERVER_UUID_CACHE)) {
Message.USER_NOT_FOUND.send(sender, target);

View File

@ -67,6 +67,7 @@ public class UserInfo extends SubCommand<User> {
Message.USER_INFO_GENERAL.send(sender,
user.getName().orElse("Unknown"),
user.getUuid(),
user.getUuid().version() == 4 ? "&2mojang" : "&8offline",
status.asString(plugin.getLocaleManager()),
user.getPrimaryGroup().getValue()
);

View File

@ -96,7 +96,7 @@ public class UserMainCommand extends MainCommand<User, UserIdentifier> {
}
}
uuid = plugin.getStorage().getUUID(target.toLowerCase()).join();
uuid = plugin.getStorage().getPlayerUuid(target.toLowerCase()).join();
if (uuid == null) {
if (!plugin.getConfiguration().get(ConfigKeys.USE_SERVER_UUID_CACHE)) {
Message.USER_NOT_FOUND.send(sender, target);
@ -111,7 +111,7 @@ public class UserMainCommand extends MainCommand<User, UserIdentifier> {
}
}
String name = plugin.getStorage().getName(uuid).join();
String name = plugin.getStorage().getPlayerName(uuid).join();
return UserIdentifier.of(uuid, name);
}

View File

@ -72,8 +72,8 @@ import me.lucko.luckperms.common.event.impl.EventUserFirstLogin;
import me.lucko.luckperms.common.event.impl.EventUserLoad;
import me.lucko.luckperms.common.event.impl.EventUserLoginProcess;
import me.lucko.luckperms.common.event.impl.EventUserPromote;
import me.lucko.luckperms.common.event.model.EntitySender;
import me.lucko.luckperms.common.event.model.SourceEntity;
import me.lucko.luckperms.common.event.model.EntitySourceImpl;
import me.lucko.luckperms.common.event.model.SenderEntity;
import me.lucko.luckperms.common.model.Group;
import me.lucko.luckperms.common.model.PermissionHolder;
import me.lucko.luckperms.common.model.Track;
@ -268,12 +268,12 @@ public final class EventFactory {
}
public void handleUserDemote(User user, Track track, String from, String to, Sender source) {
EventUserDemote event = new EventUserDemote(track.getApiDelegate(), new ApiUser(user), from, to, new SourceEntity(new EntitySender(source)));
EventUserDemote event = new EventUserDemote(track.getApiDelegate(), new ApiUser(user), from, to, new EntitySourceImpl(new SenderEntity(source)));
fireEventAsync(event);
}
public void handleUserPromote(User user, Track track, String from, String to, Sender source) {
EventUserPromote event = new EventUserPromote(track.getApiDelegate(), new ApiUser(user), from, to, new SourceEntity(new EntitySender(source)));
EventUserPromote event = new EventUserPromote(track.getApiDelegate(), new ApiUser(user), from, to, new EntitySourceImpl(new SenderEntity(source)));
fireEventAsync(event);
}

View File

@ -29,7 +29,7 @@ import me.lucko.luckperms.api.Entity;
import me.lucko.luckperms.api.LogEntry;
import me.lucko.luckperms.api.event.log.LogNotifyEvent;
import me.lucko.luckperms.common.event.AbstractEvent;
import me.lucko.luckperms.common.event.model.EntitySender;
import me.lucko.luckperms.common.event.model.SenderEntity;
import me.lucko.luckperms.common.sender.Sender;
import java.util.concurrent.atomic.AtomicBoolean;
@ -56,7 +56,7 @@ public class EventLogNotify extends AbstractEvent implements LogNotifyEvent {
@Override
public synchronized Entity getNotifiable() {
if (this.notifiable == null) {
this.notifiable = new EntitySender(this.sender);
this.notifiable = new SenderEntity(this.sender);
}
return this.notifiable;
}

View File

@ -30,10 +30,10 @@ import me.lucko.luckperms.api.event.source.EntitySource;
import javax.annotation.Nonnull;
public class SourceEntity implements EntitySource {
public class EntitySourceImpl implements EntitySource {
private final Entity entity;
public SourceEntity(Entity entity) {
public EntitySourceImpl(Entity entity) {
this.entity = entity;
}
@ -51,6 +51,6 @@ public class SourceEntity implements EntitySource {
@Override
public String toString() {
return "Sender(type=ENTITY, entity=" + this.entity + ")";
return "Source(type=ENTITY, entity=" + this.entity + ")";
}
}

View File

@ -33,10 +33,10 @@ import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class EntitySender implements Entity {
public class SenderEntity implements Entity {
private final Sender sender;
public EntitySender(Sender sender) {
public SenderEntity(Sender sender) {
this.sender = sender;
}
@ -67,6 +67,6 @@ public class EntitySender implements Entity {
@Override
public String toString() {
return "Sender(type=" + getType() + ", sender=" + this.sender + ")";
return "SenderEntity(type=" + getType() + ", sender=" + this.sender + ")";
}
}

View File

@ -29,10 +29,10 @@ import me.lucko.luckperms.api.event.source.Source;
import javax.annotation.Nonnull;
public final class SourceUnknown implements Source {
private static final Source INSTANCE = new SourceUnknown();
public final class UnknownSource implements Source {
private static final Source INSTANCE = new UnknownSource();
private SourceUnknown() {
private UnknownSource() {
}

View File

@ -29,6 +29,7 @@ import me.lucko.luckperms.common.assignments.AssignmentRule;
import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.model.User;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.storage.PlayerSaveResult;
import java.util.Set;
import java.util.UUID;
@ -61,11 +62,16 @@ public abstract class AbstractConnectionListener implements ConnectionListener {
this.plugin.getUserManager().getHouseKeeper().registerUsage(u);
// save uuid data.
String name = this.plugin.getStorage().noBuffer().getName(u).join();
if (name == null) {
PlayerSaveResult saveResult = this.plugin.getStorage().savePlayerData(u, username).join();
if (saveResult.includes(PlayerSaveResult.Status.CLEAN_INSERT)) {
this.plugin.getEventFactory().handleUserFirstLogin(u, username);
}
this.plugin.getStorage().noBuffer().saveUUIDData(u, username);
if (saveResult.includes(PlayerSaveResult.Status.OTHER_UUIDS_PRESENT_FOR_USERNAME)) {
this.plugin.getLogger().warn("LuckPerms already has data for player '" + username + "' - but this data is stored under a different uuid.");
this.plugin.getLogger().warn("'" + username + "' has previously used the unique ids " + saveResult.getOtherUuids() + " but is now connecting with '" + u + "'");
this.plugin.getLogger().warn("This is usually because the server is not authenticating correctly. If you're using BungeeCord, please ensure that IP-Forwarding is setup correctly!");
}
User user = this.plugin.getStorage().noBuffer().loadUser(u, username).join();
if (user == null) {

View File

@ -309,7 +309,7 @@ public enum Message {
USER_INFO_GENERAL(
"{PREFIX}&b&l> &bUser Info: &f{}" + "\n" +
"{PREFIX}&f- &3UUID: &f{}" + "\n" +
"{PREFIX}&f- &3UUID: &f{} &7(type: {}&7)" + "\n" +
"{PREFIX}&f- &3Status: {}" + "\n" +
"{PREFIX}&f- &3Primary Group: &f{}",
false

View File

@ -290,17 +290,17 @@ public class AbstractStorage implements Storage {
}
@Override
public CompletableFuture<Void> saveUUIDData(UUID uuid, String username) {
return makeFuture(() -> this.dao.saveUUIDData(uuid, username));
public CompletableFuture<PlayerSaveResult> savePlayerData(UUID uuid, String username) {
return makeFuture(() -> this.dao.savePlayerData(uuid, username));
}
@Override
public CompletableFuture<UUID> getUUID(String username) {
return makeFuture(() -> this.dao.getUUID(username));
public CompletableFuture<UUID> getPlayerUuid(String username) {
return makeFuture(() -> this.dao.getPlayerUuid(username));
}
@Override
public CompletableFuture<String> getName(UUID uuid) {
return makeFuture(() -> this.dao.getName(uuid));
public CompletableFuture<String> getPlayerName(UUID uuid) {
return makeFuture(() -> this.dao.getPlayerName(uuid));
}
}

View File

@ -0,0 +1,194 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* 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.
*/
package me.lucko.luckperms.common.storage;
import com.google.common.collect.ImmutableSet;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Represents the result to a player history save operation
*/
public final class PlayerSaveResult {
private static final PlayerSaveResult CLEAN_INSERT = new PlayerSaveResult(Status.CLEAN_INSERT);
private static final PlayerSaveResult NO_CHANGE = new PlayerSaveResult(Status.NO_CHANGE);
public static PlayerSaveResult cleanInsert() {
return CLEAN_INSERT;
}
public static PlayerSaveResult noChange() {
return NO_CHANGE;
}
public static PlayerSaveResult usernameUpdated(String oldUsername) {
return new PlayerSaveResult(oldUsername, null, Status.USERNAME_UPDATED);
}
public static PlayerSaveResult determineBaseResult(String username, String oldUsername) {
PlayerSaveResult result;
if (oldUsername == null) {
result = PlayerSaveResult.cleanInsert();
} else if (oldUsername.equalsIgnoreCase(username)) {
result = PlayerSaveResult.noChange();
} else {
result = PlayerSaveResult.usernameUpdated(oldUsername);
}
return result;
}
private final Set<Status> status;
@Nullable private final String oldUsername;
@Nullable private final Set<UUID> otherUuids;
private PlayerSaveResult(EnumSet<Status> status, @Nullable String oldUsername, @Nullable Set<UUID> otherUuids) {
this.status = ImmutableSet.copyOf(status);
this.oldUsername = oldUsername;
this.otherUuids = otherUuids;
}
private PlayerSaveResult(@Nullable String oldUsername, @Nullable Set<UUID> otherUuids, Status... status) {
this(EnumSet.copyOf(Arrays.asList(status)), oldUsername, otherUuids);
}
private PlayerSaveResult(Status... status) {
this(null, null, status);
}
/**
* Returns a new {@link PlayerSaveResult} with the {@link Status#OTHER_UUIDS_PRESENT_FOR_USERNAME}
* status attached to the state of this result.
*
* @param otherUuids the other uuids
* @return a new result
*/
public PlayerSaveResult withOtherUuidsPresent(@Nonnull Set<UUID> otherUuids) {
EnumSet<Status> status = EnumSet.copyOf(this.status);
status.add(Status.OTHER_UUIDS_PRESENT_FOR_USERNAME);
return new PlayerSaveResult(status, this.oldUsername, ImmutableSet.copyOf(otherUuids));
}
/**
* Gets the status returned by the operation
*
* @return the status
*/
public Set<Status> getStatus() {
return this.status;
}
/**
* Gets if the result includes a certain status code.
*
* @param status the status to check for
* @return if the result includes the status
*/
public boolean includes(Status status) {
return this.status.contains(status);
}
/**
* Gets the old username involved in the result
*
* @return the old username
* @see Status#USERNAME_UPDATED
*/
@Nullable
public String getOldUsername() {
return this.oldUsername;
}
/**
* Gets the other uuids involved in the result
*
* @return the other uuids
* @see Status#OTHER_UUIDS_PRESENT_FOR_USERNAME
*/
@Nullable
public Set<UUID> getOtherUuids() {
return this.otherUuids;
}
@Override
public boolean equals(Object that) {
if (this == that) return true;
if (that == null || getClass() != that.getClass()) return false;
PlayerSaveResult result = (PlayerSaveResult) that;
return Objects.equals(this.status, result.status) &&
Objects.equals(this.oldUsername, result.oldUsername) &&
Objects.equals(this.otherUuids, result.otherUuids);
}
@Override
public int hashCode() {
return Objects.hash(this.status, this.oldUsername, this.otherUuids);
}
/**
* The various states the result can take
*/
public enum Status {
/**
* There was no existing data saved for either the uuid or username
*/
CLEAN_INSERT,
/**
* There was existing data for the player, no change was needed.
*/
NO_CHANGE,
/**
* There was already a record for the UUID saved, but it was for a different username.
*
* <p>This is normal, players are able to change their usernames.</p>
*/
USERNAME_UPDATED,
/**
* There was already a record for the username saved, but it was under a different uuid.
*
* <p>This is a bit of a cause for concern. It's possible that "player1" has changed
* their username to "player2", and "player3" has changed their username to "player1".
* If the original "player1" doesn't join after changing their name, this conflict could
* occur.</p>
*
* <p>However, what's more likely is that the server is not setup to authenticate
* correctly. Usually this is a problem with BungeeCord "ip-forwarding", but could be
* that the user of the plugin is running a network off a shared database with one
* server in online mode and another in offline mode.</p>
*/
OTHER_UUIDS_PRESENT_FOR_USERNAME,
}
}

View File

@ -99,9 +99,9 @@ public interface Storage {
CompletableFuture<Void> deleteTrack(Track track, DeletionCause cause);
CompletableFuture<Void> saveUUIDData(UUID uuid, String username);
CompletableFuture<PlayerSaveResult> savePlayerData(UUID uuid, String username);
CompletableFuture<UUID> getUUID(String username);
CompletableFuture<UUID> getPlayerUuid(String username);
CompletableFuture<String> getName(UUID uuid);
CompletableFuture<String> getPlayerName(UUID uuid);
}

View File

@ -33,6 +33,7 @@ import me.lucko.luckperms.common.model.Group;
import me.lucko.luckperms.common.model.Track;
import me.lucko.luckperms.common.model.User;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.storage.PlayerSaveResult;
import java.util.Collections;
import java.util.List;
@ -105,12 +106,12 @@ public abstract class AbstractDao {
public abstract void deleteTrack(Track track) throws Exception;
public abstract void saveUUIDData(UUID uuid, String username) throws Exception;
public abstract PlayerSaveResult savePlayerData(UUID uuid, String username) throws Exception;
@Nullable
public abstract UUID getUUID(String username) throws Exception;
public abstract UUID getPlayerUuid(String username) throws Exception;
@Nullable
public abstract String getName(UUID uuid) throws Exception;
public abstract String getPlayerName(UUID uuid) throws Exception;
}

View File

@ -35,6 +35,7 @@ import me.lucko.luckperms.common.model.Group;
import me.lucko.luckperms.common.model.Track;
import me.lucko.luckperms.common.model.User;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.storage.PlayerSaveResult;
import me.lucko.luckperms.common.storage.SplitStorageType;
import me.lucko.luckperms.common.storage.StorageType;
@ -191,17 +192,17 @@ public class SplitStorageDao extends AbstractDao {
}
@Override
public void saveUUIDData(UUID uuid, String username) throws Exception {
this.backing.get(this.types.get(SplitStorageType.UUID)).saveUUIDData(uuid, username);
public PlayerSaveResult savePlayerData(UUID uuid, String username) throws Exception {
return this.backing.get(this.types.get(SplitStorageType.UUID)).savePlayerData(uuid, username);
}
@Override
public UUID getUUID(String username) throws Exception {
return this.backing.get(this.types.get(SplitStorageType.UUID)).getUUID(username);
public UUID getPlayerUuid(String username) throws Exception {
return this.backing.get(this.types.get(SplitStorageType.UUID)).getPlayerUuid(username);
}
@Override
public String getName(UUID uuid) throws Exception {
return this.backing.get(this.types.get(SplitStorageType.UUID)).getName(uuid);
public String getPlayerName(UUID uuid) throws Exception {
return this.backing.get(this.types.get(SplitStorageType.UUID)).getPlayerName(uuid);
}
}

View File

@ -47,6 +47,7 @@ import me.lucko.luckperms.common.node.NodeHeldPermission;
import me.lucko.luckperms.common.node.NodeModel;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.references.UserIdentifier;
import me.lucko.luckperms.common.storage.PlayerSaveResult;
import me.lucko.luckperms.common.storage.dao.AbstractDao;
import me.lucko.luckperms.common.utils.ImmutableCollectors;
import me.lucko.luckperms.common.utils.Uuids;
@ -670,17 +671,17 @@ public abstract class ConfigurateDao extends AbstractDao {
}
@Override
public void saveUUIDData(UUID uuid, String username) {
this.uuidCache.addMapping(uuid, username);
public PlayerSaveResult savePlayerData(UUID uuid, String username) {
return this.uuidCache.addMapping(uuid, username);
}
@Override
public UUID getUUID(String username) {
return this.uuidCache.lookup(username);
public UUID getPlayerUuid(String username) {
return this.uuidCache.lookupUuid(username);
}
@Override
public String getName(UUID uuid) {
public String getPlayerName(UUID uuid) {
return this.uuidCache.lookupUsername(uuid);
}

View File

@ -26,9 +26,12 @@
package me.lucko.luckperms.common.storage.dao.file;
import com.google.common.base.Splitter;
import com.google.common.collect.Maps;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import me.lucko.luckperms.common.utils.DateUtil;
import me.lucko.luckperms.common.storage.PlayerSaveResult;
import me.lucko.luckperms.common.utils.Uuids;
import java.io.BufferedReader;
import java.io.BufferedWriter;
@ -36,19 +39,60 @@ import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nullable;
public class FileUuidCache {
private static final Splitter KV_SPLIT = Splitter.on('=').omitEmptyStrings();
private static final Splitter TIME_SPLIT = Splitter.on('|').omitEmptyStrings();
private static final Splitter KV_SPLIT = Splitter.on(':').omitEmptyStrings();
private static final Splitter LEGACY_KV_SPLIT = Splitter.on('=').omitEmptyStrings();
private static final Splitter LEGACY_TIME_SPLIT = Splitter.on('|').omitEmptyStrings();
// the map for lookups
private final Map<String, Map.Entry<UUID, Long>> lookupMap = new ConcurrentHashMap<>();
// the lookup map
private final LookupMap lookupMap = new LookupMap();
private static final class LookupMap extends ConcurrentHashMap<UUID, String> {
private final SetMultimap<String, UUID> reverse = Multimaps.newSetMultimap(new ConcurrentHashMap<>(), ConcurrentHashMap::newKeySet);
@Override
public String put(UUID key, String value) {
String existing = super.put(key, value);
// check if we need to remove a reverse entry which has been replaced
// existing might be null
if (!value.equalsIgnoreCase(existing)) {
if (existing != null) {
this.reverse.remove(existing.toLowerCase(), key);
}
}
this.reverse.put(value.toLowerCase(), key);
return existing;
}
@Override
public String remove(Object k) {
UUID key = (UUID) k;
String username = super.remove(key);
if (username != null) {
this.reverse.remove(username.toLowerCase(), key);
}
return username;
}
public String lookupUsername(UUID uuid) {
return super.get(uuid);
}
public Set<UUID> lookupUuid(String name) {
return this.reverse.get(name.toLowerCase());
}
}
/**
* Adds a mapping to the cache
@ -56,8 +100,25 @@ public class FileUuidCache {
* @param uuid the uuid of the player
* @param username the username of the player
*/
public void addMapping(UUID uuid, String username) {
this.lookupMap.put(username.toLowerCase(), Maps.immutableEntry(uuid, DateUtil.unixSecondsNow()));
public PlayerSaveResult addMapping(UUID uuid, String username) {
// perform the insert
String oldUsername = this.lookupMap.put(uuid, username);
PlayerSaveResult result = PlayerSaveResult.determineBaseResult(username, oldUsername);
Set<UUID> conflicting = new HashSet<>(this.lookupMap.lookupUuid(username));
conflicting.remove(uuid);
if (!conflicting.isEmpty()) {
// remove the mappings for conflicting uuids
for (UUID conflict : conflicting) {
this.lookupMap.remove(conflict);
}
result = result.withOtherUuidsPresent(conflicting);
}
return result;
}
/**
@ -67,9 +128,9 @@ public class FileUuidCache {
* @return a uuid, or null
*/
@Nullable
public UUID lookup(String username) {
Map.Entry<UUID, Long> ret = this.lookupMap.get(username.toLowerCase());
return ret == null ? null : ret.getKey();
public UUID lookupUuid(String username) {
Set<UUID> uuids = this.lookupMap.lookupUuid(username);
return Iterables.getFirst(uuids, null);
}
/**
@ -79,23 +140,46 @@ public class FileUuidCache {
* @return a username, or null
*/
public String lookupUsername(UUID uuid) {
String username = null;
Long time = Long.MIN_VALUE;
return this.lookupMap.lookupUsername(uuid);
}
for (Map.Entry<String, Map.Entry<UUID, Long>> ent : this.lookupMap.entrySet()) {
if (!ent.getValue().getKey().equals(uuid)) {
continue;
private void loadEntry(String entry) {
if (entry.contains(":")) {
// new format
Iterator<String> parts = KV_SPLIT.split(entry).iterator();
if (!parts.hasNext()) return;
String uuidPart = parts.next();
if (!parts.hasNext()) return;
String usernamePart = parts.next();
UUID uuid = Uuids.fromString(uuidPart);
if (uuid == null) return;
this.lookupMap.put(uuid, usernamePart);
} else if (entry.contains("=")) {
// old format
Iterator<String> parts = LEGACY_KV_SPLIT.split(entry).iterator();
if (!parts.hasNext()) return;
String usernamePart = parts.next();
if (!parts.hasNext()) return;
String uuidPart = parts.next();
// contains a time
if (uuidPart.contains("|")) {
Iterator<String> valueParts = LEGACY_TIME_SPLIT.split(uuidPart).iterator();
if (!valueParts.hasNext()) return;
uuidPart = valueParts.next();
}
Long t = ent.getValue().getValue();
UUID uuid = Uuids.fromString(uuidPart);
if (uuid == null) return;
if (t > time) {
time = t;
username = ent.getKey();
}
this.lookupMap.put(uuid, usernamePart);
}
return username;
}
public void load(File file) {
@ -104,61 +188,14 @@ public class FileUuidCache {
}
try (BufferedReader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
String entry;
while ((entry = reader.readLine()) != null) {
entry = entry.trim();
if (entry.isEmpty() || entry.startsWith("#")) {
continue;
}
Iterator<String> parts = KV_SPLIT.split(entry).iterator();
if (!parts.hasNext()) continue;
String key = parts.next();
if (!parts.hasNext()) continue;
String value = parts.next();
UUID uid;
Long t;
// contains a time (backwards compat)
if (value.contains("|")) {
// try to split and extract the time element from the end.
Iterator<String> valueParts = TIME_SPLIT.split(value).iterator();
if (!valueParts.hasNext()) continue;
String uuid = valueParts.next();
if (!valueParts.hasNext()) continue;
String time = valueParts.next();
try {
uid = UUID.fromString(uuid);
} catch (IllegalArgumentException e) {
continue;
}
try {
t = Long.parseLong(time);
} catch (NumberFormatException e) {
continue;
}
} else {
// just parse from the value
try {
uid = UUID.fromString(value);
} catch (IllegalArgumentException e) {
continue;
}
t = 0L;
}
this.lookupMap.put(key, Maps.immutableEntry(uid, t));
loadEntry(entry);
}
} catch (IOException e) {
e.printStackTrace();
}
@ -168,13 +205,11 @@ public class FileUuidCache {
try (BufferedWriter writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) {
writer.write("# LuckPerms UUID lookup cache");
writer.newLine();
for (Map.Entry<String, Map.Entry<UUID, Long>> ent : this.lookupMap.entrySet()) {
String out = ent.getKey() + "=" + ent.getValue().getKey().toString() + "|" + ent.getValue().getValue().toString();
for (Map.Entry<UUID, String> ent : this.lookupMap.entrySet()) {
String out = ent.getKey() + ":" + ent.getValue();
writer.write(out);
writer.newLine();
}
writer.flush();
} catch (IOException e) {
e.printStackTrace();

View File

@ -34,6 +34,7 @@ import com.mongodb.ServerAddress;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.UpdateOptions;
import me.lucko.luckperms.api.HeldPermission;
@ -56,6 +57,7 @@ import me.lucko.luckperms.common.node.NodeHeldPermission;
import me.lucko.luckperms.common.node.NodeModel;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.references.UserIdentifier;
import me.lucko.luckperms.common.storage.PlayerSaveResult;
import me.lucko.luckperms.common.storage.StorageCredentials;
import me.lucko.luckperms.common.storage.dao.AbstractDao;
@ -569,29 +571,53 @@ public class MongoDao extends AbstractDao {
}
@Override
public void saveUUIDData(UUID uuid, String username) {
public PlayerSaveResult savePlayerData(UUID uuid, String username) {
username = username.toLowerCase();
MongoCollection<Document> c = this.database.getCollection(this.prefix + "uuid");
c.replaceOne(new Document("_id", uuid), new Document("_id", uuid).append("name", username.toLowerCase()), new UpdateOptions().upsert(true));
// find any existing mapping
String oldUsername = getPlayerName(uuid);
// do the insert
if (!username.equalsIgnoreCase(oldUsername)) {
c.replaceOne(new Document("_id", uuid), new Document("_id", uuid).append("name", username), new UpdateOptions().upsert(true));
}
PlayerSaveResult result = PlayerSaveResult.determineBaseResult(username, oldUsername);
Set<UUID> conflicting = new HashSet<>();
try (MongoCursor<Document> cursor = c.find(new Document("name", username)).iterator()) {
if (cursor.hasNext()) {
conflicting.add(cursor.next().get("_id", UUID.class));
}
}
conflicting.remove(uuid);
if (!conflicting.isEmpty()) {
// remove the mappings for conflicting uuids
c.deleteMany(Filters.and(conflicting.stream().map(u -> Filters.eq("_id", u)).collect(Collectors.toList())));
result = result.withOtherUuidsPresent(conflicting);
}
return result;
}
@Override
public UUID getUUID(String username) {
public UUID getPlayerUuid(String username) {
MongoCollection<Document> c = this.database.getCollection(this.prefix + "uuid");
try (MongoCursor<Document> cursor = c.find(new Document("name", username.toLowerCase())).iterator()) {
if (cursor.hasNext()) {
return cursor.next().get("_id", UUID.class);
}
Document doc = c.find(new Document("name", username.toLowerCase())).first();
if (doc != null) {
return doc.get("_id", UUID.class);
}
return null;
}
@Override
public String getName(UUID uuid) {
public String getPlayerName(UUID uuid) {
MongoCollection<Document> c = this.database.getCollection(this.prefix + "uuid");
try (MongoCursor<Document> cursor = c.find(new Document("_id", uuid)).iterator()) {
if (cursor.hasNext()) {
return cursor.next().get("name", String.class);
}
Document doc = c.find(new Document("_id", uuid)).first();
if (doc != null) {
return doc.get("name", String.class);
}
return null;
}

View File

@ -46,6 +46,7 @@ import me.lucko.luckperms.common.node.NodeHeldPermission;
import me.lucko.luckperms.common.node.NodeModel;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.references.UserIdentifier;
import me.lucko.luckperms.common.storage.PlayerSaveResult;
import me.lucko.luckperms.common.storage.dao.AbstractDao;
import me.lucko.luckperms.common.storage.dao.sql.connection.AbstractConnectionFactory;
import me.lucko.luckperms.common.storage.dao.sql.connection.file.SQLiteConnectionFactory;
@ -68,8 +69,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -83,14 +82,15 @@ public class SqlDao extends AbstractDao {
private static final String USER_PERMISSIONS_SELECT_DISTINCT = "SELECT DISTINCT uuid FROM {prefix}user_permissions";
private static final String USER_PERMISSIONS_SELECT_PERMISSION = "SELECT uuid, value, server, world, expiry, contexts FROM {prefix}user_permissions WHERE permission=?";
private static final String PLAYER_SELECT = "SELECT username, primary_group FROM {prefix}players WHERE uuid=?";
private static final String PLAYER_SELECT_UUID = "SELECT uuid FROM {prefix}players WHERE username=? LIMIT 1";
private static final String PLAYER_SELECT_USERNAME = "SELECT username FROM {prefix}players WHERE uuid=? LIMIT 1";
private static final String PLAYER_SELECT_PRIMARY_GROUP = "SELECT primary_group FROM {prefix}players WHERE uuid=? LIMIT 1";
private static final String PLAYER_SELECT_UUID_BY_USERNAME = "SELECT uuid FROM {prefix}players WHERE username=? LIMIT 1";
private static final String PLAYER_SELECT_USERNAME_BY_UUID = "SELECT username FROM {prefix}players WHERE uuid=? LIMIT 1";
private static final String PLAYER_UPDATE_USERNAME_FOR_UUID = "UPDATE {prefix}players SET username=? WHERE uuid=?";
private static final String PLAYER_INSERT = "INSERT INTO {prefix}players VALUES(?, ?, ?)";
private static final String PLAYER_UPDATE = "UPDATE {prefix}players SET username=? WHERE uuid=?";
private static final String PLAYER_DELETE = "DELETE FROM {prefix}players WHERE username=? AND NOT uuid=?";
private static final String PLAYER_UPDATE_PRIMARY_GROUP = "UPDATE {prefix}players SET primary_group=? WHERE uuid=?";
private static final String PLAYER_SELECT_ALL_UUIDS_BY_USERNAME = "SELECT uuid FROM {prefix}players WHERE username=? AND NOT uuid=?";
private static final String PLAYER_DELETE_ALL_UUIDS_BY_USERNAME = "DELETE FROM {prefix}players WHERE username=? AND NOT uuid=?";
private static final String PLAYER_SELECT_BY_UUID = "SELECT username, primary_group FROM {prefix}players WHERE uuid=?";
private static final String PLAYER_SELECT_PRIMARY_GROUP_BY_UUID = "SELECT primary_group FROM {prefix}players WHERE uuid=? LIMIT 1";
private static final String PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID = "UPDATE {prefix}players SET primary_group=? WHERE uuid=?";
private static final String GROUP_PERMISSIONS_SELECT = "SELECT permission, value, server, world, expiry, contexts FROM {prefix}group_permissions WHERE name=?";
private static final String GROUP_PERMISSIONS_DELETE = "DELETE FROM {prefix}group_permissions WHERE name=?";
@ -295,8 +295,8 @@ public class SqlDao extends AbstractDao {
user.getIoLock().lock();
try {
List<NodeModel> data = new ArrayList<>();
AtomicReference<String> primaryGroup = new AtomicReference<>(null);
AtomicReference<String> userName = new AtomicReference<>(null);
String primaryGroup = null;
String userName = null;
// Collect user permissions
try (Connection c = this.provider.getConnection()) {
@ -319,27 +319,26 @@ public class SqlDao extends AbstractDao {
// Collect user meta (username & primary group)
try (Connection c = this.provider.getConnection()) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_SELECT))) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_SELECT_BY_UUID))) {
ps.setString(1, user.getUuid().toString());
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
userName.set(rs.getString("username"));
primaryGroup.set(rs.getString("primary_group"));
userName = rs.getString("username");
primaryGroup = rs.getString("primary_group");
}
}
}
}
// update username & primary group
String pg = primaryGroup.get();
if (pg == null) {
pg = NodeFactory.DEFAULT_GROUP_NAME;
if (primaryGroup == null) {
primaryGroup = NodeFactory.DEFAULT_GROUP_NAME;
}
user.getPrimaryGroup().setStoredValue(pg);
user.getPrimaryGroup().setStoredValue(primaryGroup);
// Update their username to what was in the storage if the one in the local instance is null
user.setName(userName.get(), true);
user.setName(userName, true);
// If the user has any data in storage
if (!data.isEmpty()) {
@ -378,7 +377,7 @@ public class SqlDao extends AbstractDao {
ps.setString(1, user.getUuid().toString());
ps.execute();
}
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_UPDATE_PRIMARY_GROUP))) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID))) {
ps.setString(1, NodeFactory.DEFAULT_GROUP_NAME);
ps.setString(2, user.getUuid().toString());
ps.execute();
@ -453,7 +452,7 @@ public class SqlDao extends AbstractDao {
try (Connection c = this.provider.getConnection()) {
boolean hasPrimaryGroupSaved;
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_SELECT_PRIMARY_GROUP))) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_SELECT_PRIMARY_GROUP_BY_UUID))) {
ps.setString(1, user.getUuid().toString());
try (ResultSet rs = ps.executeQuery()) {
hasPrimaryGroupSaved = rs.next();
@ -462,7 +461,7 @@ public class SqlDao extends AbstractDao {
if (hasPrimaryGroupSaved) {
// update
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_UPDATE_PRIMARY_GROUP))) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID))) {
ps.setString(1, user.getPrimaryGroup().getStoredValue().orElse(NodeFactory.DEFAULT_GROUP_NAME));
ps.setString(2, user.getUuid().toString());
ps.execute();
@ -772,24 +771,23 @@ public class SqlDao extends AbstractDao {
Track track = this.plugin.getTrackManager().getOrMake(name);
track.getIoLock().lock();
try {
AtomicBoolean exists = new AtomicBoolean(false);
AtomicReference<String> groups = new AtomicReference<>(null);
boolean exists = false;
String groups = null;
try (Connection c = this.provider.getConnection()) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(TRACK_SELECT))) {
ps.setString(1, track.getName());
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
exists.set(true);
groups.set(rs.getString("groups"));
exists = true;
groups = rs.getString("groups");
}
}
}
}
if (exists.get()) {
if (exists) {
// Track exists, let's load.
track.setGroups(this.gson.fromJson(groups.get(), LIST_STRING_TYPE));
track.setGroups(this.gson.fromJson(groups, LIST_STRING_TYPE));
} else {
String json = this.gson.toJson(track.getGroups());
try (Connection c = this.provider.getConnection()) {
@ -813,14 +811,13 @@ public class SqlDao extends AbstractDao {
track.getIoLock().lock();
}
try {
AtomicReference<String> groups = new AtomicReference<>(null);
String groups;
try (Connection c = this.provider.getConnection()) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(TRACK_SELECT))) {
ps.setString(1, name);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
groups.set(rs.getString("groups"));
groups = rs.getString("groups");
} else {
return Optional.empty();
}
@ -833,7 +830,7 @@ public class SqlDao extends AbstractDao {
track.getIoLock().lock();
}
track.setGroups(this.gson.fromJson(groups.get(), LIST_STRING_TYPE));
track.setGroups(this.gson.fromJson(groups, LIST_STRING_TYPE));
return Optional.of(track);
} finally {
@ -911,91 +908,91 @@ public class SqlDao extends AbstractDao {
}
@Override
public void saveUUIDData(UUID uuid, String username) throws SQLException {
final String u = username.toLowerCase();
AtomicReference<String> remoteUserName = new AtomicReference<>(null);
public PlayerSaveResult savePlayerData(UUID uuid, String username) throws SQLException {
username = username.toLowerCase();
// cleanup any old values
try (Connection c = this.provider.getConnection()) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_DELETE))) {
ps.setString(1, u);
ps.setString(2, uuid.toString());
ps.execute();
}
}
// find any existing mapping
String oldUsername = getPlayerName(uuid);
try (Connection c = this.provider.getConnection()) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_SELECT_USERNAME))) {
ps.setString(1, uuid.toString());
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
remoteUserName.set(rs.getString("username"));
// do the insert
if (!username.equalsIgnoreCase(oldUsername)) {
try (Connection c = this.provider.getConnection()) {
if (oldUsername != null) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_UPDATE_USERNAME_FOR_UUID))) {
ps.setString(1, username);
ps.setString(2, uuid.toString());
ps.execute();
}
} else {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_INSERT))) {
ps.setString(1, uuid.toString());
ps.setString(2, username);
ps.setString(3, NodeFactory.DEFAULT_GROUP_NAME);
ps.execute();
}
}
}
}
if (remoteUserName.get() != null) {
// the value is already correct
if (remoteUserName.get().equals(u)) {
return;
}
PlayerSaveResult result = PlayerSaveResult.determineBaseResult(username, oldUsername);
Set<UUID> conflicting = new HashSet<>();
try (Connection c = this.provider.getConnection()) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_SELECT_ALL_UUIDS_BY_USERNAME))) {
ps.setString(1, username);
ps.setString(2, uuid.toString());
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
conflicting.add(UUID.fromString(rs.getString("uuid")));
}
}
}
}
if (!conflicting.isEmpty()) {
// remove the mappings for conflicting uuids
try (Connection c = this.provider.getConnection()) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_UPDATE))) {
ps.setString(1, u);
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_DELETE_ALL_UUIDS_BY_USERNAME))) {
ps.setString(1, username);
ps.setString(2, uuid.toString());
ps.execute();
}
}
} else {
// first time we've seen this uuid
try (Connection c = this.provider.getConnection()) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_INSERT))) {
ps.setString(1, uuid.toString());
ps.setString(2, u);
ps.setString(3, NodeFactory.DEFAULT_GROUP_NAME);
ps.execute();
}
}
result = result.withOtherUuidsPresent(conflicting);
}
return result;
}
@Override
public UUID getUUID(String username) throws SQLException {
final String u = username.toLowerCase();
final AtomicReference<UUID> uuid = new AtomicReference<>(null);
public UUID getPlayerUuid(String username) throws SQLException {
username = username.toLowerCase();
try (Connection c = this.provider.getConnection()) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_SELECT_UUID))) {
ps.setString(1, u);
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_SELECT_UUID_BY_USERNAME))) {
ps.setString(1, username);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
uuid.set(UUID.fromString(rs.getString("uuid")));
return UUID.fromString(rs.getString("uuid"));
}
}
}
}
return uuid.get();
return null;
}
@Override
public String getName(UUID uuid) throws SQLException {
final AtomicReference<String> name = new AtomicReference<>(null);
public String getPlayerName(UUID uuid) throws SQLException {
try (Connection c = this.provider.getConnection()) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_SELECT_USERNAME))) {
try (PreparedStatement ps = c.prepareStatement(this.prefix.apply(PLAYER_SELECT_USERNAME_BY_UUID))) {
ps.setString(1, uuid.toString());
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
name.set(rs.getString("username"));
return rs.getString("username");
}
}
}
}
return name.get();
return null;
}
/**

View File

@ -41,7 +41,7 @@ public final class Uuids {
public static final Predicate<String> PREDICATE = s -> parseNullable(s) != null;
@Nullable
private static UUID fromString(String s) {
public static UUID fromString(String s) {
try {
return UUID.fromString(s);
} catch (IllegalArgumentException e) {