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,13 +1,12 @@
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
ENV PATH=$PATH:/sbt/bin/ ENV PATH=$PATH:/sbt/bin/
@ -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

@ -2,12 +2,12 @@
LibreCaptcha is a framework that allows developers to create their own [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA)s. 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 The framework defines the API for a CAPTCHA generator and takes care of mundane details
such as: such as:
* An HTTP interface for serving CAPTCHAs * An HTTP interface for serving CAPTCHAs
* Background workers to pre-compute CAPTCHAs and to store them in a database * Background workers to pre-compute CAPTCHAs and to store them in a database
* Managing secrets for the CAPTCHAs (tokens, expected answers, etc) * Managing secrets for the CAPTCHAs (tokens, expected answers, etc)
* Safe re-impressions of CAPTCHA images (by creating unique tokens for every impression) * Safe re-impressions of CAPTCHA images (by creating unique tokens for every impression)
* Garbage collection of stale CAPTCHAs * Garbage collection of stale CAPTCHAs
* Sandboxed plugin architecture (TBD) * 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 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 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`: 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
@ -131,48 +124,48 @@ If a sufficient number of users agree on their answer to the unknown word, it is
The service can be accessed using a simple HTTP API. The service can be accessed using a simple HTTP API.
### - `/v1/captcha`: `POST` ### - `/v1/captcha`: `POST`
- Parameters: - Parameters:
- `level`: `String` - - `level`: `String` -
The difficulty level of a captcha The difficulty level of a captcha
- easy - easy
- medium - medium
- hard - hard
- `input_type`: `String` - - `input_type`: `String` -
The type of input option for a captcha The type of input option for a captcha
- text - text
- (More to come) - (More to come)
- `media`: `String` - - `media`: `String` -
The type of media of a captcha The type of media of a captcha
- 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
### - `/v1/media`: `GET` ### - `/v1/media`: `GET`
- Parameters: - Parameters:
- `id`: `String` - The uuid of the captcha - `id`: `String` - The uuid of the captcha
- Returns: - Returns:
- `image`: `Array[Byte]` - The requested media as bytes - `image`: `Array[Byte]` - The requested media as bytes
### - `/v1/answer`: `POST` ### - `/v1/answer`: `POST`
- Parameter: - Parameter:
- `id`: `String` - The uuid of the captcha that needs to be solved - `id`: `String` - The uuid of the captcha that needs to be solved
- `answer`: `String` - The answer to the captcha that needs to be validated - `answer`: `String` - The answer to the captcha that needs to be validated
- Returns: - Returns:
- `result`: `String` - The result after validation/checking of the answer - `result`: `String` - The result after validation/checking of the answer
- True - If the answer is correct - True - If the answer is correct
- False - If the answer is incorrect - False - If the answer is incorrect
- Expired - If the time limit to solve the captcha exceeds - Expired - If the time limit to solve the captcha exceeds
## Example usage ## Example usage
@ -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();
@ -190,19 +183,19 @@ const respJson = await resp.json();
let captchaId = null; let captchaId = null;
if (resp.ok) { if (resp.ok) {
// The CAPTCHA can be displayed using the data in respJson. // The CAPTCHA can be displayed using the data in respJson.
console.log(respJson); console.log(respJson);
// Store the id somewhere so that it can be used later for answer verification // Store the id somewhere so that it can be used later for answer verification
captchaId = respJson.id; captchaId = respJson.id;
} else { } else {
console.err(respJson); console.err(respJson);
} }
// 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"})
}); });
const respJson = await resp.json(); const respJson = await resp.json();
console.log(respJson.result); console.log(respJson.result);

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,49 +56,30 @@ 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 = if (i > 0) {
Color.getHSBColor(HelperFunctions.randomNumber(0, 100) / 100.0f, 0.6f, 1.0f); final var prevI = (i - 1) % text.length();
final var nextImage = g.setColor(prevColor);
makeImage( g.drawString(String.valueOf(text.charAt(prevI)), advances[prevI] + jitter(), 45 + jitter());
font, }
width, g.setColor(color);
height, g.drawString(String.valueOf(text.charAt(i)), advances[i] + jitter(), 45 + jitter());
(g) -> { });
g.scale(scale, 1); try {
if (i > 0) { writer.writeToSequence(nextImage);
final var prevI = (i - 1) % text.length(); } catch (final IOException e) {
g.setColor(prevColor); e.printStackTrace();
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();
}
});
writer.close(); writer.close();
output.close(); output.close();
return byteArrayOutputStream.toByteArray(); return byteArrayOutputStream.toByteArray();
@ -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,21 +19,17 @@ 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) { val countCreate = Math.min(1.0 + requiredCountPerCombination/10.0, countRequired).toInt
val countCreate = Math.min(1.0 + requiredCountPerCombination / 10.0, countRequired).toInt 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)
}
}
} }
} }
} }
@ -42,13 +38,11 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
} }
private def allParameterCombinations(): List[Parameters] = { private def allParameterCombinations(): List[Parameters] = {
(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()
PngImageWriter.write(baos, img); try {
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