Compare commits
No commits in common. "master" and "fix145" have entirely different histories.
|
|
@ -1,5 +0,0 @@
|
|||
# Scala Steward: Reformat with sbt-java-formatter 0.8.0
|
||||
57ce691a00babb03e0cae03a26fe56d63fc609af
|
||||
|
||||
# Scala Steward: Reformat with scalafmt 3.6.1
|
||||
f2b19baca828a4d88b46bc009aef6d7115e63924
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# If true, Scala Steward will sign off all commits (e.g. `git --signoff`).
|
||||
# Default: false
|
||||
signoffCommits = true
|
||||
|
|
@ -16,7 +16,6 @@ jobs:
|
|||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.11
|
||||
- uses: sbt/setup-sbt@v1
|
||||
- name: Run tests
|
||||
run: sbt test assembly
|
||||
- name: Run linter
|
||||
|
|
|
|||
|
|
@ -54,10 +54,6 @@ jobs:
|
|||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
linux/arm/v7
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version="3.8.3"
|
||||
version="3.4.3"
|
||||
maxColumn = 120
|
||||
runner.dialect = scala3
|
||||
|
|
|
|||
17
Dockerfile
17
Dockerfile
|
|
@ -1,13 +1,12 @@
|
|||
FROM eclipse-temurin:17-jre-jammy AS base-builder
|
||||
ARG SBT_VERSION=1.7.1
|
||||
FROM adoptopenjdk/openjdk16:alpine AS base-builder
|
||||
ARG SBT_VERSION=1.3.13
|
||||
RUN apk add --no-cache bash
|
||||
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
|
||||
ENV PATH=$PATH:${JAVA_HOME}/bin
|
||||
RUN \
|
||||
apt update && \
|
||||
apt install -y wget && \
|
||||
wget -O sbt-$SBT_VERSION.tgz https://github.com/sbt/sbt/releases/download/v$SBT_VERSION/sbt-$SBT_VERSION.tgz && \
|
||||
tar -xzvf sbt-$SBT_VERSION.tgz && \
|
||||
rm sbt-$SBT_VERSION.tgz
|
||||
tar -xzvf sbt-$SBT_VERSION.tgz && \
|
||||
rm sbt-$SBT_VERSION.tgz
|
||||
|
||||
ENV PATH=$PATH:/sbt/bin/
|
||||
|
||||
|
|
@ -23,15 +22,15 @@ FROM sbt-builder as builder
|
|||
COPY src/ src/
|
||||
RUN sbt assembly
|
||||
|
||||
FROM eclipse-temurin:17-jre-jammy AS base-core
|
||||
FROM adoptopenjdk/openjdk16:alpine-jre AS base-core
|
||||
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
|
||||
RUN apt update && apt install -y fonts-dejavu
|
||||
RUN apk add --update ttf-dejavu
|
||||
ENV PATH=$PATH:${JAVA_HOME}/bin
|
||||
|
||||
|
||||
FROM base-core
|
||||
WORKDIR /lc-core
|
||||
COPY --from=builder /build/target/scala-3.6.2/LibreCaptcha.jar .
|
||||
COPY --from=builder /build/target/scala-2.13/LibreCaptcha.jar .
|
||||
RUN mkdir data/
|
||||
|
||||
EXPOSE 8888
|
||||
|
|
|
|||
87
README.md
87
README.md
|
|
@ -2,12 +2,12 @@
|
|||
LibreCaptcha is a framework that allows developers to create their own [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA)s.
|
||||
The framework defines the API for a CAPTCHA generator and takes care of mundane details
|
||||
such as:
|
||||
* An HTTP interface for serving CAPTCHAs
|
||||
* Background workers to pre-compute CAPTCHAs and to store them in a database
|
||||
* Managing secrets for the CAPTCHAs (tokens, expected answers, etc)
|
||||
* Safe re-impressions of CAPTCHA images (by creating unique tokens for every impression)
|
||||
* Garbage collection of stale CAPTCHAs
|
||||
* Sandboxed plugin architecture (TBD)
|
||||
* An HTTP interface for serving CAPTCHAs
|
||||
* Background workers to pre-compute CAPTCHAs and to store them in a database
|
||||
* Managing secrets for the CAPTCHAs (tokens, expected answers, etc)
|
||||
* Safe re-impressions of CAPTCHA images (by creating unique tokens for every impression)
|
||||
* Garbage collection of stale CAPTCHAs
|
||||
* Sandboxed plugin architecture (TBD)
|
||||
|
||||
Some sample CAPTCHA generators are included in the distribution (see below). We will continue adding more samples to the list. For quick
|
||||
deployments the samples themselves might be sufficient. Projects with more resources might want create their own CAPTCHAs
|
||||
|
|
@ -47,25 +47,18 @@ docker-compose up
|
|||
Using `docker`:
|
||||
|
||||
```
|
||||
docker run -p=8888:8888 -v ./lcdata:/lc-core/data librecaptcha/lc-core:2.0
|
||||
docker run -v lcdata:/lc-core/data librecaptcha/lc-core:1.1.0-stable
|
||||
```
|
||||
|
||||
A default `config.json` is automatically created in the mounted volume.
|
||||
|
||||
The above commands should work with `podman` as well, if docker.io registry is pre-configured. Otherwise,
|
||||
you can manually specify the repository like so:
|
||||
|
||||
```
|
||||
podman run -p=8888:8888 -v ./lcdata:/lc-core/data docker.io/librecaptcha/lc-core:2.0
|
||||
```
|
||||
|
||||
## Quick test
|
||||
Open [localhost:8888/demo/index.html](http://localhost:8888/demo/index.html) in browser.
|
||||
|
||||
Alternatively, on the command line, try:
|
||||
|
||||
```
|
||||
> $ curl -d '{"media":"image/png","level":"easy","input_type":"text","size":"350x100"}' localhost:8888/v2/captcha
|
||||
> $ curl -d '{"media":"image/png","level":"easy","input_type":"text"}' localhost:8888/v1/captcha
|
||||
{"id":"3bf928ce-a1e7-4616-b34f-8252d777855d"}
|
||||
|
||||
> $ curl "localhost:8888/v1/media?id=3bf928ce-a1e7-4616-b34f-8252d777855d" -o sample.png
|
||||
|
|
@ -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.
|
||||
|
||||
### - `/v1/captcha`: `POST`
|
||||
- Parameters:
|
||||
- Parameters:
|
||||
- `level`: `String` -
|
||||
The difficulty level of a captcha
|
||||
- easy
|
||||
- medium
|
||||
- hard
|
||||
- easy
|
||||
- medium
|
||||
- hard
|
||||
- `input_type`: `String` -
|
||||
The type of input option for a captcha
|
||||
- text
|
||||
- (More to come)
|
||||
- text
|
||||
- (More to come)
|
||||
- `media`: `String` -
|
||||
The type of media of a captcha
|
||||
- image/png
|
||||
- image/gif
|
||||
- (More to come)
|
||||
- `size`: String -
|
||||
The dimensions of a captcha. It needs to be a string in the format `"widthxheight"` in pixels, and will be matched
|
||||
with the `allowedSizes` config setting. Example: `size: "450x200"` which requests an image of width 450 and height
|
||||
200 pixels.
|
||||
- image/png
|
||||
- image/gif
|
||||
- (More to come)
|
||||
- `size`: `Map` -
|
||||
The dimensions of a captcha (Optional). It needs two more fields nested in this parameter
|
||||
- `height`: `Int`
|
||||
- `width`: `Int`
|
||||
|
||||
- Returns:
|
||||
- Returns:
|
||||
- `id`: `String` - The uuid of the captcha generated
|
||||
|
||||
|
||||
### - `/v1/media`: `GET`
|
||||
- Parameters:
|
||||
- Parameters:
|
||||
- `id`: `String` - The uuid of the captcha
|
||||
|
||||
- Returns:
|
||||
- Returns:
|
||||
- `image`: `Array[Byte]` - The requested media as bytes
|
||||
|
||||
|
||||
### - `/v1/answer`: `POST`
|
||||
- Parameter:
|
||||
- Parameter:
|
||||
- `id`: `String` - The uuid of the captcha that needs to be solved
|
||||
- `answer`: `String` - The answer to the captcha that needs to be validated
|
||||
|
||||
- Returns:
|
||||
- Returns:
|
||||
- `result`: `String` - The result after validation/checking of the answer
|
||||
- True - If the answer is correct
|
||||
- False - If the answer is incorrect
|
||||
- Expired - If the time limit to solve the captcha exceeds
|
||||
- True - If the answer is correct
|
||||
- False - If the answer is incorrect
|
||||
- Expired - If the time limit to solve the captcha exceeds
|
||||
|
||||
|
||||
## Example usage
|
||||
|
|
@ -180,9 +173,9 @@ The service can be accessed using a simple HTTP API.
|
|||
In javascript:
|
||||
|
||||
```js
|
||||
const resp = await fetch("/v2/captcha", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({level: "easy", media: "image/png", "input_type" : "text", size: "350x100"})
|
||||
const resp = await fetch("/v1/captcha", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({level: "easy", media: "image/png", "input_type" : "text"})
|
||||
})
|
||||
|
||||
const respJson = await resp.json();
|
||||
|
|
@ -190,19 +183,19 @@ const respJson = await resp.json();
|
|||
let captchaId = null;
|
||||
|
||||
if (resp.ok) {
|
||||
// The CAPTCHA can be displayed using the data in respJson.
|
||||
console.log(respJson);
|
||||
// Store the id somewhere so that it can be used later for answer verification
|
||||
captchaId = respJson.id;
|
||||
// The CAPTCHA can be displayed using the data in respJson.
|
||||
console.log(respJson);
|
||||
// Store the id somewhere so that it can be used later for answer verification
|
||||
captchaId = respJson.id;
|
||||
} else {
|
||||
console.err(respJson);
|
||||
console.err(respJson);
|
||||
}
|
||||
|
||||
|
||||
// When user submits an answer it can be sent to the server for verification thusly:
|
||||
const resp = await fetch("/v2/answer", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({id: captchaId, answer: "user input"})
|
||||
const resp = await fetch("/v1/answer", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({id: captchaId, answer: "user input"})
|
||||
});
|
||||
const respJson = await resp.json();
|
||||
console.log(respJson.result);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
FROM eclipse-temurin:17-jre-jammy AS base-core
|
||||
FROM adoptopenjdk/openjdk16:alpine-jre AS base-core
|
||||
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
|
||||
RUN apt update && apt install -y fonts-dejavu
|
||||
RUN apk add --update ttf-dejavu
|
||||
ENV PATH=$PATH:${JAVA_HOME}/bin
|
||||
|
||||
|
||||
FROM base-core
|
||||
RUN mkdir /lc-core
|
||||
COPY target/scala-3.6.2/LibreCaptcha.jar /lc-core
|
||||
COPY target/scala-3.1.1/LibreCaptcha.jar /lc-core
|
||||
WORKDIR /lc-core
|
||||
RUN mkdir data/
|
||||
|
||||
|
|
|
|||
10
build.sbt
10
build.sbt
|
|
@ -2,8 +2,8 @@ lazy val root = (project in file(".")).settings(
|
|||
inThisBuild(
|
||||
List(
|
||||
organization := "com.example",
|
||||
scalaVersion := "3.6.2",
|
||||
version := "0.2.1-snapshot",
|
||||
scalaVersion := "3.1.1",
|
||||
version := "0.1.0-SNAPSHOT",
|
||||
semanticdbEnabled := true,
|
||||
semanticdbVersion := scalafixSemanticdb.revision
|
||||
|
||||
|
|
@ -12,9 +12,9 @@ lazy val root = (project in file(".")).settings(
|
|||
)
|
||||
),
|
||||
name := "LibreCaptcha",
|
||||
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.3.0",
|
||||
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.3.0",
|
||||
libraryDependencies += "org.json4s" %% "json4s-jackson" % "4.0.7"
|
||||
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.0.31",
|
||||
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.0.31",
|
||||
libraryDependencies += "org.json4s" %% "json4s-jackson" % "4.0.4"
|
||||
)
|
||||
|
||||
Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" }
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@ version: "3.6"
|
|||
services:
|
||||
lc-core:
|
||||
container_name: "libre-captcha"
|
||||
image: librecaptcha/lc-core:latest
|
||||
# Comment "image" & uncomment "build" if you intend to build from source
|
||||
#build: .
|
||||
image: librecaptcha/lc-core:1.0.0-stable
|
||||
volumes:
|
||||
- "./docker-data:/lc-core/data"
|
||||
ports:
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1 +1 @@
|
|||
sbt.version=1.10.6
|
||||
sbt.version=1.6.2
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0")
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
|
||||
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0")
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0")
|
||||
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.34")
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
|
||||
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.7.0")
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import java.awt.image.BufferedImage;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import lc.captchas.interfaces.Challenge;
|
||||
import lc.captchas.interfaces.ChallengeProvider;
|
||||
import lc.misc.HelperFunctions;
|
||||
import lc.misc.PngImageWriter;
|
||||
import lc.misc.HelperFunctions;
|
||||
|
||||
public class FontFunCaptcha implements ChallengeProvider {
|
||||
|
||||
|
|
@ -58,10 +58,9 @@ public class FontFunCaptcha implements ChallengeProvider {
|
|||
return null;
|
||||
}
|
||||
|
||||
private byte[] fontFun(
|
||||
final int width, final int height, String captchaText, String level, String path) {
|
||||
private byte[] fontFun(String captchaText, String level, String path) {
|
||||
String[] colors = {"#f68787", "#f8a978", "#f1eb9a", "#a4f6a5"};
|
||||
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics2D = img.createGraphics();
|
||||
for (int i = 0; i < captchaText.length(); i++) {
|
||||
Font font = loadCustomFont(level, path);
|
||||
|
|
@ -82,14 +81,10 @@ public class FontFunCaptcha implements ChallengeProvider {
|
|||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public Challenge returnChallenge(String level, String size) {
|
||||
public Challenge returnChallenge() {
|
||||
String secret = HelperFunctions.randomString(7);
|
||||
final int[] size2D = HelperFunctions.parseSize2D(size);
|
||||
final int width = size2D[0];
|
||||
final int height = size2D[1];
|
||||
String path = "./lib/fonts/";
|
||||
return new Challenge(
|
||||
fontFun(width, height, secret, "medium", path), "image/png", secret.toLowerCase());
|
||||
return new Challenge(fontFun(secret, "medium", path), "image/png", secret.toLowerCase());
|
||||
}
|
||||
|
||||
public boolean checkAnswer(String secret, String answer) {
|
||||
|
|
|
|||
|
|
@ -1,51 +1,51 @@
|
|||
package lc.captchas;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.imageio.stream.MemoryCacheImageOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import lc.captchas.interfaces.Challenge;
|
||||
import lc.captchas.interfaces.ChallengeProvider;
|
||||
import lc.misc.GifSequenceWriter;
|
||||
import lc.misc.HelperFunctions;
|
||||
import lc.misc.GifSequenceWriter;
|
||||
|
||||
public class PoppingCharactersCaptcha implements ChallengeProvider {
|
||||
private final Font font = new Font("Arial", Font.ROMAN_BASELINE, 48);
|
||||
private final int width = 250;
|
||||
private final int height = 100;
|
||||
|
||||
private int[] computeOffsets(
|
||||
final Font font, final int width, final int height, final String text) {
|
||||
private Integer[] computeOffsets(final String text) {
|
||||
final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
final var graphics2D = img.createGraphics();
|
||||
final var frc = graphics2D.getFontRenderContext();
|
||||
final var advances = new int[text.length() + 1];
|
||||
final var advances = new LinkedList<Integer>();
|
||||
final var spacing = font.getStringBounds(" ", frc).getWidth() / 3;
|
||||
var currX = 0;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
final var c = text.charAt(i);
|
||||
advances[i] = currX;
|
||||
advances.add(currX);
|
||||
currX += font.getStringBounds(String.valueOf(c), frc).getWidth();
|
||||
currX += spacing;
|
||||
}
|
||||
;
|
||||
advances[text.length()] = currX;
|
||||
};
|
||||
graphics2D.dispose();
|
||||
return advances;
|
||||
return advances.toArray(new Integer[]{});
|
||||
}
|
||||
|
||||
private BufferedImage makeImage(
|
||||
final Font font, final int width, final int height, final Consumer<Graphics2D> f) {
|
||||
private BufferedImage makeImage(final Consumer<Graphics2D> f) {
|
||||
final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
final var graphics2D = img.createGraphics();
|
||||
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
graphics2D.setRenderingHint(
|
||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
graphics2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
graphics2D.setFont(font);
|
||||
f.accept(graphics2D);
|
||||
graphics2D.dispose();
|
||||
|
|
@ -56,49 +56,30 @@ public class PoppingCharactersCaptcha implements ChallengeProvider {
|
|||
return HelperFunctions.randomNumber(-2, +2);
|
||||
}
|
||||
|
||||
private byte[] gifCaptcha(final int width, final int height, final String text) {
|
||||
private byte[] gifCaptcha(final String text) {
|
||||
try {
|
||||
final var fontHeight = (int) (height * 0.5);
|
||||
final Font font = new Font("Arial", Font.ROMAN_BASELINE, fontHeight);
|
||||
final var byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
final var output = new MemoryCacheImageOutputStream(byteArrayOutputStream);
|
||||
final var writer = new GifSequenceWriter(output, 1, 900, true);
|
||||
final var advances = computeOffsets(font, width, height, text);
|
||||
final var expectedWidth = advances[advances.length - 1];
|
||||
final var scale = width / (float) expectedWidth;
|
||||
final var advances = computeOffsets(text);
|
||||
final var prevColor = Color.getHSBColor(0f, 0f, 0.1f);
|
||||
IntStream.range(0, text.length())
|
||||
.forEach(
|
||||
i -> {
|
||||
final var color =
|
||||
Color.getHSBColor(HelperFunctions.randomNumber(0, 100) / 100.0f, 0.6f, 1.0f);
|
||||
final var nextImage =
|
||||
makeImage(
|
||||
font,
|
||||
width,
|
||||
height,
|
||||
(g) -> {
|
||||
g.scale(scale, 1);
|
||||
if (i > 0) {
|
||||
final var prevI = (i - 1) % text.length();
|
||||
g.setColor(prevColor);
|
||||
g.drawString(
|
||||
String.valueOf(text.charAt(prevI)),
|
||||
advances[prevI] + jitter(),
|
||||
fontHeight * 1.1f + jitter());
|
||||
}
|
||||
g.setColor(color);
|
||||
g.drawString(
|
||||
String.valueOf(text.charAt(i)),
|
||||
advances[i] + jitter(),
|
||||
fontHeight * 1.1f + jitter());
|
||||
});
|
||||
try {
|
||||
writer.writeToSequence(nextImage);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
IntStream.range(0, text.length()).forEach(i -> {
|
||||
final var color = Color.getHSBColor(HelperFunctions.randomNumber(0, 100)/100.0f, 0.6f, 1.0f);
|
||||
final var nextImage = makeImage((g) -> {
|
||||
if (i > 0) {
|
||||
final var prevI = (i - 1) % text.length();
|
||||
g.setColor(prevColor);
|
||||
g.drawString(String.valueOf(text.charAt(prevI)), advances[prevI] + jitter(), 45 + jitter());
|
||||
}
|
||||
g.setColor(color);
|
||||
g.drawString(String.valueOf(text.charAt(i)), advances[i] + jitter(), 45 + jitter());
|
||||
});
|
||||
try {
|
||||
writer.writeToSequence(nextImage);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
writer.close();
|
||||
output.close();
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
|
|
@ -119,12 +100,9 @@ public class PoppingCharactersCaptcha implements ChallengeProvider {
|
|||
"supportedInputType", List.of("text"));
|
||||
}
|
||||
|
||||
public Challenge returnChallenge(String level, String size) {
|
||||
public Challenge returnChallenge() {
|
||||
final var secret = HelperFunctions.randomString(6);
|
||||
final int[] size2D = HelperFunctions.parseSize2D(size);
|
||||
final int width = size2D[0];
|
||||
final int height = size2D[1];
|
||||
return new Challenge(gifCaptcha(width, height, secret), "image/gif", secret.toLowerCase());
|
||||
return new Challenge(gifCaptcha(secret), "image/gif", secret.toLowerCase());
|
||||
}
|
||||
|
||||
public boolean checkAnswer(String secret, String answer) {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
package lc.captchas;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.font.TextLayout;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ConvolveOp;
|
||||
import java.awt.image.Kernel;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lc.captchas.interfaces.Challenge;
|
||||
import lc.captchas.interfaces.ChallengeProvider;
|
||||
import java.util.List;
|
||||
|
||||
import lc.misc.HelperFunctions;
|
||||
import lc.misc.PngImageWriter;
|
||||
import lc.captchas.interfaces.Challenge;
|
||||
import lc.captchas.interfaces.ChallengeProvider;
|
||||
|
||||
public class ShadowTextCaptcha implements ChallengeProvider {
|
||||
|
||||
|
|
@ -35,43 +38,32 @@ public class ShadowTextCaptcha implements ChallengeProvider {
|
|||
return answer.toLowerCase().equals(secret);
|
||||
}
|
||||
|
||||
private float[] makeKernel(int size) {
|
||||
final int N = size * size;
|
||||
final float weight = 1.0f / (N);
|
||||
final float[] kernel = new float[N];
|
||||
java.util.Arrays.fill(kernel, weight);
|
||||
return kernel;
|
||||
};
|
||||
|
||||
private byte[] shadowText(final int width, final int height, String text) {
|
||||
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
final int fontHeight = (int) (height * 0.5f);
|
||||
Font font = new Font("Arial", Font.PLAIN, fontHeight);
|
||||
private byte[] shadowText(String text) {
|
||||
BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB);
|
||||
Font font = new Font("Arial", Font.ROMAN_BASELINE, 48);
|
||||
Graphics2D graphics2D = img.createGraphics();
|
||||
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
graphics2D.setRenderingHint(
|
||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
|
||||
TextLayout textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext());
|
||||
HelperFunctions.setRenderingHints(graphics2D);
|
||||
graphics2D.setPaint(Color.WHITE);
|
||||
graphics2D.fillRect(0, 0, width, height);
|
||||
graphics2D.fillRect(0, 0, 350, 100);
|
||||
graphics2D.setPaint(Color.BLACK);
|
||||
graphics2D.setFont(font);
|
||||
final var stringWidth = graphics2D.getFontMetrics().stringWidth(text);
|
||||
final var padding = (stringWidth > width) ? 0 : (width - stringWidth) / 2;
|
||||
final var scaleX = (stringWidth > width) ? width / ((double) stringWidth) : 1d;
|
||||
graphics2D.scale(scaleX, 1d);
|
||||
graphics2D.drawString(text, padding, fontHeight * 1.1f);
|
||||
textLayout.draw(graphics2D, 15, 50);
|
||||
graphics2D.dispose();
|
||||
final int kernelSize = (int) Math.ceil((Math.min(width, height) / 50.0));
|
||||
ConvolveOp op =
|
||||
new ConvolveOp(
|
||||
new Kernel(kernelSize, kernelSize, makeKernel(kernelSize)),
|
||||
ConvolveOp.EDGE_NO_OP,
|
||||
null);
|
||||
float[] kernel = {
|
||||
1f / 9f, 1f / 9f, 1f / 9f,
|
||||
1f / 9f, 1f / 9f, 1f / 9f,
|
||||
1f / 9f, 1f / 9f, 1f / 9f
|
||||
};
|
||||
ConvolveOp op = new ConvolveOp(new Kernel(3, 3, kernel), ConvolveOp.EDGE_NO_OP, null);
|
||||
BufferedImage img2 = op.filter(img, null);
|
||||
Graphics2D g2d = img2.createGraphics();
|
||||
HelperFunctions.setRenderingHints(g2d);
|
||||
g2d.setPaint(Color.WHITE);
|
||||
g2d.scale(scaleX, 1d);
|
||||
g2d.setFont(font);
|
||||
g2d.drawString(text, padding - kernelSize, fontHeight * 1.1f);
|
||||
textLayout.draw(g2d, 13, 50);
|
||||
g2d.dispose();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try {
|
||||
|
|
@ -82,11 +74,8 @@ public class ShadowTextCaptcha implements ChallengeProvider {
|
|||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public Challenge returnChallenge(String level, String size) {
|
||||
public Challenge returnChallenge() {
|
||||
String secret = HelperFunctions.randomString(6);
|
||||
final int[] size2D = HelperFunctions.parseSize2D(size);
|
||||
final int width = size2D[0];
|
||||
final int height = size2D[1];
|
||||
return new Challenge(shadowText(width, height, secret), "image/png", secret.toLowerCase());
|
||||
return new Challenge(shadowText(secret), "image/png", secret.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package lc.captchas.interfaces;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
public interface ChallengeProvider {
|
||||
public String getId();
|
||||
|
||||
public Challenge returnChallenge(String level, String size);
|
||||
public Challenge returnChallenge();
|
||||
|
||||
public boolean checkAnswer(String secret, String answer);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
|
||||
package lc.misc;
|
||||
|
||||
import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.util.Iterator;
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.metadata.*;
|
||||
import javax.imageio.stream.*;
|
||||
import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class GifSequenceWriter {
|
||||
protected ImageWriter gifWriter;
|
||||
|
|
|
|||
|
|
@ -11,14 +11,7 @@ public class HelperFunctions {
|
|||
random.setSeed(seed);
|
||||
}
|
||||
|
||||
public static int[] parseSize2D(final String size) {
|
||||
final String[] fields = size.split("x");
|
||||
final int[] result = {Integer.parseInt(fields[0]), Integer.parseInt(fields[1])};
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void setRenderingHints(Graphics2D g2d) {
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2d.setRenderingHint(
|
||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
g2d.setRenderingHint(
|
||||
|
|
@ -30,8 +23,7 @@ public class HelperFunctions {
|
|||
public static final String safeNumbers = "23456789";
|
||||
public static final String allNumbers = safeNumbers + "10";
|
||||
public static final String specialCharacters = "$#%@&?";
|
||||
public static final String safeAlphaNum = safeAlphabets + safeNumbers;
|
||||
public static final String safeCharacters = safeAlphaNum + specialCharacters;
|
||||
public static final String safeCharacters = safeAlphabets + safeNumbers + specialCharacters;
|
||||
|
||||
public static String randomString(final int n) {
|
||||
return randomString(n, safeCharacters);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package lc.misc;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
|
||||
import javax.imageio.IIOImage;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageTypeSpecifier;
|
||||
|
|
@ -13,6 +13,7 @@ import javax.imageio.metadata.IIOInvalidTreeException;
|
|||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
public class PngImageWriter {
|
||||
|
||||
|
|
@ -25,8 +26,7 @@ public class PngImageWriter {
|
|||
iw.hasNext(); ) {
|
||||
ImageWriter writer = iw.next();
|
||||
ImageWriteParam writeParam = writer.getDefaultWriteParam();
|
||||
ImageTypeSpecifier typeSpecifier =
|
||||
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
|
||||
ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
|
||||
IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam);
|
||||
if (metadata.isReadOnly() || !metadata.isStandardMetadataFormatSupported()) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -3,21 +3,21 @@
|
|||
|
||||
package org.limium.picoserve;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
public final class Server {
|
||||
private final HttpServer server;
|
||||
|
|
|
|||
|
|
@ -20,10 +20,9 @@
|
|||
const levelInput = document.getElementById("levelInput").value
|
||||
const mediaInput = document.getElementById("mediaInput").value
|
||||
const typeInput = document.getElementById("typeInput").value
|
||||
const sizeInput = document.getElementById("sizeInput").value
|
||||
fetch("/v2/captcha", {
|
||||
fetch("/v1/captcha", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput, "size": sizeInput})
|
||||
body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput})
|
||||
}).then(async function(resp) {
|
||||
const respJson = await resp.json()
|
||||
if (resp.ok) {
|
||||
|
|
@ -31,7 +30,7 @@
|
|||
const resultDiv = document.getElementById("result")
|
||||
const result = `
|
||||
<p>Id: ${id}</p>
|
||||
<p><img src="/v2/media?id=${id}" /> </p>
|
||||
<p><img src="/v1/media?id=${id}" /> </p>
|
||||
<input type="text" id="answerInput" />
|
||||
<button onClick="submitAnswer('${id}')">Submit</button>
|
||||
<div id="answerResult" />
|
||||
|
|
@ -44,7 +43,7 @@
|
|||
}
|
||||
async function submitAnswer(id) {
|
||||
const ans = document.getElementById("answerInput").value;
|
||||
const resp = await fetch("/v2/answer", {
|
||||
const resp = await fetch("/v1/answer", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({id: id, answer: ans})
|
||||
})
|
||||
|
|
@ -71,10 +70,6 @@
|
|||
<span>Input Type</span>
|
||||
<input type="text" id="typeInput" value="text" />
|
||||
</div>
|
||||
<div class="inputGroup">
|
||||
<span>Input Size</span>
|
||||
<input type="text" id="sizeInput" value="350x100" />
|
||||
</div>
|
||||
<div class="inputGroup">
|
||||
<button onClick="loadCaptcha()">Get New CAPTCHA</button>
|
||||
</div>
|
||||
|
|
@ -84,4 +79,4 @@
|
|||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
|
@ -25,14 +25,6 @@ object LCFramework {
|
|||
playgroundEnabled = config.playgroundEnabled,
|
||||
corsHeader = config.corsHeader
|
||||
)
|
||||
|
||||
Runtime.getRuntime.addShutdownHook(new Thread {
|
||||
override def run(): Unit = {
|
||||
println("Shutting down gracefully...")
|
||||
backgroundTask.shutdown()
|
||||
}
|
||||
})
|
||||
|
||||
server.start()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,21 +19,17 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
|
|||
challengeGCPstmt.executeUpdate()
|
||||
|
||||
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) {
|
||||
if (!shutdownInProgress) {
|
||||
val countExisting = captchaManager.getCount(param).getOrElse(0)
|
||||
val countRequired = requiredCountPerCombination - countExisting
|
||||
if (countRequired > 0) {
|
||||
val countCreate = Math.min(1.0 + requiredCountPerCombination / 10.0, countRequired).toInt
|
||||
println(s"Creating $countCreate of $countRequired captchas for $param")
|
||||
val countExisting = captchaManager.getCount(param).getOrElse(0)
|
||||
val countRequired = requiredCountPerCombination - countExisting
|
||||
if (countRequired > 0) {
|
||||
val countCreate = Math.min(1.0 + requiredCountPerCombination/10.0, countRequired).toInt
|
||||
println(s"Creating $countCreate of $countRequired captchas for $param")
|
||||
|
||||
for (i <- 0 until countCreate) {
|
||||
if (!shutdownInProgress) {
|
||||
captchaManager.generateChallenge(param)
|
||||
}
|
||||
}
|
||||
for (i <- 0 until countCreate) {
|
||||
captchaManager.generateChallenge(param)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,13 +38,11 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
|
|||
}
|
||||
|
||||
private def allParameterCombinations(): List[Parameters] = {
|
||||
(config.captchaConfig).flatMap { captcha =>
|
||||
(captcha.allowedLevels).flatMap { level =>
|
||||
(captcha.allowedMedia).flatMap { media =>
|
||||
(captcha.allowedInputType).flatMap { inputType =>
|
||||
(captcha.allowedSizes).map { size =>
|
||||
Parameters(level, media, inputType, size)
|
||||
}
|
||||
(config.captchaConfig).flatMap {captcha =>
|
||||
(captcha.allowedLevels).flatMap {level =>
|
||||
(captcha.allowedMedia).flatMap {media =>
|
||||
(captcha.allowedInputType).map {inputType =>
|
||||
Parameters(level, media, inputType, Some(Size(0, 0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -60,31 +54,17 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
|
|||
val level = pickRandom(captcha.allowedLevels)
|
||||
val media = pickRandom(captcha.allowedMedia)
|
||||
val inputType = pickRandom(captcha.allowedInputType)
|
||||
val size = pickRandom(captcha.allowedSizes)
|
||||
|
||||
Parameters(level, media, inputType, size)
|
||||
Parameters(level, media, inputType, Some(Size(0, 0)))
|
||||
}
|
||||
|
||||
private def pickRandom[T](list: List[T]): T = {
|
||||
list(HelperFunctions.randomNumber(list.size))
|
||||
}
|
||||
|
||||
private val ex = new ScheduledThreadPoolExecutor(1)
|
||||
|
||||
def beginThread(delay: Int): Unit = {
|
||||
val ex = new ScheduledThreadPoolExecutor(1)
|
||||
ex.scheduleWithFixedDelay(task, 1, delay, TimeUnit.SECONDS)
|
||||
}
|
||||
|
||||
@volatile var shutdownInProgress = false
|
||||
|
||||
def shutdown(): Unit = {
|
||||
println(" Shutting down background task...")
|
||||
shutdownInProgress = true
|
||||
ex.shutdown()
|
||||
println(" Finished Shutting background task")
|
||||
println(" Shutting down DB...")
|
||||
Statements.tlStmts.get.shutdown.execute()
|
||||
println(" Finished shutting down db")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,14 +45,14 @@ class DebugCaptcha extends ChallengeProvider {
|
|||
matches
|
||||
}
|
||||
|
||||
private def simpleText(width: Int, height: Int, text: String): Array[Byte] = {
|
||||
val img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
|
||||
private def simpleText(text: String): Array[Byte] = {
|
||||
val img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB)
|
||||
val font = new Font("Arial", Font.ROMAN_BASELINE, 56)
|
||||
val graphics2D = img.createGraphics()
|
||||
val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext())
|
||||
HelperFunctions.setRenderingHints(graphics2D)
|
||||
graphics2D.setPaint(Color.WHITE)
|
||||
graphics2D.fillRect(0, 0, width, height)
|
||||
graphics2D.fillRect(0, 0, 350, 100)
|
||||
graphics2D.setPaint(Color.BLACK)
|
||||
textLayout.draw(graphics2D, 15, 50)
|
||||
graphics2D.dispose()
|
||||
|
|
@ -66,11 +66,8 @@ class DebugCaptcha extends ChallengeProvider {
|
|||
baos.toByteArray()
|
||||
}
|
||||
|
||||
def returnChallenge(level: String, size: String): Challenge = {
|
||||
def returnChallenge(): Challenge = {
|
||||
val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets)
|
||||
val size2D = HelperFunctions.parseSize2D(size)
|
||||
val width = size2D(0)
|
||||
val height = size2D(1)
|
||||
new Challenge(simpleText(width, height, secret), "image/png", secret.toLowerCase())
|
||||
new Challenge(simpleText(secret), "image/png", secret.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import lc.captchas.interfaces.Challenge
|
|||
import java.util.{List => JavaList, Map => JavaMap}
|
||||
import java.io.ByteArrayOutputStream
|
||||
import lc.misc.PngImageWriter
|
||||
import lc.misc.HelperFunctions
|
||||
|
||||
class FilterChallenge extends ChallengeProvider {
|
||||
def getId = "FilterChallenge"
|
||||
|
|
@ -30,38 +29,31 @@ class FilterChallenge extends ChallengeProvider {
|
|||
)
|
||||
}
|
||||
|
||||
private val filterTypes = List(new FilterType1, new FilterType2)
|
||||
|
||||
def returnChallenge(level: String, size: String): Challenge = {
|
||||
val mediumLevel = level == "medium"
|
||||
def returnChallenge(): Challenge = {
|
||||
val filterTypes = List(new FilterType1, new FilterType2)
|
||||
val r = new scala.util.Random
|
||||
val characters = if (mediumLevel) HelperFunctions.safeAlphaNum else HelperFunctions.safeCharacters
|
||||
val n = if (mediumLevel) 5 else 7
|
||||
val secret = LazyList.continually(r.nextInt(characters.size)).map(characters).take(n).mkString
|
||||
val size2D = HelperFunctions.parseSize2D(size)
|
||||
val width = size2D(0)
|
||||
val height = size2D(1)
|
||||
val canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
|
||||
val alphabet = "abcdefghijklmnopqrstuvwxyz"
|
||||
val n = 8
|
||||
val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString
|
||||
val canvas = new BufferedImage(225, 50, BufferedImage.TYPE_INT_RGB)
|
||||
val g = canvas.createGraphics()
|
||||
val fontHeight = (height * 0.6).toInt
|
||||
g.setColor(Color.WHITE)
|
||||
g.fillRect(0, 0, canvas.getWidth, canvas.getHeight)
|
||||
g.setColor(Color.BLACK)
|
||||
val font = new Font("Serif", Font.BOLD, fontHeight)
|
||||
g.setFont(font)
|
||||
val stringWidth = g.getFontMetrics().stringWidth(secret)
|
||||
val scaleX = if (stringWidth > width) width / (stringWidth.toDouble) else 1d
|
||||
val margin = if (stringWidth > width) 0 else (width - stringWidth)
|
||||
val xOffset = (margin * r.nextDouble).toInt
|
||||
g.scale(scaleX, 1d)
|
||||
g.drawString(secret, xOffset, fontHeight)
|
||||
g.setFont(new Font("Serif", Font.PLAIN, 30))
|
||||
g.drawString(secret, 5, 30)
|
||||
g.dispose()
|
||||
var image = ImmutableImage.fromAwt(canvas)
|
||||
val s = r.nextInt(2)
|
||||
image = filterTypes(s).applyFilter(image, !mediumLevel)
|
||||
val s = scala.util.Random.nextInt(2)
|
||||
image = filterTypes(s).applyFilter(image)
|
||||
val img = image.awt()
|
||||
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)
|
||||
}
|
||||
def checkAnswer(secret: String, answer: String): Boolean = {
|
||||
|
|
@ -70,15 +62,14 @@ class FilterChallenge extends ChallengeProvider {
|
|||
}
|
||||
|
||||
trait FilterType {
|
||||
def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage
|
||||
def applyFilter(image: ImmutableImage): ImmutableImage
|
||||
}
|
||||
|
||||
class FilterType1 extends FilterType {
|
||||
override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = {
|
||||
val radius = if (hardLevel) 3 else 2
|
||||
val blur = new GaussianBlurFilter(radius)
|
||||
override def applyFilter(image: ImmutableImage): ImmutableImage = {
|
||||
val blur = new GaussianBlurFilter(2)
|
||||
val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1)
|
||||
val diffuse = new DiffuseFilter(radius.toFloat)
|
||||
val diffuse = new DiffuseFilter(2)
|
||||
blur.apply(image)
|
||||
diffuse.apply(image)
|
||||
smear.apply(image)
|
||||
|
|
@ -87,10 +78,9 @@ class FilterType1 extends FilterType {
|
|||
}
|
||||
|
||||
class FilterType2 extends FilterType {
|
||||
override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = {
|
||||
val radius = if (hardLevel) 2f else 1f
|
||||
override def applyFilter(image: ImmutableImage): ImmutableImage = {
|
||||
val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1)
|
||||
val diffuse = new DiffuseFilter(radius)
|
||||
val diffuse = new DiffuseFilter(1)
|
||||
val ripple = new RippleFilter(com.sksamuel.scrimage.filter.RippleType.Noise, 1, 1, 0.005.toFloat, 0.005.toFloat)
|
||||
diffuse.apply(image)
|
||||
ripple.apply(image)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class LabelCaptcha extends ChallengeProvider {
|
|||
)
|
||||
}
|
||||
|
||||
def returnChallenge(level: String, size: String): Challenge =
|
||||
def returnChallenge(): Challenge =
|
||||
synchronized {
|
||||
val r = scala.util.Random.nextInt(knownFiles.length)
|
||||
val s = scala.util.Random.nextInt(unknownFiles.length)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import lc.captchas.interfaces.ChallengeProvider
|
|||
import lc.captchas.interfaces.Challenge
|
||||
import lc.misc.GifSequenceWriter
|
||||
import java.util.{List => JavaList, Map => JavaMap}
|
||||
import lc.misc.HelperFunctions
|
||||
|
||||
class Drop {
|
||||
var x = 0
|
||||
|
|
@ -25,6 +24,8 @@ class Drop {
|
|||
}
|
||||
|
||||
class RainDropsCP extends ChallengeProvider {
|
||||
private val alphabet = "abcdefghijklmnopqrstuvwxyz"
|
||||
private val n = 6
|
||||
private val bgColor = new Color(200, 200, 200)
|
||||
private val textColor = new Color(208, 208, 218)
|
||||
private val textHighlightColor = new Color(100, 100, 125)
|
||||
|
|
@ -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 n = if (level == "easy") 4 else 6
|
||||
val secret = HelperFunctions.randomString(n, HelperFunctions.safeAlphaNum)
|
||||
val size2D = HelperFunctions.parseSize2D(size)
|
||||
val width = size2D(0)
|
||||
val height = size2D(1)
|
||||
val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString
|
||||
val width = 450
|
||||
val height = 100
|
||||
val imgType = BufferedImage.TYPE_INT_RGB
|
||||
val xOffset = 2 + r.nextInt(3)
|
||||
val xBias = (height / 10) - 2
|
||||
|
|
@ -81,8 +80,7 @@ class RainDropsCP extends ChallengeProvider {
|
|||
xOffset
|
||||
)
|
||||
|
||||
val fontHeight = (height * 0.5f).toInt
|
||||
val baseFont = new Font(Font.MONOSPACED, Font.BOLD, fontHeight)
|
||||
val baseFont = new Font(Font.MONOSPACED, Font.BOLD, 80)
|
||||
val attributes = new java.util.HashMap[TextAttribute, Object]()
|
||||
attributes.put(TextAttribute.TRACKING, Double.box(0.2))
|
||||
attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_EXTRABOLD)
|
||||
|
|
@ -119,22 +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
|
||||
val textX = if (textWidth > width) 0 else ((width - textWidth) / 2)
|
||||
g.setFont(spacedFont)
|
||||
val textWidth = g.getFontMetrics().charsWidth(secret.toCharArray, 0, secret.toCharArray.length)
|
||||
val textX = (width - textWidth) / 2
|
||||
|
||||
// this will be overlapped by the following text to show the top outline because of the offset
|
||||
val yOffset = (fontHeight * 0.01).ceil.toInt
|
||||
// paint the top outline
|
||||
g.setColor(textHighlightColor)
|
||||
g.drawString(secret, textX, (fontHeight * 1.1).toInt - yOffset)
|
||||
|
||||
g.drawString(secret, textX, 69)
|
||||
// paint the text
|
||||
g.setColor(textColor)
|
||||
g.drawString(secret, textX, (fontHeight * 1.1).toInt)
|
||||
g.drawString(secret, textX, 70)
|
||||
|
||||
g.dispose()
|
||||
writer.writeToSequence(canvas)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ object ParametersEnum extends Enumeration {
|
|||
val ALLOWEDLEVELS: Value = Value("allowedLevels")
|
||||
val ALLOWEDMEDIA: Value = Value("allowedMedia")
|
||||
val ALLOWEDINPUTTYPE: Value = Value("allowedInputType")
|
||||
val ALLOWEDSIZES: Value = Value("allowedSizes")
|
||||
|
||||
}
|
||||
|
||||
object AttributesEnum extends Enumeration {
|
||||
|
|
@ -21,12 +21,12 @@ object AttributesEnum extends Enumeration {
|
|||
val PORT: Value = Value("port")
|
||||
val ADDRESS: Value = Value("address")
|
||||
val CAPTCHA_EXPIRY_TIME_LIMIT: Value = Value("captchaExpiryTimeLimit")
|
||||
val BUFFER_COUNT: Value = Value("bufferCount")
|
||||
val THROTTLE: Value = Value("throttle")
|
||||
val THREAD_DELAY: Value = Value("threadDelay")
|
||||
val PLAYGROUND_ENABLED: Value = Value("playgroundEnabled")
|
||||
val CORS_HEADER: Value = Value("corsHeader")
|
||||
val CONFIG: Value = Value("config")
|
||||
val MAX_ATTEMPTS_RATIO: Value = Value("maxAttemptsRatio")
|
||||
val MAX_ATTEMPTS: Value = Value("maxAttempts")
|
||||
}
|
||||
|
||||
object ResultEnum extends Enumeration {
|
||||
|
|
|
|||
|
|
@ -34,19 +34,17 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
|
|||
}
|
||||
|
||||
def generateChallenge(param: Parameters): Option[Int] = {
|
||||
try {
|
||||
captchaProviders.getProvider(param).flatMap { provider =>
|
||||
val providerId = provider.getId()
|
||||
val challenge = provider.returnChallenge(param.level, param.size)
|
||||
val provider = captchaProviders.getProvider(param)
|
||||
provider match {
|
||||
case Some(value) => {
|
||||
val providerId = value.getId()
|
||||
val challenge = value.returnChallenge()
|
||||
val blob = new ByteArrayInputStream(challenge.content)
|
||||
val token = insertCaptcha(provider, challenge, providerId, param, blob)
|
||||
val token = insertCaptcha(value, challenge, providerId, param, blob)
|
||||
// println("Added new challenge: " + token.toString)
|
||||
token.map(_.toInt)
|
||||
}
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
e.printStackTrace()
|
||||
None
|
||||
case None => None
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,8 +62,7 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
|
|||
insertPstmt.setString(4, challenge.contentType)
|
||||
insertPstmt.setString(5, param.level)
|
||||
insertPstmt.setString(6, param.input_type)
|
||||
insertPstmt.setString(7, param.size)
|
||||
insertPstmt.setBlob(8, blob)
|
||||
insertPstmt.setBlob(7, blob)
|
||||
insertPstmt.executeUpdate()
|
||||
val rs: ResultSet = insertPstmt.getGeneratedKeys()
|
||||
if (rs.next()) {
|
||||
|
|
@ -113,7 +110,6 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
|
|||
countPstmt.setString(1, param.level)
|
||||
countPstmt.setString(2, param.media)
|
||||
countPstmt.setString(3, param.input_type)
|
||||
countPstmt.setString(4, param.size.toString())
|
||||
val rs = countPstmt.executeQuery()
|
||||
if (rs.next()) {
|
||||
Some(rs.getInt("count"))
|
||||
|
|
@ -131,8 +127,7 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
|
|||
tokenPstmt.setString(1, param.level)
|
||||
tokenPstmt.setString(2, param.media)
|
||||
tokenPstmt.setString(3, param.input_type)
|
||||
tokenPstmt.setString(4, param.size)
|
||||
tokenPstmt.setInt(5, count)
|
||||
tokenPstmt.setInt(4, count)
|
||||
val rs = tokenPstmt.executeQuery()
|
||||
if (rs.next()) {
|
||||
Some(rs.getInt("token"))
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class CaptchaProviders(config: Config) {
|
|||
|
||||
def generateChallengeSamples(): Map[String, Challenge] = {
|
||||
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.allowedMedia.contains(param.media)
|
||||
if configValue.allowedInputType.contains(param.input_type)
|
||||
if configValue.allowedSizes.contains(param.size)
|
||||
} yield (configValue.name, configValue.config)
|
||||
|
||||
val providerFilter = for {
|
||||
|
|
|
|||
|
|
@ -45,13 +45,13 @@ class Config(configFilePath: String) {
|
|||
|
||||
val port: Int = configFields.portInt.getOrElse(8888)
|
||||
val address: String = configFields.address.getOrElse("0.0.0.0")
|
||||
val bufferCount: Int = configFields.bufferCountInt.getOrElse(1000)
|
||||
val throttle: Int = configFields.throttleInt.getOrElse(1000)
|
||||
val seed: Int = configFields.seedInt.getOrElse(375264328)
|
||||
val captchaExpiryTimeLimit: Int = configFields.captchaExpiryTimeLimitInt.getOrElse(5)
|
||||
val threadDelay: Int = configFields.threadDelayInt.getOrElse(2)
|
||||
val playgroundEnabled: Boolean = configFields.playgroundEnabledBool.getOrElse(true)
|
||||
val corsHeader: String = configFields.corsHeader.getOrElse("")
|
||||
val maxAttempts: Int = Math.max(1, (configFields.maxAttemptsRatioFloat.getOrElse(0.01f) * bufferCount).toInt)
|
||||
val maxAttempts: Int = configFields.maxAttemptsInt.getOrElse(10)
|
||||
|
||||
private val captchaConfigJson = (configJson \ "captchas")
|
||||
val captchaConfigTransform: JValue = captchaConfigJson transformField { case JField("config", JObject(config)) =>
|
||||
|
|
@ -70,18 +70,17 @@ class Config(configFilePath: String) {
|
|||
(AttributesEnum.PORT.toString -> 8888) ~
|
||||
(AttributesEnum.ADDRESS.toString -> "0.0.0.0") ~
|
||||
(AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString -> 5) ~
|
||||
(AttributesEnum.BUFFER_COUNT.toString -> 1000) ~
|
||||
(AttributesEnum.THROTTLE.toString -> 1000) ~
|
||||
(AttributesEnum.THREAD_DELAY.toString -> 2) ~
|
||||
(AttributesEnum.PLAYGROUND_ENABLED.toString -> true) ~
|
||||
(AttributesEnum.CORS_HEADER.toString -> "") ~
|
||||
(AttributesEnum.MAX_ATTEMPTS_RATIO.toString -> 0.01f) ~
|
||||
(AttributesEnum.MAX_ATTEMPTS.toString -> 10) ~
|
||||
("captchas" -> List(
|
||||
(
|
||||
(AttributesEnum.NAME.toString -> "FilterChallenge") ~
|
||||
(ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~
|
||||
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
|
||||
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
||||
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
|
||||
(AttributesEnum.CONFIG.toString -> JObject())
|
||||
),
|
||||
(
|
||||
|
|
@ -89,7 +88,6 @@ class Config(configFilePath: String) {
|
|||
(ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~
|
||||
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
|
||||
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
||||
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
|
||||
(AttributesEnum.CONFIG.toString -> JObject())
|
||||
),
|
||||
(
|
||||
|
|
@ -97,7 +95,6 @@ class Config(configFilePath: String) {
|
|||
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~
|
||||
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
|
||||
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
||||
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
|
||||
(AttributesEnum.CONFIG.toString -> JObject())
|
||||
),
|
||||
(
|
||||
|
|
@ -105,7 +102,6 @@ class Config(configFilePath: String) {
|
|||
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~
|
||||
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
|
||||
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
||||
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
|
||||
(AttributesEnum.CONFIG.toString -> JObject())
|
||||
)
|
||||
))
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import lc.core.Config.formats
|
|||
|
||||
trait ByteConvert { def toBytes(): Array[Byte] }
|
||||
case class Size(height: Int, width: Int)
|
||||
case class Parameters(level: String, media: String, input_type: String, size: String)
|
||||
case class Parameters(level: String, media: String, input_type: String, size: Option[Size])
|
||||
case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } }
|
||||
case class Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[Byte] = { image } }
|
||||
case class Answer(answer: String, id: String)
|
||||
|
|
@ -16,32 +16,28 @@ case class CaptchaConfig(
|
|||
allowedLevels: List[String],
|
||||
allowedMedia: List[String],
|
||||
allowedInputType: List[String],
|
||||
allowedSizes: List[String],
|
||||
config: String
|
||||
)
|
||||
case class ConfigField(
|
||||
port: Option[Integer],
|
||||
address: Option[String],
|
||||
bufferCount: Option[Integer],
|
||||
throttle: Option[Integer],
|
||||
seed: Option[Integer],
|
||||
captchaExpiryTimeLimit: Option[Integer],
|
||||
threadDelay: Option[Integer],
|
||||
playgroundEnabled: Option[java.lang.Boolean],
|
||||
corsHeader: Option[String],
|
||||
maxAttemptsRatio: Option[java.lang.Float]
|
||||
maxAttempts: Option[Integer]
|
||||
) {
|
||||
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 captchaExpiryTimeLimitInt: Option[Int] = mapInt(captchaExpiryTimeLimit)
|
||||
lazy val threadDelayInt: Option[Int] = mapInt(threadDelay)
|
||||
lazy val maxAttemptsRatioFloat: Option[Float] = mapFloat(maxAttemptsRatio)
|
||||
lazy val playgroundEnabledBool: Option[Boolean] = playgroundEnabled.map(_ || false)
|
||||
lazy val maxAttemptsInt: Option[Int] = mapInt(maxAttempts)
|
||||
lazy val playgroundEnabledBool: Option[Boolean] = playgroundEnabled.map(_ || true)
|
||||
|
||||
private def mapInt(x: Option[Integer]): Option[Int] = {
|
||||
x.map(_ + 0)
|
||||
}
|
||||
private def mapFloat(x: Option[java.lang.Float]): Option[Float] = {
|
||||
x.map(_ + 0.0f)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ package lc.database
|
|||
import java.sql.{Connection, DriverManager, Statement}
|
||||
|
||||
class DBConn() {
|
||||
val con: Connection =
|
||||
DriverManager.getConnection("jdbc:h2:./data/H2/captcha3;MAX_COMPACT_TIME=8000;DB_CLOSE_ON_EXIT=FALSE", "sa", "")
|
||||
val con: Connection = DriverManager.getConnection("jdbc:h2:./data/H2/captcha2", "sa", "")
|
||||
|
||||
def getStatement(): Statement = {
|
||||
con.createStatement()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
|
|||
"contentType varchar, " +
|
||||
"contentLevel varchar, " +
|
||||
"contentInput varchar, " +
|
||||
"size varchar, " +
|
||||
"image blob, " +
|
||||
"attempted int default 0, " +
|
||||
"PRIMARY KEY(token));" +
|
||||
|
|
@ -38,8 +37,8 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
|
|||
|
||||
val insertPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
||||
"INSERT INTO " +
|
||||
"challenge(id, secret, provider, contentType, contentLevel, contentInput, size, image) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"challenge(id, secret, provider, contentType, contentLevel, contentInput, image) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
Statement.RETURN_GENERATED_KEYS
|
||||
)
|
||||
|
||||
|
|
@ -78,8 +77,7 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
|
|||
WHERE attempted < $maxAttempts AND
|
||||
contentLevel = ? AND
|
||||
contentType = ? AND
|
||||
contentInput = ? AND
|
||||
size = ?
|
||||
contentInput = ?
|
||||
"""
|
||||
)
|
||||
|
||||
|
|
@ -90,8 +88,7 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
|
|||
WHERE attempted < $maxAttempts AND
|
||||
contentLevel = ? AND
|
||||
contentType = ? AND
|
||||
contentInput = ? AND
|
||||
size = ?
|
||||
contentInput = ?
|
||||
LIMIT 1
|
||||
OFFSET FLOOR(RAND()*?)
|
||||
"""
|
||||
|
|
@ -123,14 +120,6 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
|
|||
"SELECT * FROM mapId"
|
||||
)
|
||||
|
||||
val shutdown: PreparedStatement = dbConn.con.prepareStatement(
|
||||
"SHUTDOWN"
|
||||
)
|
||||
|
||||
val shutdownCompact: PreparedStatement = dbConn.con.prepareStatement(
|
||||
"SHUTDOWN COMPACT"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
object Statements {
|
||||
|
|
|
|||
|
|
@ -13,13 +13,7 @@ import java.net.InetSocketAddress
|
|||
import java.util
|
||||
import scala.jdk.CollectionConverters._
|
||||
|
||||
class Server(
|
||||
address: String,
|
||||
port: Int,
|
||||
captchaManager: CaptchaManager,
|
||||
playgroundEnabled: Boolean,
|
||||
corsHeader: String
|
||||
) {
|
||||
class Server(address: String, port: Int, captchaManager: CaptchaManager, playgroundEnabled: Boolean, corsHeader: String) {
|
||||
var headerMap: util.Map[String, util.List[String]] = _
|
||||
if (corsHeader.nonEmpty) {
|
||||
headerMap = Map("Access-Control-Allow-Origin" -> List(corsHeader).asJava).asJava
|
||||
|
|
@ -29,7 +23,7 @@ class Server(
|
|||
.address(new InetSocketAddress(address, port))
|
||||
.backlog(32)
|
||||
.POST(
|
||||
"/v2/captcha",
|
||||
"/v1/captcha",
|
||||
(request) => {
|
||||
val json = parse(request.getBodyString())
|
||||
val param = json.extract[Parameters]
|
||||
|
|
@ -38,7 +32,7 @@ class Server(
|
|||
}
|
||||
)
|
||||
.GET(
|
||||
"/v2/media",
|
||||
"/v1/media",
|
||||
(request) => {
|
||||
val params = request.getQueryParams()
|
||||
val result = if (params.containsKey("id")) {
|
||||
|
|
@ -52,7 +46,7 @@ class Server(
|
|||
}
|
||||
)
|
||||
.POST(
|
||||
"/v2/answer",
|
||||
"/v1/answer",
|
||||
(request) => {
|
||||
val json = parse(request.getBodyString())
|
||||
val answer = json.extract[Answer]
|
||||
|
|
@ -69,20 +63,6 @@ class Server(
|
|||
new StringResponse(200, str)
|
||||
}
|
||||
)
|
||||
serverBuilder.GET(
|
||||
"/",
|
||||
(_) => {
|
||||
val str = """
|
||||
<html>
|
||||
<h2>Welcome to LibreCaptcha server</h2>
|
||||
<h3><a href="/demo/index.html">Link to Demo</a></h3>
|
||||
<h3>API is served at <b>/v2/</b></h3>
|
||||
</html>
|
||||
"""
|
||||
new StringResponse(200, str)
|
||||
}
|
||||
)
|
||||
println("Playground enabled on /demo/index.html")
|
||||
}
|
||||
|
||||
val server: picoserve.Server = serverBuilder.build()
|
||||
|
|
|
|||
|
|
@ -3,17 +3,16 @@
|
|||
"port" : 8888,
|
||||
"address" : "0.0.0.0",
|
||||
"captchaExpiryTimeLimit" : 5,
|
||||
"bufferCount" : 10,
|
||||
"throttle" : 10,
|
||||
"threadDelay" : 2,
|
||||
"playgroundEnabled" : false,
|
||||
"corsHeader" : "*",
|
||||
"maxAttemptsRatio" : 0.01,
|
||||
"maxAttempts" : 20,
|
||||
"captchas" : [ {
|
||||
"name" : "DebugCaptcha",
|
||||
"allowedLevels" : [ "debug" ],
|
||||
"allowedMedia" : [ "image/png" ],
|
||||
"allowedInputType" : [ "text" ],
|
||||
"allowedSizes" : [ "350x100" ],
|
||||
"config" : { }
|
||||
}]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ class QuickStartUser(SequentialTaskSet):
|
|||
|
||||
@task
|
||||
def captcha(self):
|
||||
captcha_params = {"level":"debug","media":"image/png","input_type":"text", "size":"350x100"}
|
||||
captcha_params = {"level":"debug","media":"image/png","input_type":"text"}
|
||||
|
||||
with self.client.post(path="/v2/captcha", json=captcha_params, name="/captcha", catch_response = True) as resp:
|
||||
with self.client.post(path="/v1/captcha", json=captcha_params, name="/captcha", catch_response = True) as resp:
|
||||
if resp.status_code != 200:
|
||||
resp.failure("Status was not 200: " + resp.text)
|
||||
captchaJson = resp.json()
|
||||
|
|
@ -32,7 +32,7 @@ class QuickStartUser(SequentialTaskSet):
|
|||
if not uuid:
|
||||
resp.failure("uuid not returned on /captcha endpoint: " + resp.text)
|
||||
|
||||
with self.client.get(path="/v2/media?id=%s" % uuid, name="/media", stream=True, catch_response = True) as resp:
|
||||
with self.client.get(path="/v1/media?id=%s" % uuid, name="/media", stream=True, catch_response = True) as resp:
|
||||
if resp.status_code != 200:
|
||||
resp.failure("Status was not 200: " + resp.text)
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ class QuickStartUser(SequentialTaskSet):
|
|||
ocrAnswer = self.solve(uuid, media)
|
||||
|
||||
answerBody = {"answer": ocrAnswer,"id": uuid}
|
||||
with self.client.post(path='/v2/answer', json=answerBody, name="/answer", catch_response=True) as resp:
|
||||
with self.client.post(path='/v1/answer', json=answerBody, name="/answer", catch_response=True) as resp:
|
||||
if resp.status_code != 200:
|
||||
resp.failure("Status was not 200: " + resp.text)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ class QuickStartUser(SequentialTaskSet):
|
|||
@task
|
||||
def captcha(self):
|
||||
# TODO: Iterate over parameters for a more comprehensive test
|
||||
captcha_params = {"level":"easy","media":"image/png","input_type":"text", "size":"350x100"}
|
||||
captcha_params = {"level":"easy","media":"image/png","input_type":"text"}
|
||||
|
||||
resp = self.client.post(path="/v2/captcha", json=captcha_params, name="/captcha")
|
||||
resp = self.client.post(path="/v1/captcha", json=captcha_params, name="/captcha")
|
||||
if resp.status_code != 200:
|
||||
print("\nError on /captcha endpoint: ")
|
||||
print(resp)
|
||||
|
|
@ -36,14 +36,14 @@ class QuickStartUser(SequentialTaskSet):
|
|||
uuid = json.loads(resp.text).get("id")
|
||||
answerBody = {"answer": "qwer123","id": uuid}
|
||||
|
||||
resp = self.client.get(path="/v2/media?id=%s" % uuid, name="/media")
|
||||
resp = self.client.get(path="/v1/media?id=%s" % uuid, name="/media")
|
||||
if resp.status_code != 200:
|
||||
print("\nError on /media endpoint: ")
|
||||
print(resp)
|
||||
print(resp.text)
|
||||
print("----------------END.MEDIA-------------------\n\n")
|
||||
|
||||
resp = self.client.post(path='/v2/answer', json=answerBody, name="/answer")
|
||||
resp = self.client.post(path='/v1/answer', json=answerBody, name="/answer")
|
||||
if resp.status_code != 200:
|
||||
print("\nError on /answer endpoint: ")
|
||||
print(resp)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ python3 -m venv testEnv
|
|||
source ./testEnv/bin/activate
|
||||
pip install locust
|
||||
mkdir -p data/
|
||||
java -jar target/scala-3.6.2/LibreCaptcha.jar &
|
||||
java -jar target/scala-3.1.1/LibreCaptcha.jar &
|
||||
JAVA_PID=$!
|
||||
sleep 4
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ echo Run functional test
|
|||
cp data/config.json data/config.json.bak
|
||||
cp tests/debug-config.json data/config.json
|
||||
|
||||
java -jar target/scala-3.6.2/LibreCaptcha.jar &
|
||||
java -jar target/scala-3.1.1/LibreCaptcha.jar &
|
||||
JAVA_PID=$!
|
||||
sleep 4
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue