Skip to content

Commit 2f5ddf6

Browse files
authored
feat: add ProtocolLib support for deeper-level packet cancellation (#274)
* feat: add support for ProtocolLib packet-level state cancelling * refactor: move commands to event listener, document ProtocolLib support * docs: make Setup less claustrophobic * fix: remove `@Getter` on `PlayerPacketAdapter` * build: add missing license headers * fix: inaccessible method on Paper * test: add ProtocolLib to network spin test * fix: whoops I targeted the wrong packets * fix: bad command disabled check logic * fix: final protocollib adjustments
1 parent 4dfbc0e commit 2f5ddf6

File tree

14 files changed

+308
-120
lines changed

14 files changed

+308
-120
lines changed

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ allprojects {
7070
mavenLocal()
7171
mavenCentral()
7272
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
73+
maven { url "https://repo.dmulloy2.net/repository/public/" }
7374
maven { url 'https://repo.codemc.io/repository/maven-public/' }
7475
maven { url 'https://repo.minebench.de/' }
7576
maven { url 'https://repo.alessiodp.com/releases/' }

bukkit/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies {
1313
implementation 'de.tr7zw:item-nbt-api:2.12.3'
1414

1515
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
16+
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
1617
compileOnly 'org.projectlombok:lombok:1.18.32'
1718
compileOnly 'commons-io:commons-io:2.16.0'
1819
compileOnly 'org.json:json:20240303'

bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java

+22-104
Original file line numberDiff line numberDiff line change
@@ -24,41 +24,37 @@
2424
import net.william278.husksync.data.BukkitData;
2525
import net.william278.husksync.user.BukkitUser;
2626
import net.william278.husksync.user.OnlineUser;
27-
import org.bukkit.Bukkit;
2827
import org.bukkit.entity.Player;
29-
import org.bukkit.entity.Projectile;
30-
import org.bukkit.event.Cancellable;
3128
import org.bukkit.event.EventHandler;
3229
import org.bukkit.event.EventPriority;
3330
import org.bukkit.event.Listener;
34-
import org.bukkit.event.block.BlockBreakEvent;
35-
import org.bukkit.event.block.BlockPlaceEvent;
36-
import org.bukkit.event.entity.EntityDamageEvent;
37-
import org.bukkit.event.entity.EntityPickupItemEvent;
3831
import org.bukkit.event.entity.PlayerDeathEvent;
39-
import org.bukkit.event.entity.ProjectileLaunchEvent;
40-
import org.bukkit.event.inventory.InventoryClickEvent;
41-
import org.bukkit.event.inventory.InventoryOpenEvent;
42-
import org.bukkit.event.inventory.PrepareItemCraftEvent;
43-
import org.bukkit.event.player.*;
32+
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
4433
import org.bukkit.event.server.MapInitializeEvent;
4534
import org.bukkit.event.world.WorldSaveEvent;
4635
import org.bukkit.inventory.ItemStack;
4736
import org.jetbrains.annotations.NotNull;
4837

49-
import java.util.List;
50-
import java.util.Locale;
51-
import java.util.UUID;
5238
import java.util.stream.Collectors;
5339

5440
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
5541
BukkitDeathEventListener, Listener {
56-
protected final List<String> blacklistedCommands;
5742

58-
public BukkitEventListener(@NotNull BukkitHuskSync huskSync) {
59-
super(huskSync);
60-
this.blacklistedCommands = huskSync.getSettings().getSynchronization().getBlacklistedCommandsWhileLocked();
61-
Bukkit.getServer().getPluginManager().registerEvents(this, huskSync);
43+
protected final LockedHandler lockedHandler;
44+
45+
public BukkitEventListener(@NotNull BukkitHuskSync plugin) {
46+
super(plugin);
47+
plugin.getServer().getPluginManager().registerEvents(this, plugin);
48+
this.lockedHandler = createLockedHandler(plugin);
49+
}
50+
51+
@NotNull
52+
private LockedHandler createLockedHandler(@NotNull BukkitHuskSync plugin) {
53+
if (getPlugin().isDependencyLoaded("ProtocolLib") && getPlugin().getSettings().isCancelPackets()) {
54+
return new BukkitLockedPacketListener(plugin);
55+
} else {
56+
return new BukkitLockedEventListener(plugin);
57+
}
6258
}
6359

6460
@Override
@@ -88,7 +84,7 @@ public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
8884
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
8985

9086
// If the player is locked or the plugin disabling, clear their drops
91-
if (cancelPlayerEvent(user.getUuid())) {
87+
if (lockedHandler.cancelPlayerEvent(user.getUuid())) {
9288
event.getDrops().clear();
9389
return;
9490
}
@@ -125,91 +121,13 @@ public void onMapInitialize(@NotNull MapInitializeEvent event) {
125121
}
126122
}
127123

128-
129-
/*
130-
* Events to cancel if the player has not been set yet
131-
*/
132-
133-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
134-
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
135-
final Projectile projectile = event.getEntity();
136-
if (projectile.getShooter() instanceof Player player) {
137-
cancelPlayerEvent(player.getUniqueId(), event);
138-
}
139-
}
140-
141-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
142-
public void onDropItem(@NotNull PlayerDropItemEvent event) {
143-
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
144-
}
145-
146-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
147-
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
148-
if (event.getEntity() instanceof Player player) {
149-
cancelPlayerEvent(player.getUniqueId(), event);
150-
}
151-
}
152-
153-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
154-
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
155-
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
156-
}
157-
158-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
159-
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
160-
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
161-
}
162-
163-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
164-
public void onPlayerInteractArmorStand(@NotNull PlayerArmorStandManipulateEvent event) {
165-
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
166-
}
167-
168-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
169-
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
170-
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
171-
}
172-
173-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
174-
public void onBlockBreak(@NotNull BlockBreakEvent event) {
175-
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
176-
}
177-
178-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
179-
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
180-
if (event.getPlayer() instanceof Player player) {
181-
cancelPlayerEvent(player.getUniqueId(), event);
182-
}
183-
}
184-
185-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
186-
public void onInventoryClick(@NotNull InventoryClickEvent event) {
187-
cancelPlayerEvent(event.getWhoClicked().getUniqueId(), event);
188-
}
189-
190-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
191-
public void onCraftItem(@NotNull PrepareItemCraftEvent event) {
192-
}
193-
194-
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
195-
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
196-
if (event.getEntity() instanceof Player player) {
197-
cancelPlayerEvent(player.getUniqueId(), event);
198-
}
199-
}
200-
124+
// We handle commands here to allow specific command handling on ProtocolLib servers
201125
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
202-
public void onPermissionCommand(@NotNull PlayerCommandPreprocessEvent event) {
203-
final String[] commandArgs = event.getMessage().substring(1).split(" ");
204-
final String commandLabel = commandArgs[0].toLowerCase(Locale.ENGLISH);
205-
206-
if (blacklistedCommands.contains("*") || blacklistedCommands.contains(commandLabel)) {
207-
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
126+
public void onCommandProcessed(@NotNull PlayerCommandPreprocessEvent event) {
127+
if (!lockedHandler.isCommandDisabled(event.getMessage().substring(1).split(" ")[0])) {
128+
return;
208129
}
209-
}
210-
211-
private void cancelPlayerEvent(@NotNull UUID uuid, @NotNull Cancellable event) {
212-
if (cancelPlayerEvent(uuid)) {
130+
if (lockedHandler.cancelPlayerEvent(event.getPlayer().getUniqueId())) {
213131
event.setCancelled(true);
214132
}
215133
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* This file is part of HuskSync, licensed under the Apache License 2.0.
3+
*
4+
* Copyright (c) William278 <will27528@gmail.com>
5+
* Copyright (c) contributors
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package net.william278.husksync.listener;
21+
22+
import lombok.Getter;
23+
import net.william278.husksync.BukkitHuskSync;
24+
import org.bukkit.entity.Player;
25+
import org.bukkit.entity.Projectile;
26+
import org.bukkit.event.Cancellable;
27+
import org.bukkit.event.EventHandler;
28+
import org.bukkit.event.EventPriority;
29+
import org.bukkit.event.Listener;
30+
import org.bukkit.event.block.BlockBreakEvent;
31+
import org.bukkit.event.block.BlockPlaceEvent;
32+
import org.bukkit.event.entity.EntityDamageEvent;
33+
import org.bukkit.event.entity.EntityPickupItemEvent;
34+
import org.bukkit.event.entity.ProjectileLaunchEvent;
35+
import org.bukkit.event.inventory.InventoryClickEvent;
36+
import org.bukkit.event.inventory.InventoryOpenEvent;
37+
import org.bukkit.event.player.PlayerArmorStandManipulateEvent;
38+
import org.bukkit.event.player.PlayerDropItemEvent;
39+
import org.bukkit.event.player.PlayerInteractEntityEvent;
40+
import org.bukkit.event.player.PlayerInteractEvent;
41+
import org.jetbrains.annotations.NotNull;
42+
43+
import java.util.UUID;
44+
45+
@Getter
46+
public class BukkitLockedEventListener implements LockedHandler, Listener {
47+
48+
protected final BukkitHuskSync plugin;
49+
50+
protected BukkitLockedEventListener(@NotNull BukkitHuskSync plugin) {
51+
this.plugin = plugin;
52+
plugin.getServer().getPluginManager().registerEvents(this, plugin);
53+
}
54+
55+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
56+
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
57+
final Projectile projectile = event.getEntity();
58+
if (projectile.getShooter() instanceof Player player) {
59+
cancelPlayerEvent(player.getUniqueId(), event);
60+
}
61+
}
62+
63+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
64+
public void onDropItem(@NotNull PlayerDropItemEvent event) {
65+
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
66+
}
67+
68+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
69+
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
70+
if (event.getEntity() instanceof Player player) {
71+
cancelPlayerEvent(player.getUniqueId(), event);
72+
}
73+
}
74+
75+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
76+
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
77+
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
78+
}
79+
80+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
81+
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
82+
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
83+
}
84+
85+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
86+
public void onPlayerInteractArmorStand(@NotNull PlayerArmorStandManipulateEvent event) {
87+
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
88+
}
89+
90+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
91+
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
92+
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
93+
}
94+
95+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
96+
public void onBlockBreak(@NotNull BlockBreakEvent event) {
97+
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
98+
}
99+
100+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
101+
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
102+
if (event.getPlayer() instanceof Player player) {
103+
cancelPlayerEvent(player.getUniqueId(), event);
104+
}
105+
}
106+
107+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
108+
public void onInventoryClick(@NotNull InventoryClickEvent event) {
109+
cancelPlayerEvent(event.getWhoClicked().getUniqueId(), event);
110+
}
111+
112+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
113+
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
114+
if (event.getEntity() instanceof Player player) {
115+
cancelPlayerEvent(player.getUniqueId(), event);
116+
}
117+
}
118+
119+
private void cancelPlayerEvent(@NotNull UUID uuid, @NotNull Cancellable event) {
120+
if (cancelPlayerEvent(uuid)) {
121+
event.setCancelled(true);
122+
}
123+
}
124+
125+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* This file is part of HuskSync, licensed under the Apache License 2.0.
3+
*
4+
* Copyright (c) William278 <will27528@gmail.com>
5+
* Copyright (c) contributors
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package net.william278.husksync.listener;
21+
22+
import com.comphenix.protocol.PacketType;
23+
import com.comphenix.protocol.ProtocolLibrary;
24+
import com.comphenix.protocol.events.ListenerPriority;
25+
import com.comphenix.protocol.events.PacketAdapter;
26+
import com.comphenix.protocol.events.PacketEvent;
27+
import com.google.common.collect.Sets;
28+
import net.william278.husksync.BukkitHuskSync;
29+
import org.jetbrains.annotations.NotNull;
30+
31+
import java.util.Set;
32+
import java.util.logging.Level;
33+
34+
import static com.comphenix.protocol.PacketType.Play.Client;
35+
36+
public class BukkitLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
37+
38+
protected BukkitLockedPacketListener(@NotNull BukkitHuskSync plugin) {
39+
super(plugin);
40+
ProtocolLibrary.getProtocolManager().addPacketListener(new PlayerPacketAdapter(this));
41+
plugin.log(Level.INFO, "Using ProtocolLib to cancel packets for locked players");
42+
}
43+
44+
private static class PlayerPacketAdapter extends PacketAdapter {
45+
46+
// Packets we want the player to still be able to send/receiver to/from the server
47+
private static final Set<PacketType> ALLOWED_PACKETS = Set.of(
48+
Client.KEEP_ALIVE, Client.PONG, // Connection packets
49+
Client.CHAT_COMMAND, Client.CHAT, Client.CHAT_SESSION_UPDATE, // Chat / command packets
50+
Client.POSITION, Client.POSITION_LOOK, Client.LOOK
51+
);
52+
53+
private final BukkitLockedPacketListener listener;
54+
55+
public PlayerPacketAdapter(@NotNull BukkitLockedPacketListener listener) {
56+
super(listener.getPlugin(), ListenerPriority.HIGHEST, getPacketsToListenFor());
57+
this.listener = listener;
58+
}
59+
60+
@Override
61+
public void onPacketReceiving(@NotNull PacketEvent event) {
62+
if (listener.cancelPlayerEvent(event.getPlayer().getUniqueId()) && !event.isReadOnly()) {
63+
event.setCancelled(true);
64+
}
65+
}
66+
67+
@Override
68+
public void onPacketSending(PacketEvent event) {
69+
if (listener.cancelPlayerEvent(event.getPlayer().getUniqueId()) && !event.isReadOnly()) {
70+
event.setCancelled(true);
71+
}
72+
}
73+
74+
// Returns the set of ALL Server packets, excluding the set of allowed packets
75+
@NotNull
76+
private static Set<PacketType> getPacketsToListenFor() {
77+
return Sets.difference(Client.getInstance().values(), ALLOWED_PACKETS);
78+
}
79+
80+
}
81+
82+
}

bukkit/src/main/resources/plugin.yml

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ author: 'William278'
66
description: '${description}'
77
website: 'https://william278.net'
88
softdepend:
9+
- 'ProtocolLib'
910
- 'MysqlPlayerDataBridge'
1011
- 'Plan'
1112
libraries:

0 commit comments

Comments
 (0)