Compare commits

..

No commits in common. "master" and "v1.1.0-beta1" have entirely different histories.

43 changed files with 339 additions and 793 deletions

View File

@ -1,5 +0,0 @@
# Scala Steward: Reformat with sbt-java-formatter 0.8.0
57ce691a00babb03e0cae03a26fe56d63fc609af
# Scala Steward: Reformat with scalafmt 3.6.1
f2b19baca828a4d88b46bc009aef6d7115e63924

View File

@ -1,3 +0,0 @@
# If true, Scala Steward will sign off all commits (e.g. `git --signoff`).
# Default: false
signoffCommits = true

View File

@ -16,7 +16,6 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 1.11
- uses: sbt/setup-sbt@v1
- name: Run tests
run: sbt test assembly
- name: Run linter

View File

@ -3,8 +3,6 @@ name: Update docker image
on:
push:
branches: [ master ]
tags:
- 'v*'
jobs:
build:
@ -22,20 +20,7 @@ jobs:
- name: Assemble Jar
run: sbt assembly
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: librecaptcha/lc-core
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@ -51,13 +36,8 @@ jobs:
with:
context: ./
file: ./Runner.Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: |
linux/amd64
linux/arm64
linux/arm/v7
push: true
tags: librecaptcha/lc-core:latest
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View File

@ -1,5 +1,8 @@
rules=[
ExplicitResultTypes,
RemoveUnused,
DisableSyntax,
LeakingImplicitClassVal,
NoValInForComprehension,
ProcedureSyntax
]

View File

@ -1,3 +1,2 @@
version="3.8.3"
maxColumn = 120
runner.dialect = scala3
version=2.5.2
maxColumn = 120

View File

@ -1,13 +1,12 @@
FROM eclipse-temurin:17-jre-jammy AS base-builder
ARG SBT_VERSION=1.7.1
FROM adoptopenjdk/openjdk16:alpine AS base-builder
ARG SBT_VERSION=1.3.13
RUN apk add --no-cache bash
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
ENV PATH=$PATH:${JAVA_HOME}/bin
RUN \
apt update && \
apt install -y wget && \
wget -O sbt-$SBT_VERSION.tgz https://github.com/sbt/sbt/releases/download/v$SBT_VERSION/sbt-$SBT_VERSION.tgz && \
tar -xzvf sbt-$SBT_VERSION.tgz && \
rm sbt-$SBT_VERSION.tgz
tar -xzvf sbt-$SBT_VERSION.tgz && \
rm sbt-$SBT_VERSION.tgz
ENV PATH=$PATH:/sbt/bin/
@ -23,15 +22,15 @@ FROM sbt-builder as builder
COPY src/ src/
RUN sbt assembly
FROM eclipse-temurin:17-jre-jammy AS base-core
FROM adoptopenjdk/openjdk16:alpine-jre AS base-core
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
RUN apt update && apt install -y fonts-dejavu
RUN apk add --update ttf-dejavu
ENV PATH=$PATH:${JAVA_HOME}/bin
FROM base-core
WORKDIR /lc-core
COPY --from=builder /build/target/scala-3.6.2/LibreCaptcha.jar .
COPY --from=builder /build/target/scala-2.13/LibreCaptcha.jar .
RUN mkdir data/
EXPOSE 8888

117
README.md
View File

