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
|
||||
run: sbt "scalafixAll --check"
|
||||
- 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);
|
||||
}
|
||||
|
||||
public static String randomString(int n) {
|
||||
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz23456789$#%@&?";
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
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 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++) {
|
||||
int index = (int) (characters.length() * Math.random());
|
||||
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,
|
||||
"GifCaptcha" -> new GifCaptcha,
|
||||
"ShadowTextCaptcha" -> new ShadowTextCaptcha,
|
||||
"RainDropsCaptcha" -> new RainDropsCP
|
||||
"RainDropsCaptcha" -> new RainDropsCP,
|
||||
"DebugCaptcha" -> new DebugCaptcha,
|
||||
//"LabelCaptcha" -> new LabelCaptcha
|
||||
)
|
||||
|
||||
|
@ -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 =
|
||||
|
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=$!
|
||||
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=$?
|
||||
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user