mirror of
https://github.com/librecaptcha/lc-core.git
synced 2025-02-12 03:08:06 -05:00
Merge pull request #83 from UprootStaging/debugCaptcha
Add Debug Captcha and functional tests
This commit is contained in:
commit
ab64bb217c
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -21,4 +21,4 @@ jobs:
|
|||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: sbt "scalafixAll --check"
|
run: sbt "scalafixAll --check"
|
||||||
- name: Run locust tests
|
- name: Run locust tests
|
||||||
run: ./tests/run.sh
|
run: sudo apt-get install -y tesseract-ocr && ./tests/run.sh
|
||||||
|
@ -11,9 +11,19 @@ public class HelperFunctions {
|
|||||||
RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String randomString(int n) {
|
public static final String safeAlphabets = "ABCDEFGHJKMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||||
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz23456789$#%@&?";
|
public static final String allAlphabets = safeAlphabets + "ILl";
|
||||||
StringBuilder stringBuilder = new StringBuilder();
|
public static final String safeNumbers = "23456789";
|
||||||
|
public static final String allNumbers = safeNumbers + "1";
|
||||||
|
public static final String specialCharacters = "$#%@&?";
|
||||||
|
public static final String safeCharacters = safeAlphabets + safeNumbers + specialCharacters;
|
||||||
|
|
||||||
|
public static String randomString(final int n) {
|
||||||
|
return randomString(n, safeCharacters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String randomString(final int n, final String characters) {
|
||||||
|
final StringBuilder stringBuilder = new StringBuilder();
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
int index = (int) (characters.length() * Math.random());
|
int index = (int) (characters.length() * Math.random());
|
||||||
stringBuilder.append(characters.charAt(index));
|
stringBuilder.append(characters.charAt(index));
|
||||||
|
64
src/main/scala/lc/captchas/DebugCaptcha.scala
Normal file
64
src/main/scala/lc/captchas/DebugCaptcha.scala
Normal file
@ -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 debugging purposes. It creates very simple captchas that are deliberately 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, 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.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, HelperFunctions.safeAlphabets)
|
||||||
|
new Challenge(simpleText(secret), "image/png", secret.toLowerCase())
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,8 @@ object CaptchaProviders {
|
|||||||
//"FontFunCaptcha" -> new FontFunCaptcha,
|
//"FontFunCaptcha" -> new FontFunCaptcha,
|
||||||
"GifCaptcha" -> new GifCaptcha,
|
"GifCaptcha" -> new GifCaptcha,
|
||||||
"ShadowTextCaptcha" -> new ShadowTextCaptcha,
|
"ShadowTextCaptcha" -> new ShadowTextCaptcha,
|
||||||
"RainDropsCaptcha" -> new RainDropsCP
|
"RainDropsCaptcha" -> new RainDropsCP,
|
||||||
|
"DebugCaptcha" -> new DebugCaptcha,
|
||||||
//"LabelCaptcha" -> new LabelCaptcha
|
//"LabelCaptcha" -> new LabelCaptcha
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,25 +44,9 @@ object Config {
|
|||||||
case JField("config", JObject(config)) => ("config", JString(config.toString))
|
case JField("config", JObject(config)) => ("config", JString(config.toString))
|
||||||
}
|
}
|
||||||
val captchaConfig: List[CaptchaConfig] = captchaConfigTransform.extract[List[CaptchaConfig]]
|
val captchaConfig: List[CaptchaConfig] = captchaConfigTransform.extract[List[CaptchaConfig]]
|
||||||
val allowedLevels: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDLEVELS.toString)
|
val allowedLevels: Set[String] = captchaConfig.flatMap(_.allowedLevels).toSet
|
||||||
val allowedMedia: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDMEDIA.toString)
|
val allowedMedia: Set[String] = captchaConfig.flatMap(_.allowedMedia).toSet
|
||||||
val allowedInputType: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDINPUTTYPE.toString)
|
val allowedInputType: Set[String] = captchaConfig.flatMap(_.allowedInputType).toSet
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
private def getDefaultConfig(): String = {
|
private def getDefaultConfig(): String = {
|
||||||
val defaultConfigMap =
|
val defaultConfigMap =
|
||||||
|
14
tests/debug-config.json
Normal file
14
tests/debug-config.json
Normal file
@ -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" : { }
|
||||||
|
}]
|
||||||
|
}
|
65
tests/locustfile-functional.py
Normal file
65
tests/locustfile-functional.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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):
|
||||||
|
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 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
|
||||||
|
|
||||||
|
class QuickStartUser(SequentialTaskSet):
|
||||||
|
wait_time = between(0.1,0.2)
|
||||||
|
|
||||||
|
@task
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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:
|
||||||
|
resp.failure("Status was not 200: " + resp.text)
|
||||||
|
else:
|
||||||
|
if resp.json().get("result") != "True":
|
||||||
|
resp.failure("Answer was not accepted: " + ocrAnswer)
|
||||||
|
|
||||||
|
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)
|
||||||
|
ocrResult = subprocess.Popen("tesseract %s stdout -l eng" % 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)
|
||||||
|
tasks = [QuickStartUser]
|
||||||
|
host = "http://localhost:8888"
|
22
tests/run.sh
22
tests/run.sh
@ -7,9 +7,27 @@ java -jar target/scala-2.13/LibreCaptcha.jar &
|
|||||||
JAVA_PID=$!
|
JAVA_PID=$!
|
||||||
sleep 4
|
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=$?
|
status=$?
|
||||||
|
|
||||||
kill $JAVA_PID
|
if [ $status != 0 ]; then
|
||||||
|
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 --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
|
||||||
|
|
||||||
|
kill $JAVA_PID
|
||||||
exit $status
|
exit $status
|
||||||
|
Loading…
x
Reference in New Issue
Block a user