From edd211fb52a9bf74308419c0a371fc92a246fa8e Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 1 Mar 2022 22:51:38 +0530 Subject: [PATCH 01/21] respect size and level parameters * Add size and level parameters to ChallengeProvider.returnChallenge() * Add size column to challenge table * Pass size and level parameters to all relevant calls * Add size field to demo app --- src/main/java/lc/captchas/FontFunCaptcha.java | 2 +- .../lc/captchas/PoppingCharactersCaptcha.java | 2 +- src/main/java/lc/captchas/ShadowTextCaptcha.java | 2 +- .../lc/captchas/interfaces/ChallengeProvider.java | 2 +- src/main/resources/index.html | 13 +++++++++---- src/main/scala/lc/background/taskThread.scala | 9 ++++++--- src/main/scala/lc/captchas/DebugCaptcha.scala | 2 +- src/main/scala/lc/captchas/FilterChallenge.scala | 2 +- src/main/scala/lc/captchas/LabelCaptcha.scala | 2 +- src/main/scala/lc/captchas/RainDropsCaptcha.scala | 2 +- src/main/scala/lc/core/captchaFields.scala | 2 +- src/main/scala/lc/core/captchaManager.scala | 15 +++++++++------ src/main/scala/lc/core/captchaProviders.scala | 3 ++- src/main/scala/lc/core/config.scala | 4 ++++ src/main/scala/lc/core/models.scala | 4 +++- src/main/scala/lc/database/DB.scala | 2 +- src/main/scala/lc/database/statements.scala | 11 +++++++---- src/main/scala/lc/server/Server.scala | 8 ++++---- 18 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/main/java/lc/captchas/FontFunCaptcha.java b/src/main/java/lc/captchas/FontFunCaptcha.java index 8dbea52..6cf533c 100644 --- a/src/main/java/lc/captchas/FontFunCaptcha.java +++ b/src/main/java/lc/captchas/FontFunCaptcha.java @@ -81,7 +81,7 @@ public class FontFunCaptcha implements ChallengeProvider { return baos.toByteArray(); } - public Challenge returnChallenge() { + public Challenge returnChallenge(String level, String size) { String secret = HelperFunctions.randomString(7); String path = "./lib/fonts/"; return new Challenge(fontFun(secret, "medium", path), "image/png", secret.toLowerCase()); diff --git a/src/main/java/lc/captchas/PoppingCharactersCaptcha.java b/src/main/java/lc/captchas/PoppingCharactersCaptcha.java index 0ffa4df..5027891 100644 --- a/src/main/java/lc/captchas/PoppingCharactersCaptcha.java +++ b/src/main/java/lc/captchas/PoppingCharactersCaptcha.java @@ -100,7 +100,7 @@ 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()); } diff --git a/src/main/java/lc/captchas/ShadowTextCaptcha.java b/src/main/java/lc/captchas/ShadowTextCaptcha.java index a797c50..7e9b71b 100644 --- a/src/main/java/lc/captchas/ShadowTextCaptcha.java +++ b/src/main/java/lc/captchas/ShadowTextCaptcha.java @@ -74,7 +74,7 @@ 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()); } 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/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 431c93e..98b05b9 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..4f2bbb0 100644 --- a/src/main/scala/lc/captchas/DebugCaptcha.scala +++ b/src/main/scala/lc/captchas/DebugCaptcha.scala @@ -66,7 +66,7 @@ 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()) } diff --git a/src/main/scala/lc/captchas/FilterChallenge.scala b/src/main/scala/lc/captchas/FilterChallenge.scala index 5f83ff6..ef3bbc2 100644 --- a/src/main/scala/lc/captchas/FilterChallenge.scala +++ b/src/main/scala/lc/captchas/FilterChallenge.scala @@ -29,7 +29,7 @@ class FilterChallenge extends ChallengeProvider { ) } - def returnChallenge(): Challenge = { + def returnChallenge(level: String, size: String): Challenge = { val filterTypes = List(new FilterType1, new FilterType2) val r = new scala.util.Random val alphabet = "abcdefghijklmnopqrstuvwxyz" 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..02dfd88 100644 --- a/src/main/scala/lc/captchas/RainDropsCaptcha.scala +++ b/src/main/scala/lc/captchas/RainDropsCaptcha.scala @@ -56,7 +56,7 @@ 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 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 96620bf..2df9c42 100644 --- a/src/main/scala/lc/core/captchaManager.scala +++ b/src/main/scala/lc/core/captchaManager.scala @@ -36,11 +36,11 @@ 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 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) + // println("Added new challenge: " + token.toString) + token.map(_.toInt) } } @@ -58,7 +58,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 +107,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 +125,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 ee6532f..3acc3ce 100644 --- a/src/main/scala/lc/database/DB.scala +++ b/src/main/scala/lc/database/DB.scala @@ -3,7 +3,7 @@ package lc.database 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", "") + val con: Connection = 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 da8f3cf..12cc416 100644 --- a/src/main/scala/lc/server/Server.scala +++ b/src/main/scala/lc/server/Server.scala @@ -23,7 +23,7 @@ class Server(address: String, port: Int, captchaManager: CaptchaManager, playgro .address(new InetSocketAddress(address, port)) .backlog(32) .POST( - "/v1/captcha", + "/v2/captcha", (request) => { val json = parse(request.getBodyString()) val param = json.extract[Parameters] @@ -32,7 +32,7 @@ class Server(address: String, port: Int, captchaManager: CaptchaManager, playgro } ) .GET( - "/v1/media", + "/v2/media", (request) => { val params = request.getQueryParams() val result = if (params.containsKey("id")) { @@ -46,7 +46,7 @@ class Server(address: String, port: Int, captchaManager: CaptchaManager, playgro } ) .POST( - "/v1/answer", + "/v2/answer", (request) => { val json = parse(request.getBodyString()) val answer = json.extract[Answer] @@ -70,7 +70,7 @@ class Server(address: String, port: Int, captchaManager: CaptchaManager, playgro

Welcome to LibreCaptcha server

Link to Demo

-

API is served at /v1/

+

API is served at /v2/

""" new StringResponse(200, str) From 4f3bec0bc69337943e317af61a03ca6293355e38 Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 1 Mar 2022 23:03:43 +0530 Subject: [PATCH 02/21] each challenge provider now respects the size parameter when construction image --- src/main/java/lc/captchas/FontFunCaptcha.java | 9 ++++++--- .../lc/captchas/PoppingCharactersCaptcha.java | 17 +++++++++-------- .../java/lc/captchas/ShadowTextCaptcha.java | 9 ++++++--- src/main/java/lc/misc/HelperFunctions.java | 6 ++++++ src/main/scala/lc/captchas/DebugCaptcha.scala | 11 +++++++---- .../scala/lc/captchas/FilterChallenge.scala | 6 +++++- .../scala/lc/captchas/RainDropsCaptcha.scala | 6 ++++-- 7 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/main/java/lc/captchas/FontFunCaptcha.java b/src/main/java/lc/captchas/FontFunCaptcha.java index 6cf533c..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); @@ -83,8 +83,11 @@ public class FontFunCaptcha implements ChallengeProvider { 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 5027891..d5c4cdd 100644 --- a/src/main/java/lc/captchas/PoppingCharactersCaptcha.java +++ b/src/main/java/lc/captchas/PoppingCharactersCaptcha.java @@ -21,10 +21,8 @@ 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 Integer[] computeOffsets(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(); @@ -41,7 +39,7 @@ public class PoppingCharactersCaptcha implements ChallengeProvider { return advances.toArray(new Integer[]{}); } - private BufferedImage makeImage(final Consumer f) { + private BufferedImage makeImage(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,16 +54,16 @@ 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 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(width, height, text); 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(width, height, (g) -> { if (i > 0) { final var prevI = (i - 1) % text.length(); g.setColor(prevColor); @@ -102,7 +100,10 @@ public class PoppingCharactersCaptcha implements ChallengeProvider { 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 7e9b71b..9ac92a9 100644 --- a/src/main/java/lc/captchas/ShadowTextCaptcha.java +++ b/src/main/java/lc/captchas/ShadowTextCaptcha.java @@ -38,8 +38,8 @@ 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); + private byte[] shadowText(final int width, final int height, String text) { + BufferedImage img = new BufferedImage(width, height, 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); @@ -76,6 +76,9 @@ public class ShadowTextCaptcha implements ChallengeProvider { 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/misc/HelperFunctions.java b/src/main/java/lc/misc/HelperFunctions.java index 22b3a8a..9b196aa 100644 --- a/src/main/java/lc/misc/HelperFunctions.java +++ b/src/main/java/lc/misc/HelperFunctions.java @@ -11,6 +11,12 @@ 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_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); diff --git a/src/main/scala/lc/captchas/DebugCaptcha.scala b/src/main/scala/lc/captchas/DebugCaptcha.scala index 4f2bbb0..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() @@ -68,6 +68,9 @@ class DebugCaptcha extends ChallengeProvider { 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 ef3bbc2..dbf0771 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" @@ -35,7 +36,10 @@ class FilterChallenge extends ChallengeProvider { 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 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() g.setColor(Color.WHITE) g.fillRect(0, 0, canvas.getWidth, canvas.getHeight) diff --git a/src/main/scala/lc/captchas/RainDropsCaptcha.scala b/src/main/scala/lc/captchas/RainDropsCaptcha.scala index 02dfd88..9b32d76 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 @@ -59,8 +60,9 @@ class RainDropsCP extends ChallengeProvider { 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 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 From d7c1f9a4ccfed0f1dbf664df235631b02463cd27 Mon Sep 17 00:00:00 2001 From: hrj Date: Sat, 2 Apr 2022 22:51:19 +0530 Subject: [PATCH 03/21] handle exceptions thrown by captcha provider --- src/main/scala/lc/core/captchaManager.scala | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/scala/lc/core/captchaManager.scala b/src/main/scala/lc/core/captchaManager.scala index 2df9c42..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(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) + 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 } } From cc8addb0c72931cfb3a062b8ae79bb5eff225ff7 Mon Sep 17 00:00:00 2001 From: hrj Date: Sat, 2 Apr 2022 22:51:45 +0530 Subject: [PATCH 04/21] filter challenge: scale font height based on image size --- src/main/scala/lc/captchas/FilterChallenge.scala | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/scala/lc/captchas/FilterChallenge.scala b/src/main/scala/lc/captchas/FilterChallenge.scala index dbf0771..f846dbb 100644 --- a/src/main/scala/lc/captchas/FilterChallenge.scala +++ b/src/main/scala/lc/captchas/FilterChallenge.scala @@ -41,23 +41,21 @@ class FilterChallenge extends ChallengeProvider { 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) + g.setFont(new Font("Serif", Font.PLAIN, fontHeight)) + val stringWidth = g.getFontMetrics().stringWidth(secret) + val xOffset = ((width - stringWidth)*r.nextDouble).toInt + g.drawString(secret, xOffset, fontHeight) g.dispose() var image = ImmutableImage.fromAwt(canvas) - val s = scala.util.Random.nextInt(2) + val s = r.nextInt(2) image = filterTypes(s).applyFilter(image) 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 = { From 444982e65f7ab4d6ffce722bdc3bbc7e44bc9dab Mon Sep 17 00:00:00 2001 From: hrj Date: Sat, 2 Apr 2022 23:06:57 +0530 Subject: [PATCH 05/21] added HelperFunctions.safeAlphaNum --- src/main/java/lc/misc/HelperFunctions.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/lc/misc/HelperFunctions.java b/src/main/java/lc/misc/HelperFunctions.java index 9b196aa..755f390 100644 --- a/src/main/java/lc/misc/HelperFunctions.java +++ b/src/main/java/lc/misc/HelperFunctions.java @@ -29,7 +29,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); From d797acdcac2dd2befa47a8ac2e0024e996959dd9 Mon Sep 17 00:00:00 2001 From: hrj Date: Sat, 2 Apr 2022 23:07:38 +0530 Subject: [PATCH 06/21] FilterChallenge: adjust difficult based on parameter --- src/main/scala/lc/captchas/FilterChallenge.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/scala/lc/captchas/FilterChallenge.scala b/src/main/scala/lc/captchas/FilterChallenge.scala index f846dbb..8178433 100644 --- a/src/main/scala/lc/captchas/FilterChallenge.scala +++ b/src/main/scala/lc/captchas/FilterChallenge.scala @@ -31,11 +31,12 @@ class FilterChallenge extends ChallengeProvider { } def returnChallenge(level: String, size: String): Challenge = { + val mediumLevel = level == "medium" val filterTypes = List(new FilterType1, new FilterType2) 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 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) From b33dd8adcf0e990d951bad01334696c19121138c Mon Sep 17 00:00:00 2001 From: hrj Date: Sat, 2 Apr 2022 23:18:46 +0530 Subject: [PATCH 07/21] filter challenge: adjust filter effect based on difficult level --- .../scala/lc/captchas/FilterChallenge.scala | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/scala/lc/captchas/FilterChallenge.scala b/src/main/scala/lc/captchas/FilterChallenge.scala index 8178433..2a704b5 100644 --- a/src/main/scala/lc/captchas/FilterChallenge.scala +++ b/src/main/scala/lc/captchas/FilterChallenge.scala @@ -30,9 +30,10 @@ class FilterChallenge extends ChallengeProvider { ) } + private val filterTypes = List(new FilterType1, new FilterType2) + def returnChallenge(level: String, size: String): Challenge = { val mediumLevel = level == "medium" - val filterTypes = List(new FilterType1, new FilterType2) val r = new scala.util.Random val characters = if (mediumLevel) HelperFunctions.safeAlphaNum else HelperFunctions.safeCharacters val n = if (mediumLevel) 5 else 7 @@ -53,7 +54,7 @@ class FilterChallenge extends ChallengeProvider { g.dispose() var image = ImmutableImage.fromAwt(canvas) val s = r.nextInt(2) - image = filterTypes(s).applyFilter(image) + image = filterTypes(s).applyFilter(image, !mediumLevel) val img = image.awt() val baos = new ByteArrayOutputStream() PngImageWriter.write(baos, img); @@ -65,14 +66,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) @@ -81,9 +83,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) From 0c48f8fbd1e824a774e30177f7ac60329d857e2a Mon Sep 17 00:00:00 2001 From: hrj Date: Sun, 3 Apr 2022 16:45:33 +0530 Subject: [PATCH 08/21] filter challenge: ensure that text fits within image width --- src/main/scala/lc/captchas/FilterChallenge.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/scala/lc/captchas/FilterChallenge.scala b/src/main/scala/lc/captchas/FilterChallenge.scala index 2a704b5..bfda62a 100644 --- a/src/main/scala/lc/captchas/FilterChallenge.scala +++ b/src/main/scala/lc/captchas/FilterChallenge.scala @@ -47,9 +47,13 @@ class FilterChallenge extends ChallengeProvider { g.setColor(Color.WHITE) g.fillRect(0, 0, canvas.getWidth, canvas.getHeight) g.setColor(Color.BLACK) - g.setFont(new Font("Serif", Font.PLAIN, fontHeight)) + val font = new Font("Serif", Font.PLAIN, fontHeight) + g.setFont(font) val stringWidth = g.getFontMetrics().stringWidth(secret) - val xOffset = ((width - stringWidth)*r.nextDouble).toInt + 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) From bfc7174e2a701ff269fafba8503bb8d93ae3749f Mon Sep 17 00:00:00 2001 From: hrj Date: Sun, 3 Apr 2022 16:45:53 +0530 Subject: [PATCH 09/21] FilterChallenge: use bold font --- src/main/scala/lc/captchas/FilterChallenge.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/lc/captchas/FilterChallenge.scala b/src/main/scala/lc/captchas/FilterChallenge.scala index bfda62a..2944cb1 100644 --- a/src/main/scala/lc/captchas/FilterChallenge.scala +++ b/src/main/scala/lc/captchas/FilterChallenge.scala @@ -47,7 +47,7 @@ class FilterChallenge extends ChallengeProvider { g.setColor(Color.WHITE) g.fillRect(0, 0, canvas.getWidth, canvas.getHeight) g.setColor(Color.BLACK) - val font = new Font("Serif", Font.PLAIN, fontHeight) + 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 From 7f77f819ddcacf8bfa851a491d5215c8c7580369 Mon Sep 17 00:00:00 2001 From: hrj Date: Sun, 3 Apr 2022 22:49:35 +0530 Subject: [PATCH 10/21] popping characters: adjust height to size --- .../lc/captchas/PoppingCharactersCaptcha.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/lc/captchas/PoppingCharactersCaptcha.java b/src/main/java/lc/captchas/PoppingCharactersCaptcha.java index d5c4cdd..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,26 +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 Integer[] computeOffsets(final int width, final int height, 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 int width, final int height, 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,21 +55,26 @@ public class PoppingCharactersCaptcha implements ChallengeProvider { 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(width, height, 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(width, height, (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); From 23a6a43d2d70930552f3e01537b1a0bcdd3e8fb1 Mon Sep 17 00:00:00 2001 From: hrj Date: Sun, 3 Apr 2022 22:58:12 +0530 Subject: [PATCH 11/21] shadow text: simplify text drawing --- src/main/java/lc/captchas/ShadowTextCaptcha.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/lc/captchas/ShadowTextCaptcha.java b/src/main/java/lc/captchas/ShadowTextCaptcha.java index 9ac92a9..7616a72 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; @@ -46,12 +45,12 @@ public class ShadowTextCaptcha implements ChallengeProvider { graphics2D.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - TextLayout 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.setFont(font); + graphics2D.drawString(text, 15, 50); graphics2D.dispose(); float[] kernel = { 1f / 9f, 1f / 9f, 1f / 9f, @@ -63,7 +62,8 @@ public class ShadowTextCaptcha implements ChallengeProvider { Graphics2D g2d = img2.createGraphics(); HelperFunctions.setRenderingHints(g2d); g2d.setPaint(Color.WHITE); - textLayout.draw(g2d, 13, 50); + g2d.setFont(font); + g2d.drawString(text, 13, 50); g2d.dispose(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { From 6480da09ffd76102716269b913105ee0210e5dbe Mon Sep 17 00:00:00 2001 From: hrj Date: Sun, 3 Apr 2022 23:01:26 +0530 Subject: [PATCH 12/21] helper setRenderingHints: set antialiasing on --- src/main/java/lc/misc/HelperFunctions.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/lc/misc/HelperFunctions.java b/src/main/java/lc/misc/HelperFunctions.java index 755f390..5fc7912 100644 --- a/src/main/java/lc/misc/HelperFunctions.java +++ b/src/main/java/lc/misc/HelperFunctions.java @@ -18,6 +18,7 @@ public class HelperFunctions { } 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( From 832053f6e9f5912d089c91b247a19f416710111f Mon Sep 17 00:00:00 2001 From: hrj Date: Sun, 3 Apr 2022 23:01:40 +0530 Subject: [PATCH 13/21] minor, simplification --- src/main/java/lc/captchas/ShadowTextCaptcha.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/lc/captchas/ShadowTextCaptcha.java b/src/main/java/lc/captchas/ShadowTextCaptcha.java index 7616a72..ebc35bf 100644 --- a/src/main/java/lc/captchas/ShadowTextCaptcha.java +++ b/src/main/java/lc/captchas/ShadowTextCaptcha.java @@ -37,14 +37,16 @@ public class ShadowTextCaptcha implements ChallengeProvider { return answer.toLowerCase().equals(secret); } + private float[] kernel = { + 1f / 9f, 1f / 9f, 1f / 9f, + 1f / 9f, 1f / 9f, 1f / 9f, + 1f / 9f, 1f / 9f, 1f / 9f + }; + private byte[] shadowText(final int width, final int height, String text) { BufferedImage img = new BufferedImage(width, height, 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); - HelperFunctions.setRenderingHints(graphics2D); graphics2D.setPaint(Color.WHITE); graphics2D.fillRect(0, 0, width, height); @@ -52,11 +54,6 @@ public class ShadowTextCaptcha implements ChallengeProvider { graphics2D.setFont(font); graphics2D.drawString(text, 15, 50); 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); BufferedImage img2 = op.filter(img, null); Graphics2D g2d = img2.createGraphics(); From 83b0eb069e35b5dc9af5c4af16c44415d1255c2c Mon Sep 17 00:00:00 2001 From: hrj Date: Sun, 3 Apr 2022 23:09:45 +0530 Subject: [PATCH 14/21] shadow text: dynamic kernel size --- src/main/java/lc/captchas/ShadowTextCaptcha.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/lc/captchas/ShadowTextCaptcha.java b/src/main/java/lc/captchas/ShadowTextCaptcha.java index ebc35bf..16bbdc1 100644 --- a/src/main/java/lc/captchas/ShadowTextCaptcha.java +++ b/src/main/java/lc/captchas/ShadowTextCaptcha.java @@ -37,10 +37,12 @@ public class ShadowTextCaptcha implements ChallengeProvider { return answer.toLowerCase().equals(secret); } - private float[] kernel = { - 1f / 9f, 1f / 9f, 1f / 9f, - 1f / 9f, 1f / 9f, 1f / 9f, - 1f / 9f, 1f / 9f, 1f / 9f + 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; }; private byte[] shadowText(final int width, final int height, String text) { @@ -54,7 +56,8 @@ public class ShadowTextCaptcha implements ChallengeProvider { graphics2D.setFont(font); graphics2D.drawString(text, 15, 50); graphics2D.dispose(); - 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); From f8626e3670734baf06c3274b6cd5d47ac3c5e147 Mon Sep 17 00:00:00 2001 From: hrj Date: Sun, 3 Apr 2022 23:22:45 +0530 Subject: [PATCH 15/21] shadow text: adapt text to size dynamically --- src/main/java/lc/captchas/ShadowTextCaptcha.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/lc/captchas/ShadowTextCaptcha.java b/src/main/java/lc/captchas/ShadowTextCaptcha.java index 16bbdc1..cdc314f 100644 --- a/src/main/java/lc/captchas/ShadowTextCaptcha.java +++ b/src/main/java/lc/captchas/ShadowTextCaptcha.java @@ -47,14 +47,18 @@ public class ShadowTextCaptcha implements ChallengeProvider { private byte[] shadowText(final int width, final int height, String text) { BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - Font font = new Font("Arial", Font.ROMAN_BASELINE, 48); + 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, width, height); graphics2D.setPaint(Color.BLACK); graphics2D.setFont(font); - graphics2D.drawString(text, 15, 50); + 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(); 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); @@ -62,8 +66,9 @@ public class ShadowTextCaptcha implements ChallengeProvider { Graphics2D g2d = img2.createGraphics(); HelperFunctions.setRenderingHints(g2d); g2d.setPaint(Color.WHITE); + g2d.scale(scaleX, 1d); g2d.setFont(font); - g2d.drawString(text, 13, 50); + g2d.drawString(text, -kernelSize, fontHeight*1.1f); g2d.dispose(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { From 3aeb2588518a3be103bb5944ec0a4c4129c3bcc0 Mon Sep 17 00:00:00 2001 From: hrj Date: Mon, 4 Apr 2022 11:07:50 +0530 Subject: [PATCH 16/21] rain drops: scale text to fit image size --- .../scala/lc/captchas/RainDropsCaptcha.scala | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/scala/lc/captchas/RainDropsCaptcha.scala b/src/main/scala/lc/captchas/RainDropsCaptcha.scala index 9b32d76..0c02066 100644 --- a/src/main/scala/lc/captchas/RainDropsCaptcha.scala +++ b/src/main/scala/lc/captchas/RainDropsCaptcha.scala @@ -82,7 +82,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) @@ -119,17 +120,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) From caf03d7a485de27298ccf3a06769f0de0e099578 Mon Sep 17 00:00:00 2001 From: hrj Date: Mon, 4 Apr 2022 11:13:25 +0530 Subject: [PATCH 17/21] raindrops: vary length based on level, and use safe alphaNumeric characters --- src/main/scala/lc/captchas/RainDropsCaptcha.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/scala/lc/captchas/RainDropsCaptcha.scala b/src/main/scala/lc/captchas/RainDropsCaptcha.scala index 0c02066..40cd217 100644 --- a/src/main/scala/lc/captchas/RainDropsCaptcha.scala +++ b/src/main/scala/lc/captchas/RainDropsCaptcha.scala @@ -25,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) @@ -59,7 +57,8 @@ class RainDropsCP extends ChallengeProvider { 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 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) From ba796fddf7fb187e0ece00ea42621560e06892b5 Mon Sep 17 00:00:00 2001 From: hrj Date: Mon, 4 Apr 2022 19:48:28 +0530 Subject: [PATCH 18/21] locust tests: use new api path --- tests/locustfile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/locustfile.py b/tests/locustfile.py index 941c45e..7a7b40d 100644 --- a/tests/locustfile.py +++ b/tests/locustfile.py @@ -26,7 +26,7 @@ class QuickStartUser(SequentialTaskSet): # TODO: Iterate over parameters for a more comprehensive test captcha_params = {"level":"easy","media":"image/png","input_type":"text"} - 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) From cbb8abd3524d3e7f446e35e9ce5c0184ba55397c Mon Sep 17 00:00:00 2001 From: hrj Date: Mon, 4 Apr 2022 20:27:35 +0530 Subject: [PATCH 19/21] functional tests: use new api path --- tests/locustfile-functional.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/locustfile-functional.py b/tests/locustfile-functional.py index 5034e43..9224494 100644 --- a/tests/locustfile-functional.py +++ b/tests/locustfile-functional.py @@ -24,7 +24,7 @@ class QuickStartUser(SequentialTaskSet): def captcha(self): captcha_params = {"level":"debug","media":"image/png","input_type":"text"} - 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: From e26bd32b2f60ba3571121e3aa6673b4f62d79b09 Mon Sep 17 00:00:00 2001 From: hrj Date: Mon, 4 Apr 2022 20:28:13 +0530 Subject: [PATCH 20/21] set size in test config --- tests/debug-config.json | 1 + 1 file changed, 1 insertion(+) 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" : { } }] } From 7ef308e556d2455bf145a577aec58e271fe12cd8 Mon Sep 17 00:00:00 2001 From: hrj Date: Mon, 4 Apr 2022 20:30:28 +0530 Subject: [PATCH 21/21] tests: specify size parameter --- tests/locustfile-functional.py | 2 +- tests/locustfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/locustfile-functional.py b/tests/locustfile-functional.py index 9224494..57e4840 100644 --- a/tests/locustfile-functional.py +++ b/tests/locustfile-functional.py @@ -22,7 +22,7 @@ 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="/v2/captcha", json=captcha_params, name="/captcha", catch_response = True) as resp: if resp.status_code != 200: diff --git a/tests/locustfile.py b/tests/locustfile.py index 7a7b40d..07cf572 100644 --- a/tests/locustfile.py +++ b/tests/locustfile.py @@ -24,7 +24,7 @@ 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="/v2/captcha", json=captcha_params, name="/captcha") if resp.status_code != 200: