Compare commits
374 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a53903a757 | ||
|
|
12ddf59bd1 | ||
|
|
5a5c9a73d9 | ||
|
|
a8f50f6098 | ||
|
|
ab1387814c | ||
|
|
f0d2ddda44 | ||
|
|
2fefc62ba2 | ||
|
|
2ec93c4b95 | ||
|
|
f60cb8dd05 | ||
|
|
5c73a0fea0 | ||
|
|
45299fb8eb | ||
|
|
3c6bbe8c9f | ||
|
|
e122976878 | ||
|
|
f702a22dcd | ||
|
|
b9bd305599 | ||
|
|
f4737424f7 | ||
|
|
355e8c553a | ||
|
|
777be7cf08 | ||
|
|
c7c89a6396 | ||
|
|
471a5828cd | ||
|
|
c7327f07ee | ||
|
|
aa31b2d9cf | ||
|
|
592a85257e | ||
|
|
5c3160157a | ||
|
|
24ac9c5ee7 | ||
|
|
6cf0b322fc | ||
|
|
967145b73b | ||
|
|
332aac57f0 | ||
|
|
f6e0130a53 | ||
|
|
20f32bdabb | ||
|
|
660d18d6f2 | ||
|
|
b786330bff | ||
|
|
dbb675b975 | ||
|
|
220eec02b0 | ||
|
|
3e822ef02a | ||
|
|
1c273c1996 | ||
|
|
7887622040 | ||
|
|
bf303108aa | ||
|
|
cc7e1232f2 | ||
|
|
a9b148c1a9 | ||
|
|
13459fbf21 | ||
|
|
fa366be315 | ||
|
|
9e39f6d293 | ||
|
|
f0050494a1 | ||
|
|
52156b1677 | ||
|
|
4749486112 | ||
|
|
7c7d7f6633 | ||
|
|
17db5767f4 | ||
|
|
3d7b7e8bd9 | ||
|
|
1991375d6b | ||
|
|
b77bd94ae6 | ||
|
|
b14415add5 | ||
|
|
ac44980f50 | ||
|
|
fba5b10e77 | ||
|
|
c4515d3f77 | ||
|
|
7c394b4359 | ||
|
|
6db9934bec | ||
|
|
39b776fde8 | ||
|
|
4c3a7ad0fa | ||
|
|
7f70d60dbb | ||
|
|
4d79567227 | ||
|
|
f6b7278ed1 | ||
|
|
984bbacb69 | ||
|
|
0dd00f5d3e | ||
|
|
d355333bd4 | ||
|
|
55064c8532 | ||
|
|
1742772de1 | ||
|
|
1234bd4acb | ||
|
|
df82593d90 | ||
|
|
b782ded765 | ||
|
|
aa6a6a1015 | ||
|
|
157054d9d8 | ||
|
|
daa8903933 | ||
|
|
de3a2bad6f | ||
|
|
121b190c32 | ||
|
|
b95350b1e2 | ||
|
|
26bc159342 | ||
|
|
476ef499e3 | ||
|
|
3b925adfde | ||
|
|
16a8282206 | ||
|
|
4cedff874d | ||
|
|
d8360c6192 | ||
|
|
e81e8a77e8 | ||
|
|
c045682708 | ||
|
|
414c07beaf | ||
|
|
aa6607237a | ||
|
|
931ab0a086 | ||
|
|
bc4acb1bae | ||
|
|
2dc2066ad8 | ||
|
|
bc8c6ad227 | ||
|
|
be0612b94d | ||
|
|
fe2315fc89 | ||
|
|
1696050702 | ||
|
|
5ddfab95d1 | ||
|
|
15c95747e8 | ||
|
|
12bb1dd1e7 | ||
|
|
1de046362d | ||
|
|
f9a089bed1 | ||
|
|
caac0552c3 | ||
|
|
1701a497b6 | ||
|
|
ecabb6baae | ||
|
|
065748e339 | ||
|
|
707d02a71f | ||
|
|
a69bd1afaf | ||
|
|
f0f62eb4fc | ||
|
|
fbaa0f24a8 | ||
|
|
ddd4054db1 | ||
|
|
66c93c5069 | ||
|
|
75ac7b2f88 | ||
|
|
367eef011f | ||
|
|
8be4ad2be8 | ||
|
|
f7568ffd5f | ||
|
|
061dc82c68 | ||
|
|
6ef321c418 | ||
|
|
d3048b3b4a | ||
|
|
33fbd9a49b | ||
|
|
f9e626415d | ||
|
|
ddb333a1df | ||
|
|
62c0c5c363 | ||
|
|
cff297a48f | ||
|
|
10ca28ba07 | ||
|
|
afab1b4803 | ||
|
|
205e652e1e | ||
|
|
03ac5e5e83 | ||
|
|
ce71407d74 | ||
|
|
2a33fe1e90 | ||
|
|
9399adf2c3 | ||
|
|
ef19bbc0c1 | ||
|
|
1c95665f67 | ||
|
|
6bb9fbd7ea | ||
|
|
6e6daf6c36 | ||
|
|
daa6c49f8a | ||
|
|
cb9b0d361b | ||
|
|
ae9163d106 | ||
|
|
ba21b133fc | ||
|
|
3b0b464109 | ||
|
|
1663e2b306 | ||
|
|
d77bc658a0 | ||
|
|
33ad07c768 | ||
|
|
beccf4dff6 | ||
|
|
e2688f7e51 | ||
|
|
ef4d1690e1 | ||
|
|
43291222a2 | ||
|
|
044d39ad44 | ||
|
|
b0f75677c9 | ||
|
|
cda8bf6aa5 | ||
|
|
57b43e0f31 | ||
|
|
bfa2cfdc88 | ||
|
|
5cbb23b714 | ||
|
|
a1739e5288 | ||
|
|
d5f7fdcad1 | ||
|
|
5950e42973 | ||
|
|
59934539a7 | ||
|
|
399ee06b62 | ||
|
|
5d7ccbb1c5 | ||
|
|
25fb901e9e | ||
|
|
dd48e9e4e7 | ||
|
|
43cb991e16 | ||
|
|
737ebd76ae | ||
|
|
30c978ea1c | ||
|
|
7721df414b | ||
|
|
901759e06e | ||
|
|
2dab3e0638 | ||
|
|
f2b19baca8 | ||
|
|
56b42801d9 | ||
|
|
448bb5fe9f | ||
|
|
fadfffafb0 | ||
|
|
f7d38f5f88 | ||
|
|
85cfdf642c | ||
|
|
7dea087e2d | ||
|
|
f7d03ac5cf | ||
|
|
a40b33d492 | ||
|
|
6eb38929d2 | ||
|
|
a88068b865 | ||
|
|
5e9c9e2d5d | ||
|
|
f9ae1e3970 | ||
|
|
57ce691a00 | ||
|
|
55e68d23f4 | ||
|
|
b27bcc8e52 | ||
|
|
6ce5bff8c5 | ||
|
|
9ef3d162f2 | ||
|
|
515782ea8b | ||
|
|
70ca5673d1 | ||
|
|
2a092b777e | ||
|
|
a2fc659eda | ||
|
|
a020426329 | ||
|
|
907210f229 | ||
|
|
58c6e96dc9 | ||
|
|
7beecd7b80 | ||
|
|
2f18edef10 | ||
|
|
76dcd1b41e | ||
|
|
32ac8f5baa | ||
|
|
5794693f51 | ||
|
|
c02f284cc9 | ||
|
|
055525bd74 | ||
|
|
dc5d9d86ff | ||
|
|
ffe3142fbf | ||
|
|
5537d907a1 | ||
|
|
15dfc1576e | ||
|
|
603e6eda22 | ||
|
|
3532f2ec29 | ||
|
|
0a4ca07ed4 | ||
|
|
95a7ee1710 | ||
|
|
aa484e4bc0 | ||
|
|
0549d9cfcf | ||
|
|
94dddd99c0 | ||
|
|
353ecaa1eb | ||
|
|
b402d34be8 | ||
|
|
37d840b365 | ||
|
|
a9b630724d | ||
|
|
b50df7ec2f | ||
|
|
b3c0a83594 | ||
|
|
35909261ce | ||
|
|
073740b98b | ||
|
|
c7726edc1c | ||
|
|
e35d569cbb | ||
|
|
e6b09b8d33 | ||
|
|
a43b18587e | ||
|
|
fb1bfbd881 | ||
|
|
41afb10cb5 | ||
|
|
e1a88735e3 | ||
|
|
9b56759c80 | ||
|
|
8df6039a7d | ||
|
|
a4df7a346d | ||
|
|
5218653f56 | ||
|
|
8c38ba2a21 | ||
|
|
cd6c8d790a | ||
|
|
44432cbd5a | ||
|
|
c71fdbb8de | ||
|
|
7ef308e556 | ||
|
|
e26bd32b2f | ||
|
|
cbb8abd352 | ||
|
|
ba796fddf7 | ||
|
|
c809872b8d | ||
|
|
caf03d7a48 | ||
|
|
3aeb258851 | ||
|
|
f8626e3670 | ||
|
|
83b0eb069e | ||
|
|
832053f6e9 | ||
|
|
6480da09ff | ||
|
|
23a6a43d2d | ||
|
|
7f77f819dd | ||
|
|
bfc7174e2a | ||
|
|
0c48f8fbd1 | ||
|
|
595815921f | ||
|
|
b33dd8adcf | ||
|
|
d797acdcac | ||
|
|
444982e65f | ||
|
|
c396fe5c4a | ||
|
|
2ee331e6b5 | ||
|
|
cc8addb0c7 | ||
|
|
d7c1f9a4cc | ||
|
|
07cce460b7 | ||
|
|
7da5a5b9af | ||
|
|
9f069e1402 | ||
|
|
70f8e42d93 | ||
|
|
c3eb078635 | ||
|
|
8f34d58521 | ||
|
|
4f3bec0bc6 | ||
|
|
edd211fb52 | ||
|
|
f0659da3eb | ||
|
|
c8db914cfd | ||
|
|
a54947cfc2 | ||
|
|
2d9fd3c68e | ||
|
|
226d3bfa74 | ||
|
|
cb0c244779 | ||
|
|
8316084ee7 | ||
|
|
e44a1df22e | ||
|
|
d2a4a92463 | ||
|
|
d3056699eb | ||
|
|
3b6a09649d | ||
|
|
52b071d680 | ||
|
|
497b88f0d3 | ||
|
|
6758ec3acf | ||
|
|
10e0ff08ca | ||
|
|
50b5e00ddf | ||
|
|
34556a8e93 | ||
|
|
836ee0f7f1 | ||
|
|
0f8782b6d3 | ||
|
|
44d0464830 | ||
|
|
33a4411689 | ||
|
|
86e11a9075 | ||
|
|
25bab9eeeb | ||
|
|
7405707250 | ||
|
|
181bcdae0c | ||
|
|
02ac9456e1 | ||
|
|
a74c7a8ed0 | ||
|
|
9c83170bf3 | ||
|
|
f24ede129c | ||
|
|
2f2d54a513 | ||
|
|
756ae8c2a7 | ||
|
|
3c7c47e0d8 | ||
|
|
a3fbbf836b | ||
|
|
300a04d47f | ||
|
|
3a7cb16286 | ||
|
|
d0145cebe6 | ||
|
|
ba9e125f9a | ||
|
|
6f22131162 | ||
|
|
ade9a41b14 | ||
|
|
8c70c53fde | ||
|
|
250eea2e3f | ||
|
|
1bf19d352e | ||
|
|
925b45d6f6 | ||
|
|
b8eb7223e2 | ||
|
|
1840ec4f59 | ||
|
|
7cb2d28968 | ||
|
|
dbe0692b8b | ||
|
|
46031ce2e4 | ||
|
|
b7ea87a629 | ||
|
|
c01139d37e | ||
|
|
a2a91d8f45 | ||
|
|
85d20d8fff | ||
|
|
715758ad48 | ||
|
|
1bd342e825 | ||
|
|
5072926bfc | ||
|
|
f286ef1741 | ||
|
|
2a01c176f7 | ||
|
|
b78331819e | ||
|
|
d5cf860629 | ||
|
|
71350f7105 | ||
|
|
8e66f3fa8e | ||
|
|
8ec72ad397 | ||
|
|
a649aa7c33 | ||
|
|
fabd5dd7ec | ||
|
|
e1505a7573 | ||
|
|
8e39505644 | ||
|
|
8a87da88c0 | ||
|
|
0ed1f980dd | ||
|
|
d706d91ee1 | ||
|
|
e78af73951 | ||
|
|
4853e6bccf | ||
|
|
9688387b9b | ||
|
|
0b17419ccf | ||
|
|
3e99c0d38b | ||
|
|
fe0766320b | ||
|
|
ff9462e9bc | ||
|
|
3122206ff7 | ||
|
|
992b07c365 | ||
|
|
0f191e69a3 | ||
|
|
d4e3fb7002 | ||
|
|
4988e2e856 | ||
|
|
700bd2d910 | ||
|
|
f4187720d2 | ||
|
|
4fc2d4ebb0 | ||
|
|
dff305ae14 | ||
|
|
e17e0cab36 | ||
|
|
56d04ba722 | ||
|
|
5e15ca6c35 | ||
|
|
632eb49fd3 | ||
|
|
72cbbcde1f | ||
|
|
e878353b96 | ||
|
|
9d0ed85f56 | ||
|
|
018a52a6fd | ||
|
|
b7deb03b02 | ||
|
|
d60eee1d13 | ||
|
|
81ddfbdd4b | ||
|
|
5e7a668016 | ||
|
|
f08daf0026 | ||
|
|
c78b1c96bc | ||
|
|
e4e3ed413d | ||
|
|
6a0ffd4ad6 | ||
|
|
6cb10f409b | ||
|
|
d8c2f43989 | ||
|
|
e3494de0bd | ||
|
|
f463b487eb | ||
|
|
cbe26a6097 | ||
|
|
1c71f2ae68 | ||
|
|
83dcd65da3 | ||
|
|
46e084095c | ||
|
|
74df5a8b7b | ||
|
|
0ce5e23e2f | ||
|
|
6ee4aa596d | ||
|
|
3717454f89 | ||
|
|
b8679fb5cc |
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Scala Steward: Reformat with sbt-java-formatter 0.8.0
|
||||||
|
57ce691a00babb03e0cae03a26fe56d63fc609af
|
||||||
|
|
||||||
|
# Scala Steward: Reformat with scalafmt 3.6.1
|
||||||
|
f2b19baca828a4d88b46bc009aef6d7115e63924
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# If true, Scala Steward will sign off all commits (e.g. `git --signoff`).
|
||||||
|
# Default: false
|
||||||
|
signoffCommits = true
|
||||||
|
|
@ -16,6 +16,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ name: Update docker image
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
@ -20,7 +22,20 @@ jobs:
|
||||||
- name: Assemble Jar
|
- name: Assemble Jar
|
||||||
run: sbt assembly
|
run: sbt assembly
|
||||||
|
|
||||||
|
-
|
||||||
|
name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v3
|
||||||
|
with:
|
||||||
|
images: librecaptcha/lc-core
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
|
@ -36,8 +51,13 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Runner.Dockerfile
|
file: ./Runner.Dockerfile
|
||||||
push: true
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: librecaptcha/lc-core:latest
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
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 }}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
rules=[
|
rules=[
|
||||||
ExplicitResultTypes,
|
|
||||||
RemoveUnused,
|
|
||||||
DisableSyntax,
|
DisableSyntax,
|
||||||
LeakingImplicitClassVal,
|
LeakingImplicitClassVal,
|
||||||
NoValInForComprehension,
|
NoValInForComprehension,
|
||||||
ProcedureSyntax
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version="3.0.5"
|
version="3.8.3"
|
||||||
maxColumn = 120
|
maxColumn = 120
|
||||||
runner.dialect = scala213source3
|
runner.dialect = scala3
|
||||||
|
|
|
||||||
13
Dockerfile
13
Dockerfile
|
|
@ -1,9 +1,10 @@
|
||||||
FROM adoptopenjdk/openjdk16:alpine AS base-builder
|
FROM eclipse-temurin:17-jre-jammy AS base-builder
|
||||||
ARG SBT_VERSION=1.3.13
|
ARG SBT_VERSION=1.7.1
|
||||||
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
|
||||||
|
|
@ -22,15 +23,15 @@ FROM sbt-builder as builder
|
||||||
COPY src/ src/
|
COPY src/ src/
|
||||||
RUN sbt assembly
|
RUN sbt assembly
|
||||||
|
|
||||||
FROM adoptopenjdk/openjdk16:alpine-jre AS base-core
|
FROM eclipse-temurin:17-jre-jammy AS base-core
|
||||||
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
|
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
|
||||||
RUN apk add --update ttf-dejavu
|
RUN apt update && apt install -y fonts-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-2.13/LibreCaptcha.jar .
|
COPY --from=builder /build/target/scala-3.6.2/LibreCaptcha.jar .
|
||||||
RUN mkdir data/
|
RUN mkdir data/
|
||||||
|
|
||||||
EXPOSE 8888
|
EXPOSE 8888
|
||||||
|
|
|
||||||
30
README.md
30
README.md
|
|
@ -47,18 +47,25 @@ docker-compose up
|
||||||
Using `docker`:
|
Using `docker`:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run -v lcdata:/lc-core/data librecaptcha/lc-core:latest
|
docker run -p=8888:8888 -v ./lcdata:/lc-core/data librecaptcha/lc-core:2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
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"}' localhost:8888/v1/captcha
|
> $ curl -d '{"media":"image/png","level":"easy","input_type":"text","size":"350x100"}' localhost:8888/v2/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
|
||||||
|
|
@ -91,6 +98,11 @@ create CAPTCHAs that suit their application and audience, with matching themes a
|
||||||
And, the more the variety of CAPTCHAS, the harder it is for bots to crack CAPTCHAs.
|
And, the more the variety of CAPTCHAS, the harder it is for bots to crack CAPTCHAs.
|
||||||
|
|
||||||
## Sample CAPTCHAs
|
## Sample CAPTCHAs
|
||||||
|
These are included in this server.
|
||||||
|
|
||||||
|
### ShadowText
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
### FilterCaptcha
|
### FilterCaptcha
|
||||||
|
|
||||||
|
|
@ -134,10 +146,10 @@ The service can be accessed using a simple HTTP API.
|
||||||
- image/png
|
- image/png
|
||||||
- image/gif
|
- image/gif
|
||||||
- (More to come)
|
- (More to come)
|
||||||
- `size`: `Map` -
|
- `size`: String -
|
||||||
The dimensions of a captcha (Optional). It needs two more fields nested in this parameter
|
The dimensions of a captcha. It needs to be a string in the format `"widthxheight"` in pixels, and will be matched
|
||||||
- `height`: `Int`
|
with the `allowedSizes` config setting. Example: `size: "450x200"` which requests an image of width 450 and height
|
||||||
- `width`: `Int`
|
200 pixels.
|
||||||
|
|
||||||
- Returns:
|
- Returns:
|
||||||
- `id`: `String` - The uuid of the captcha generated
|
- `id`: `String` - The uuid of the captcha generated
|
||||||
|
|
@ -168,9 +180,9 @@ The service can be accessed using a simple HTTP API.
|
||||||
In javascript:
|
In javascript:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const resp = await fetch("/v1/captcha", {
|
const resp = await fetch("/v2/captcha", {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({level: "easy", media: "image/png", "input_type" : "text"})
|
body: JSON.stringify({level: "easy", media: "image/png", "input_type" : "text", size: "350x100"})
|
||||||
})
|
})
|
||||||
|
|
||||||
const respJson = await resp.json();
|
const respJson = await resp.json();
|
||||||
|
|
@ -188,7 +200,7 @@ if (resp.ok) {
|
||||||
|
|
||||||
|
|
||||||
// When user submits an answer it can be sent to the server for verification thusly:
|
// When user submits an answer it can be sent to the server for verification thusly:
|
||||||
const resp = await fetch("/v1/answer", {
|
const resp = await fetch("/v2/answer", {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({id: captchaId, answer: "user input"})
|
body: JSON.stringify({id: captchaId, answer: "user input"})
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
FROM adoptopenjdk/openjdk16:alpine-jre AS base-core
|
FROM eclipse-temurin:17-jre-jammy AS base-core
|
||||||
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
|
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
|
||||||
RUN apk add --update ttf-dejavu
|
RUN apt update && apt install -y fonts-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-2.13/LibreCaptcha.jar /lc-core
|
COPY target/scala-3.6.2/LibreCaptcha.jar /lc-core
|
||||||
WORKDIR /lc-core
|
WORKDIR /lc-core
|
||||||
RUN mkdir data/
|
RUN mkdir data/
|
||||||
|
|
||||||
|
|
|
||||||
29
build.sbt
29
build.sbt
|
|
@ -2,25 +2,24 @@ lazy val root = (project in file(".")).settings(
|
||||||
inThisBuild(
|
inThisBuild(
|
||||||
List(
|
List(
|
||||||
organization := "com.example",
|
organization := "com.example",
|
||||||
scalaVersion := "2.13.7",
|
scalaVersion := "3.6.2",
|
||||||
version := "0.1.0-SNAPSHOT",
|
version := "0.2.1-snapshot",
|
||||||
semanticdbEnabled := true,
|
semanticdbEnabled := true,
|
||||||
semanticdbVersion := scalafixSemanticdb.revision,
|
semanticdbVersion := scalafixSemanticdb.revision
|
||||||
scalafixScalaBinaryVersion := "2.13"
|
|
||||||
|
// This is apparently not supported on Scala 3 currently
|
||||||
|
// scalafixScalaBinaryVersion := "3.1"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
name := "LibreCaptcha",
|
name := "LibreCaptcha",
|
||||||
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.0.24",
|
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.3.0",
|
||||||
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.0.24",
|
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.3.0",
|
||||||
libraryDependencies += "org.json4s" % "json4s-jackson_2.13" % "3.6.11"
|
libraryDependencies += "org.json4s" %% "json4s-jackson" % "4.0.7"
|
||||||
)
|
)
|
||||||
|
|
||||||
Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" }
|
Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" }
|
||||||
scalacOptions ++= List(
|
scalacOptions ++= List(
|
||||||
"-Yrangepos",
|
"-deprecation"
|
||||||
"-Ywarn-unused",
|
|
||||||
"-deprecation",
|
|
||||||
"-Xsource:3"
|
|
||||||
)
|
)
|
||||||
javacOptions += "-g:none"
|
javacOptions += "-g:none"
|
||||||
compileOrder := CompileOrder.JavaThenScala
|
compileOrder := CompileOrder.JavaThenScala
|
||||||
|
|
@ -29,4 +28,12 @@ assembly / mainClass := Some("lc.LCFramework")
|
||||||
Compile / run / mainClass := Some("lc.LCFramework")
|
Compile / run / mainClass := Some("lc.LCFramework")
|
||||||
assembly / assemblyJarName := "LibreCaptcha.jar"
|
assembly / assemblyJarName := "LibreCaptcha.jar"
|
||||||
|
|
||||||
|
ThisBuild / assemblyMergeStrategy := {
|
||||||
|
case PathList("module-info.class") => MergeStrategy.discard
|
||||||
|
case x if x.endsWith("/module-info.class") => MergeStrategy.discard
|
||||||
|
case x =>
|
||||||
|
val oldStrategy = (ThisBuild / assemblyMergeStrategy).value
|
||||||
|
oldStrategy(x)
|
||||||
|
}
|
||||||
|
|
||||||
run / fork := true
|
run / fork := true
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ services:
|
||||||
lc-core:
|
lc-core:
|
||||||
container_name: "libre-captcha"
|
container_name: "libre-captcha"
|
||||||
image: librecaptcha/lc-core:latest
|
image: librecaptcha/lc-core:latest
|
||||||
|
# Comment "image" & uncomment "build" if you intend to build from source
|
||||||
|
#build: .
|
||||||
volumes:
|
volumes:
|
||||||
- "./docker-data:/lc-core/data"
|
- "./docker-data:/lc-core/data"
|
||||||
ports:
|
ports:
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1 +1 @@
|
||||||
sbt.version=1.5.5
|
sbt.version=1.10.6
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.33")
|
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0")
|
||||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.5")
|
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
|
||||||
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.7.0")
|
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0")
|
||||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0")
|
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0")
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -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.Map;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import lc.captchas.interfaces.Challenge;
|
import lc.captchas.interfaces.Challenge;
|
||||||
import lc.captchas.interfaces.ChallengeProvider;
|
import lc.captchas.interfaces.ChallengeProvider;
|
||||||
import lc.misc.PngImageWriter;
|
|
||||||
import lc.misc.HelperFunctions;
|
import lc.misc.HelperFunctions;
|
||||||
|
import lc.misc.PngImageWriter;
|
||||||
|
|
||||||
public class FontFunCaptcha implements ChallengeProvider {
|
public class FontFunCaptcha implements ChallengeProvider {
|
||||||
|
|
||||||
|
|
@ -58,9 +58,10 @@ public class FontFunCaptcha implements ChallengeProvider {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] fontFun(String captchaText, String level, String path) {
|
private byte[] fontFun(
|
||||||
|
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(350, 100, BufferedImage.TYPE_INT_RGB);
|
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
Graphics2D graphics2D = img.createGraphics();
|
Graphics2D graphics2D = img.createGraphics();
|
||||||
for (int i = 0; i < captchaText.length(); i++) {
|
for (int i = 0; i < captchaText.length(); i++) {
|
||||||
Font font = loadCustomFont(level, path);
|
Font font = loadCustomFont(level, path);
|
||||||
|
|
@ -81,10 +82,14 @@ public class FontFunCaptcha implements ChallengeProvider {
|
||||||
return baos.toByteArray();
|
return baos.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Challenge returnChallenge() {
|
public Challenge returnChallenge(String level, String size) {
|
||||||
String secret = HelperFunctions.randomString(7);
|
String secret = HelperFunctions.randomString(7);
|
||||||
|
final int[] size2D = HelperFunctions.parseSize2D(size);
|
||||||
|
final int width = size2D[0];
|
||||||
|
final int height = size2D[1];
|
||||||
String path = "./lib/fonts/";
|
String path = "./lib/fonts/";
|
||||||
return new Challenge(fontFun(secret, "medium", path), "image/png", secret.toLowerCase());
|
return new Challenge(
|
||||||
|
fontFun(width, height, secret, "medium", path), "image/png", secret.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkAnswer(String secret, String answer) {
|
public boolean checkAnswer(String secret, String answer) {
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,51 @@
|
||||||
package lc.captchas;
|
package lc.captchas;
|
||||||
|
|
||||||
import java.awt.Font;
|
|
||||||
import java.awt.RenderingHints;
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
import java.awt.Font;
|
||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.imageio.stream.MemoryCacheImageOutputStream;
|
import javax.imageio.stream.MemoryCacheImageOutputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import lc.captchas.interfaces.Challenge;
|
import lc.captchas.interfaces.Challenge;
|
||||||
import lc.captchas.interfaces.ChallengeProvider;
|
import lc.captchas.interfaces.ChallengeProvider;
|
||||||
import lc.misc.HelperFunctions;
|
|
||||||
import lc.misc.GifSequenceWriter;
|
import lc.misc.GifSequenceWriter;
|
||||||
|
import lc.misc.HelperFunctions;
|
||||||
|
|
||||||
public class PoppingCharactersCaptcha implements ChallengeProvider {
|
public class PoppingCharactersCaptcha implements ChallengeProvider {
|
||||||
private final Font font = new Font("Arial", Font.ROMAN_BASELINE, 48);
|
|
||||||
private final int width = 250;
|
|
||||||
private final int height = 100;
|
|
||||||
|
|
||||||
private Integer[] computeOffsets(final String text) {
|
private int[] computeOffsets(
|
||||||
|
final Font font, final int width, final int height, final String text) {
|
||||||
final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
final var graphics2D = img.createGraphics();
|
final var graphics2D = img.createGraphics();
|
||||||
final var frc = graphics2D.getFontRenderContext();
|
final var frc = graphics2D.getFontRenderContext();
|
||||||
final var advances = new LinkedList<Integer>();
|
final var advances = new int[text.length() + 1];
|
||||||
final var spacing = font.getStringBounds(" ", frc).getWidth() / 3;
|
final var spacing = font.getStringBounds(" ", frc).getWidth() / 3;
|
||||||
var currX = 0;
|
var currX = 0;
|
||||||
for (int i = 0; i < text.length(); i++) {
|
for (int i = 0; i < text.length(); i++) {
|
||||||
final var c = text.charAt(i);
|
final var c = text.charAt(i);
|
||||||
advances.add(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;
|
||||||
graphics2D.dispose();
|
graphics2D.dispose();
|
||||||
return advances.toArray(new Integer[]{});
|
return advances;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BufferedImage makeImage(final Consumer<Graphics2D> f) {
|
private BufferedImage makeImage(
|
||||||
|
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(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
graphics2D.setRenderingHint(
|
||||||
|
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
graphics2D.setFont(font);
|
graphics2D.setFont(font);
|
||||||
f.accept(graphics2D);
|
f.accept(graphics2D);
|
||||||
graphics2D.dispose();
|
graphics2D.dispose();
|
||||||
|
|
@ -56,23 +56,42 @@ public class PoppingCharactersCaptcha implements ChallengeProvider {
|
||||||
return HelperFunctions.randomNumber(-2, +2);
|
return HelperFunctions.randomNumber(-2, +2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] gifCaptcha(final String text) {
|
private byte[] gifCaptcha(final int width, final int height, final String text) {
|
||||||
try {
|
try {
|
||||||
|
final var fontHeight = (int) (height * 0.5);
|
||||||
|
final Font font = new Font("Arial", Font.ROMAN_BASELINE, fontHeight);
|
||||||
final var byteArrayOutputStream = new ByteArrayOutputStream();
|
final var byteArrayOutputStream = new ByteArrayOutputStream();
|
||||||
final var output = new MemoryCacheImageOutputStream(byteArrayOutputStream);
|
final var output = new MemoryCacheImageOutputStream(byteArrayOutputStream);
|
||||||
final var writer = new GifSequenceWriter(output, 1, 900, true);
|
final var writer = new GifSequenceWriter(output, 1, 900, true);
|
||||||
final var advances = computeOffsets(text);
|
final var advances = computeOffsets(font, width, height, text);
|
||||||
|
final var expectedWidth = advances[advances.length - 1];
|
||||||
|
final var scale = width / (float) expectedWidth;
|
||||||
final var prevColor = Color.getHSBColor(0f, 0f, 0.1f);
|
final var prevColor = Color.getHSBColor(0f, 0f, 0.1f);
|
||||||
IntStream.range(0, text.length()).forEach(i -> {
|
IntStream.range(0, text.length())
|
||||||
final var color = Color.getHSBColor(HelperFunctions.randomNumber(0, 100)/100.0f, 0.6f, 1.0f);
|
.forEach(
|
||||||
final var nextImage = makeImage((g) -> {
|
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) {
|
if (i > 0) {
|
||||||
final var prevI = (i - 1) % text.length();
|
final var prevI = (i - 1) % text.length();
|
||||||
g.setColor(prevColor);
|
g.setColor(prevColor);
|
||||||
g.drawString(String.valueOf(text.charAt(prevI)), advances[prevI] + jitter(), 45 + jitter());
|
g.drawString(
|
||||||
|
String.valueOf(text.charAt(prevI)),
|
||||||
|
advances[prevI] + jitter(),
|
||||||
|
fontHeight * 1.1f + jitter());
|
||||||
}
|
}
|
||||||
g.setColor(color);
|
g.setColor(color);
|
||||||
g.drawString(String.valueOf(text.charAt(i)), advances[i] + jitter(), 45 + jitter());
|
g.drawString(
|
||||||
|
String.valueOf(text.charAt(i)),
|
||||||
|
advances[i] + jitter(),
|
||||||
|
fontHeight * 1.1f + jitter());
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
writer.writeToSequence(nextImage);
|
writer.writeToSequence(nextImage);
|
||||||
|
|
@ -100,9 +119,12 @@ public class PoppingCharactersCaptcha implements ChallengeProvider {
|
||||||
"supportedInputType", List.of("text"));
|
"supportedInputType", List.of("text"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Challenge returnChallenge() {
|
public Challenge returnChallenge(String level, String size) {
|
||||||
final var secret = HelperFunctions.randomString(6);
|
final var secret = HelperFunctions.randomString(6);
|
||||||
return new Challenge(gifCaptcha(secret), "image/gif", secret.toLowerCase());
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkAnswer(String secret, String answer) {
|
public boolean checkAnswer(String secret, String answer) {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,18 @@
|
||||||
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.font.TextLayout;
|
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.Map;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import lc.misc.HelperFunctions;
|
|
||||||
import lc.misc.PngImageWriter;
|
|
||||||
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;
|
||||||
|
|
||||||
public class ShadowTextCaptcha implements ChallengeProvider {
|
public class ShadowTextCaptcha implements ChallengeProvider {
|
||||||
|
|
||||||
|
|
@ -38,32 +35,43 @@ public class ShadowTextCaptcha implements ChallengeProvider {
|
||||||
return answer.toLowerCase().equals(secret);
|
return answer.toLowerCase().equals(secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] shadowText(String text) {
|
private float[] makeKernel(int size) {
|
||||||
BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB);
|
final int N = size * size;
|
||||||
Font font = new Font("Arial", Font.ROMAN_BASELINE, 48);
|
final float weight = 1.0f / (N);
|
||||||
Graphics2D graphics2D = img.createGraphics();
|
final float[] kernel = new float[N];
|
||||||
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
java.util.Arrays.fill(kernel, weight);
|
||||||
graphics2D.setRenderingHint(
|
return kernel;
|
||||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
};
|
||||||
|
|
||||||
TextLayout textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext());
|
private byte[] shadowText(final int width, final int height, String text) {
|
||||||
|
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
|
final int fontHeight = (int) (height * 0.5f);
|
||||||
|
Font font = new Font("Arial", Font.PLAIN, fontHeight);
|
||||||
|
Graphics2D graphics2D = img.createGraphics();
|
||||||
HelperFunctions.setRenderingHints(graphics2D);
|
HelperFunctions.setRenderingHints(graphics2D);
|
||||||
graphics2D.setPaint(Color.WHITE);
|
graphics2D.setPaint(Color.WHITE);
|
||||||
graphics2D.fillRect(0, 0, 350, 100);
|
graphics2D.fillRect(0, 0, width, height);
|
||||||
graphics2D.setPaint(Color.BLACK);
|
graphics2D.setPaint(Color.BLACK);
|
||||||
textLayout.draw(graphics2D, 15, 50);
|
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);
|
||||||
graphics2D.dispose();
|
graphics2D.dispose();
|
||||||
float[] kernel = {
|
final int kernelSize = (int) Math.ceil((Math.min(width, height) / 50.0));
|
||||||
1f / 9f, 1f / 9f, 1f / 9f,
|
ConvolveOp op =
|
||||||
1f / 9f, 1f / 9f, 1f / 9f,
|
new ConvolveOp(
|
||||||
1f / 9f, 1f / 9f, 1f / 9f
|
new Kernel(kernelSize, kernelSize, makeKernel(kernelSize)),
|
||||||
};
|
ConvolveOp.EDGE_NO_OP,
|
||||||
ConvolveOp op = new ConvolveOp(new Kernel(3, 3, kernel), ConvolveOp.EDGE_NO_OP, null);
|
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);
|
||||||
textLayout.draw(g2d, 13, 50);
|
g2d.scale(scaleX, 1d);
|
||||||
|
g2d.setFont(font);
|
||||||
|
g2d.drawString(text, padding - kernelSize, fontHeight * 1.1f);
|
||||||
g2d.dispose();
|
g2d.dispose();
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
try {
|
try {
|
||||||
|
|
@ -74,8 +82,11 @@ public class ShadowTextCaptcha implements ChallengeProvider {
|
||||||
return baos.toByteArray();
|
return baos.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Challenge returnChallenge() {
|
public Challenge returnChallenge(String level, String size) {
|
||||||
String secret = HelperFunctions.randomString(6);
|
String secret = HelperFunctions.randomString(6);
|
||||||
return new Challenge(shadowText(secret), "image/png", secret.toLowerCase());
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
package lc.captchas.interfaces;
|
package lc.captchas.interfaces;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public interface ChallengeProvider {
|
public interface ChallengeProvider {
|
||||||
public String getId();
|
public String getId();
|
||||||
|
|
||||||
public Challenge returnChallenge();
|
public Challenge returnChallenge(String level, String size);
|
||||||
|
|
||||||
public boolean checkAnswer(String secret, String answer);
|
public boolean checkAnswer(String secret, String answer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
|
|
||||||
package lc.misc;
|
package lc.misc;
|
||||||
|
|
||||||
import javax.imageio.*;
|
|
||||||
import javax.imageio.metadata.*;
|
|
||||||
import javax.imageio.stream.*;
|
|
||||||
import java.awt.image.*;
|
import java.awt.image.*;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import javax.imageio.*;
|
||||||
|
import javax.imageio.metadata.*;
|
||||||
|
import javax.imageio.stream.*;
|
||||||
|
|
||||||
public class GifSequenceWriter {
|
public class GifSequenceWriter {
|
||||||
protected ImageWriter gifWriter;
|
protected ImageWriter gifWriter;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,14 @@ public class HelperFunctions {
|
||||||
random.setSeed(seed);
|
random.setSeed(seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int[] parseSize2D(final String size) {
|
||||||
|
final String[] fields = size.split("x");
|
||||||
|
final int[] result = {Integer.parseInt(fields[0]), Integer.parseInt(fields[1])};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public static void setRenderingHints(Graphics2D g2d) {
|
public static void setRenderingHints(Graphics2D g2d) {
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
g2d.setRenderingHint(
|
g2d.setRenderingHint(
|
||||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
g2d.setRenderingHint(
|
g2d.setRenderingHint(
|
||||||
|
|
@ -23,7 +30,8 @@ public class HelperFunctions {
|
||||||
public static final String safeNumbers = "23456789";
|
public static final String safeNumbers = "23456789";
|
||||||
public static final String allNumbers = safeNumbers + "10";
|
public static final String allNumbers = safeNumbers + "10";
|
||||||
public static final String specialCharacters = "$#%@&?";
|
public static final String specialCharacters = "$#%@&?";
|
||||||
public static final String safeCharacters = safeAlphabets + safeNumbers + specialCharacters;
|
public static final String safeAlphaNum = safeAlphabets + safeNumbers;
|
||||||
|
public static final String safeCharacters = safeAlphaNum + specialCharacters;
|
||||||
|
|
||||||
public static String randomString(final int n) {
|
public static String randomString(final int n) {
|
||||||
return randomString(n, safeCharacters);
|
return randomString(n, safeCharacters);
|
||||||
|
|
|
||||||
|
|
@ -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,7 +13,6 @@ 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 {
|
||||||
|
|
||||||
|
|
@ -26,7 +25,8 @@ 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.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
|
ImageTypeSpecifier typeSpecifier =
|
||||||
|
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;
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,21 @@
|
||||||
|
|
||||||
package org.limium.picoserve;
|
package org.limium.picoserve;
|
||||||
|
|
||||||
import java.net.InetSocketAddress;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import com.sun.net.httpserver.HttpHandler;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.Map;
|
|
||||||
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.net.URLDecoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import java.util.Arrays;
|
||||||
import com.sun.net.httpserver.HttpHandler;
|
import java.util.LinkedList;
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public final class Server {
|
public final class Server {
|
||||||
private final HttpServer server;
|
private final HttpServer server;
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,10 @@
|
||||||
const levelInput = document.getElementById("levelInput").value
|
const levelInput = document.getElementById("levelInput").value
|
||||||
const mediaInput = document.getElementById("mediaInput").value
|
const mediaInput = document.getElementById("mediaInput").value
|
||||||
const typeInput = document.getElementById("typeInput").value
|
const typeInput = document.getElementById("typeInput").value
|
||||||
fetch("/v1/captcha", {
|
const sizeInput = document.getElementById("sizeInput").value
|
||||||
|
fetch("/v2/captcha", {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput})
|
body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput, "size": sizeInput})
|
||||||
}).then(async function(resp) {
|
}).then(async function(resp) {
|
||||||
const respJson = await resp.json()
|
const respJson = await resp.json()
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
|
|
@ -30,7 +31,7 @@
|
||||||
const resultDiv = document.getElementById("result")
|
const resultDiv = document.getElementById("result")
|
||||||
const result = `
|
const result = `
|
||||||
<p>Id: ${id}</p>
|
<p>Id: ${id}</p>
|
||||||
<p><img src="/v1/media?id=${id}" /> </p>
|
<p><img src="/v2/media?id=${id}" /> </p>
|
||||||
<input type="text" id="answerInput" />
|
<input type="text" id="answerInput" />
|
||||||
<button onClick="submitAnswer('${id}')">Submit</button>
|
<button onClick="submitAnswer('${id}')">Submit</button>
|
||||||
<div id="answerResult" />
|
<div id="answerResult" />
|
||||||
|
|
@ -43,7 +44,7 @@
|
||||||
}
|
}
|
||||||
async function submitAnswer(id) {
|
async function submitAnswer(id) {
|
||||||
const ans = document.getElementById("answerInput").value;
|
const ans = document.getElementById("answerInput").value;
|
||||||
const resp = await fetch("/v1/answer", {
|
const resp = await fetch("/v2/answer", {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({id: id, answer: ans})
|
body: JSON.stringify({id: id, answer: ans})
|
||||||
})
|
})
|
||||||
|
|
@ -70,6 +71,10 @@
|
||||||
<span>Input Type</span>
|
<span>Input Type</span>
|
||||||
<input type="text" id="typeInput" value="text" />
|
<input type="text" id="typeInput" value="text" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="inputGroup">
|
||||||
|
<span>Input Size</span>
|
||||||
|
<input type="text" id="sizeInput" value="350x100" />
|
||||||
|
</div>
|
||||||
<div class="inputGroup">
|
<div class="inputGroup">
|
||||||
<button onClick="loadCaptcha()">Get New CAPTCHA</button>
|
<button onClick="loadCaptcha()">Get New CAPTCHA</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package lc
|
package lc
|
||||||
|
|
||||||
import lc.core.{CaptchaProviders, Captcha, Config}
|
import lc.core.{CaptchaProviders, CaptchaManager, Config}
|
||||||
import lc.server.Server
|
import lc.server.Server
|
||||||
import lc.background.BackgroundTask
|
import lc.background.BackgroundTask
|
||||||
|
import lc.database.Statements
|
||||||
|
|
||||||
object LCFramework {
|
object LCFramework {
|
||||||
def main(args: scala.Array[String]): Unit = {
|
def main(args: scala.Array[String]): Unit = {
|
||||||
|
|
@ -12,11 +13,26 @@ object LCFramework {
|
||||||
"data/config.json"
|
"data/config.json"
|
||||||
}
|
}
|
||||||
val config = new Config(configFilePath)
|
val config = new Config(configFilePath)
|
||||||
|
Statements.maxAttempts = config.maxAttempts
|
||||||
val captchaProviders = new CaptchaProviders(config = config)
|
val captchaProviders = new CaptchaProviders(config = config)
|
||||||
val captcha = new Captcha(config = config, captchaProviders = captchaProviders)
|
val captchaManager = new CaptchaManager(config = config, captchaProviders = captchaProviders)
|
||||||
val backgroundTask = new BackgroundTask(config = config, captcha = captcha)
|
val backgroundTask = new BackgroundTask(config = config, captchaManager = captchaManager)
|
||||||
backgroundTask.beginThread(delay = config.threadDelay)
|
backgroundTask.beginThread(delay = config.threadDelay)
|
||||||
val server = new Server(address = config.address, port = config.port, captcha = captcha, playgroundEnabled = config.playgroundEnabled, corsHeader = config.corsHeader)
|
val server = new Server(
|
||||||
|
address = config.address,
|
||||||
|
port = config.port,
|
||||||
|
captchaManager = captchaManager,
|
||||||
|
playgroundEnabled = config.playgroundEnabled,
|
||||||
|
corsHeader = config.corsHeader
|
||||||
|
)
|
||||||
|
|
||||||
|
Runtime.getRuntime.addShutdownHook(new Thread {
|
||||||
|
override def run(): Unit = {
|
||||||
|
println("Shutting down gracefully...")
|
||||||
|
backgroundTask.shutdown()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.start()
|
server.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -31,8 +47,7 @@ object MakeSamples {
|
||||||
val config = new Config(configFilePath)
|
val config = new Config(configFilePath)
|
||||||
val captchaProviders = new CaptchaProviders(config = config)
|
val captchaProviders = new CaptchaProviders(config = config)
|
||||||
val samples = captchaProviders.generateChallengeSamples()
|
val samples = captchaProviders.generateChallengeSamples()
|
||||||
samples.foreach {
|
samples.foreach { case (key, sample) =>
|
||||||
case (key, sample) =>
|
|
||||||
val extensionMap = Map("image/png" -> "png", "image/gif" -> "gif")
|
val extensionMap = Map("image/png" -> "png", "image/gif" -> "gif")
|
||||||
println(key + ": " + sample)
|
println(key + ": " + sample)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ package lc.background
|
||||||
|
|
||||||
import lc.database.Statements
|
import lc.database.Statements
|
||||||
import java.util.concurrent.{ScheduledThreadPoolExecutor, TimeUnit}
|
import java.util.concurrent.{ScheduledThreadPoolExecutor, TimeUnit}
|
||||||
import lc.core.{Captcha, Config}
|
import lc.core.{CaptchaManager, Config}
|
||||||
import lc.core.{Parameters, Size}
|
import lc.core.{Parameters, Size}
|
||||||
import lc.misc.HelperFunctions
|
import lc.misc.HelperFunctions
|
||||||
|
|
||||||
class BackgroundTask(config: Config, captcha: Captcha) {
|
class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
|
||||||
|
|
||||||
private val task = new Runnable {
|
private val task = new Runnable {
|
||||||
def run(): Unit = {
|
def run(): Unit = {
|
||||||
|
|
@ -18,34 +18,73 @@ class BackgroundTask(config: Config, captcha: Captcha) {
|
||||||
val challengeGCPstmt = Statements.tlStmts.get.challengeGCPstmt
|
val challengeGCPstmt = Statements.tlStmts.get.challengeGCPstmt
|
||||||
challengeGCPstmt.executeUpdate()
|
challengeGCPstmt.executeUpdate()
|
||||||
|
|
||||||
val imageNum = Statements.tlStmts.get.getCountChallengeTable.executeQuery()
|
val allCombinations = allParameterCombinations()
|
||||||
var throttleIn = (config.throttle * 1.1).toInt
|
val requiredCountPerCombination = Math.max(1, (config.bufferCount * 1.01) / allCombinations.size).toInt
|
||||||
if (imageNum.next())
|
|
||||||
throttleIn = (throttleIn - imageNum.getInt("total"))
|
for (param <- allCombinations) {
|
||||||
while (0 < throttleIn) {
|
if (!shutdownInProgress) {
|
||||||
captcha.generateChallenge(getRandomParam())
|
val countExisting = captchaManager.getCount(param).getOrElse(0)
|
||||||
throttleIn -= 1
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch { case exception: Exception => println(exception) }
|
} catch { case exception: Exception => println(exception) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def allParameterCombinations(): List[Parameters] = {
|
||||||
|
(config.captchaConfig).flatMap { captcha =>
|
||||||
|
(captcha.allowedLevels).flatMap { level =>
|
||||||
|
(captcha.allowedMedia).flatMap { media =>
|
||||||
|
(captcha.allowedInputType).flatMap { inputType =>
|
||||||
|
(captcha.allowedSizes).map { size =>
|
||||||
|
Parameters(level, media, inputType, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private def getRandomParam(): Parameters = {
|
private def getRandomParam(): Parameters = {
|
||||||
val captcha = pickRandom(config.captchaConfig)
|
val captcha = pickRandom(config.captchaConfig)
|
||||||
val level = pickRandom(captcha.allowedLevels)
|
val level = pickRandom(captcha.allowedLevels)
|
||||||
val media = pickRandom(captcha.allowedMedia)
|
val media = pickRandom(captcha.allowedMedia)
|
||||||
val inputType = pickRandom(captcha.allowedInputType)
|
val inputType = pickRandom(captcha.allowedInputType)
|
||||||
|
val size = pickRandom(captcha.allowedSizes)
|
||||||
|
|
||||||
Parameters(level, media, inputType, Some(Size(0, 0)))
|
Parameters(level, media, inputType, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def pickRandom[T](list: List[T]): T = {
|
private def pickRandom[T](list: List[T]): T = {
|
||||||
list(HelperFunctions.randomNumber(list.size))
|
list(HelperFunctions.randomNumber(list.size))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val ex = new ScheduledThreadPoolExecutor(1)
|
||||||
|
|
||||||
def beginThread(delay: Int): Unit = {
|
def beginThread(delay: Int): Unit = {
|
||||||
val ex = new ScheduledThreadPoolExecutor(1)
|
|
||||||
ex.scheduleWithFixedDelay(task, 1, delay, TimeUnit.SECONDS)
|
ex.scheduleWithFixedDelay(task, 1, delay, TimeUnit.SECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@volatile var shutdownInProgress = false
|
||||||
|
|
||||||
|
def shutdown(): Unit = {
|
||||||
|
println(" Shutting down background task...")
|
||||||
|
shutdownInProgress = true
|
||||||
|
ex.shutdown()
|
||||||
|
println(" Finished Shutting background task")
|
||||||
|
println(" Shutting down DB...")
|
||||||
|
Statements.tlStmts.get.shutdown.execute()
|
||||||
|
println(" Finished shutting down db")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ import lc.captchas.interfaces.Challenge
|
||||||
import lc.captchas.interfaces.ChallengeProvider
|
import lc.captchas.interfaces.ChallengeProvider
|
||||||
import lc.misc.PngImageWriter
|
import lc.misc.PngImageWriter
|
||||||
|
|
||||||
/** This captcha is only for debugging purposes. It creates very simple captchas that are deliberately easy to solve with OCR engines */
|
/** This captcha is only for debugging purposes. It creates very simple captchas that are deliberately easy to solve
|
||||||
|
* with OCR engines
|
||||||
|
*/
|
||||||
class DebugCaptcha extends ChallengeProvider {
|
class DebugCaptcha extends ChallengeProvider {
|
||||||
|
|
||||||
def getId(): String = {
|
def getId(): String = {
|
||||||
|
|
@ -26,9 +28,12 @@ class DebugCaptcha extends ChallengeProvider {
|
||||||
|
|
||||||
def supportedParameters(): Map[String, List[String]] = {
|
def supportedParameters(): Map[String, List[String]] = {
|
||||||
Map.of(
|
Map.of(
|
||||||
"supportedLevels", List.of("debug"),
|
"supportedLevels",
|
||||||
"supportedMedia", List.of("image/png"),
|
List.of("debug"),
|
||||||
"supportedInputType", List.of("text")
|
"supportedMedia",
|
||||||
|
List.of("image/png"),
|
||||||
|
"supportedInputType",
|
||||||
|
List.of("text")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,14 +45,14 @@ class DebugCaptcha extends ChallengeProvider {
|
||||||
matches
|
matches
|
||||||
}
|
}
|
||||||
|
|
||||||
private def simpleText(text: String): Array[Byte] = {
|
private def simpleText(width: Int, height: Int, text: String): Array[Byte] = {
|
||||||
val img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB)
|
val img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
|
||||||
val font = new Font("Arial", Font.ROMAN_BASELINE, 56)
|
val font = new Font("Arial", Font.ROMAN_BASELINE, 56)
|
||||||
val graphics2D = img.createGraphics()
|
val graphics2D = img.createGraphics()
|
||||||
val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext())
|
val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext())
|
||||||
HelperFunctions.setRenderingHints(graphics2D)
|
HelperFunctions.setRenderingHints(graphics2D)
|
||||||
graphics2D.setPaint(Color.WHITE)
|
graphics2D.setPaint(Color.WHITE)
|
||||||
graphics2D.fillRect(0, 0, 350, 100)
|
graphics2D.fillRect(0, 0, width, height)
|
||||||
graphics2D.setPaint(Color.BLACK)
|
graphics2D.setPaint(Color.BLACK)
|
||||||
textLayout.draw(graphics2D, 15, 50)
|
textLayout.draw(graphics2D, 15, 50)
|
||||||
graphics2D.dispose()
|
graphics2D.dispose()
|
||||||
|
|
@ -61,8 +66,11 @@ class DebugCaptcha extends ChallengeProvider {
|
||||||
baos.toByteArray()
|
baos.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
def returnChallenge(): Challenge = {
|
def returnChallenge(level: String, size: String): Challenge = {
|
||||||
val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets)
|
val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets)
|
||||||
new Challenge(simpleText(secret), "image/png", secret.toLowerCase())
|
val size2D = HelperFunctions.parseSize2D(size)
|
||||||
|
val width = size2D(0)
|
||||||
|
val height = size2D(1)
|
||||||
|
new Challenge(simpleText(width, height, secret), "image/png", secret.toLowerCase())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import lc.captchas.interfaces.Challenge
|
||||||
import java.util.{List => JavaList, Map => JavaMap}
|
import java.util.{List => JavaList, Map => JavaMap}
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import lc.misc.PngImageWriter
|
import lc.misc.PngImageWriter
|
||||||
|
import lc.misc.HelperFunctions
|
||||||
|
|
||||||
class FilterChallenge extends ChallengeProvider {
|
class FilterChallenge extends ChallengeProvider {
|
||||||
def getId = "FilterChallenge"
|
def getId = "FilterChallenge"
|
||||||
|
|
@ -20,37 +21,47 @@ class FilterChallenge extends ChallengeProvider {
|
||||||
|
|
||||||
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
||||||
JavaMap.of(
|
JavaMap.of(
|
||||||
"supportedLevels",JavaList.of("medium", "hard"),
|
"supportedLevels",
|
||||||
"supportedMedia", JavaList.of("image/png"),
|
JavaList.of("medium", "hard"),
|
||||||
"supportedInputType", JavaList.of("text")
|
"supportedMedia",
|
||||||
|
JavaList.of("image/png"),
|
||||||
|
"supportedInputType",
|
||||||
|
JavaList.of("text")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def returnChallenge(): Challenge = {
|
private val filterTypes = List(new FilterType1, new FilterType2)
|
||||||
val filterTypes = List(new FilterType1, new FilterType2)
|
|
||||||
|
def returnChallenge(level: String, size: String): Challenge = {
|
||||||
|
val mediumLevel = level == "medium"
|
||||||
val r = new scala.util.Random
|
val r = new scala.util.Random
|
||||||
val alphabet = "abcdefghijklmnopqrstuvwxyz"
|
val characters = if (mediumLevel) HelperFunctions.safeAlphaNum else HelperFunctions.safeCharacters
|
||||||
val n = 8
|
val n = if (mediumLevel) 5 else 7
|
||||||
val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString
|
val secret = LazyList.continually(r.nextInt(characters.size)).map(characters).take(n).mkString
|
||||||
val canvas = new BufferedImage(225, 50, BufferedImage.TYPE_INT_RGB)
|
val size2D = HelperFunctions.parseSize2D(size)
|
||||||
|
val width = size2D(0)
|
||||||
|
val height = size2D(1)
|
||||||
|
val canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
|
||||||
val g = canvas.createGraphics()
|
val g = canvas.createGraphics()
|
||||||
|
val fontHeight = (height * 0.6).toInt
|
||||||
g.setColor(Color.WHITE)
|
g.setColor(Color.WHITE)
|
||||||
g.fillRect(0, 0, canvas.getWidth, canvas.getHeight)
|
g.fillRect(0, 0, canvas.getWidth, canvas.getHeight)
|
||||||
g.setColor(Color.BLACK)
|
g.setColor(Color.BLACK)
|
||||||
g.setFont(new Font("Serif", Font.PLAIN, 30))
|
val font = new Font("Serif", Font.BOLD, fontHeight)
|
||||||
g.drawString(secret, 5, 30)
|
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.dispose()
|
g.dispose()
|
||||||
var image = ImmutableImage.fromAwt(canvas)
|
var image = ImmutableImage.fromAwt(canvas)
|
||||||
val s = scala.util.Random.nextInt(2)
|
val s = r.nextInt(2)
|
||||||
image = filterTypes(s).applyFilter(image)
|
image = filterTypes(s).applyFilter(image, !mediumLevel)
|
||||||
val img = image.awt()
|
val img = image.awt()
|
||||||
val baos = new ByteArrayOutputStream()
|
val baos = new ByteArrayOutputStream()
|
||||||
try {
|
|
||||||
PngImageWriter.write(baos, img);
|
PngImageWriter.write(baos, img);
|
||||||
} catch {
|
|
||||||
case e: Exception =>
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
new Challenge(baos.toByteArray, "image/png", secret)
|
new Challenge(baos.toByteArray, "image/png", secret)
|
||||||
}
|
}
|
||||||
def checkAnswer(secret: String, answer: String): Boolean = {
|
def checkAnswer(secret: String, answer: String): Boolean = {
|
||||||
|
|
@ -59,14 +70,15 @@ class FilterChallenge extends ChallengeProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
trait FilterType {
|
trait FilterType {
|
||||||
def applyFilter(image: ImmutableImage): ImmutableImage
|
def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage
|
||||||
}
|
}
|
||||||
|
|
||||||
class FilterType1 extends FilterType {
|
class FilterType1 extends FilterType {
|
||||||
override def applyFilter(image: ImmutableImage): ImmutableImage = {
|
override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = {
|
||||||
val blur = new GaussianBlurFilter(2)
|
val radius = if (hardLevel) 3 else 2
|
||||||
|
val blur = new GaussianBlurFilter(radius)
|
||||||
val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1)
|
val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1)
|
||||||
val diffuse = new DiffuseFilter(2)
|
val diffuse = new DiffuseFilter(radius.toFloat)
|
||||||
blur.apply(image)
|
blur.apply(image)
|
||||||
diffuse.apply(image)
|
diffuse.apply(image)
|
||||||
smear.apply(image)
|
smear.apply(image)
|
||||||
|
|
@ -75,9 +87,10 @@ class FilterType1 extends FilterType {
|
||||||
}
|
}
|
||||||
|
|
||||||
class FilterType2 extends FilterType {
|
class FilterType2 extends FilterType {
|
||||||
override def applyFilter(image: ImmutableImage): ImmutableImage = {
|
override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = {
|
||||||
|
val radius = if (hardLevel) 2f else 1f
|
||||||
val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1)
|
val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1)
|
||||||
val diffuse = new DiffuseFilter(1)
|
val diffuse = new DiffuseFilter(radius)
|
||||||
val ripple = new RippleFilter(com.sksamuel.scrimage.filter.RippleType.Noise, 1, 1, 0.005.toFloat, 0.005.toFloat)
|
val ripple = new RippleFilter(com.sksamuel.scrimage.filter.RippleType.Noise, 1, 1, 0.005.toFloat, 0.005.toFloat)
|
||||||
diffuse.apply(image)
|
diffuse.apply(image)
|
||||||
ripple.apply(image)
|
ripple.apply(image)
|
||||||
|
|
|
||||||
|
|
@ -31,13 +31,16 @@ class LabelCaptcha extends ChallengeProvider {
|
||||||
|
|
||||||
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
||||||
JavaMap.of(
|
JavaMap.of(
|
||||||
"supportedLevels", JavaList.of("hard"),
|
"supportedLevels",
|
||||||
"supportedMedia", JavaList.of("image/png"),
|
JavaList.of("hard"),
|
||||||
"supportedInputType", JavaList.of("text")
|
"supportedMedia",
|
||||||
|
JavaList.of("image/png"),
|
||||||
|
"supportedInputType",
|
||||||
|
JavaList.of("text")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def returnChallenge(): Challenge =
|
def returnChallenge(level: String, size: String): Challenge =
|
||||||
synchronized {
|
synchronized {
|
||||||
val r = scala.util.Random.nextInt(knownFiles.length)
|
val r = scala.util.Random.nextInt(knownFiles.length)
|
||||||
val s = scala.util.Random.nextInt(unknownFiles.length)
|
val s = scala.util.Random.nextInt(unknownFiles.length)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import lc.captchas.interfaces.ChallengeProvider
|
||||||
import lc.captchas.interfaces.Challenge
|
import lc.captchas.interfaces.Challenge
|
||||||
import lc.misc.GifSequenceWriter
|
import lc.misc.GifSequenceWriter
|
||||||
import java.util.{List => JavaList, Map => JavaMap}
|
import java.util.{List => JavaList, Map => JavaMap}
|
||||||
|
import lc.misc.HelperFunctions
|
||||||
|
|
||||||
class Drop {
|
class Drop {
|
||||||
var x = 0
|
var x = 0
|
||||||
|
|
@ -24,8 +25,6 @@ class Drop {
|
||||||
}
|
}
|
||||||
|
|
||||||
class RainDropsCP extends ChallengeProvider {
|
class RainDropsCP extends ChallengeProvider {
|
||||||
private val alphabet = "abcdefghijklmnopqrstuvwxyz"
|
|
||||||
private val n = 6
|
|
||||||
private val bgColor = new Color(200, 200, 200)
|
private val bgColor = new Color(200, 200, 200)
|
||||||
private val textColor = new Color(208, 208, 218)
|
private val textColor = new Color(208, 208, 218)
|
||||||
private val textHighlightColor = new Color(100, 100, 125)
|
private val textHighlightColor = new Color(100, 100, 125)
|
||||||
|
|
@ -38,9 +37,12 @@ class RainDropsCP extends ChallengeProvider {
|
||||||
|
|
||||||
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
||||||
JavaMap.of(
|
JavaMap.of(
|
||||||
"supportedLevels", JavaList.of("medium", "easy"),
|
"supportedLevels",
|
||||||
"supportedMedia", JavaList.of("image/gif"),
|
JavaList.of("medium", "easy"),
|
||||||
"supportedInputType", JavaList.of("text")
|
"supportedMedia",
|
||||||
|
JavaList.of("image/gif"),
|
||||||
|
"supportedInputType",
|
||||||
|
JavaList.of("text")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,11 +55,13 @@ class RainDropsCP extends ChallengeProvider {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
def returnChallenge(): Challenge = {
|
def returnChallenge(level: String, size: String): Challenge = {
|
||||||
val r = new scala.util.Random
|
val r = new scala.util.Random
|
||||||
val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString
|
val n = if (level == "easy") 4 else 6
|
||||||
val width = 450
|
val secret = HelperFunctions.randomString(n, HelperFunctions.safeAlphaNum)
|
||||||
val height = 100
|
val size2D = HelperFunctions.parseSize2D(size)
|
||||||
|
val width = size2D(0)
|
||||||
|
val height = size2D(1)
|
||||||
val imgType = BufferedImage.TYPE_INT_RGB
|
val imgType = BufferedImage.TYPE_INT_RGB
|
||||||
val xOffset = 2 + r.nextInt(3)
|
val xOffset = 2 + r.nextInt(3)
|
||||||
val xBias = (height / 10) - 2
|
val xBias = (height / 10) - 2
|
||||||
|
|
@ -77,7 +81,8 @@ class RainDropsCP extends ChallengeProvider {
|
||||||
xOffset
|
xOffset
|
||||||
)
|
)
|
||||||
|
|
||||||
val baseFont = new Font(Font.MONOSPACED, Font.BOLD, 80)
|
val fontHeight = (height * 0.5f).toInt
|
||||||
|
val baseFont = new Font(Font.MONOSPACED, Font.BOLD, fontHeight)
|
||||||
val attributes = new java.util.HashMap[TextAttribute, Object]()
|
val attributes = new java.util.HashMap[TextAttribute, Object]()
|
||||||
attributes.put(TextAttribute.TRACKING, Double.box(0.2))
|
attributes.put(TextAttribute.TRACKING, Double.box(0.2))
|
||||||
attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_EXTRABOLD)
|
attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_EXTRABOLD)
|
||||||
|
|
@ -114,17 +119,22 @@ class RainDropsCP extends ChallengeProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// center the text
|
|
||||||
g.setFont(spacedFont)
|
g.setFont(spacedFont)
|
||||||
val textWidth = g.getFontMetrics().charsWidth(secret.toCharArray, 0, secret.toCharArray.length)
|
val textWidth = g.getFontMetrics().stringWidth(secret)
|
||||||
val textX = (width - textWidth) / 2
|
val scaleX = if (textWidth > width) width / textWidth.toDouble else 1.0d
|
||||||
|
g.scale(scaleX, 1)
|
||||||
|
|
||||||
// paint the top outline
|
// center the text
|
||||||
|
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
|
||||||
|
val yOffset = (fontHeight * 0.01).ceil.toInt
|
||||||
g.setColor(textHighlightColor)
|
g.setColor(textHighlightColor)
|
||||||
g.drawString(secret, textX, 69)
|
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, 70)
|
g.drawString(secret, textX, (fontHeight * 1.1).toInt)
|
||||||
|
|
||||||
g.dispose()
|
g.dispose()
|
||||||
writer.writeToSequence(canvas)
|
writer.writeToSequence(canvas)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ object ParametersEnum extends Enumeration {
|
||||||
val ALLOWEDLEVELS: Value = Value("allowedLevels")
|
val ALLOWEDLEVELS: Value = Value("allowedLevels")
|
||||||
val ALLOWEDMEDIA: Value = Value("allowedMedia")
|
val ALLOWEDMEDIA: Value = Value("allowedMedia")
|
||||||
val ALLOWEDINPUTTYPE: Value = Value("allowedInputType")
|
val ALLOWEDINPUTTYPE: Value = Value("allowedInputType")
|
||||||
|
val ALLOWEDSIZES: Value = Value("allowedSizes")
|
||||||
}
|
}
|
||||||
|
|
||||||
object AttributesEnum extends Enumeration {
|
object AttributesEnum extends Enumeration {
|
||||||
|
|
@ -21,11 +21,12 @@ object AttributesEnum extends Enumeration {
|
||||||
val PORT: Value = Value("port")
|
val PORT: Value = Value("port")
|
||||||
val ADDRESS: Value = Value("address")
|
val ADDRESS: Value = Value("address")
|
||||||
val CAPTCHA_EXPIRY_TIME_LIMIT: Value = Value("captchaExpiryTimeLimit")
|
val CAPTCHA_EXPIRY_TIME_LIMIT: Value = Value("captchaExpiryTimeLimit")
|
||||||
val THROTTLE: Value = Value("throttle")
|
val BUFFER_COUNT: Value = Value("bufferCount")
|
||||||
val THREAD_DELAY: Value = Value("threadDelay")
|
val THREAD_DELAY: Value = Value("threadDelay")
|
||||||
val PLAYGROUND_ENABLED: Value = Value("playgroundEnabled")
|
val PLAYGROUND_ENABLED: Value = Value("playgroundEnabled")
|
||||||
val CORS_HEADER: Value = Value("corsHeader")
|
val CORS_HEADER: Value = Value("corsHeader")
|
||||||
val CONFIG: Value = Value("config")
|
val CONFIG: Value = Value("config")
|
||||||
|
val MAX_ATTEMPTS_RATIO: Value = Value("maxAttemptsRatio")
|
||||||
}
|
}
|
||||||
|
|
||||||
object ResultEnum extends Enumeration {
|
object ResultEnum extends Enumeration {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
package lc.core
|
package lc.core
|
||||||
|
|
||||||
import java.sql.ResultSet
|
import lc.captchas.interfaces.{Challenge, ChallengeProvider}
|
||||||
import java.util.UUID
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import lc.database.Statements
|
import lc.database.Statements
|
||||||
import lc.core.CaptchaProviders
|
import java.io.ByteArrayInputStream
|
||||||
import lc.captchas.interfaces.ChallengeProvider
|
import java.sql.{Blob, ResultSet}
|
||||||
import lc.captchas.interfaces.Challenge
|
import java.util.UUID
|
||||||
import java.sql.Blob
|
|
||||||
|
|
||||||
class Captcha(config: Config, captchaProviders: CaptchaProviders) {
|
class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
|
||||||
|
|
||||||
def getCaptcha(id: Id): Either[Error, Image] = {
|
def getCaptcha(id: Id): Either[Error, Image] = {
|
||||||
val blob = getImage(id.id)
|
val blob = getImage(id.id)
|
||||||
|
|
@ -37,17 +34,19 @@ class Captcha(config: Config, captchaProviders: CaptchaProviders) {
|
||||||
}
|
}
|
||||||
|
|
||||||
def generateChallenge(param: Parameters): Option[Int] = {
|
def generateChallenge(param: Parameters): Option[Int] = {
|
||||||
val provider = captchaProviders.getProvider(param)
|
try {
|
||||||
provider match {
|
captchaProviders.getProvider(param).flatMap { provider =>
|
||||||
case Some(value) => {
|
val providerId = provider.getId()
|
||||||
val providerId = value.getId()
|
val challenge = provider.returnChallenge(param.level, param.size)
|
||||||
val challenge = value.returnChallenge()
|
|
||||||
val blob = new ByteArrayInputStream(challenge.content)
|
val blob = new ByteArrayInputStream(challenge.content)
|
||||||
val token = insertCaptcha(value, challenge, providerId, param, blob)
|
val token = insertCaptcha(provider, challenge, providerId, param, blob)
|
||||||
// println("Added new challenge: " + token.toString)
|
// println("Added new challenge: " + token.toString)
|
||||||
token.map(_.toInt)
|
token.map(_.toInt)
|
||||||
}
|
}
|
||||||
case None => None
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
e.printStackTrace()
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,7 +64,8 @@ class Captcha(config: Config, captchaProviders: CaptchaProviders) {
|
||||||
insertPstmt.setString(4, challenge.contentType)
|
insertPstmt.setString(4, challenge.contentType)
|
||||||
insertPstmt.setString(5, param.level)
|
insertPstmt.setString(5, param.level)
|
||||||
insertPstmt.setString(6, param.input_type)
|
insertPstmt.setString(6, param.input_type)
|
||||||
insertPstmt.setBlob(7, blob)
|
insertPstmt.setString(7, param.size)
|
||||||
|
insertPstmt.setBlob(8, blob)
|
||||||
insertPstmt.executeUpdate()
|
insertPstmt.executeUpdate()
|
||||||
val rs: ResultSet = insertPstmt.getGeneratedKeys()
|
val rs: ResultSet = insertPstmt.getGeneratedKeys()
|
||||||
if (rs.next()) {
|
if (rs.next()) {
|
||||||
|
|
@ -108,11 +108,31 @@ class Captcha(config: Config, captchaProviders: CaptchaProviders) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def getCount(param: Parameters): Option[Int] = {
|
||||||
|
val countPstmt = Statements.tlStmts.get.countForParameterPstmt
|
||||||
|
countPstmt.setString(1, param.level)
|
||||||
|
countPstmt.setString(2, param.media)
|
||||||
|
countPstmt.setString(3, param.input_type)
|
||||||
|
countPstmt.setString(4, param.size.toString())
|
||||||
|
val rs = countPstmt.executeQuery()
|
||||||
|
if (rs.next()) {
|
||||||
|
Some(rs.getInt("count"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private def getToken(param: Parameters): Option[Int] = {
|
private def getToken(param: Parameters): Option[Int] = {
|
||||||
|
val count = getCount(param).getOrElse(0)
|
||||||
|
if (count == 0) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
val tokenPstmt = Statements.tlStmts.get.tokenPstmt
|
val tokenPstmt = Statements.tlStmts.get.tokenPstmt
|
||||||
tokenPstmt.setString(1, param.level)
|
tokenPstmt.setString(1, param.level)
|
||||||
tokenPstmt.setString(2, param.media)
|
tokenPstmt.setString(2, param.media)
|
||||||
tokenPstmt.setString(3, param.input_type)
|
tokenPstmt.setString(3, param.input_type)
|
||||||
|
tokenPstmt.setString(4, param.size)
|
||||||
|
tokenPstmt.setInt(5, count)
|
||||||
val rs = tokenPstmt.executeQuery()
|
val rs = tokenPstmt.executeQuery()
|
||||||
if (rs.next()) {
|
if (rs.next()) {
|
||||||
Some(rs.getInt("token"))
|
Some(rs.getInt("token"))
|
||||||
|
|
@ -120,6 +140,7 @@ class Captcha(config: Config, captchaProviders: CaptchaProviders) {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private def updateAttempted(token: Int): Unit = {
|
private def updateAttempted(token: Int): Unit = {
|
||||||
val updateAttemptedPstmt = Statements.tlStmts.get.updateAttemptedPstmt
|
val updateAttemptedPstmt = Statements.tlStmts.get.updateAttemptedPstmt
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package lc.core
|
package lc.core
|
||||||
|
|
||||||
import lc.captchas._
|
import lc.captchas.*
|
||||||
import lc.captchas.interfaces.ChallengeProvider
|
import lc.captchas.interfaces.ChallengeProvider
|
||||||
import lc.captchas.interfaces.Challenge
|
import lc.captchas.interfaces.Challenge
|
||||||
import scala.collection.mutable.Map
|
import scala.collection.mutable.Map
|
||||||
|
|
@ -18,9 +18,8 @@ class CaptchaProviders(config: Config) {
|
||||||
)
|
)
|
||||||
|
|
||||||
def generateChallengeSamples(): Map[String, Challenge] = {
|
def generateChallengeSamples(): Map[String, Challenge] = {
|
||||||
providers.map {
|
providers.map { case (key, provider) =>
|
||||||
case (key, provider) =>
|
(key, provider.returnChallenge("easy", "350x100"))
|
||||||
(key, provider.returnChallenge())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,6 +35,7 @@ class CaptchaProviders(config: Config) {
|
||||||
if configValue.allowedLevels.contains(param.level)
|
if configValue.allowedLevels.contains(param.level)
|
||||||
if configValue.allowedMedia.contains(param.media)
|
if configValue.allowedMedia.contains(param.media)
|
||||||
if configValue.allowedInputType.contains(param.input_type)
|
if configValue.allowedInputType.contains(param.input_type)
|
||||||
|
if configValue.allowedSizes.contains(param.size)
|
||||||
} yield (configValue.name, configValue.config)
|
} yield (configValue.name, configValue.config)
|
||||||
|
|
||||||
val providerFilter = for {
|
val providerFilter = for {
|
||||||
|
|
@ -51,7 +51,7 @@ class CaptchaProviders(config: Config) {
|
||||||
|
|
||||||
def getProvider(param: Parameters): Option[ChallengeProvider] = {
|
def getProvider(param: Parameters): Option[ChallengeProvider] = {
|
||||||
val providerConfig = filterProviderByParam(param).toList
|
val providerConfig = filterProviderByParam(param).toList
|
||||||
if (providerConfig.length > 0) {
|
if (providerConfig.nonEmpty) {
|
||||||
val randomIndex = HelperFunctions.randomNumber(providerConfig.length)
|
val randomIndex = HelperFunctions.randomNumber(providerConfig.length)
|
||||||
val providerIndex = providerConfig(randomIndex)._1
|
val providerIndex = providerConfig(randomIndex)._1
|
||||||
val selectedProvider = providers(providerIndex)
|
val selectedProvider = providers(providerIndex)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import scala.io.Source.fromFile
|
||||||
import org.json4s.{DefaultFormats, JValue, JObject, JField, JString}
|
import org.json4s.{DefaultFormats, JValue, JObject, JField, JString}
|
||||||
import org.json4s.jackson.JsonMethods.{parse, render, pretty}
|
import org.json4s.jackson.JsonMethods.{parse, render, pretty}
|
||||||
import org.json4s.JsonDSL._
|
import org.json4s.JsonDSL._
|
||||||
|
import org.json4s.StringInput
|
||||||
|
import org.json4s.jvalue2monadic
|
||||||
|
import org.json4s.jvalue2extractable
|
||||||
import java.io.{FileNotFoundException, File, PrintWriter}
|
import java.io.{FileNotFoundException, File, PrintWriter}
|
||||||
import java.{util => ju}
|
import java.{util => ju}
|
||||||
import lc.misc.HelperFunctions
|
import lc.misc.HelperFunctions
|
||||||
|
|
@ -38,19 +41,21 @@ class Config(configFilePath: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val configJson = parse(configString)
|
private val configJson = parse(configString)
|
||||||
|
private val configFields: ConfigField = configJson.extract[ConfigField]
|
||||||
|
|
||||||
val port: Int = (configJson \ AttributesEnum.PORT.toString).extract[Int]
|
val port: Int = configFields.portInt.getOrElse(8888)
|
||||||
val address: String = (configJson \ AttributesEnum.ADDRESS.toString).extract[String]
|
val address: String = configFields.address.getOrElse("0.0.0.0")
|
||||||
val throttle: Int = (configJson \ AttributesEnum.THROTTLE.toString).extract[Int]
|
val bufferCount: Int = configFields.bufferCountInt.getOrElse(1000)
|
||||||
val seed: Int = (configJson \ AttributesEnum.RANDOM_SEED.toString).extract[Int]
|
val seed: Int = configFields.seedInt.getOrElse(375264328)
|
||||||
val captchaExpiryTimeLimit: Int = (configJson \ AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString).extract[Int]
|
val captchaExpiryTimeLimit: Int = configFields.captchaExpiryTimeLimitInt.getOrElse(5)
|
||||||
val threadDelay: Int = (configJson \ AttributesEnum.THREAD_DELAY.toString).extract[Int]
|
val threadDelay: Int = configFields.threadDelayInt.getOrElse(2)
|
||||||
val playgroundEnabled: Boolean = (configJson \ AttributesEnum.PLAYGROUND_ENABLED.toString).extract[Boolean]
|
val playgroundEnabled: Boolean = configFields.playgroundEnabledBool.getOrElse(true)
|
||||||
val corsHeader: String = (configJson \ AttributesEnum.CORS_HEADER.toString).extract[String]
|
val corsHeader: String = configFields.corsHeader.getOrElse("")
|
||||||
|
val maxAttempts: Int = Math.max(1, (configFields.maxAttemptsRatioFloat.getOrElse(0.01f) * bufferCount).toInt)
|
||||||
|
|
||||||
private val captchaConfigJson = (configJson \ "captchas")
|
private val captchaConfigJson = (configJson \ "captchas")
|
||||||
val captchaConfigTransform: JValue = captchaConfigJson transformField {
|
val captchaConfigTransform: JValue = captchaConfigJson transformField { case JField("config", JObject(config)) =>
|
||||||
case JField("config", JObject(config)) => ("config", JString(config.toString))
|
("config", JString(config.toString))
|
||||||
}
|
}
|
||||||
val captchaConfig: List[CaptchaConfig] = captchaConfigTransform.extract[List[CaptchaConfig]]
|
val captchaConfig: List[CaptchaConfig] = captchaConfigTransform.extract[List[CaptchaConfig]]
|
||||||
val allowedLevels: Set[String] = captchaConfig.flatMap(_.allowedLevels).toSet
|
val allowedLevels: Set[String] = captchaConfig.flatMap(_.allowedLevels).toSet
|
||||||
|
|
@ -65,16 +70,18 @@ class Config(configFilePath: String) {
|
||||||
(AttributesEnum.PORT.toString -> 8888) ~
|
(AttributesEnum.PORT.toString -> 8888) ~
|
||||||
(AttributesEnum.ADDRESS.toString -> "0.0.0.0") ~
|
(AttributesEnum.ADDRESS.toString -> "0.0.0.0") ~
|
||||||
(AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString -> 5) ~
|
(AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString -> 5) ~
|
||||||
(AttributesEnum.THROTTLE.toString -> 1000) ~
|
(AttributesEnum.BUFFER_COUNT.toString -> 1000) ~
|
||||||
(AttributesEnum.THREAD_DELAY.toString -> 2) ~
|
(AttributesEnum.THREAD_DELAY.toString -> 2) ~
|
||||||
(AttributesEnum.PLAYGROUND_ENABLED.toString -> true) ~
|
(AttributesEnum.PLAYGROUND_ENABLED.toString -> true) ~
|
||||||
(AttributesEnum.CORS_HEADER.toString -> "") ~
|
(AttributesEnum.CORS_HEADER.toString -> "") ~
|
||||||
|
(AttributesEnum.MAX_ATTEMPTS_RATIO.toString -> 0.01f) ~
|
||||||
("captchas" -> List(
|
("captchas" -> List(
|
||||||
(
|
(
|
||||||
(AttributesEnum.NAME.toString -> "FilterChallenge") ~
|
(AttributesEnum.NAME.toString -> "FilterChallenge") ~
|
||||||
(ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~
|
(ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~
|
||||||
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
|
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
|
||||||
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
||||||
|
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
|
||||||
(AttributesEnum.CONFIG.toString -> JObject())
|
(AttributesEnum.CONFIG.toString -> JObject())
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
@ -82,6 +89,7 @@ class Config(configFilePath: String) {
|
||||||
(ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~
|
(ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~
|
||||||
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
|
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
|
||||||
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
||||||
|
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
|
||||||
(AttributesEnum.CONFIG.toString -> JObject())
|
(AttributesEnum.CONFIG.toString -> JObject())
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
@ -89,6 +97,7 @@ class Config(configFilePath: String) {
|
||||||
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~
|
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~
|
||||||
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
|
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
|
||||||
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
||||||
|
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
|
||||||
(AttributesEnum.CONFIG.toString -> JObject())
|
(AttributesEnum.CONFIG.toString -> JObject())
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
@ -96,6 +105,7 @@ class Config(configFilePath: String) {
|
||||||
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~
|
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~
|
||||||
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
|
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
|
||||||
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
||||||
|
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
|
||||||
(AttributesEnum.CONFIG.toString -> JObject())
|
(AttributesEnum.CONFIG.toString -> JObject())
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import lc.core.Config.formats
|
||||||
|
|
||||||
trait ByteConvert { def toBytes(): Array[Byte] }
|
trait ByteConvert { def toBytes(): Array[Byte] }
|
||||||
case class Size(height: Int, width: Int)
|
case class Size(height: Int, width: Int)
|
||||||
case class Parameters(level: String, media: String, input_type: String, size: Option[Size])
|
case class Parameters(level: String, media: String, input_type: String, size: String)
|
||||||
case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } }
|
case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } }
|
||||||
case class Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[Byte] = { image } }
|
case class Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[Byte] = { image } }
|
||||||
case class Answer(answer: String, id: String)
|
case class Answer(answer: String, id: String)
|
||||||
|
|
@ -16,5 +16,32 @@ case class CaptchaConfig(
|
||||||
allowedLevels: List[String],
|
allowedLevels: List[String],
|
||||||
allowedMedia: List[String],
|
allowedMedia: List[String],
|
||||||
allowedInputType: List[String],
|
allowedInputType: List[String],
|
||||||
|
allowedSizes: List[String],
|
||||||
config: String
|
config: String
|
||||||
)
|
)
|
||||||
|
case class ConfigField(
|
||||||
|
port: Option[Integer],
|
||||||
|
address: Option[String],
|
||||||
|
bufferCount: Option[Integer],
|
||||||
|
seed: Option[Integer],
|
||||||
|
captchaExpiryTimeLimit: Option[Integer],
|
||||||
|
threadDelay: Option[Integer],
|
||||||
|
playgroundEnabled: Option[java.lang.Boolean],
|
||||||
|
corsHeader: Option[String],
|
||||||
|
maxAttemptsRatio: Option[java.lang.Float]
|
||||||
|
) {
|
||||||
|
lazy val portInt: Option[Int] = mapInt(port)
|
||||||
|
lazy val bufferCountInt: Option[Int] = mapInt(bufferCount)
|
||||||
|
lazy val seedInt: Option[Int] = mapInt(seed)
|
||||||
|
lazy val captchaExpiryTimeLimitInt: Option[Int] = mapInt(captchaExpiryTimeLimit)
|
||||||
|
lazy val threadDelayInt: Option[Int] = mapInt(threadDelay)
|
||||||
|
lazy val maxAttemptsRatioFloat: Option[Float] = mapFloat(maxAttemptsRatio)
|
||||||
|
lazy val playgroundEnabledBool: Option[Boolean] = playgroundEnabled.map(_ || false)
|
||||||
|
|
||||||
|
private def mapInt(x: Option[Integer]): Option[Int] = {
|
||||||
|
x.map(_ + 0)
|
||||||
|
}
|
||||||
|
private def mapFloat(x: Option[java.lang.Float]): Option[Float] = {
|
||||||
|
x.map(_ + 0.0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package lc.database
|
package lc.database
|
||||||
|
|
||||||
import java.sql._
|
import java.sql.{Connection, DriverManager, Statement}
|
||||||
|
|
||||||
class DBConn() {
|
class DBConn() {
|
||||||
val con: Connection = DriverManager.getConnection("jdbc:h2:./data/H2/captcha2", "sa", "")
|
val con: Connection =
|
||||||
|
DriverManager.getConnection("jdbc:h2:./data/H2/captcha3;MAX_COMPACT_TIME=8000;DB_CLOSE_ON_EXIT=FALSE", "sa", "")
|
||||||
|
|
||||||
def getStatement(): Statement = {
|
def getStatement(): Statement = {
|
||||||
con.createStatement()
|
con.createStatement()
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
|
||||||
"contentType varchar, " +
|
"contentType varchar, " +
|
||||||
"contentLevel varchar, " +
|
"contentLevel varchar, " +
|
||||||
"contentInput varchar, " +
|
"contentInput varchar, " +
|
||||||
|
"size varchar, " +
|
||||||
"image blob, " +
|
"image blob, " +
|
||||||
"attempted int default 0, " +
|
"attempted int default 0, " +
|
||||||
"PRIMARY KEY(token));" +
|
"PRIMARY KEY(token));" +
|
||||||
|
|
@ -37,8 +38,8 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
|
||||||
|
|
||||||
val insertPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
val insertPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
||||||
"INSERT INTO " +
|
"INSERT INTO " +
|
||||||
"challenge(id, secret, provider, contentType, contentLevel, contentInput, image) " +
|
"challenge(id, secret, provider, contentType, contentLevel, contentInput, size, image) " +
|
||||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
Statement.RETURN_GENERATED_KEYS
|
Statement.RETURN_GENERATED_KEYS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -70,6 +71,18 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
|
||||||
"WHERE token = ?;"
|
"WHERE token = ?;"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val countForParameterPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
||||||
|
s"""
|
||||||
|
SELECT count(*) as count
|
||||||
|
FROM challenge
|
||||||
|
WHERE attempted < $maxAttempts AND
|
||||||
|
contentLevel = ? AND
|
||||||
|
contentType = ? AND
|
||||||
|
contentInput = ? AND
|
||||||
|
size = ?
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
val tokenPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
val tokenPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
||||||
s"""
|
s"""
|
||||||
SELECT token, attempted
|
SELECT token, attempted
|
||||||
|
|
@ -77,8 +90,11 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
|
||||||
WHERE attempted < $maxAttempts AND
|
WHERE attempted < $maxAttempts AND
|
||||||
contentLevel = ? AND
|
contentLevel = ? AND
|
||||||
contentType = ? AND
|
contentType = ? AND
|
||||||
contentInput = ?
|
contentInput = ? AND
|
||||||
ORDER BY attempted ASC LIMIT 1"""
|
size = ?
|
||||||
|
LIMIT 1
|
||||||
|
OFFSET FLOOR(RAND()*?)
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
val deleteAnswerPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
val deleteAnswerPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
||||||
|
|
@ -107,6 +123,14 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
|
||||||
"SELECT * FROM mapId"
|
"SELECT * FROM mapId"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val shutdown: PreparedStatement = dbConn.con.prepareStatement(
|
||||||
|
"SHUTDOWN"
|
||||||
|
)
|
||||||
|
|
||||||
|
val shutdownCompact: PreparedStatement = dbConn.con.prepareStatement(
|
||||||
|
"SHUTDOWN COMPACT"
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Statements {
|
object Statements {
|
||||||
|
|
@ -118,6 +142,6 @@ object Statements {
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
private val dbConn: DBConn = new DBConn()
|
private val dbConn: DBConn = new DBConn()
|
||||||
private val maxAttempts = 10
|
var maxAttempts: Int = 10
|
||||||
val tlStmts: ThreadLocal[Statements] = ThreadLocal.withInitial(() => new Statements(dbConn, maxAttempts))
|
val tlStmts: ThreadLocal[Statements] = ThreadLocal.withInitial(() => new Statements(dbConn, maxAttempts))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
package lc.server
|
package lc.server
|
||||||
|
|
||||||
import org.json4s.jackson.JsonMethods.parse
|
import org.json4s.jackson.JsonMethods.parse
|
||||||
import lc.core.Captcha
|
import org.json4s.jvalue2extractable
|
||||||
|
import lc.core.CaptchaManager
|
||||||
import lc.core.ErrorMessageEnum
|
import lc.core.ErrorMessageEnum
|
||||||
import lc.core.{Answer, ByteConvert, Error, Id, Parameters}
|
import lc.core.{Answer, ByteConvert, Error, Id, Parameters}
|
||||||
import lc.core.Config.formats
|
import lc.core.Config.formats
|
||||||
|
|
@ -12,7 +13,13 @@ import java.net.InetSocketAddress
|
||||||
import java.util
|
import java.util
|
||||||
import scala.jdk.CollectionConverters._
|
import scala.jdk.CollectionConverters._
|
||||||
|
|
||||||
class Server(address: String, port: Int, captcha: Captcha, playgroundEnabled: Boolean, corsHeader: String) {
|
class Server(
|
||||||
|
address: String,
|
||||||
|
port: Int,
|
||||||
|
captchaManager: CaptchaManager,
|
||||||
|
playgroundEnabled: Boolean,
|
||||||
|
corsHeader: String
|
||||||
|
) {
|
||||||
var headerMap: util.Map[String, util.List[String]] = _
|
var headerMap: util.Map[String, util.List[String]] = _
|
||||||
if (corsHeader.nonEmpty) {
|
if (corsHeader.nonEmpty) {
|
||||||
headerMap = Map("Access-Control-Allow-Origin" -> List(corsHeader).asJava).asJava
|
headerMap = Map("Access-Control-Allow-Origin" -> List(corsHeader).asJava).asJava
|
||||||
|
|
@ -22,22 +29,22 @@ class Server(address: String, port: Int, captcha: Captcha, playgroundEnabled: Bo
|
||||||
.address(new InetSocketAddress(address, port))
|
.address(new InetSocketAddress(address, port))
|
||||||
.backlog(32)
|
.backlog(32)
|
||||||
.POST(
|
.POST(
|
||||||
"/v1/captcha",
|
"/v2/captcha",
|
||||||
(request) => {
|
(request) => {
|
||||||
val json = parse(request.getBodyString())
|
val json = parse(request.getBodyString())
|
||||||
val param = json.extract[Parameters]
|
val param = json.extract[Parameters]
|
||||||
val id = captcha.getChallenge(param)
|
val id = captchaManager.getChallenge(param)
|
||||||
getResponse(id, headerMap)
|
getResponse(id, headerMap)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.GET(
|
.GET(
|
||||||
"/v1/media",
|
"/v2/media",
|
||||||
(request) => {
|
(request) => {
|
||||||
val params = request.getQueryParams()
|
val params = request.getQueryParams()
|
||||||
val result = if (params.containsKey("id")) {
|
val result = if (params.containsKey("id")) {
|
||||||
val paramId = params.get("id").get(0)
|
val paramId = params.get("id").get(0)
|
||||||
val id = Id(paramId)
|
val id = Id(paramId)
|
||||||
captcha.getCaptcha(id)
|
captchaManager.getCaptcha(id)
|
||||||
} else {
|
} else {
|
||||||
Left(Error(ErrorMessageEnum.INVALID_PARAM.toString + "=> id"))
|
Left(Error(ErrorMessageEnum.INVALID_PARAM.toString + "=> id"))
|
||||||
}
|
}
|
||||||
|
|
@ -45,11 +52,11 @@ class Server(address: String, port: Int, captcha: Captcha, playgroundEnabled: Bo
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.POST(
|
.POST(
|
||||||
"/v1/answer",
|
"/v2/answer",
|
||||||
(request) => {
|
(request) => {
|
||||||
val json = parse(request.getBodyString())
|
val json = parse(request.getBodyString())
|
||||||
val answer = json.extract[Answer]
|
val answer = json.extract[Answer]
|
||||||
val result = captcha.checkAnswer(answer)
|
val result = captchaManager.checkAnswer(answer)
|
||||||
getResponse(result, headerMap)
|
getResponse(result, headerMap)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -62,11 +69,28 @@ class Server(address: String, port: Int, captcha: Captcha, playgroundEnabled: Bo
|
||||||
new StringResponse(200, str)
|
new StringResponse(200, str)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
serverBuilder.GET(
|
||||||
|
"/",
|
||||||
|
(_) => {
|
||||||
|
val str = """
|
||||||
|
<html>
|
||||||
|
<h2>Welcome to LibreCaptcha server</h2>
|
||||||
|
<h3><a href="/demo/index.html">Link to Demo</a></h3>
|
||||||
|
<h3>API is served at <b>/v2/</b></h3>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
new StringResponse(200, str)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
println("Playground enabled on /demo/index.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
val server: picoserve.Server = serverBuilder.build()
|
val server: picoserve.Server = serverBuilder.build()
|
||||||
|
|
||||||
private def getResponse(response: Either[Error, ByteConvert], responseHeaders: util.Map[String, util.List[String]]): ByteResponse = {
|
private def getResponse(
|
||||||
|
response: Either[Error, ByteConvert],
|
||||||
|
responseHeaders: util.Map[String, util.List[String]]
|
||||||
|
): ByteResponse = {
|
||||||
response match {
|
response match {
|
||||||
case Right(value) => {
|
case Right(value) => {
|
||||||
new ByteResponse(200, value.toBytes(), responseHeaders)
|
new ByteResponse(200, value.toBytes(), responseHeaders)
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,17 @@
|
||||||
"port" : 8888,
|
"port" : 8888,
|
||||||
"address" : "0.0.0.0",
|
"address" : "0.0.0.0",
|
||||||
"captchaExpiryTimeLimit" : 5,
|
"captchaExpiryTimeLimit" : 5,
|
||||||
"throttle" : 10,
|
"bufferCount" : 10,
|
||||||
"threadDelay" : 2,
|
"threadDelay" : 2,
|
||||||
"playgroundEnabled" : false,
|
"playgroundEnabled" : false,
|
||||||
"corsHeader" : "*",
|
"corsHeader" : "*",
|
||||||
|
"maxAttemptsRatio" : 0.01,
|
||||||
"captchas" : [ {
|
"captchas" : [ {
|
||||||
"name" : "DebugCaptcha",
|
"name" : "DebugCaptcha",
|
||||||
"allowedLevels" : [ "debug" ],
|
"allowedLevels" : [ "debug" ],
|
||||||
"allowedMedia" : [ "image/png" ],
|
"allowedMedia" : [ "image/png" ],
|
||||||
"allowedInputType" : [ "text" ],
|
"allowedInputType" : [ "text" ],
|
||||||
|
"allowedSizes" : [ "350x100" ],
|
||||||
"config" : { }
|
"config" : { }
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ class QuickStartUser(SequentialTaskSet):
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def captcha(self):
|
def captcha(self):
|
||||||
captcha_params = {"level":"debug","media":"image/png","input_type":"text"}
|
captcha_params = {"level":"debug","media":"image/png","input_type":"text", "size":"350x100"}
|
||||||
|
|
||||||
with self.client.post(path="/v1/captcha", json=captcha_params, name="/captcha", catch_response = True) as resp:
|
with self.client.post(path="/v2/captcha", json=captcha_params, name="/captcha", catch_response = True) as resp:
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
resp.failure("Status was not 200: " + resp.text)
|
resp.failure("Status was not 200: " + resp.text)
|
||||||
captchaJson = resp.json()
|
captchaJson = resp.json()
|
||||||
|
|
@ -32,7 +32,7 @@ class QuickStartUser(SequentialTaskSet):
|
||||||
if not uuid:
|
if not uuid:
|
||||||
resp.failure("uuid not returned on /captcha endpoint: " + resp.text)
|
resp.failure("uuid not returned on /captcha endpoint: " + resp.text)
|
||||||
|
|
||||||
with self.client.get(path="/v1/media?id=%s" % uuid, name="/media", stream=True, catch_response = True) as resp:
|
with self.client.get(path="/v2/media?id=%s" % uuid, name="/media", stream=True, catch_response = True) as resp:
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
resp.failure("Status was not 200: " + resp.text)
|
resp.failure("Status was not 200: " + resp.text)
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ class QuickStartUser(SequentialTaskSet):
|
||||||
ocrAnswer = self.solve(uuid, media)
|
ocrAnswer = self.solve(uuid, media)
|
||||||
|
|
||||||
answerBody = {"answer": ocrAnswer,"id": uuid}
|
answerBody = {"answer": ocrAnswer,"id": uuid}
|
||||||
with self.client.post(path='/v1/answer', json=answerBody, name="/answer", catch_response=True) as resp:
|
with self.client.post(path='/v2/answer', json=answerBody, name="/answer", catch_response=True) as resp:
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
resp.failure("Status was not 200: " + resp.text)
|
resp.failure("Status was not 200: " + resp.text)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,9 @@ class QuickStartUser(SequentialTaskSet):
|
||||||
@task
|
@task
|
||||||
def captcha(self):
|
def captcha(self):
|
||||||
# TODO: Iterate over parameters for a more comprehensive test
|
# TODO: Iterate over parameters for a more comprehensive test
|
||||||
captcha_params = {"level":"easy","media":"image/png","input_type":"text"}
|
captcha_params = {"level":"easy","media":"image/png","input_type":"text", "size":"350x100"}
|
||||||
|
|
||||||
resp = self.client.post(path="/v1/captcha", json=captcha_params, name="/captcha")
|
resp = self.client.post(path="/v2/captcha", json=captcha_params, name="/captcha")
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
print("\nError on /captcha endpoint: ")
|
print("\nError on /captcha endpoint: ")
|
||||||
print(resp)
|
print(resp)
|
||||||
|
|
@ -36,14 +36,14 @@ class QuickStartUser(SequentialTaskSet):
|
||||||
uuid = json.loads(resp.text).get("id")
|
uuid = json.loads(resp.text).get("id")
|
||||||
answerBody = {"answer": "qwer123","id": uuid}
|
answerBody = {"answer": "qwer123","id": uuid}
|
||||||
|
|
||||||
resp = self.client.get(path="/v1/media?id=%s" % uuid, name="/media")
|
resp = self.client.get(path="/v2/media?id=%s" % uuid, name="/media")
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
print("\nError on /media endpoint: ")
|
print("\nError on /media endpoint: ")
|
||||||
print(resp)
|
print(resp)
|
||||||
print(resp.text)
|
print(resp.text)
|
||||||
print("----------------END.MEDIA-------------------\n\n")
|
print("----------------END.MEDIA-------------------\n\n")
|
||||||
|
|
||||||
resp = self.client.post(path='/v1/answer', json=answerBody, name="/answer")
|
resp = self.client.post(path='/v2/answer', json=answerBody, name="/answer")
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
print("\nError on /answer endpoint: ")
|
print("\nError on /answer endpoint: ")
|
||||||
print(resp)
|
print(resp)
|
||||||
|
|
|
||||||
|
|
@ -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-2.13/LibreCaptcha.jar &
|
java -jar target/scala-3.6.2/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-2.13/LibreCaptcha.jar &
|
java -jar target/scala-3.6.2/LibreCaptcha.jar &
|
||||||
JAVA_PID=$!
|
JAVA_PID=$!
|
||||||
sleep 4
|
sleep 4
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue