Merge pull request #83 from UprootStaging/debugCaptcha

Add Debug Captcha and functional tests
This commit is contained in:
hrj 2021-04-13 17:44:49 +05:30 committed by GitHub
commit ab64bb217c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 183 additions and 27 deletions

View File

@ -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

View File

@ -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));

View 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())
}
}

View File

@ -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
)
@ -56,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)

View File

@ -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
View 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" : { }
}]
}

View 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"

View File

@ -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