Compare commits

...
5 Commits
Author SHA1 Message Date
Jones 475bf28ecf Release 2.0.24 (#244) 2024-03-24 20:25:30 +01:00
Jones 9c29753084 Recode map CAPTCHA (#243)
* feat: implement capja for CAPTCHA generation

* feat: implement map color palette for 1.7.2-1.20.4
2024-03-24 20:17:18 +01:00
Jones f0f5196fbe Don't drop messages that aren't ByteBufs (#242) 2024-03-22 14:01:32 +01:00
Jones d98ce28a59 Implement missing packet limits (#241) 2024-03-22 13:56:55 +01:00
MISHA e0d5c67cdf Fix translation error in the Russian language (#240) 2024-03-16 11:25:43 +01:00
23 changed files with 472 additions and 430 deletions

No files matched your search

+1
View File
@@ -82,6 +82,7 @@ tasks {
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
relocate("com.mysql", "xyz.jonesdev.sonar.libs.mysql")
relocate("org.mariadb", "xyz.jonesdev.sonar.libs.mariadb")
relocate("xyz.jonesdev.capja", "xyz.jonesdev.sonar.libs.capja")
// TODO: actually fix this :/
// We have to be careful here, so we don't accidentally break adventure
+1 -1
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.23
version=2.0.24
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
@@ -179,19 +179,22 @@ public final class SonarConfiguration {
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(generalConfig.getString("verification.checks.gravity.gamemode"));
verification.map.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.map-captcha.timing"));
verification.map.randomizePositions = generalConfig.getBoolean("verification.checks.map-captcha.random-position");
verification.map.distortionsFactorX = clamp(generalConfig.getInt(
"verification.checks.map-captcha.distortions-factor-x"), 0, 100) / 100D;
verification.map.distortionsFactorY = clamp(generalConfig.getInt(
"verification.checks.map-captcha.distortions-factor-y"), 0, 100) / 100D;
verification.map.randomLinesAmount = generalConfig.getInt("verification.checks.map-captcha.random-lines");
verification.map.randomOvalsAmount = generalConfig.getInt("verification.checks.map-captcha.random-ovals");
verification.map.randomizeFontSize = generalConfig.getBoolean("verification.checks.map-captcha.random-font-size");
verification.map.flare = generalConfig.getBoolean("verification.checks.map-captcha.effects.flare");
verification.map.scratches = generalConfig.getBoolean("verification.checks.map-captcha.effects.scratches");
verification.map.ripple = generalConfig.getBoolean("verification.checks.map-captcha.effects.ripple");
verification.map.smear = generalConfig.getBoolean("verification.checks.map-captcha.effects.smear");
verification.map.pinch = generalConfig.getBoolean("verification.checks.map-captcha.effects.pinch");
verification.map.saturation = (float) generalConfig.getYaml().getDouble(
"verification.checks.map-captcha.effects.saturation", 0.3);
verification.map.distortion = (float) generalConfig.getYaml().getDouble(
"verification.checks.map-captcha.effects.distortion", 2);
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute");
verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration");
verification.map.maxTries = generalConfig.getInt("verification.checks.map-captcha.max-tries");
verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary");
verification.map.fonts = generalConfig.getStringList("verification.checks.map-captcha.fonts");
verification.map.fonts = generalConfig.getStringList("verification.checks.map-captcha.font-names");
verification.brand.enabled = generalConfig.getBoolean("verification.checks.client-brand.enabled");
verification.brand.validRegex = Pattern.compile(generalConfig.getString("verification.checks.client-brand.valid-regex"));
@@ -437,12 +440,13 @@ public final class SonarConfiguration {
@Getter
public static final class Map {
private Timing timing;
private boolean randomizePositions;
private boolean randomizeFontSize;
private double distortionsFactorX;
private double distortionsFactorY;
private int randomLinesAmount;
private int randomOvalsAmount;
private boolean flare;
private boolean scratches;
private boolean ripple;
private boolean smear;
private boolean pinch;
private float saturation;
private float distortion;
private int precomputeAmount;
private int maxDuration;
private int maxTries;
+2
View File
@@ -1,5 +1,7 @@
dependencies {
compileOnly(project(":api"))
implementation("xyz.jonesdev.capja:capja:0.1.2")
}
java.sourceCompatibility = JavaVersion.VERSION_11
@@ -32,8 +32,7 @@ import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.*;
import xyz.jonesdev.sonar.common.fallback.protocol.map.ItemMapType;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.map.PreparedMapInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapCaptchaInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
@@ -73,7 +72,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Map captcha
private final SystemTimer keepAlive = new SystemTimer();
private final SystemTimer actionBar = new SystemTimer();
private @Nullable PreparedMapInfo captcha;
private @Nullable MapCaptchaInfo captcha;
private int captchaTriesLeft;
public enum State {
@@ -262,9 +261,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Handle incoming chat messages
if (packet instanceof UniversalChatPacket) {
checkFrame(captcha != null, "Captcha not sent yet");
final UniversalChatPacket chat = (UniversalChatPacket) packet;
Objects.requireNonNull(captcha);
if (!chat.getMessage().equals(captcha.getInfo().getAnswer())) {
if (!chat.getMessage().equals(captcha.getAnswer())) {
// Captcha is incorrect
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
user.write(incorrectCaptcha);
@@ -582,7 +581,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
private void handleCAPTCHA() {
if (MapInfoPreparer.getPreparedCAPTCHAs() == 0) {
if (!MAP_INFO_PREPARER.isCaptchaAvailable()) {
// This should not happen, but we have to return if there is no captcha prepared
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
return;
@@ -601,8 +600,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
user.delayedWrite(new SetSlotPacket(0, 36, 1, 0,
ItemMapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlotPacket.MAP_NBT));
// Send random captcha to the player
captcha = MapInfoPreparer.getRandomCaptcha();
Objects.requireNonNull(captcha).write(user);
captcha = MAP_INFO_PREPARER.getRandomCaptcha();
Objects.requireNonNull(captcha).delayedWrite(user);
// Teleport the player to the position above the platform
user.delayedWrite(CAPTCHA_POSITION);
// Make sure the player cannot move
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.netty;
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.netty;
package xyz.jonesdev.sonar.common.fallback.netty;
import org.jetbrains.annotations.NotNull;
@@ -98,10 +98,6 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
}
} else {
// Packets are always ByteBufs - if we detect an unknown object, fail verification
user.fail("packet is not a ByteBuf");
throw new CorruptedFrameException("Packet is not a ByteBuf?!");
}
}
@@ -23,7 +23,7 @@ import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.map.CaptchaPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryDataPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
@@ -82,6 +82,9 @@ public class FallbackPreparer {
public double[] preparedCachedYMotions;
public double maxFallDistance;
// CAPTCHA
public final CaptchaPreparer MAP_INFO_PREPARER = new CaptchaPreparer();
public void prepare() {
joinGame = new JoinGamePacket(1,
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
@@ -134,7 +137,7 @@ public class FallbackPreparer {
incorrectCaptcha = new UniversalChatPacket(
Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha(), UniversalChatPacket.SYSTEM_TYPE);
MapInfoPreparer.prepare();
MAP_INFO_PREPARER.prepare();
}
}
}
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.map;
import xyz.jonesdev.capja.CaptchaGenerator;
import xyz.jonesdev.capja.CaptchaHolder;
import xyz.jonesdev.capja.config.CaptchaConfiguration;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.awt.*;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class CaptchaPreparer {
private static final ExecutorService PREPARATION_SERVICE = Executors.newSingleThreadExecutor();
private static final Random RANDOM = new Random();
private MapCaptchaInfo[] cached;
private int preparedAmount;
public void prepare() {
// Make sure we're not preparing when Sonar is already preparing answers
if (cached != null && preparedAmount != cached.length) return;
preparedAmount = 0;
final SystemTimer timer = new SystemTimer();
Sonar.get().getLogger().info("Asynchronously preparing CAPTCHA answers...");
Sonar.get().getLogger().info("Players will be able to join even if the preparation isn't finished");
// Prepare cache
final SonarConfiguration.Verification.Map config = Sonar.get().getConfig().getVerification().getMap();
cached = new MapCaptchaInfo[config.getPrecomputeAmount()];
// Prepare everything asynchronously
PREPARATION_SERVICE.execute(() -> {
final List<String> fonts = config.getFonts();
if (fonts.isEmpty()) {
throw new IllegalArgumentException("No fonts specified!");
}
// Create the configuration for the image generation
final String[] fontNames = fonts.toArray(new String[0]);
final CaptchaConfiguration configuration = new CaptchaConfiguration(
128, 128, config.getDictionary().toCharArray(), 5,
config.isFlare(), config.isScratches(), config.isRipple(), config.isSmear(), config.isPinch(),
config.getSaturation(), config.getDistortion(), new int[]{Font.PLAIN, Font.BOLD}, fontNames);
for (int i = 0; i < config.getPrecomputeAmount(); i++) {
final CaptchaGenerator captchaGenerator = new CaptchaGenerator(configuration);
final CaptchaHolder captchaHolder = captchaGenerator.generate();
final byte[] buffer = MapColorPalette.getBufferFromImage(captchaHolder.getImage());
cached[i] = new MapCaptchaInfo(captchaHolder.getAnswer(), buffer);
preparedAmount++;
}
Sonar.get().getLogger().info("Finished preparing {} CAPTCHA answers ({}s)!", preparedAmount, timer);
});
}
public boolean isCaptchaAvailable() {
return cached != null && cached[0] != null;
}
public MapCaptchaInfo getRandomCaptcha() {
// Give the player a random CAPTCHA out of the ones that we've already prepared
return cached[RANDOM.nextInt(preparedAmount)];
}
}
@@ -25,41 +25,41 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.MapDataPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
@Getter
public final class PreparedMapInfo {
public static final int DIMENSIONS = (int) Math.pow(2, 7);
public static final int SCALE = DIMENSIONS * DIMENSIONS;
public final class MapCaptchaInfo {
private final String answer;
private final byte[] buffer;
private final MapInfo info;
private final MapDataPacket[] legacy;
private final MapDataPacket modern;
public PreparedMapInfo(final String answer,
final int columns, final int rows,
final byte @NotNull [] buffer) {
this.info = new MapInfo(answer, columns, rows, 0, 0, buffer);
public MapCaptchaInfo(final @NotNull String answer, final byte @NotNull [] buffer) {
this.answer = answer;
this.buffer = buffer;
// Prepare 1.7 map data using a grid
final byte[][] grid = new byte[DIMENSIONS][DIMENSIONS];
final byte[][] grid = new byte[128][128];
for (int i = 0; i < buffer.length; i++) {
final byte buf = buffer[i];
grid[i & Byte.MAX_VALUE][i >> 7] = buf;
}
this.legacy = new MapDataPacket[grid.length];
for (int i = 0; i < grid.length; i++) {
this.legacy[i] = new MapDataPacket(new MapInfo(answer, DIMENSIONS, DIMENSIONS, i, 0, grid[i]));
this.legacy[i] = new MapDataPacket(grid[i], i, 0);
}
// Prepare 1.8+ map data
this.modern = new MapDataPacket(info);
this.modern = new MapDataPacket(buffer, 0, 0);
}
public void write(final @NotNull FallbackUser user) {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
for (final MapDataPacket data : legacy) {
user.delayedWrite(data);
}
} else {
public void delayedWrite(final @NotNull FallbackUser user) {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
user.delayedWrite(modern);
return;
}
// 1.7.2-1.7.10
for (final MapDataPacket legacyPacket : legacy) {
user.delayedWrite(legacyPacket);
}
}
}
@@ -0,0 +1,198 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.map;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import java.awt.image.BufferedImage;
@UtilityClass
public class MapColorPalette {
// https://minecraft.wiki/w/Map_item_format#Full_color_tables
public static final int[] COLOR_MAP = {
0,
0,
0,
0,
-10912473,
-9594576,
-8408520,
-12362211,
-5331853,
-2766452,
-530013,
-8225962,
-7697782,
-5658199,
-3815995,
-9934744,
-4980736,
-2359296,
-65536,
-7929856,
-9408332,
-7697700,
-6250241,
-11250553,
-9079435,
-7303024,
-5789785,
-10987432,
-16754944,
-16750080,
-16745472,
-16760576,
-4934476,
-2302756,
-1,
-7895161,
-9210239,
-7499618,
-5986120,
-11118495,
-8304095,
-6464728,
-4756945,
-10471400,
-11579569,
-10461088,
-9408400,
-12895429,
-10136525,
-8689858,
-7375032,
-11780314,
-13816396,
-13158436,
-12566273,
-14605945,
-4935252,
-2303533,
-779,
-7895679,
-6792924,
-4559572,
-2588877,
-9288933,
-8571496,
-6733382,
-5092136,
-10606478,
-12030824,
-10976070,
-10053160,
-13217422,
-6184668,
-3816148,
-1710797,
-8816357,
-10907631,
-9588715,
-8401895,
-12358643,
-5613196,
-3117682,
-884827,
-8371369,
-13290187,
-12500671,
-11776948,
-14145496,
-9671572,
-8092540,
-6710887,
-11447983,
-13280916,
-12489340,
-11763815,
-14138543,
-10933123,
-9619815,
-8437838,
-12377762,
-14404227,
-13876839,
-13415246,
-14997410,
-12045020,
-10993364,
-10073037,
-13228005,
-12035804,
-10982100,
-10059981,
-13221093,
-9690076,
-8115156,
-6737101,
-11461861,
-15658735,
-15395563,
-15132391,
-15921907,
-5199818,
-2634430,
-332211,
-8094168,
-12543338,
-11551561,
-10691627,
};
public byte[] getBufferFromImage(final @NotNull BufferedImage bufferedImage) {
final int width = bufferedImage.getWidth();
final int height = bufferedImage.getHeight();
final int[] pixels = new int[128 * 128];
final byte[] buffer = new byte[128 * 128];
bufferedImage.getRGB(0, 0, width, height, pixels, 0, width);
for (int i = 0; i < pixels.length; i++) {
buffer[i] = mapColorToMinecraft(pixels[i]);
}
return buffer;
}
private byte mapColorToMinecraft(final int argb) {
int minDistance = Integer.MAX_VALUE;
byte closestIndex = 0;
for (byte i = 0; i < COLOR_MAP.length; i++) {
final int distance = calculateColorDistance(argb, COLOR_MAP[i]);
if (distance < minDistance) {
minDistance = distance;
closestIndex = i;
}
}
return closestIndex;
}
private int calculateColorDistance(final int argb0, final int argb1) {
final int red0 = (argb0 >> 16) & 0xFF;
final int green0 = (argb0 >> 8) & 0xFF;
final int blue0 = argb0 & 0xFF;
final int red1 = (argb1 >> 16) & 0xFF;
final int green1 = (argb1 >> 8) & 0xFF;
final int blue1 = argb1 & 0xFF;
// Color distance metric (e.g. squared Euclidean distance)
return (red0 - red1) * (red0 - red1)
+ (green0 - green1) * (green0 - green1)
+ (blue0 - blue1) * (blue0 - blue1);
}
}
@@ -1,30 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.map;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public final class MapInfo {
private final String answer;
private final int columns, rows;
private final int x, y;
private final byte[] buffer;
}
@@ -1,248 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.map;
import lombok.Getter;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.awt.Font.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.map.PreparedMapInfo.DIMENSIONS;
@UtilityClass
public class MapInfoPreparer {
private final Random RANDOM = new Random();
private final ExecutorService PREPARATION_SERVICE = Executors.newSingleThreadExecutor();
private final int[] FONT_STYLES = new int[]{
PLAIN,
BOLD,
ITALIC,
ITALIC | BOLD
};
private final int[][] COLOR_PALETTE = new int[][]{
new int[]{ // Blue
48,
49,
50,
51
},
new int[]{ // Orange
60,
61,
62,
63,
},
new int[]{ // Green
4,
28,
29,
30,
},
new int[]{ // Black
27,
45,
46,
47,
},
new int[]{ // Red
16,
17,
18,
19
}
};
private PreparedMapInfo[] cached;
@Getter
private int preparedCAPTCHAs;
private boolean currentlyPreparing;
public PreparedMapInfo getRandomCaptcha() {
return cached[RANDOM.nextInt(preparedCAPTCHAs)];
}
public void prepare() {
final SystemTimer timer = new SystemTimer();
Sonar.get().getLogger().info("Precomputing map captcha answers...");
// Make sure we're not running into concurrency issues
if (currentlyPreparing) {
Sonar.get().getLogger().warn("Did not precompute map captcha answers as another task is running.");
return;
}
// Reset all statistics
preparedCAPTCHAs = 0;
currentlyPreparing = true;
final SonarConfiguration.Verification.Map config = Sonar.get().getConfig().getVerification().getMap();
cached = null;
cached = new PreparedMapInfo[config.getPrecomputeAmount()];
// Prepare fonts from config
final String dictionary = config.getDictionary();
final List<String> fonts = config.getFonts();
if (fonts.isEmpty()) {
Sonar.get().getLogger().warn("No fonts found, using fallback font...");
fonts.add(DIALOG);
}
final String[] fontTypes = fonts.toArray(new String[0]);
// Prepare distortions values from config
final double distortionsFactorX = config.getDistortionsFactorX();
final double distortionsFactorY = config.getDistortionsFactorY();
final double halfDistortionsFactorX = distortionsFactorX / 2D;
final double halfDistortionsFactorY = distortionsFactorY / 2D;
for (int _i = 0; _i < cached.length; _i++) {
final int currentIndex = _i;
final byte[] buffer = new byte[PreparedMapInfo.SCALE];
PREPARATION_SERVICE.execute(() -> {
// Create image
final BufferedImage image = new BufferedImage(DIMENSIONS, DIMENSIONS, BufferedImage.TYPE_3BYTE_BGR);
final Graphics2D graphics = (Graphics2D) image.getGraphics();
try {
// Create random font
final String fontType = fontTypes[RANDOM.nextInt(fontTypes.length)];
final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)];
final int fontSize = 27
+ (config.isRandomizeFontSize()
? RANDOM.nextInt(6) - 2 : 5);
@SuppressWarnings("all") final Font answerFont = new Font(fontType, fontStyle, fontSize);
graphics.setFont(answerFont);
// Build answer to the captcha
final StringBuilder answerBuilder = new StringBuilder();
for (int _j = 0; _j < 5; _j++) {
answerBuilder.append(dictionary.charAt(RANDOM.nextInt(dictionary.length())));
}
final String answer = answerBuilder.toString();
// Calculate text position
final int stringWidth = graphics.getFontMetrics().stringWidth(answer);
final int halfWidth = image.getWidth() / 2;
final int halfHeight = image.getHeight() / 2;
final int spacing = 5;
double _x = halfWidth - stringWidth / 2f - spacing;
double _y = halfHeight + fontSize / 3f;
// Draw each character one by one
final FontRenderContext fontRenderContext = graphics.getFontRenderContext();
for (final char c : answer.toCharArray()) {
// Randomize x and y
if (config.isRandomizePositions()) {
_y += RANDOM.nextInt(8) - 4;
}
// Apply random distortion
final double distortionFactorX = RANDOM.nextDouble() * distortionsFactorX - halfDistortionsFactorX;
final double distortionFactorY = RANDOM.nextDouble() * distortionsFactorY - halfDistortionsFactorY;
// Create a GlyphVector for the character
final String character = String.valueOf(c);
final GlyphVector glyphVector = graphics.getFont().createGlyphVector(fontRenderContext, character);
// Apply the distortion to the GlyphVector using AffineTransform
final AffineTransform transform = AffineTransform.getTranslateInstance(_x, _y);
transform.shear(distortionFactorX, distortionFactorY);
final Shape distortedCharacter = glyphVector.getOutline();
final Shape distorted = transform.createTransformedShape(distortedCharacter);
// Draw the distorted character
graphics.fill(distorted);
// Update x by width
_x += glyphVector.getVisualBounds().getWidth() + spacing;
}
// TODO: Color converter
graphics.setColor(Color.RED);
// Draw random lines
for (int i = 0; i < config.getRandomLinesAmount(); i++) {
final int startX = RANDOM.nextInt(halfWidth);
final int startY = RANDOM.nextInt(halfHeight);
final int endX = halfWidth + RANDOM.nextInt(halfWidth);
final int endY = halfHeight + RANDOM.nextInt(halfHeight);
graphics.drawLine(startX, startY, endX, endY);
}
// Draw random ovals
for (int i = 0; i < config.getRandomOvalsAmount(); i++) {
final int startX = RANDOM.nextInt(halfWidth);
final int startY = RANDOM.nextInt(halfHeight);
final int endX = halfWidth + RANDOM.nextInt(halfWidth);
final int endY = halfHeight + RANDOM.nextInt(halfHeight);
graphics.drawOval(startX, startY, endX, endY);
}
// Select random color palette
final int[] colorPalette = COLOR_PALETTE[currentIndex % COLOR_PALETTE.length];
// Select the next color palette for geometry
final int[] nextColorPalette = COLOR_PALETTE[(currentIndex + 1) % COLOR_PALETTE.length];
// Clear background
Arrays.fill(buffer, (byte) 57);
// Color every pixel individually
final int spacingY = halfHeight - fontSize;
for (int x = spacing; x < image.getWidth() - spacing; x++) {
for (int y = spacingY; y < image.getHeight() - spacingY; y++) {
final int pixel = image.getRGB(x, y);
final int index = y * image.getWidth() + x;
if (pixel == -16777216 && RANDOM.nextInt(100) < 97) continue;
// Set color of pixel to random color from the palette
final byte color = (byte) (pixel == -16777216 ? RANDOM.nextInt(100) < 75 ? 14 :
RANDOM.nextInt(Byte.MAX_VALUE)
: pixel != -65536 ? colorPalette[RANDOM.nextInt(colorPalette.length)]
: nextColorPalette[RANDOM.nextInt(nextColorPalette.length)]);
// Write pixel color to buffer
buffer[index] = color;
}
}
// Cache buffer to map
cached[currentIndex] = new PreparedMapInfo(answer, image.getWidth(), image.getHeight(), buffer);
} finally {
// Dispose graphics
graphics.dispose();
// Finished?
if (currentIndex == cached.length - 1) {
Sonar.get().getLogger().info("Successfully precomputed map captcha answers in {}s!", timer);
currentlyPreparing = false;
}
preparedCAPTCHAs++;
}
});
}
}
}
@@ -30,4 +30,10 @@ public final class LoginAcknowledgedPacket implements FallbackPacket {
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledgedPacket.java#L41
return 0;
}
}
@@ -24,8 +24,8 @@ import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.netty.FastUUID;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.netty.FastUUID;
import java.util.UUID;
@@ -25,7 +25,6 @@ import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfo;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@@ -34,19 +33,20 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@NoArgsConstructor
@AllArgsConstructor
public final class MapDataPacket implements FallbackPacket {
private MapInfo mapInfo;
private byte[] buffer;
private int x, y;
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
writeVarInt(byteBuf, 0);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.writeShort(mapInfo.getBuffer().length + 3);
byteBuf.writeShort(buffer.length + 3);
byteBuf.writeByte(0); // scaling
byteBuf.writeByte(mapInfo.getX());
byteBuf.writeByte(mapInfo.getY());
byteBuf.writeByte(x);
byteBuf.writeByte(y);
byteBuf.writeBytes(mapInfo.getBuffer());
byteBuf.writeBytes(buffer);
} else {
byteBuf.writeByte(0); // scaling
@@ -65,13 +65,13 @@ public final class MapDataPacket implements FallbackPacket {
writeVarInt(byteBuf, 0);
}
byteBuf.writeByte(mapInfo.getColumns());
byteBuf.writeByte(mapInfo.getRows());
byteBuf.writeByte(mapInfo.getX());
byteBuf.writeByte(mapInfo.getY());
byteBuf.writeByte(128); // rows
byteBuf.writeByte(128); // columns
byteBuf.writeByte(x);
byteBuf.writeByte(y);
writeVarInt(byteBuf, mapInfo.getBuffer().length);
byteBuf.writeBytes(mapInfo.getBuffer());
writeVarInt(byteBuf, buffer.length);
byteBuf.writeBytes(buffer);
}
}
@@ -22,8 +22,8 @@ import lombok.Getter;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.netty.DeferredByteBufHolder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.netty.DeferredByteBufHolder;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
@@ -99,34 +99,39 @@ verification:
# - DURING_ATTACK: Noví hráči dostanou map captchu jenom při útoku
# - NEVER: Noví hráči nikdy nedostanou map CAPTCHU (Doporučeno)
timing: NEVER
# Měl by Sonar randomizovat pozici X a Y na CAPTCHE?
random-position: true
# Jak moc by měl Sonar znehodnotit charaktery (faktor pro randomizaci)
distortions-factor-x: 50
distortions-factor-y: 50
# Kolik náhodných čar by měl Sonar vykreslit za CAPTCHOU
random-lines: 4
# Kolik náhodných oválů by měl Sonar vykreslit za CAPTCHOU
random-ovals: 1
# Měl by Sonar randomizovat font pro vykreslování textu v CAPTCHE?
random-font-size: true
# Options for the effects on the image shown to the player
effects:
# Adds a glow effect to the image
flare: true
# Adds random lines to the image
scratches: true
# Applies a ripple (sine) distortion filter
ripple: true
# Applies a horizontal distortion filter
smear: true
# Adds a bit of blurriness/distortion to the image
pinch: true
# Applies color correction (color saturation)
saturation: 0.15
# Applies a horizontal triangular ripple filter
distortion: 2
# Kolik odpovědí by měl Sonar předgenerovat?
# Tento úkol běží souběžně v pozadí;
# Hráči jsou schopni se připojit pokud je CAPTCHA připravena
precompute: 1000
# Tento úkol běží souběžně v pozadí
precompute: 500
# Jak dlouho by měl Sonar čekat než hráč selže CAPTCHU?
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
max-duration: 45000
# Kolikrát musí hráč selhat CAPTCHU před celkovém selhání verifikace?
max-tries: 3
# Znaky (čísla a písmena), které jsou povoleny k zobrazování v CAPTCHE
dictionary: '123456789'
# It is not recommended to add numbers or letters that might look like one another
dictionary: '1235689'
# Jaké typy fontů by měl Sonar používat pro map CAPTCHU?
fonts:
font-names:
- Dialog
- DialogInput
- Serif
- SansSerif
- Monospaced
# Zkouší pokuď hráč posílá správný "client brand packet" serveru
client-brand:
@@ -96,36 +96,40 @@ verification:
# - DURING_ATTACK: Neue Spieler erhalten nur während eines Angriffs ein CAPTCHA
# - NEVER: Neue Spieler erhalten nie ein CAPTCHA (Empfohlen)
timing: NEVER
# Soll Sonar die X- und Y-Position des CAPTCHAs zufällig wählen?
random-position: true
# Wie stark sollen Zeichen verzerrt werden (Faktor für Zufälligkeit)?
distortions-factor-x: 50
# Wie stark sollen Zeichen verzerrt werden (Faktor für Zufälligkeit)?
distortions-factor-y: 50
# Wie viele zufällige Linien hinter dem CAPTCHA soll Sonar zeichnen?
random-lines: 4
# Wie viele zufällige Ovale hinter dem CAPTCHA soll Sonar zeichnen?
random-ovals: 1
# Soll Sonar die Schriftgröße für das Rendern des CAPTCHAs zufällig wählen?
random-font-size: true
# Options for the effects on the image shown to the player
effects:
# Adds a glow effect to the image
flare: true
# Adds random lines to the image
scratches: true
# Applies a ripple (sine) distortion filter
ripple: true
# Applies a horizontal distortion filter
smear: true
# Adds a bit of blurriness/distortion to the image
pinch: true
# Applies color correction (color saturation)
saturation: 0.15
# Applies a horizontal triangular ripple filter
distortion: 2
# Wie viele Antworten soll Sonar vorbereiten?
# Dieser Vorgang erfolgt asynchron im Hintergrund;
# Spieler können beitreten, sobald ein CAPTCHA vorbereitet wurde
precompute: 1000
# Dieser Vorgang erfolgt asynchron im Hintergrund
precompute: 500
# Wie lange soll Sonar warten, bis der Spieler das CAPTCHA nicht besteht?
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
max-duration: 45000
# Wie oft muss ein Spieler das CAPTCHA nicht bestehen, bevor er die Überprüfung nicht besteht?
max-tries: 3
# Zeichen (Buchstaben und Zahlen), die im Antwort-CAPTCHA erscheinen dürfen
dictionary: '123456789'
# It is not recommended to add numbers or letters that might look like one another
dictionary: '1235689'
# Welche Schriftarten soll Sonar für die Map-CAPTCHA-Codes verwenden?
fonts:
font-names:
- Dialog
- DialogInput
- Serif
- SansSerif
- Monospaced
# Überprüft, ob die Spieler eine gültige Client-Marke an den Server senden
client-brand:
# Soll Sonar auf gültige Client-Marken prüfen? (Empfohlen)
@@ -98,34 +98,39 @@ verification:
# - DURING_ATTACK: New players will only receive a CAPTCHA during an attack
# - NEVER: New players will never receive a CAPTCHA (Recommended)
timing: NEVER
# Should Sonar randomize the X and Y position of the CAPTCHA?
random-position: true
# How much should Sonar distort characters (factor for randomization)?
distortions-factor-x: 50
distortions-factor-y: 50
# How many random lines behind the CAPTCHA should Sonar draw?
random-lines: 4
# How many random ovals behind the CAPTCHA should Sonar draw?
random-ovals: 1
# Should Sonar randomize the size of the font used for rendering the CAPTCHA?
random-font-size: true
# Options for the effects on the image shown to the player
effects:
# Adds a glow effect to the image
flare: true
# Adds random lines to the image
scratches: true
# Applies a ripple (sine) distortion filter
ripple: true
# Applies a horizontal distortion filter
smear: true
# Adds a bit of blurriness/distortion to the image
pinch: true
# Applies color correction (color saturation)
saturation: 0.15
# Applies a horizontal triangular ripple filter
distortion: 2
# How many answers should Sonar prepare?
# This task happens asynchronously in the background;
# Players are able to join once one CAPTCHA has been prepared
precompute: 1000
# This task happens asynchronously in the background
precompute: 500
# How long should Sonar wait until the player fails the CAPTCHA?
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
max-duration: 45000
# How many times must a player fail the CAPTCHA before failing the verification?
max-tries: 3
# Characters (letters and numbers) that are allowed to appear in the answer to the CAPTCHA
dictionary: '123456789'
# Which font types should Sonar use for the map CAPTCHA codes?
fonts:
# It is not recommended to add numbers or letters that might look like one another
dictionary: '1235689'
# Which fonts should Sonar use for the map CAPTCHA codes?
font-names:
- Dialog
- DialogInput
- Serif
- SansSerif
- Monospaced
# Checks if the players is sending a valid client brand to the server
client-brand:
@@ -98,34 +98,39 @@ verification:
# - DURING_ATTACK: ახალი მოთამაშებს მოუწევთ CAPTCHA-ს გაკეთება მხოლოდ შეტევის დროს
# - NEVER: ახალ მოთამაშეებს არასდროს მოუწევთ CAPTCHA-ს გაკეთება (Recommended)
timing: NEVER
# მოახდინოს თუ არა Sonar-მა CAPTCHA-ს X და Y პოზიციების რანდომიზაცია?
random-position: true
# რამდენად უნდა დაამახინჯოს Sonar-მა სიმბოლოები (რანდომიზაციის ფაქტორი)?
distortions-factor-x: 50
distortions-factor-y: 50
# რამდენი რანდომული ხაზი უნდა დახატოს Sonar-მა CAPTCHA-ს უკან?
random-lines: 4
# რამდენი რანდომული ოვალი უნდა დახატოს სონარმა CAPTCHA-ს უკან?
random-ovals: 1
# დაარანდომაიზოს Sonar-მა თუ არა შერჩევისას შრიფტის ზომა, რომელიც გამოიყენება CAPTCHA-ზე?
random-font-size: true
# Options for the effects on the image shown to the player
effects:
# Adds a glow effect to the image
flare: true
# Adds random lines to the image
scratches: true
# Applies a ripple (sine) distortion filter
ripple: true
# Applies a horizontal distortion filter
smear: true
# Adds a bit of blurriness/distortion to the image
pinch: true
# Applies color correction (color saturation)
saturation: 0.15
# Applies a horizontal triangular ripple filter
distortion: 2
# რამდენი პასუხი უნდა მოამზადოს Sonar-მა?
# ეს ხდება ასინქრონულად უკანა ფონზე;
# მოთამაშეებს შეუძლიათ შესვლა, როდესაც ერთი CAPTCHA მზად არის
precompute: 1000
# ეს ხდება ასინქრონულად უკანა ფონზე
precompute: 500
# რამდენ ხანს უნდა დაელოდოს Sonar-ი, სანამ მოთამაშე ვერ გააკეთებს წარმატებულად CAPTCHA-ს?
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
max-duration: 45000
# რამდენჯერ ვერ უდნა გაიაროს CAPTCHA მოთამაშემ სანამ ვერიფიკაციას ვერ გაივლის?
max-tries: 3
# სიმბოლოები (ასოები და რიცხვები), რომლებიც დაშვებულია გამოჩნდეს CAPTCHA-ს პასუხში
dictionary: '123456789'
# რომელი შრიფტის ტიპები უნდა გამოიყენოს Sonar-მა რუკის CAPTCHA კოდებისთვის?
fonts:
# It is not recommended to add numbers or letters that might look like one another
dictionary: '1235689'
# Which fonts should Sonar use for the map CAPTCHA codes?
font-names:
- Dialog
- DialogInput
- Serif
- SansSerif
- Monospaced
#ამოწმებს, აგზავნიან თუ არა მოთამაშეები სერვერზე რეალურ კლიენტის სახელს
client-brand:
@@ -77,7 +77,7 @@ verification:
enabled: true
# Должен ли Sonar проверять, сталкивается ли игрок с блоками? (Рекомендуется)
check-collisions: true
# Должен ли Sonar проверять, игрока решать CAPTCHA, если он не прошел проверку гравитации?
# Должен ли Sonar заставлять игрока решать CAPTCHA, если он не проходит проверку на гравитацию?
captcha-on-fail: false
# Максимальное количество тиков, на которое должен упасть игрок, чтобы ему было разрешено упасть на платформу
max-movement-ticks: 8
@@ -98,34 +98,39 @@ verification:
# - DURING_ATTACK: Новые игроки будут получать капчу только во время атаки
# - NEVER: Новые игроки никогда не получат капчу (Рекомендуется)
timing: NEVER
# Должен ли Sonar рандомизировать положение X и Y капчи?
random-position: true
# Насколько сильно Sonar должен искажать символы (фактор для рандомизации)?
distortions-factor-x: 50
distortions-factor-y: 50
# Сколько случайных линий за капчей должен нарисовать Sonar?
random-lines: 4
# Сколько случайных овалов за капчей должен нарисовать Sonar?
random-ovals: 1
# Должен ли Sonar рандомизировать размер шрифта, используемого для отображения капчи?
random-font-size: true
# Options for the effects on the image shown to the player
effects:
# Adds a glow effect to the image
flare: true
# Adds random lines to the image
scratches: true
# Applies a ripple (sine) distortion filter
ripple: true
# Applies a horizontal distortion filter
smear: true
# Adds a bit of blurriness/distortion to the image
pinch: true
# Applies color correction (color saturation)
saturation: 0.15
# Applies a horizontal triangular ripple filter
distortion: 2
# Сколько капч должен подготовить Sonar?
# Эта задача выполняется асинхронно в фоновом режиме
# Игроки смогут присоединиться, как только будет подготовлена одна капча
precompute: 1000
precompute: 500
# Как долго Sonar должен ждать, пока игрок не пройдет капча?
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
max-duration: 45000
# Сколько раз игрок должен завалить капчу, чтобы не пройти проверку?
max-tries: 3
# Символы (буквы и цифры), которые разрешено использовать в ответе на капчу
dictionary: '123456789'
# It is not recommended to add numbers or letters that might look like one another
dictionary: '1235689'
# Какие типы шрифтов следует использовать в Sonar для текста в капче?
fonts:
font-names:
- Dialog
- DialogInput
- Serif
- SansSerif
- Monospaced
# Проверяет, отправляет ли игрок на сервер действительный клиентский бренд.
client-brand: