Rewrite MessagingService message encoding, conform to new plugin message channel requirements

* This change isn't backwards compatible with previous builds, this version (& onwards) won't be able to "communicate" with old versions, and vice versa.

* MC 1.13 requires plugin message channel names to be namespaced, so 'lpuc' has been changed to 'luckperms:update'

* The channel names for Redis/Lily types have also changed, I've been wanting to change the encoded format to something a bit saner for a while, and this seemed like a good time. Changing the channel names a) keeps things consistent and b) means I don't have to worry about old versions interpreting the new format.
This commit is contained in:
Luck 2018-07-15 22:42:37 -07:00
parent 08869da96e
commit 07469599bc
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
12 changed files with 238 additions and 235 deletions

View File

@ -47,7 +47,7 @@ import javax.annotation.Nonnull;
* An implementation of {@link Messenger} using the plugin messaging channels.
*/
public class BungeeMessenger implements Messenger, PluginMessageListener {
private static final String CHANNEL = "lpuc";
private static final String CHANNEL = "luckperms:update";
private final LPBukkitPlugin plugin;
private final IncomingMessageConsumer consumer;

View File

@ -45,7 +45,7 @@ import javax.annotation.Nonnull;
* An implementation of {@link Messenger} using LilyPad.
*/
public class LilyPadMessenger implements Messenger {
private static final String CHANNEL = "lpuc";
private static final String CHANNEL = "luckperms:update";
private final LPBukkitPlugin plugin;
private final IncomingMessageConsumer consumer;

View File

@ -46,7 +46,7 @@ import javax.annotation.Nonnull;
* An implementation of {@link Messenger} using the plugin messaging channels.
*/
public class BungeeMessenger implements Messenger, Listener {
private static final String CHANNEL = "lpuc";
private static final String CHANNEL = "luckperms:update";
private final LPBungeePlugin plugin;
private final IncomingMessageConsumer consumer;

View File

@ -43,7 +43,7 @@ import javax.annotation.Nonnull;
* An implementation of {@link Messenger} using Redis, via RedisBungee's API.
*/
public class RedisBungeeMessenger implements Messenger, Listener {
private static final String CHANNEL = "lpuc";
private static final String CHANNEL = "luckperms:update";
private final LPBungeePlugin plugin;
private final IncomingMessageConsumer consumer;

View File

@ -0,0 +1,72 @@
/*
* 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.actionlog;
import com.google.common.base.Preconditions;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import me.lucko.luckperms.api.LogEntry;
import java.util.UUID;
public final class LogEntryJsonSerializer {
public static JsonObject serialize(LogEntry logEntry) {
JsonObject data = new JsonObject();
data.add("actor", new JsonPrimitive(logEntry.getActor().toString()));
data.add("actorName", new JsonPrimitive(logEntry.getActorName()));
data.add("type", new JsonPrimitive(logEntry.getType().name()));
if (logEntry.getActed().isPresent()) {
data.add("acted", new JsonPrimitive(logEntry.getActed().get().toString()));
}
data.add("actedName", new JsonPrimitive(logEntry.getActedName()));
data.add("action", new JsonPrimitive(logEntry.getAction()));
return data;
}
public static ExtendedLogEntry deserialize(JsonElement element) {
Preconditions.checkArgument(element.isJsonObject());
JsonObject data = element.getAsJsonObject();
ExtendedLogEntry.Builder builder = ExtendedLogEntry.build();
builder.actor(UUID.fromString(data.get("actor").getAsString()));
builder.actorName(data.get("actorName").getAsString());
builder.type(LogEntry.Type.valueOf(data.get("type").getAsString()));
if (data.has("acted")) {
builder.actor(UUID.fromString(data.get("acted").getAsString()));
}
builder.actedName(data.get("actedName").getAsString());
builder.action(data.get("action").getAsString());
return builder.build();
}
private LogEntryJsonSerializer() {}
}

View File

@ -25,6 +25,11 @@
package me.lucko.luckperms.common.messaging;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import me.lucko.luckperms.api.LogEntry;
import me.lucko.luckperms.api.messenger.IncomingMessageConsumer;
import me.lucko.luckperms.api.messenger.Messenger;
@ -41,6 +46,7 @@ import me.lucko.luckperms.common.messaging.message.UpdateMessageImpl;
import me.lucko.luckperms.common.messaging.message.UserUpdateMessageImpl;
import me.lucko.luckperms.common.model.User;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.utils.gson.JObject;
import java.util.Collections;
import java.util.HashSet;
@ -50,8 +56,11 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class LuckPermsMessagingService implements InternalMessagingService, IncomingMessageConsumer {
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create();
private final LuckPermsPlugin plugin;
private final Set<UUID> receivedMessages;
private final PushUpdateBuffer updateBuffer;
@ -137,77 +146,127 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco
public boolean consumeIncomingMessage(@Nonnull Message message) {
Objects.requireNonNull(message, "message");
if (message instanceof UpdateMessage) {
UpdateMessage msg = (UpdateMessage) message;
if (!this.receivedMessages.add(msg.getId())) {
if (!this.receivedMessages.add(message.getId())) {
return false;
}
this.plugin.getLogger().info("[" + getName() + " Messaging] Received update ping with id: " + msg.getId());
// determine if the message can be handled by us
boolean valid = message instanceof UpdateMessage ||
message instanceof UserUpdateMessage ||
message instanceof LogMessage;
if (this.plugin.getEventFactory().handleNetworkPreSync(false, msg.getId())) {
return true;
}
this.plugin.getUpdateTaskBuffer().request();
return true;
} else if (message instanceof UserUpdateMessage) {
UserUpdateMessage msg = (UserUpdateMessage) message;
if (!this.receivedMessages.add(msg.getId())) {
// instead of throwing an exception here, just return false
// it means an instance of LP can gracefully handle messages it doesn't
// "understand" yet. (sent from an instance running a newer version, etc)
if (!valid) {
return false;
}
User user = this.plugin.getUserManager().getIfLoaded(msg.getUser());
if (user == null) {
processIncomingMessage(message);
return true;
}
this.plugin.getLogger().info("[" + getName() + " Messaging] Received user update ping for '" + user.getFriendlyName() + "' with id: " + msg.getId());
if (this.plugin.getEventFactory().handleNetworkPreSync(false, msg.getId())) {
return true;
}
this.plugin.getStorage().loadUser(user.getUuid(), null);
return true;
} else if (message instanceof LogMessage) {
LogMessage msg = (LogMessage) message;
if (!this.receivedMessages.add(msg.getId())) {
return false;
}
this.plugin.getEventFactory().handleLogReceive(msg.getId(), msg.getLogEntry());
this.plugin.getLogDispatcher().dispatchFromRemote((ExtendedLogEntry) msg.getLogEntry());
return true;
} else {
this.plugin.getLogger().warn("Unable to decode incoming message: " + message + " (" + message.getClass().getName() + ")");
return false;
}
}
@Override
public boolean consumeIncomingMessageAsString(@Nonnull String encodedString) {
Objects.requireNonNull(encodedString, "encodedString");
JsonObject decodedObject = GSON.fromJson(encodedString, JsonObject.class).getAsJsonObject();
Message decoded = UpdateMessageImpl.decode(encodedString);
if (decoded != null) {
return consumeIncomingMessage(decoded);
// extract id
JsonElement idElement = decodedObject.get("id");
if (idElement == null) {
throw new IllegalStateException("Incoming message has no id argument: " + encodedString);
}
UUID id = UUID.fromString(idElement.getAsString());
// ensure the message hasn't been received already
if (!this.receivedMessages.add(id)) {
return false;
}
decoded = UserUpdateMessageImpl.decode(encodedString);
if (decoded != null) {
return consumeIncomingMessage(decoded);
// extract type
JsonElement typeElement = decodedObject.get("type");
if (typeElement == null) {
throw new IllegalStateException("Incoming message has no type argument: " + encodedString);
}
String type = typeElement.getAsString();
// extract content
@Nullable JsonElement content = decodedObject.get("content");
// decode message
Message decoded;
switch (type) {
case UpdateMessageImpl.TYPE:
decoded = UpdateMessageImpl.decode(content, id);
break;
case UserUpdateMessageImpl.TYPE:
decoded = UserUpdateMessageImpl.decode(content, id);
break;
case LogMessageImpl.TYPE:
decoded = LogMessageImpl.decode(content, id);
break;
default:
// gracefully return if we just don't recognise the type
return false;
}
decoded = LogMessageImpl.decode(encodedString);
return decoded != null && consumeIncomingMessage(decoded);
// consume the message
processIncomingMessage(decoded);
return true;
}
public static String encodeMessageAsString(String type, UUID id, @Nullable JsonElement content) {
JsonObject json = new JObject()
.add("id", id.toString())
.add("type", type)
.consume(o -> {
if (content != null) {
o.add("content", content);
}
})
.toJson();
return GSON.toJson(json);
}
private void processIncomingMessage(Message message) {
if (message instanceof UpdateMessage) {
UpdateMessage msg = (UpdateMessage) message;
this.plugin.getLogger().info("[" + getName() + " Messaging] Received update ping with id: " + msg.getId());
if (this.plugin.getEventFactory().handleNetworkPreSync(false, msg.getId())) {
return;
}
this.plugin.getUpdateTaskBuffer().request();
} else if (message instanceof UserUpdateMessage) {
UserUpdateMessage msg = (UserUpdateMessage) message;
User user = this.plugin.getUserManager().getIfLoaded(msg.getUser());
if (user == null) {
return;
}
this.plugin.getLogger().info("[" + getName() + " Messaging] Received user update ping for '" + user.getFriendlyName() + "' with id: " + msg.getId());
if (this.plugin.getEventFactory().handleNetworkPreSync(false, msg.getId())) {
return;
}
this.plugin.getStorage().loadUser(user.getUuid(), null);
} else if (message instanceof LogMessage) {
LogMessage msg = (LogMessage) message;
this.plugin.getEventFactory().handleLogReceive(msg.getId(), msg.getLogEntry());
this.plugin.getLogDispatcher().dispatchFromRemote((ExtendedLogEntry) msg.getLogEntry());
} else {
throw new IllegalArgumentException("Unknown message type: " + message.getClass().getName());
}
}
private final class PushUpdateBuffer extends BufferedRequest<Void> {
public PushUpdateBuffer(LuckPermsPlugin plugin) {
PushUpdateBuffer(LuckPermsPlugin plugin) {
super(2, TimeUnit.SECONDS, plugin.getBootstrap().getScheduler());
}

View File

@ -25,37 +25,27 @@
package me.lucko.luckperms.common.messaging.message;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonElement;
import me.lucko.luckperms.api.LogEntry;
import me.lucko.luckperms.api.messenger.message.type.LogMessage;
import me.lucko.luckperms.common.actionlog.ExtendedLogEntry;
import me.lucko.luckperms.common.actionlog.LogEntryJsonSerializer;
import me.lucko.luckperms.common.messaging.LuckPermsMessagingService;
import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class LogMessageImpl extends AbstractMessage implements LogMessage {
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create();
private static final String LOG_HEADER = "log";
public static final String TYPE = "log";
public static LogMessageImpl decode(String msg) {
if (msg.startsWith(LOG_HEADER) && msg.length() > LOG_HEADER.length()) {
String content = msg.substring(LOG_HEADER.length());
try {
return decodeContent(GSON.fromJson(content, JsonObject.class));
} catch (Exception e) {
return null;
}
public static LogMessageImpl decode(@Nullable JsonElement content, UUID id) {
if (content == null) {
throw new IllegalStateException("Missing content");
}
return null;
return new LogMessageImpl(id, LogEntryJsonSerializer.deserialize(content));
}
private final LogEntry logEntry;
@ -74,65 +64,9 @@ public class LogMessageImpl extends AbstractMessage implements LogMessage {
@Nonnull
@Override
public String asEncodedString() {
return LOG_HEADER + GSON.toJson(encodeContent(uuidToString(getId()), this.logEntry));
}
private static String uuidToString(UUID uuid) {
ByteBuffer buf = ByteBuffer.allocate(Long.BYTES * 2);
buf.putLong(uuid.getMostSignificantBits());
buf.putLong(uuid.getLeastSignificantBits());
return Base64.getEncoder().encodeToString(buf.array());
}
private static UUID uuidFromString(String s) {
try {
byte[] bytes = Base64.getDecoder().decode(s);
ByteBuffer buf = ByteBuffer.wrap(bytes);
return new UUID(buf.getLong(), buf.getLong());
} catch (IllegalArgumentException e) {
return null;
}
}
private static JsonObject encodeContent(String id, LogEntry entry) {
JsonObject data = new JsonObject();
data.add("id", new JsonPrimitive(id));
data.add("actor", new JsonPrimitive(entry.getActor().toString()));
data.add("actorName", new JsonPrimitive(entry.getActorName()));
data.add("type", new JsonPrimitive(entry.getType().name()));
if (entry.getActed().isPresent()) {
data.add("acted", new JsonPrimitive(entry.getActed().get().toString()));
}
data.add("actedName", new JsonPrimitive(entry.getActedName()));
data.add("action", new JsonPrimitive(entry.getAction()));
return data;
}
private static LogMessageImpl decodeContent(JsonObject object) {
ExtendedLogEntry.Builder builder = ExtendedLogEntry.build();
String id = object.get("id").getAsString();
if (id == null) {
return null;
}
UUID uuid = uuidFromString(id);
if (uuid == null) {
return null;
}
builder.actor(UUID.fromString(object.get("actor").getAsString()));
builder.actorName(object.get("actorName").getAsString());
builder.type(LogEntry.Type.valueOf(object.get("type").getAsString()));
if (object.has("acted")) {
builder.actor(UUID.fromString(object.get("acted").getAsString()));
}
builder.actedName(object.get("actedName").getAsString());
builder.action(object.get("action").getAsString());
return new LogMessageImpl(uuid, builder.build());
return LuckPermsMessagingService.encodeMessageAsString(
TYPE, getId(), LogEntryJsonSerializer.serialize(logEntry)
);
}
}

View File

@ -25,24 +25,21 @@
package me.lucko.luckperms.common.messaging.message;
import me.lucko.luckperms.api.messenger.message.type.UpdateMessage;
import com.google.gson.JsonElement;
import me.lucko.luckperms.api.messenger.message.type.UpdateMessage;
import me.lucko.luckperms.common.messaging.LuckPermsMessagingService;
import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class UpdateMessageImpl extends AbstractMessage implements UpdateMessage {
private static final String UPDATE_HEADER = "update:";
public static final String TYPE = "update";
public static UpdateMessageImpl decode(String msg) {
if (msg.startsWith(UPDATE_HEADER) && msg.length() > UPDATE_HEADER.length()) {
String content = msg.substring(UPDATE_HEADER.length());
return decodeContent(content);
}
return null;
public static UpdateMessageImpl decode(@Nullable JsonElement content, UUID id) {
return new UpdateMessageImpl(id);
}
public UpdateMessageImpl(UUID id) {
@ -52,23 +49,6 @@ public class UpdateMessageImpl extends AbstractMessage implements UpdateMessage
@Nonnull
@Override
public String asEncodedString() {
return UPDATE_HEADER + encodeContent(getId());
}
private static String encodeContent(UUID uuid) {
ByteBuffer buf = ByteBuffer.allocate(Long.BYTES * 2);
buf.putLong(uuid.getMostSignificantBits());
buf.putLong(uuid.getLeastSignificantBits());
return Base64.getEncoder().encodeToString(buf.array());
}
private static UpdateMessageImpl decodeContent(String s) {
try {
byte[] bytes = Base64.getDecoder().decode(s);
ByteBuffer buf = ByteBuffer.wrap(bytes);
return new UpdateMessageImpl(new UUID(buf.getLong(), buf.getLong()));
} catch (IllegalArgumentException e) {
return null;
}
return LuckPermsMessagingService.encodeMessageAsString(TYPE, getId(), null);
}
}

View File

@ -25,24 +25,33 @@
package me.lucko.luckperms.common.messaging.message;
import me.lucko.luckperms.api.messenger.message.type.UserUpdateMessage;
import com.google.gson.JsonElement;
import me.lucko.luckperms.api.messenger.message.type.UserUpdateMessage;
import me.lucko.luckperms.common.messaging.LuckPermsMessagingService;
import me.lucko.luckperms.common.utils.gson.JObject;
import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class UserUpdateMessageImpl extends AbstractMessage implements UserUpdateMessage {
private static final String USER_UPDATE_HEADER = "userupdate:";
public static final String TYPE = "userupdate";
public static UserUpdateMessageImpl decode(String msg) {
if (msg.startsWith(USER_UPDATE_HEADER) && msg.length() > USER_UPDATE_HEADER.length()) {
String content = msg.substring(USER_UPDATE_HEADER.length());
return decodeContent(content);
public static UserUpdateMessageImpl decode(@Nullable JsonElement content, UUID id) {
if (content == null) {
throw new IllegalStateException("Missing content");
}
return null;
// extract user uuid
JsonElement uuidElement = content.getAsJsonObject().get("userUuid");
if (uuidElement == null) {
throw new IllegalStateException("Incoming message has no userUuid argument: " + content);
}
UUID userUuid = UUID.fromString(uuidElement.getAsString());
return new UserUpdateMessageImpl(id, userUuid);
}
private final UUID userUuid;
@ -61,27 +70,8 @@ public class UserUpdateMessageImpl extends AbstractMessage implements UserUpdate
@Nonnull
@Override
public String asEncodedString() {
return USER_UPDATE_HEADER + encodeContent(getId(), this.userUuid);
}
private static String encodeContent(UUID id, UUID userUuid) {
ByteBuffer buf = ByteBuffer.allocate(Long.BYTES * 4);
buf.putLong(id.getMostSignificantBits());
buf.putLong(id.getLeastSignificantBits());
buf.putLong(userUuid.getMostSignificantBits());
buf.putLong(userUuid.getLeastSignificantBits());
return Base64.getEncoder().encodeToString(buf.array());
}
private static UserUpdateMessageImpl decodeContent(String s) {
try {
byte[] bytes = Base64.getDecoder().decode(s);
ByteBuffer buf = ByteBuffer.wrap(bytes);
UUID id = new UUID(buf.getLong(), buf.getLong());
UUID userUuid = new UUID(buf.getLong(), buf.getLong());
return new UserUpdateMessageImpl(id, userUuid);
} catch (IllegalArgumentException e) {
return null;
}
return LuckPermsMessagingService.encodeMessageAsString(
TYPE, getId(), new JObject().add("userUuid", userUuid.toString()).toJson()
);
}
}

View File

@ -41,7 +41,7 @@ import javax.annotation.Nonnull;
* An implementation of {@link Messenger} using Redis.
*/
public class RedisMessenger implements Messenger {
private static final String CHANNEL = "lpuc";
private static final String CHANNEL = "luckperms:update";
private final LuckPermsPlugin plugin;
private final IncomingMessageConsumer consumer;

View File

@ -28,24 +28,21 @@ package me.lucko.luckperms.common.storage.dao.file;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import me.lucko.luckperms.api.LogEntry;
import me.lucko.luckperms.common.actionlog.ExtendedLogEntry;
import me.lucko.luckperms.common.actionlog.Log;
import me.lucko.luckperms.common.actionlog.LogEntryJsonSerializer;
import me.lucko.luckperms.common.buffers.BufferedRequest;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.utils.gson.JObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@ -109,19 +106,7 @@ public class FileActionLogger {
// poll the queue for new entries
for (LogEntry e; (e = this.entryQueue.poll()) != null; ) {
JObject object = new JObject()
.add("timestamp", e.getTimestamp())
.add("actor", e.getActor().toString())
.add("actorName", e.getActorName())
.add("type", Character.toString(e.getType().getCode()))
.add("actedName", e.getActedName())
.add("action", e.getAction());
if (e.getActed().isPresent()) {
object.add("acted", e.getActed().get().toString());
}
array.add(object.toJson());
array.add(LogEntryJsonSerializer.serialize(e));
}
// write the full content back to the file
@ -142,24 +127,7 @@ public class FileActionLogger {
try (JsonReader reader = new JsonReader(Files.newBufferedReader(this.contentFile, StandardCharsets.UTF_8))) {
JsonArray array = JSON_PARSER.parse(reader).getAsJsonArray();
for (JsonElement element : array) {
JsonObject object = element.getAsJsonObject();
UUID actedUuid = null;
if (object.has("acted")) {
actedUuid = UUID.fromString(object.get("acted").getAsString());
}
ExtendedLogEntry e = ExtendedLogEntry.build()
.timestamp(object.get("timestamp").getAsLong())
.actor(UUID.fromString(object.get("actor").getAsString()))
.actorName(object.get("actorName").getAsString())
.type(LogEntry.Type.valueOf(object.get("type").getAsCharacter()))
.acted(actedUuid)
.actedName(object.get("actedName").getAsString())
.action(object.get("action").getAsString())
.build();
log.add(e);
log.add(LogEntryJsonSerializer.deserialize(element));
}
}
return log.build();

View File

@ -48,7 +48,7 @@ import javax.annotation.Nonnull;
* An implementation of {@link Messenger} using the plugin messaging channels.
*/
public class BungeeMessenger implements Messenger, RawDataListener {
private static final String CHANNEL = "lpuc";
private static final String CHANNEL = "luckperms:update";
private final LPSpongePlugin plugin;
private final IncomingMessageConsumer consumer;