Compare commits

..

No commits in common. "master" and "fix145" have entirely different histories.

40 changed files with 256 additions and 425 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 uses: actions/setup-java@v1
with: with:
java-version: 1.11 java-version: 1.11
- uses: sbt/setup-sbt@v1
- name: Run tests - name: Run tests
run: sbt test assembly run: sbt test assembly
- name: Run linter - name: Run linter

View File

@ -54,10 +54,6 @@ jobs:
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: |
linux/amd64
linux/arm64
linux/arm/v7
- name: Image digest - name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }} run: echo ${{ steps.docker_build.outputs.digest }}

View File

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

View File

@ -1,10 +1,9 @@
FROM eclipse-temurin:17-jre-jammy AS base-builder FROM adoptopenjdk/openjdk16:alpine AS base-builder
ARG SBT_VERSION=1.7.1 ARG SBT_VERSION=1.3.13
RUN apk add --no-cache bash
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/" ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
ENV PATH=$PATH:${JAVA_HOME}/bin ENV PATH=$PATH:${JAVA_HOME}/bin
RUN \ 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 && \ 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 && \ tar -xzvf sbt-$SBT_VERSION.tgz && \
rm sbt-$SBT_VERSION.tgz rm sbt-$SBT_VERSION.tgz
@ -23,15 +22,15 @@ FROM sbt-builder as builder
COPY src/ src/ COPY src/ src/
RUN sbt assembly 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/" 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 ENV PATH=$PATH:${JAVA_HOME}/bin
FROM base-core FROM base-core
WORKDIR /lc-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/ RUN mkdir data/
EXPOSE 8888 EXPOSE 8888

View File

@ -47,25 +47,18 @@ docker-compose up
Using `docker`: 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:1.1.0-stable
``` ```
A default `config.json` is automatically created in the mounted volume. 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 ## Quick test
Open [localhost:8888/demo/index.html](http://localhost:8888/demo/index.html) in browser. Open [localhost:8888/demo/index.html](http://localhost:8888/demo/index.html) in browser.
Alternatively, on the command line, try: 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"} {"id":"3bf928ce-a1e7-4616-b34f-8252d777855d"}
> $ curl "localhost:8888/v1/media?id=3bf928ce-a1e7-4616-b34f-8252d777855d" -o sample.png > $ curl "localhost:8888/v1/media?id=3bf928ce-a1e7-4616-b34f-8252d777855d" -o sample.png
@ -146,10 +139,10 @@ The service can be accessed using a simple HTTP API.
- image/png - image/png
- image/gif - image/gif
- (More to come) - (More to come)
- `size`: String - - `size`: `Map` -
The dimensions of a captcha. It needs to be a string in the format `"widthxheight"` in pixels, and will be matched The dimensions of a captcha (Optional). It needs two more fields nested in this parameter
with the `allowedSizes` config setting. Example: `size: "450x200"` which requests an image of width 450 and height - `height`: `Int`
200 pixels. - `width`: `Int`
- Returns: - Returns:
- `id`: `String` - The uuid of the captcha generated - `id`: `String` - The uuid of the captcha generated
@ -180,9 +173,9 @@ The service can be accessed using a simple HTTP API.
In javascript: In javascript:
```js ```js
const resp = await fetch("/v2/captcha", { const resp = await fetch("/v1/captcha", {
method: 'POST', method: 'POST',
body: JSON.stringify({level: "easy", media: "image/png", "input_type" : "text", size: "350x100"}) body: JSON.stringify({level: "easy", media: "image/png", "input_type" : "text"})
}) })
const respJson = await resp.json(); const respJson = await resp.json();
@ -200,7 +193,7 @@ if (resp.ok) {
// When user submits an answer it can be sent to the server for verification thusly: // When user submits an answer it can be sent to the server for verification thusly:
const resp = await fetch("/v2/answer", { const resp = await fetch("/v1/answer", {
method: 'POST', method: 'POST',
body: JSON.stringify({id: captchaId, answer: "user input"}) body: JSON.stringify({id: captchaId, answer: "user input"})
}); });

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/" 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 ENV PATH=$PATH:${JAVA_HOME}/bin
FROM base-core FROM base-core
RUN mkdir /lc-core RUN mkdir /lc-core
COPY target/scala-3.6.2/LibreCaptcha.jar /lc-core COPY target/scala-3.1.1/LibreCaptcha.jar /lc-core
WORKDIR /lc-core WORKDIR /lc-core
RUN mkdir data/ RUN mkdir data/

View File

@ -2,8 +2,8 @@ lazy val root = (project in file(".")).settings(
inThisBuild( inThisBuild(
List( List(
organization := "com.example", organization := "com.example",
scalaVersion := "3.6.2", scalaVersion := "3.1.1",
version := "0.2.1-snapshot", version := "0.1.0-SNAPSHOT",
semanticdbEnabled := true, semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision semanticdbVersion := scalafixSemanticdb.revision
@ -12,9 +12,9 @@ lazy val root = (project in file(".")).settings(
) )
), ),
name := "LibreCaptcha", name := "LibreCaptcha",
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.3.0", libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.0.31",
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.3.0", libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.0.31",
libraryDependencies += "org.json4s" %% "json4s-jackson" % "4.0.7" libraryDependencies += "org.json4s" %% "json4s-jackson" % "4.0.4"
) )
Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" } Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" }

View File

@ -3,9 +3,7 @@ version: "3.6"
services: services:
lc-core: lc-core:
container_name: "libre-captcha" container_name: "libre-captcha"
image: librecaptcha/lc-core:latest image: librecaptcha/lc-core:1.0.0-stable
# Comment "image" & uncomment "build" if you intend to build from source
#build: .
volumes: volumes:
- "./docker-data:/lc-core/data" - "./docker-data:/lc-core/data"
ports: ports:

View File

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

View File

@ -1,4 +1,4 @@
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.34")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.7.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")

