[BLEEDING] Prevent noclip using commands with untracked moves.

A cheat client could move such that they are inside of a block, but
CraftBukkit will not fire an event, because the distance and looking
direction don't change enough. Teleporting other players or yourself to
that location would result in getting someone into a block. Consequently
 we also have to block commands like /sethome at such locations.

Our first attempt to patch that will monitor teleports that use the
TeleportCause.COMMAND (might miss out on plugins that are not using the
appropriate cause, and on plugins that use items for teleportation), in
addition we monitor certain commands (configurable prefixes), to catch
things like "sethome" and "sethome2". The world spawn is exempted. Only
teleports into blocks are monitored.

This does not yet sanity-check the distance to the last tracked
location, but it will ignore if none is set.
This commit is contained in:
asofold 2014-12-17 19:50:45 +01:00
parent ea5b249197
commit fb6ac2ad5a
10 changed files with 407 additions and 177 deletions

View File

@ -134,6 +134,20 @@ public class CharPrefixTree<N extends CharNode<N>, L extends CharLookupEntry<N>>
return false;
}
/**
* Test if there is an end-point in the tree that is a prefix of any of the inputs.
* @param inputs
* @return
*/
public boolean hasAnyPrefix(final Collection<String> inputs) {
for (final String input : inputs) {
if (hasPrefix(input)) {
return true;
}
}
return false;
}
public boolean isPrefix(final char[] chars){
return isPrefix(toCharacterList(chars));
}

View File

@ -3,6 +3,7 @@ package fr.neatmonster.nocheatplus.checks.chat;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Location;
import org.bukkit.command.Command;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@ -12,15 +13,20 @@ import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent.Result;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import fr.neatmonster.nocheatplus.NCPAPIProvider;
import fr.neatmonster.nocheatplus.checks.CheckListener;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.moving.MovingConfig;
import fr.neatmonster.nocheatplus.checks.moving.MovingUtil;
import fr.neatmonster.nocheatplus.command.CommandUtil;
import fr.neatmonster.nocheatplus.components.INotifyReload;
import fr.neatmonster.nocheatplus.components.JoinLeaveListener;
import fr.neatmonster.nocheatplus.config.ConfPaths;
import fr.neatmonster.nocheatplus.config.ConfigFile;
import fr.neatmonster.nocheatplus.config.ConfigManager;
import fr.neatmonster.nocheatplus.logging.Streams;
import fr.neatmonster.nocheatplus.utilities.StringUtil;
import fr.neatmonster.nocheatplus.utilities.TickTask;
import fr.neatmonster.nocheatplus.utilities.ds.prefixtree.SimpleCharPrefixTree;
@ -63,6 +69,9 @@ public class ChatListener extends CheckListener implements INotifyReload, JoinLe
/** Commands not to be executed in-game. */
private final SimpleCharPrefixTree consoleOnlyCommands = new SimpleCharPrefixTree();
/** Set world to null after use, primary thread only. */
private final Location useLoc = new Location(null, 0, 0, 0);
public ChatListener() {
super(CheckType.CHAT);
ConfigFile config = ConfigManager.getConfigFile();
@ -176,11 +185,40 @@ public class ChatListener extends CheckListener implements INotifyReload, JoinLe
// Treat as command.
if (commands.isEnabled(player) && commands.check(player, checkMessage, captcha)) {
event.setCancelled(true);
} else {
// TODO: Consider always checking these?
// Note that this checks for prefixes, not prefix words.
final MovingConfig mcc = MovingConfig.getConfig(player);
if (mcc.passableUntrackedCommandCheck && mcc.passableUntrackedCommandPrefixes.hasAnyPrefix(messageVars)) {
if (checkUntrackedLocation(player, message, mcc)) {
event.setCancelled(true);
}
}
}
}
}
private boolean checkUntrackedLocation(final Player player, final String message, final MovingConfig mcc) {
final Location loc = player.getLocation(useLoc);
boolean cancel = false;
if (MovingUtil.shouldCheckUntrackedLocation(player, loc)) {
final Location newTo = MovingUtil.checkUntrackedLocation(loc);
if (newTo != null) {
if (mcc.passableUntrackedCommandTryTeleport && player.teleport(newTo, TeleportCause.PLUGIN)) {
NCPAPIProvider.getNoCheatPlusAPI().getLogManager().info(Streams.TRACE_FILE, player.getName() + " runs the command '" + message + "' at an untracked location: " + loc + " , teleport to: " + newTo);
} else {
// TODO: Allow disabling cancel?
// TODO: Should message the player?
NCPAPIProvider.getNoCheatPlusAPI().getLogManager().info(Streams.TRACE_FILE, player.getName() + " runs the command '" + message + "' at an untracked location: " + loc + " , cancel the command.");
cancel = true;
}
}
}
useLoc.setWorld(null); // Cleanup.
return cancel;
}
private boolean textChecks(final Player player, final String message, final boolean isMainThread, final boolean alreadyCancelled) {
return text.isEnabled(player) && text.check(player, message, captcha, isMainThread, alreadyCancelled);
}

