From dd1129b484fd8c7bc6f2fb978c9a0747aec9f468 Mon Sep 17 00:00:00 2001 From: hrj Date: Mon, 12 Apr 2021 19:01:57 +0530 Subject: [PATCH 01/22] added a debug captcha Signed-off-by: hrj --- src/main/scala/lc/captchas/DebugCaptcha.scala | 64 +++++++++++++++++++ src/main/scala/lc/core/captchaProviders.scala | 3 +- 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/main/scala/lc/captchas/DebugCaptcha.scala diff --git a/src/main/scala/lc/captchas/DebugCaptcha.scala b/src/main/scala/lc/captchas/DebugCaptcha.scala new file mode 100644 index 0000000..d0df653 --- /dev/null +++ b/src/main/scala/lc/captchas/DebugCaptcha.scala @@ -0,0 +1,64 @@ +package lc.captchas + +import javax.imageio.ImageIO +import java.awt.Color +import java.awt.Font +import java.awt.font.TextLayout +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.util.Map +import java.util.List + +import lc.misc.HelperFunctions +import lc.captchas.interfaces.Challenge +import lc.captchas.interfaces.ChallengeProvider + +/** This captcha is only for debuggin purposes. It creates very simple captchas that are delibertely easy to solve with OCR engines */ +class DebugCaptcha extends ChallengeProvider { + + def getId(): String = { + "DebugCaptcha" + } + + def configure(config: String): Unit = { + // TODO: Add custom config + } + + def supportedParameters(): Map[String, List[String]] = { + Map.of( + "supportedLevels", List.of("debug"), + "supportedMedia", List.of("image/png"), + "supportedInputType", List.of("text") + ) + } + + def checkAnswer(secret: String, answer: String): Boolean = { + answer.toLowerCase().equals(secret) + } + + private def simpleText(text: String): Array[Byte] = { + val img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB) + val font = new Font("Arial", Font.ROMAN_BASELINE, 48) + 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.setPaint(Color.BLACK) + textLayout.draw(graphics2D, 15, 50) + graphics2D.dispose() + val baos = new ByteArrayOutputStream() + try { + ImageIO.write(img, "png", baos) + } catch { + case e: Exception => + e.printStackTrace() + } + baos.toByteArray() + } + + def returnChallenge(): Challenge = { + val secret = HelperFunctions.randomString(6) + new Challenge(simpleText(secret), "image/png", secret.toLowerCase()) + } +} diff --git a/src/main/scala/lc/core/captchaProviders.scala b/src/main/scala/lc/core/captchaProviders.scala index d23f888..b36fed5 100644 --- a/src/main/scala/lc/core/captchaProviders.scala +++ b/src/main/scala/lc/core/captchaProviders.scala @@ -11,7 +11,8 @@ object CaptchaProviders { //"FontFunCaptcha" -> new FontFunCaptcha, "GifCaptcha" -> new GifCaptcha, "ShadowTextCaptcha" -> new ShadowTextCaptcha, - "RainDropsCaptcha" -> new RainDropsCP + "RainDropsCaptcha" -> new RainDropsCP, + "DebugCaptcha" -> new DebugCaptcha, //"LabelCaptcha" -> new LabelCaptcha ) From d9fefca8411a8531ffaa3ca3c1fe67a4863199e6 Mon Sep 17 00:00:00 2001 From: hrj Date: Mon, 12 Apr 2021 19:04:19 +0530 Subject: [PATCH 02/22] minor, spacing Signed-off-by: hrj --- src/main/scala/lc/core/captchaProviders.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/lc/core/captchaProviders.scala b/src/main/scala/lc/core/captchaProviders.scala index b36fed5..1174ade 100644 --- a/src/main/scala/lc/core/captchaProviders.scala +++ b/src/main/scala/lc/core/captchaProviders.scala @@ -57,7 +57,7 @@ object CaptchaProviders { def getProvider(param: Parameters): ChallengeProvider = { val providerConfig = filterProviderByParam(param).toList - if (providerConfig.length == 0) throw new NoSuchElementException(ErrorMessageEnum.NO_CAPTCHA.toString) + if (providerConfig.length == 0) throw new NoSuchElementException(ErrorMessageEnum.NO_CAPTCHA.toString) val randomIndex = getNextRandomInt(providerConfig.length) val providerIndex = providerConfig(randomIndex)._1 val selectedProvider = providers(providerIndex) From 3cfba7a08e1adc01710bb46c8f9234ebc67701b6 Mon Sep 17 00:00:00 2001 From: hrj Date: Mon, 12 Apr 2021 20:25:31 +0530 Subject: [PATCH 03/22] run functional test Signed-off-by: hrj --- tests/locustfile-functional.py | 68 ++++++++++++++++++++++++++++++++++ tests/run.sh | 10 ++++- 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/locustfile-functional.py diff --git a/tests/locustfile-functional.py b/tests/locustfile-functional.py new file mode 100644 index 0000000..4effb4f --- /dev/null +++ b/tests/locustfile-functional.py @@ -0,0 +1,68 @@ +from locust import task, between, SequentialTaskSet +from locust.contrib.fasthttp import FastHttpUser +from locust import events +import json +import logging +import subprocess + +@events.quitting.add_listener +def _(environment, **kw): + if environment.stats.total.fail_ratio > 0.02: + logging.error("Test failed due to failure ratio > 2%") + environment.process_exit_code = 1 + elif environment.stats.total.avg_response_time > 300: + logging.error("Test failed due to average response time ratio > 300 ms") + environment.process_exit_code = 1 + elif environment.stats.total.get_response_time_percentile(0.95) > 800: + logging.error("Test failed due to 95th percentile response time > 800 ms") + environment.process_exit_code = 1 + else: + environment.process_exit_code = 0 + +class QuickStartUser(SequentialTaskSet): + wait_time = between(0.1,0.2) + + @task + def captcha(self): + # TODO: Iterate over parameters for a more comprehensive test + captcha_params = {"level":"debug","media":"image/png","input_type":"text"} + + resp = self.client.post(path="/v1/captcha", json=captcha_params, name="/captcha") + if resp.status_code != 200: + print("\nError on /captcha endpoint: ") + print(resp) + print(resp.text) + print("----------------END.CAPTCHA-------------------\n\n") + + uuid = resp.json().get("id") + + resp = self.client.get(path="/v1/media?id=%s" % uuid, name="/media", stream=True) + if resp.status_code != 200: + print("\nError on /media endpoint: ") + print(resp) + print(resp.text) + print("----------------END.MEDIA-------------------\n\n") + + media = resp.content + mediaFileName = "tests/test-%s.png" % uuid + with open(mediaFileName, "wb") as f: + f.write(media) + ocrResult = subprocess.Popen("gocr %s" % mediaFileName, shell=True, stdout=subprocess.PIPE) + ocrAnswer = ocrResult.stdout.readlines()[0].strip().decode() + + answerBody = {"answer": ocrAnswer,"id": uuid} + with self.client.post(path='/v1/answer', json=answerBody, name="/answer", catch_response=True) as resp: + if resp.status_code != 200: + print("\nError on /answer endpoint: ") + print(resp) + print(resp.text) + print("----------------END.ANSWER-------------------\n\n") + else: + if resp.json().get("result") != "True": + resp.failure("Answer was not accepted") + + +class User(FastHttpUser): + wait_time = between(0.1,0.2) + tasks = [QuickStartUser] + host = "http://localhost:8888" diff --git a/tests/run.sh b/tests/run.sh index 0d10e93..b00e7fa 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -10,6 +10,14 @@ sleep 4 locust --headless -u 300 -r 100 --run-time 4m --stop-timeout 30 -f tests/locustfile.py status=$? -kill $JAVA_PID +if [ $status != 0 ]; then + kill $JAVA_PID + exit $status +fi +echo Run functional test +locust --headless -u 1 -r 1 --run-time 1m --stop-timeout 30 -f tests/locustfile-functional.py +status=$? + +kill $JAVA_PID exit $status From a51defd2c7a6142f93ee34508b51603628d60a16 Mon Sep 17 00:00:00 2001 From: hrj Date: Mon, 12 Apr 2021 20:28:07 +0530 Subject: [PATCH 04/22] install gocr during CI Signed-off-by: hrj --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b48fcb0..edceef0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,4 +21,4 @@ jobs: - name: Run linter run: sbt "scalafixAll --check" - name: Run locust tests - run: ./tests/run.sh + run: apt-get update && apt-get -y upgrade && apt-get install -y gocr && ./tests/run.sh From 68dcfb1e49e2a4da6d1ca464e1c4085bd5e2016c Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 07:55:45 +0530 Subject: [PATCH 05/22] CI: fix permission error Signed-off-by: hrj --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edceef0..e3f7aa3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,4 +21,4 @@ jobs: - name: Run linter run: sbt "scalafixAll --check" - name: Run locust tests - run: apt-get update && apt-get -y upgrade && apt-get install -y gocr && ./tests/run.sh + run: sudo apt-get install -y gocr && ./tests/run.sh From c0ac570746a800a2d459ea0038a7d39380be0374 Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 09:02:11 +0530 Subject: [PATCH 06/22] simplify locust file Signed-off-by: hrj --- tests/locustfile-functional.py | 48 ++++++++++++++++------------------ 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/tests/locustfile-functional.py b/tests/locustfile-functional.py index 4effb4f..7ba9639 100644 --- a/tests/locustfile-functional.py +++ b/tests/locustfile-functional.py @@ -24,43 +24,41 @@ class QuickStartUser(SequentialTaskSet): @task def captcha(self): - # TODO: Iterate over parameters for a more comprehensive test captcha_params = {"level":"debug","media":"image/png","input_type":"text"} - resp = self.client.post(path="/v1/captcha", json=captcha_params, name="/captcha") - if resp.status_code != 200: - print("\nError on /captcha endpoint: ") - print(resp) - print(resp.text) - print("----------------END.CAPTCHA-------------------\n\n") - - uuid = resp.json().get("id") + with self.client.post(path="/v1/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() + uuid = captchaJson.get("id") + if not uuid: + resp.failure("uuid not returned on /captcha endpoint: " + resp.text) - resp = self.client.get(path="/v1/media?id=%s" % uuid, name="/media", stream=True) - if resp.status_code != 200: - print("\nError on /media endpoint: ") - print(resp) - print(resp.text) - print("----------------END.MEDIA-------------------\n\n") + with self.client.get(path="/v1/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) - media = resp.content - mediaFileName = "tests/test-%s.png" % uuid - with open(mediaFileName, "wb") as f: - f.write(media) - ocrResult = subprocess.Popen("gocr %s" % mediaFileName, shell=True, stdout=subprocess.PIPE) - ocrAnswer = ocrResult.stdout.readlines()[0].strip().decode() + media = resp.content + + 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: if resp.status_code != 200: - print("\nError on /answer endpoint: ") - print(resp) - print(resp.text) - print("----------------END.ANSWER-------------------\n\n") + resp.failure("Status was not 200: " + resp.text) else: if resp.json().get("result") != "True": resp.failure("Answer was not accepted") + def solve(self, uuid, media): + mediaFileName = "tests/test-%s.png" % uuid + with open(mediaFileName, "wb") as f: + f.write(media) + ocrResult = subprocess.Popen("gocr %s" % mediaFileName, shell=True, stdout=subprocess.PIPE) + ocrAnswer = ocrResult.stdout.readlines()[0].strip().decode() + return ocrAnswer + + class User(FastHttpUser): wait_time = between(0.1,0.2) From 3845645f9a4b3e2c286054cc5b0e917f11645223 Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 09:05:44 +0530 Subject: [PATCH 07/22] use debug config Signed-off-by: hrj --- tests/debug-config.json | 14 ++++++++++++++ tests/run.sh | 12 +++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 tests/debug-config.json diff --git a/tests/debug-config.json b/tests/debug-config.json new file mode 100644 index 0000000..ae04974 --- /dev/null +++ b/tests/debug-config.json @@ -0,0 +1,14 @@ +{ + "randomSeed" : 20, + "port" : 8888, + "captchaExpiryTimeLimit" : 5, + "throttle" : 10, + "threadDelay" : 2, + "captchas" : [ { + "name" : "DebugCaptcha", + "allowedLevels" : [ "debug" ], + "allowedMedia" : [ "image/png" ], + "allowedInputType" : [ "text" ], + "config" : { } + }] +} diff --git a/tests/run.sh b/tests/run.sh index b00e7fa..9a07c97 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -11,13 +11,23 @@ locust --headless -u 300 -r 100 --run-time 4m --stop-timeout 30 -f tests/locustf status=$? if [ $status != 0 ]; then - kill $JAVA_PID exit $status fi +kill $JAVA_PID +sleep 4 + echo Run functional test +cp data/config.json data/config.json.bak +cp tests/debug-config.json data/config.json + +java -jar target/scala-2.13/LibreCaptcha.jar & +JAVA_PID=$! +sleep 4 + locust --headless -u 1 -r 1 --run-time 1m --stop-timeout 30 -f tests/locustfile-functional.py status=$? +mv data/config.json.bak data/config.json kill $JAVA_PID exit $status From 328f0463790bae164943454999f458630bfce6b9 Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 09:46:51 +0530 Subject: [PATCH 08/22] make locust output less verbose Signed-off-by: hrj --- tests/run.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/run.sh b/tests/run.sh index 9a07c97..0a81ad4 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -7,7 +7,7 @@ java -jar target/scala-2.13/LibreCaptcha.jar & JAVA_PID=$! sleep 4 -locust --headless -u 300 -r 100 --run-time 4m --stop-timeout 30 -f tests/locustfile.py +locust --only-summary --headless -u 300 -r 100 --run-time 4m --stop-timeout 30 -f tests/locustfile.py status=$? if [ $status != 0 ]; then @@ -25,7 +25,7 @@ java -jar target/scala-2.13/LibreCaptcha.jar & JAVA_PID=$! sleep 4 -locust --headless -u 1 -r 1 --run-time 1m --stop-timeout 30 -f tests/locustfile-functional.py +locust --only-summary --headless -u 1 -r 1 --run-time 1m --stop-timeout 30 -f tests/locustfile-functional.py status=$? mv data/config.json.bak data/config.json From 332bb2113b33784a3ec73a2511e0678d7b293562 Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 11:48:07 +0530 Subject: [PATCH 09/22] minor, typos in comment Signed-off-by: hrj --- src/main/scala/lc/captchas/DebugCaptcha.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/lc/captchas/DebugCaptcha.scala b/src/main/scala/lc/captchas/DebugCaptcha.scala index d0df653..c581c05 100644 --- a/src/main/scala/lc/captchas/DebugCaptcha.scala +++ b/src/main/scala/lc/captchas/DebugCaptcha.scala @@ -13,7 +13,7 @@ import lc.misc.HelperFunctions import lc.captchas.interfaces.Challenge import lc.captchas.interfaces.ChallengeProvider -/** This captcha is only for debuggin purposes. It creates very simple captchas that are delibertely easy to solve with OCR engines */ +/** This captcha is only for debugging purposes. It creates very simple captchas that are deliberately easy to solve with OCR engines */ class DebugCaptcha extends ChallengeProvider { def getId(): String = { From 954399042ccff507e827b9e75a77f5694f90fb00 Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 12:36:30 +0530 Subject: [PATCH 10/22] debug catpcha: only use alphabets Signed-off-by: hrj --- src/main/java/lc/misc/HelperFunctions.java | 14 +++++++++++--- src/main/scala/lc/captchas/DebugCaptcha.scala | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/lc/misc/HelperFunctions.java b/src/main/java/lc/misc/HelperFunctions.java index f46d49e..446d903 100644 --- a/src/main/java/lc/misc/HelperFunctions.java +++ b/src/main/java/lc/misc/HelperFunctions.java @@ -11,9 +11,17 @@ public class HelperFunctions { RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); } - public static String randomString(int n) { - String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz23456789$#%@&?"; - StringBuilder stringBuilder = new StringBuilder(); + public static final String alphabets = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + public static final String numbers = "23456789"; + public static final String specialCharacters = "$#%@&?"; + public static final String allCharacters = alphabets + numbers + specialCharacters; + + public static String randomString(final int n) { + return randomString(n, allCharacters); + } + + public static String randomString(final int n, final String characters) { + final StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < n; i++) { int index = (int) (characters.length() * Math.random()); stringBuilder.append(characters.charAt(index)); diff --git a/src/main/scala/lc/captchas/DebugCaptcha.scala b/src/main/scala/lc/captchas/DebugCaptcha.scala index c581c05..fde6ab4 100644 --- a/src/main/scala/lc/captchas/DebugCaptcha.scala +++ b/src/main/scala/lc/captchas/DebugCaptcha.scala @@ -58,7 +58,7 @@ class DebugCaptcha extends ChallengeProvider { } def returnChallenge(): Challenge = { - val secret = HelperFunctions.randomString(6) + val secret = HelperFunctions.randomString(6, HelperFunctions.alphabets) new Challenge(simpleText(secret), "image/png", secret.toLowerCase()) } } From 41bdbc7fbf7a2d0fcc2de820f239c507f7f9a976 Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 12:36:39 +0530 Subject: [PATCH 11/22] debug captcha: use a larger font Signed-off-by: hrj --- src/main/scala/lc/captchas/DebugCaptcha.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/lc/captchas/DebugCaptcha.scala b/src/main/scala/lc/captchas/DebugCaptcha.scala index fde6ab4..a2c4a62 100644 --- a/src/main/scala/lc/captchas/DebugCaptcha.scala +++ b/src/main/scala/lc/captchas/DebugCaptcha.scala @@ -38,7 +38,7 @@ class DebugCaptcha extends ChallengeProvider { private def simpleText(text: String): Array[Byte] = { val img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB) - val font = new Font("Arial", Font.ROMAN_BASELINE, 48) + val font = new Font("Arial", Font.ROMAN_BASELINE, 56) val graphics2D = img.createGraphics() val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext()) HelperFunctions.setRenderingHints(graphics2D) From b765399f68e5372bba141e04bb4f968790679bbf Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 13:07:44 +0530 Subject: [PATCH 12/22] debug captcha: use safe alphabets only Signed-off-by: hrj --- src/main/java/lc/misc/HelperFunctions.java | 10 ++++++---- src/main/scala/lc/captchas/DebugCaptcha.scala | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/lc/misc/HelperFunctions.java b/src/main/java/lc/misc/HelperFunctions.java index 446d903..fa12fc8 100644 --- a/src/main/java/lc/misc/HelperFunctions.java +++ b/src/main/java/lc/misc/HelperFunctions.java @@ -11,13 +11,15 @@ public class HelperFunctions { RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); } - public static final String alphabets = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - public static final String numbers = "23456789"; + public static final String safeAlphabets = "ABCDEFGHJKMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + public static final String allAlphabets = safeAlphabets + "ILl"; + public static final String safeNumbers = "23456789"; + public static final String allNumbers = safeNumbers + "1"; public static final String specialCharacters = "$#%@&?"; - public static final String allCharacters = alphabets + numbers + specialCharacters; + public static final String safeCharacters = safeAlphabets + safeNumbers + specialCharacters; public static String randomString(final int n) { - return randomString(n, allCharacters); + return randomString(n, safeCharacters); } public static String randomString(final int n, final String characters) { diff --git a/src/main/scala/lc/captchas/DebugCaptcha.scala b/src/main/scala/lc/captchas/DebugCaptcha.scala index a2c4a62..084f1e1 100644 --- a/src/main/scala/lc/captchas/DebugCaptcha.scala +++ b/src/main/scala/lc/captchas/DebugCaptcha.scala @@ -58,7 +58,7 @@ class DebugCaptcha extends ChallengeProvider { } def returnChallenge(): Challenge = { - val secret = HelperFunctions.randomString(6, HelperFunctions.alphabets) + val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets) new Challenge(simpleText(secret), "image/png", secret.toLowerCase()) } } From 1ff4a30da74de0a396c2dfce00251c6b38b78b6f Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 13:42:34 +0530 Subject: [PATCH 13/22] use tesseract instead of gocr Signed-off-by: hrj --- .github/workflows/ci.yml | 2 +- tests/locustfile-functional.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3f7aa3..8a5d80d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,4 +21,4 @@ jobs: - name: Run linter run: sbt "scalafixAll --check" - name: Run locust tests - run: sudo apt-get install -y gocr && ./tests/run.sh + run: sudo apt-get install -y tesseract-ocr && ./tests/run.sh diff --git a/tests/locustfile-functional.py b/tests/locustfile-functional.py index 7ba9639..21cfb92 100644 --- a/tests/locustfile-functional.py +++ b/tests/locustfile-functional.py @@ -54,7 +54,8 @@ class QuickStartUser(SequentialTaskSet): mediaFileName = "tests/test-%s.png" % uuid with open(mediaFileName, "wb") as f: f.write(media) - ocrResult = subprocess.Popen("gocr %s" % mediaFileName, shell=True, stdout=subprocess.PIPE) + #ocrResult = subprocess.Popen("gocr %s" % mediaFileName, shell=True, stdout=subprocess.PIPE) + ocrResult = subprocess.Popen("tesseract %s stdout -l eng" % mediaFileName, shell=True, stdout=subprocess.PIPE) ocrAnswer = ocrResult.stdout.readlines()[0].strip().decode() return ocrAnswer From 96b5808628fa199d09e6e09a96c8b3f9c1e70641 Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 14:01:25 +0530 Subject: [PATCH 14/22] correctly parse parameter values from config Signed-off-by: hrj --- src/main/scala/lc/core/config.scala | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/src/main/scala/lc/core/config.scala b/src/main/scala/lc/core/config.scala index 09dbc20..233108f 100644 --- a/src/main/scala/lc/core/config.scala +++ b/src/main/scala/lc/core/config.scala @@ -44,25 +44,9 @@ object Config { case JField("config", JObject(config)) => ("config", JString(config.toString)) } val captchaConfig: List[CaptchaConfig] = captchaConfigTransform.extract[List[CaptchaConfig]] - val allowedLevels: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDLEVELS.toString) - val allowedMedia: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDMEDIA.toString) - val allowedInputType: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDINPUTTYPE.toString) - - private def getAllValues(config: JValue, param: String): Set[String] = { - val configValues = (config \\ param) - val result = for { - JObject(child) <- configValues - JField(param) <- child - } yield (param) - - var valueSet = Set[String]() - for (valueList <- result) { - for (value <- valueList._2.children) { - valueSet += value.values.toString - } - } - valueSet - } + val allowedLevels: Set[String] = captchaConfig.flatMap(_.allowedLevels).toSet + val allowedMedia: Set[String] = captchaConfig.flatMap(_.allowedMedia).toSet + val allowedInputType: Set[String] = captchaConfig.flatMap(_.allowedInputType).toSet private def getDefaultConfig(): String = { val defaultConfigMap = From 1d746f7655bc238c0f39c9c99ce41d4d35619a87 Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 14:26:50 +0530 Subject: [PATCH 15/22] functional test: reduce failure criteria Signed-off-by: hrj --- tests/locustfile-functional.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/locustfile-functional.py b/tests/locustfile-functional.py index 21cfb92..c68d195 100644 --- a/tests/locustfile-functional.py +++ b/tests/locustfile-functional.py @@ -7,14 +7,12 @@ import subprocess @events.quitting.add_listener def _(environment, **kw): - if environment.stats.total.fail_ratio > 0.02: - logging.error("Test failed due to failure ratio > 2%") + totalStats = environment.stats.total + if totalStats.fail_ratio > 0.20: + logging.error("Test failed due to failure ratio " + totalStats.fail_ratio + " > 20%") environment.process_exit_code = 1 - elif environment.stats.total.avg_response_time > 300: - logging.error("Test failed due to average response time ratio > 300 ms") - environment.process_exit_code = 1 - elif environment.stats.total.get_response_time_percentile(0.95) > 800: - logging.error("Test failed due to 95th percentile response time > 800 ms") + elif totalStats.get_response_time_percentile(0.80) > 800: + logging.error("Test failed due to 80th percentile response time > 800 ms") environment.process_exit_code = 1 else: environment.process_exit_code = 0 From caf3669bd907be7257e56704ee23c6f675d521df Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 14:28:14 +0530 Subject: [PATCH 16/22] functional tests: show failed answers Signed-off-by: hrj --- tests/locustfile-functional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/locustfile-functional.py b/tests/locustfile-functional.py index c68d195..5034e43 100644 --- a/tests/locustfile-functional.py +++ b/tests/locustfile-functional.py @@ -46,7 +46,7 @@ class QuickStartUser(SequentialTaskSet): resp.failure("Status was not 200: " + resp.text) else: if resp.json().get("result") != "True": - resp.failure("Answer was not accepted") + resp.failure("Answer was not accepted: " + ocrAnswer) def solve(self, uuid, media): mediaFileName = "tests/test-%s.png" % uuid From 321bc678975e41671c0fd3cb50bac164d543b8c8 Mon Sep 17 00:00:00 2001 From: hrj Date: Tue, 13 Apr 2021 17:58:21 +0530 Subject: [PATCH 17/22] update readme with more details and clarifications --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3298712..5d19266 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,9 @@ The sample CAPTCHAs are also just that, samples. They have not been tested again ## Quick start with Java 1. Download the `jar` file from the latest release -2. Type `java -jar LibreCaptch.jar` +2. Type `mkdir data/`. + (The data directory is used to store a config file that you can tweak, and for storing the Database) +2. Type `java -jar LibreCaptcha.jar` We recommend a Java 11+ runtime as that's what we compile the code with. @@ -48,6 +50,7 @@ docker run -v lcdata:/lc-core/data librecaptcha/lc-core:latest A default `config.json` is automatically created in the mounted volume. +## Quick test To test the installation, try: ``` @@ -60,7 +63,7 @@ To test the installation, try: sample.png: PNG image data, 350 x 100, 8-bit/color RGB, non-interlaced ``` -The API endpoints are described below. +The API endpoints are described at the end of this file. ## Configuration If a `config.json` file is not present in the `data/` folder, the app creates one, and this can be modified @@ -98,7 +101,12 @@ An image of a random string of alphabets is created. Then a series of image filt An image of a word is blurred before being shown to the user. ### LabelCaptcha -An image that has a pair of words is created. The answer to one of the words is known and to that of the other is unknown. The user is tested on the known word, and their answer to the unknown word is recorded. If a sufficient number of users agree on their answer to the unknown word, it is transferred to the list of known words. +This providers takes in two sets of images. One with known labels, and the other unknown. +The created image has a pair of words one from each set. +The user is tested on the known word, and their answer to the unknown word is recorded. +If a sufficient number of users agree on their answer to the unknown word, it is transferred to the list of known words. + +(There is a known issue with this provider; see issue #68 ) *** From d3a2c6fa35637d5fa1278b0473296bbb8edc5cbd Mon Sep 17 00:00:00 2001 From: hrj Date: Wed, 14 Apr 2021 09:38:02 +0530 Subject: [PATCH 18/22] Minor: use Map.of() Signed-off-by: hrj --- src/main/java/lc/captchas/FontFunCaptcha.java | 15 +++++++-------- src/main/java/lc/captchas/GifCaptcha.java | 15 +++++++-------- src/main/java/lc/captchas/ShadowTextCaptcha.java | 15 +++++++-------- src/main/scala/lc/captchas/FilterChallenge.scala | 13 +++++-------- src/main/scala/lc/captchas/LabelCaptcha.scala | 13 +++++-------- src/main/scala/lc/captchas/RainDropsCaptcha.scala | 13 +++++-------- 6 files changed, 36 insertions(+), 48 deletions(-) diff --git a/src/main/java/lc/captchas/FontFunCaptcha.java b/src/main/java/lc/captchas/FontFunCaptcha.java index bb1c278..3ad134c 100644 --- a/src/main/java/lc/captchas/FontFunCaptcha.java +++ b/src/main/java/lc/captchas/FontFunCaptcha.java @@ -6,7 +6,7 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FilenameFilter; -import java.util.HashMap; +import java.util.Map; import java.util.List; import lc.captchas.interfaces.Challenge; import lc.captchas.interfaces.ChallengeProvider; @@ -18,13 +18,12 @@ public class FontFunCaptcha implements ChallengeProvider { return "FontFunCaptcha"; } - public HashMap> supportedParameters() { - HashMap> supportedParams = new HashMap>(); - supportedParams.put("supportedLevels", List.of("medium")); - supportedParams.put("supportedMedia", List.of("image/png")); - supportedParams.put("supportedInputType", List.of("text")); - - return supportedParams; + public Map> supportedParameters() { + return Map.of( + "supportedLevels", List.of("medium"), + "supportedMedia", List.of("image/png"), + "supportedInputType", List.of("text") + ); } public void configure(String config) { diff --git a/src/main/java/lc/captchas/GifCaptcha.java b/src/main/java/lc/captchas/GifCaptcha.java index 13cf0fa..19b161a 100644 --- a/src/main/java/lc/captchas/GifCaptcha.java +++ b/src/main/java/lc/captchas/GifCaptcha.java @@ -6,7 +6,7 @@ import java.awt.RenderingHints; import java.awt.Color; import java.awt.image.BufferedImage; import java.io.IOException; -import java.util.HashMap; +import java.util.Map; import java.util.List; import javax.imageio.stream.ImageOutputStream; @@ -55,13 +55,12 @@ public class GifCaptcha implements ChallengeProvider { // TODO: Add custom config } - public HashMap> supportedParameters() { - HashMap> supportedParams = new HashMap>(); - supportedParams.put("supportedLevels", List.of("hard")); - supportedParams.put("supportedMedia", List.of("image/gif")); - supportedParams.put("supportedInputType", List.of("text")); - - return supportedParams; + public Map> supportedParameters() { + return Map.of( + "supportedLevels", List.of("hard"), + "supportedMedia", List.of("image/gif"), + "supportedInputType", List.of("text") + ); } public Challenge returnChallenge() { diff --git a/src/main/java/lc/captchas/ShadowTextCaptcha.java b/src/main/java/lc/captchas/ShadowTextCaptcha.java index 5b3b873..ba26513 100644 --- a/src/main/java/lc/captchas/ShadowTextCaptcha.java +++ b/src/main/java/lc/captchas/ShadowTextCaptcha.java @@ -10,7 +10,7 @@ import java.awt.image.BufferedImage; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.io.ByteArrayOutputStream; -import java.util.HashMap; +import java.util.Map; import java.util.List; import lc.misc.HelperFunctions; @@ -27,13 +27,12 @@ public class ShadowTextCaptcha implements ChallengeProvider { // TODO: Add custom config } - public HashMap> supportedParameters() { - HashMap> supportedParams = new HashMap>(); - supportedParams.put("supportedLevels", List.of("easy")); - supportedParams.put("supportedMedia", List.of("image/png")); - supportedParams.put("supportedInputType", List.of("text")); - - return supportedParams; + public Map> supportedParameters() { + return Map.of( + "supportedLevels", List.of("easy"), + "supportedMedia", List.of("image/png"), + "supportedInputType", List.of("text") + ); } public boolean checkAnswer(String secret, String answer) { diff --git a/src/main/scala/lc/captchas/FilterChallenge.scala b/src/main/scala/lc/captchas/FilterChallenge.scala index 4fcc89d..8c21e78 100644 --- a/src/main/scala/lc/captchas/FilterChallenge.scala +++ b/src/main/scala/lc/captchas/FilterChallenge.scala @@ -7,7 +7,6 @@ import java.awt.Font import java.awt.Color import lc.captchas.interfaces.ChallengeProvider import lc.captchas.interfaces.Challenge -import scala.jdk.CollectionConverters.MapHasAsJava import java.util.{List => JavaList, Map => JavaMap} class FilterChallenge extends ChallengeProvider { @@ -18,13 +17,11 @@ class FilterChallenge extends ChallengeProvider { } def supportedParameters(): JavaMap[String, JavaList[String]] = { - val supportedParams = Map( - "supportedLevels" -> JavaList.of("medium", "hard"), - "supportedMedia" -> JavaList.of("image/png"), - "supportedInputType" -> JavaList.of("text") - ).asJava - - supportedParams + JavaMap.of( + "supportedLevels",JavaList.of("medium", "hard"), + "supportedMedia", JavaList.of("image/png"), + "supportedInputType", JavaList.of("text") + ) } def returnChallenge(): Challenge = { diff --git a/src/main/scala/lc/captchas/LabelCaptcha.scala b/src/main/scala/lc/captchas/LabelCaptcha.scala index 5233678..cd008e7 100644 --- a/src/main/scala/lc/captchas/LabelCaptcha.scala +++ b/src/main/scala/lc/captchas/LabelCaptcha.scala @@ -9,7 +9,6 @@ import java.awt.image.BufferedImage import java.awt.Color import lc.captchas.interfaces.ChallengeProvider import lc.captchas.interfaces.Challenge -import scala.jdk.CollectionConverters.MapHasAsJava import java.util.{List => JavaList, Map => JavaMap} class LabelCaptcha extends ChallengeProvider { @@ -30,13 +29,11 @@ class LabelCaptcha extends ChallengeProvider { } def supportedParameters(): JavaMap[String, JavaList[String]] = { - val supportedParams = Map( - "supportedLevels" -> JavaList.of("hard"), - "supportedMedia" -> JavaList.of("image/png"), - "supportedInputType" -> JavaList.of("text") - ).asJava - - supportedParams + JavaMap.of( + "supportedLevels", JavaList.of("hard"), + "supportedMedia", JavaList.of("image/png"), + "supportedInputType", JavaList.of("text") + ) } def returnChallenge(): Challenge = diff --git a/src/main/scala/lc/captchas/RainDropsCaptcha.scala b/src/main/scala/lc/captchas/RainDropsCaptcha.scala index 0b08dcd..88581fd 100644 --- a/src/main/scala/lc/captchas/RainDropsCaptcha.scala +++ b/src/main/scala/lc/captchas/RainDropsCaptcha.scala @@ -10,7 +10,6 @@ import javax.imageio.stream.MemoryCacheImageOutputStream; import lc.captchas.interfaces.ChallengeProvider import lc.captchas.interfaces.Challenge import lc.misc.GifSequenceWriter -import scala.jdk.CollectionConverters.MapHasAsJava import java.util.{List => JavaList, Map => JavaMap} class Drop { @@ -38,13 +37,11 @@ class RainDropsCP extends ChallengeProvider { } def supportedParameters(): JavaMap[String, JavaList[String]] = { - val supportedParams = Map( - "supportedLevels" -> JavaList.of("medium", "easy"), - "supportedMedia" -> JavaList.of("image/gif"), - "supportedInputType" -> JavaList.of("text") - ).asJava - - supportedParams + JavaMap.of( + "supportedLevels", JavaList.of("medium", "easy"), + "supportedMedia", JavaList.of("image/gif"), + "supportedInputType", JavaList.of("text") + ) } private def extendDrops(drops: Array[Drop], steps: Int, xOffset: Int) = { From 433621f046129b39f7552057437081860d9b89d8 Mon Sep 17 00:00:00 2001 From: hrj Date: Wed, 14 Apr 2021 09:47:22 +0530 Subject: [PATCH 19/22] Update simpleTest to latest API and solve before answering Signed-off-by: hrj --- scripts/simpleTest.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/scripts/simpleTest.py b/scripts/simpleTest.py index 760c120..4503199 100644 --- a/scripts/simpleTest.py +++ b/scripts/simpleTest.py @@ -1,12 +1,8 @@ import http.client import json +import subprocess conn = http.client.HTTPConnection('localhost', 8888) -conn.request("GET", "/v1/token?email=test") -response = conn.getresponse() -responseStr = response.read() -user = json.loads(responseStr) -token = user["token"] params = """{ "level": "medium", @@ -15,13 +11,25 @@ params = """{ }""" def getCaptcha(): - conn.request("POST", "/v1/captcha", body=params, headers={'access-token': user["token"]}) + conn.request("POST", "/v1/captcha", body=params) response = conn.getresponse() if response: responseStr = response.read() return json.loads(responseStr) +def getAndSolve(idStr): + conn.request("GET", "/v1/media?id=" + idStr) + response = conn.getresponse() + + if response: + responseBytes = response.read() + with open("captcha.png", "wb") as f: + f.write(responseBytes) + ocrResult = subprocess.Popen("gocr captcha.png", shell=True, stdout=subprocess.PIPE) + ocrAnswer = ocrResult.stdout.readlines()[0].strip().decode() + return ocrAnswer + def postAnswer(captchaId, ans): reply = {"answer": ans, "id" : captchaId} conn.request("POST", "/v1/answer", json.dumps(reply)) @@ -33,6 +41,6 @@ def postAnswer(captchaId, ans): for i in range(0, 10000): captcha = getCaptcha() - #print(captcha) captchaId = captcha["id"] - print(i, postAnswer(captchaId, "xyz")) + ans = getAndSolve(captchaId) + print(i, postAnswer(captchaId, ans)) From 352424e8f5169b39ebc9cd70785df59bf4069745 Mon Sep 17 00:00:00 2001 From: hrj Date: Wed, 14 Apr 2021 10:00:36 +0530 Subject: [PATCH 20/22] simpleTest: use XDG_RUNTIME_DIR for storing temp files Signed-off-by: hrj --- scripts/simpleTest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/simpleTest.py b/scripts/simpleTest.py index 4503199..04f260b 100644 --- a/scripts/simpleTest.py +++ b/scripts/simpleTest.py @@ -1,7 +1,9 @@ import http.client import json import subprocess +import os +tempDir = os.getenv('XDG_RUNTIME_DIR', '.') conn = http.client.HTTPConnection('localhost', 8888) params = """{ @@ -24,9 +26,10 @@ def getAndSolve(idStr): if response: responseBytes = response.read() - with open("captcha.png", "wb") as f: + fileName = tempDir + "/captcha.png" + with open(fileName, "wb") as f: f.write(responseBytes) - ocrResult = subprocess.Popen("gocr captcha.png", shell=True, stdout=subprocess.PIPE) + ocrResult = subprocess.Popen("gocr " + fileName, shell=True, stdout=subprocess.PIPE) ocrAnswer = ocrResult.stdout.readlines()[0].strip().decode() return ocrAnswer From 8fd294f0cf75c1953200f547e70b217ba5114799 Mon Sep 17 00:00:00 2001 From: hrj Date: Wed, 14 Apr 2021 10:00:58 +0530 Subject: [PATCH 21/22] simpleTest.py : use debug level of difficulty Signed-off-by: hrj --- scripts/simpleTest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/simpleTest.py b/scripts/simpleTest.py index 04f260b..daa3f7f 100644 --- a/scripts/simpleTest.py +++ b/scripts/simpleTest.py @@ -7,7 +7,7 @@ tempDir = os.getenv('XDG_RUNTIME_DIR', '.') conn = http.client.HTTPConnection('localhost', 8888) params = """{ -"level": "medium", +"level": "debug", "media": "image/png", "input_type": "text" }""" From 660447798f30b24322b1fb8088d10da405e32ab9 Mon Sep 17 00:00:00 2001 From: hrj Date: Wed, 14 Apr 2021 10:01:30 +0530 Subject: [PATCH 22/22] Debug Captcha: Print mismatches Signed-off-by: hrj --- src/main/scala/lc/captchas/DebugCaptcha.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/scala/lc/captchas/DebugCaptcha.scala b/src/main/scala/lc/captchas/DebugCaptcha.scala index 084f1e1..787b6cf 100644 --- a/src/main/scala/lc/captchas/DebugCaptcha.scala +++ b/src/main/scala/lc/captchas/DebugCaptcha.scala @@ -33,7 +33,11 @@ class DebugCaptcha extends ChallengeProvider { } def checkAnswer(secret: String, answer: String): Boolean = { - answer.toLowerCase().equals(secret) + val matches = answer.toLowerCase().replaceAll(" ", "").equals(secret) + if (!matches) { + println(s"Didn't match, answer: '$answer' to secret '$secret'") + } + matches } private def simpleText(text: String): Array[Byte] = {