Merge remote-tracking branch 'upstream/master' into Error-handling

This commit is contained in:
Rahul Rudragoudar 2021-04-14 12:59:55 +05:30
commit 8f9719acbf
No known key found for this signature in database
GPG Key ID: 0D2CE231A7287EBC
16 changed files with 253 additions and 86 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

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

View File

@ -1,27 +1,38 @@
import http.client
import json
import subprocess
import os
tempDir = os.getenv('XDG_RUNTIME_DIR', '.')
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",
"level": "debug",
"media": "image/png",
"input_type": "text"
}"""
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()
fileName = tempDir + "/captcha.png"
with open(fileName, "wb") as f:
f.write(responseBytes)
ocrResult = subprocess.Popen("gocr " + fileName, 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 +44,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))

View File

@ -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<String, List<String>> supportedParameters() {
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
supportedParams.put("supportedLevels", List.of("medium"));
supportedParams.put("supportedMedia", List.of("image/png"));
supportedParams.put("supportedInputType", List.of("text"));
return supportedParams;
public Map<String, List<String>> supportedParameters() {
return Map.of(
"supportedLevels", List.of("medium"),
"supportedMedia", List.of("image/png"),
"supportedInputType", List.of("text")
);
}
public void configure(String config) {

View File

@ -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<String, List<String>> supportedParameters() {
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
supportedParams.put("supportedLevels", List.of("hard"));
supportedParams.put("supportedMedia", List.of("image/gif"));
supportedParams.put("supportedInputType", List.of("text"));
return supportedParams;
public Map<String, List<String>> supportedParameters() {
return Map.of(
"supportedLevels", List.of("hard"),
"supportedMedia", List.of("image/gif"),
"supportedInputType", List.of("text")
);
}
public Challenge returnChallenge() {

View File

@ -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<String, List<String>> supportedParameters() {
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
supportedParams.put("supportedLevels", List.of("easy"));
supportedParams.put("supportedMedia", List.of("image/png"));
supportedParams.put("supportedInputType", List.of("text"));
return supportedParams;
public Map<String, List<String>> supportedParameters() {
return Map.of(
"supportedLevels", List.of("easy"),
"supportedMedia", List.of("image/png"),
"supportedInputType", List.of("text")
);
}
public boolean checkAnswer(String secret, String answer) {

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,68 @@
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 = {
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] = {
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

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

View File

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

View File

@ -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) = {

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
)

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