View File

@ -5,6 +5,7 @@ import java.util.Map;
import org.bukkit.entity.Player;
import fr.neatmonster.nocheatplus.NCPAPIProvider;
import fr.neatmonster.nocheatplus.actions.ActionList;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.access.ACheckConfig;
@ -13,7 +14,9 @@ import fr.neatmonster.nocheatplus.checks.access.ICheckConfig;
import fr.neatmonster.nocheatplus.config.ConfPaths;
import fr.neatmonster.nocheatplus.config.ConfigFile;
import fr.neatmonster.nocheatplus.config.ConfigManager;
import fr.neatmonster.nocheatplus.logging.Streams;
import fr.neatmonster.nocheatplus.permissions.Permissions;
import fr.neatmonster.nocheatplus.utilities.ds.prefixtree.SimpleCharPrefixTree;
/**
* Configurations specific for the moving checks. Every world gets one of these assigned to it.
@ -107,6 +110,10 @@ public class MovingConfig extends ACheckConfig {
public final boolean passableRayTracingBlockChangeOnly;
// TODO: passableAccuracy: also use if not using ray-tracing
public final ActionList passableActions;
public final boolean passableUntrackedTeleportCheck;
public final boolean passableUntrackedCommandCheck;
public final boolean passableUntrackedCommandTryTeleport;
public final SimpleCharPrefixTree passableUntrackedCommandPrefixes = new SimpleCharPrefixTree();
public final boolean survivalFlyCheck;
public final int survivalFlyBlockingSpeed;
@ -198,6 +205,18 @@ public class MovingConfig extends ACheckConfig {
passableRayTracingCheck = config.getBoolean(ConfPaths.MOVING_PASSABLE_RAYTRACING_CHECK);
passableRayTracingBlockChangeOnly = config.getBoolean(ConfPaths.MOVING_PASSABLE_RAYTRACING_BLOCKCHANGEONLY);
passableActions = config.getOptimizedActionList(ConfPaths.MOVING_PASSABLE_ACTIONS, Permissions.MOVING_PASSABLE);
passableUntrackedTeleportCheck = config.getBoolean(ConfPaths.MOVING_PASSABLE_UNTRACKED_TELEPORT_ACTIVE);
passableUntrackedCommandCheck = config.getBoolean(ConfPaths.MOVING_PASSABLE_UNTRACKED_CMD_ACTIVE);
passableUntrackedCommandTryTeleport = config.getBoolean(ConfPaths.MOVING_PASSABLE_UNTRACKED_CMD_TRYTELEPORT);
try {
for (String prefix : config.getStringList(ConfPaths.MOVING_PASSABLE_UNTRACKED_CMD_PREFIXES)) {
if (prefix != null && !prefix.isEmpty()) {
passableUntrackedCommandPrefixes.feed(prefix.toLowerCase());
}
}
} catch (Exception e) {
NCPAPIProvider.getNoCheatPlusAPI().getLogManager().warning(Streams.STATUS, "[NoCheatPlus] Bad prefixes definition (String list) for " + ConfPaths.MOVING_PASSABLE_UNTRACKED_CMD_PREFIXES);
}
survivalFlyCheck = config.getBoolean(ConfPaths.MOVING_SURVIVALFLY_CHECK);
// Default values are specified here because this settings aren't showed by default into the configuration file.

View File

@ -127,11 +127,11 @@ public class MovingData extends ACheckData {
private final AxisVelocity horVel = new AxisVelocity();
// Coordinates.
/** Last from coordinates. */
/** Last from coordinates. X is at Double.MAX_VALUE, if not set. */
public double fromX = Double.MAX_VALUE, fromY, fromZ;
/** Last to coordinates. */
/** Last to coordinates. X is at Double.MAX_VALUE, if not set. */
public double toX = Double.MAX_VALUE, toY, toZ;
/** Moving trace (to-positions, use tick as time). This is initialized on "playerJoins, i.e. MONITOR, and set to null on playerLeaves."*/
/** Moving trace (to-positions, use tick as time). This is initialized on "playerJoins, i.e. MONITOR, and set to null on playerLeaves." */
private LocationTrace trace = null;
// sf rather

