diff --git a/src/main/java/lc/captchas/FontFunCaptcha.java b/src/main/java/lc/captchas/FontFunCaptcha.java index 8dbea52..18fe53d 100644 --- a/src/main/java/lc/captchas/FontFunCaptcha.java +++ b/src/main/java/lc/captchas/FontFunCaptcha.java @@ -58,9 +58,9 @@ public class FontFunCaptcha implements ChallengeProvider { return null; } - private byte[] fontFun(String captchaText, String level, String path) { + private byte[] fontFun(final int width, final int height, String captchaText, String level, String path) { String[] colors = {"#f68787", "#f8a978", "#f1eb9a", "#a4f6a5"}; - BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB); + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D graphics2D = img.createGraphics(); for (int i = 0; i < captchaText.length(); i++) { Font font = loadCustomFont(level, path); @@ -81,10 +81,13 @@ public class FontFunCaptcha implements ChallengeProvider { return baos.toByteArray(); } - public Challenge returnChallenge() { + public Challenge returnChallenge(String level, String size) { String secret = HelperFunctions.randomString(7); + final int[] size2D = HelperFunctions.parseSize2D(size); + final int width = size2D[0]; + final int height = size2D[1]; String path = "./lib/fonts/"; - return new Challenge(fontFun(secret, "medium", path), "image/png", secret.toLowerCase()); + return new Challenge(fontFun(width, height, secret, "medium", path), "image/png", secret.toLowerCase()); } public boolean checkAnswer(String secret, String answer) { diff --git a/src/main/java/lc/captchas/PoppingCharactersCaptcha.java b/src/main/java/lc/captchas/PoppingCharactersCaptcha.java index 0ffa4df..a8bc688 100644 --- a/src/main/java/lc/captchas/PoppingCharactersCaptcha.java +++ b/src/main/java/lc/captchas/PoppingCharactersCaptcha.java @@ -9,7 +9,6 @@ import java.io.IOException; import java.util.Map; import java.util.function.Consumer; import java.util.stream.IntStream; -import java.util.LinkedList; import java.util.List; import javax.imageio.stream.MemoryCacheImageOutputStream; @@ -20,28 +19,26 @@ import lc.misc.HelperFunctions; import lc.misc.GifSequenceWriter; public class PoppingCharactersCaptcha implements ChallengeProvider { - private final Font font = new Font("Arial", Font.ROMAN_BASELINE, 48); - private final int width = 250; - private final int height = 100; - private Integer[] computeOffsets(final String text) { + private int[] computeOffsets(final Font font, final int width, final int height, final String text) { final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); final var graphics2D = img.createGraphics(); final var frc = graphics2D.getFontRenderContext(); - final var advances = new LinkedList(); + final var advances = new int[text.length() + 1]; final var spacing = font.getStringBounds(" ", frc).getWidth() / 3; var currX = 0; for (int i = 0; i < text.length(); i++) { final var c = text.charAt(i); - advances.add(currX); + advances[i] = currX; currX += font.getStringBounds(String.valueOf(c), frc).getWidth(); currX += spacing; }; + advances[text.length()] = currX; graphics2D.dispose(); - return advances.toArray(new Integer[]{}); + return advances; } - private BufferedImage makeImage(final Consumer f) { + private BufferedImage makeImage(final Font font, final int width, final int height, final Consumer f) { final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); final var graphics2D = img.createGraphics(); graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); @@ -56,23 +53,28 @@ public class PoppingCharactersCaptcha implements ChallengeProvider { return HelperFunctions.randomNumber(-2, +2); } - private byte[] gifCaptcha(final String text) { + private byte[] gifCaptcha(final int width, final int height, final String text) { try { + final var fontHeight = (int) (height * 0.5); + final Font font = new Font("Arial", Font.ROMAN_BASELINE, fontHeight); final var byteArrayOutputStream = new ByteArrayOutputStream(); final var output = new MemoryCacheImageOutputStream(byteArrayOutputStream); final var writer = new GifSequenceWriter(output, 1, 900, true); - final var advances = computeOffsets(text); + final var advances = computeOffsets(font, width, height, text); + final var expectedWidth = advances[advances.length - 1]; + final var scale = width / (float) expectedWidth; final var prevColor = Color.getHSBColor(0f, 0f, 0.1f); IntStream.range(0, text.length()).forEach(i -> { final var color = Color.getHSBColor(HelperFunctions.randomNumber(0, 100)/100.0f, 0.6f, 1.0f); - final var nextImage = makeImage((g) -> { + final var nextImage = makeImage(font, width, height, (g) -> { + g.scale(scale, 1); if (i > 0) { final var prevI = (i - 1) % text.length(); g.setColor(prevColor); - g.drawString(String.valueOf(text.charAt(prevI)), advances[prevI] + jitter(), 45 + jitter()); + g.drawString(String.valueOf(text.charAt(prevI)), advances[prevI] + jitter(), fontHeight*1.1f + jitter()); } g.setColor(color); - g.drawString(String.valueOf(text.charAt(i)), advances[i] + jitter(), 45 + jitter()); + g.drawString(String.valueOf(text.charAt(i)), advances[i] + jitter(), fontHeight*1.1f + jitter()); }); try { writer.writeToSequence(nextImage); @@ -100,9 +102,12 @@ public class PoppingCharactersCaptcha implements ChallengeProvider { "supportedInputType", List.of("text")); } - public Challenge returnChallenge() { + public Challenge returnChallenge(String level, String size) { final var secret = HelperFunctions.randomString(6); - return new Challenge(gifCaptcha(secret), "image/gif", secret.toLowerCase()); + final int[] size2D = HelperFunctions.parseSize2D(size); + final int width = size2D[0]; + final int height = size2D[1]; + return new Challenge(gifCaptcha(width, height, secret), "image/gif", secret.toLowerCase()); } public boolean checkAnswer(String secret, String answer) { diff --git a/src/main/java/lc/captchas/ShadowTextCaptcha.java b/src/main/java/lc/captchas/ShadowTextCaptcha.java index a797c50..cdc314f 100644 --- a/src/main/java/lc/captchas/ShadowTextCaptcha.java +++ b/src/main/java/lc/captchas/ShadowTextCaptcha.java @@ -4,7 +4,6 @@ import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Color; import java.awt.Font; -import java.awt.font.TextLayout; import java.awt.image.BufferedImage; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; @@ -38,32 +37,38 @@ public class ShadowTextCaptcha implements ChallengeProvider { return answer.toLowerCase().equals(secret); } - private byte[] shadowText(String text) { - BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB); - Font font = new Font("Arial", Font.ROMAN_BASELINE, 48); - Graphics2D graphics2D = img.createGraphics(); - graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - graphics2D.setRenderingHint( - RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + private float[] makeKernel(int size) { + final int N = size * size; + final float weight = 1.0f / (N); + final float[] kernel = new float[N]; + java.util.Arrays.fill(kernel, weight); + return kernel; + }; - TextLayout textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext()); + private byte[] shadowText(final int width, final int height, String text) { + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + final int fontHeight = (int) (height * 0.5f); + Font font = new Font("Arial", Font.PLAIN, fontHeight); + Graphics2D graphics2D = img.createGraphics(); HelperFunctions.setRenderingHints(graphics2D); graphics2D.setPaint(Color.WHITE); - graphics2D.fillRect(0, 0, 350, 100); + graphics2D.fillRect(0, 0, width, height); graphics2D.setPaint(Color.BLACK); - textLayout.draw(graphics2D, 15, 50); + graphics2D.setFont(font); + final var stringWidth = graphics2D.getFontMetrics().stringWidth(text); + final var scaleX = (stringWidth > width) ? width/((double) stringWidth) : 1d; + graphics2D.scale(scaleX, 1d); + graphics2D.drawString(text, 0, fontHeight*1.1f); graphics2D.dispose(); - float[] kernel = { - 1f / 9f, 1f / 9f, 1f / 9f, - 1f / 9f, 1f / 9f, 1f / 9f, - 1f / 9f, 1f / 9f, 1f / 9f - }; - ConvolveOp op = new ConvolveOp(new Kernel(3, 3, kernel), ConvolveOp.EDGE_NO_OP, null); + final int kernelSize = (int) Math.ceil((Math.min(width, height) / 50.0)); + ConvolveOp op = new ConvolveOp(new Kernel(kernelSize, kernelSize, makeKernel(kernelSize)), ConvolveOp.EDGE_NO_OP, null); BufferedImage img2 = op.filter(img, null); Graphics2D g2d = img2.createGraphics(); HelperFunctions.setRenderingHints(g2d); g2d.setPaint(Color.WHITE); - textLayout.draw(g2d, 13, 50); + g2d.scale(scaleX, 1d); + g2d.setFont(font); + g2d.drawString(text, -kernelSize, fontHeight*1.1f); g2d.dispose(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { @@ -74,8 +79,11 @@ public class ShadowTextCaptcha implements ChallengeProvider { return baos.toByteArray(); } - public Challenge returnChallenge() { + public Challenge returnChallenge(String level, String size) { String secret = HelperFunctions.randomString(6); - return new Challenge(shadowText(secret), "image/png", secret.toLowerCase()); + final int[] size2D = HelperFunctions.parseSize2D(size); + final int width = size2D[0]; + final int height = size2D[1]; + return new Challenge(shadowText(width, height, secret), "image/png", secret.toLowerCase()); } } diff --git a/src/main/java/lc/captchas/interfaces/ChallengeProvider.java b/src/main/java/lc/captchas/interfaces/ChallengeProvider.java index a3a70e2..f445a7d 100644 --- a/src/main/java/lc/captchas/interfaces/ChallengeProvider.java +++ b/src/main/java/lc/captchas/interfaces/ChallengeProvider.java @@ -6,7 +6,7 @@ import java.util.List; public interface ChallengeProvider { public String getId(); - public Challenge returnChallenge(); + public Challenge returnChallenge(String level, String size); public boolean checkAnswer(String secret, String answer); diff --git a/src/main/java/lc/misc/HelperFunctions.java b/src/main/java/lc/misc/HelperFunctions.java index 22b3a8a..5fc7912 100644 --- a/src/main/java/lc/misc/HelperFunctions.java +++ b/src/main/java/lc/misc/HelperFunctions.java @@ -11,7 +11,14 @@ public class HelperFunctions { random.setSeed(seed); } + public static int[] parseSize2D(final String size) { + final String[] fields = size.split("x"); + final int[] result = {Integer.parseInt(fields[0]), Integer.parseInt(fields[1])}; + return result; + } + public static void setRenderingHints(Graphics2D g2d) { + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2d.setRenderingHint( @@ -23,7 +30,8 @@ public class HelperFunctions { public static final String safeNumbers = "23456789"; public static final String allNumbers = safeNumbers + "10"; public static final String specialCharacters = "$#%@&?"; - public static final String safeCharacters = safeAlphabets + safeNumbers + specialCharacters; + public static final String safeAlphaNum = safeAlphabets + safeNumbers; + public static final String safeCharacters = safeAlphaNum + specialCharacters; public static String randomString(final int n) { return randomString(n, safeCharacters); diff --git a/src/main/resources/index.html b/src/main/resources/index.html index db1a33b..9321607 100644 --- a/src/main/resources/index.html +++ b/src/main/resources/index.html @@ -20,9 +20,10 @@ const levelInput = document.getElementById("levelInput").value const mediaInput = document.getElementById("mediaInput").value const typeInput = document.getElementById("typeInput").value - fetch("/v1/captcha", { + const sizeInput = document.getElementById("sizeInput").value + fetch("/v2/captcha", { method: 'POST', - body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput}) + body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput, "size": sizeInput}) }).then(async function(resp) { const respJson = await resp.json() if (resp.ok) { @@ -30,7 +31,7 @@ const resultDiv = document.getElementById("result") const result = `

Id: ${id}

-

+

@@ -43,7 +44,7 @@ } async function submitAnswer(id) { const ans = document.getElementById("answerInput").value; - const resp = await fetch("/v1/answer", { + const resp = await fetch("/v2/answer", { method: 'POST', body: JSON.stringify({id: id, answer: ans}) }) @@ -70,6 +71,10 @@ Input Type
+
+ Input Size + +
diff --git a/src/main/scala/lc/background/taskThread.scala b/src/main/scala/lc/background/taskThread.scala index 7398e41..8b1ca5c 100644 --- a/src/main/scala/lc/background/taskThread.scala +++ b/src/main/scala/lc/background/taskThread.scala @@ -45,8 +45,10 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) { (config.captchaConfig).flatMap { captcha => (captcha.allowedLevels).flatMap { level => (captcha.allowedMedia).flatMap { media => - (captcha.allowedInputType).map { inputType => - Parameters(level, media, inputType, Some(Size(0, 0))) + (captcha.allowedInputType).flatMap { inputType => + (captcha.allowedSizes).map {size => + Parameters(level, media, inputType, size) + } } } } @@ -58,8 +60,9 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) { val level = pickRandom(captcha.allowedLevels) val media = pickRandom(captcha.allowedMedia) val inputType = pickRandom(captcha.allowedInputType) + val size = pickRandom(captcha.allowedSizes) - Parameters(level, media, inputType, Some(Size(0, 0))) + Parameters(level, media, inputType, size) } private def pickRandom[T](list: List[T]): T = { diff --git a/src/main/scala/lc/captchas/DebugCaptcha.scala b/src/main/scala/lc/captchas/DebugCaptcha.scala index b0ca809..8c3a605 100644 --- a/src/main/scala/lc/captchas/DebugCaptcha.scala +++ b/src/main/scala/lc/captchas/DebugCaptcha.scala @@ -45,14 +45,14 @@ class DebugCaptcha extends ChallengeProvider { matches } - private def simpleText(text: String): Array[Byte] = { - val img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB) + private def simpleText(width: Int, height: Int, text: String): Array[Byte] = { + val img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) val font = new Font("Arial", Font.ROMAN_BASELINE, 56) val graphics2D = img.createGraphics() val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext()) HelperFunctions.setRenderingHints(graphics2D) graphics2D.setPaint(Color.WHITE) - graphics2D.fillRect(0, 0, 350, 100) + graphics2D.fillRect(0, 0, width, height) graphics2D.setPaint(Color.BLACK) textLayout.draw(graphics2D, 15, 50) graphics2D.dispose() @@ -66,8 +66,11 @@ class DebugCaptcha extends ChallengeProvider { baos.toByteArray() } - def returnChallenge(): Challenge = { + def returnChallenge(level: String, size: String): Challenge = { val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets) - new Challenge(simpleText(secret), "image/png", secret.toLowerCase()) + val size2D = HelperFunctions.parseSize2D(size) + val width = size2D(0) + val height = size2D(1) + new Challenge(simpleText(width, height, secret), "image/png", secret.toLowerCase()) } } diff --git a/src/main/scala/lc/captchas/FilterChallenge.scala b/src/main/scala/lc/captchas/FilterChallenge.scala index 5f83ff6..2944cb1 100644 --- a/src/main/scala/lc/captchas/FilterChallenge.scala +++ b/src/main/scala/lc/captchas/FilterChallenge.scala @@ -10,6 +10,7 @@ import lc.captchas.interfaces.Challenge import java.util.{List => JavaList, Map => JavaMap} import java.io.ByteArrayOutputStream import lc.misc.PngImageWriter +import lc.misc.HelperFunctions class FilterChallenge extends ChallengeProvider { def getId = "FilterChallenge" @@ -29,31 +30,38 @@ class FilterChallenge extends ChallengeProvider { ) } - def returnChallenge(): Challenge = { - val filterTypes = List(new FilterType1, new FilterType2) + private val filterTypes = List(new FilterType1, new FilterType2) + + def returnChallenge(level: String, size: String): Challenge = { + val mediumLevel = level == "medium" val r = new scala.util.Random - val alphabet = "abcdefghijklmnopqrstuvwxyz" - val n = 8 - val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString - val canvas = new BufferedImage(225, 50, BufferedImage.TYPE_INT_RGB) + val characters = if (mediumLevel) HelperFunctions.safeAlphaNum else HelperFunctions.safeCharacters + val n = if (mediumLevel) 5 else 7 + val secret = LazyList.continually(r.nextInt(characters.size)).map(characters).take(n).mkString + val size2D = HelperFunctions.parseSize2D(size) + val width = size2D(0) + val height = size2D(1) + val canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) val g = canvas.createGraphics() + val fontHeight = (height*0.6).toInt g.setColor(Color.WHITE) g.fillRect(0, 0, canvas.getWidth, canvas.getHeight) g.setColor(Color.BLACK) - g.setFont(new Font("Serif", Font.PLAIN, 30)) - g.drawString(secret, 5, 30) + val font = new Font("Serif", Font.BOLD, fontHeight) + g.setFont(font) + val stringWidth = g.getFontMetrics().stringWidth(secret) + val scaleX = if (stringWidth > width) width/(stringWidth.toDouble) else 1d + val margin = if (stringWidth > width) 0 else (width - stringWidth) + val xOffset = (margin*r.nextDouble).toInt + g.scale(scaleX, 1d) + g.drawString(secret, xOffset, fontHeight) g.dispose() var image = ImmutableImage.fromAwt(canvas) - val s = scala.util.Random.nextInt(2) - image = filterTypes(s).applyFilter(image) + val s = r.nextInt(2) + image = filterTypes(s).applyFilter(image, !mediumLevel) val img = image.awt() val baos = new ByteArrayOutputStream() - try { - PngImageWriter.write(baos, img); - } catch { - case e: Exception => - e.printStackTrace() - } + PngImageWriter.write(baos, img); new Challenge(baos.toByteArray, "image/png", secret) } def checkAnswer(secret: String, answer: String): Boolean = { @@ -62,14 +70,15 @@ class FilterChallenge extends ChallengeProvider { } trait FilterType { - def applyFilter(image: ImmutableImage): ImmutableImage + def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage } class FilterType1 extends FilterType { - override def applyFilter(image: ImmutableImage): ImmutableImage = { - val blur = new GaussianBlurFilter(2) + override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = { + val radius = if (hardLevel) 3 else 2 + val blur = new GaussianBlurFilter(radius) val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1) - val diffuse = new DiffuseFilter(2) + val diffuse = new DiffuseFilter(radius.toFloat) blur.apply(image) diffuse.apply(image) smear.apply(image) @@ -78,9 +87,10 @@ class FilterType1 extends FilterType { } class FilterType2 extends FilterType { - override def applyFilter(image: ImmutableImage): ImmutableImage = { + override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = { + val radius = if (hardLevel) 2f else 1f val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1) - val diffuse = new DiffuseFilter(1) + val diffuse = new DiffuseFilter(radius) val ripple = new RippleFilter(com.sksamuel.scrimage.filter.RippleType.Noise, 1, 1, 0.005.toFloat, 0.005.toFloat) diffuse.apply(image) ripple.apply(image) diff --git a/src/main/scala/lc/captchas/LabelCaptcha.scala b/src/main/scala/lc/captchas/LabelCaptcha.scala index 5173346..3a02770 100644 --- a/src/main/scala/lc/captchas/LabelCaptcha.scala +++ b/src/main/scala/lc/captchas/LabelCaptcha.scala @@ -40,7 +40,7 @@ class LabelCaptcha extends ChallengeProvider { ) } - def returnChallenge(): Challenge = + def returnChallenge(level: String, size: String): Challenge = synchronized { val r = scala.util.Random.nextInt(knownFiles.length) val s = scala.util.Random.nextInt(unknownFiles.length) diff --git a/src/main/scala/lc/captchas/RainDropsCaptcha.scala b/src/main/scala/lc/captchas/RainDropsCaptcha.scala index 9e8f6f3..40cd217 100644 --- a/src/main/scala/lc/captchas/RainDropsCaptcha.scala +++ b/src/main/scala/lc/captchas/RainDropsCaptcha.scala @@ -11,6 +11,7 @@ import lc.captchas.interfaces.ChallengeProvider import lc.captchas.interfaces.Challenge import lc.misc.GifSequenceWriter import java.util.{List => JavaList, Map => JavaMap} +import lc.misc.HelperFunctions class Drop { var x = 0 @@ -24,8 +25,6 @@ class Drop { } class RainDropsCP extends ChallengeProvider { - private val alphabet = "abcdefghijklmnopqrstuvwxyz" - private val n = 6 private val bgColor = new Color(200, 200, 200) private val textColor = new Color(208, 208, 218) private val textHighlightColor = new Color(100, 100, 125) @@ -56,11 +55,13 @@ class RainDropsCP extends ChallengeProvider { }) } - def returnChallenge(): Challenge = { + def returnChallenge(level: String, size: String): Challenge = { val r = new scala.util.Random - val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString - val width = 450 - val height = 100 + val n = if (level == "easy") 4 else 6 + val secret = HelperFunctions.randomString(n, HelperFunctions.safeAlphaNum) + val size2D = HelperFunctions.parseSize2D(size) + val width = size2D(0) + val height = size2D(1) val imgType = BufferedImage.TYPE_INT_RGB val xOffset = 2 + r.nextInt(3) val xBias = (height / 10) - 2 @@ -80,7 +81,8 @@ class RainDropsCP extends ChallengeProvider { xOffset ) - val baseFont = new Font(Font.MONOSPACED, Font.BOLD, 80) + val fontHeight = (height * 0.5f).toInt + val baseFont = new Font(Font.MONOSPACED, Font.BOLD, fontHeight) val attributes = new java.util.HashMap[TextAttribute, Object]() attributes.put(TextAttribute.TRACKING, Double.box(0.2)) attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_EXTRABOLD) @@ -117,17 +119,22 @@ class RainDropsCP extends ChallengeProvider { } } - // center the text g.setFont(spacedFont) - val textWidth = g.getFontMetrics().charsWidth(secret.toCharArray, 0, secret.toCharArray.length) - val textX = (width - textWidth) / 2 + val textWidth = g.getFontMetrics().stringWidth(secret) + val scaleX = if (textWidth > width) width / textWidth.toDouble else 1.0d + g.scale(scaleX, 1) - // paint the top outline + // center the text + val textX = if (textWidth > width) 0 else ((width - textWidth) / 2) + + // this will be overlapped by the following text to show the top outline because of the offset + val yOffset = (fontHeight*0.01).ceil.toInt g.setColor(textHighlightColor) - g.drawString(secret, textX, 69) + g.drawString(secret, textX, (fontHeight*1.1).toInt - yOffset) + // paint the text g.setColor(textColor) - g.drawString(secret, textX, 70) + g.drawString(secret, textX, (fontHeight*1.1).toInt) g.dispose() writer.writeToSequence(canvas) diff --git a/src/main/scala/lc/core/captchaFields.scala b/src/main/scala/lc/core/captchaFields.scala index acc9f00..ee0285c 100644 --- a/src/main/scala/lc/core/captchaFields.scala +++ b/src/main/scala/lc/core/captchaFields.scala @@ -10,7 +10,7 @@ object ParametersEnum extends Enumeration { val ALLOWEDLEVELS: Value = Value("allowedLevels") val ALLOWEDMEDIA: Value = Value("allowedMedia") val ALLOWEDINPUTTYPE: Value = Value("allowedInputType") - + val ALLOWEDSIZES: Value = Value("allowedSizes") } object AttributesEnum extends Enumeration { diff --git a/src/main/scala/lc/core/captchaManager.scala b/src/main/scala/lc/core/captchaManager.scala index 4943485..95a951c 100644 --- a/src/main/scala/lc/core/captchaManager.scala +++ b/src/main/scala/lc/core/captchaManager.scala @@ -34,13 +34,19 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) { } def generateChallenge(param: Parameters): Option[Int] = { - captchaProviders.getProvider(param).flatMap { provider => - val providerId = provider.getId() - val challenge = provider.returnChallenge() - val blob = new ByteArrayInputStream(challenge.content) - val token = insertCaptcha(provider, challenge, providerId, param, blob) - // println("Added new challenge: " + token.toString) - token.map(_.toInt) + try { + captchaProviders.getProvider(param).flatMap { provider => + val providerId = provider.getId() + val challenge = provider.returnChallenge(param.level, param.size) + val blob = new ByteArrayInputStream(challenge.content) + val token = insertCaptcha(provider, challenge, providerId, param, blob) + // println("Added new challenge: " + token.toString) + token.map(_.toInt) + } + } catch { + case e: Exception => + e.printStackTrace() + None } } @@ -58,7 +64,8 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) { insertPstmt.setString(4, challenge.contentType) insertPstmt.setString(5, param.level) insertPstmt.setString(6, param.input_type) - insertPstmt.setBlob(7, blob) + insertPstmt.setString(7, param.size) + insertPstmt.setBlob(8, blob) insertPstmt.executeUpdate() val rs: ResultSet = insertPstmt.getGeneratedKeys() if (rs.next()) { @@ -106,6 +113,7 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) { countPstmt.setString(1, param.level) countPstmt.setString(2, param.media) countPstmt.setString(3, param.input_type) + countPstmt.setString(4, param.size.toString()) val rs = countPstmt.executeQuery() if (rs.next()) { Some(rs.getInt("count")) @@ -123,7 +131,8 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) { tokenPstmt.setString(1, param.level) tokenPstmt.setString(2, param.media) tokenPstmt.setString(3, param.input_type) - tokenPstmt.setInt(4, count) + tokenPstmt.setString(4, param.size) + tokenPstmt.setInt(5, count) val rs = tokenPstmt.executeQuery() if (rs.next()) { Some(rs.getInt("token")) diff --git a/src/main/scala/lc/core/captchaProviders.scala b/src/main/scala/lc/core/captchaProviders.scala index e2ed404..058d535 100644 --- a/src/main/scala/lc/core/captchaProviders.scala +++ b/src/main/scala/lc/core/captchaProviders.scala @@ -19,7 +19,7 @@ class CaptchaProviders(config: Config) { def generateChallengeSamples(): Map[String, Challenge] = { providers.map { case (key, provider) => - (key, provider.returnChallenge()) + (key, provider.returnChallenge("easy", "350x100")) } } @@ -35,6 +35,7 @@ class CaptchaProviders(config: Config) { if configValue.allowedLevels.contains(param.level) if configValue.allowedMedia.contains(param.media) if configValue.allowedInputType.contains(param.input_type) + if configValue.allowedSizes.contains(param.size) } yield (configValue.name, configValue.config) val providerFilter = for { diff --git a/src/main/scala/lc/core/config.scala b/src/main/scala/lc/core/config.scala index 04241ba..dfbab88 100644 --- a/src/main/scala/lc/core/config.scala +++ b/src/main/scala/lc/core/config.scala @@ -81,6 +81,7 @@ class Config(configFilePath: String) { (ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ + (ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~ (AttributesEnum.CONFIG.toString -> JObject()) ), ( @@ -88,6 +89,7 @@ class Config(configFilePath: String) { (ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ + (ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~ (AttributesEnum.CONFIG.toString -> JObject()) ), ( @@ -95,6 +97,7 @@ class Config(configFilePath: String) { (ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ + (ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~ (AttributesEnum.CONFIG.toString -> JObject()) ), ( @@ -102,6 +105,7 @@ class Config(configFilePath: String) { (ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ + (ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~ (AttributesEnum.CONFIG.toString -> JObject()) ) )) diff --git a/src/main/scala/lc/core/models.scala b/src/main/scala/lc/core/models.scala index d2941d7..f496d57 100644 --- a/src/main/scala/lc/core/models.scala +++ b/src/main/scala/lc/core/models.scala @@ -4,8 +4,9 @@ import org.json4s.jackson.Serialization.write import lc.core.Config.formats trait ByteConvert { def toBytes(): Array[Byte] } +// case class Size(height: Int, width: Int) case class Size(height: Int, width: Int) -case class Parameters(level: String, media: String, input_type: String, size: Option[Size]) +case class Parameters(level: String, media: String, input_type: String, size: String) case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } } case class Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[Byte] = { image } } case class Answer(answer: String, id: String) @@ -16,6 +17,7 @@ case class CaptchaConfig( allowedLevels: List[String], allowedMedia: List[String], allowedInputType: List[String], + allowedSizes: List[String], config: String ) case class ConfigField( diff --git a/src/main/scala/lc/database/DB.scala b/src/main/scala/lc/database/DB.scala index 78111a4..edb4da3 100644 --- a/src/main/scala/lc/database/DB.scala +++ b/src/main/scala/lc/database/DB.scala @@ -4,7 +4,7 @@ import java.sql.{Connection, DriverManager, Statement} class DBConn() { val con: Connection = - DriverManager.getConnection("jdbc:h2:./data/H2/captcha2;MAX_COMPACT_TIME=8000;DB_CLOSE_ON_EXIT=FALSE", "sa", "") + DriverManager.getConnection("jdbc:h2:./data/H2/captcha3;MAX_COMPACT_TIME=8000;DB_CLOSE_ON_EXIT=FALSE", "sa", "") def getStatement(): Statement = { con.createStatement() diff --git a/src/main/scala/lc/database/statements.scala b/src/main/scala/lc/database/statements.scala index bc89fb4..599e876 100644 --- a/src/main/scala/lc/database/statements.scala +++ b/src/main/scala/lc/database/statements.scala @@ -17,6 +17,7 @@ class Statements(dbConn: DBConn, maxAttempts: Int) { "contentType varchar, " + "contentLevel varchar, " + "contentInput varchar, " + + "size varchar, " + "image blob, " + "attempted int default 0, " + "PRIMARY KEY(token));" + @@ -37,8 +38,8 @@ class Statements(dbConn: DBConn, maxAttempts: Int) { val insertPstmt: PreparedStatement = dbConn.con.prepareStatement( "INSERT INTO " + - "challenge(id, secret, provider, contentType, contentLevel, contentInput, image) " + - "VALUES (?, ?, ?, ?, ?, ?, ?)", + "challenge(id, secret, provider, contentType, contentLevel, contentInput, size, image) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS ) @@ -77,7 +78,8 @@ class Statements(dbConn: DBConn, maxAttempts: Int) { WHERE attempted < $maxAttempts AND contentLevel = ? AND contentType = ? AND - contentInput = ? + contentInput = ? AND + size = ? """ ) @@ -88,7 +90,8 @@ class Statements(dbConn: DBConn, maxAttempts: Int) { WHERE attempted < $maxAttempts AND contentLevel = ? AND contentType = ? AND - contentInput = ? + contentInput = ? AND + size = ? LIMIT 1 OFFSET FLOOR(RAND()*?) """ diff --git a/src/main/scala/lc/server/Server.scala b/src/main/scala/lc/server/Server.scala index 197a9cc..4801925 100644 --- a/src/main/scala/lc/server/Server.scala +++ b/src/main/scala/lc/server/Server.scala @@ -29,7 +29,7 @@ class Server( .address(new InetSocketAddress(address, port)) .backlog(32) .POST( - "/v1/captcha", + "/v2/captcha", (request) => { val json = parse(request.getBodyString()) val param = json.extract[Parameters] @@ -38,7 +38,7 @@ class Server( } ) .GET( - "/v1/media", + "/v2/media", (request) => { val params = request.getQueryParams() val result = if (params.containsKey("id")) { @@ -52,7 +52,7 @@ class Server( } ) .POST( - "/v1/answer", + "/v2/answer", (request) => { val json = parse(request.getBodyString()) val answer = json.extract[Answer] @@ -76,7 +76,7 @@ class Server(

Welcome to LibreCaptcha server

Link to Demo

-

API is served at /v1/

+

API is served at /v2/

""" new StringResponse(200, str) diff --git a/tests/debug-config.json b/tests/debug-config.json index c0d91b4..85b0ba6 100644 --- a/tests/debug-config.json +++ b/tests/debug-config.json @@ -13,6 +13,7 @@ "allowedLevels" : [ "debug" ], "allowedMedia" : [ "image/png" ], "allowedInputType" : [ "text" ], + "allowedSizes" : [ "350x100" ], "config" : { } }] } diff --git a/tests/locustfile-functional.py b/tests/locustfile-functional.py index 5034e43..57e4840 100644 --- a/tests/locustfile-functional.py +++ b/tests/locustfile-functional.py @@ -22,9 +22,9 @@ class QuickStartUser(SequentialTaskSet): @task def captcha(self): - captcha_params = {"level":"debug","media":"image/png","input_type":"text"} + captcha_params = {"level":"debug","media":"image/png","input_type":"text", "size":"350x100"} - with self.client.post(path="/v1/captcha", json=captcha_params, name="/captcha", catch_response = True) as resp: + with self.client.post(path="/v2/captcha", json=captcha_params, name="/captcha", catch_response = True) as resp: if resp.status_code != 200: resp.failure("Status was not 200: " + resp.text) captchaJson = resp.json() @@ -32,7 +32,7 @@ class QuickStartUser(SequentialTaskSet): if not uuid: resp.failure("uuid not returned on /captcha endpoint: " + resp.text) - with self.client.get(path="/v1/media?id=%s" % uuid, name="/media", stream=True, catch_response = True) as resp: + with self.client.get(path="/v2/media?id=%s" % uuid, name="/media", stream=True, catch_response = True) as resp: if resp.status_code != 200: resp.failure("Status was not 200: " + resp.text) @@ -41,7 +41,7 @@ class QuickStartUser(SequentialTaskSet): ocrAnswer = self.solve(uuid, media) answerBody = {"answer": ocrAnswer,"id": uuid} - with self.client.post(path='/v1/answer', json=answerBody, name="/answer", catch_response=True) as resp: + with self.client.post(path='/v2/answer', json=answerBody, name="/answer", catch_response=True) as resp: if resp.status_code != 200: resp.failure("Status was not 200: " + resp.text) else: diff --git a/tests/locustfile.py b/tests/locustfile.py index 941c45e..07cf572 100644 --- a/tests/locustfile.py +++ b/tests/locustfile.py @@ -24,9 +24,9 @@ class QuickStartUser(SequentialTaskSet): @task def captcha(self): # TODO: Iterate over parameters for a more comprehensive test - captcha_params = {"level":"easy","media":"image/png","input_type":"text"} + captcha_params = {"level":"easy","media":"image/png","input_type":"text", "size":"350x100"} - resp = self.client.post(path="/v1/captcha", json=captcha_params, name="/captcha") + resp = self.client.post(path="/v2/captcha", json=captcha_params, name="/captcha") if resp.status_code != 200: print("\nError on /captcha endpoint: ") print(resp) @@ -36,14 +36,14 @@ class QuickStartUser(SequentialTaskSet): uuid = json.loads(resp.text).get("id") answerBody = {"answer": "qwer123","id": uuid} - resp = self.client.get(path="/v1/media?id=%s" % uuid, name="/media") + resp = self.client.get(path="/v2/media?id=%s" % uuid, name="/media") if resp.status_code != 200: print("\nError on /media endpoint: ") print(resp) print(resp.text) print("----------------END.MEDIA-------------------\n\n") - resp = self.client.post(path='/v1/answer', json=answerBody, name="/answer") + resp = self.client.post(path='/v2/answer', json=answerBody, name="/answer") if resp.status_code != 200: print("\nError on /answer endpoint: ") print(resp)