@ -2,12 +2,12 @@
LibreCaptcha is a framework that allows developers to create their own [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA)s.
The framework defines the API for a CAPTCHA generator and takes care of mundane details
such as:
* An HTTP interface for serving CAPTCHAs
* Background workers to pre-compute CAPTCHAs and to store them in a database
* Managing secrets for the CAPTCHAs (tokens, expected answers, etc)
* Safe re-impressions of CAPTCHA images (by creating unique tokens for every impression)
* Garbage collection of stale CAPTCHAs
* Sandboxed plugin architecture (TBD)
* An HTTP interface for serving CAPTCHAs
* Background workers to pre-compute CAPTCHAs and to store them in a database
* Managing secrets for the CAPTCHAs (tokens, expected answers, etc)
* Safe re-impressions of CAPTCHA images (by creating unique tokens for every impression)
* Garbage collection of stale CAPTCHAs
* Sandboxed plugin architecture (TBD)
Some sample CAPTCHA generators are included in the distribution (see below). We will continue adding more samples to the list. For quick
deployments the samples themselves might be sufficient. Projects with more resources might want create their own CAPTCHAs
@ -47,25 +47,18 @@ docker-compose up
Using `docker`:
```
docker run -p=8888:8888 -v ./lcdata:/lc-core/data librecaptcha/lc-core:2.0
docker run -v lcdata:/lc-core/data librecaptcha/lc-core:latest
```
A default `config.json` is automatically created in the mounted volume.
The above commands should work with `podman` as well, if docker.io registry is pre-configured. Otherwise,
you can manually specify the repository like so:
```
podman run -p=8888:8888 -v ./lcdata:/lc-core/data docker.io/librecaptcha/lc-core:2.0
```
## Quick test
Open [localhost:8888/demo/index.html](http://localhost:8888/demo/index.html) in browser.
Alternatively, on the command line, try:
```
> $ curl -d '{"media":"image/png","level":"easy","input_type":"text","size":"350x100"}' localhost:8888/v2/captcha
> $ curl -d '{"media":"image/png","level":"easy","input_type":"text"}' localhost:8888/v1/captcha
{"id":"3bf928ce-a1e7-4616-b34f-8252d777855d"}
> $ curl "localhost:8888/v1/media?id=3bf928ce-a1e7-4616-b34f-8252d777855d" -o sample.png
@ -98,11 +91,6 @@ create CAPTCHAs that suit their application and audience, with matching themes a
And, the more the variety of CAPTCHAS, the harder it is for bots to crack CAPTCHAs.
## Sample CAPTCHAs
These are included in this server.
### ShadowText
![ShadowText Sample](./samples/shadowText.png)
### FilterCaptcha
@ -126,87 +114,50 @@ If a sufficient number of users agree on their answer to the unknown word, it is
***
## HTTP API
The service can be accessed using a simple HTTP API.
## HTTP API
### - `/v1/captcha`: `POST`
- Parameters:
- `level`: `String` -
- Parameters:
- `level`: `String` -
The difficulty level of a captcha
- easy
- medium
- hard
- `input_type`: `String` -
- easy
- medium
- hard
- `input_type`: `String` -
The type of input option for a captcha
- text
- (More to come)
- `media`: `String` -
- text
- (More to come)
- `media`: `String` -
The type of media of a captcha
- image/png
- image/gif
- (More to come)
- `size`: String -
The dimensions of a captcha. It needs to be a string in the format `"widthxheight"` in pixels, and will be matched
with the `allowedSizes` config setting. Example: `size: "450x200"` which requests an image of width 450 and height
200 pixels.
- image/png
- image/gif
- (More to come)
- `size`: `Map` -
The dimensions of a captcha (Optional). It needs two more fields nested in this parameter
- `height`: `Int`
- `width`: `Int`
- Returns:
- Return type:
- `id`: `String` - The uuid of the captcha generated
### - `/v1/media`: `GET`
- Parameters:
### - `/v1/media`: `GET`
- Parameters:
- `id`: `String` - The uuid of the captcha
- Returns:
- Return type:
- `image`: `Array[Byte]` - The requested media as bytes
### - `/v1/answer`: `POST`
- Parameter:
- Parameter:
- `id`: `String` - The uuid of the captcha that needs to be solved
- `answer`: `String` - The answer to the captcha that needs to be validated
- Returns:
- Return Type:
- `result`: `String` - The result after validation/checking of the answer
- True - If the answer is correct
- False - If the answer is incorrect
- Expired - If the time limit to solve the captcha exceeds
## Example usage
In javascript:
```js
const resp = await fetch("/v2/captcha", {
method: 'POST',
body: JSON.stringify({level: "easy", media: "image/png", "input_type" : "text", size: "350x100"})
})
const respJson = await resp.json();
let captchaId = null;
if (resp.ok) {
// The CAPTCHA can be displayed using the data in respJson.
console.log(respJson);
// Store the id somewhere so that it can be used later for answer verification
captchaId = respJson.id;
} else {
console.err(respJson);
}
// When user submits an answer it can be sent to the server for verification thusly:
const resp = await fetch("/v2/answer", {
method: 'POST',
body: JSON.stringify({id: captchaId, answer: "user input"})
});
const respJson = await resp.json();
console.log(respJson.result);
```
- True - If the answer is correct
- False - If the answer is incorrect
- Expired - If the time limit to solve the captcha exceeds
***

View File

@ -1,12 +1,12 @@
FROM eclipse-temurin:17-jre-jammy AS base-core
FROM adoptopenjdk/openjdk16:alpine-jre AS base-core
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
RUN apt update && apt install -y fonts-dejavu
RUN apk add --update ttf-dejavu
ENV PATH=$PATH:${JAVA_HOME}/bin
FROM base-core
RUN mkdir /lc-core
COPY target/scala-3.6.2/LibreCaptcha.jar /lc-core
COPY target/scala-2.13/LibreCaptcha.jar /lc-core
WORKDIR /lc-core
RUN mkdir data/

View File

@ -2,23 +2,23 @@ lazy val root = (project in file(".")).settings(
inThisBuild(
List(
organization := "com.example",
scalaVersion := "3.6.2",
version := "0.2.1-snapshot",
scalaVersion := "2.13.5",
version := "0.1.0-SNAPSHOT",
semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision
// This is apparently not supported on Scala 3 currently
// scalafixScalaBinaryVersion := "3.1"
semanticdbVersion := scalafixSemanticdb.revision,
scalafixScalaBinaryVersion := "2.13"
)
),
name := "LibreCaptcha",
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.3.0",
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.3.0",
libraryDependencies += "org.json4s" %% "json4s-jackson" % "4.0.7"
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.0.12",
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.0.12",
libraryDependencies += "org.json4s" % "json4s-jackson_2.13" % "3.6.11"
)
Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" }
scalacOptions ++= List(
"-Yrangepos",
"-Ywarn-unused",
"-deprecation"
)
javacOptions += "-g:none"
@ -26,14 +26,6 @@ compileOrder := CompileOrder.JavaThenScala
javafmtOnCompile := false
assembly / mainClass := Some("lc.LCFramework")
Compile / run / mainClass := Some("lc.LCFramework")
assembly / assemblyJarName := "LibreCaptcha.jar"
ThisBuild / assemblyMergeStrategy := {
case PathList("module-info.class") => MergeStrategy.discard
case x if x.endsWith("/module-info.class") => MergeStrategy.discard
case x =>
val oldStrategy = (ThisBuild / assemblyMergeStrategy).value
oldStrategy(x)
}
assembly / assemblyJarName := "LibreCaptcha.jar"
run / fork := true

View File

@ -4,8 +4,6 @@ services:
lc-core:
container_name: "libre-captcha"
image: librecaptcha/lc-core:latest
# Comment "image" & uncomment "build" if you intend to build from source
#build: .
volumes:
- "./docker-data:/lc-core/data"
ports:

BIN
lib/h2-1.4.200.jar Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1 +1 @@
sbt.version=1.10.6
sbt.version=1.5.0

View File

@ -1,4 +1,4 @@
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.27")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2")
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.6.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,16 +1,16 @@
package lc.captchas;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.util.List;
import java.util.Map;
import java.util.List;
import lc.captchas.interfaces.Challenge;
import lc.captchas.interfaces.ChallengeProvider;
import lc.misc.HelperFunctions;
import lc.misc.PngImageWriter;
public class FontFunCaptcha implements ChallengeProvider {
@ -58,10 +58,9 @@ public class FontFunCaptcha implements ChallengeProvider {
return null;
}
private byte[] fontFun(
final int width, final int height, String captchaText, String level, String path) {
private byte[] fontFun(String captchaText, String level, String path) {
String[] colors = {"#f68787", "#f8a978", "#f1eb9a", "#a4f6a5"};
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics2D = img.createGraphics();
for (int i = 0; i < captchaText.length(); i++) {
Font font = loadCustomFont(level, path);
@ -75,21 +74,17 @@ public class FontFunCaptcha implements ChallengeProvider {
graphics2D.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
PngImageWriter.write(baos, img);
ImageIO.write(img, "png", baos);
} catch (Exception e) {
e.printStackTrace();
}
return baos.toByteArray();
}
public Challenge returnChallenge(String level, String size) {
public Challenge returnChallenge() {
String secret = HelperFunctions.randomString(7);
final int[] size2D = HelperFunctions.parseSize2D(size);
final int width = size2D[0];
final int height = size2D[1];
String path = "./lib/fonts/";
return new Challenge(
fontFun(width, height, secret, "medium", path), "image/png", secret.toLowerCase());
return new Challenge(fontFun(secret, "medium", path), "image/png", secret.toLowerCase());
}
public boolean checkAnswer(String secret, String answer) {

View File

@ -1,51 +1,51 @@
package lc.captchas;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.IntStream;
import java.util.LinkedList;
import java.util.List;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.io.ByteArrayOutputStream;
import lc.captchas.interfaces.Challenge;
import lc.captchas.interfaces.ChallengeProvider;
import lc.misc.GifSequenceWriter;
import lc.misc.HelperFunctions;
import lc.misc.GifSequenceWriter;
public class PoppingCharactersCaptcha implements ChallengeProvider {
private final Font font = new Font("Arial", Font.ROMAN_BASELINE, 48);
private final int width = 250;
private final int height = 100;
private int[] computeOffsets(
final Font font, final int width, final int height, final String text) {
private Integer[] computeOffsets(final String text) {
final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final var graphics2D = img.createGraphics();
final var frc = graphics2D.getFontRenderContext();
final var advances = new int[text.length() + 1];
final var advances = new LinkedList<Integer>();
final var spacing = font.getStringBounds(" ", frc).getWidth() / 3;
var currX = 0;
for (int i = 0; i < text.length(); i++) {
final var c = text.charAt(i);
advances[i] = currX;
advances.add(currX);
currX += font.getStringBounds(String.valueOf(c), frc).getWidth();
currX += spacing;
}
;
advances[text.length()] = currX;
};
graphics2D.dispose();
return advances;
return advances.toArray(new Integer[]{});
}
private BufferedImage makeImage(
final Font font, final int width, final int height, final Consumer<Graphics2D> f) {
private BufferedImage makeImage(final Consumer<Graphics2D> f) {
final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final var graphics2D = img.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
graphics2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
graphics2D.setFont(font);
f.accept(graphics2D);
graphics2D.dispose();
@ -56,49 +56,30 @@ public class PoppingCharactersCaptcha implements ChallengeProvider {
return HelperFunctions.randomNumber(-2, +2);
}
private byte[] gifCaptcha(final int width, final int height, final String text) {
private byte[] gifCaptcha(final String text) {
try {
final var fontHeight = (int) (height * 0.5);
final Font font = new Font("Arial", Font.ROMAN_BASELINE, fontHeight);
final var byteArrayOutputStream = new ByteArrayOutputStream();
final var output = new MemoryCacheImageOutputStream(byteArrayOutputStream);
final var writer = new GifSequenceWriter(output, 1, 900, true);
final var advances = computeOffsets(font, width, height, text);
final var expectedWidth = advances[advances.length - 1];
final var scale = width / (float) expectedWidth;
final var advances = computeOffsets(text);
final var prevColor = Color.getHSBColor(0f, 0f, 0.1f);
IntStream.range(0, text.length())
.forEach(
i -> {
final var color =
Color.getHSBColor(HelperFunctions.randomNumber(0, 100) / 100.0f, 0.6f, 1.0f);
final var nextImage =
makeImage(
font,
width,
height,
(g) -> {
g.scale(scale, 1);
if (i > 0) {
final var prevI = (i - 1) % text.length();
g.setColor(prevColor);
g.drawString(
String.valueOf(text.charAt(prevI)),
advances[prevI] + jitter(),
fontHeight * 1.1f + jitter());
}
g.setColor(color);
g.drawString(
String.valueOf(text.charAt(i)),
advances[i] + jitter(),
fontHeight * 1.1f + jitter());
});
try {
writer.writeToSequence(nextImage);
} catch (final IOException e) {
e.printStackTrace();
}
});
IntStream.range(0, text.length()).forEach(i -> {
final var color = Color.getHSBColor(HelperFunctions.randomNumber(0, 100)/100.0f, 0.6f, 1.0f);
final var nextImage = makeImage((g) -> {
if (i > 0) {
final var prevI = (i - 1) % text.length();
g.setColor(prevColor);
g.drawString(String.valueOf(text.charAt(prevI)), advances[prevI] + jitter(), 45 + jitter());
}
g.setColor(color);
g.drawString(String.valueOf(text.charAt(i)), advances[i] + jitter(), 45 + jitter());
});
try {
writer.writeToSequence(nextImage);
} catch (final IOException e) {
e.printStackTrace();
}
});
writer.close();
output.close();
return byteArrayOutputStream.toByteArray();
@ -119,12 +100,9 @@ public class PoppingCharactersCaptcha implements ChallengeProvider {
"supportedInputType", List.of("text"));
}
public Challenge returnChallenge(String level, String size) {
public Challenge returnChallenge() {
final var secret = HelperFunctions.randomString(6);
final int[] size2D = HelperFunctions.parseSize2D(size);
final int width = size2D[0];
final int height = size2D[1];
return new Challenge(gifCaptcha(width, height, secret), "image/gif", secret.toLowerCase());
return new Challenge(gifCaptcha(secret), "image/gif", secret.toLowerCase());
}
public boolean checkAnswer(String secret, String answer) {

View File

@ -1,18 +1,21 @@
package lc.captchas;
import javax.imageio.ImageIO;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.font.TextLayout;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Map;
import java.util.List;
import lc.misc.HelperFunctions;
import lc.captchas.interfaces.Challenge;
import lc.captchas.interfaces.ChallengeProvider;
import lc.misc.HelperFunctions;
import lc.misc.PngImageWriter;
public class ShadowTextCaptcha implements ChallengeProvider {
@ -35,58 +38,44 @@ public class ShadowTextCaptcha implements ChallengeProvider {
return answer.toLowerCase().equals(secret);
}
private float[] makeKernel(int size) {
final int N = size * size;
final float weight = 1.0f / (N);
final float[] kernel = new float[N];
java.util.Arrays.fill(kernel, weight);
return kernel;
};
private byte[] shadowText(final int width, final int height, String text) {
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final int fontHeight = (int) (height * 0.5f);
Font font = new Font("Arial", Font.PLAIN, fontHeight);
private byte[] shadowText(String text) {
BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB);
Font font = new Font("Arial", Font.ROMAN_BASELINE, 48);
Graphics2D graphics2D = img.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
TextLayout textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext());
HelperFunctions.setRenderingHints(graphics2D);
graphics2D.setPaint(Color.WHITE);
graphics2D.fillRect(0, 0, width, height);
graphics2D.fillRect(0, 0, 350, 100);
graphics2D.setPaint(Color.BLACK);
graphics2D.setFont(font);
final var stringWidth = graphics2D.getFontMetrics().stringWidth(text);
final var padding = (stringWidth > width) ? 0 : (width - stringWidth) / 2;
final var scaleX = (stringWidth > width) ? width / ((double) stringWidth) : 1d;
graphics2D.scale(scaleX, 1d);
graphics2D.drawString(text, padding, fontHeight * 1.1f);
textLayout.draw(graphics2D, 15, 50);
graphics2D.dispose();
final int kernelSize = (int) Math.ceil((Math.min(width, height) / 50.0));
ConvolveOp op =
new ConvolveOp(
new Kernel(kernelSize, kernelSize, makeKernel(kernelSize)),
ConvolveOp.EDGE_NO_OP,
null);
float[] kernel = {
1f / 9f, 1f / 9f, 1f / 9f,
1f / 9f, 1f / 9f, 1f / 9f,
1f / 9f, 1f / 9f, 1f / 9f
};
ConvolveOp op = new ConvolveOp(new Kernel(3, 3, kernel), ConvolveOp.EDGE_NO_OP, null);
BufferedImage img2 = op.filter(img, null);
Graphics2D g2d = img2.createGraphics();
HelperFunctions.setRenderingHints(g2d);
g2d.setPaint(Color.WHITE);
g2d.scale(scaleX, 1d);
g2d.setFont(font);
g2d.drawString(text, padding - kernelSize, fontHeight * 1.1f);
textLayout.draw(g2d, 13, 50);
g2d.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
PngImageWriter.write(baos, img2);
ImageIO.write(img2, "png", baos);
} catch (Exception e) {
e.printStackTrace();
}
return baos.toByteArray();
}
public Challenge returnChallenge(String level, String size) {
public Challenge returnChallenge() {
String secret = HelperFunctions.randomString(6);
final int[] size2D = HelperFunctions.parseSize2D(size);
final int width = size2D[0];
final int height = size2D[1];
return new Challenge(shadowText(width, height, secret), "image/png", secret.toLowerCase());
return new Challenge(shadowText(secret), "image/png", secret.toLowerCase());
}
}

View File

@ -1,12 +1,12 @@
package lc.captchas.interfaces;
import java.util.List;
import java.util.Map;
import java.util.List;
public interface ChallengeProvider {
public String getId();
public Challenge returnChallenge(String level, String size);
public Challenge returnChallenge();
public boolean checkAnswer(String secret, String answer);

View File

@ -3,12 +3,12 @@
package lc.misc;
import java.awt.image.*;
import java.io.*;
import java.util.Iterator;
import javax.imageio.*;
import javax.imageio.metadata.*;
import javax.imageio.stream.*;
import java.awt.image.*;
import java.io.*;
import java.util.Iterator;
public class GifSequenceWriter {
protected ImageWriter gifWriter;

View File

@ -7,18 +7,11 @@ public class HelperFunctions {
private static Random random = new Random();
public static synchronized void setSeed(long seed) {
synchronized public static void setSeed(long seed){
random.setSeed(seed);
}
public static int[] parseSize2D(final String size) {
final String[] fields = size.split("x");
final int[] result = {Integer.parseInt(fields[0]), Integer.parseInt(fields[1])};
return result;
}
}
public static void setRenderingHints(Graphics2D g2d) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(
@ -30,8 +23,7 @@ public class HelperFunctions {
public static final String safeNumbers = "23456789";
public static final String allNumbers = safeNumbers + "10";
public static final String specialCharacters = "$#%@&?";
public static final String safeAlphaNum = safeAlphabets + safeNumbers;
public static final String safeCharacters = safeAlphaNum + specialCharacters;
public static final String safeCharacters = safeAlphabets + safeNumbers + specialCharacters;
public static String randomString(final int n) {
return randomString(n, safeCharacters);
@ -46,11 +38,12 @@ public class HelperFunctions {
return stringBuilder.toString();
}
public static synchronized int randomNumber(int min, int max) {
synchronized public static int randomNumber(int min, int max) {
return random.nextInt((max - min) + 1) + min;
}
public static synchronized int randomNumber(int bound) {
return random.nextInt(bound);
synchronized public static int randomNumber(int bound) {
return random.nextInt(bound);
}
}

View File

@ -1,68 +0,0 @@
package lc.misc;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageOutputStream;
public class PngImageWriter {
static final int DPI = 245;
static final double INCH_2_CM = 2.54;
public static void write(ByteArrayOutputStream boas, BufferedImage gridImage) throws IOException {
final String formatName = "png";
for (Iterator<ImageWriter> iw = ImageIO.getImageWritersByFormatName(formatName);
iw.hasNext(); ) {
ImageWriter writer = iw.next();
ImageWriteParam writeParam = writer.getDefaultWriteParam();
ImageTypeSpecifier typeSpecifier =
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam);
if (metadata.isReadOnly() || !metadata.isStandardMetadataFormatSupported()) {
continue;
}
setDPIMeta(metadata);
final ImageOutputStream stream = ImageIO.createImageOutputStream(boas);
try {
writer.setOutput(stream);
writer.write(metadata, new IIOImage(gridImage, null, metadata), writeParam);
} finally {
stream.close();
}
break;
}
}
private static void setDPIMeta(IIOMetadata metadata) throws IIOInvalidTreeException {
// for PNG, it's dots per millimeter
double dotsPerMilli = 1.0 * DPI / 10 / INCH_2_CM;
IIOMetadataNode horiz = new IIOMetadataNode("HorizontalPixelSize");
horiz.setAttribute("value", Double.toString(dotsPerMilli));
IIOMetadataNode vert = new IIOMetadataNode("VerticalPixelSize");
vert.setAttribute("value", Double.toString(dotsPerMilli));
IIOMetadataNode dim = new IIOMetadataNode("Dimension");
dim.appendChild(horiz);
dim.appendChild(vert);
IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0");
root.appendChild(dim);
metadata.mergeTree("javax_imageio_1.0", root);
}
}

View File

@ -3,21 +3,21 @@
package org.limium.picoserve;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedList;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.LinkedList;
import java.util.concurrent.Executor;
import java.util.regex.Pattern;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;
public final class Server {
private final HttpServer server;

View File

@ -20,10 +20,9 @@
const levelInput = document.getElementById("levelInput").value
const mediaInput = document.getElementById("mediaInput").value
const typeInput = document.getElementById("typeInput").value
const sizeInput = document.getElementById("sizeInput").value
fetch("/v2/captcha", {
fetch("/v1/captcha", {
method: 'POST',
body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput, "size": sizeInput})
body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput})
}).then(async function(resp) {
const respJson = await resp.json()
if (resp.ok) {
@ -31,7 +30,7 @@
const resultDiv = document.getElementById("result")
const result = `
<p>Id: ${id}</p>
<p><img src="/v2/media?id=${id}" /> </p>
<p><img src="/v1/media?id=${id}" /> </p>
<input type="text" id="answerInput" />
<button onClick="submitAnswer('${id}')">Submit</button>
<div id="answerResult" />
@ -44,7 +43,7 @@
}
async function submitAnswer(id) {
const ans = document.getElementById("answerInput").value;
const resp = await fetch("/v2/answer", {
const resp = await fetch("/v1/answer", {
method: 'POST',
body: JSON.stringify({id: id, answer: ans})
})
@ -71,10 +70,6 @@
<span>Input Type</span>
<input type="text" id="typeInput" value="text" />
</div>
<div class="inputGroup">
<span>Input Size</span>
<input type="text" id="sizeInput" value="350x100" />
</div>
<div class="inputGroup">
<button onClick="loadCaptcha()">Get New CAPTCHA</button>
</div>
@ -84,4 +79,4 @@
</div>
</body>
</html>
</html>

View File

@ -1,59 +1,33 @@
package lc
import lc.core.{CaptchaProviders, CaptchaManager, Config}
import lc.core.CaptchaProviders
import lc.server.Server
import lc.background.BackgroundTask
import lc.database.Statements
import lc.core.Config
object LCFramework {
def main(args: scala.Array[String]): Unit = {
val configFilePath = if (args.length > 0) {
args(0)
} else {
"data/config.json"
}
val config = new Config(configFilePath)
Statements.maxAttempts = config.maxAttempts
val captchaProviders = new CaptchaProviders(config = config)
val captchaManager = new CaptchaManager(config = config, captchaProviders = captchaProviders)
val backgroundTask = new BackgroundTask(config = config, captchaManager = captchaManager)
backgroundTask.beginThread(delay = config.threadDelay)
val server = new Server(
address = config.address,
port = config.port,
captchaManager = captchaManager,
playgroundEnabled = config.playgroundEnabled,
corsHeader = config.corsHeader
val backgroundTask = new BackgroundTask(
throttle = Config.throttle,
timeLimit = Config.captchaExpiryTimeLimit
)
Runtime.getRuntime.addShutdownHook(new Thread {
override def run(): Unit = {
println("Shutting down gracefully...")
backgroundTask.shutdown()
}
})
backgroundTask.beginThread(delay = Config.threadDelay)
val server = new Server(port = Config.port)
server.start()
}
}
object MakeSamples {
def main(args: scala.Array[String]): Unit = {
val configFilePath = if (args.length > 0) {
args(0)
} else {
"data/config.json"
}
val config = new Config(configFilePath)
val captchaProviders = new CaptchaProviders(config = config)
val samples = captchaProviders.generateChallengeSamples()
samples.foreach { case (key, sample) =>
val extensionMap = Map("image/png" -> "png", "image/gif" -> "gif")
println(key + ": " + sample)
val samples = CaptchaProviders.generateChallengeSamples()
samples.foreach {
case (key, sample) =>
val extensionMap = Map("image/png" -> "png", "image/gif" -> "gif")
println(key + ": " + sample)
val outStream = new java.io.FileOutputStream("samples/" + key + "." + extensionMap(sample.contentType))
outStream.write(sample.content)
outStream.close
val outStream = new java.io.FileOutputStream("samples/" + key + "." + extensionMap(sample.contentType))
outStream.write(sample.content)
outStream.close
}
}
}

View File

@ -2,89 +2,50 @@ package lc.background
import lc.database.Statements
import java.util.concurrent.{ScheduledThreadPoolExecutor, TimeUnit}
import lc.core.{CaptchaManager, Config}
import lc.core.{Captcha, Config}
import lc.core.{Parameters, Size}
import lc.misc.HelperFunctions
class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
class BackgroundTask(throttle: Int, timeLimit: Int) {
private val task = new Runnable {
def run(): Unit = {
try {
val mapIdGCPstmt = Statements.tlStmts.get.mapIdGCPstmt
mapIdGCPstmt.setInt(1, config.captchaExpiryTimeLimit)
mapIdGCPstmt.setInt(1, timeLimit)
mapIdGCPstmt.executeUpdate()
val challengeGCPstmt = Statements.tlStmts.get.challengeGCPstmt
challengeGCPstmt.executeUpdate()
val allCombinations = allParameterCombinations()
val requiredCountPerCombination = Math.max(1, (config.bufferCount * 1.01) / allCombinations.size).toInt
for (param <- allCombinations) {
if (!shutdownInProgress) {
val countExisting = captchaManager.getCount(param).getOrElse(0)
val countRequired = requiredCountPerCombination - countExisting
if (countRequired > 0) {
val countCreate = Math.min(1.0 + requiredCountPerCombination / 10.0, countRequired).toInt
println(s"Creating $countCreate of $countRequired captchas for $param")
for (i <- 0 until countCreate) {
if (!shutdownInProgress) {
captchaManager.generateChallenge(param)
}
}
}
}
val imageNum = Statements.tlStmts.get.getCountChallengeTable.executeQuery()
var throttleIn = (throttle * 1.1).toInt
if (imageNum.next())
throttleIn = (throttleIn - imageNum.getInt("total"))
while (0 < throttleIn) {
Captcha.generateChallenge(getRandomParam())
throttleIn -= 1
}
} catch { case exception: Exception => println(exception) }
}
}
private def allParameterCombinations(): List[Parameters] = {
(config.captchaConfig).flatMap { captcha =>
(captcha.allowedLevels).flatMap { level =>
(captcha.allowedMedia).flatMap { media =>
(captcha.allowedInputType).flatMap { inputType =>
(captcha.allowedSizes).map { size =>
Parameters(level, media, inputType, size)
}
}
}
}
}
}
private def getRandomParam(): Parameters = {
val captcha = pickRandom(config.captchaConfig)
val captcha = pickRandom(Config.captchaConfig)
val level = pickRandom(captcha.allowedLevels)
val media = pickRandom(captcha.allowedMedia)
val inputType = pickRandom(captcha.allowedInputType)
val size = pickRandom(captcha.allowedSizes)
Parameters(level, media, inputType, size)
Parameters(level, media, inputType, Some(Size(0, 0)))
}
private def pickRandom[T](list: List[T]): T = {
list(HelperFunctions.randomNumber(list.size))
}
private val ex = new ScheduledThreadPoolExecutor(1)
def beginThread(delay: Int): Unit = {
val ex = new ScheduledThreadPoolExecutor(1)
ex.scheduleWithFixedDelay(task, 1, delay, TimeUnit.SECONDS)
}
@volatile var shutdownInProgress = false
def shutdown(): Unit = {
println(" Shutting down background task...")
shutdownInProgress = true
ex.shutdown()
println(" Finished Shutting background task")
println(" Shutting down DB...")
Statements.tlStmts.get.shutdown.execute()
println(" Finished shutting down db")
}
}

View File

@ -1,5 +1,6 @@
package lc.captchas
import javax.imageio.ImageIO
import java.awt.Color
import java.awt.Font
import java.awt.font.TextLayout
@ -11,11 +12,8 @@ import java.util.List
import lc.misc.HelperFunctions
import lc.captchas.interfaces.Challenge
import lc.captchas.interfaces.ChallengeProvider
import lc.misc.PngImageWriter
/** This captcha is only for debugging purposes. It creates very simple captchas that are deliberately 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 = {
@ -28,12 +26,9 @@ class DebugCaptcha extends ChallengeProvider {
def supportedParameters(): Map[String, List[String]] = {
Map.of(
"supportedLevels",
List.of("debug"),
"supportedMedia",
List.of("image/png"),
"supportedInputType",
List.of("text")
"supportedLevels", List.of("debug"),
"supportedMedia", List.of("image/png"),
"supportedInputType", List.of("text")
)
}
@ -45,20 +40,20 @@ class DebugCaptcha extends ChallengeProvider {
matches
}
private def simpleText(width: Int, height: Int, text: String): Array[Byte] = {
val img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
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, width, height)
graphics2D.fillRect(0, 0, 350, 100)
graphics2D.setPaint(Color.BLACK)
textLayout.draw(graphics2D, 15, 50)
graphics2D.dispose()
val baos = new ByteArrayOutputStream()
try {
PngImageWriter.write(baos, img);
ImageIO.write(img, "png", baos)
} catch {
case e: Exception =>
e.printStackTrace()
@ -66,11 +61,8 @@ class DebugCaptcha extends ChallengeProvider {
baos.toByteArray()
}
def returnChallenge(level: String, size: String): Challenge = {
def returnChallenge(): Challenge = {
val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets)
val size2D = HelperFunctions.parseSize2D(size)
val width = size2D(0)
val height = size2D(1)
new Challenge(simpleText(width, height, secret), "image/png", secret.toLowerCase())
new Challenge(simpleText(secret), "image/png", secret.toLowerCase())
}
}

View File

@ -8,9 +8,6 @@ import java.awt.Color
import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import java.util.{List => JavaList, Map => JavaMap}
import java.io.ByteArrayOutputStream
import lc.misc.PngImageWriter
import lc.misc.HelperFunctions
class FilterChallenge extends ChallengeProvider {
def getId = "FilterChallenge"
@ -21,48 +18,30 @@ class FilterChallenge extends ChallengeProvider {
def supportedParameters(): JavaMap[String, JavaList[String]] = {
JavaMap.of(
"supportedLevels",
JavaList.of("medium", "hard"),
"supportedMedia",
JavaList.of("image/png"),
"supportedInputType",
JavaList.of("text")
"supportedLevels",JavaList.of("medium", "hard"),
"supportedMedia", JavaList.of("image/png"),
"supportedInputType", JavaList.of("text")
)
}
private val filterTypes = List(new FilterType1, new FilterType2)
def returnChallenge(level: String, size: String): Challenge = {
val mediumLevel = level == "medium"
def returnChallenge(): Challenge = {
val filterTypes = List(new FilterType1, new FilterType2)
val r = new scala.util.Random
val characters = if (mediumLevel) HelperFunctions.safeAlphaNum else HelperFunctions.safeCharacters
val n = if (mediumLevel) 5 else 7
val secret = LazyList.continually(r.nextInt(characters.size)).map(characters).take(n).mkString
val size2D = HelperFunctions.parseSize2D(size)
val width = size2D(0)
val height = size2D(1)
val canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
val alphabet = "abcdefghijklmnopqrstuvwxyz"
val n = 8
val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString
val canvas = new BufferedImage(225, 50, BufferedImage.TYPE_INT_RGB)
val g = canvas.createGraphics()
val fontHeight = (height * 0.6).toInt
g.setColor(Color.WHITE)
g.fillRect(0, 0, canvas.getWidth, canvas.getHeight)
g.setColor(Color.BLACK)
val font = new Font("Serif", Font.BOLD, fontHeight)
g.setFont(font)
val stringWidth = g.getFontMetrics().stringWidth(secret)
val scaleX = if (stringWidth > width) width / (stringWidth.toDouble) else 1d
val margin = if (stringWidth > width) 0 else (width - stringWidth)
val xOffset = (margin * r.nextDouble).toInt
g.scale(scaleX, 1d)
g.drawString(secret, xOffset, fontHeight)
g.setFont(new Font("Serif", Font.PLAIN, 30))
g.drawString(secret, 5, 30)
g.dispose()
var image = ImmutableImage.fromAwt(canvas)
val s = r.nextInt(2)
image = filterTypes(s).applyFilter(image, !mediumLevel)
val img = image.awt()
val baos = new ByteArrayOutputStream()
PngImageWriter.write(baos, img);
new Challenge(baos.toByteArray, "image/png", secret)
val s = scala.util.Random.nextInt(2)
image = filterTypes(s).applyFilter(image)
new Challenge(image.bytes(new nio.PngWriter()), "image/png", secret)
}
def checkAnswer(secret: String, answer: String): Boolean = {
secret == answer
@ -70,15 +49,14 @@ class FilterChallenge extends ChallengeProvider {
}
trait FilterType {
def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage
def applyFilter(image: ImmutableImage): ImmutableImage
}
class FilterType1 extends FilterType {
override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = {
val radius = if (hardLevel) 3 else 2
val blur = new GaussianBlurFilter(radius)
override def applyFilter(image: ImmutableImage): ImmutableImage = {
val blur = new GaussianBlurFilter(2)
val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1)
val diffuse = new DiffuseFilter(radius.toFloat)
val diffuse = new DiffuseFilter(2)
blur.apply(image)
diffuse.apply(image)
smear.apply(image)
@ -87,10 +65,9 @@ class FilterType1 extends FilterType {
}
class FilterType2 extends FilterType {
override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = {
val radius = if (hardLevel) 2f else 1f
override def applyFilter(image: ImmutableImage): ImmutableImage = {
val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1)
val diffuse = new DiffuseFilter(radius)
val diffuse = new DiffuseFilter(1)
val ripple = new RippleFilter(com.sksamuel.scrimage.filter.RippleType.Noise, 1, 1, 0.005.toFloat, 0.005.toFloat)
diffuse.apply(image)
ripple.apply(image)

View File

@ -10,7 +10,6 @@ import java.awt.Color
import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import java.util.{List => JavaList, Map => JavaMap}
import lc.misc.PngImageWriter
class LabelCaptcha extends ChallengeProvider {
private var knownFiles = new File("known").list.toList
@ -31,16 +30,13 @@ class LabelCaptcha extends ChallengeProvider {
def supportedParameters(): JavaMap[String, JavaList[String]] = {
JavaMap.of(
"supportedLevels",
JavaList.of("hard"),
"supportedMedia",
JavaList.of("image/png"),
"supportedInputType",
JavaList.of("text")
"supportedLevels", JavaList.of("hard"),
"supportedMedia", JavaList.of("image/png"),
"supportedInputType", JavaList.of("text")
)
}
def returnChallenge(level: String, size: String): Challenge =
def returnChallenge(): Challenge =
synchronized {
val r = scala.util.Random.nextInt(knownFiles.length)
val s = scala.util.Random.nextInt(unknownFiles.length)
@ -53,7 +49,7 @@ class LabelCaptcha extends ChallengeProvider {
val token = encrypt(knownImageFile + "," + unknownImageFile)
val baos = new ByteArrayOutputStream()
PngImageWriter.write(baos, mergedImage);
ImageIO.write(mergedImage, "png", baos)
new Challenge(baos.toByteArray(), "image/png", token)
}

View File

@ -11,7 +11,6 @@ import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import lc.misc.GifSequenceWriter
import java.util.{List => JavaList, Map => JavaMap}
import lc.misc.HelperFunctions
class Drop {
var x = 0
@ -25,6 +24,8 @@ class Drop {
}
class RainDropsCP extends ChallengeProvider {
private val alphabet = "abcdefghijklmnopqrstuvwxyz"
private val n = 6
private val bgColor = new Color(200, 200, 200)
private val textColor = new Color(208, 208, 218)
private val textHighlightColor = new Color(100, 100, 125)
@ -37,12 +38,9 @@ class RainDropsCP extends ChallengeProvider {
def supportedParameters(): JavaMap[String, JavaList[String]] = {
JavaMap.of(
"supportedLevels",
JavaList.of("medium", "easy"),
"supportedMedia",
JavaList.of("image/gif"),
"supportedInputType",
JavaList.of("text")
"supportedLevels", JavaList.of("medium", "easy"),
"supportedMedia", JavaList.of("image/gif"),
"supportedInputType", JavaList.of("text")
)
}
@ -55,13 +53,11 @@ class RainDropsCP extends ChallengeProvider {
})
}
def returnChallenge(level: String, size: String): Challenge = {
def returnChallenge(): Challenge = {
val r = new scala.util.Random
val n = if (level == "easy") 4 else 6
val secret = HelperFunctions.randomString(n, HelperFunctions.safeAlphaNum)
val size2D = HelperFunctions.parseSize2D(size)
val width = size2D(0)
val height = size2D(1)
val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString
val width = 450
val height = 100
val imgType = BufferedImage.TYPE_INT_RGB
val xOffset = 2 + r.nextInt(3)
val xBias = (height / 10) - 2
@ -81,8 +77,7 @@ class RainDropsCP extends ChallengeProvider {
xOffset
)
val fontHeight = (height * 0.5f).toInt
val baseFont = new Font(Font.MONOSPACED, Font.BOLD, fontHeight)
val baseFont = new Font(Font.MONOSPACED, Font.BOLD, 80)
val attributes = new java.util.HashMap[TextAttribute, Object]()
attributes.put(TextAttribute.TRACKING, Double.box(0.2))
attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_EXTRABOLD)
@ -119,22 +114,17 @@ class RainDropsCP extends ChallengeProvider {
}
}
g.setFont(spacedFont)
val textWidth = g.getFontMetrics().stringWidth(secret)
val scaleX = if (textWidth > width) width / textWidth.toDouble else 1.0d
g.scale(scaleX, 1)
// center the text
val textX = if (textWidth > width) 0 else ((width - textWidth) / 2)
g.setFont(spacedFont)
val textWidth = g.getFontMetrics().charsWidth(secret.toCharArray, 0, secret.toCharArray.length)
val textX = (width - textWidth) / 2
// this will be overlapped by the following text to show the top outline because of the offset
val yOffset = (fontHeight * 0.01).ceil.toInt
// paint the top outline
g.setColor(textHighlightColor)
g.drawString(secret, textX, (fontHeight * 1.1).toInt - yOffset)
g.drawString(secret, textX, 69)
// paint the text
g.setColor(textColor)
g.drawString(secret, textX, (fontHeight * 1.1).toInt)
g.drawString(secret, textX, 70)
g.dispose()
writer.writeToSequence(canvas)

View File

@ -1,12 +1,15 @@
package lc.core
import lc.captchas.interfaces.{Challenge, ChallengeProvider}
import lc.database.Statements
import java.io.ByteArrayInputStream
import java.sql.{Blob, ResultSet}
import java.sql.ResultSet
import java.util.UUID
import java.io.ByteArrayInputStream
import lc.database.Statements
import lc.core.CaptchaProviders
import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import java.sql.Blob
class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
object Captcha {
def getCaptcha(id: Id): Either[Error, Image] = {
val blob = getImage(id.id)
@ -34,19 +37,17 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
}
def generateChallenge(param: Parameters): Option[Int] = {
try {
captchaProviders.getProvider(param).flatMap { provider =>
val providerId = provider.getId()
val challenge = provider.returnChallenge(param.level, param.size)
val provider = CaptchaProviders.getProvider(param)
provider match {
case Some(value) => {
val providerId = value.getId()
val challenge = value.returnChallenge()
val blob = new ByteArrayInputStream(challenge.content)
val token = insertCaptcha(provider, challenge, providerId, param, blob)
val token = insertCaptcha(value, challenge, providerId, param, blob)
// println("Added new challenge: " + token.toString)
token.map(_.toInt)
}
} catch {
case e: Exception =>
e.printStackTrace()
None
case None => None
}
}
@ -64,8 +65,7 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
insertPstmt.setString(4, challenge.contentType)
insertPstmt.setString(5, param.level)
insertPstmt.setString(6, param.input_type)
insertPstmt.setString(7, param.size)
insertPstmt.setBlob(8, blob)
insertPstmt.setBlob(7, blob)
insertPstmt.executeUpdate()
val rs: ResultSet = insertPstmt.getGeneratedKeys()
if (rs.next()) {
@ -75,9 +75,9 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
}
}
val allowedInputType = config.allowedInputType
val allowedLevels = config.allowedLevels
val allowedMedia = config.allowedMedia
val allowedInputType = Config.allowedInputType
val allowedLevels = Config.allowedLevels
val allowedMedia = Config.allowedMedia
private def validateParam(param: Parameters): Array[String] = {
var invalid_params = Array[String]()
@ -108,37 +108,16 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
}
}
def getCount(param: Parameters): Option[Int] = {
val countPstmt = Statements.tlStmts.get.countForParameterPstmt
countPstmt.setString(1, param.level)
countPstmt.setString(2, param.media)
countPstmt.setString(3, param.input_type)
countPstmt.setString(4, param.size.toString())
val rs = countPstmt.executeQuery()
if (rs.next()) {
Some(rs.getInt("count"))
} else {
None
}
}
private def getToken(param: Parameters): Option[Int] = {
val count = getCount(param).getOrElse(0)
if (count == 0) {
None
val tokenPstmt = Statements.tlStmts.get.tokenPstmt
tokenPstmt.setString(1, param.level)
tokenPstmt.setString(2, param.media)
tokenPstmt.setString(3, param.input_type)
val rs = tokenPstmt.executeQuery()
if (rs.next()) {
Some(rs.getInt("token"))
} else {
val tokenPstmt = Statements.tlStmts.get.tokenPstmt
tokenPstmt.setString(1, param.level)
tokenPstmt.setString(2, param.media)
tokenPstmt.setString(3, param.input_type)
tokenPstmt.setString(4, param.size)
tokenPstmt.setInt(5, count)
val rs = tokenPstmt.executeQuery()
if (rs.next()) {
Some(rs.getInt("token"))
} else {
None
}
None
}
}
@ -163,7 +142,7 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
case None => Right(Success(ResultEnum.EXPIRED.toString))
case Some(value) => {
val (provider, secret) = value
val check = captchaProviders.getProviderById(provider).checkAnswer(secret, answer.answer)
val check = CaptchaProviders.getProviderById(provider).checkAnswer(secret, answer.answer)
deleteCaptcha(answer.id)
val result = if (check) ResultEnum.TRUE.toString else ResultEnum.FALSE.toString
Right(Success(result))
@ -173,7 +152,7 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
private def getSecret(id: String): Option[(String, String)] = {
val selectPstmt = Statements.tlStmts.get.selectPstmt
selectPstmt.setInt(1, config.captchaExpiryTimeLimit)
selectPstmt.setInt(1, Config.captchaExpiryTimeLimit)
selectPstmt.setString(2, id)
val rs: ResultSet = selectPstmt.executeQuery()
if (rs.first()) {

View File

@ -10,7 +10,7 @@ object ParametersEnum extends Enumeration {
val ALLOWEDLEVELS: Value = Value("allowedLevels")
val ALLOWEDMEDIA: Value = Value("allowedMedia")
val ALLOWEDINPUTTYPE: Value = Value("allowedInputType")
val ALLOWEDSIZES: Value = Value("allowedSizes")
}
object AttributesEnum extends Enumeration {
@ -19,14 +19,10 @@ object AttributesEnum extends Enumeration {
val NAME: Value = Value("name")
val RANDOM_SEED: Value = Value("randomSeed")
val PORT: Value = Value("port")
val ADDRESS: Value = Value("address")
val CAPTCHA_EXPIRY_TIME_LIMIT: Value = Value("captchaExpiryTimeLimit")
val BUFFER_COUNT: Value = Value("bufferCount")
val THROTTLE: Value = Value("throttle")
val THREAD_DELAY: Value = Value("threadDelay")
val PLAYGROUND_ENABLED: Value = Value("playgroundEnabled")
val CORS_HEADER: Value = Value("corsHeader")
val CONFIG: Value = Value("config")
val MAX_ATTEMPTS_RATIO: Value = Value("maxAttemptsRatio")
}
object ResultEnum extends Enumeration {

View File

@ -1,29 +1,30 @@
package lc.core
import lc.captchas.*
import lc.captchas._
import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import scala.collection.mutable.Map
import lc.misc.HelperFunctions
class CaptchaProviders(config: Config) {
object CaptchaProviders {
private val providers = Map(
"FilterChallenge" -> new FilterChallenge,
// "FontFunCaptcha" -> new FontFunCaptcha,
//"FontFunCaptcha" -> new FontFunCaptcha,
"PoppingCharactersCaptcha" -> new PoppingCharactersCaptcha,
"ShadowTextCaptcha" -> new ShadowTextCaptcha,
"RainDropsCaptcha" -> new RainDropsCP,
"DebugCaptcha" -> new DebugCaptcha
// "LabelCaptcha" -> new LabelCaptcha
//"LabelCaptcha" -> new LabelCaptcha
)
def generateChallengeSamples(): Map[String, Challenge] = {
providers.map { case (key, provider) =>
(key, provider.returnChallenge("easy", "350x100"))
providers.map {
case (key, provider) =>
(key, provider.returnChallenge())
}
}
private val captchaConfig = config.captchaConfig
private val config = Config.captchaConfig
def getProviderById(id: String): ChallengeProvider = {
return providers(id)
@ -31,11 +32,10 @@ class CaptchaProviders(config: Config) {
private def filterProviderByParam(param: Parameters): Iterable[(String, String)] = {
val configFilter = for {
configValue <- captchaConfig
configValue <- config
if configValue.allowedLevels.contains(param.level)
if configValue.allowedMedia.contains(param.media)
if configValue.allowedInputType.contains(param.input_type)
if configValue.allowedSizes.contains(param.size)
} yield (configValue.name, configValue.config)
val providerFilter = for {
@ -51,7 +51,7 @@ class CaptchaProviders(config: Config) {
def getProvider(param: Parameters): Option[ChallengeProvider] = {
val providerConfig = filterProviderByParam(param).toList
if (providerConfig.nonEmpty) {
if (providerConfig.length > 0) {
val randomIndex = HelperFunctions.randomNumber(providerConfig.length)
val providerIndex = providerConfig(randomIndex)._1
val selectedProvider = providers(providerIndex)

View File

@ -4,17 +4,15 @@ import scala.io.Source.fromFile
import org.json4s.{DefaultFormats, JValue, JObject, JField, JString}
import org.json4s.jackson.JsonMethods.{parse, render, pretty}
import org.json4s.JsonDSL._
import org.json4s.StringInput
import org.json4s.jvalue2monadic
import org.json4s.jvalue2extractable
import java.io.{FileNotFoundException, File, PrintWriter}
import java.{util => ju}
import lc.misc.HelperFunctions
class Config(configFilePath: String) {
object Config {
import Config.formats
implicit val formats: DefaultFormats.type = DefaultFormats
private val configFilePath = "data/config.json"
private val configString =
try {
val configFile = fromFile(configFilePath)
@ -24,12 +22,7 @@ class Config(configFilePath: String) {
} catch {
case _: FileNotFoundException => {
val configFileContent = getDefaultConfig()
val file = if (new File(configFilePath).isDirectory) {
new File(configFilePath.concat("/config.json"))
} else {
new File(configFilePath)
}
val configFile = new PrintWriter(file)
val configFile = new PrintWriter(new File(configFilePath))
configFile.write(configFileContent)
configFile.close
configFileContent
@ -41,21 +34,16 @@ class Config(configFilePath: String) {
}
private val configJson = parse(configString)
private val configFields: ConfigField = configJson.extract[ConfigField]
val port: Int = configFields.portInt.getOrElse(8888)
val address: String = configFields.address.getOrElse("0.0.0.0")
val bufferCount: Int = configFields.bufferCountInt.getOrElse(1000)
val seed: Int = configFields.seedInt.getOrElse(375264328)
val captchaExpiryTimeLimit: Int = configFields.captchaExpiryTimeLimitInt.getOrElse(5)
val threadDelay: Int = configFields.threadDelayInt.getOrElse(2)
val playgroundEnabled: Boolean = configFields.playgroundEnabledBool.getOrElse(true)
val corsHeader: String = configFields.corsHeader.getOrElse("")
val maxAttempts: Int = Math.max(1, (configFields.maxAttemptsRatioFloat.getOrElse(0.01f) * bufferCount).toInt)
val port: Int = (configJson \ AttributesEnum.PORT.toString).extract[Int]
val throttle: Int = (configJson \ AttributesEnum.THROTTLE.toString).extract[Int]
val seed: Int = (configJson \ AttributesEnum.RANDOM_SEED.toString).extract[Int]
val captchaExpiryTimeLimit: Int = (configJson \ AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString).extract[Int]
val threadDelay: Int = (configJson \ AttributesEnum.THREAD_DELAY.toString).extract[Int]
private val captchaConfigJson = (configJson \ "captchas")
val captchaConfigTransform: JValue = captchaConfigJson transformField { case JField("config", JObject(config)) =>
("config", JString(config.toString))
val captchaConfigTransform: JValue = captchaConfigJson transformField {
case JField("config", JObject(config)) => ("config", JString(config.toString))
}
val captchaConfig: List[CaptchaConfig] = captchaConfigTransform.extract[List[CaptchaConfig]]
val allowedLevels: Set[String] = captchaConfig.flatMap(_.allowedLevels).toSet
@ -68,20 +56,15 @@ class Config(configFilePath: String) {
val defaultConfigMap =
(AttributesEnum.RANDOM_SEED.toString -> new ju.Random().nextInt()) ~
(AttributesEnum.PORT.toString -> 8888) ~
(AttributesEnum.ADDRESS.toString -> "0.0.0.0") ~
(AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString -> 5) ~
(AttributesEnum.BUFFER_COUNT.toString -> 1000) ~
(AttributesEnum.THROTTLE.toString -> 1000) ~
(AttributesEnum.THREAD_DELAY.toString -> 2) ~
(AttributesEnum.PLAYGROUND_ENABLED.toString -> true) ~
(AttributesEnum.CORS_HEADER.toString -> "") ~
(AttributesEnum.MAX_ATTEMPTS_RATIO.toString -> 0.01f) ~
("captchas" -> List(
(
(AttributesEnum.NAME.toString -> "FilterChallenge") ~
(ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject())
),
(
@ -89,7 +72,6 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject())
),
(
@ -97,7 +79,6 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject())
),
(
@ -105,7 +86,6 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject())
)
))
@ -114,7 +94,3 @@ class Config(configFilePath: String) {
}
}
object Config {
implicit val formats: DefaultFormats.type = DefaultFormats
}

View File

@ -5,7 +5,7 @@ import lc.core.Config.formats
trait ByteConvert { def toBytes(): Array[Byte] }
case class Size(height: Int, width: Int)
case class Parameters(level: String, media: String, input_type: String, size: String)
case class Parameters(level: String, media: String, input_type: String, size: Option[Size])
case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } }
case class Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[Byte] = { image } }
case class Answer(answer: String, id: String)
@ -16,32 +16,5 @@ case class CaptchaConfig(
allowedLevels: List[String],
allowedMedia: List[String],
allowedInputType: List[String],
allowedSizes: List[String],
config: String
)
case class ConfigField(
port: Option[Integer],
address: Option[String],
bufferCount: Option[Integer],
seed: Option[Integer],
captchaExpiryTimeLimit: Option[Integer],
threadDelay: Option[Integer],
playgroundEnabled: Option[java.lang.Boolean],
corsHeader: Option[String],
maxAttemptsRatio: Option[java.lang.Float]
) {
lazy val portInt: Option[Int] = mapInt(port)
lazy val bufferCountInt: Option[Int] = mapInt(bufferCount)
lazy val seedInt: Option[Int] = mapInt(seed)
lazy val captchaExpiryTimeLimitInt: Option[Int] = mapInt(captchaExpiryTimeLimit)
lazy val threadDelayInt: Option[Int] = mapInt(threadDelay)
lazy val maxAttemptsRatioFloat: Option[Float] = mapFloat(maxAttemptsRatio)
lazy val playgroundEnabledBool: Option[Boolean] = playgroundEnabled.map(_ || false)
private def mapInt(x: Option[Integer]): Option[Int] = {
x.map(_ + 0)
}
private def mapFloat(x: Option[java.lang.Float]): Option[Float] = {
x.map(_ + 0.0f)
}
}

View File

@ -1,10 +1,9 @@
package lc.database
import java.sql.{Connection, DriverManager, Statement}
import java.sql._
class DBConn() {
val con: Connection =
DriverManager.getConnection("jdbc:h2:./data/H2/captcha3;MAX_COMPACT_TIME=8000;DB_CLOSE_ON_EXIT=FALSE", "sa", "")
val con: Connection = DriverManager.getConnection("jdbc:h2:./data/H2/captcha", "sa", "")
def getStatement(): Statement = {
con.createStatement()

View File

@ -17,7 +17,6 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
"contentType varchar, " +
"contentLevel varchar, " +
"contentInput varchar, " +
"size varchar, " +
"image blob, " +
"attempted int default 0, " +
"PRIMARY KEY(token));" +
@ -38,8 +37,8 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
val insertPstmt: PreparedStatement = dbConn.con.prepareStatement(
"INSERT INTO " +
"challenge(id, secret, provider, contentType, contentLevel, contentInput, size, image) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
"challenge(id, secret, provider, contentType, contentLevel, contentInput, image) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS
)
@ -71,18 +70,6 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
"WHERE token = ?;"
)
val countForParameterPstmt: PreparedStatement = dbConn.con.prepareStatement(
s"""
SELECT count(*) as count
FROM challenge
WHERE attempted < $maxAttempts AND
contentLevel = ? AND
contentType = ? AND
contentInput = ? AND
size = ?
"""
)
val tokenPstmt: PreparedStatement = dbConn.con.prepareStatement(
s"""
SELECT token, attempted
@ -90,11 +77,8 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
WHERE attempted < $maxAttempts AND
contentLevel = ? AND
contentType = ? AND
contentInput = ? AND
size = ?
LIMIT 1
OFFSET FLOOR(RAND()*?)
"""
contentInput = ?
ORDER BY attempted ASC LIMIT 1"""
)
val deleteAnswerPstmt: PreparedStatement = dbConn.con.prepareStatement(
@ -123,14 +107,6 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
"SELECT * FROM mapId"
)
val shutdown: PreparedStatement = dbConn.con.prepareStatement(
"SHUTDOWN"
)
val shutdownCompact: PreparedStatement = dbConn.con.prepareStatement(
"SHUTDOWN COMPACT"
)
}
object Statements {
@ -142,6 +118,6 @@ object Statements {
```
*/
private val dbConn: DBConn = new DBConn()
var maxAttempts: Int = 10
private val maxAttempts = 10
val tlStmts: ThreadLocal[Statements] = ThreadLocal.withInitial(() => new Statements(dbConn, maxAttempts))
}

View File

@ -1,67 +1,53 @@
package lc.server
import org.json4s.jackson.JsonMethods.parse
import org.json4s.jvalue2extractable
import lc.core.CaptchaManager
import lc.core.Captcha
import lc.core.ErrorMessageEnum
import lc.core.{Answer, ByteConvert, Error, Id, Parameters}
import lc.core.{Parameters, Id, Answer, Error, ByteConvert}
import lc.core.Config.formats
import org.limium.picoserve
import org.limium.picoserve.Server.{ByteResponse, ServerBuilder, StringResponse}
import org.limium.picoserve.Server.ByteResponse
import scala.io.Source
import java.net.InetSocketAddress
import java.util
import scala.jdk.CollectionConverters._
import org.limium.picoserve.Server.StringResponse
class Server(
address: String,
port: Int,
captchaManager: CaptchaManager,
playgroundEnabled: Boolean,
corsHeader: String
) {
var headerMap: util.Map[String, util.List[String]] = _
if (corsHeader.nonEmpty) {
headerMap = Map("Access-Control-Allow-Origin" -> List(corsHeader).asJava).asJava
}
val serverBuilder: ServerBuilder = picoserve.Server
class Server(port: Int) {
val server: picoserve.Server = picoserve.Server
.builder()
.address(new InetSocketAddress(address, port))
.port(port)
.backlog(32)
.POST(
"/v2/captcha",
"/v1/captcha",
(request) => {
val json = parse(request.getBodyString())
val param = json.extract[Parameters]
val id = captchaManager.getChallenge(param)
getResponse(id, headerMap)
val id = Captcha.getChallenge(param)
getResponse(id)
}
)
.GET(
"/v2/media",
"/v1/media",
(request) => {
val params = request.getQueryParams()
val result = if (params.containsKey("id")) {
val paramId = params.get("id").get(0)
val id = Id(paramId)
captchaManager.getCaptcha(id)
Captcha.getCaptcha(id)
} else {
Left(Error(ErrorMessageEnum.INVALID_PARAM.toString + "=> id"))
}
getResponse(result, headerMap)
getResponse(result)
}
)
.POST(
"/v2/answer",
"/v1/answer",
(request) => {
val json = parse(request.getBodyString())
val answer = json.extract[Answer]
val result = captchaManager.checkAnswer(answer)
getResponse(result, headerMap)
val result = Captcha.checkAnswer(answer)
getResponse(result)
}
)
if (playgroundEnabled) {
serverBuilder.GET(
.GET(
"/demo/index.html",
(_) => {
val resStream = getClass().getResourceAsStream("/index.html")
@ -69,40 +55,21 @@ class Server(
new StringResponse(200, str)
}
)
serverBuilder.GET(
"/",
(_) => {
val str = """
<html>
<h2>Welcome to LibreCaptcha server</h2>
<h3><a href="/demo/index.html">Link to Demo</a></h3>
<h3>API is served at <b>/v2/</b></h3>
</html>
"""
new StringResponse(200, str)
}
)
println("Playground enabled on /demo/index.html")
}
.build()
val server: picoserve.Server = serverBuilder.build()
private def getResponse(
response: Either[Error, ByteConvert],
responseHeaders: util.Map[String, util.List[String]]
): ByteResponse = {
private def getResponse(response: Either[Error, ByteConvert]): ByteResponse = {
response match {
case Right(value) => {
new ByteResponse(200, value.toBytes(), responseHeaders)
new ByteResponse(200, value.toBytes())
}
case Left(value) => {
new ByteResponse(500, value.toBytes(), responseHeaders)
new ByteResponse(500, value.toBytes())
}
}
}
def start(): Unit = {
println("Starting server on " + address + ":" + port)
println("Starting server on port:" + port)
server.start()
}
}

View File

@ -1,19 +1,14 @@
{
"randomSeed" : 20,
"port" : 8888,
"address" : "0.0.0.0",
"captchaExpiryTimeLimit" : 5,
"bufferCount" : 10,
"throttle" : 10,
"threadDelay" : 2,
"playgroundEnabled" : false,
"corsHeader" : "*",
"maxAttemptsRatio" : 0.01,
"captchas" : [ {
"name" : "DebugCaptcha",
"allowedLevels" : [ "debug" ],
"allowedMedia" : [ "image/png" ],
"allowedInputType" : [ "text" ],
"allowedSizes" : [ "350x100" ],
"config" : { }
}]
}

View File

@ -22,9 +22,9 @@ class QuickStartUser(SequentialTaskSet):
@task
def captcha(self):
captcha_params = {"level":"debug","media":"image/png","input_type":"text", "size":"350x100"}
captcha_params = {"level":"debug","media":"image/png","input_type":"text"}
with self.client.post(path="/v2/captcha", json=captcha_params, name="/captcha", catch_response = True) as resp:
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()
@ -32,7 +32,7 @@ class QuickStartUser(SequentialTaskSet):
if not uuid:
resp.failure("uuid not returned on /captcha endpoint: " + resp.text)
with self.client.get(path="/v2/media?id=%s" % uuid, name="/media", stream=True, catch_response = True) as resp:
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)
@ -41,7 +41,7 @@ class QuickStartUser(SequentialTaskSet):
ocrAnswer = self.solve(uuid, media)
answerBody = {"answer": ocrAnswer,"id": uuid}
with self.client.post(path='/v2/answer', json=answerBody, name="/answer", catch_response=True) as resp:
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:

View File

@ -24,9 +24,9 @@ class QuickStartUser(SequentialTaskSet):
@task
def captcha(self):
# TODO: Iterate over parameters for a more comprehensive test
captcha_params = {"level":"easy","media":"image/png","input_type":"text", "size":"350x100"}
captcha_params = {"level":"easy","media":"image/png","input_type":"text"}
resp = self.client.post(path="/v2/captcha", json=captcha_params, name="/captcha")
resp = self.client.post(path="/v1/captcha", json=captcha_params, name="/captcha")
if resp.status_code != 200:
print("\nError on /captcha endpoint: ")
print(resp)
@ -36,14 +36,14 @@ class QuickStartUser(SequentialTaskSet):
uuid = json.loads(resp.text).get("id")
answerBody = {"answer": "qwer123","id": uuid}
resp = self.client.get(path="/v2/media?id=%s" % uuid, name="/media")
resp = self.client.get(path="/v1/media?id=%s" % uuid, name="/media")
if resp.status_code != 200:
print("\nError on /media endpoint: ")
print(resp)
print(resp.text)
print("----------------END.MEDIA-------------------\n\n")
resp = self.client.post(path='/v2/answer', json=answerBody, name="/answer")
resp = self.client.post(path='/v1/answer', json=answerBody, name="/answer")
if resp.status_code != 200:
print("\nError on /answer endpoint: ")
print(resp)

View File

@ -4,7 +4,7 @@ python3 -m venv testEnv
source ./testEnv/bin/activate
pip install locust
mkdir -p data/
java -jar target/scala-3.6.2/LibreCaptcha.jar &
java -jar target/scala-2.13/LibreCaptcha.jar &
JAVA_PID=$!
sleep 4
@ -22,7 +22,7 @@ echo Run functional test
cp data/config.json data/config.json.bak
cp tests/debug-config.json data/config.json
java -jar target/scala-3.6.2/LibreCaptcha.jar &
java -jar target/scala-2.13/LibreCaptcha.jar &
JAVA_PID=$!
sleep 4