View File

@ -129,7 +129,7 @@ public class MovingListener extends CheckListener implements TickListener, IRemo
private final Set<EntityType> normalVehicles = new HashSet<EntityType>();
/** Location for temporary use with getLocation(useLoc). Always call setWorld(null) after use. Use LocUtil.clone before passing to other API. */
private final Location useLoc = new Location(null, 0, 0, 0); // TODO: Put to use...
final Location useLoc = new Location(null, 0, 0, 0); // TODO: Put to use...
/** Statistics / debugging counters. */
private final Counters counters = NCPAPIProvider.getNoCheatPlusAPI().getGenericInstance(Counters.class);
@ -858,7 +858,7 @@ public class MovingListener extends CheckListener implements TickListener, IRemo
final Location teleported = data.getTeleported();
// If it was a teleport initialized by NoCheatPlus, do it anyway even if another plugin said "no".
final Location to = event.getTo();
Location to = event.getTo();
final Location ref;
if (teleported != null && teleported.equals(to)) {
// Teleport by NCP.
@ -915,6 +915,22 @@ public class MovingListener extends CheckListener implements TickListener, IRemo
// pass = true;
}
}
else if (cause == TeleportCause.COMMAND) {
// Attempt to prevent teleporting to players inside of blocks at untracked coordinates.
// TODO: Consider checking this on low or lowest (!).
// TODO: Other like TeleportCause.PLUGIN?
if (cc.passableUntrackedTeleportCheck && MovingUtil.shouldCheckUntrackedLocation(player, to)) {
final Location newTo = MovingUtil.checkUntrackedLocation(to);
if (newTo != null) {
// Adjust the teleport to go to the last tracked to-location of the other player.
to = newTo;
event.setTo(newTo);
cancel = smallRange = false;
// TODO: Consider console, consider data.debug.
NCPAPIProvider.getNoCheatPlusAPI().getLogManager().warning(Streams.TRACE_FILE, player.getName() + " correct untracked teleport destination (" + to + " corrected to " + newTo + ").");
}
}
}
// if (pass) {
// ref = to;

View File

