Merge pull request #153 from librecaptcha/fix98SizeParam

Respect size param
This commit is contained in:
hrj 2022-04-04 21:42:11 +05:30 committed by GitHub
commit c71fdbb8de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 191 additions and 119 deletions

View File

@ -58,9 +58,9 @@ public class FontFunCaptcha implements ChallengeProvider {
return null; 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"}; 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(); Graphics2D graphics2D = img.createGraphics();
for (int i = 0; i < captchaText.length(); i++) { for (int i = 0; i < captchaText.length(); i++) {
Font font = loadCustomFont(level, path); Font font = loadCustomFont(level, path);
@ -81,10 +81,13 @@ public class FontFunCaptcha implements ChallengeProvider {
return baos.toByteArray(); return baos.toByteArray();
} }
public Challenge returnChallenge() { public Challenge returnChallenge(String level, String size) {
String secret = HelperFunctions.randomString(7); 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/"; 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) { public boolean checkAnswer(String secret, String answer) {

View File

@ -9,7 +9,6 @@ import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import javax.imageio.stream.MemoryCacheImageOutputStream; import javax.imageio.stream.MemoryCacheImageOutputStream;
@ -20,28 +19,26 @@ import lc.misc.HelperFunctions;
import lc.misc.GifSequenceWriter; import lc.misc.GifSequenceWriter;
public class PoppingCharactersCaptcha implements ChallengeProvider { 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 img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final var graphics2D = img.createGraphics(); final var graphics2D = img.createGraphics();
final var frc = graphics2D.getFontRenderContext(); final var frc = graphics2D.getFontRenderContext();
final var advances = new LinkedList<Integer>(); final var advances = new int[text.length() + 1];
final var spacing = font.getStringBounds(" ", frc).getWidth() / 3; final var spacing = font.getStringBounds(" ", frc).getWidth() / 3;
var currX = 0; var currX = 0;
for (int i = 0; i < text.length(); i++) { for (int i = 0; i < text.length(); i++) {
final var c = text.charAt(i); final var c = text.charAt(i);
advances.add(currX); advances[i] = currX;
currX += font.getStringBounds(String.valueOf(c), frc).getWidth(); currX += font.getStringBounds(String.valueOf(c), frc).getWidth();
currX += spacing; currX += spacing;
}; };
advances[text.length()] = currX;
graphics2D.dispose(); graphics2D.dispose();
return advances.toArray(new Integer[]{}); return advances;
} }
private BufferedImage makeImage(final Consumer<Graphics2D> f) { private BufferedImage makeImage(final Font font, final int width, final int height, final Consumer<Graphics2D> f) {
final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final var graphics2D = img.createGraphics(); final var graphics2D = img.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
@ -56,23 +53,28 @@ public class PoppingCharactersCaptcha implements ChallengeProvider {
return HelperFunctions.randomNumber(-2, +2); return HelperFunctions.randomNumber(-2, +2);
} }
private byte[] gifCaptcha(final String text) { private byte[] gifCaptcha(final int width, final int height, final String text) {
try { 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 byteArrayOutputStream = new ByteArrayOutputStream();
final var output = new MemoryCacheImageOutputStream(byteArrayOutputStream); final var output = new MemoryCacheImageOutputStream(byteArrayOutputStream);
final var writer = new GifSequenceWriter(output, 1, 900, true); 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); final var prevColor = Color.getHSBColor(0f, 0f, 0.1f);
IntStream.range(0, text.length()).forEach(i -> { IntStream.range(0, text.length()).forEach(i -> {
final var color = Color.getHSBColor(HelperFunctions.randomNumber(0, 100)/100.0f, 0.6f, 1.0f); 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) { if (i > 0) {
final var prevI = (i - 1) % text.length(); final var prevI = (i - 1) % text.length();
g.setColor(prevColor); 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.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 { try {
writer.writeToSequence(nextImage); writer.writeToSequence(nextImage);
@ -100,9 +102,12 @@ public class PoppingCharactersCaptcha implements ChallengeProvider {
"supportedInputType", List.of("text")); "supportedInputType", List.of("text"));
} }
public Challenge returnChallenge() { public Challenge returnChallenge(String level, String size) {
final var secret = HelperFunctions.randomString(6); 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) { public boolean checkAnswer(String secret, String answer) {

View File

@ -4,7 +4,6 @@ import java.awt.Graphics2D;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.Color; import java.awt.Color;
import java.awt.Font; import java.awt.Font;
import java.awt.font.TextLayout;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp; import java.awt.image.ConvolveOp;
import java.awt.image.Kernel; import java.awt.image.Kernel;
@ -38,32 +37,38 @@ public class ShadowTextCaptcha implements ChallengeProvider {
return answer.toLowerCase().equals(secret); return answer.toLowerCase().equals(secret);
} }
private byte[] shadowText(String text) { private float[] makeKernel(int size) {
BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB); final int N = size * size;
Font font = new Font("Arial", Font.ROMAN_BASELINE, 48); final float weight = 1.0f / (N);
Graphics2D graphics2D = img.createGraphics(); final float[] kernel = new float[N];
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); java.util.Arrays.fill(kernel, weight);
graphics2D.setRenderingHint( return kernel;
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); };
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); HelperFunctions.setRenderingHints(graphics2D);
graphics2D.setPaint(Color.WHITE); graphics2D.setPaint(Color.WHITE);
graphics2D.fillRect(0, 0, 350, 100); graphics2D.fillRect(0, 0, width, height);
graphics2D.setPaint(Color.BLACK); 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(); graphics2D.dispose();
float[] kernel = { final int kernelSize = (int) Math.ceil((Math.min(width, height) / 50.0));
1f / 9f, 1f / 9f, 1f / 9f, ConvolveOp op = new ConvolveOp(new Kernel(kernelSize, kernelSize, makeKernel(kernelSize)), ConvolveOp.EDGE_NO_OP, null);
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);
BufferedImage img2 = op.filter(img, null); BufferedImage img2 = op.filter(img, null);
Graphics2D g2d = img2.createGraphics(); Graphics2D g2d = img2.createGraphics();
HelperFunctions.setRenderingHints(g2d); HelperFunctions.setRenderingHints(g2d);
g2d.setPaint(Color.WHITE); 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(); g2d.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
try { try {
@ -74,8 +79,11 @@ public class ShadowTextCaptcha implements ChallengeProvider {
return baos.toByteArray(); return baos.toByteArray();
} }
public Challenge returnChallenge() { public Challenge returnChallenge(String level, String size) {
String secret = HelperFunctions.randomString(6); 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());
} }
} }

View File

@ -6,7 +6,7 @@ import java.util.List;
public interface ChallengeProvider { public interface ChallengeProvider {
public String getId(); public String getId();
public Challenge returnChallenge(); public Challenge returnChallenge(String level, String size);
public boolean checkAnswer(String secret, String answer); public boolean checkAnswer(String secret, String answer);

View File

@ -11,7 +11,14 @@ public class HelperFunctions {
random.setSeed(seed); 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) { public static void setRenderingHints(Graphics2D g2d) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint( g2d.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint( g2d.setRenderingHint(
@ -23,7 +30,8 @@ public class HelperFunctions {
public static final String safeNumbers = "23456789"; public static final String safeNumbers = "23456789";
public static final String allNumbers = safeNumbers + "10"; public static final String allNumbers = safeNumbers + "10";
public static final String specialCharacters = "$#%@&?"; 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) { public static String randomString(final int n) {
return randomString(n, safeCharacters); return randomString(n, safeCharacters);

View File

@ -20,9 +20,10 @@
const levelInput = document.getElementById("levelInput").value const levelInput = document.getElementById("levelInput").value
const mediaInput = document.getElementById("mediaInput").value const mediaInput = document.getElementById("mediaInput").value
const typeInput = document.getElementById("typeInput").value const typeInput = document.getElementById("typeInput").value
fetch("/v1/captcha", { const sizeInput = document.getElementById("sizeInput").value
fetch("/v2/captcha", {
method: 'POST', 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) { }).then(async function(resp) {
const respJson = await resp.json() const respJson = await resp.json()
if (resp.ok) { if (resp.ok) {
@ -30,7 +31,7 @@
const resultDiv = document.getElementById("result") const resultDiv = document.getElementById("result")
const result = ` const result = `
<p>Id: ${id}</p> <p>Id: ${id}</p>
<p><img src="/v1/media?id=${id}" /> </p> <p><img src="/v2/media?id=${id}" /> </p>
<input type="text" id="answerInput" /> <input type="text" id="answerInput" />
<button onClick="submitAnswer('${id}')">Submit</button> <button onClick="submitAnswer('${id}')">Submit</button>
<div id="answerResult" /> <div id="answerResult" />
@ -43,7 +44,7 @@
} }
async function submitAnswer(id) { async function submitAnswer(id) {
const ans = document.getElementById("answerInput").value; const ans = document.getElementById("answerInput").value;
const resp = await fetch("/v1/answer", { const resp = await fetch("/v2/answer", {
method: 'POST', method: 'POST',
body: JSON.stringify({id: id, answer: ans}) body: JSON.stringify({id: id, answer: ans})
}) })
@ -70,6 +71,10 @@
<span>Input Type</span> <span>Input Type</span>
<input type="text" id="typeInput" value="text" /> <input type="text" id="typeInput" value="text" />
</div> </div>
<div class="inputGroup">
<span>Input Size</span>
<input type="text" id="sizeInput" value="150x100" />
</div>
<div class="inputGroup"> <div class="inputGroup">
<button onClick="loadCaptcha()">Get New CAPTCHA</button> <button onClick="loadCaptcha()">Get New CAPTCHA</button>
</div> </div>

View File

@ -45,8 +45,10 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
(config.captchaConfig).flatMap { captcha => (config.captchaConfig).flatMap { captcha =>
(captcha.allowedLevels).flatMap { level => (captcha.allowedLevels).flatMap { level =>
(captcha.allowedMedia).flatMap { media => (captcha.allowedMedia).flatMap { media =>
(captcha.allowedInputType).map { inputType => (captcha.allowedInputType).flatMap { inputType =>
Parameters(level, media, inputType, Some(Size(0, 0))) (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 level = pickRandom(captcha.allowedLevels)
val media = pickRandom(captcha.allowedMedia) val media = pickRandom(captcha.allowedMedia)
val inputType = pickRandom(captcha.allowedInputType) 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 = { private def pickRandom[T](list: List[T]): T = {

View File

@ -45,14 +45,14 @@ class DebugCaptcha extends ChallengeProvider {
matches matches
} }
private def simpleText(text: String): Array[Byte] = { private def simpleText(width: Int, height: Int, text: String): Array[Byte] = {
val img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB) val img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
val font = new Font("Arial", Font.ROMAN_BASELINE, 56) val font = new Font("Arial", Font.ROMAN_BASELINE, 56)
val graphics2D = img.createGraphics() val graphics2D = img.createGraphics()
val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext()) val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext())
HelperFunctions.setRenderingHints(graphics2D) HelperFunctions.setRenderingHints(graphics2D)
graphics2D.setPaint(Color.WHITE) graphics2D.setPaint(Color.WHITE)
graphics2D.fillRect(0, 0, 350, 100) graphics2D.fillRect(0, 0, width, height)
graphics2D.setPaint(Color.BLACK) graphics2D.setPaint(Color.BLACK)
textLayout.draw(graphics2D, 15, 50) textLayout.draw(graphics2D, 15, 50)
graphics2D.dispose() graphics2D.dispose()
@ -66,8 +66,11 @@ class DebugCaptcha extends ChallengeProvider {
baos.toByteArray() baos.toByteArray()
} }
def returnChallenge(): Challenge = { def returnChallenge(level: String, size: String): Challenge = {
val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets) 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())
} }
} }

View File

@ -10,6 +10,7 @@ import lc.captchas.interfaces.Challenge
import java.util.{List => JavaList, Map => JavaMap} import java.util.{List => JavaList, Map => JavaMap}
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import lc.misc.PngImageWriter import lc.misc.PngImageWriter
import lc.misc.HelperFunctions
class FilterChallenge extends ChallengeProvider { class FilterChallenge extends ChallengeProvider {
def getId = "FilterChallenge" def getId = "FilterChallenge"
@ -29,31 +30,38 @@ class FilterChallenge extends ChallengeProvider {
) )
} }
def returnChallenge(): Challenge = { private val filterTypes = List(new FilterType1, new FilterType2)
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 r = new scala.util.Random
val alphabet = "abcdefghijklmnopqrstuvwxyz" val characters = if (mediumLevel) HelperFunctions.safeAlphaNum else HelperFunctions.safeCharacters
val n = 8 val n = if (mediumLevel) 5 else 7
val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString val secret = LazyList.continually(r.nextInt(characters.size)).map(characters).take(n).mkString
val canvas = new BufferedImage(225, 50, BufferedImage.TYPE_INT_RGB) 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 g = canvas.createGraphics()
val fontHeight = (height*0.6).toInt
g.setColor(Color.WHITE) g.setColor(Color.WHITE)
g.fillRect(0, 0, canvas.getWidth, canvas.getHeight) g.fillRect(0, 0, canvas.getWidth, canvas.getHeight)
g.setColor(Color.BLACK) g.setColor(Color.BLACK)
g.setFont(new Font("Serif", Font.PLAIN, 30)) val font = new Font("Serif", Font.BOLD, fontHeight)
g.drawString(secret, 5, 30) 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() g.dispose()
var image = ImmutableImage.fromAwt(canvas) var image = ImmutableImage.fromAwt(canvas)
val s = scala.util.Random.nextInt(2) val s = r.nextInt(2)
image = filterTypes(s).applyFilter(image) image = filterTypes(s).applyFilter(image, !mediumLevel)
val img = image.awt() val img = image.awt()
val baos = new ByteArrayOutputStream() val baos = new ByteArrayOutputStream()
try { PngImageWriter.write(baos, img);
PngImageWriter.write(baos, img);
} catch {
case e: Exception =>
e.printStackTrace()
}
new Challenge(baos.toByteArray, "image/png", secret) new Challenge(baos.toByteArray, "image/png", secret)
} }
def checkAnswer(secret: String, answer: String): Boolean = { def checkAnswer(secret: String, answer: String): Boolean = {
@ -62,14 +70,15 @@ class FilterChallenge extends ChallengeProvider {
} }
trait FilterType { trait FilterType {
def applyFilter(image: ImmutableImage): ImmutableImage def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage
} }
class FilterType1 extends FilterType { class FilterType1 extends FilterType {
override def applyFilter(image: ImmutableImage): ImmutableImage = { override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = {
val blur = new GaussianBlurFilter(2) 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 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) blur.apply(image)
diffuse.apply(image) diffuse.apply(image)
smear.apply(image) smear.apply(image)
@ -78,9 +87,10 @@ class FilterType1 extends FilterType {
} }
class FilterType2 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 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) val ripple = new RippleFilter(com.sksamuel.scrimage.filter.RippleType.Noise, 1, 1, 0.005.toFloat, 0.005.toFloat)
diffuse.apply(image) diffuse.apply(image)
ripple.apply(image) ripple.apply(image)

View File

@ -40,7 +40,7 @@ class LabelCaptcha extends ChallengeProvider {
) )
} }
def returnChallenge(): Challenge = def returnChallenge(level: String, size: String): Challenge =
synchronized { synchronized {
val r = scala.util.Random.nextInt(knownFiles.length) val r = scala.util.Random.nextInt(knownFiles.length)
val s = scala.util.Random.nextInt(unknownFiles.length) val s = scala.util.Random.nextInt(unknownFiles.length)

View File

@ -11,6 +11,7 @@ import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge import lc.captchas.interfaces.Challenge
import lc.misc.GifSequenceWriter import lc.misc.GifSequenceWriter
import java.util.{List => JavaList, Map => JavaMap} import java.util.{List => JavaList, Map => JavaMap}
import lc.misc.HelperFunctions
class Drop { class Drop {
var x = 0 var x = 0
@ -24,8 +25,6 @@ class Drop {
} }
class RainDropsCP extends ChallengeProvider { class RainDropsCP extends ChallengeProvider {
private val alphabet = "abcdefghijklmnopqrstuvwxyz"
private val n = 6
private val bgColor = new Color(200, 200, 200) private val bgColor = new Color(200, 200, 200)
private val textColor = new Color(208, 208, 218) private val textColor = new Color(208, 208, 218)
private val textHighlightColor = new Color(100, 100, 125) 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 r = new scala.util.Random
val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString val n = if (level == "easy") 4 else 6
val width = 450 val secret = HelperFunctions.randomString(n, HelperFunctions.safeAlphaNum)
val height = 100 val size2D = HelperFunctions.parseSize2D(size)
val width = size2D(0)
val height = size2D(1)
val imgType = BufferedImage.TYPE_INT_RGB val imgType = BufferedImage.TYPE_INT_RGB
val xOffset = 2 + r.nextInt(3) val xOffset = 2 + r.nextInt(3)
val xBias = (height / 10) - 2 val xBias = (height / 10) - 2
@ -80,7 +81,8 @@ class RainDropsCP extends ChallengeProvider {
xOffset 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]() val attributes = new java.util.HashMap[TextAttribute, Object]()
attributes.put(TextAttribute.TRACKING, Double.box(0.2)) attributes.put(TextAttribute.TRACKING, Double.box(0.2))
attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_EXTRABOLD) attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_EXTRABOLD)
@ -117,17 +119,22 @@ class RainDropsCP extends ChallengeProvider {
} }
} }
// center the text
g.setFont(spacedFont) g.setFont(spacedFont)
val textWidth = g.getFontMetrics().charsWidth(secret.toCharArray, 0, secret.toCharArray.length) val textWidth = g.getFontMetrics().stringWidth(secret)
val textX = (width - textWidth) / 2 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.setColor(textHighlightColor)
g.drawString(secret, textX, 69) g.drawString(secret, textX, (fontHeight*1.1).toInt - yOffset)
// paint the text // paint the text
g.setColor(textColor) g.setColor(textColor)
g.drawString(secret, textX, 70) g.drawString(secret, textX, (fontHeight*1.1).toInt)
g.dispose() g.dispose()
writer.writeToSequence(canvas) writer.writeToSequence(canvas)

View File

@ -10,7 +10,7 @@ object ParametersEnum extends Enumeration {
val ALLOWEDLEVELS: Value = Value("allowedLevels") val ALLOWEDLEVELS: Value = Value("allowedLevels")
val ALLOWEDMEDIA: Value = Value("allowedMedia") val ALLOWEDMEDIA: Value = Value("allowedMedia")
val ALLOWEDINPUTTYPE: Value = Value("allowedInputType") val ALLOWEDINPUTTYPE: Value = Value("allowedInputType")
val ALLOWEDSIZES: Value = Value("allowedSizes")
} }
object AttributesEnum extends Enumeration { object AttributesEnum extends Enumeration {

View File

@ -34,13 +34,19 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
} }
def generateChallenge(param: Parameters): Option[Int] = { def generateChallenge(param: Parameters): Option[Int] = {
captchaProviders.getProvider(param).flatMap { provider => try {
val providerId = provider.getId() captchaProviders.getProvider(param).flatMap { provider =>
val challenge = provider.returnChallenge() val providerId = provider.getId()
val blob = new ByteArrayInputStream(challenge.content) val challenge = provider.returnChallenge(param.level, param.size)
val token = insertCaptcha(provider, challenge, providerId, param, blob) val blob = new ByteArrayInputStream(challenge.content)
// println("Added new challenge: " + token.toString) val token = insertCaptcha(provider, challenge, providerId, param, blob)
token.map(_.toInt) // 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(4, challenge.contentType)
insertPstmt.setString(5, param.level) insertPstmt.setString(5, param.level)
insertPstmt.setString(6, param.input_type) insertPstmt.setString(6, param.input_type)
insertPstmt.setBlob(7, blob) insertPstmt.setString(7, param.size)
insertPstmt.setBlob(8, blob)
insertPstmt.executeUpdate() insertPstmt.executeUpdate()
val rs: ResultSet = insertPstmt.getGeneratedKeys() val rs: ResultSet = insertPstmt.getGeneratedKeys()
if (rs.next()) { if (rs.next()) {
@ -106,6 +113,7 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
countPstmt.setString(1, param.level) countPstmt.setString(1, param.level)
countPstmt.setString(2, param.media) countPstmt.setString(2, param.media)
countPstmt.setString(3, param.input_type) countPstmt.setString(3, param.input_type)
countPstmt.setString(4, param.size.toString())
val rs = countPstmt.executeQuery() val rs = countPstmt.executeQuery()
if (rs.next()) { if (rs.next()) {
Some(rs.getInt("count")) Some(rs.getInt("count"))
@ -123,7 +131,8 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
tokenPstmt.setString(1, param.level) tokenPstmt.setString(1, param.level)
tokenPstmt.setString(2, param.media) tokenPstmt.setString(2, param.media)
tokenPstmt.setString(3, param.input_type) tokenPstmt.setString(3, param.input_type)
tokenPstmt.setInt(4, count) tokenPstmt.setString(4, param.size)
tokenPstmt.setInt(5, count)
val rs = tokenPstmt.executeQuery() val rs = tokenPstmt.executeQuery()
if (rs.next()) { if (rs.next()) {
Some(rs.getInt("token")) Some(rs.getInt("token"))

View File

@ -19,7 +19,7 @@ class CaptchaProviders(config: Config) {
def generateChallengeSamples(): Map[String, Challenge] = { def generateChallengeSamples(): Map[String, Challenge] = {
providers.map { case (key, provider) => 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.allowedLevels.contains(param.level)
if configValue.allowedMedia.contains(param.media) if configValue.allowedMedia.contains(param.media)
if configValue.allowedInputType.contains(param.input_type) if configValue.allowedInputType.contains(param.input_type)
if configValue.allowedSizes.contains(param.size)
} yield (configValue.name, configValue.config) } yield (configValue.name, configValue.config)
val providerFilter = for { val providerFilter = for {

View File

@ -81,6 +81,7 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~ (ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject()) (AttributesEnum.CONFIG.toString -> JObject())
), ),
( (
@ -88,6 +89,7 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~ (ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject()) (AttributesEnum.CONFIG.toString -> JObject())
), ),
( (
@ -95,6 +97,7 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~ (ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject()) (AttributesEnum.CONFIG.toString -> JObject())
), ),
( (
@ -102,6 +105,7 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~ (ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject()) (AttributesEnum.CONFIG.toString -> JObject())
) )
)) ))

View File

@ -4,8 +4,9 @@ import org.json4s.jackson.Serialization.write
import lc.core.Config.formats import lc.core.Config.formats
trait ByteConvert { def toBytes(): Array[Byte] } trait ByteConvert { def toBytes(): Array[Byte] }
// case class Size(height: Int, width: Int)
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 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 Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[Byte] = { image } }
case class Answer(answer: String, id: String) case class Answer(answer: String, id: String)
@ -16,6 +17,7 @@ case class CaptchaConfig(
allowedLevels: List[String], allowedLevels: List[String],
allowedMedia: List[String], allowedMedia: List[String],
allowedInputType: List[String], allowedInputType: List[String],
allowedSizes: List[String],
config: String config: String
) )
case class ConfigField( case class ConfigField(

View File

@ -4,7 +4,7 @@ import java.sql.{Connection, DriverManager, Statement}
class DBConn() { class DBConn() {
val con: Connection = 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 = { def getStatement(): Statement = {
con.createStatement() con.createStatement()

View File

@ -17,6 +17,7 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
"contentType varchar, " + "contentType varchar, " +
"contentLevel varchar, " + "contentLevel varchar, " +
"contentInput varchar, " + "contentInput varchar, " +
"size varchar, " +
"image blob, " + "image blob, " +
"attempted int default 0, " + "attempted int default 0, " +
"PRIMARY KEY(token));" + "PRIMARY KEY(token));" +
@ -37,8 +38,8 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
val insertPstmt: PreparedStatement = dbConn.con.prepareStatement( val insertPstmt: PreparedStatement = dbConn.con.prepareStatement(
"INSERT INTO " + "INSERT INTO " +
"challenge(id, secret, provider, contentType, contentLevel, contentInput, image) " + "challenge(id, secret, provider, contentType, contentLevel, contentInput, size, image) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)", "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS Statement.RETURN_GENERATED_KEYS
) )
@ -77,7 +78,8 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
WHERE attempted < $maxAttempts AND WHERE attempted < $maxAttempts AND
contentLevel = ? AND contentLevel = ? AND
contentType = ? AND contentType = ? AND
contentInput = ? contentInput = ? AND
size = ?
""" """
) )
@ -88,7 +90,8 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
WHERE attempted < $maxAttempts AND WHERE attempted < $maxAttempts AND
contentLevel = ? AND contentLevel = ? AND
contentType = ? AND contentType = ? AND
contentInput = ? contentInput = ? AND
size = ?
LIMIT 1 LIMIT 1
OFFSET FLOOR(RAND()*?) OFFSET FLOOR(RAND()*?)
""" """

View File

@ -29,7 +29,7 @@ class Server(
.address(new InetSocketAddress(address, port)) .address(new InetSocketAddress(address, port))
.backlog(32) .backlog(32)
.POST( .POST(
"/v1/captcha", "/v2/captcha",
(request) => { (request) => {
val json = parse(request.getBodyString()) val json = parse(request.getBodyString())
val param = json.extract[Parameters] val param = json.extract[Parameters]
@ -38,7 +38,7 @@ class Server(
} }
) )
.GET( .GET(
"/v1/media", "/v2/media",
(request) => { (request) => {
val params = request.getQueryParams() val params = request.getQueryParams()
val result = if (params.containsKey("id")) { val result = if (params.containsKey("id")) {
@ -52,7 +52,7 @@ class Server(
} }
) )
.POST( .POST(
"/v1/answer", "/v2/answer",
(request) => { (request) => {
val json = parse(request.getBodyString()) val json = parse(request.getBodyString())
val answer = json.extract[Answer] val answer = json.extract[Answer]
@ -76,7 +76,7 @@ class Server(
<html> <html>
<h2>Welcome to LibreCaptcha server</h2> <h2>Welcome to LibreCaptcha server</h2>
<h3><a href="/demo/index.html">Link to Demo</a></h3> <h3><a href="/demo/index.html">Link to Demo</a></h3>
<h3>API is served at <b>/v1/</b></h3> <h3>API is served at <b>/v2/</b></h3>
</html> </html>
""" """
new StringResponse(200, str) new StringResponse(200, str)

View File

@ -13,6 +13,7 @@
"allowedLevels" : [ "debug" ], "allowedLevels" : [ "debug" ],
"allowedMedia" : [ "image/png" ], "allowedMedia" : [ "image/png" ],
"allowedInputType" : [ "text" ], "allowedInputType" : [ "text" ],
"allowedSizes" : [ "350x100" ],
"config" : { } "config" : { }
}] }]
} }

View File

@ -22,9 +22,9 @@ class QuickStartUser(SequentialTaskSet):
@task @task
def captcha(self): 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: if resp.status_code != 200:
resp.failure("Status was not 200: " + resp.text) resp.failure("Status was not 200: " + resp.text)
captchaJson = resp.json() captchaJson = resp.json()
@ -32,7 +32,7 @@ class QuickStartUser(SequentialTaskSet):
if not uuid: if not uuid:
resp.failure("uuid not returned on /captcha endpoint: " + resp.text) 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: if resp.status_code != 200:
resp.failure("Status was not 200: " + resp.text) resp.failure("Status was not 200: " + resp.text)
@ -41,7 +41,7 @@ class QuickStartUser(SequentialTaskSet):
ocrAnswer = self.solve(uuid, media) ocrAnswer = self.solve(uuid, media)
answerBody = {"answer": ocrAnswer,"id": uuid} 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: if resp.status_code != 200:
resp.failure("Status was not 200: " + resp.text) resp.failure("Status was not 200: " + resp.text)
else: else:

View File

@ -24,9 +24,9 @@ class QuickStartUser(SequentialTaskSet):
@task @task
def captcha(self): def captcha(self):
# TODO: Iterate over parameters for a more comprehensive test # 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: if resp.status_code != 200:
print("\nError on /captcha endpoint: ") print("\nError on /captcha endpoint: ")
print(resp) print(resp)
@ -36,14 +36,14 @@ class QuickStartUser(SequentialTaskSet):
uuid = json.loads(resp.text).get("id") uuid = json.loads(resp.text).get("id")
answerBody = {"answer": "qwer123","id": uuid} 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: if resp.status_code != 200:
print("\nError on /media endpoint: ") print("\nError on /media endpoint: ")
print(resp) print(resp)
print(resp.text) print(resp.text)
print("----------------END.MEDIA-------------------\n\n") 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: if resp.status_code != 200:
print("\nError on /answer endpoint: ") print("\nError on /answer endpoint: ")
print(resp) print(resp)