Compare commits

..

No commits in common. "master" and "v2.0.0-beta" have entirely different histories.

25 changed files with 125 additions and 168 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.5.0"
maxColumn = 120 maxColumn = 120
runner.dialect = scala3 runner.dialect = scala3

View File

@ -1,10 +1,9 @@
FROM eclipse-temurin:17-jre-jammy AS base-builder FROM adoptopenjdk/openjdk16:alpine AS base-builder
ARG SBT_VERSION=1.7.1 ARG SBT_VERSION=1.6.2
RUN apk add --no-cache bash
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/" ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
ENV PATH=$PATH:${JAVA_HOME}/bin ENV PATH=$PATH:${JAVA_HOME}/bin
RUN \ RUN \
apt update && \
apt install -y wget && \
wget -O sbt-$SBT_VERSION.tgz https://github.com/sbt/sbt/releases/download/v$SBT_VERSION/sbt-$SBT_VERSION.tgz && \ wget -O sbt-$SBT_VERSION.tgz https://github.com/sbt/sbt/releases/download/v$SBT_VERSION/sbt-$SBT_VERSION.tgz && \
tar -xzvf sbt-$SBT_VERSION.tgz && \ tar -xzvf sbt-$SBT_VERSION.tgz && \
rm sbt-$SBT_VERSION.tgz rm sbt-$SBT_VERSION.tgz
@ -23,15 +22,15 @@ FROM sbt-builder as builder
COPY src/ src/ COPY src/ src/
RUN sbt assembly RUN sbt assembly
FROM eclipse-temurin:17-jre-jammy AS base-core FROM adoptopenjdk/openjdk16:alpine-jre AS base-core
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/" ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
RUN apt update && apt install -y fonts-dejavu RUN apk add --update ttf-dejavu
ENV PATH=$PATH:${JAVA_HOME}/bin ENV PATH=$PATH:${JAVA_HOME}/bin
FROM base-core FROM base-core
WORKDIR /lc-core WORKDIR /lc-core
COPY --from=builder /build/target/scala-3.6.2/LibreCaptcha.jar . COPY --from=builder /build/target/scala-3.1.1/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`: String -
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. 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 with the `allowedSizes` config setting. Example: `size: "450x200"` which requests an image of width 450 and height
200 pixels. 200 pixels.
- 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

@ -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,8 +58,7 @@ public class FontFunCaptcha implements ChallengeProvider {
return null; return null;
} }
private byte[] fontFun( private byte[] fontFun(final int width, final int height, 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(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics2D = img.createGraphics(); Graphics2D graphics2D = img.createGraphics();
@ -88,8 +87,7 @@ public class FontFunCaptcha implements ChallengeProvider {
final int width = size2D[0]; final int width = size2D[0];
final int height = size2D[1]; final int height = size2D[1];
String path = "./lib/fonts/"; String path = "./lib/fonts/";
return new Challenge( return new Challenge(fontFun(width, height, 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,26 +1,26 @@
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.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 int[] computeOffsets( private int[] computeOffsets(final Font font, final int width, final int height, 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();
@ -32,20 +32,17 @@ public class PoppingCharactersCaptcha implements ChallengeProvider {
advances[i] = currX; advances[i] = 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; advances[text.length()] = currX;
graphics2D.dispose(); graphics2D.dispose();
return advances; return advances;
} }
private BufferedImage makeImage( private BufferedImage makeImage(final Font font, final int width, final int height, 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();
@ -67,38 +64,24 @@ public class PoppingCharactersCaptcha implements ChallengeProvider {
final var expectedWidth = advances[advances.length - 1]; final var expectedWidth = advances[advances.length - 1];
final var scale = width / (float) expectedWidth; 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(font, width, height, (g) -> {
final var color = g.scale(scale, 1);
Color.getHSBColor(HelperFunctions.randomNumber(0, 100) / 100.0f, 0.6f, 1.0f); if (i > 0) {
final var nextImage = final var prevI = (i - 1) % text.length();
makeImage( g.setColor(prevColor);
font, g.drawString(String.valueOf(text.charAt(prevI)), advances[prevI] + jitter(), fontHeight*1.1f + jitter());
width, }
height, g.setColor(color);
(g) -> { g.drawString(String.valueOf(text.charAt(i)), advances[i] + jitter(), fontHeight*1.1f + jitter());
g.scale(scale, 1); });
if (i > 0) { try {
final var prevI = (i - 1) % text.length(); writer.writeToSequence(nextImage);
g.setColor(prevColor); } catch (final IOException e) {
g.drawString( e.printStackTrace();
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();

View File

@ -1,18 +1,20 @@
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.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 {
@ -54,24 +56,20 @@ public class ShadowTextCaptcha implements ChallengeProvider {
graphics2D.setPaint(Color.BLACK); graphics2D.setPaint(Color.BLACK);
graphics2D.setFont(font); graphics2D.setFont(font);
final var stringWidth = graphics2D.getFontMetrics().stringWidth(text); final var stringWidth = graphics2D.getFontMetrics().stringWidth(text);
final var padding = (stringWidth > width) ? 0 : (width - stringWidth) / 2; final var padding = (stringWidth > width) ? 0 : (width - stringWidth)/2;
final var scaleX = (stringWidth > width) ? width / ((double) stringWidth) : 1d; final var scaleX = (stringWidth > width) ? width/((double) stringWidth) : 1d;
graphics2D.scale(scaleX, 1d); graphics2D.scale(scaleX, 1d);
graphics2D.drawString(text, padding, fontHeight * 1.1f); graphics2D.drawString(text, padding, fontHeight*1.1f);
graphics2D.dispose(); graphics2D.dispose();
final int kernelSize = (int) Math.ceil((Math.min(width, height) / 50.0)); final int kernelSize = (int) Math.ceil((Math.min(width, height) / 50.0));
ConvolveOp op = ConvolveOp op = new ConvolveOp(new Kernel(kernelSize, kernelSize, makeKernel(kernelSize)), ConvolveOp.EDGE_NO_OP, null);
new ConvolveOp(
new Kernel(kernelSize, kernelSize, makeKernel(kernelSize)),
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); g2d.scale(scaleX, 1d);
g2d.setFont(font); g2d.setFont(font);
g2d.drawString(text, padding - kernelSize, fontHeight * 1.1f); g2d.drawString(text, padding-kernelSize, fontHeight*1.1f);
g2d.dispose(); g2d.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
try { try {

View File

@ -1,7 +1,7 @@
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();

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

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

@ -46,7 +46,7 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
(captcha.allowedLevels).flatMap { level => (captcha.allowedLevels).flatMap { level =>
(captcha.allowedMedia).flatMap { media => (captcha.allowedMedia).flatMap { media =>
(captcha.allowedInputType).flatMap { inputType => (captcha.allowedInputType).flatMap { inputType =>
(captcha.allowedSizes).map { size => (captcha.allowedSizes).map {size =>
Parameters(level, media, inputType, size) Parameters(level, media, inputType, size)
} }
} }

View File

@ -43,16 +43,16 @@ class FilterChallenge extends ChallengeProvider {
val height = size2D(1) val height = size2D(1)
val canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) val canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
val g = canvas.createGraphics() val g = canvas.createGraphics()
val fontHeight = (height * 0.6).toInt 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) val font = new Font("Serif", Font.BOLD, fontHeight)
g.setFont(font) g.setFont(font)
val stringWidth = g.getFontMetrics().stringWidth(secret) val stringWidth = g.getFontMetrics().stringWidth(secret)
val scaleX = if (stringWidth > width) width / (stringWidth.toDouble) else 1d val scaleX = if (stringWidth > width) width/(stringWidth.toDouble) else 1d
val margin = if (stringWidth > width) 0 else (width - stringWidth) val margin = if (stringWidth > width) 0 else (width - stringWidth)
val xOffset = (margin * r.nextDouble).toInt val xOffset = (margin*r.nextDouble).toInt
g.scale(scaleX, 1d) g.scale(scaleX, 1d)
g.drawString(secret, xOffset, fontHeight) g.drawString(secret, xOffset, fontHeight)
g.dispose() g.dispose()

View File

@ -128,13 +128,13 @@ class RainDropsCP extends ChallengeProvider {
val textX = if (textWidth > width) 0 else ((width - textWidth) / 2) val textX = if (textWidth > width) 0 else ((width - textWidth) / 2)
// this will be overlapped by the following text to show the top outline because of the offset // this will be overlapped by the following text to show the top outline because of the offset
val yOffset = (fontHeight * 0.01).ceil.toInt 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, (fontHeight*1.1).toInt - yOffset)
// paint the text // paint the text
g.setColor(textColor) g.setColor(textColor)
g.drawString(secret, textX, (fontHeight * 1.1).toInt) g.drawString(secret, textX, (fontHeight*1.1).toInt)
g.dispose() g.dispose()
writer.writeToSequence(canvas) writer.writeToSequence(canvas)

View File

@ -36,7 +36,7 @@ case class ConfigField(
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 maxAttemptsRatioFloat: Option[Float] = mapFloat(maxAttemptsRatio)
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)

View File

@ -82,7 +82,6 @@ class Server(
new StringResponse(200, str) 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

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