@ -1,8 +1,11 @@
package fr.neatmonster.nocheatplus.checks.moving;
import org.bukkit.Chunk;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerMoveEvent;
@ -14,6 +17,7 @@ import fr.neatmonster.nocheatplus.permissions.Permissions;
import fr.neatmonster.nocheatplus.utilities.BlockProperties;
import fr.neatmonster.nocheatplus.utilities.CheckUtils;
import fr.neatmonster.nocheatplus.utilities.PlayerLocation;
import fr.neatmonster.nocheatplus.utilities.TrigUtil;
/**
* Static utility methods.
@ -22,6 +26,11 @@ import fr.neatmonster.nocheatplus.utilities.PlayerLocation;
*/
public class MovingUtil {
/**
* Always set world to null after use, careful with nested methods. Main thread only.
*/
private static final Location useLoc = new Location(null, 0, 0, 0);
/**
* Check if the player is to be checked by the survivalfly check.
* @param player
@ -89,4 +98,79 @@ public class MovingUtil {
return BlockProperties.isGround(blockType) || BlockProperties.isSolid(blockType);
}
/**
* Check the context-independent pre-conditions for checking for untracked
* locations (not the world spawn, location is not passable, passable is
* enabled for the player).
*
* @param player
* @param loc
* @return
*/
public static boolean shouldCheckUntrackedLocation(final Player player, final Location loc) {
return !TrigUtil.isSamePos(loc, loc.getWorld().getSpawnLocation())
&& !BlockProperties.isPassable(loc)
&& CheckType.MOVING_PASSABLE.isEnabled(player);
}
/**
* Detect if the given location is an untracked spot. This is spots for
* which a player is at the location, but the moving data has another
* "last to" position set for that player. Note that one matching player
* with "last to" being consistent is enough to let this return null, world spawn is exempted.
* <hr>
* Pre-conditions:<br>
* <li>Context-specific (e.g. activation flags for command, teleport).</li>
* <li>See MovingUtils.shouldCheckUntrackedLocation.</li>
*
* @param loc
* @return Corrected location, if loc is an "untracked location".
*/
public static Location checkUntrackedLocation(final Location loc) {
// TODO: More efficient method to get entities at the same position (might use MCAccess).
final Chunk toChunk = loc.getChunk();
final Entity[] entities = toChunk.getEntities();
MovingData untrackedData = null;
for (int i = 0; i < entities.length; i++) {
final Entity entity = entities[i];
if (entity.getType() != EntityType.PLAYER) {
continue;
}
final Location refLoc = entity.getLocation(useLoc);
// Exempt world spawn.
// TODO: Exempt other warps -> HASH based exemption (expire by time, keep high count)?
if (TrigUtil.isSamePos(loc, refLoc) && (entity instanceof Player)) {
final Player other = (Player) entity;
final MovingData otherData = MovingData.getData(other);
if (otherData.toX == Double.MAX_VALUE) {
// Data might have been removed.
// TODO: Consider counting as tracked?
continue;
}
else if (TrigUtil.isSamePos(refLoc, otherData.toX, otherData.toY, otherData.toZ)) {
// Tracked.
return null;
}
else {
// Untracked location.
// TODO: Discard locations in the same block, if passable.
// TODO: Sanity check distance?
// More leniency: allow moving inside of the same block.
if (TrigUtil.isSameBlock(loc, otherData.toX, otherData.toY, otherData.toZ) && !BlockProperties.isPassable(refLoc.getWorld(), otherData.toX, otherData.toY, otherData.toZ)) {
continue;
}
untrackedData = otherData;
}
}
}
useLoc.setWorld(null); // Cleanup.
if (untrackedData == null) {
return null;
}
else {
// TODO: Count and log to TRACE_FILE, if multiple locations would match (!).
return new Location(loc.getWorld(), untrackedData.toX, untrackedData.toY, untrackedData.toZ, loc.getYaw(), loc.getPitch());
}
}
}

View File

