Web editor socket connection (#3303)

This commit is contained in:
lucko 2022-02-07 19:13:09 +00:00 committed by GitHub
parent f61d9ff9f0
commit d3029a8467
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 2380 additions and 417 deletions

View File

@ -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 {

View File

@ -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())

View File

@ -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),

View File

@ -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>",

View File

@ -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);
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;
}

View File

@ -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
*

View File

@ -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;
}
}
}

View File

@ -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)

View File

@ -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 {

View File

@ -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;
}
}
}

View File

@ -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);
}

View File

@ -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 {

View File

@ -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));
}

View File

@ -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

View File

@ -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.

View File

@ -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()) {

View File

@ -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;
}
}
}

View File

@ -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) {

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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()
);
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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()
);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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}

View File

@ -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;

View File

@ -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;