mirror of
https://github.com/LuckPerms/LuckPerms.git
synced 2024-12-28 20:17:55 +01:00
Web editor socket connection (#3303)
This commit is contained in:
parent
f61d9ff9f0
commit
d3029a8467
@ -57,6 +57,9 @@ luckperms {
|
||||
applyedits {
|
||||
code brigadier:string single_word;
|
||||
}
|
||||
trusteditor {
|
||||
id brigadier:string single_word;
|
||||
}
|
||||
creategroup {
|
||||
name brigadier:string single_word {
|
||||
weight brigadier:integer {
|
||||
|
@ -51,6 +51,7 @@ import me.lucko.luckperms.common.commands.misc.SearchCommand;
|
||||
import me.lucko.luckperms.common.commands.misc.SyncCommand;
|
||||
import me.lucko.luckperms.common.commands.misc.TranslationsCommand;
|
||||
import me.lucko.luckperms.common.commands.misc.TreeCommand;
|
||||
import me.lucko.luckperms.common.commands.misc.TrustEditorCommand;
|
||||
import me.lucko.luckperms.common.commands.misc.VerboseCommand;
|
||||
import me.lucko.luckperms.common.commands.track.CreateTrack;
|
||||
import me.lucko.luckperms.common.commands.track.DeleteTrack;
|
||||
@ -123,6 +124,7 @@ public class CommandManager {
|
||||
.add(new BulkUpdateCommand())
|
||||
.add(new TranslationsCommand())
|
||||
.add(new ApplyEditsCommand())
|
||||
.add(new TrustEditorCommand())
|
||||
.add(new CreateGroup())
|
||||
.add(new DeleteGroup())
|
||||
.add(new ListGroups())
|
||||
|
@ -44,6 +44,7 @@ public enum CommandPermission {
|
||||
RELOAD_CONFIG("reloadconfig", Type.NONE),
|
||||
BULK_UPDATE("bulkupdate", Type.NONE),
|
||||
APPLY_EDITS("applyedits", Type.NONE),
|
||||
TRUST_EDITOR("trusteditor", Type.NONE),
|
||||
TRANSLATIONS("translations", Type.NONE),
|
||||
|
||||
CREATE_GROUP("creategroup", Type.NONE),
|
||||
|
@ -86,9 +86,11 @@ public enum CommandSpec {
|
||||
TRANSLATIONS("/%s translations",
|
||||
arg("install", false)
|
||||
),
|
||||
APPLY_EDITS("/%s applyedits <code> [target]",
|
||||
arg("code", true),
|
||||
arg("target", false)
|
||||
APPLY_EDITS("/%s applyedits <code>",
|
||||
arg("code", true)
|
||||
),
|
||||
TRUST_EDITOR("/%s trusteditor <id>",
|
||||
arg("id", true)
|
||||
),
|
||||
|
||||
CREATE_GROUP("/%s creategroup <group>",
|
||||
|
@ -42,6 +42,7 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||
import me.lucko.luckperms.common.sender.Sender;
|
||||
import me.lucko.luckperms.common.util.Predicates;
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorRequest;
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorSession;
|
||||
|
||||
import net.luckperms.api.node.Node;
|
||||
|
||||
@ -84,8 +85,7 @@ public class HolderEditor<T extends PermissionHolder> extends ChildCommand<T> {
|
||||
|
||||
Message.EDITOR_START.send(sender);
|
||||
|
||||
WebEditorRequest.generate(holders, Collections.emptyList(), sender, label, plugin)
|
||||
.createSession(plugin, sender);
|
||||
WebEditorSession.createAndOpen(holders, Collections.emptyList(), sender, label, plugin);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ public class ApplyEditsCommand extends SingleCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
new WebEditorResponse(code, data).apply(plugin, sender, label, ignoreSessionWarning);
|
||||
new WebEditorResponse(code, data).apply(plugin, sender, null, label, ignoreSessionWarning);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -40,6 +40,7 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||
import me.lucko.luckperms.common.sender.Sender;
|
||||
import me.lucko.luckperms.common.util.Predicates;
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorRequest;
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorSession;
|
||||
|
||||
import net.luckperms.api.node.Node;
|
||||
|
||||
@ -107,8 +108,7 @@ public class EditorCommand extends SingleCommand {
|
||||
|
||||
Message.EDITOR_START.send(sender);
|
||||
|
||||
WebEditorRequest.generate(holders, tracks, sender, label, plugin)
|
||||
.createSession(plugin, sender);
|
||||
WebEditorSession.createAndOpen(holders, tracks, sender, label, plugin);
|
||||
}
|
||||
|
||||
private enum Type {
|
||||
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.commands.misc;
|
||||
|
||||
import me.lucko.luckperms.common.command.abstraction.SingleCommand;
|
||||
import me.lucko.luckperms.common.command.access.CommandPermission;
|
||||
import me.lucko.luckperms.common.command.spec.CommandSpec;
|
||||
import me.lucko.luckperms.common.command.utils.ArgumentList;
|
||||
import me.lucko.luckperms.common.locale.Message;
|
||||
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||
import me.lucko.luckperms.common.sender.Sender;
|
||||
import me.lucko.luckperms.common.util.Predicates;
|
||||
import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket;
|
||||
|
||||
public class TrustEditorCommand extends SingleCommand {
|
||||
public TrustEditorCommand() {
|
||||
super(CommandSpec.TRUST_EDITOR, "TrustEditor", CommandPermission.TRUST_EDITOR, Predicates.not(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, String label) {
|
||||
String id = args.get(0);
|
||||
|
||||
if (id.isEmpty()) {
|
||||
Message.APPLY_EDITS_INVALID_CODE.send(sender, id);
|
||||
return;
|
||||
}
|
||||
|
||||
WebEditorSocket socket = plugin.getWebEditorStore().sockets().getSocket(sender);
|
||||
if (socket == null) {
|
||||
Message.EDITOR_SOCKET_TRUST_FAILURE.send(sender);
|
||||
return;
|
||||
}
|
||||
|
||||
if (socket.trustConnection(id)) {
|
||||
Message.EDITOR_SOCKET_TRUST_SUCCESS.send(sender);
|
||||
} else {
|
||||
Message.EDITOR_SOCKET_TRUST_FAILURE.send(sender);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDisplay() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -42,6 +42,7 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||
import me.lucko.luckperms.common.sender.Sender;
|
||||
import me.lucko.luckperms.common.util.Predicates;
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorRequest;
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorSession;
|
||||
|
||||
import net.luckperms.api.node.Node;
|
||||
|
||||
@ -97,8 +98,7 @@ public class TrackEditor extends ChildCommand<Track> {
|
||||
|
||||
Message.EDITOR_START.send(sender);
|
||||
|
||||
WebEditorRequest.generate(holders, Collections.singletonList(target), sender, label, plugin)
|
||||
.createSession(plugin, sender);
|
||||
WebEditorSession.createAndOpen(holders, Collections.singletonList(target), sender, label, plugin);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -692,7 +692,12 @@ public final class ConfigKeys {
|
||||
/**
|
||||
* The URL of the bytebin instance used to upload data
|
||||
*/
|
||||
public static final ConfigKey<String> BYTEBIN_URL = stringKey("bytebin-url", "https://bytebin.lucko.me/");
|
||||
public static final ConfigKey<String> BYTEBIN_URL = stringKey("bytebin-url", "https://usercontent.luckperms.net/");
|
||||
|
||||
/**
|
||||
* The host of the bytesocks instance used to communicate with
|
||||
*/
|
||||
public static final ConfigKey<String> BYTESOCKS_HOST = stringKey("bytesocks-host", "usersockets.luckperms.net");
|
||||
|
||||
/**
|
||||
* The URL of the web editor
|
||||
|
@ -40,8 +40,8 @@ import me.lucko.luckperms.common.model.HolderType;
|
||||
import me.lucko.luckperms.common.model.PermissionHolder;
|
||||
import me.lucko.luckperms.common.model.Track;
|
||||
import me.lucko.luckperms.common.model.User;
|
||||
import me.lucko.luckperms.common.model.nodemap.MutateResult;
|
||||
import me.lucko.luckperms.common.sender.Sender;
|
||||
import me.lucko.luckperms.common.util.Difference;
|
||||
|
||||
import net.luckperms.api.actionlog.Action;
|
||||
import net.luckperms.api.event.LuckPermsEvent;
|
||||
@ -226,7 +226,7 @@ public final class EventDispatcher {
|
||||
postAsync(LogReceiveEvent.class, id, entry);
|
||||
}
|
||||
|
||||
public void dispatchNodeChanges(PermissionHolder target, DataType dataType, MutateResult changes) {
|
||||
public void dispatchNodeChanges(PermissionHolder target, DataType dataType, Difference<Node> changes) {
|
||||
if (!this.eventBus.shouldPost(NodeAddEvent.class) && !this.eventBus.shouldPost(NodeRemoveEvent.class)) {
|
||||
return;
|
||||
}
|
||||
@ -239,15 +239,15 @@ public final class EventDispatcher {
|
||||
ImmutableSet<Node> state = target.getData(dataType).asImmutableSet();
|
||||
|
||||
// call an event for each recorded change
|
||||
for (MutateResult.Change change : changes.getChanges()) {
|
||||
Class<? extends NodeMutateEvent> type = change.getType() == MutateResult.ChangeType.ADD ?
|
||||
for (Difference.Change<Node> change : changes.getChanges()) {
|
||||
Class<? extends NodeMutateEvent> type = change.type() == Difference.ChangeType.ADD ?
|
||||
NodeAddEvent.class : NodeRemoveEvent.class;
|
||||
|
||||
postAsync(type, proxy, dataType, state, change.getNode());
|
||||
postAsync(type, proxy, dataType, state, change.value());
|
||||
}
|
||||
}
|
||||
|
||||
public void dispatchNodeClear(PermissionHolder target, DataType dataType, MutateResult changes) {
|
||||
public void dispatchNodeClear(PermissionHolder target, DataType dataType, Difference<Node> changes) {
|
||||
if (!this.eventBus.shouldPost(NodeClearEvent.class)) {
|
||||
return;
|
||||
}
|
||||
|
@ -83,15 +83,21 @@ public class BytebinClient extends AbstractHttpClient {
|
||||
*
|
||||
* @param buf the compressed content
|
||||
* @param contentType the type of the content
|
||||
* @param userAgentExtra extra string to append to the user agent
|
||||
* @return the key of the resultant content
|
||||
* @throws IOException if an error occurs
|
||||
*/
|
||||
public Content postContent(byte[] buf, MediaType contentType) throws IOException, UnsuccessfulRequestException {
|
||||
public Content postContent(byte[] buf, MediaType contentType, String userAgentExtra) throws IOException, UnsuccessfulRequestException {
|
||||
RequestBody body = RequestBody.create(contentType, buf);
|
||||
|
||||
String userAgent = this.userAgent;
|
||||
if (userAgentExtra != null) {
|
||||
userAgent += "/" + userAgentExtra;
|
||||
}
|
||||
|
||||
Request.Builder requestBuilder = new Request.Builder()
|
||||
.url(this.url + "post")
|
||||
.header("User-Agent", this.userAgent)
|
||||
.header("User-Agent", userAgent)
|
||||
.header("Content-Encoding", "gzip");
|
||||
|
||||
Request request = requestBuilder.post(body).build();
|
||||
@ -104,6 +110,10 @@ public class BytebinClient extends AbstractHttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
public Content postContent(byte[] buf, MediaType contentType) throws IOException, UnsuccessfulRequestException {
|
||||
return postContent(buf, contentType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* GETs json content from bytebin
|
||||
*
|
||||
|
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.http;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BytesocksClient extends AbstractHttpClient {
|
||||
|
||||
/* The bytesocks urls */
|
||||
private final String httpUrl;
|
||||
private final String wsUrl;
|
||||
|
||||
/** The client user agent */
|
||||
private final String userAgent;
|
||||
|
||||
/**
|
||||
* Creates a new bytesocks instance
|
||||
*
|
||||
* @param host the bytesocks host
|
||||
* @param userAgent the client user agent string
|
||||
*/
|
||||
public BytesocksClient(OkHttpClient okHttpClient, String host, String userAgent) {
|
||||
super(okHttpClient);
|
||||
|
||||
this.httpUrl = "https://" + host + "/";
|
||||
this.wsUrl = "wss://" + host + "/";
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
public Socket createSocket(WebSocketListener listener) throws IOException, UnsuccessfulRequestException {
|
||||
Request createRequest = new Request.Builder()
|
||||
.url(this.httpUrl + "create")
|
||||
.header("User-Agent", this.userAgent)
|
||||
.build();
|
||||
|
||||
String id;
|
||||
try (Response response = makeHttpRequest(createRequest)) {
|
||||
if (response.code() != 201) {
|
||||
throw new UnsuccessfulRequestException(response);
|
||||
}
|
||||
|
||||
id = Objects.requireNonNull(response.header("Location"));
|
||||
}
|
||||
|
||||
Request socketRequest = new Request.Builder()
|
||||
.url(this.wsUrl + id)
|
||||
.header("User-Agent", this.userAgent)
|
||||
.build();
|
||||
|
||||
return new Socket(id, this.okHttp.newWebSocket(socketRequest, listener));
|
||||
}
|
||||
|
||||
public static final class Socket {
|
||||
private final String channelId;
|
||||
private final WebSocket socket;
|
||||
|
||||
public Socket(String channelId, WebSocket socket) {
|
||||
this.channelId = channelId;
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public String channelId() {
|
||||
return this.channelId;
|
||||
}
|
||||
|
||||
public WebSocket socket() {
|
||||
return this.socket;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1159,6 +1159,97 @@ public interface Message {
|
||||
.clickEvent(ClickEvent.openUrl(url))
|
||||
);
|
||||
|
||||
Args0 EDITOR_SOCKET_CONNECTED = () -> prefixed(translatable()
|
||||
// "&bEditor window connected successfully."
|
||||
.color(AQUA)
|
||||
.key("luckperms.command.editor.socket.connected")
|
||||
.append(FULL_STOP)
|
||||
);
|
||||
|
||||
Args0 EDITOR_SOCKET_RECONNECTED = () -> prefixed(translatable()
|
||||
// "&7Editor window reconnected successfully."
|
||||
.color(GRAY)
|
||||
.key("luckperms.command.editor.socket.reconnected")
|
||||
.append(FULL_STOP)
|
||||
);
|
||||
|
||||
Args0 EDITOR_SOCKET_CHANGES_RECEIVED = () -> prefixed(translatable()
|
||||
// "&7Changes have been received from the connected web editor session."
|
||||
.color(GRAY)
|
||||
.key("luckperms.command.editor.socket.changes-received")
|
||||
.append(FULL_STOP)
|
||||
);
|
||||
|
||||
Args4<String, String, String, Boolean> EDITOR_SOCKET_UNTRUSTED = (nonce, browser, cmdLabel, console) -> join(newline(),
|
||||
// "&bAn editor window has connected, but it is not yet trusted."
|
||||
// "&8(&7session id = &faaaaa&7, browser = &fChrome on Windows 10&8)"
|
||||
// "&7If it was you, &aclick here&7 to trust the session!"
|
||||
// "&7If it was you, run &a/lp trusteditor aaaaa&7 to trust the session!"
|
||||
prefixed(translatable()
|
||||
.key("luckperms.command.editor.socket.untrusted")
|
||||
.color(AQUA)
|
||||
.append(FULL_STOP)),
|
||||
prefixed(text()
|
||||
.color(DARK_GRAY)
|
||||
.append(OPEN_BRACKET)
|
||||
.append(translatable()
|
||||
.key("luckperms.command.editor.socket.untrusted.sessioninfo")
|
||||
.color(GRAY)
|
||||
.args(
|
||||
text(nonce, WHITE),
|
||||
text(browser, WHITE)
|
||||
)
|
||||
)
|
||||
.append(CLOSE_BRACKET)
|
||||
),
|
||||
prefixed(text()
|
||||
.color(GRAY)
|
||||
.apply(builder -> {
|
||||
String command = "/" + cmdLabel + " trusteditor " + nonce;
|
||||
if (console) {
|
||||
builder.append(translatable()
|
||||
.key("luckperms.command.editor.socket.untrusted.prompt.runcommand")
|
||||
.args(text(command, GREEN))
|
||||
.build()
|
||||
);
|
||||
} else {
|
||||
builder.append(translatable()
|
||||
.key("luckperms.command.editor.socket.untrusted.prompt.click")
|
||||
.args(translatable()
|
||||
.key("luckperms.command.editor.socket.untrusted.prompt.click.action")
|
||||
.color(GREEN)
|
||||
.clickEvent(ClickEvent.runCommand(command))
|
||||
)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}))
|
||||
);
|
||||
|
||||
Args0 EDITOR_SOCKET_TRUST_SUCCESS = () -> join(newline(),
|
||||
// "&aThe editor session has been marked as trusted."
|
||||
// "&7In the future, connections from the same browser will be trusted automatically."
|
||||
// "&7The plugin will now attempt to establish a connection with the editor..."
|
||||
prefixed(translatable()
|
||||
.key("luckperms.command.editor.socket.trust.success")
|
||||
.color(GREEN)
|
||||
.append(FULL_STOP)),
|
||||
prefixed(translatable()
|
||||
.key("luckperms.command.editor.socket.trust.futureinfo")
|
||||
.color(GRAY)
|
||||
.append(FULL_STOP)),
|
||||
prefixed(translatable()
|
||||
.key("luckperms.command.editor.socket.trust.connecting")
|
||||
.color(GRAY))
|
||||
);
|
||||
|
||||
Args0 EDITOR_SOCKET_TRUST_FAILURE = () -> prefixed(translatable()
|
||||
// "&cUnable to trust the given session because the socket is closed, or because a different connection was established instead."
|
||||
.color(RED)
|
||||
.key("luckperms.command.editor.socket.trust.failure")
|
||||
.append(FULL_STOP)
|
||||
);
|
||||
|
||||
Args2<Integer, String> EDITOR_HTTP_REQUEST_FAILURE = (code, message) -> prefixed(text()
|
||||
// "&cUnable to communicate with the editor. (response code &4{}&c, message='{}')"
|
||||
.color(RED)
|
||||
|
@ -31,7 +31,6 @@ import me.lucko.luckperms.common.cacheddata.HolderCachedDataManager;
|
||||
import me.lucko.luckperms.common.cacheddata.type.MetaAccumulator;
|
||||
import me.lucko.luckperms.common.inheritance.InheritanceComparator;
|
||||
import me.lucko.luckperms.common.inheritance.InheritanceGraph;
|
||||
import me.lucko.luckperms.common.model.nodemap.MutateResult;
|
||||
import me.lucko.luckperms.common.model.nodemap.NodeMap;
|
||||
import me.lucko.luckperms.common.model.nodemap.NodeMapMutable;
|
||||
import me.lucko.luckperms.common.model.nodemap.RecordedNodeMap;
|
||||
@ -39,6 +38,7 @@ import me.lucko.luckperms.common.node.NodeEquality;
|
||||
import me.lucko.luckperms.common.node.comparator.NodeWithContextComparator;
|
||||
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||
import me.lucko.luckperms.common.query.DataSelector;
|
||||
import me.lucko.luckperms.common.util.Difference;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.luckperms.api.context.ContextSet;
|
||||
@ -223,8 +223,17 @@ public abstract class PermissionHolder {
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
public MutateResult setNodes(DataType type, Iterable<? extends Node> set, boolean callEvent) {
|
||||
MutateResult res = getData(type).setContent(set);
|
||||
public Difference<Node> setNodes(DataType type, Iterable<? extends Node> set, boolean callEvent) {
|
||||
Difference<Node> res = getData(type).setContent(set);
|
||||
invalidateCache();
|
||||
if (callEvent) {
|
||||
getPlugin().getEventDispatcher().dispatchNodeChanges(this, type, res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public Difference<Node> setNodes(DataType type, Difference<Node> changes, boolean callEvent) {
|
||||
Difference<Node> res = getData(type).applyChanges(changes);
|
||||
invalidateCache();
|
||||
if (callEvent) {
|
||||
getPlugin().getEventDispatcher().dispatchNodeChanges(this, type, res);
|
||||
@ -420,7 +429,7 @@ public abstract class PermissionHolder {
|
||||
}
|
||||
|
||||
private boolean auditTemporaryNodes(DataType dataType) {
|
||||
MutateResult result = getData(dataType).removeIf(Node::hasExpired);
|
||||
Difference<Node> result = getData(dataType).removeIf(Node::hasExpired);
|
||||
if (!result.isEmpty()) {
|
||||
invalidateCache();
|
||||
}
|
||||
@ -454,7 +463,7 @@ public abstract class PermissionHolder {
|
||||
return DataMutateResult.FAIL_ALREADY_HAS;
|
||||
}
|
||||
|
||||
MutateResult changes = getData(dataType).add(node);
|
||||
Difference<Node> changes = getData(dataType).add(node);
|
||||
invalidateCache();
|
||||
if (callEvent) {
|
||||
this.plugin.getEventDispatcher().dispatchNodeChanges(this, dataType, changes);
|
||||
@ -491,7 +500,7 @@ public abstract class PermissionHolder {
|
||||
|
||||
if (newNode != null) {
|
||||
// Remove the old Node & add the new one.
|
||||
MutateResult changes = data.removeThenAdd(otherMatch, newNode);
|
||||
Difference<Node> changes = data.removeThenAdd(otherMatch, newNode);
|
||||
invalidateCache();
|
||||
this.plugin.getEventDispatcher().dispatchNodeChanges(this, dataType, changes);
|
||||
|
||||
@ -509,7 +518,7 @@ public abstract class PermissionHolder {
|
||||
return DataMutateResult.FAIL_LACKS;
|
||||
}
|
||||
|
||||
MutateResult changes = getData(dataType).remove(node);
|
||||
Difference<Node> changes = getData(dataType).remove(node);
|
||||
invalidateCache();
|
||||
this.plugin.getEventDispatcher().dispatchNodeChanges(this, dataType, changes);
|
||||
|
||||
@ -531,7 +540,7 @@ public abstract class PermissionHolder {
|
||||
Node newNode = node.toBuilder().expiry(newExpiry).build();
|
||||
|
||||
// Remove the old Node & add the new one.
|
||||
MutateResult changes = data.removeThenAdd(otherMatch, newNode);
|
||||
Difference<Node> changes = data.removeThenAdd(otherMatch, newNode);
|
||||
invalidateCache();
|
||||
this.plugin.getEventDispatcher().dispatchNodeChanges(this, dataType, changes);
|
||||
|
||||
@ -545,7 +554,7 @@ public abstract class PermissionHolder {
|
||||
}
|
||||
|
||||
public boolean removeIf(DataType dataType, @Nullable ContextSet contextSet, Predicate<? super Node> predicate, boolean giveDefault) {
|
||||
MutateResult changes;
|
||||
Difference<Node> changes;
|
||||
if (contextSet == null) {
|
||||
changes = getData(dataType).removeIf(predicate);
|
||||
} else {
|
||||
@ -566,7 +575,7 @@ public abstract class PermissionHolder {
|
||||
}
|
||||
|
||||
public boolean clearNodes(DataType dataType, ContextSet contextSet, boolean giveDefault) {
|
||||
MutateResult changes;
|
||||
Difference<Node> changes;
|
||||
if (contextSet == null) {
|
||||
changes = getData(dataType).clear();
|
||||
} else {
|
||||
|
@ -1,152 +0,0 @@
|
||||
/*
|
||||
* 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.model.nodemap;
|
||||
|
||||
import net.luckperms.api.node.Node;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Records a log of the changes that occur as a result of a {@link NodeMap} mutation(s).
|
||||
*/
|
||||
public class MutateResult {
|
||||
private final LinkedHashSet<Change> changes = new LinkedHashSet<>();
|
||||
|
||||
public Set<Change> getChanges() {
|
||||
return this.changes;
|
||||
}
|
||||
|
||||
public Set<Node> getChanges(ChangeType type) {
|
||||
Set<Node> changes = new LinkedHashSet<>(this.changes.size());
|
||||
for (Change change : this.changes) {
|
||||
if (change.getType() == type) {
|
||||
changes.add(change.getNode());
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
this.changes.clear();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return this.changes.isEmpty();
|
||||
}
|
||||
|
||||
public Set<Node> getAdded() {
|
||||
return getChanges(ChangeType.ADD);
|
||||
}
|
||||
|
||||
public Set<Node> getRemoved() {
|
||||
return getChanges(ChangeType.REMOVE);
|
||||
}
|
||||
|
||||
private void recordChange(Change change) {
|
||||
// This method is the magic of this class.
|
||||
// When tracking, we want to ignore changes that cancel each other out, and only
|
||||
// keep track of the net difference.
|
||||
// e.g. adding then removing the same node = zero net change, so ignore it.
|
||||
|
||||
if (this.changes.remove(change.inverse())) {
|
||||
return;
|
||||
}
|
||||
this.changes.add(change);
|
||||
}
|
||||
|
||||
public void recordChange(ChangeType type, Node node) {
|
||||
recordChange(new Change(type, node));
|
||||
}
|
||||
|
||||
public void recordChanges(ChangeType type, Iterable<Node> nodes) {
|
||||
for (Node node : nodes) {
|
||||
recordChange(new Change(type, node));
|
||||
}
|
||||
}
|
||||
|
||||
public MutateResult mergeFrom(MutateResult other) {
|
||||
for (Change change : other.changes) {
|
||||
recordChange(change);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MutateResult{changes=" + this.changes + '}';
|
||||
}
|
||||
|
||||
public static final class Change {
|
||||
private final ChangeType type;
|
||||
private final Node node;
|
||||
|
||||
public Change(ChangeType type, Node node) {
|
||||
this.type = type;
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
public ChangeType getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public Node getNode() {
|
||||
return this.node;
|
||||
}
|
||||
|
||||
public Change inverse() {
|
||||
return new Change(this.type.inverse(), this.node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Change change = (Change) o;
|
||||
return this.type == change.type && this.node.equals(change.node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.type, this.node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Change{type=" + this.type + ", node=" + this.node + '}';
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChangeType {
|
||||
ADD, REMOVE;
|
||||
|
||||
public ChangeType inverse() {
|
||||
return this == ADD ? REMOVE : ADD;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -30,6 +30,7 @@ import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import me.lucko.luckperms.common.model.PermissionHolder;
|
||||
import me.lucko.luckperms.common.node.comparator.NodeWithContextComparator;
|
||||
import me.lucko.luckperms.common.util.Difference;
|
||||
|
||||
import net.luckperms.api.context.ContextSet;
|
||||
import net.luckperms.api.context.ImmutableContextSet;
|
||||
@ -134,28 +135,30 @@ public interface NodeMap {
|
||||
|
||||
// mutate methods
|
||||
|
||||
MutateResult add(Node nodeWithoutInheritanceOrigin);
|
||||
Difference<Node> add(Node nodeWithoutInheritanceOrigin);
|
||||
|
||||
MutateResult remove(Node node);
|
||||
Difference<Node> remove(Node node);
|
||||
|
||||
MutateResult removeExact(Node node);
|
||||
Difference<Node> removeExact(Node node);
|
||||
|
||||
MutateResult removeIf(Predicate<? super Node> predicate);
|
||||
Difference<Node> removeIf(Predicate<? super Node> predicate);
|
||||
|
||||
MutateResult removeIf(ContextSet contextSet, Predicate<? super Node> predicate);
|
||||
Difference<Node> removeIf(ContextSet contextSet, Predicate<? super Node> predicate);
|
||||
|
||||
MutateResult removeThenAdd(Node nodeToRemove, Node nodeToAdd);
|
||||
Difference<Node> removeThenAdd(Node nodeToRemove, Node nodeToAdd);
|
||||
|
||||
MutateResult clear();
|
||||
Difference<Node> clear();
|
||||
|
||||
MutateResult clear(ContextSet contextSet);
|
||||
Difference<Node> clear(ContextSet contextSet);
|
||||
|
||||
MutateResult setContent(Iterable<? extends Node> set);
|
||||
Difference<Node> setContent(Iterable<? extends Node> set);
|
||||
|
||||
MutateResult setContent(Stream<? extends Node> stream);
|
||||
Difference<Node> setContent(Stream<? extends Node> stream);
|
||||
|
||||
MutateResult addAll(Iterable<? extends Node> set);
|
||||
Difference<Node> applyChanges(Difference<Node> changes);
|
||||
|
||||
MutateResult addAll(Stream<? extends Node> stream);
|
||||
Difference<Node> addAll(Iterable<? extends Node> set);
|
||||
|
||||
Difference<Node> addAll(Stream<? extends Node> stream);
|
||||
|
||||
}
|
||||
|
@ -29,8 +29,9 @@ import me.lucko.luckperms.common.config.ConfigKeys;
|
||||
import me.lucko.luckperms.common.context.comparator.ContextSetComparator;
|
||||
import me.lucko.luckperms.common.model.InheritanceOrigin;
|
||||
import me.lucko.luckperms.common.model.PermissionHolder;
|
||||
import me.lucko.luckperms.common.model.nodemap.MutateResult.ChangeType;
|
||||
import me.lucko.luckperms.common.node.comparator.NodeComparator;
|
||||
import me.lucko.luckperms.common.util.Difference;
|
||||
import me.lucko.luckperms.common.util.Difference.ChangeType;
|
||||
|
||||
import net.luckperms.api.context.ContextSatisfyMode;
|
||||
import net.luckperms.api.context.ContextSet;
|
||||
@ -122,11 +123,11 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult add(Node nodeWithoutInheritanceOrigin) {
|
||||
public Difference<Node> add(Node nodeWithoutInheritanceOrigin) {
|
||||
Node node = addInheritanceOrigin(nodeWithoutInheritanceOrigin);
|
||||
|
||||
ImmutableContextSet context = node.getContexts();
|
||||
MutateResult result = new MutateResult();
|
||||
Difference<Node> result = new Difference<>();
|
||||
|
||||
this.lock.lock();
|
||||
try {
|
||||
@ -162,9 +163,9 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult remove(Node node) {
|
||||
public Difference<Node> remove(Node node) {
|
||||
ImmutableContextSet context = node.getContexts();
|
||||
MutateResult result = new MutateResult();
|
||||
Difference<Node> result = new Difference<>();
|
||||
|
||||
this.lock.lock();
|
||||
try {
|
||||
@ -191,7 +192,7 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void removeMatching(Iterator<Node> it, Node node, MutateResult result) {
|
||||
private static void removeMatching(Iterator<Node> it, Node node, Difference<Node> result) {
|
||||
while (it.hasNext()) {
|
||||
Node el = it.next();
|
||||
if (node.equals(el, NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE)) {
|
||||
@ -201,7 +202,7 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
}
|
||||
|
||||
private static void removeMatchingButNotSame(Iterator<Node> it, Node node, MutateResult result) {
|
||||
private static void removeMatchingButNotSame(Iterator<Node> it, Node node, Difference<Node> result) {
|
||||
while (it.hasNext()) {
|
||||
Node el = it.next();
|
||||
if (el != node && node.equals(el, NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE)) {
|
||||
@ -212,9 +213,9 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult removeExact(Node node) {
|
||||
public Difference<Node> removeExact(Node node) {
|
||||
ImmutableContextSet context = node.getContexts();
|
||||
MutateResult result = new MutateResult();
|
||||
Difference<Node> result = new Difference<>();
|
||||
|
||||
this.lock.lock();
|
||||
try {
|
||||
@ -245,8 +246,8 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult removeIf(Predicate<? super Node> predicate) {
|
||||
MutateResult result = new MutateResult();
|
||||
public Difference<Node> removeIf(Predicate<? super Node> predicate) {
|
||||
Difference<Node> result = new Difference<>();
|
||||
|
||||
this.lock.lock();
|
||||
try {
|
||||
@ -261,9 +262,9 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult removeIf(ContextSet contextSet, Predicate<? super Node> predicate) {
|
||||
public Difference<Node> removeIf(ContextSet contextSet, Predicate<? super Node> predicate) {
|
||||
ImmutableContextSet context = contextSet.immutableCopy();
|
||||
MutateResult result = new MutateResult();
|
||||
Difference<Node> result = new Difference<>();
|
||||
|
||||
this.lock.lock();
|
||||
try {
|
||||
@ -279,7 +280,7 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
return result;
|
||||
}
|
||||
|
||||
private void removeMatching(Iterator<Node> it, Predicate<? super Node> predicate, MutateResult result) {
|
||||
private void removeMatching(Iterator<Node> it, Predicate<? super Node> predicate, Difference<Node> result) {
|
||||
while (it.hasNext()) {
|
||||
Node node = it.next();
|
||||
|
||||
@ -300,9 +301,9 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult removeThenAdd(Node nodeToRemove, Node nodeToAdd) {
|
||||
public Difference<Node> removeThenAdd(Node nodeToRemove, Node nodeToAdd) {
|
||||
if (nodeToAdd.equals(nodeToRemove)) {
|
||||
return new MutateResult();
|
||||
return new Difference<>();
|
||||
}
|
||||
|
||||
this.lock.lock();
|
||||
@ -314,8 +315,8 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult clear() {
|
||||
MutateResult result = new MutateResult();
|
||||
public Difference<Node> clear() {
|
||||
Difference<Node> result = new Difference<>();
|
||||
|
||||
this.lock.lock();
|
||||
try {
|
||||
@ -336,9 +337,9 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult clear(ContextSet contextSet) {
|
||||
public Difference<Node> clear(ContextSet contextSet) {
|
||||
ImmutableContextSet context = contextSet.immutableCopy();
|
||||
MutateResult result = new MutateResult();
|
||||
Difference<Node> result = new Difference<>();
|
||||
|
||||
this.lock.lock();
|
||||
try {
|
||||
@ -355,8 +356,8 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult setContent(Iterable<? extends Node> set) {
|
||||
MutateResult result = new MutateResult();
|
||||
public Difference<Node> setContent(Iterable<? extends Node> set) {
|
||||
Difference<Node> result = new Difference<>();
|
||||
|
||||
this.lock.lock();
|
||||
try {
|
||||
@ -370,8 +371,8 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult setContent(Stream<? extends Node> stream) {
|
||||
MutateResult result = new MutateResult();
|
||||
public Difference<Node> setContent(Stream<? extends Node> stream) {
|
||||
Difference<Node> result = new Difference<>();
|
||||
|
||||
this.lock.lock();
|
||||
try {
|
||||
@ -385,8 +386,27 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult addAll(Iterable<? extends Node> set) {
|
||||
MutateResult result = new MutateResult();
|
||||
public Difference<Node> applyChanges(Difference<Node> changes) {
|
||||
Difference<Node> result = new Difference<>();
|
||||
|
||||
this.lock.lock();
|
||||
try {
|
||||
for (Node n : changes.getAdded()) {
|
||||
result.mergeFrom(add(n));
|
||||
}
|
||||
for (Node n : changes.getRemoved()) {
|
||||
result.mergeFrom(removeExact(n));
|
||||
}
|
||||
} finally {
|
||||
this.lock.unlock();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Difference<Node> addAll(Iterable<? extends Node> set) {
|
||||
Difference<Node> result = new Difference<>();
|
||||
|
||||
this.lock.lock();
|
||||
try {
|
||||
@ -401,8 +421,8 @@ public class NodeMapMutable extends NodeMapBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult addAll(Stream<? extends Node> stream) {
|
||||
MutateResult result = new MutateResult();
|
||||
public Difference<Node> addAll(Stream<? extends Node> stream) {
|
||||
Difference<Node> result = new Difference<>();
|
||||
|
||||
this.lock.lock();
|
||||
try {
|
||||
|
@ -28,6 +28,8 @@ package me.lucko.luckperms.common.model.nodemap;
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import me.lucko.luckperms.common.util.Difference;
|
||||
|
||||
import net.luckperms.api.context.ContextSet;
|
||||
import net.luckperms.api.context.ImmutableContextSet;
|
||||
import net.luckperms.api.node.Node;
|
||||
@ -53,7 +55,7 @@ public class RecordedNodeMap implements NodeMap {
|
||||
|
||||
private final NodeMap delegate;
|
||||
private final Lock lock = new ReentrantLock();
|
||||
private MutateResult changes = new MutateResult();
|
||||
private Difference<Node> changes = new Difference<>();
|
||||
|
||||
public RecordedNodeMap(NodeMap delegate) {
|
||||
this.delegate = delegate;
|
||||
@ -72,12 +74,12 @@ public class RecordedNodeMap implements NodeMap {
|
||||
}
|
||||
}
|
||||
|
||||
public MutateResult exportChanges(Predicate<MutateResult> onlyIf) {
|
||||
public Difference<Node> exportChanges(Predicate<Difference<Node>> onlyIf) {
|
||||
this.lock.lock();
|
||||
try {
|
||||
MutateResult existing = this.changes;
|
||||
Difference<Node> existing = this.changes;
|
||||
if (onlyIf.test(existing)) {
|
||||
this.changes = new MutateResult();
|
||||
this.changes = new Difference<>();
|
||||
return existing;
|
||||
}
|
||||
return null;
|
||||
@ -86,7 +88,7 @@ public class RecordedNodeMap implements NodeMap {
|
||||
}
|
||||
}
|
||||
|
||||
private MutateResult record(MutateResult result) {
|
||||
private Difference<Node> record(Difference<Node> result) {
|
||||
this.lock.lock();
|
||||
try {
|
||||
this.changes.mergeFrom(result);
|
||||
@ -99,62 +101,67 @@ public class RecordedNodeMap implements NodeMap {
|
||||
// delegate, but pass the result through #record(MutateResult)
|
||||
|
||||
@Override
|
||||
public MutateResult add(Node nodeWithoutInheritanceOrigin) {
|
||||
public Difference<Node> add(Node nodeWithoutInheritanceOrigin) {
|
||||
return record(this.delegate.add(nodeWithoutInheritanceOrigin));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult remove(Node node) {
|
||||
public Difference<Node> remove(Node node) {
|
||||
return record(this.delegate.remove(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult removeExact(Node node) {
|
||||
public Difference<Node> removeExact(Node node) {
|
||||
return record(this.delegate.removeExact(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult removeIf(Predicate<? super Node> predicate) {
|
||||
public Difference<Node> removeIf(Predicate<? super Node> predicate) {
|
||||
return record(this.delegate.removeIf(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult removeIf(ContextSet contextSet, Predicate<? super Node> predicate) {
|
||||
public Difference<Node> removeIf(ContextSet contextSet, Predicate<? super Node> predicate) {
|
||||
return record(this.delegate.removeIf(contextSet, predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult removeThenAdd(Node nodeToRemove, Node nodeToAdd) {
|
||||
public Difference<Node> removeThenAdd(Node nodeToRemove, Node nodeToAdd) {
|
||||
return record(this.delegate.removeThenAdd(nodeToRemove, nodeToAdd));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult clear() {
|
||||
public Difference<Node> clear() {
|
||||
return record(this.delegate.clear());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult clear(ContextSet contextSet) {
|
||||
public Difference<Node> clear(ContextSet contextSet) {
|
||||
return record(this.delegate.clear(contextSet));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult setContent(Iterable<? extends Node> set) {
|
||||
public Difference<Node> setContent(Iterable<? extends Node> set) {
|
||||
return record(this.delegate.setContent(set));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult setContent(Stream<? extends Node> stream) {
|
||||
public Difference<Node> setContent(Stream<? extends Node> stream) {
|
||||
return record(this.delegate.setContent(stream));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult addAll(Iterable<? extends Node> set) {
|
||||
public Difference<Node> applyChanges(Difference<Node> changes) {
|
||||
return record(this.delegate.applyChanges(changes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Difference<Node> addAll(Iterable<? extends Node> set) {
|
||||
return record(this.delegate.addAll(set));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MutateResult addAll(Stream<? extends Node> stream) {
|
||||
public Difference<Node> addAll(Stream<? extends Node> stream) {
|
||||
return record(this.delegate.addAll(stream));
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,7 @@ import me.lucko.luckperms.common.event.EventDispatcher;
|
||||
import me.lucko.luckperms.common.event.gen.GeneratedEventClass;
|
||||
import me.lucko.luckperms.common.extension.SimpleExtensionManager;
|
||||
import me.lucko.luckperms.common.http.BytebinClient;
|
||||
import me.lucko.luckperms.common.http.BytesocksClient;
|
||||
import me.lucko.luckperms.common.inheritance.InheritanceGraphFactory;
|
||||
import me.lucko.luckperms.common.locale.Message;
|
||||
import me.lucko.luckperms.common.locale.TranslationManager;
|
||||
@ -57,7 +58,7 @@ import me.lucko.luckperms.common.tasks.ExpireTemporaryTask;
|
||||
import me.lucko.luckperms.common.tasks.SyncTask;
|
||||
import me.lucko.luckperms.common.treeview.PermissionRegistry;
|
||||
import me.lucko.luckperms.common.verbose.VerboseHandler;
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorSessionStore;
|
||||
import me.lucko.luckperms.common.webeditor.store.WebEditorStore;
|
||||
|
||||
import net.luckperms.api.LuckPerms;
|
||||
|
||||
@ -90,7 +91,8 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin {
|
||||
private LogDispatcher logDispatcher;
|
||||
private LuckPermsConfiguration configuration;
|
||||
private BytebinClient bytebin;
|
||||
private WebEditorSessionStore webEditorSessionStore;
|
||||
private BytesocksClient bytesocks;
|
||||
private WebEditorStore webEditorStore;
|
||||
private TranslationRepository translationRepository;
|
||||
private FileWatcher fileWatcher = null;
|
||||
private Storage storage;
|
||||
@ -136,7 +138,8 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin {
|
||||
.build();
|
||||
|
||||
this.bytebin = new BytebinClient(httpClient, getConfiguration().get(ConfigKeys.BYTEBIN_URL), "luckperms");
|
||||
this.webEditorSessionStore = new WebEditorSessionStore();
|
||||
this.bytesocks = new BytesocksClient(httpClient, getConfiguration().get(ConfigKeys.BYTESOCKS_HOST), "luckperms/editor");
|
||||
this.webEditorStore = new WebEditorStore(this);
|
||||
|
||||
// init translation repo and update bundle files
|
||||
this.translationRepository = new TranslationRepository(this);
|
||||
@ -420,8 +423,13 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin {
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebEditorSessionStore getWebEditorSessionStore() {
|
||||
return this.webEditorSessionStore;
|
||||
public BytesocksClient getBytesocks() {
|
||||
return this.bytesocks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebEditorStore getWebEditorStore() {
|
||||
return this.webEditorStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -36,6 +36,7 @@ import me.lucko.luckperms.common.dependencies.DependencyManager;
|
||||
import me.lucko.luckperms.common.event.EventDispatcher;
|
||||
import me.lucko.luckperms.common.extension.SimpleExtensionManager;
|
||||
import me.lucko.luckperms.common.http.BytebinClient;
|
||||
import me.lucko.luckperms.common.http.BytesocksClient;
|
||||
import me.lucko.luckperms.common.inheritance.InheritanceGraphFactory;
|
||||
import me.lucko.luckperms.common.locale.TranslationManager;
|
||||
import me.lucko.luckperms.common.locale.TranslationRepository;
|
||||
@ -55,7 +56,7 @@ import me.lucko.luckperms.common.storage.implementation.file.watcher.FileWatcher
|
||||
import me.lucko.luckperms.common.tasks.SyncTask;
|
||||
import me.lucko.luckperms.common.treeview.PermissionRegistry;
|
||||
import me.lucko.luckperms.common.verbose.VerboseHandler;
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorSessionStore;
|
||||
import me.lucko.luckperms.common.webeditor.store.WebEditorStore;
|
||||
|
||||
import net.luckperms.api.query.QueryOptions;
|
||||
|
||||
@ -250,11 +251,18 @@ public interface LuckPermsPlugin {
|
||||
BytebinClient getBytebin();
|
||||
|
||||
/**
|
||||
* Gets the web editor session store
|
||||
* Gets the bytesocks instance in use by platform.
|
||||
*
|
||||
* @return the web editor session store
|
||||
* @return the bytesocks instance
|
||||
*/
|
||||
WebEditorSessionStore getWebEditorSessionStore();
|
||||
BytesocksClient getBytesocks();
|
||||
|
||||
/**
|
||||
* Gets the web editor store
|
||||
*
|
||||
* @return the web editor store
|
||||
*/
|
||||
WebEditorStore getWebEditorStore();
|
||||
|
||||
/**
|
||||
* Gets a calculated context instance for the user using the rules of the platform.
|
||||
|
@ -39,7 +39,6 @@ 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.model.manager.group.GroupManager;
|
||||
import me.lucko.luckperms.common.model.nodemap.MutateResult;
|
||||
import me.lucko.luckperms.common.node.factory.NodeBuilders;
|
||||
import me.lucko.luckperms.common.node.matcher.ConstraintNodeMatcher;
|
||||
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||
@ -47,6 +46,7 @@ import me.lucko.luckperms.common.storage.implementation.StorageImplementation;
|
||||
import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory;
|
||||
import me.lucko.luckperms.common.storage.misc.NodeEntry;
|
||||
import me.lucko.luckperms.common.storage.misc.PlayerSaveResultImpl;
|
||||
import me.lucko.luckperms.common.util.Difference;
|
||||
import me.lucko.luckperms.common.util.Uuids;
|
||||
import me.lucko.luckperms.common.util.gson.GsonProvider;
|
||||
|
||||
@ -351,15 +351,15 @@ public class SqlStorage implements StorageImplementation {
|
||||
|
||||
@Override
|
||||
public void saveUser(User user) throws SQLException {
|
||||
MutateResult changes = user.normalData().exportChanges(results -> {
|
||||
Difference<Node> changes = user.normalData().exportChanges(results -> {
|
||||
if (this.plugin.getUserManager().isNonDefaultUser(user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if the only change is adding the default node, we don't need to export
|
||||
if (results.getChanges().size() == 1) {
|
||||
MutateResult.Change onlyChange = results.getChanges().iterator().next();
|
||||
return !(onlyChange.getType() == MutateResult.ChangeType.ADD && this.plugin.getUserManager().isDefaultNode(onlyChange.getNode()));
|
||||
Difference.Change<Node> onlyChange = results.getChanges().iterator().next();
|
||||
return !(onlyChange.type() == Difference.ChangeType.ADD && this.plugin.getUserManager().isDefaultNode(onlyChange.value()));
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -480,7 +480,7 @@ public class SqlStorage implements StorageImplementation {
|
||||
|
||||
@Override
|
||||
public void saveGroup(Group group) throws SQLException {
|
||||
MutateResult changes = group.normalData().exportChanges(c -> true);
|
||||
Difference<Node> changes = group.normalData().exportChanges(c -> true);
|
||||
|
||||
if (!changes.isEmpty()) {
|
||||
try (Connection c = this.connectionFactory.getConnection()) {
|
||||
|
@ -0,0 +1,208 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Records a log of the changes that occur as a result
|
||||
* of mutations (add or remove operations).
|
||||
*
|
||||
* @param <T> the value type
|
||||
*/
|
||||
public class Difference<T> {
|
||||
private final LinkedHashSet<Change<T>> changes = new LinkedHashSet<>();
|
||||
|
||||
/**
|
||||
* Gets the recorded changes.
|
||||
*
|
||||
* @return the changes
|
||||
*/
|
||||
public Set<Change<T>> getChanges() {
|
||||
return this.changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if no changes have been recorded.
|
||||
*
|
||||
* @return if no changes have been recorded
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return this.changes.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the recorded changes of a given type
|
||||
*
|
||||
* @param type the type of change
|
||||
* @return the changes
|
||||
*/
|
||||
public Set<T> getChanges(ChangeType type) {
|
||||
Set<T> changes = new LinkedHashSet<>(this.changes.size());
|
||||
for (Change<T> change : this.changes) {
|
||||
if (change.type() == type) {
|
||||
changes.add(change.value());
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values that have been added.
|
||||
*
|
||||
* @return the added values
|
||||
*/
|
||||
public Set<T> getAdded() {
|
||||
return getChanges(ChangeType.ADD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values that have been removed.
|
||||
*
|
||||
* @return the removed values
|
||||
*/
|
||||
public Set<T> getRemoved() {
|
||||
return getChanges(ChangeType.REMOVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all recorded changes.
|
||||
*/
|
||||
public void clear() {
|
||||
this.changes.clear();
|
||||
}
|
||||
|
||||
private void recordChange(Change<T> change) {
|
||||
// This method is the magic of this class.
|
||||
// When tracking, we want to ignore changes that cancel each other out, and only
|
||||
// keep track of the net difference.
|
||||
// e.g. adding then removing the same value = zero net change, so ignore it.
|
||||
|
||||
if (this.changes.remove(change.inverse())) {
|
||||
return;
|
||||
}
|
||||
this.changes.add(change);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a change.
|
||||
*
|
||||
* @param type the type of change
|
||||
* @param value the changed value
|
||||
*/
|
||||
public void recordChange(ChangeType type, T value) {
|
||||
recordChange(new Change<>(type, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Records some changes.
|
||||
*
|
||||
* @param type the type of change
|
||||
* @param values the changed values
|
||||
*/
|
||||
public void recordChanges(ChangeType type, Iterable<T> values) {
|
||||
for (T value : values) {
|
||||
recordChange(new Change<>(type, value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the recorded differences in {@code other} into this.
|
||||
*
|
||||
* @param other the other differences
|
||||
* @return this
|
||||
*/
|
||||
public Difference<T> mergeFrom(Difference<T> other) {
|
||||
for (Change<T> change : other.changes) {
|
||||
recordChange(change);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Difference{" + this.changes + '}';
|
||||
}
|
||||
|
||||
/**
|
||||
* A single change recorded in the {@link Difference} tracker.
|
||||
*
|
||||
* @param <T> the value type
|
||||
*/
|
||||
public static final class Change<T> {
|
||||
private final ChangeType type;
|
||||
private final T value;
|
||||
|
||||
public Change(ChangeType type, T value) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public ChangeType type() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public T value() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public Change<T> inverse() {
|
||||
return new Change<>(this.type.inverse(), this.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Change<?> change = (Change<?>) o;
|
||||
return this.type == change.type && this.value.equals(change.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.type, this.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "(" + this.type + ": " + this.value + ')';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of change.
|
||||
*/
|
||||
public enum ChangeType {
|
||||
ADD, REMOVE;
|
||||
|
||||
public ChangeType inverse() {
|
||||
return this == ADD ? REMOVE : ADD;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -28,14 +28,11 @@ package me.lucko.luckperms.common.webeditor;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import me.lucko.luckperms.common.config.ConfigKeys;
|
||||
import me.lucko.luckperms.common.context.ImmutableContextSetImpl;
|
||||
import me.lucko.luckperms.common.context.serializer.ContextSetJsonSerializer;
|
||||
import me.lucko.luckperms.common.http.AbstractHttpClient;
|
||||
import me.lucko.luckperms.common.http.UnsuccessfulRequestException;
|
||||
import me.lucko.luckperms.common.locale.Message;
|
||||
import me.lucko.luckperms.common.model.Group;
|
||||
import me.lucko.luckperms.common.model.PermissionHolder;
|
||||
import me.lucko.luckperms.common.model.PermissionHolderIdentifier;
|
||||
import me.lucko.luckperms.common.model.Track;
|
||||
import me.lucko.luckperms.common.model.User;
|
||||
import me.lucko.luckperms.common.node.matcher.ConstraintNodeMatcher;
|
||||
@ -43,6 +40,7 @@ import me.lucko.luckperms.common.node.utils.NodeJsonSerializer;
|
||||
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||
import me.lucko.luckperms.common.sender.Sender;
|
||||
import me.lucko.luckperms.common.storage.misc.NodeEntry;
|
||||
import me.lucko.luckperms.common.util.ImmutableCollectors;
|
||||
import me.lucko.luckperms.common.util.gson.GsonProvider;
|
||||
import me.lucko.luckperms.common.util.gson.JArray;
|
||||
import me.lucko.luckperms.common.util.gson.JObject;
|
||||
@ -64,6 +62,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
@ -75,6 +74,48 @@ public class WebEditorRequest {
|
||||
|
||||
public static final int MAX_USERS = 500;
|
||||
|
||||
/**
|
||||
* The encoded json object this payload is made up of
|
||||
*/
|
||||
private final JsonObject payload;
|
||||
|
||||
private final Map<PermissionHolderIdentifier, List<Node>> holders;
|
||||
private final Map<String, List<String>> tracks;
|
||||
|
||||
private WebEditorRequest(JsonObject payload, Map<PermissionHolder, List<Node>> holders, Map<Track, List<String>> tracks) {
|
||||
this.payload = payload;
|
||||
this.holders = holders.entrySet().stream().collect(ImmutableCollectors.toMap(
|
||||
e -> e.getKey().getIdentifier(),
|
||||
Map.Entry::getValue
|
||||
));
|
||||
this.tracks = tracks.entrySet().stream().collect(ImmutableCollectors.toMap(
|
||||
e -> e.getKey().getName(),
|
||||
Map.Entry::getValue
|
||||
));
|
||||
}
|
||||
|
||||
public JsonObject getPayload() {
|
||||
return this.payload;
|
||||
}
|
||||
|
||||
public byte[] encode() {
|
||||
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
|
||||
try (Writer writer = new OutputStreamWriter(new GZIPOutputStream(bytesOut), StandardCharsets.UTF_8)) {
|
||||
GsonProvider.normal().toJson(this.payload, writer);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return bytesOut.toByteArray();
|
||||
}
|
||||
|
||||
public Map<PermissionHolderIdentifier, List<Node>> getHolders() {
|
||||
return this.holders;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getTracks() {
|
||||
return this.tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a web editor request payload.
|
||||
*
|
||||
@ -95,34 +136,35 @@ public class WebEditorRequest {
|
||||
}
|
||||
|
||||
// form the payload data
|
||||
return new WebEditorRequest(holders, tracks, sender, cmdLabel, potentialContexts.build(), plugin);
|
||||
Map<PermissionHolder, List<Node>> holdersMap = holders.stream().collect(ImmutableCollectors.toMap(
|
||||
Function.identity(),
|
||||
holder -> holder.normalData().asList()
|
||||
));
|
||||
|
||||
Map<Track, List<String>> tracksMap = tracks.stream().collect(ImmutableCollectors.toMap(
|
||||
Function.identity(),
|
||||
Track::getGroups
|
||||
));
|
||||
|
||||
JsonObject json = createJsonPayload(holdersMap, tracksMap, sender, cmdLabel, potentialContexts.build(), plugin).toJson();
|
||||
return new WebEditorRequest(json, holdersMap, tracksMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* The encoded json object this payload is made up of
|
||||
*/
|
||||
private final JsonObject payload;
|
||||
|
||||
private WebEditorRequest(List<PermissionHolder> holders, List<Track> tracks, Sender sender, String cmdLabel, ImmutableContextSet potentialContexts, LuckPermsPlugin plugin) {
|
||||
this.payload = new JObject()
|
||||
private static JObject createJsonPayload(Map<PermissionHolder, List<Node>> holders, Map<Track, List<String>> tracks, Sender sender, String cmdLabel, ImmutableContextSet potentialContexts, LuckPermsPlugin plugin) {
|
||||
return new JObject()
|
||||
.add("metadata", formMetadata(sender, cmdLabel, plugin.getBootstrap().getVersion()))
|
||||
.add("permissionHolders", new JArray()
|
||||
.consume(arr -> {
|
||||
for (PermissionHolder holder : holders) {
|
||||
arr.add(formPermissionHolder(holder));
|
||||
}
|
||||
})
|
||||
)
|
||||
.add("tracks", new JArray()
|
||||
.consume(arr -> {
|
||||
for (Track track : tracks) {
|
||||
arr.add(formTrack(track));
|
||||
}
|
||||
})
|
||||
)
|
||||
.add("permissionHolders", new JArray().consume(arr ->
|
||||
holders.forEach((holder, data) ->
|
||||
arr.add(formPermissionHolder(holder, data))
|
||||
)
|
||||
))
|
||||
.add("tracks", new JArray().consume(arr ->
|
||||
tracks.forEach((track, data) ->
|
||||
arr.add(formTrack(track, data))
|
||||
)
|
||||
))
|
||||
.add("knownPermissions", new JArray().addAll(plugin.getPermissionRegistry().rootAsList()))
|
||||
.add("potentialContexts", ContextSetJsonSerializer.serialize(potentialContexts))
|
||||
.toJson();
|
||||
.add("potentialContexts", ContextSetJsonSerializer.serialize(potentialContexts));
|
||||
}
|
||||
|
||||
private static JObject formMetadata(Sender sender, String cmdLabel, String pluginVersion) {
|
||||
@ -136,56 +178,19 @@ public class WebEditorRequest {
|
||||
.add("pluginVersion", pluginVersion);
|
||||
}
|
||||
|
||||
private static JObject formPermissionHolder(PermissionHolder holder) {
|
||||
private static JObject formPermissionHolder(PermissionHolder holder, List<Node> data) {
|
||||
return new JObject()
|
||||
.add("type", holder.getType().toString())
|
||||
.add("id", holder.getIdentifier().getName())
|
||||
.add("displayName", holder.getPlainDisplayName())
|
||||
.add("nodes", NodeJsonSerializer.serializeNodes(holder.normalData().asList()));
|
||||
.add("nodes", NodeJsonSerializer.serializeNodes(data));
|
||||
}
|
||||
|
||||
private static JObject formTrack(Track track) {
|
||||
private static JObject formTrack(Track track, List<String> data) {
|
||||
return new JObject()
|
||||
.add("type", "track")
|
||||
.add("id", track.getName())
|
||||
.add("groups", new JArray().addAll(track.getGroups()));
|
||||
}
|
||||
|
||||
public byte[] encode() {
|
||||
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
|
||||
try (Writer writer = new OutputStreamWriter(new GZIPOutputStream(bytesOut), StandardCharsets.UTF_8)) {
|
||||
GsonProvider.normal().toJson(this.payload, writer);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return bytesOut.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a web editor session, and sends the URL to the sender.
|
||||
*
|
||||
* @param plugin the plugin
|
||||
* @param sender the sender creating the session
|
||||
* @return the command result
|
||||
*/
|
||||
public void createSession(LuckPermsPlugin plugin, Sender sender) {
|
||||
String pasteId;
|
||||
try {
|
||||
pasteId = plugin.getBytebin().postContent(encode(), AbstractHttpClient.JSON_TYPE).key();
|
||||
} catch (UnsuccessfulRequestException e) {
|
||||
Message.EDITOR_HTTP_REQUEST_FAILURE.send(sender, e.getResponse().code(), e.getResponse().message());
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
new RuntimeException("Error uploading data to bytebin", e).printStackTrace();
|
||||
Message.EDITOR_HTTP_UNKNOWN_FAILURE.send(sender);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getWebEditorSessionStore().addNewSession(pasteId);
|
||||
|
||||
// form a url for the editor
|
||||
String url = plugin.getConfiguration().get(ConfigKeys.WEB_EDITOR_URL_PATTERN) + pasteId;
|
||||
Message.EDITOR_URL.send(sender, url);
|
||||
.add("groups", new JArray().addAll(data));
|
||||
}
|
||||
|
||||
public static void includeMatchingGroups(List<? super Group> holders, Predicate<? super Group> filter, LuckPermsPlugin plugin) {
|
||||
|
@ -40,11 +40,12 @@ import me.lucko.luckperms.common.model.PermissionHolder;
|
||||
import me.lucko.luckperms.common.model.Track;
|
||||
import me.lucko.luckperms.common.model.User;
|
||||
import me.lucko.luckperms.common.model.manager.group.GroupManager;
|
||||
import me.lucko.luckperms.common.model.nodemap.MutateResult;
|
||||
import me.lucko.luckperms.common.node.utils.NodeJsonSerializer;
|
||||
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||
import me.lucko.luckperms.common.sender.Sender;
|
||||
import me.lucko.luckperms.common.util.Difference;
|
||||
import me.lucko.luckperms.common.util.Uuids;
|
||||
import me.lucko.luckperms.common.webeditor.store.RemoteSession;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.luckperms.api.actionlog.Action;
|
||||
@ -55,7 +56,6 @@ import net.luckperms.api.node.Node;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
@ -86,41 +86,31 @@ public class WebEditorResponse {
|
||||
* @param plugin the plugin
|
||||
* @param sender the sender who is applying the session
|
||||
*/
|
||||
public void apply(LuckPermsPlugin plugin, Sender sender, String commandLabel, boolean ignoreSessionWarning) {
|
||||
JsonElement sessionIdJson = this.payload.get("sessionId");
|
||||
if (sessionIdJson != null) {
|
||||
String sessionId = sessionIdJson.getAsString();
|
||||
WebEditorSessionStore sessionStore = plugin.getWebEditorSessionStore();
|
||||
public void apply(LuckPermsPlugin plugin, Sender sender, WebEditorSession editorSession, String commandLabel, boolean ignoreSessionWarning) {
|
||||
String sessionId = this.payload.get("sessionId").getAsString();
|
||||
RemoteSession remoteSession = plugin.getWebEditorStore().sessions().getSession(sessionId);
|
||||
|
||||
SessionState state = sessionStore.getSessionState(sessionId);
|
||||
switch (state) {
|
||||
case COMPLETED:
|
||||
if (!ignoreSessionWarning) {
|
||||
Message.APPLY_EDITS_SESSION_APPLIED_ALREADY.send(sender, this.id, commandLabel);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case NOT_KNOWN:
|
||||
if (!ignoreSessionWarning) {
|
||||
Message.APPLY_EDITS_SESSION_UNKNOWN.send(sender, this.id, commandLabel);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case IN_PROGRESS:
|
||||
sessionStore.markSessionCompleted(sessionId);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError(state);
|
||||
if (remoteSession == null) {
|
||||
// session is unknown
|
||||
if (!ignoreSessionWarning) {
|
||||
Message.APPLY_EDITS_SESSION_UNKNOWN.send(sender, this.id, commandLabel);
|
||||
return;
|
||||
}
|
||||
} else if (remoteSession.isCompleted()) {
|
||||
// session has been completed already
|
||||
if (!ignoreSessionWarning) {
|
||||
Message.APPLY_EDITS_SESSION_APPLIED_ALREADY.send(sender, this.id, commandLabel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Session session = new Session(plugin, sender);
|
||||
ChangeApplier changeApplier = new ChangeApplier(plugin, sender, editorSession, remoteSession);
|
||||
boolean work = false;
|
||||
|
||||
if (this.payload.has("changes")) {
|
||||
JsonArray changes = this.payload.get("changes").getAsJsonArray();
|
||||
for (JsonElement change : changes) {
|
||||
if (session.applyChange(change.getAsJsonObject())) {
|
||||
if (changeApplier.applyChange(change.getAsJsonObject())) {
|
||||
work = true;
|
||||
}
|
||||
}
|
||||
@ -128,7 +118,7 @@ public class WebEditorResponse {
|
||||
if (this.payload.has("userDeletions")) {
|
||||
JsonArray userDeletions = this.payload.get("userDeletions").getAsJsonArray();
|
||||
for (JsonElement userDeletion : userDeletions) {
|
||||
if (session.applyUserDelete(userDeletion)) {
|
||||
if (changeApplier.applyUserDelete(userDeletion)) {
|
||||
work = true;
|
||||
}
|
||||
}
|
||||
@ -136,7 +126,7 @@ public class WebEditorResponse {
|
||||
if (this.payload.has("groupDeletions")) {
|
||||
JsonArray groupDeletions = this.payload.get("groupDeletions").getAsJsonArray();
|
||||
for (JsonElement groupDeletion : groupDeletions) {
|
||||
if (session.applyGroupDelete(groupDeletion)) {
|
||||
if (changeApplier.applyGroupDelete(groupDeletion)) {
|
||||
work = true;
|
||||
}
|
||||
}
|
||||
@ -144,12 +134,16 @@ public class WebEditorResponse {
|
||||
if (this.payload.has("trackDeletions")) {
|
||||
JsonArray trackDeletions = this.payload.get("trackDeletions").getAsJsonArray();
|
||||
for (JsonElement trackDeletion : trackDeletions) {
|
||||
if (session.applyTrackDelete(trackDeletion)) {
|
||||
if (changeApplier.applyTrackDelete(trackDeletion)) {
|
||||
work = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (remoteSession != null) {
|
||||
remoteSession.complete();
|
||||
}
|
||||
|
||||
if (!work) {
|
||||
Message.APPLY_EDITS_TARGET_NO_CHANGES_PRESENT.send(sender);
|
||||
}
|
||||
@ -158,13 +152,17 @@ public class WebEditorResponse {
|
||||
/**
|
||||
* Represents the application of a given editor session on this platform.
|
||||
*/
|
||||
private static class Session {
|
||||
private static class ChangeApplier {
|
||||
private final LuckPermsPlugin plugin;
|
||||
private final Sender sender;
|
||||
private final WebEditorSession session;
|
||||
private final RemoteSession remoteSession;
|
||||
|
||||
Session(LuckPermsPlugin plugin, Sender sender) {
|
||||
ChangeApplier(LuckPermsPlugin plugin, Sender sender, WebEditorSession session, RemoteSession remoteSession) {
|
||||
this.plugin = plugin;
|
||||
this.sender = sender;
|
||||
this.session = session;
|
||||
this.remoteSession = remoteSession;
|
||||
}
|
||||
|
||||
private boolean applyChange(JsonObject changeInfo) {
|
||||
@ -202,6 +200,9 @@ public class WebEditorResponse {
|
||||
holder = this.plugin.getStorage().loadGroup(id).join().orElse(null);
|
||||
if (holder == null) {
|
||||
holder = this.plugin.getStorage().createAndLoadGroup(id, CreationCause.WEB_EDITOR).join();
|
||||
if (this.session != null) {
|
||||
this.session.includeCreatedGroup((Group) holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,7 +212,7 @@ public class WebEditorResponse {
|
||||
}
|
||||
|
||||
Set<Node> nodes = NodeJsonSerializer.deserializeNodes(changeInfo.getAsJsonArray("nodes"));
|
||||
MutateResult res = holder.setNodes(DataType.NORMAL, nodes, true);
|
||||
Difference<Node> res = applyNodeChanges(holder, nodes);
|
||||
|
||||
if (res.isEmpty()) {
|
||||
return false;
|
||||
@ -233,22 +234,51 @@ public class WebEditorResponse {
|
||||
|
||||
Message.APPLY_EDITS_SUCCESS.send(this.sender, type, holder.getFormattedDisplayName());
|
||||
Message.APPLY_EDITS_SUCCESS_SUMMARY.send(this.sender, added.size(), removed.size());
|
||||
|
||||
for (Node n : added) {
|
||||
Message.APPLY_EDITS_DIFF_ADDED.send(this.sender, n);
|
||||
}
|
||||
for (Node n : removed) {
|
||||
Message.APPLY_EDITS_DIFF_REMOVED.send(this.sender, n);
|
||||
}
|
||||
|
||||
StorageAssistant.save(holder, this.sender, this.plugin);
|
||||
return true;
|
||||
}
|
||||
|
||||
private Difference<Node> applyNodeChanges(PermissionHolder holder, Set<Node> nodes) {
|
||||
if (this.remoteSession != null) {
|
||||
|
||||
WebEditorRequest request = this.remoteSession.request();
|
||||
if (request != null) {
|
||||
|
||||
List<Node> nodesBefore = request.getHolders().get(holder.getIdentifier());
|
||||
if (nodesBefore != null) {
|
||||
|
||||
// if the initial data sent to the remote session is still known
|
||||
// use that to calculate a diff of the changes made to avoid overriding
|
||||
// modified/added/removed nodes since the editor session was created
|
||||
Difference<Node> diff = new Difference<>();
|
||||
diff.recordChanges(Difference.ChangeType.REMOVE, nodesBefore);
|
||||
diff.recordChanges(Difference.ChangeType.ADD, nodes);
|
||||
|
||||
return holder.setNodes(DataType.NORMAL, diff, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return holder.setNodes(DataType.NORMAL, nodes, true);
|
||||
}
|
||||
|
||||
private boolean applyTrackChange(JsonObject changeInfo) {
|
||||
String id = changeInfo.get("id").getAsString();
|
||||
|
||||
Track track = this.plugin.getStorage().loadTrack(id).join().orElse(null);
|
||||
if (track == null) {
|
||||
track = this.plugin.getStorage().createAndLoadTrack(id, CreationCause.WEB_EDITOR).join();
|
||||
if (this.session != null) {
|
||||
this.session.includeCreatedTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
if (ArgumentPermissions.checkModifyPerms(this.plugin, this.sender, CommandPermission.APPLY_EDITS, track)) {
|
||||
@ -264,32 +294,33 @@ public class WebEditorResponse {
|
||||
return false;
|
||||
}
|
||||
|
||||
Set<String> diffAdded = getAdded(before, after);
|
||||
Set<String> diffRemoved = getRemoved(before, after);
|
||||
Difference<String> diff = new Difference<>();
|
||||
diff.recordChanges(Difference.ChangeType.REMOVE, before);
|
||||
diff.recordChanges(Difference.ChangeType.ADD, after);
|
||||
|
||||
int additions = diffAdded.size();
|
||||
int deletions = diffRemoved.size();
|
||||
Set<String> added = diff.getAdded();
|
||||
Set<String> removed = diff.getRemoved();
|
||||
|
||||
track.setGroups(after);
|
||||
|
||||
if (hasBeenReordered(before, after, diffAdded, diffRemoved)) {
|
||||
if (hasBeenReordered(before, after, added, removed)) {
|
||||
LoggedAction.build().source(this.sender).target(track)
|
||||
.description("webeditor", "reorder", after)
|
||||
.build().submit(this.plugin, this.sender);
|
||||
}
|
||||
for (String n : diffAdded) {
|
||||
for (String n : added) {
|
||||
LoggedAction.build().source(this.sender).target(track)
|
||||
.description("webeditor", "add", n)
|
||||
.build().submit(this.plugin, this.sender);
|
||||
}
|
||||
for (String n : diffRemoved) {
|
||||
for (String n : removed) {
|
||||
LoggedAction.build().source(this.sender).target(track)
|
||||
.description("webeditor", "remove", n)
|
||||
.build().submit(this.plugin, this.sender);
|
||||
}
|
||||
|
||||
Message.APPLY_EDITS_SUCCESS.send(this.sender, "track", Component.text(track.getName()));
|
||||
Message.APPLY_EDITS_SUCCESS_SUMMARY.send(this.sender, additions, deletions);
|
||||
Message.APPLY_EDITS_SUCCESS_SUMMARY.send(this.sender, added.size(), removed.size());
|
||||
Message.APPLY_EDITS_TRACK_BEFORE.send(this.sender, before);
|
||||
Message.APPLY_EDITS_TRACK_AFTER.send(this.sender, after);
|
||||
|
||||
@ -339,6 +370,10 @@ public class WebEditorResponse {
|
||||
.description("webeditor", "delete")
|
||||
.build().submit(this.plugin, this.sender);
|
||||
|
||||
if (this.session != null) {
|
||||
this.session.excludeDeletedUser(user);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -374,6 +409,10 @@ public class WebEditorResponse {
|
||||
.description("webeditor", "delete")
|
||||
.build().submit(this.plugin, this.sender);
|
||||
|
||||
if (this.session != null) {
|
||||
this.session.excludeDeletedGroup(group);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -404,21 +443,13 @@ public class WebEditorResponse {
|
||||
.description("webeditor", "delete")
|
||||
.build().submit(this.plugin, this.sender);
|
||||
|
||||
if (this.session != null) {
|
||||
this.session.excludeDeletedTrack(track);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static <T> Set<T> getAdded(Collection<T> before, Collection<T> after) {
|
||||
Set<T> added = new LinkedHashSet<>(after);
|
||||
added.removeAll(before);
|
||||
return added;
|
||||
}
|
||||
|
||||
private static <T> Set<T> getRemoved(Collection<T> before, Collection<T> after) {
|
||||
Set<T> removed = new LinkedHashSet<>(before);
|
||||
removed.removeAll(after);
|
||||
return removed;
|
||||
}
|
||||
|
||||
private static <T> boolean hasBeenReordered(List<T> before, List<T> after, Collection<T> diffAdded, Collection<T> diffRemoved) {
|
||||
after = new ArrayList<>(after);
|
||||
before = new ArrayList<>(before);
|
||||
|
@ -0,0 +1,206 @@
|
||||
/*
|
||||
* 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.webeditor;
|
||||
|
||||
import me.lucko.luckperms.common.config.ConfigKeys;
|
||||
import me.lucko.luckperms.common.http.AbstractHttpClient;
|
||||
import me.lucko.luckperms.common.http.UnsuccessfulRequestException;
|
||||
import me.lucko.luckperms.common.locale.Message;
|
||||
import me.lucko.luckperms.common.model.Group;
|
||||
import me.lucko.luckperms.common.model.PermissionHolder;
|
||||
import me.lucko.luckperms.common.model.PermissionHolderIdentifier;
|
||||
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.sender.Sender;
|
||||
import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Encapsulates a session with the web editor.
|
||||
*
|
||||
* <p>A session is tied to a specific user, and can comprise of multiple requests to and
|
||||
* responses from the web editor.</p>
|
||||
*/
|
||||
public class WebEditorSession {
|
||||
|
||||
public static void createAndOpen(List<PermissionHolder> holders, List<Track> tracks, Sender sender, String cmdLabel, LuckPermsPlugin plugin) {
|
||||
WebEditorRequest initialRequest = WebEditorRequest.generate(holders, tracks, sender, cmdLabel, plugin);
|
||||
WebEditorSession session = new WebEditorSession(initialRequest, plugin, sender, cmdLabel);
|
||||
session.open();
|
||||
}
|
||||
|
||||
private WebEditorRequest initialRequest;
|
||||
|
||||
private final LuckPermsPlugin plugin;
|
||||
private final Sender sender;
|
||||
private final String cmdLabel;
|
||||
|
||||
private final Set<PermissionHolderIdentifier> holders;
|
||||
private final Set<String> tracks;
|
||||
|
||||
private WebEditorSocket socket = null;
|
||||
|
||||
public WebEditorSession(WebEditorRequest initialRequest, LuckPermsPlugin plugin, Sender sender, String cmdLabel) {
|
||||
this.initialRequest = initialRequest;
|
||||
this.plugin = plugin;
|
||||
this.sender = sender;
|
||||
this.cmdLabel = cmdLabel;
|
||||
|
||||
this.holders = new HashSet<>(initialRequest.getHolders().keySet());
|
||||
this.tracks = new HashSet<>(initialRequest.getTracks().keySet());
|
||||
}
|
||||
|
||||
public void open() {
|
||||
createSocket();
|
||||
createInitialSession();
|
||||
}
|
||||
|
||||
private void createSocket() {
|
||||
try {
|
||||
// create and connect to a socket
|
||||
WebEditorSocket socket = new WebEditorSocket(this.plugin, this.sender, this);
|
||||
socket.initialize(this.plugin.getBytesocks());
|
||||
socket.waitForConnect(5, TimeUnit.SECONDS);
|
||||
|
||||
this.socket = socket;
|
||||
this.plugin.getWebEditorStore().sockets().putSocket(this.sender, this.socket);
|
||||
} catch (Exception e) {
|
||||
if (e instanceof UnsuccessfulRequestException && ((UnsuccessfulRequestException) e).getResponse().code() == 502) {
|
||||
// 502 - bad gateway, probably means the socket service is offline
|
||||
// that's ok, no need to send a warning
|
||||
return;
|
||||
}
|
||||
|
||||
this.plugin.getLogger().warn("Unable to establish socket connection", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void createInitialSession() {
|
||||
Objects.requireNonNull(this.initialRequest);
|
||||
|
||||
WebEditorRequest request = this.initialRequest;
|
||||
this.initialRequest = null;
|
||||
|
||||
if (this.socket != null) {
|
||||
this.socket.appendDetailToRequest(request);
|
||||
}
|
||||
|
||||
String id = uploadRequestData(request);
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// form a url for the editor
|
||||
String url = this.plugin.getConfiguration().get(ConfigKeys.WEB_EDITOR_URL_PATTERN) + id;
|
||||
Message.EDITOR_URL.send(this.sender, url);
|
||||
|
||||
// schedule socket close
|
||||
if (this.socket != null) {
|
||||
this.socket.scheduleCleanupIfUnused();
|
||||
}
|
||||
}
|
||||
|
||||
public void includeCreatedGroup(Group group) {
|
||||
this.holders.add(group.getIdentifier());
|
||||
}
|
||||
|
||||
public void includeCreatedTrack(Track track) {
|
||||
this.tracks.add(track.getName());
|
||||
}
|
||||
|
||||
public void excludeDeletedUser(User user) {
|
||||
this.holders.remove(user.getIdentifier());
|
||||
}
|
||||
|
||||
public void excludeDeletedGroup(Group group) {
|
||||
this.holders.remove(group.getIdentifier());
|
||||
}
|
||||
|
||||
public void excludeDeletedTrack(Track track) {
|
||||
this.tracks.remove(track.getName());
|
||||
}
|
||||
|
||||
public String createFollowUpSession() {
|
||||
List<PermissionHolder> holders = this.holders.stream()
|
||||
.map(id -> {
|
||||
switch (id.getType()) {
|
||||
case PermissionHolderIdentifier.USER_TYPE:
|
||||
return this.plugin.getStorage().loadUser(UUID.fromString(id.getName()), null);
|
||||
case PermissionHolderIdentifier.GROUP_TYPE:
|
||||
return this.plugin.getStorage().loadGroup(id.getName()).thenApply(o -> o.orElse(null));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.map(CompletableFuture::join)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Track> tracks = this.tracks.stream()
|
||||
.map(id -> this.plugin.getStorage().loadTrack(id).thenApply(o -> o.orElse(null)))
|
||||
.map(CompletableFuture::join)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return uploadRequestData(WebEditorRequest.generate(holders, tracks, this.sender, this.cmdLabel, this.plugin));
|
||||
}
|
||||
|
||||
public String getCommandLabel() {
|
||||
return this.cmdLabel;
|
||||
}
|
||||
|
||||
private String uploadRequestData(WebEditorRequest request) {
|
||||
byte[] requestBuf = request.encode();
|
||||
|
||||
String pasteId;
|
||||
try {
|
||||
pasteId = this.plugin.getBytebin().postContent(requestBuf, AbstractHttpClient.JSON_TYPE, "editor").key();
|
||||
} catch (UnsuccessfulRequestException e) {
|
||||
Message.EDITOR_HTTP_REQUEST_FAILURE.send(this.sender, e.getResponse().code(), e.getResponse().message());
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
new RuntimeException("Error uploading data to bytebin", e).printStackTrace();
|
||||
Message.EDITOR_HTTP_UNKNOWN_FAILURE.send(this.sender);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.plugin.getWebEditorStore().sessions().addNewSession(pasteId, request);
|
||||
return pasteId;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.webeditor.socket;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Utilities for public/private key crypto used by the web editor socket connection.
|
||||
*/
|
||||
public final class CryptographyUtils {
|
||||
private CryptographyUtils() {}
|
||||
|
||||
/**
|
||||
* Parse a public key from the given string.
|
||||
*
|
||||
* @param base64String a base64 string encoding the public key
|
||||
* @return the parsed public key
|
||||
* @throws IllegalArgumentException if the input was invalid
|
||||
*/
|
||||
public static PublicKey parsePublicKey(String base64String) throws IllegalArgumentException {
|
||||
try {
|
||||
byte[] bytes = Base64.getDecoder().decode(base64String);
|
||||
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
|
||||
KeyFactory rsa = KeyFactory.getInstance("RSA");
|
||||
return rsa.generatePublic(spec);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Exception parsing public key", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a public/private key pair.
|
||||
*
|
||||
* @return the generated key pair
|
||||
*/
|
||||
public static KeyPair generateKeyPair() {
|
||||
try {
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
||||
generator.initialize(4096);
|
||||
return generator.generateKeyPair();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Exception generating keypair", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs {@code msg} using the given {@link PrivateKey}.
|
||||
*
|
||||
* @param privateKey the private key to sign with
|
||||
* @param msg the message
|
||||
* @return a base64 string containing the signature
|
||||
*/
|
||||
public static String sign(PrivateKey privateKey, String msg) {
|
||||
try {
|
||||
Signature sign = Signature.getInstance("SHA256withRSA");
|
||||
sign.initSign(privateKey);
|
||||
sign.update(msg.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return Base64.getEncoder().encodeToString(sign.sign());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the given base64 encoded signature matches
|
||||
* the given message and {@link PublicKey}.
|
||||
*
|
||||
* @param publicKey the public key that the message was supposedly signed with
|
||||
* @param msg the message
|
||||
* @param signatureBase64 the provided signature
|
||||
* @return true if the signature is ok
|
||||
*/
|
||||
public static boolean verify(PublicKey publicKey, String msg, String signatureBase64) {
|
||||
try {
|
||||
Signature sign = Signature.getInstance("SHA256withRSA");
|
||||
sign.initVerify(publicKey);
|
||||
sign.update(msg.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64);
|
||||
return sign.verify(signatureBytes);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.webeditor.socket;
|
||||
|
||||
import me.lucko.luckperms.common.util.ImmutableCollectors;
|
||||
import me.lucko.luckperms.common.util.gson.JObject;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public enum SocketMessageType {
|
||||
|
||||
/** Sent when the editor first says "hello" over the channel. (editor -> plugin) */
|
||||
HELLO("hello"),
|
||||
|
||||
/** Sent when the plugin replies to the editors "hello" message. (plugin -> editor) */
|
||||
HELLO_REPLY("hello-reply"),
|
||||
|
||||
/** Sent by the editor to confirm that a connection has been established. (editor -> plugin) */
|
||||
CONNECTED("connected"),
|
||||
|
||||
/** Sent by the editor to request that the plugin applies a change. (editor -> plugin) */
|
||||
CHANGE_REQUEST("change-request"),
|
||||
|
||||
/** Sent by the plugin to confirm that the changes sent by the editor have been accepted or applied. (plugin -> editor) */
|
||||
CHANGE_RESPONSE("change-response"),
|
||||
|
||||
/** Ping message to keep the socket alive. (editor -> plugin) */
|
||||
PING("ping"),
|
||||
|
||||
/** Ping response. (plugin -> editor) */
|
||||
PONG("pong");
|
||||
|
||||
public final String id;
|
||||
|
||||
SocketMessageType(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public JObject builder() {
|
||||
return new JObject().add("type", this.id);
|
||||
}
|
||||
|
||||
private static final Map<String, SocketMessageType> LOOKUP = Arrays.stream(SocketMessageType.values())
|
||||
.collect(ImmutableCollectors.toMap(m -> m.id, Function.identity()));
|
||||
|
||||
public static SocketMessageType getById(String id) {
|
||||
SocketMessageType type = LOOKUP.get(id);
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException(id);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
}
|
@ -0,0 +1,258 @@
|
||||
/*
|
||||
* 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.webeditor.socket;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import me.lucko.luckperms.common.http.BytesocksClient;
|
||||
import me.lucko.luckperms.common.http.UnsuccessfulRequestException;
|
||||
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||
import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask;
|
||||
import me.lucko.luckperms.common.sender.Sender;
|
||||
import me.lucko.luckperms.common.util.gson.GsonProvider;
|
||||
import me.lucko.luckperms.common.util.gson.JObject;
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorRequest;
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorSession;
|
||||
import me.lucko.luckperms.common.webeditor.socket.listener.WebEditorSocketListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class WebEditorSocket {
|
||||
|
||||
private static final int PROTOCOL_VERSION = 1;
|
||||
|
||||
/** The plugin */
|
||||
private final LuckPermsPlugin plugin;
|
||||
/** The sender who created the editor session */
|
||||
private final Sender sender;
|
||||
/** The web editor session */
|
||||
private final WebEditorSession session;
|
||||
/** The socket listener that handles incoming messages */
|
||||
private final WebEditorSocketListener listener;
|
||||
|
||||
/** The websocket backing the connection */
|
||||
private BytesocksClient.Socket socket;
|
||||
/** The public and private keys used to sign messages sent by the plugin */
|
||||
private KeyPair localKeys;
|
||||
/** A task to check if the socket is still active */
|
||||
private SchedulerTask keepaliveTask;
|
||||
/** The public key used by the editor to sign messages */
|
||||
private PublicKey remotePublicKey;
|
||||
/** If the connection is closed */
|
||||
private boolean closed = false;
|
||||
|
||||
public WebEditorSocket(LuckPermsPlugin plugin, Sender sender, WebEditorSession session) {
|
||||
this.plugin = plugin;
|
||||
this.sender = sender;
|
||||
this.session = session;
|
||||
this.listener = new WebEditorSocketListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the socket connection.
|
||||
*
|
||||
* @param client the bytesocks client to connect to
|
||||
* @throws UnsuccessfulRequestException if the request fails
|
||||
* @throws IOException if an i/o error occurs
|
||||
*/
|
||||
public void initialize(BytesocksClient client) throws UnsuccessfulRequestException, IOException {
|
||||
this.socket = client.createSocket(this.listener);
|
||||
this.localKeys = CryptographyUtils.generateKeyPair();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits the specified amount of time for the socket to connect,
|
||||
* before throwing an exception if a timeout occurs.
|
||||
*
|
||||
* @param timeout the timeout
|
||||
* @param unit the timeout unit
|
||||
*/
|
||||
public void waitForConnect(long timeout, TimeUnit unit) {
|
||||
try {
|
||||
this.listener.connectFuture().get(timeout, unit);
|
||||
} catch (ExecutionException | TimeoutException | InterruptedException e) {
|
||||
throw new RuntimeException("Timed out waiting to socket to connect", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds detail about the socket channel and the plugin public key to
|
||||
* the editor request payload that gets sent via bytebin to the viewer.
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
public void appendDetailToRequest(WebEditorRequest request) {
|
||||
String channelId = this.socket.channelId();
|
||||
String publicKey = Base64.getEncoder().encodeToString(this.localKeys.getPublic().getEncoded());
|
||||
|
||||
JsonObject socket = new JsonObject();
|
||||
socket.addProperty("protocolVersion", PROTOCOL_VERSION);
|
||||
socket.addProperty("channelId", channelId);
|
||||
socket.addProperty("publicKey", publicKey);
|
||||
|
||||
JsonObject payload = request.getPayload();
|
||||
payload.add("socket", socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the socket.
|
||||
*
|
||||
* <p>The message will be encoded as JSON and
|
||||
* signed using the public public key.</p>
|
||||
*
|
||||
* @param msg the message
|
||||
*/
|
||||
public void send(JsonObject msg) {
|
||||
String encoded = GsonProvider.normal().toJson(msg);
|
||||
String signature = CryptographyUtils.sign(this.localKeys.getPrivate(), encoded);
|
||||
|
||||
JsonObject frame = new JObject()
|
||||
.add("msg", encoded)
|
||||
.add("signature", signature)
|
||||
.toJson();
|
||||
|
||||
this.socket.socket().send(GsonProvider.normal().toJson(frame));
|
||||
}
|
||||
|
||||
public boolean trustConnection(String nonce) {
|
||||
if (this.listener.shouldIgnoreMessages()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.remotePublicKey != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PublicKey publicKey = this.listener.helloHandler().getAttemptedConnection(nonce);
|
||||
if (publicKey == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.remotePublicKey = publicKey;
|
||||
|
||||
// save the key in the keystore
|
||||
this.plugin.getWebEditorStore().keystore().trust(this.sender, this.remotePublicKey.getEncoded());
|
||||
|
||||
// send a reply back to the editor to say that it is now trusted
|
||||
send(SocketMessageType.HELLO_REPLY.builder()
|
||||
.add("nonce", nonce)
|
||||
.add("state", "trusted")
|
||||
.toJson()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void scheduleCleanupIfUnused() {
|
||||
this.plugin.getBootstrap().getScheduler().asyncLater(this::afterOpenFor1Minute, 1, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
private void afterOpenFor1Minute() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.remotePublicKey == null && !this.listener.helloHandler().hasAttemptedConnection()) {
|
||||
// If the editor hasn't made an initial connection after 1 minute,
|
||||
// then close + stop listening to the socket.
|
||||
closeSocket();
|
||||
} else {
|
||||
// Otherwise, setup a keepalive monitoring task
|
||||
this.keepaliveTask = this.plugin.getBootstrap().getScheduler().asyncRepeating(this::keepalive, 10, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The keepalive tasks checks to see when the last ping from the editor was. If the editor
|
||||
* hasn't sent anything for 1 minute, then close the connection
|
||||
*/
|
||||
private void keepalive() {
|
||||
if (System.currentTimeMillis() - this.listener.pingHandler().getLastPing() > TimeUnit.MINUTES.toMillis(1)) {
|
||||
cancelKeepalive();
|
||||
closeSocket();
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
send(SocketMessageType.PONG.builder()
|
||||
.add("ok", false)
|
||||
.toJson()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
cancelKeepalive();
|
||||
closeSocket();
|
||||
}
|
||||
|
||||
private void closeSocket() {
|
||||
this.socket.socket().close(1000, "Normal");
|
||||
this.closed = true;
|
||||
}
|
||||
|
||||
private void cancelKeepalive() {
|
||||
if (this.keepaliveTask != null) {
|
||||
this.keepaliveTask.cancel();
|
||||
this.keepaliveTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
public LuckPermsPlugin getPlugin() {
|
||||
return this.plugin;
|
||||
}
|
||||
|
||||
public Sender getSender() {
|
||||
return this.sender;
|
||||
}
|
||||
|
||||
public WebEditorSession getSession() {
|
||||
return this.session;
|
||||
}
|
||||
|
||||
public BytesocksClient.Socket getSocket() {
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
public PublicKey getRemotePublicKey() {
|
||||
return this.remotePublicKey;
|
||||
}
|
||||
|
||||
public void setRemotePublicKey(PublicKey remotePublicKey) {
|
||||
this.remotePublicKey = remotePublicKey;
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return this.closed;
|
||||
}
|
||||
|
||||
}
|
@ -23,26 +23,15 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package me.lucko.luckperms.common.webeditor;
|
||||
package me.lucko.luckperms.common.webeditor.socket.listener;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Represents the state of a web editor session
|
||||
* A handler for a given type of message.
|
||||
*/
|
||||
enum SessionState {
|
||||
public interface Handler {
|
||||
|
||||
/**
|
||||
* The session is not known to this server.
|
||||
*/
|
||||
NOT_KNOWN,
|
||||
|
||||
/**
|
||||
* The session is in progress - it has been created, but updates have not been applied.
|
||||
*/
|
||||
IN_PROGRESS,
|
||||
|
||||
/**
|
||||
* The session is complete - updates have been applied.
|
||||
*/
|
||||
COMPLETED
|
||||
void handle(JsonObject msg);
|
||||
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.webeditor.socket.listener;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import me.lucko.luckperms.common.command.access.CommandPermission;
|
||||
import me.lucko.luckperms.common.http.UnsuccessfulRequestException;
|
||||
import me.lucko.luckperms.common.locale.Message;
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorResponse;
|
||||
import me.lucko.luckperms.common.webeditor.socket.SocketMessageType;
|
||||
import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Handler for {@link SocketMessageType#CHANGE_REQUEST}
|
||||
*/
|
||||
public class HandlerChangeRequest implements Handler {
|
||||
|
||||
/** The change has been accepted, and the plugin will now apply it. */
|
||||
private static final String STATE_ACCEPTED = "accepted";
|
||||
/** The change has been applied. */
|
||||
private static final String STATE_APPLIED = "applied";
|
||||
|
||||
/** The socket */
|
||||
private final WebEditorSocket socket;
|
||||
|
||||
public HandlerChangeRequest(WebEditorSocket socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(JsonObject msg) {
|
||||
if (!this.socket.getSender().hasPermission(CommandPermission.APPLY_EDITS)) {
|
||||
throw new IllegalStateException("Sender does not have applyedits permission");
|
||||
}
|
||||
|
||||
// get the bytebin code containing the editor data
|
||||
String code = msg.get("code").getAsString();
|
||||
if (code == null || code.isEmpty()) {
|
||||
throw new IllegalArgumentException("Invalid code");
|
||||
}
|
||||
|
||||
// send "change-accepted" response
|
||||
this.socket.getPlugin().getBootstrap().getScheduler().executeAsync(() ->
|
||||
this.socket.send(SocketMessageType.CHANGE_RESPONSE.builder()
|
||||
.add("state", STATE_ACCEPTED)
|
||||
.toJson()
|
||||
)
|
||||
);
|
||||
|
||||
// download data from bytebin
|
||||
JsonObject data;
|
||||
try {
|
||||
data = this.socket.getPlugin().getBytebin().getJsonContent(code).getAsJsonObject();
|
||||
Objects.requireNonNull(data);
|
||||
} catch (UnsuccessfulRequestException | IOException e) {
|
||||
throw new RuntimeException("Error reading data", e);
|
||||
}
|
||||
|
||||
// apply changes
|
||||
Message.EDITOR_SOCKET_CHANGES_RECEIVED.send(this.socket.getSender());
|
||||
new WebEditorResponse(code, data).apply(
|
||||
this.socket.getPlugin(),
|
||||
this.socket.getSender(),
|
||||
this.socket.getSession(),
|
||||
"lp",
|
||||
false
|
||||
);
|
||||
|
||||
// create a new session
|
||||
String newSessionCode = this.socket.getSession().createFollowUpSession();
|
||||
this.socket.send(SocketMessageType.CHANGE_RESPONSE.builder()
|
||||
.add("state", STATE_APPLIED)
|
||||
.add("newSessionCode", newSessionCode)
|
||||
.toJson()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.webeditor.socket.listener;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import me.lucko.luckperms.common.locale.Message;
|
||||
import me.lucko.luckperms.common.webeditor.socket.SocketMessageType;
|
||||
import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket;
|
||||
|
||||
/**
|
||||
* Handler for {@link SocketMessageType#CONNECTED}
|
||||
*/
|
||||
public class HandlerConnected implements Handler {
|
||||
|
||||
/** The socket */
|
||||
private final WebEditorSocket socket;
|
||||
|
||||
public HandlerConnected(WebEditorSocket socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(JsonObject msg) {
|
||||
Message.EDITOR_SOCKET_CONNECTED.send(this.socket.getSender());
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.webeditor.socket.listener;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import me.lucko.luckperms.common.locale.Message;
|
||||
import me.lucko.luckperms.common.webeditor.socket.CryptographyUtils;
|
||||
import me.lucko.luckperms.common.webeditor.socket.SocketMessageType;
|
||||
import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket;
|
||||
import me.lucko.luckperms.common.webeditor.store.RemoteSession;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Handler for {@link SocketMessageType#HELLO}
|
||||
*/
|
||||
public class HandlerHello implements Handler {
|
||||
|
||||
/** The session is accepted, the editor public key is already known so no further action is needed */
|
||||
private static final String STATE_ACCEPTED = "accepted";
|
||||
/** The session is accepted, but the user needs to confirm in-game before any changes will be accepted. */
|
||||
private static final String STATE_UNTRUSTED = "untrusted";
|
||||
/** A session has already been established with a different identity */
|
||||
private static final String STATE_REJECTED = "rejected";
|
||||
/** The remote editor session is based off session data which has already been applied */
|
||||
private static final String STATE_INVALID = "invalid";
|
||||
|
||||
/** The socket */
|
||||
private final WebEditorSocket socket;
|
||||
|
||||
/** A list of attempted connections (connections that have been attempted with an untrusted public key) */
|
||||
private final Map<String, PublicKey> attemptedConnections = new HashMap<>();
|
||||
|
||||
public HandlerHello(WebEditorSocket socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public PublicKey getAttemptedConnection(String nonce) {
|
||||
return this.attemptedConnections.get(nonce);
|
||||
}
|
||||
|
||||
public boolean hasAttemptedConnection() {
|
||||
return !this.attemptedConnections.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(JsonObject msg) {
|
||||
String nonce = getStringOrThrow(msg, "nonce");
|
||||
String sessionId = getStringOrThrow(msg, "sessionId");
|
||||
String browser = msg.get("browser").getAsString();
|
||||
PublicKey remotePublicKey = CryptographyUtils.parsePublicKey(msg.get("publicKey").getAsString());
|
||||
|
||||
// check if the public keys are the same
|
||||
// (this allows the same editor to re-connect, but prevents new connections)
|
||||
if (this.socket.getRemotePublicKey() != null && !this.socket.getRemotePublicKey().equals(remotePublicKey)) {
|
||||
sendReply(nonce, STATE_REJECTED);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if session is valid
|
||||
RemoteSession session = this.socket.getPlugin().getWebEditorStore().sessions().getSession(sessionId);
|
||||
if (session == null || session.isCompleted()) {
|
||||
sendReply(nonce, STATE_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the public key is trusted
|
||||
if (!this.socket.getPlugin().getWebEditorStore().keystore().isTrusted(this.socket.getSender(), remotePublicKey.getEncoded())) {
|
||||
sendReply(nonce, STATE_UNTRUSTED);
|
||||
|
||||
// ask the user if they want to trust the connection
|
||||
Message.EDITOR_SOCKET_UNTRUSTED.send(this.socket.getSender(), nonce, browser, this.socket.getSession().getCommandLabel(), this.socket.getSender().isConsole());
|
||||
this.attemptedConnections.put(nonce, remotePublicKey);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean reconnected = this.socket.getRemotePublicKey() != null;
|
||||
this.socket.setRemotePublicKey(remotePublicKey);
|
||||
|
||||
sendReply(nonce, STATE_ACCEPTED);
|
||||
|
||||
if (reconnected) {
|
||||
Message.EDITOR_SOCKET_RECONNECTED.send(this.socket.getSender());
|
||||
} else {
|
||||
Message.EDITOR_SOCKET_CONNECTED.send(this.socket.getSender());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendReply(String nonce, String state) {
|
||||
this.socket.send(SocketMessageType.HELLO_REPLY.builder()
|
||||
.add("nonce", nonce)
|
||||
.add("state", state)
|
||||
.toJson()
|
||||
);
|
||||
}
|
||||
|
||||
private static String getStringOrThrow(JsonObject msg, String key) {
|
||||
String val = msg.get(key).getAsString();
|
||||
if (val == null || val.isEmpty()) {
|
||||
throw new IllegalStateException("missing " + key);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.webeditor.socket.listener;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import me.lucko.luckperms.common.webeditor.socket.SocketMessageType;
|
||||
import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket;
|
||||
|
||||
/**
|
||||
* Handler for {@link SocketMessageType#PING}
|
||||
*/
|
||||
public class HandlerPing implements Handler {
|
||||
|
||||
/** The socket */
|
||||
private final WebEditorSocket socket;
|
||||
|
||||
/** The time a ping was last received */
|
||||
private long lastPing = 0;
|
||||
|
||||
public HandlerPing(WebEditorSocket socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public long getLastPing() {
|
||||
return this.lastPing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(JsonObject msg) {
|
||||
this.lastPing = System.currentTimeMillis();
|
||||
this.socket.send(SocketMessageType.PONG.builder()
|
||||
.add("ok", true)
|
||||
.toJson()
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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.webeditor.socket.listener;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import me.lucko.luckperms.common.util.gson.GsonProvider;
|
||||
import me.lucko.luckperms.common.webeditor.socket.CryptographyUtils;
|
||||
import me.lucko.luckperms.common.webeditor.socket.SocketMessageType;
|
||||
import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.security.PublicKey;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class WebEditorSocketListener extends WebSocketListener {
|
||||
|
||||
/** The socket */
|
||||
private final WebEditorSocket socket;
|
||||
|
||||
// Individual handlers for each message type
|
||||
private final HandlerHello helloHandler;
|
||||
private final HandlerConnected connectedHandler;
|
||||
private final HandlerPing pingHandler;
|
||||
private final HandlerChangeRequest changeRequestHandler;
|
||||
|
||||
/** A future that will complete when the connection is established successfully */
|
||||
private final CompletableFuture<Void> connectFuture = new CompletableFuture<>();
|
||||
|
||||
/** Message receive lock */
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public WebEditorSocketListener(WebEditorSocket socket) {
|
||||
this.socket = socket;
|
||||
this.helloHandler = new HandlerHello(socket);
|
||||
this.connectedHandler = new HandlerConnected(socket);
|
||||
this.pingHandler = new HandlerPing(socket);
|
||||
this.changeRequestHandler = new HandlerChangeRequest(socket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(@NonNull WebSocket webSocket, @NonNull Response response) {
|
||||
this.connectFuture.complete(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull WebSocket webSocket, @NonNull Throwable e, Response response) {
|
||||
if (e instanceof EOFException) {
|
||||
return; // ignore
|
||||
}
|
||||
this.socket.getPlugin().getLogger().warn("Exception occurred in web socket", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(@NonNull WebSocket webSocket, @NonNull String msg) {
|
||||
this.socket.getPlugin().getBootstrap().getScheduler().executeAsync(() -> {
|
||||
this.lock.lock();
|
||||
try {
|
||||
if (shouldIgnoreMessages()) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleMessageFrame(msg);
|
||||
} catch (Exception e) {
|
||||
this.socket.getPlugin().getLogger().warn("Exception occurred handling message from socket", e);
|
||||
} finally {
|
||||
this.lock.unlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if incoming messages should be ignored.
|
||||
*
|
||||
* @return true if messages should be ignored
|
||||
*/
|
||||
public boolean shouldIgnoreMessages() {
|
||||
if (this.socket.isClosed()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.socket.getSender().isValid()) {
|
||||
this.socket.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleMessageFrame(String stringMsg) {
|
||||
JsonObject frame = GsonProvider.parser().parse(stringMsg).getAsJsonObject();
|
||||
|
||||
String innerMsg = frame.get("msg").getAsString();
|
||||
String signature = frame.get("signature").getAsString();
|
||||
|
||||
if (innerMsg == null || innerMsg.isEmpty() || signature == null || signature.isEmpty()) {
|
||||
throw new IllegalArgumentException("Incomplete message");
|
||||
}
|
||||
|
||||
// check signature to ensure the message is from the connected editor
|
||||
PublicKey remotePublicKey = this.socket.getRemotePublicKey();
|
||||
boolean verified = remotePublicKey == null || CryptographyUtils.verify(remotePublicKey, innerMsg, signature);
|
||||
|
||||
// parse the inner message
|
||||
JsonObject msg = GsonProvider.parser().parse(innerMsg).getAsJsonObject();
|
||||
SocketMessageType type = SocketMessageType.getById(msg.get("type").getAsString());
|
||||
|
||||
if (type == SocketMessageType.HELLO) {
|
||||
this.helloHandler.handle(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!verified) {
|
||||
throw new IllegalStateException("Signature not accepted");
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case CHANGE_REQUEST:
|
||||
this.changeRequestHandler.handle(msg);
|
||||
break;
|
||||
case CONNECTED:
|
||||
this.connectedHandler.handle(msg);
|
||||
break;
|
||||
case PING:
|
||||
this.pingHandler.handle(msg);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Invalid message type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> connectFuture() {
|
||||
return this.connectFuture;
|
||||
}
|
||||
|
||||
public HandlerHello helloHandler() {
|
||||
return this.helloHandler;
|
||||
}
|
||||
|
||||
public HandlerConnected connectedHandler() {
|
||||
return this.connectedHandler;
|
||||
}
|
||||
|
||||
public HandlerPing pingHandler() {
|
||||
return this.pingHandler;
|
||||
}
|
||||
|
||||
public HandlerChangeRequest changeRequestHandler() {
|
||||
return this.changeRequestHandler;
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.webeditor.store;
|
||||
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorRequest;
|
||||
|
||||
public final class RemoteSession {
|
||||
private WebEditorRequest request;
|
||||
private boolean completed;
|
||||
|
||||
public RemoteSession(WebEditorRequest request) {
|
||||
this.request = request;
|
||||
this.completed = false;
|
||||
}
|
||||
|
||||
public WebEditorRequest request() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return this.completed;
|
||||
}
|
||||
|
||||
public void complete() {
|
||||
this.completed = true;
|
||||
this.request = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
/*
|
||||
* 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.webeditor.store;
|
||||
|
||||
import me.lucko.luckperms.common.context.ImmutableContextSetImpl;
|
||||
import me.lucko.luckperms.common.model.User;
|
||||
import me.lucko.luckperms.common.node.types.Meta;
|
||||
import me.lucko.luckperms.common.query.QueryOptionsImpl;
|
||||
import me.lucko.luckperms.common.sender.Sender;
|
||||
import me.lucko.luckperms.common.util.gson.GsonProvider;
|
||||
import me.lucko.luckperms.common.verbose.event.CheckOrigin;
|
||||
|
||||
import net.luckperms.api.model.data.DataType;
|
||||
import net.luckperms.api.node.NodeType;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
public final class WebEditorKeystore {
|
||||
private static final String META_KEY = "lp-editor-key";
|
||||
|
||||
private final Path consoleKeysPath;
|
||||
private final Set<String> trustedConsoleKeys;
|
||||
|
||||
public WebEditorKeystore(Path consoleKeysPath) {
|
||||
this.consoleKeysPath = consoleKeysPath;
|
||||
this.trustedConsoleKeys = new CopyOnWriteArraySet<>();
|
||||
|
||||
try {
|
||||
load();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given public key has been trusted by the sender.
|
||||
*
|
||||
* @param sender the sender
|
||||
* @param publicKey the public key
|
||||
* @return true if trusted
|
||||
*/
|
||||
public boolean isTrusted(Sender sender, byte[] publicKey) {
|
||||
return isTrusted(sender, hash(publicKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given public key hash has been trusted by the sender.
|
||||
*
|
||||
* @param sender the sender
|
||||
* @param hash the public key hash
|
||||
* @return true if trusted
|
||||
*/
|
||||
public boolean isTrusted(Sender sender, String hash) {
|
||||
if (sender.isConsole()) {
|
||||
return isTrustedConsole(hash);
|
||||
} else {
|
||||
User user = sender.getPlugin().getUserManager().getIfLoaded(sender.getUniqueId());
|
||||
return user != null && isTrusted(user, hash);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trusts the given public key for the sender.
|
||||
*
|
||||
* @param sender the sender
|
||||
* @param publicKey the public key
|
||||
*/
|
||||
public void trust(Sender sender, byte[] publicKey) {
|
||||
trust(sender, hash(publicKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trusts the given public key hash for the sender.
|
||||
*
|
||||
* @param sender the sender
|
||||
* @param hash the public key hash
|
||||
*/
|
||||
public void trust(Sender sender, String hash) {
|
||||
if (sender.isConsole()) {
|
||||
trustConsole(hash);
|
||||
} else {
|
||||
User user = sender.getPlugin().getUserManager().getIfLoaded(sender.getUniqueId());
|
||||
if (user != null) {
|
||||
trust(user, hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// console
|
||||
|
||||
private boolean isTrustedConsole(String hash) {
|
||||
return this.trustedConsoleKeys.contains(hash);
|
||||
}
|
||||
|
||||
private void trustConsole(String hash) {
|
||||
this.trustedConsoleKeys.add(hash);
|
||||
|
||||
try {
|
||||
save();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void load() throws Exception {
|
||||
if (Files.exists(this.consoleKeysPath)) {
|
||||
try (BufferedReader reader = Files.newBufferedReader(this.consoleKeysPath, StandardCharsets.UTF_8)) {
|
||||
KeystoreFile file = GsonProvider.normal().fromJson(reader, KeystoreFile.class);
|
||||
if (file != null && file.consoleKeys != null) {
|
||||
this.trustedConsoleKeys.addAll(file.consoleKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void save() throws Exception {
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(this.consoleKeysPath, StandardCharsets.UTF_8)) {
|
||||
KeystoreFile file = new KeystoreFile();
|
||||
file.consoleKeys = new ArrayList<>(this.trustedConsoleKeys);
|
||||
GsonProvider.prettyPrinting().toJson(file, writer);
|
||||
}
|
||||
}
|
||||
|
||||
// users
|
||||
|
||||
private boolean isTrusted(User user, String hash) {
|
||||
String key = user.getCachedData().getMetaData(QueryOptionsImpl.DEFAULT_CONTEXTUAL)
|
||||
.getMetaValue(META_KEY, CheckOrigin.INTERNAL).result();
|
||||
|
||||
if (key == null || key.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hash.equals(key);
|
||||
}
|
||||
|
||||
private void trust(User user, String hash) {
|
||||
user.removeIf(DataType.NORMAL, ImmutableContextSetImpl.EMPTY, NodeType.META.predicate(mn -> mn.getMetaKey().equals(META_KEY)), false);
|
||||
user.setNode(DataType.NORMAL, Meta.builder(META_KEY, hash).build(), false);
|
||||
|
||||
user.getPlugin().getStorage().saveUser(user).join();
|
||||
}
|
||||
|
||||
private static String hash(byte[] buf) {
|
||||
byte[] digest = createDigest().digest(buf);
|
||||
return Base64.getEncoder().encodeToString(digest);
|
||||
}
|
||||
|
||||
private static MessageDigest createDigest() {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"FieldMayBeFinal", "unused"})
|
||||
private static class KeystoreFile {
|
||||
private String _comment = "This file stores a list of trusted editor public keys";
|
||||
private List<String> consoleKeys = null;
|
||||
}
|
||||
}
|
@ -23,45 +23,35 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package me.lucko.luckperms.common.webeditor;
|
||||
package me.lucko.luckperms.common.webeditor.store;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import me.lucko.luckperms.common.webeditor.WebEditorRequest;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Contains a store of known web editor sessions.
|
||||
*/
|
||||
public class WebEditorSessionStore {
|
||||
private final Map<String, SessionState> sessions = new ConcurrentHashMap<>();
|
||||
public final class WebEditorSessionMap {
|
||||
private final Map<String, RemoteSession> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Adds a newly created session to the store.
|
||||
*
|
||||
* @param id the id of the session
|
||||
*/
|
||||
public void addNewSession(String id) {
|
||||
this.sessions.put(id, SessionState.IN_PROGRESS);
|
||||
public void addNewSession(String id, WebEditorRequest request) {
|
||||
this.sessions.put(id, new RemoteSession(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the session state for the given session id.
|
||||
* Gets the session for the given session id.
|
||||
*
|
||||
* @param id the id of the session
|
||||
* @return the session state
|
||||
* @return the session
|
||||
*/
|
||||
public @NonNull SessionState getSessionState(String id) {
|
||||
return this.sessions.getOrDefault(id, SessionState.NOT_KNOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a given session as complete.
|
||||
*
|
||||
* @param id the id of the session
|
||||
*/
|
||||
public void markSessionCompleted(String id) {
|
||||
this.sessions.put(id, SessionState.COMPLETED);
|
||||
public @Nullable RemoteSession getSession(String id) {
|
||||
return this.sessions.get(id);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.webeditor.store;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
|
||||
import me.lucko.luckperms.common.sender.Sender;
|
||||
import me.lucko.luckperms.common.util.CaffeineFactory;
|
||||
import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class WebEditorSocketMap {
|
||||
private final Cache<UUID, WebEditorSocket> sockets = CaffeineFactory.newBuilder()
|
||||
.weakValues()
|
||||
.expireAfterWrite(5, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
public WebEditorSocket getSocket(Sender sender) {
|
||||
return this.sockets.getIfPresent(sender.getUniqueId());
|
||||
}
|
||||
|
||||
public void putSocket(Sender sender, WebEditorSocket socket) {
|
||||
this.sockets.put(sender.getUniqueId(), socket);
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.webeditor.store;
|
||||
|
||||
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
|
||||
|
||||
/**
|
||||
* Contains a store of known web editor sessions and provides a lookup function for
|
||||
* trusted editor public keys.
|
||||
*/
|
||||
public class WebEditorStore {
|
||||
private final WebEditorSessionMap sessions;
|
||||
private final WebEditorSocketMap sockets;
|
||||
private final WebEditorKeystore keystore;
|
||||
|
||||
public WebEditorStore(LuckPermsPlugin plugin) {
|
||||
this.sessions = new WebEditorSessionMap();
|
||||
this.sockets = new WebEditorSocketMap();
|
||||
this.keystore = new WebEditorKeystore(plugin.getBootstrap().getConfigDirectory().resolve("editor-keystore.json"));
|
||||
}
|
||||
|
||||
public WebEditorSessionMap sessions() {
|
||||
return this.sessions;
|
||||
}
|
||||
|
||||
public WebEditorSocketMap sockets() {
|
||||
return this.sockets;
|
||||
}
|
||||
|
||||
public WebEditorKeystore keystore() {
|
||||
return this.keystore;
|
||||
}
|
||||
|
||||
}
|
@ -91,6 +91,18 @@ luckperms.command.misc.loading.error.track-invalid={0} is not a valid track name
|
||||
luckperms.command.editor.no-match=Unable to open editor, no objects matched the desired type
|
||||
luckperms.command.editor.start=Preparing a new editor session, please wait...
|
||||
luckperms.command.editor.url=Click the link below to open the editor
|
||||
luckperms.command.editor.socket.connected=Editor window connected successfully
|
||||
luckperms.command.editor.socket.reconnected=Editor window reconnected successfully
|
||||
luckperms.command.editor.socket.changes-received=Changes have been received from the connected web editor session
|
||||
luckperms.command.editor.socket.untrusted=An editor window has connected, but it is not yet trusted
|
||||
luckperms.command.editor.socket.untrusted.prompt.click=If it was you, {0} to trust the session!
|
||||
luckperms.command.editor.socket.untrusted.prompt.click.action=click here
|
||||
luckperms.command.editor.socket.untrusted.prompt.runcommand=If it was you, run {0} to trust the session!
|
||||
luckperms.command.editor.socket.untrusted.sessioninfo=session id = {0}, browser = {1}
|
||||
luckperms.command.editor.socket.trust.success=The editor session has been marked as trusted
|
||||
luckperms.command.editor.socket.trust.futureinfo=In the future, connections from the same browser will be trusted automatically
|
||||
luckperms.command.editor.socket.trust.connecting=The plugin will now attempt to establish a connection with the editor...
|
||||
luckperms.command.editor.socket.trust.failure=Unable to trust the given session because the socket is closed, or because a different connection was established instead
|
||||
luckperms.command.editor.unable-to-communicate=Unable to communicate with the editor
|
||||
luckperms.command.editor.apply-edits.success=Web editor data was applied to {0} {1} successfully
|
||||
luckperms.command.editor.apply-edits.success-summary={0} {1} and {2} {3}
|
||||
|
@ -27,16 +27,16 @@ package me.lucko.luckperms.fabric.listeners;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
|
||||
import me.lucko.luckperms.fabric.LPFabricPlugin;
|
||||
import me.lucko.luckperms.common.api.implementation.ApiGroup;
|
||||
import me.lucko.luckperms.common.cache.BufferedRequest;
|
||||
import me.lucko.luckperms.common.event.LuckPermsEventListener;
|
||||
import me.lucko.luckperms.common.util.CaffeineFactory;
|
||||
import me.lucko.luckperms.common.api.implementation.ApiGroup;
|
||||
import me.lucko.luckperms.fabric.LPFabricPlugin;
|
||||
|
||||
import net.luckperms.api.event.EventBus;
|
||||
import net.luckperms.api.event.context.ContextUpdateEvent;
|
||||
import net.luckperms.api.event.user.UserDataRecalculateEvent;
|
||||
import net.luckperms.api.event.group.GroupDataRecalculateEvent;
|
||||
import net.luckperms.api.event.user.UserDataRecalculateEvent;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
|
||||
import java.util.UUID;
|
||||
|
@ -29,6 +29,7 @@ import me.lucko.luckperms.fabric.event.PreExecuteCommandCallback;
|
||||
|
||||
import net.minecraft.server.command.CommandManager;
|
||||
import net.minecraft.server.command.ServerCommandSource;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
|
Loading…
Reference in New Issue
Block a user