@ -531,8 +531,15 @@ public abstract class ConfPaths {
public static final String MOVING_PASSABLE_CHECK = MOVING_PASSABLE + "active";
private static final String MOVING_PASSABLE_RAYTRACING = MOVING_PASSABLE + "raytracing.";
public static final String MOVING_PASSABLE_RAYTRACING_CHECK = MOVING_PASSABLE_RAYTRACING + "active";
public static final String MOVING_PASSABLE_RAYTRACING_BLOCKCHANGEONLY= MOVING_PASSABLE_RAYTRACING + "blockchangeonly";
public static final String MOVING_PASSABLE_RAYTRACING_BLOCKCHANGEONLY = MOVING_PASSABLE_RAYTRACING + "blockchangeonly";
public static final String MOVING_PASSABLE_ACTIONS = MOVING_PASSABLE + "actions";
private static final String MOVING_PASSABLE_UNTRACKED = MOVING_PASSABLE + "untracked.";
private static final String MOVING_PASSABLE_UNTRACKED_TELEPORT = MOVING_PASSABLE_UNTRACKED + "teleport.";
public static final String MOVING_PASSABLE_UNTRACKED_TELEPORT_ACTIVE = MOVING_PASSABLE_UNTRACKED_TELEPORT + "active";
private static final String MOVING_PASSABLE_UNTRACKED_CMD = MOVING_PASSABLE_UNTRACKED + "command.";
public static final String MOVING_PASSABLE_UNTRACKED_CMD_ACTIVE = MOVING_PASSABLE_UNTRACKED_CMD + "active";
public static final String MOVING_PASSABLE_UNTRACKED_CMD_TRYTELEPORT = MOVING_PASSABLE_UNTRACKED_CMD + "tryteleport";
public static final String MOVING_PASSABLE_UNTRACKED_CMD_PREFIXES = MOVING_PASSABLE_UNTRACKED_CMD + "prefixes";
private static final String MOVING_SURVIVALFLY = MOVING + "survivalfly.";
public static final String MOVING_SURVIVALFLY_CHECK = MOVING_SURVIVALFLY + "active";

View File

@ -381,6 +381,10 @@ public class DefaultConfig extends ConfigFile {
set(ConfPaths.MOVING_PASSABLE_RAYTRACING_CHECK, true);
set(ConfPaths.MOVING_PASSABLE_RAYTRACING_BLOCKCHANGEONLY, false);
set(ConfPaths.MOVING_PASSABLE_ACTIONS, "cancel vl>10 log:passable:0:5:if cancel vl>50 log:passable:0:5:icf cancel");
set(ConfPaths.MOVING_PASSABLE_UNTRACKED_TELEPORT_ACTIVE, true);
set(ConfPaths.MOVING_PASSABLE_UNTRACKED_CMD_ACTIVE, true);
set(ConfPaths.MOVING_PASSABLE_UNTRACKED_CMD_TRYTELEPORT, true);
set(ConfPaths.MOVING_PASSABLE_UNTRACKED_CMD_PREFIXES, Arrays.asList("sethome", "home set", "setwarp", "warp set", "setback", "set back", "back set"));
set(ConfPaths.MOVING_SURVIVALFLY_CHECK, true);
// set(ConfPaths.MOVING_SURVIVALFLY_EXTENDED_HACC, false);

View File

@ -14,6 +14,7 @@ import java.util.Map.Entry;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
@ -1886,13 +1887,25 @@ public class BlockProperties {
}
/**
* Convenience method for debugging purposes.
* Convenience method.
* @param loc
* @return
*/
public static final boolean isPassable(final Location loc) {
blockCache.setAccess(loc.getWorld());
boolean res = isPassable(blockCache, loc.getX(), loc.getY(), loc.getZ(), blockCache.getTypeId(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()));
return isPassable(loc.getWorld(), loc.getX(), loc.getY(), loc.getZ());
}
/**
* Convenience method.
* @param world
* @param x
* @param y
* @param z
* @return
*/
public static final boolean isPassable(final World world, final double x, final double y, final double z) {
blockCache.setAccess(world);
boolean res = isPassable(blockCache, x, y, z, blockCache.getTypeId(x, y, z));
blockCache.cleanup();
return res;
}

View File

@ -478,4 +478,39 @@ public class TrigUtil {
|| xDistance > 0D && zDistance > 0D && yaw > 90F && yaw < 180F;
}
/**
* Test if both locations have the exact same coordinates. Does not check yaw/pitch.
* @param loc1
* @param loc2
* @return Returns false if either is null.
*/
public static boolean isSamePos(final Location loc1, final Location loc2) {
if (loc1 == null || loc2 == null) {
return false;
}
return loc1.getX() == loc2.getX() && loc1.getZ() == loc2.getZ() && loc1.getY() == loc2.getY();
}
/**
* Test if the location has the given coordinates.
* @param loc
* @param x
* @param y
* @param z
* @return Returns false if loc is null;
*/
public static boolean isSamePos(final Location loc, final double x, final double y, final double z) {
if (loc == null) {
return false;
}
return loc.getX() == x && loc.getZ() == z && loc.getY() == y;
}
public static boolean isSameBlock(final Location loc, final double x, final double y, final double z) {
if (loc == null) {
return false;
}
return loc.getBlockX() == Location.locToBlock(x) && loc.getBlockZ() == Location.locToBlock(z) && loc.getBlockY() == Location.locToBlock(y);
}
}