398 lines
17 KiB
Java
398 lines
17 KiB
Java
/*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package fr.neatmonster.nocheatplus.checks.net.protocollib;
|
|
|
|
import java.util.Arrays;
|
|
import java.util.HashSet;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.plugin.Plugin;
|
|
|
|
import com.comphenix.protocol.PacketType;
|
|
import com.comphenix.protocol.PacketType.Protocol;
|
|
import com.comphenix.protocol.PacketType.Sender;
|
|
import com.comphenix.protocol.events.ListenerPriority;
|
|
import com.comphenix.protocol.events.PacketContainer;
|
|
import com.comphenix.protocol.events.PacketEvent;
|
|
import com.comphenix.protocol.reflect.StructureModifier;
|
|
|
|
import fr.neatmonster.nocheatplus.NCPAPIProvider;
|
|
import fr.neatmonster.nocheatplus.checks.CheckType;
|
|
import fr.neatmonster.nocheatplus.checks.net.FlyingFrequency;
|
|
import fr.neatmonster.nocheatplus.checks.net.NetConfig;
|
|
import fr.neatmonster.nocheatplus.checks.net.NetData;
|
|
import fr.neatmonster.nocheatplus.checks.net.model.DataPacketFlying;
|
|
import fr.neatmonster.nocheatplus.checks.net.model.DataPacketFlying.PACKET_CONTENT;
|
|
import fr.neatmonster.nocheatplus.checks.net.model.TeleportQueue.AckReference;
|
|
import fr.neatmonster.nocheatplus.compat.AlmostBoolean;
|
|
import fr.neatmonster.nocheatplus.logging.StaticLog;
|
|
import fr.neatmonster.nocheatplus.logging.Streams;
|
|
import fr.neatmonster.nocheatplus.players.DataManager;
|
|
import fr.neatmonster.nocheatplus.players.IPlayerData;
|
|
import fr.neatmonster.nocheatplus.time.monotonic.Monotonic;
|
|
import fr.neatmonster.nocheatplus.utilities.CheckUtils;
|
|
import fr.neatmonster.nocheatplus.utilities.StringUtil;
|
|
import fr.neatmonster.nocheatplus.utilities.ds.count.ActionFrequency;
|
|
import fr.neatmonster.nocheatplus.utilities.location.LocUtil;
|
|
import fr.neatmonster.nocheatplus.worlds.IWorldData;
|
|
|
|
/**
|
|
* Run checks related to moving (pos/look/flying). Skip packets that shouldn't
|
|
* get processed anyway due to a teleport. Also update lastKeepAliveTime.
|
|
*
|
|
* @author dev1mc
|
|
*
|
|
*/
|
|
public class MovingFlying extends BaseAdapter {
|
|
|
|
// Setup for flying packets.
|
|
public static final int indexOnGround = 0;
|
|
public static final int indexhasPos = 1;
|
|
public static final int indexhasLook = 2;
|
|
public static final int indexX = 0;
|
|
public static final int indexY = 1;
|
|
public static final int indexZ = 2;
|
|
/** 1.7.10 */
|
|
public static final int indexStance = 3;
|
|
public static final int indexYaw = 0;
|
|
public static final int indexPitch = 1;
|
|
|
|
private static PacketType[] initPacketTypes() {
|
|
final List<PacketType> types = new LinkedList<PacketType>(Arrays.asList(
|
|
PacketType.Play.Client.FLYING,
|
|
PacketType.Play.Client.LOOK,
|
|
PacketType.Play.Client.POSITION,
|
|
PacketType.Play.Client.POSITION_LOOK
|
|
));
|
|
// Add confirm teleport.
|
|
// PacketPlayInTeleportAccept
|
|
PacketType confirmType = ProtocolLibComponent.findPacketTypeByName(Protocol.PLAY, Sender.CLIENT, "PacketPlayInTeleportAccept");
|
|
if (confirmType != null) {
|
|
StaticLog.logInfo("Confirm teleport packet available (via name): " + confirmType);
|
|
types.add(confirmType);
|
|
}
|
|
return types.toArray(new PacketType[types.size()]);
|
|
}
|
|
|
|
|
|
/** Frequency check for flying packets. */
|
|
private final FlyingFrequency flyingFrequency = new FlyingFrequency();
|
|
|
|
private final int idFlying = counters.registerKey("packet.flying");
|
|
private final int idAsyncFlying = counters.registerKey("packet.flying.asynchronous");
|
|
|
|
/**
|
|
* If a packet can't be parsed, this time stamp is set for occasional
|
|
* logging.
|
|
*/
|
|
private long packetMismatch = Long.MIN_VALUE;
|
|
private long packetMismatchLogFrequency = 60000; // Every minute max, good for updating :).
|
|
|
|
private final HashSet<PACKET_CONTENT> validContent = new LinkedHashSet<PACKET_CONTENT>();
|
|
private final PacketType confirmTeleportType = ProtocolLibComponent.findPacketTypeByName(Protocol.PLAY, Sender.CLIENT, "PacketPlayInTeleportAccept");
|
|
private boolean acceptConfirmTeleportPackets = confirmTeleportType != null;
|
|
|
|
public MovingFlying(Plugin plugin) {
|
|
// PacketPlayInFlying[3, legacy: 10]
|
|
super(plugin, ListenerPriority.LOW, initPacketTypes());
|
|
// Keep the CheckType NET for now.
|
|
// Add feature tags for checks.
|
|
if (NCPAPIProvider.getNoCheatPlusAPI().getWorldDataManager().isActiveAnywhere(
|
|
CheckType.NET_FLYINGFREQUENCY)) {
|
|
NCPAPIProvider.getNoCheatPlusAPI().addFeatureTags(
|
|
"checks", Arrays.asList(FlyingFrequency.class.getSimpleName()));
|
|
}
|
|
NCPAPIProvider.getNoCheatPlusAPI().addComponent(flyingFrequency);
|
|
}
|
|
|
|
@Override
|
|
public void onPacketReceiving(final PacketEvent event) {
|
|
|
|
if (event.getPacketType().equals(confirmTeleportType)) {
|
|
if (acceptConfirmTeleportPackets) {
|
|
onConfirmTeleportPacket(event);
|
|
}
|
|
}
|
|
else {
|
|
onFlyingPacket(event);
|
|
}
|
|
|
|
}
|
|
|
|
private void onConfirmTeleportPacket(final PacketEvent event) {
|
|
try {
|
|
processConfirmTeleport(event);
|
|
}
|
|
catch (Throwable t) {
|
|
noConfirmTeleportPacket();
|
|
}
|
|
}
|
|
|
|
private void processConfirmTeleport(final PacketEvent event) {
|
|
final PacketContainer packet = event.getPacket();
|
|
final StructureModifier<Integer> integers = packet.getIntegers();
|
|
if (integers.size() != 1) {
|
|
noConfirmTeleportPacket();
|
|
return;
|
|
}
|
|
// TODO: Cross check legacy types (if they even had an integer).
|
|
Integer teleportId = integers.read(0);
|
|
if (teleportId == null) {
|
|
// TODO: Not sure ...
|
|
return;
|
|
}
|
|
final Player player = event.getPlayer();
|
|
final IPlayerData pData = DataManager.getPlayerDataSafe(player);
|
|
final NetData data = pData.getGenericInstance(NetData.class);
|
|
final AlmostBoolean matched = data.teleportQueue.processAck(teleportId);
|
|
if (matched.decideOptimistically()) {
|
|
ActionFrequency.subtract(System.currentTimeMillis(), 1, data.flyingFrequencyAll);
|
|
}
|
|
if (pData.isDebugActive(this.checkType)) { // TODO: FlyingFrequency / NET_MOVING? + check others who depend
|
|
debug(player, "Confirm teleport packet" + (matched.decideOptimistically() ? (" (matched=" + matched + ")") : "") + ": " + teleportId);
|
|
}
|
|
}
|
|
|
|
private void noConfirmTeleportPacket() {
|
|
acceptConfirmTeleportPackets = false;
|
|
// TODO: Attempt to unregister.
|
|
NCPAPIProvider.getNoCheatPlusAPI().getLogManager().info(Streams.STATUS, "Confirm teleport packet not available.");
|
|
}
|
|
|
|
private void onFlyingPacket(final PacketEvent event) {
|
|
final boolean primaryThread = Bukkit.isPrimaryThread(); // TODO: Code review protocol plugin :p.
|
|
counters.add(idFlying, 1, primaryThread);
|
|
if (event.isAsync() == primaryThread) {
|
|
counters.add(ProtocolLibComponent.idInconsistentIsAsync, 1, primaryThread);
|
|
}
|
|
if (!primaryThread) {
|
|
// Count all asynchronous events extra.
|
|
counters.addSynchronized(idAsyncFlying, 1);
|
|
// TODO: Detect game phase for the player?
|
|
}
|
|
final long time = System.currentTimeMillis();
|
|
final Player player = event.getPlayer();
|
|
if (player == null) {
|
|
// TODO: Need config?
|
|
counters.add(ProtocolLibComponent.idNullPlayer, 1, primaryThread);
|
|
event.setCancelled(true);
|
|
return;
|
|
}
|
|
|
|
final IPlayerData pData = DataManager.getPlayerDataSafe(player);
|
|
// Always update last received time.
|
|
final NetData data = pData.getGenericInstance(NetData.class);
|
|
data.lastKeepAliveTime = time; // Update without much of a contract.
|
|
// TODO: Leniency options too (packet order inversion). -> current: flyingQueue is fetched.
|
|
final IWorldData worldData = pData.getCurrentWorldDataSafe();
|
|
if (!worldData.isCheckActive(CheckType.NET_FLYINGFREQUENCY)) {
|
|
return;
|
|
}
|
|
|
|
final NetConfig cc = pData.getGenericInstance(NetConfig.class);
|
|
boolean cancel = false;
|
|
|
|
// Interpret the packet content.
|
|
final DataPacketFlying packetData = interpretPacket(event, time);
|
|
|
|
// Early return tests, if the packet can be interpreted.
|
|
boolean skipFlyingFrequency = false;
|
|
if (packetData != null) {
|
|
// Prevent processing packets with obviously malicious content.
|
|
if (isInvalidContent(packetData)) {
|
|
// TODO: extra actions: log and kick (cancel state is not evaluated)
|
|
event.setCancelled(true);
|
|
if (pData.isDebugActive(this.checkType)) {
|
|
debug(player, "Incoming packet, cancel due to malicious content: " + packetData.toString());
|
|
}
|
|
return;
|
|
}
|
|
switch(data.teleportQueue.processAck(packetData)) {
|
|
case WAITING: {
|
|
if (pData.isDebugActive(this.checkType)) {
|
|
debug(player, "Incoming packet, still waiting for ACK on outgoing position.");
|
|
}
|
|
if (confirmTeleportType != null && cc.supersededFlyingCancelWaiting) {
|
|
// Don't add to the flying queue for now (assumed invalid).
|
|
final AckReference ackReference = data.teleportQueue.getLastAckReference();
|
|
if (ackReference.lastOutgoingId != Integer.MIN_VALUE
|
|
&& ackReference.lastOutgoingId != ackReference.maxConfirmedId) {
|
|
// Still waiting for a 'confirm teleport' packet. More or less safe to cancel this out.
|
|
/*
|
|
* TODO: The actual issue with this, apart from
|
|
* potential freezing, also concerns gameplay experience
|
|
* in case of minor set backs, which also could be
|
|
* caused by the server, e.g. with 'moved wrongly' or
|
|
* setting players outside of blocks. In this case the
|
|
* moves sent before teleport ack would still be valid
|
|
* after the teleport, because distances are small. The
|
|
* actual solution should still be to a) not have false
|
|
* positives b) somehow get rid all the
|
|
* position-correction teleporting the server does, for
|
|
* the cases a plugin can handle.
|
|
*/
|
|
// TODO: Timeout -> either skip cancel or schedule a set back (to last valid pos or other).
|
|
// TODO: Config?
|
|
cancel = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ACK: {
|
|
// Skip processing ACK packets, no cancel.
|
|
skipFlyingFrequency = true;
|
|
if (pData.isDebugActive(this.checkType)) {
|
|
debug(player, "Incoming packet, interpret as ACK for outgoing position.");
|
|
}
|
|
}
|
|
default: {
|
|
// Continue.
|
|
data.addFlyingQueue(packetData); // TODO: Not the optimal position, perhaps.
|
|
}
|
|
}
|
|
// Add as valid packet (exclude invalid coordinates etc. for now).
|
|
validContent.add(packetData.getSimplifiedContentType());
|
|
}
|
|
|
|
// TODO: Counters for hasPos, hasLook, both, none.
|
|
|
|
// Actual packet frequency check.
|
|
// TODO: Consider using the NetStatic check.
|
|
if (!cancel && !skipFlyingFrequency
|
|
&& !pData.hasBypass(CheckType.NET_FLYINGFREQUENCY, player)
|
|
&& flyingFrequency.check(player, packetData, time, data, cc, pData)) {
|
|
cancel = true;
|
|
}
|
|
|
|
// TODO: Run other checks based on the packet content.
|
|
|
|
// Cancel redundant packets, when frequency is high anyway.
|
|
// TODO: Recode to detect cheating in a more reliable way, normally this is not the primary thread.
|
|
// if (!cancel && primaryThread && packetData != null && cc.flyingFrequencyRedundantActive && checkRedundantPackets(player, packetData, allScore, time, data, cc)) {
|
|
// event.setCancelled(true);
|
|
// }
|
|
|
|
// Process cancel and debug log.
|
|
if (cancel) {
|
|
event.setCancelled(true);
|
|
}
|
|
if (pData.isDebugActive(this.checkType)) {
|
|
debug(player, (packetData == null ? "(Incompatible data)" : packetData.toString()) + (event.isCancelled() ? " CANCEL" : ""));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private boolean isInvalidContent(final DataPacketFlying packetData) {
|
|
if (packetData.hasPos && LocUtil.isBadCoordinate(packetData.getX(), packetData.getY(), packetData.getZ())) {
|
|
return true;
|
|
}
|
|
if (packetData.hasLook && LocUtil.isBadCoordinate(packetData.getYaw(), packetData.getPitch())) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Interpret the packet content and do with it whatever is suitable.
|
|
* @param player
|
|
* @param event
|
|
* @param allScore
|
|
* @param time
|
|
* @param data
|
|
* @param cc
|
|
* @return Packet data if successful, or null on packet mismatch.
|
|
*/
|
|
private DataPacketFlying interpretPacket(final PacketEvent event, final long time) {
|
|
|
|
final PacketContainer packet = event.getPacket();
|
|
final List<Boolean> booleans = packet.getBooleans().getValues();
|
|
if (booleans.size() != 3) {
|
|
packetMismatch(event);
|
|
return null;
|
|
}
|
|
final boolean onGround = booleans.get(MovingFlying.indexOnGround).booleanValue();
|
|
final boolean hasPos = booleans.get(MovingFlying.indexhasPos).booleanValue();
|
|
final boolean hasLook = booleans.get(MovingFlying.indexhasLook).booleanValue();
|
|
|
|
if (!hasPos && !hasLook) {
|
|
return new DataPacketFlying(onGround, time);
|
|
}
|
|
final List<Double> doubles;
|
|
final List<Float> floats;
|
|
|
|
if (hasPos) {
|
|
doubles = packet.getDoubles().getValues();
|
|
if (doubles.size() != 3 && doubles.size() != 4) {
|
|
// 3: 1.8, 4: 1.7.10 and before (stance).
|
|
packetMismatch(event);
|
|
return null;
|
|
}
|
|
// TODO: before 1.8: stance (should make possible to reject in isInvalidContent).
|
|
}
|
|
else {
|
|
doubles = null;
|
|
}
|
|
|
|
if (hasLook) {
|
|
floats = packet.getFloat().getValues();
|
|
if (floats.size() != 2) {
|
|
packetMismatch(event);
|
|
return null;
|
|
}
|
|
}
|
|
else {
|
|
floats = null;
|
|
}
|
|
if (hasPos && hasLook) {
|
|
return new DataPacketFlying(onGround, doubles.get(indexX), doubles.get(indexY), doubles.get(indexZ), floats.get(indexYaw), floats.get(indexPitch), time);
|
|
}
|
|
else if (hasLook) {
|
|
return new DataPacketFlying(onGround, floats.get(indexYaw), floats.get(indexPitch), time);
|
|
}
|
|
else if (hasPos) {
|
|
return new DataPacketFlying(onGround, doubles.get(indexX), doubles.get(indexY), doubles.get(indexZ), time);
|
|
}
|
|
else {
|
|
throw new IllegalStateException("Can't be, it can't be!");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log warning to console, stop interpreting packet content.
|
|
*/
|
|
private void packetMismatch(final PacketEvent packetEvent) {
|
|
final long time = Monotonic.synchMillis();
|
|
if (time - packetMismatchLogFrequency > packetMismatch) {
|
|
packetMismatch = time;
|
|
StringBuilder builder = new StringBuilder(512);
|
|
builder.append(CheckUtils.getLogMessagePrefix(packetEvent.getPlayer(), checkType));
|
|
builder.append("Incoming packet could not be interpreted. Are server and plugins up to date (NCP/ProtocolLib...)? This message is logged every ");
|
|
builder.append(Long.toString(packetMismatchLogFrequency / 1000));
|
|
builder.append(" seconds, disregarding for which player this happens.");
|
|
if (!validContent.isEmpty()) {
|
|
builder.append(" On other occasion, valid content was received for: ");
|
|
StringUtil.join(validContent, ", ", builder);
|
|
}
|
|
NCPAPIProvider.getNoCheatPlusAPI().getLogManager().warning(Streams.STATUS, builder.toString());
|
|
}
|
|
}
|
|
|
|
}
|