View File

@ -5,12 +5,12 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.List;
import lc.captchas.interfaces.Challenge; import lc.captchas.interfaces.Challenge;
import lc.captchas.interfaces.ChallengeProvider; import lc.captchas.interfaces.ChallengeProvider;
import lc.misc.HelperFunctions;
import lc.misc.PngImageWriter; import lc.misc.PngImageWriter;
import lc.misc.HelperFunctions;
public class FontFunCaptcha implements ChallengeProvider { public class FontFunCaptcha implements ChallengeProvider {
@ -58,10 +58,9 @@ public class FontFunCaptcha implements ChallengeProvider {
return null; return null;
} }
private byte[] fontFun( private byte[] fontFun(String captchaText, String level, String path) {
final int width, final int height, String captchaText, String level, String path) {
String[] colors = {"#f68787", "#f8a978", "#f1eb9a", "#a4f6a5"}; 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(); Graphics2D graphics2D = img.createGraphics();
for (int i = 0; i < captchaText.length(); i++) { for (int i = 0; i < captchaText.length(); i++) {
Font font = loadCustomFont(level, path); Font font = loadCustomFont(level, path);
@ -82,14 +81,10 @@ public class FontFunCaptcha implements ChallengeProvider {
return baos.toByteArray(); return baos.toByteArray();
} }
public Challenge returnChallenge(String level, String size) { public Challenge returnChallenge() {
String secret = HelperFunctions.randomString(7); 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/"; String path = "./lib/fonts/";
return new Challenge( return new Challenge(fontFun(secret, "medium", path), "image/png", secret.toLowerCase());
fontFun(width, height, secret, "medium", path), "image/png", secret.toLowerCase());
} }
public boolean checkAnswer(String secret, String answer) { public boolean checkAnswer(String secret, String answer) {

View File

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

View File

@ -1,18 +1,21 @@
package lc.captchas; package lc.captchas;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Color; import java.awt.Color;
import java.awt.Font; import java.awt.Font;
import java.awt.Graphics2D; import java.awt.font.TextLayout;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp; import java.awt.image.ConvolveOp;
import java.awt.image.Kernel; import java.awt.image.Kernel;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Map; import java.util.Map;
import lc.captchas.interfaces.Challenge; import java.util.List;
import lc.captchas.interfaces.ChallengeProvider;
import lc.misc.HelperFunctions; import lc.misc.HelperFunctions;
import lc.misc.PngImageWriter; import lc.misc.PngImageWriter;
import lc.captchas.interfaces.Challenge;
import lc.captchas.interfaces.ChallengeProvider;
public class ShadowTextCaptcha implements ChallengeProvider { public class ShadowTextCaptcha implements ChallengeProvider {
@ -35,43 +38,32 @@ public class ShadowTextCaptcha implements ChallengeProvider {
return answer.toLowerCase().equals(secret); return answer.toLowerCase().equals(secret);
} }
private float[] makeKernel(int size) { private byte[] shadowText(String text) {
final int N = size * size; BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB);
final float weight = 1.0f / (N); Font font = new Font("Arial", Font.ROMAN_BASELINE, 48);
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);
Graphics2D graphics2D = img.createGraphics(); 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); HelperFunctions.setRenderingHints(graphics2D);
graphics2D.setPaint(Color.WHITE); graphics2D.setPaint(Color.WHITE);
graphics2D.fillRect(0, 0, width, height); graphics2D.fillRect(0, 0, 350, 100);
graphics2D.setPaint(Color.BLACK); graphics2D.setPaint(Color.BLACK);
graphics2D.setFont(font); textLayout.draw(graphics2D, 15, 50);
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);
graphics2D.dispose(); graphics2D.dispose();
final int kernelSize = (int) Math.ceil((Math.min(width, height) / 50.0)); float[] kernel = {
ConvolveOp op = 1f / 9f, 1f / 9f, 1f / 9f,
new ConvolveOp( 1f / 9f, 1f / 9f, 1f / 9f,
new Kernel(kernelSize, kernelSize, makeKernel(kernelSize)), 1f / 9f, 1f / 9f, 1f / 9f
ConvolveOp.EDGE_NO_OP, };
null); ConvolveOp op = new ConvolveOp(new Kernel(3, 3, kernel), ConvolveOp.EDGE_NO_OP, null);
BufferedImage img2 = op.filter(img, null); BufferedImage img2 = op.filter(img, null);
Graphics2D g2d = img2.createGraphics(); Graphics2D g2d = img2.createGraphics();
HelperFunctions.setRenderingHints(g2d); HelperFunctions.setRenderingHints(g2d);
g2d.setPaint(Color.WHITE); g2d.setPaint(Color.WHITE);
g2d.scale(scaleX, 1d); textLayout.draw(g2d, 13, 50);
g2d.setFont(font);
g2d.drawString(text, padding - kernelSize, fontHeight * 1.1f);
g2d.dispose(); g2d.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
try { try {
@ -82,11 +74,8 @@ public class ShadowTextCaptcha implements ChallengeProvider {
return baos.toByteArray(); return baos.toByteArray();
} }
public Challenge returnChallenge(String level, String size) { public Challenge returnChallenge() {
String secret = HelperFunctions.randomString(6); String secret = HelperFunctions.randomString(6);
final int[] size2D = HelperFunctions.parseSize2D(size); return new Challenge(shadowText(secret), "image/png", secret.toLowerCase());
final int width = size2D[0];
final int height = size2D[1];
return new Challenge(shadowText(width, height, secret), "image/png", secret.toLowerCase());
} }
} }

View File

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

View File

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

View File

@ -11,14 +11,7 @@ public class HelperFunctions {
random.setSeed(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) { public static void setRenderingHints(Graphics2D g2d) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint( g2d.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint( g2d.setRenderingHint(
@ -30,8 +23,7 @@ public class HelperFunctions {
public static final String safeNumbers = "23456789"; public static final String safeNumbers = "23456789";
public static final String allNumbers = safeNumbers + "10"; public static final String allNumbers = safeNumbers + "10";
public static final String specialCharacters = "$#%@&?"; public static final String specialCharacters = "$#%@&?";
public static final String safeAlphaNum = safeAlphabets + safeNumbers; public static final String safeCharacters = safeAlphabets + safeNumbers + specialCharacters;
public static final String safeCharacters = safeAlphaNum + specialCharacters;
public static String randomString(final int n) { public static String randomString(final int n) {
return randomString(n, safeCharacters); return randomString(n, safeCharacters);

View File

@ -1,9 +1,9 @@
package lc.misc; package lc.misc;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Iterator; import java.util.Iterator;
import javax.imageio.IIOImage; import javax.imageio.IIOImage;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageTypeSpecifier;
@ -13,6 +13,7 @@ import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
public class PngImageWriter { public class PngImageWriter {
@ -25,8 +26,7 @@ public class PngImageWriter {
iw.hasNext(); ) { iw.hasNext(); ) {
ImageWriter writer = iw.next(); ImageWriter writer = iw.next();
ImageWriteParam writeParam = writer.getDefaultWriteParam(); ImageWriteParam writeParam = writer.getDefaultWriteParam();
ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam); IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam);
if (metadata.isReadOnly() || !metadata.isStandardMetadataFormatSupported()) { if (metadata.isReadOnly() || !metadata.isStandardMetadataFormatSupported()) {
continue; continue;

View File

@ -3,21 +3,21 @@
package org.limium.picoserve; 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.InetSocketAddress;
import java.net.URLDecoder; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.LinkedList;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors; 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 { public final class Server {
private final HttpServer server; private final HttpServer server;

View File

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

View File

@ -25,14 +25,6 @@ object LCFramework {
playgroundEnabled = config.playgroundEnabled, playgroundEnabled = config.playgroundEnabled,
corsHeader = config.corsHeader corsHeader = config.corsHeader
) )
Runtime.getRuntime.addShutdownHook(new Thread {
override def run(): Unit = {
println("Shutting down gracefully...")
backgroundTask.shutdown()
}
})
server.start() server.start()
} }
} }

View File

@ -19,10 +19,9 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
challengeGCPstmt.executeUpdate() challengeGCPstmt.executeUpdate()
val allCombinations = allParameterCombinations() val allCombinations = allParameterCombinations()
val requiredCountPerCombination = Math.max(1, (config.bufferCount * 1.01) / allCombinations.size).toInt val requiredCountPerCombination = Math.max(1, (config.throttle * 1.01) / allCombinations.size).toInt
for (param <- allCombinations) { for (param <- allCombinations) {
if (!shutdownInProgress) {
val countExisting = captchaManager.getCount(param).getOrElse(0) val countExisting = captchaManager.getCount(param).getOrElse(0)
val countRequired = requiredCountPerCombination - countExisting val countRequired = requiredCountPerCombination - countExisting
if (countRequired > 0) { if (countRequired > 0) {
@ -30,13 +29,10 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
println(s"Creating $countCreate of $countRequired captchas for $param") println(s"Creating $countCreate of $countRequired captchas for $param")
for (i <- 0 until countCreate) { for (i <- 0 until countCreate) {
if (!shutdownInProgress) {
captchaManager.generateChallenge(param) captchaManager.generateChallenge(param)
} }
} }
} }
}
}
} catch { case exception: Exception => println(exception) } } catch { case exception: Exception => println(exception) }
} }
} }
@ -45,10 +41,8 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
(config.captchaConfig).flatMap {captcha => (config.captchaConfig).flatMap {captcha =>
(captcha.allowedLevels).flatMap {level => (captcha.allowedLevels).flatMap {level =>
(captcha.allowedMedia).flatMap {media => (captcha.allowedMedia).flatMap {media =>
(captcha.allowedInputType).flatMap { inputType => (captcha.allowedInputType).map {inputType =>
(captcha.allowedSizes).map { size => Parameters(level, media, inputType, Some(Size(0, 0)))
Parameters(level, media, inputType, size)
}
} }
} }
} }
@ -60,31 +54,17 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
val level = pickRandom(captcha.allowedLevels) val level = pickRandom(captcha.allowedLevels)
val media = pickRandom(captcha.allowedMedia) val media = pickRandom(captcha.allowedMedia)
val inputType = pickRandom(captcha.allowedInputType) 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 = { private def pickRandom[T](list: List[T]): T = {
list(HelperFunctions.randomNumber(list.size)) list(HelperFunctions.randomNumber(list.size))
} }
private val ex = new ScheduledThreadPoolExecutor(1)
def beginThread(delay: Int): Unit = { def beginThread(delay: Int): Unit = {
val ex = new ScheduledThreadPoolExecutor(1)
ex.scheduleWithFixedDelay(task, 1, delay, TimeUnit.SECONDS) 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

@ -45,14 +45,14 @@ class DebugCaptcha extends ChallengeProvider {
matches matches
} }
private def simpleText(width: Int, height: Int, text: String): Array[Byte] = { private def simpleText(text: String): Array[Byte] = {
val img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) val img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB)
val font = new Font("Arial", Font.ROMAN_BASELINE, 56) val font = new Font("Arial", Font.ROMAN_BASELINE, 56)
val graphics2D = img.createGraphics() val graphics2D = img.createGraphics()
val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext()) val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext())
HelperFunctions.setRenderingHints(graphics2D) HelperFunctions.setRenderingHints(graphics2D)
graphics2D.setPaint(Color.WHITE) graphics2D.setPaint(Color.WHITE)
graphics2D.fillRect(0, 0, width, height) graphics2D.fillRect(0, 0, 350, 100)
graphics2D.setPaint(Color.BLACK) graphics2D.setPaint(Color.BLACK)
textLayout.draw(graphics2D, 15, 50) textLayout.draw(graphics2D, 15, 50)
graphics2D.dispose() graphics2D.dispose()
@ -66,11 +66,8 @@ class DebugCaptcha extends ChallengeProvider {
baos.toByteArray() baos.toByteArray()
} }
def returnChallenge(level: String, size: String): Challenge = { def returnChallenge(): Challenge = {
val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets) val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets)
val size2D = HelperFunctions.parseSize2D(size) new Challenge(simpleText(secret), "image/png", secret.toLowerCase())
val width = size2D(0)
val height = size2D(1)
new Challenge(simpleText(width, height, secret), "image/png", secret.toLowerCase())
} }
} }

View File

@ -10,7 +10,6 @@ import lc.captchas.interfaces.Challenge
import java.util.{List => JavaList, Map => JavaMap} import java.util.{List => JavaList, Map => JavaMap}
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import lc.misc.PngImageWriter import lc.misc.PngImageWriter
import lc.misc.HelperFunctions
class FilterChallenge extends ChallengeProvider { class FilterChallenge extends ChallengeProvider {
def getId = "FilterChallenge" def getId = "FilterChallenge"
@ -30,38 +29,31 @@ class FilterChallenge extends ChallengeProvider {
) )
} }
private val filterTypes = List(new FilterType1, new FilterType2) def returnChallenge(): Challenge = {
val filterTypes = List(new FilterType1, new FilterType2)
def returnChallenge(level: String, size: String): Challenge = {
val mediumLevel = level == "medium"
val r = new scala.util.Random val r = new scala.util.Random
val characters = if (mediumLevel) HelperFunctions.safeAlphaNum else HelperFunctions.safeCharacters val alphabet = "abcdefghijklmnopqrstuvwxyz"
val n = if (mediumLevel) 5 else 7 val n = 8
val secret = LazyList.continually(r.nextInt(characters.size)).map(characters).take(n).mkString val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString
val size2D = HelperFunctions.parseSize2D(size) val canvas = new BufferedImage(225, 50, BufferedImage.TYPE_INT_RGB)
val width = size2D(0)
val height = size2D(1)
val canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
val g = canvas.createGraphics() val g = canvas.createGraphics()
val fontHeight = (height * 0.6).toInt
g.setColor(Color.WHITE) g.setColor(Color.WHITE)
g.fillRect(0, 0, canvas.getWidth, canvas.getHeight) g.fillRect(0, 0, canvas.getWidth, canvas.getHeight)
g.setColor(Color.BLACK) g.setColor(Color.BLACK)
val font = new Font("Serif", Font.BOLD, fontHeight) g.setFont(new Font("Serif", Font.PLAIN, 30))
g.setFont(font) g.drawString(secret, 5, 30)
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.dispose() g.dispose()
var image = ImmutableImage.fromAwt(canvas) var image = ImmutableImage.fromAwt(canvas)
val s = r.nextInt(2) val s = scala.util.Random.nextInt(2)
image = filterTypes(s).applyFilter(image, !mediumLevel) image = filterTypes(s).applyFilter(image)
val img = image.awt() val img = image.awt()
val baos = new ByteArrayOutputStream() val baos = new ByteArrayOutputStream()
try {
PngImageWriter.write(baos, img); PngImageWriter.write(baos, img);
} catch {
case e: Exception =>
e.printStackTrace()
}
new Challenge(baos.toByteArray, "image/png", secret) new Challenge(baos.toByteArray, "image/png", secret)
} }
def checkAnswer(secret: String, answer: String): Boolean = { def checkAnswer(secret: String, answer: String): Boolean = {
@ -70,15 +62,14 @@ class FilterChallenge extends ChallengeProvider {
} }
trait FilterType { trait FilterType {
def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage def applyFilter(image: ImmutableImage): ImmutableImage
} }
class FilterType1 extends FilterType { class FilterType1 extends FilterType {
override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = { override def applyFilter(image: ImmutableImage): ImmutableImage = {
val radius = if (hardLevel) 3 else 2 val blur = new GaussianBlurFilter(2)
val blur = new GaussianBlurFilter(radius)
val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1) 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) blur.apply(image)
diffuse.apply(image) diffuse.apply(image)
smear.apply(image) smear.apply(image)
@ -87,10 +78,9 @@ class FilterType1 extends FilterType {
} }
class FilterType2 extends FilterType { class FilterType2 extends FilterType {
override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = { override def applyFilter(image: ImmutableImage): ImmutableImage = {
val radius = if (hardLevel) 2f else 1f
val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1) 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) val ripple = new RippleFilter(com.sksamuel.scrimage.filter.RippleType.Noise, 1, 1, 0.005.toFloat, 0.005.toFloat)
diffuse.apply(image) diffuse.apply(image)
ripple.apply(image) ripple.apply(image)

View File

@ -40,7 +40,7 @@ class LabelCaptcha extends ChallengeProvider {
) )
} }
def returnChallenge(level: String, size: String): Challenge = def returnChallenge(): Challenge =
synchronized { synchronized {
val r = scala.util.Random.nextInt(knownFiles.length) val r = scala.util.Random.nextInt(knownFiles.length)
val s = scala.util.Random.nextInt(unknownFiles.length) val s = scala.util.Random.nextInt(unknownFiles.length)

View File

@ -11,7 +11,6 @@ import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge import lc.captchas.interfaces.Challenge
import lc.misc.GifSequenceWriter import lc.misc.GifSequenceWriter
import java.util.{List => JavaList, Map => JavaMap} import java.util.{List => JavaList, Map => JavaMap}
import lc.misc.HelperFunctions
class Drop { class Drop {
var x = 0 var x = 0
@ -25,6 +24,8 @@ class Drop {
} }
class RainDropsCP extends ChallengeProvider { class RainDropsCP extends ChallengeProvider {
private val alphabet = "abcdefghijklmnopqrstuvwxyz"
private val n = 6
private val bgColor = new Color(200, 200, 200) private val bgColor = new Color(200, 200, 200)
private val textColor = new Color(208, 208, 218) private val textColor = new Color(208, 208, 218)
private val textHighlightColor = new Color(100, 100, 125) private val textHighlightColor = new Color(100, 100, 125)
@ -55,13 +56,11 @@ class RainDropsCP extends ChallengeProvider {
}) })
} }
def returnChallenge(level: String, size: String): Challenge = { def returnChallenge(): Challenge = {
val r = new scala.util.Random val r = new scala.util.Random
val n = if (level == "easy") 4 else 6 val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString
val secret = HelperFunctions.randomString(n, HelperFunctions.safeAlphaNum) val width = 450
val size2D = HelperFunctions.parseSize2D(size) val height = 100
val width = size2D(0)
val height = size2D(1)
val imgType = BufferedImage.TYPE_INT_RGB val imgType = BufferedImage.TYPE_INT_RGB
val xOffset = 2 + r.nextInt(3) val xOffset = 2 + r.nextInt(3)
val xBias = (height / 10) - 2 val xBias = (height / 10) - 2
@ -81,8 +80,7 @@ class RainDropsCP extends ChallengeProvider {
xOffset xOffset
) )
val fontHeight = (height * 0.5f).toInt val baseFont = new Font(Font.MONOSPACED, Font.BOLD, 80)
val baseFont = new Font(Font.MONOSPACED, Font.BOLD, fontHeight)
val attributes = new java.util.HashMap[TextAttribute, Object]() val attributes = new java.util.HashMap[TextAttribute, Object]()
attributes.put(TextAttribute.TRACKING, Double.box(0.2)) attributes.put(TextAttribute.TRACKING, Double.box(0.2))
attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_EXTRABOLD) attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_EXTRABOLD)
@ -119,22 +117,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 // 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 // paint the top outline
val yOffset = (fontHeight * 0.01).ceil.toInt
g.setColor(textHighlightColor) g.setColor(textHighlightColor)
g.drawString(secret, textX, (fontHeight * 1.1).toInt - yOffset) g.drawString(secret, textX, 69)
// paint the text // paint the text
g.setColor(textColor) g.setColor(textColor)
g.drawString(secret, textX, (fontHeight * 1.1).toInt) g.drawString(secret, textX, 70)
g.dispose() g.dispose()
writer.writeToSequence(canvas) writer.writeToSequence(canvas)

View File

@ -10,7 +10,7 @@ object ParametersEnum extends Enumeration {
val ALLOWEDLEVELS: Value = Value("allowedLevels") val ALLOWEDLEVELS: Value = Value("allowedLevels")
val ALLOWEDMEDIA: Value = Value("allowedMedia") val ALLOWEDMEDIA: Value = Value("allowedMedia")
val ALLOWEDINPUTTYPE: Value = Value("allowedInputType") val ALLOWEDINPUTTYPE: Value = Value("allowedInputType")
val ALLOWEDSIZES: Value = Value("allowedSizes")
} }
object AttributesEnum extends Enumeration { object AttributesEnum extends Enumeration {
@ -21,12 +21,12 @@ object AttributesEnum extends Enumeration {
val PORT: Value = Value("port") val PORT: Value = Value("port")
val ADDRESS: Value = Value("address") val ADDRESS: Value = Value("address")
val CAPTCHA_EXPIRY_TIME_LIMIT: Value = Value("captchaExpiryTimeLimit") 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 THREAD_DELAY: Value = Value("threadDelay")
val PLAYGROUND_ENABLED: Value = Value("playgroundEnabled") val PLAYGROUND_ENABLED: Value = Value("playgroundEnabled")
val CORS_HEADER: Value = Value("corsHeader") val CORS_HEADER: Value = Value("corsHeader")
val CONFIG: Value = Value("config") val CONFIG: Value = Value("config")
val MAX_ATTEMPTS_RATIO: Value = Value("maxAttemptsRatio") val MAX_ATTEMPTS: Value = Value("maxAttempts")
} }
object ResultEnum extends Enumeration { object ResultEnum extends Enumeration {

View File

@ -34,19 +34,17 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
} }
def generateChallenge(param: Parameters): Option[Int] = { def generateChallenge(param: Parameters): Option[Int] = {
try { val provider = captchaProviders.getProvider(param)
captchaProviders.getProvider(param).flatMap { provider => provider match {
val providerId = provider.getId() case Some(value) => {
val challenge = provider.returnChallenge(param.level, param.size) val providerId = value.getId()
val challenge = value.returnChallenge()
val blob = new ByteArrayInputStream(challenge.content) 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) // println("Added new challenge: " + token.toString)
token.map(_.toInt) token.map(_.toInt)
} }
} catch { case None => None
case e: Exception =>
e.printStackTrace()
None
} }
} }
@ -64,8 +62,7 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
insertPstmt.setString(4, challenge.contentType) insertPstmt.setString(4, challenge.contentType)
insertPstmt.setString(5, param.level) insertPstmt.setString(5, param.level)
insertPstmt.setString(6, param.input_type) insertPstmt.setString(6, param.input_type)
insertPstmt.setString(7, param.size) insertPstmt.setBlob(7, blob)
insertPstmt.setBlob(8, blob)
insertPstmt.executeUpdate() insertPstmt.executeUpdate()
val rs: ResultSet = insertPstmt.getGeneratedKeys() val rs: ResultSet = insertPstmt.getGeneratedKeys()
if (rs.next()) { if (rs.next()) {
@ -113,7 +110,6 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
countPstmt.setString(1, param.level) countPstmt.setString(1, param.level)
countPstmt.setString(2, param.media) countPstmt.setString(2, param.media)
countPstmt.setString(3, param.input_type) countPstmt.setString(3, param.input_type)
countPstmt.setString(4, param.size.toString())
val rs = countPstmt.executeQuery() val rs = countPstmt.executeQuery()
if (rs.next()) { if (rs.next()) {
Some(rs.getInt("count")) Some(rs.getInt("count"))
@ -131,8 +127,7 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
tokenPstmt.setString(1, param.level) tokenPstmt.setString(1, param.level)
tokenPstmt.setString(2, param.media) tokenPstmt.setString(2, param.media)
tokenPstmt.setString(3, param.input_type) tokenPstmt.setString(3, param.input_type)
tokenPstmt.setString(4, param.size) tokenPstmt.setInt(4, count)
tokenPstmt.setInt(5, count)
val rs = tokenPstmt.executeQuery() val rs = tokenPstmt.executeQuery()
if (rs.next()) { if (rs.next()) {
Some(rs.getInt("token")) Some(rs.getInt("token"))

View File

@ -19,7 +19,7 @@ class CaptchaProviders(config: Config) {
def generateChallengeSamples(): Map[String, Challenge] = { def generateChallengeSamples(): Map[String, Challenge] = {
providers.map { case (key, provider) => providers.map { case (key, provider) =>
(key, provider.returnChallenge("easy", "350x100")) (key, provider.returnChallenge())
} }
} }
@ -35,7 +35,6 @@ class CaptchaProviders(config: Config) {
if configValue.allowedLevels.contains(param.level) if configValue.allowedLevels.contains(param.level)
if configValue.allowedMedia.contains(param.media) if configValue.allowedMedia.contains(param.media)
if configValue.allowedInputType.contains(param.input_type) if configValue.allowedInputType.contains(param.input_type)
if configValue.allowedSizes.contains(param.size)
} yield (configValue.name, configValue.config) } yield (configValue.name, configValue.config)
val providerFilter = for { val providerFilter = for {

View File

@ -45,13 +45,13 @@ class Config(configFilePath: String) {
val port: Int = configFields.portInt.getOrElse(8888) val port: Int = configFields.portInt.getOrElse(8888)
val address: String = configFields.address.getOrElse("0.0.0.0") val address: String = configFields.address.getOrElse("0.0.0.0")
val bufferCount: Int = configFields.bufferCountInt.getOrElse(1000) val throttle: Int = configFields.throttleInt.getOrElse(1000)
val seed: Int = configFields.seedInt.getOrElse(375264328) val seed: Int = configFields.seedInt.getOrElse(375264328)
val captchaExpiryTimeLimit: Int = configFields.captchaExpiryTimeLimitInt.getOrElse(5) val captchaExpiryTimeLimit: Int = configFields.captchaExpiryTimeLimitInt.getOrElse(5)
val threadDelay: Int = configFields.threadDelayInt.getOrElse(2) val threadDelay: Int = configFields.threadDelayInt.getOrElse(2)
val playgroundEnabled: Boolean = configFields.playgroundEnabledBool.getOrElse(true) val playgroundEnabled: Boolean = configFields.playgroundEnabledBool.getOrElse(true)
val corsHeader: String = configFields.corsHeader.getOrElse("") val corsHeader: String = configFields.corsHeader.getOrElse("")
val maxAttempts: Int = Math.max(1, (configFields.maxAttemptsRatioFloat.getOrElse(0.01f) * bufferCount).toInt) val maxAttempts: Int = configFields.maxAttemptsInt.getOrElse(10)
private val captchaConfigJson = (configJson \ "captchas") private val captchaConfigJson = (configJson \ "captchas")
val captchaConfigTransform: JValue = captchaConfigJson transformField { case JField("config", JObject(config)) => val captchaConfigTransform: JValue = captchaConfigJson transformField { case JField("config", JObject(config)) =>
@ -70,18 +70,17 @@ class Config(configFilePath: String) {
(AttributesEnum.PORT.toString -> 8888) ~ (AttributesEnum.PORT.toString -> 8888) ~
(AttributesEnum.ADDRESS.toString -> "0.0.0.0") ~ (AttributesEnum.ADDRESS.toString -> "0.0.0.0") ~
(AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString -> 5) ~ (AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString -> 5) ~
(AttributesEnum.BUFFER_COUNT.toString -> 1000) ~ (AttributesEnum.THROTTLE.toString -> 1000) ~
(AttributesEnum.THREAD_DELAY.toString -> 2) ~ (AttributesEnum.THREAD_DELAY.toString -> 2) ~
(AttributesEnum.PLAYGROUND_ENABLED.toString -> true) ~ (AttributesEnum.PLAYGROUND_ENABLED.toString -> true) ~
(AttributesEnum.CORS_HEADER.toString -> "") ~ (AttributesEnum.CORS_HEADER.toString -> "") ~
(AttributesEnum.MAX_ATTEMPTS_RATIO.toString -> 0.01f) ~ (AttributesEnum.MAX_ATTEMPTS.toString -> 10) ~
("captchas" -> List( ("captchas" -> List(
( (
(AttributesEnum.NAME.toString -> "FilterChallenge") ~ (AttributesEnum.NAME.toString -> "FilterChallenge") ~
(ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~ (ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject()) (AttributesEnum.CONFIG.toString -> JObject())
), ),
( (
@ -89,7 +88,6 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~ (ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject()) (AttributesEnum.CONFIG.toString -> JObject())
), ),
( (
@ -97,7 +95,6 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~ (ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject()) (AttributesEnum.CONFIG.toString -> JObject())
), ),
( (
@ -105,7 +102,6 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~ (ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject()) (AttributesEnum.CONFIG.toString -> JObject())
) )
)) ))

View File

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

View File

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

View File

@ -17,7 +17,6 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
"contentType varchar, " + "contentType varchar, " +
"contentLevel varchar, " + "contentLevel varchar, " +
"contentInput varchar, " + "contentInput varchar, " +
"size varchar, " +
"image blob, " + "image blob, " +
"attempted int default 0, " + "attempted int default 0, " +
"PRIMARY KEY(token));" + "PRIMARY KEY(token));" +
@ -38,8 +37,8 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
val insertPstmt: PreparedStatement = dbConn.con.prepareStatement( val insertPstmt: PreparedStatement = dbConn.con.prepareStatement(
"INSERT INTO " + "INSERT INTO " +
"challenge(id, secret, provider, contentType, contentLevel, contentInput, size, image) " + "challenge(id, secret, provider, contentType, contentLevel, contentInput, image) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)", "VALUES (?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS Statement.RETURN_GENERATED_KEYS
) )
@ -78,8 +77,7 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
WHERE attempted < $maxAttempts AND WHERE attempted < $maxAttempts AND
contentLevel = ? AND contentLevel = ? AND
contentType = ? AND contentType = ? AND
contentInput = ? AND contentInput = ?
size = ?
""" """
) )
@ -90,8 +88,7 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
WHERE attempted < $maxAttempts AND WHERE attempted < $maxAttempts AND
contentLevel = ? AND contentLevel = ? AND
contentType = ? AND contentType = ? AND
contentInput = ? AND contentInput = ?
size = ?
LIMIT 1 LIMIT 1
OFFSET FLOOR(RAND()*?) OFFSET FLOOR(RAND()*?)
""" """
@ -123,14 +120,6 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
"SELECT * FROM mapId" "SELECT * FROM mapId"
) )
val shutdown: PreparedStatement = dbConn.con.prepareStatement(
"SHUTDOWN"
)
val shutdownCompact: PreparedStatement = dbConn.con.prepareStatement(
"SHUTDOWN COMPACT"
)
} }
object Statements { object Statements {

View File

@ -13,13 +13,7 @@ import java.net.InetSocketAddress
import java.util import java.util
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
class Server( class Server(address: String, port: Int, captchaManager: CaptchaManager, playgroundEnabled: Boolean, corsHeader: String) {
address: String,
port: Int,
captchaManager: CaptchaManager,
playgroundEnabled: Boolean,
corsHeader: String
) {
var headerMap: util.Map[String, util.List[String]] = _ var headerMap: util.Map[String, util.List[String]] = _
if (corsHeader.nonEmpty) { if (corsHeader.nonEmpty) {
headerMap = Map("Access-Control-Allow-Origin" -> List(corsHeader).asJava).asJava headerMap = Map("Access-Control-Allow-Origin" -> List(corsHeader).asJava).asJava
@ -29,7 +23,7 @@ class Server(
.address(new InetSocketAddress(address, port)) .address(new InetSocketAddress(address, port))
.backlog(32) .backlog(32)
.POST( .POST(
"/v2/captcha", "/v1/captcha",
(request) => { (request) => {
val json = parse(request.getBodyString()) val json = parse(request.getBodyString())
val param = json.extract[Parameters] val param = json.extract[Parameters]
@ -38,7 +32,7 @@ class Server(
} }
) )
.GET( .GET(
"/v2/media", "/v1/media",
(request) => { (request) => {
val params = request.getQueryParams() val params = request.getQueryParams()
val result = if (params.containsKey("id")) { val result = if (params.containsKey("id")) {
@ -52,7 +46,7 @@ class Server(
} }
) )
.POST( .POST(
"/v2/answer", "/v1/answer",
(request) => { (request) => {
val json = parse(request.getBodyString()) val json = parse(request.getBodyString())
val answer = json.extract[Answer] val answer = json.extract[Answer]
@ -69,20 +63,6 @@ class Server(
new StringResponse(200, str) 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")
} }
val server: picoserve.Server = serverBuilder.build() val server: picoserve.Server = serverBuilder.build()

View File

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

View File

@ -22,9 +22,9 @@ class QuickStartUser(SequentialTaskSet):
@task @task
def captcha(self): 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: if resp.status_code != 200:
resp.failure("Status was not 200: " + resp.text) resp.failure("Status was not 200: " + resp.text)
captchaJson = resp.json() captchaJson = resp.json()
@ -32,7 +32,7 @@ class QuickStartUser(SequentialTaskSet):
if not uuid: if not uuid:
resp.failure("uuid not returned on /captcha endpoint: " + resp.text) 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: if resp.status_code != 200:
resp.failure("Status was not 200: " + resp.text) resp.failure("Status was not 200: " + resp.text)
@ -41,7 +41,7 @@ class QuickStartUser(SequentialTaskSet):
ocrAnswer = self.solve(uuid, media) ocrAnswer = self.solve(uuid, media)
answerBody = {"answer": ocrAnswer,"id": uuid} 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: if resp.status_code != 200:
resp.failure("Status was not 200: " + resp.text) resp.failure("Status was not 200: " + resp.text)
else: else:

View File

@ -24,9 +24,9 @@ class QuickStartUser(SequentialTaskSet):
@task @task
def captcha(self): def captcha(self):
# TODO: Iterate over parameters for a more comprehensive test # 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: if resp.status_code != 200:
print("\nError on /captcha endpoint: ") print("\nError on /captcha endpoint: ")
print(resp) print(resp)
@ -36,14 +36,14 @@ class QuickStartUser(SequentialTaskSet):
uuid = json.loads(resp.text).get("id") uuid = json.loads(resp.text).get("id")
answerBody = {"answer": "qwer123","id": uuid} 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: if resp.status_code != 200:
print("\nError on /media endpoint: ") print("\nError on /media endpoint: ")
print(resp) print(resp)
print(resp.text) print(resp.text)
print("----------------END.MEDIA-------------------\n\n") 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: if resp.status_code != 200:
print("\nError on /answer endpoint: ") print("\nError on /answer endpoint: ")
print(resp) print(resp)

View File

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