Compare commits
486 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 | ||
|
|
51a1f0a710 | ||
|
|
5f77eee1ac | ||
|
|
ab1e09142a | ||
|
|
529e482a17 | ||
|
|
abc9f5440f | ||
|
|
6b2d565601 | ||
|
|
928fc786cf | ||
|
|
99ee7c55b5 | ||
|
|
8f606da92f | ||
|
|
a0afba6fa5 | ||
|
|
b46f6795ce | ||
|
|
f83289514b | ||
|
|
26d86bca4c | ||
|
|
d30249a89f | ||
|
|
1d26b76b01 | ||
|
|
6f6c6eda66 | ||
|
|
ab9c0e6247 | ||
|
|
bbe7c8b858 | ||
|
|
b5a33d2e42 | ||
|
|
c914484b42 | ||
|
|
ecff4087ad | ||
|
|
6f38a77c2f | ||
|
|
1a41724fdc | ||
|
|
e3241ff7ac | ||
|
|
6196a34aae | ||
|
|
1708347504 | ||
|
|
04755c0a07 | ||
|
|
30bb26473e | ||
|
|
0d33f51f9e | ||
|
|
d62951fa51 | ||
|
|
aa5e7da0bc | ||
|
|
e38c3b680a | ||
|
|
98c304ccd4 | ||
|
|
d3d5296ccd | ||
|
|
241be1631c | ||
|
|
8f0f29c579 | ||
|
|
8ea0652331 | ||
|
|
3682b2cb7d | ||
|
|
ef31ee8a57 | ||
|
|
9e7efc1cf3 | ||
|
|
f5a262cf50 | ||
|
|
ed3afbc47e | ||
|
|
9b978212dc | ||
|
|
8595b2cc79 | ||
|
|
c362ed5cb0 | ||
|
|
d81cf17a08 | ||
|
|
7ad164e3c2 | ||
|
|
d32d836475 | ||
|
|
d2ef8c5259 | ||
|
|
46012b2ce3 | ||
|
|
523805263a | ||
|
|
da1b7eee23 | ||
|
|
6189ffce89 | ||
|
|
f38d6ee191 | ||
|
|
2885decb56 | ||
|
|
7684b68efd | ||
|
|
4335740bfc | ||
|
|
a942cd2a93 | ||
|
|
d02a3504b7 | ||
|
|
32169dbe80 | ||
|
|
55288d3346 | ||
|
|
43331f8dd7 | ||
|
|
3a1b01688a | ||
|
|
660447798f | ||
|
|
8fd294f0cf | ||
|
|
352424e8f5 | ||
|
|
433621f046 | ||
|
|
d3a2c6fa35 | ||
|
|
321bc67897 | ||
|
|
ab64bb217c | ||
|
|
caf3669bd9 | ||
|
|
1d746f7655 | ||
|
|
96b5808628 | ||
|
|
1ff4a30da7 | ||
|
|
b765399f68 | ||
|
|
41bdbc7fbf | ||
|
|
954399042c | ||
|
|
332bb2113b | ||
|
|
328f046379 | ||
|
|
3845645f9a | ||
|
|
c0ac570746 | ||
|
|
68dcfb1e49 | ||
|
|
a51defd2c7 | ||
|
|
3cfba7a08e | ||
|
|
d9fefca841 | ||
|
|
dd1129b484 | ||
|
|
2d7f229d30 | ||
|
|
f8de5a5932 | ||
|
|
79428bc5e7 | ||
|
|
e8416ff70e | ||
|
|
8840a13a8f | ||
|
|
43e1a379ac | ||
|
|
fb400a6aea | ||
|
|
6fd34c16ac | ||
|
|
89eeb76c46 | ||
|
|
4612dfa1cd | ||
|
|
ff666396b8 | ||
|
|
12668d3785 | ||
|
|
02e9115641 | ||
|
|
7e87f6721b | ||
|
|
c9abe141ec | ||
|
|
30f5705aa5 | ||
|
|
5226e0032e | ||
|
|
f02eadb945 | ||
|
|
e79da3b881 | ||
|
|
6ea5691c5e | ||
|
|
72f092e6b5 | ||
|
|
19fc5f98f9 | ||
|
|
e9ff454be5 | ||
|
|
5aad8d2135 | ||
|
|
87f1bdd72e | ||
|
|
f4154de561 |
|
|
@ -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
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
name: Scala CI
|
name: Core CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
|
|
@ -17,7 +16,10 @@ 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
|
run: sbt test assembly
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: sbt "scalafixAll --check"
|
run: sbt "scalafixAll --check"
|
||||||
|
- name: Run locust tests
|
||||||
|
run: sudo apt-get install -y tesseract-ocr && ./tests/run.sh
|
||||||
|
|
@ -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,6 +1,5 @@
|
||||||
/*.log
|
/*.log
|
||||||
/*.png
|
/*.png
|
||||||
**/*.db
|
|
||||||
/bin/
|
/bin/
|
||||||
/project/**
|
/project/**
|
||||||
/target/
|
/target/
|
||||||
|
|
@ -8,8 +7,13 @@
|
||||||
.bloop
|
.bloop
|
||||||
.metals
|
.metals
|
||||||
.vscode
|
.vscode
|
||||||
|
.bsp
|
||||||
|
|
||||||
|
# for python test env
|
||||||
|
/testEnv/
|
||||||
|
|
||||||
# for various captcha
|
# for various captcha
|
||||||
/known/
|
/known/
|
||||||
/unknown/
|
/unknown/
|
||||||
/lib/fonts/
|
/lib/fonts/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
rules=[
|
rules=[
|
||||||
ExplicitResultTypes,
|
|
||||||
RemoveUnused,
|
|
||||||
DisableSyntax,
|
DisableSyntax,
|
||||||
LeakingImplicitClassVal,
|
LeakingImplicitClassVal,
|
||||||
NoValInForComprehension,
|
NoValInForComprehension,
|
||||||
ProcedureSyntax
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
version=2.5.2
|
version="3.8.3"
|
||||||
maxColumn = 120
|
maxColumn = 120
|
||||||
|
runner.dialect = scala3
|
||||||
|
|
|
||||||
17
Dockerfile
17
Dockerfile
|
|
@ -1,12 +1,13 @@
|
||||||
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
|
||||||
|
|
||||||
ENV PATH=$PATH:/sbt/bin/
|
ENV PATH=$PATH:/sbt/bin/
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
165
README.md
165
README.md
|
|
@ -1,16 +1,40 @@
|
||||||
# LibreCaptcha
|
# LibreCaptcha
|
||||||
LibreCaptcha is a framework that allows developers to create their own [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA)s.
|
LibreCaptcha is a framework that allows developers to create their own [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA)s.
|
||||||
It allows developers to easily create new types of CAPTCHAs by defining a structure for them. Mundane details are handled by the
|
The framework defines the API for a CAPTCHA generator and takes care of mundane details
|
||||||
framework itself. Details such as:
|
such as:
|
||||||
* Background workers to render CAPTCHAs and to store them in a database
|
* An HTTP interface for serving CAPTCHAs
|
||||||
* Providing an HTTP interface for serving CAPTCHAs
|
* Background workers to pre-compute CAPTCHAs and to store them in a database
|
||||||
* Managing secrets for the CAPTCHAs (tokens, expected answers, etc)
|
* Managing secrets for the CAPTCHAs (tokens, expected answers, etc)
|
||||||
* Safe re-impressions of CAPTCHA images (by creating unique tokens for every impression)
|
* Safe re-impressions of CAPTCHA images (by creating unique tokens for every impression)
|
||||||
* Sandboxed plugin architecture (To be done)
|
* Garbage collection of stale CAPTCHAs
|
||||||
|
* Sandboxed plugin architecture (TBD)
|
||||||
|
|
||||||
|
Some sample CAPTCHA generators are included in the distribution (see below). We will continue adding more samples to the list. For quick
|
||||||
|
deployments the samples themselves might be sufficient. Projects with more resources might want create their own CAPTCHAs
|
||||||
|
and use the samples as inspiration. See the [CAPTCHA creation guide](https://github.com/librecaptcha/lc-core/wiki/Creating-your-own-CAPTCHA-provider).
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
The framework is stable, but since it is our first public release, we recommend using it only on small to medium scale
|
||||||
|
web apps.
|
||||||
|
|
||||||
|
The sample CAPTCHAs are also just that, samples. They have not been tested against bots or CAPTCHA crackers yet.
|
||||||
|
|
||||||
|
## Quick start with Java
|
||||||
|
|
||||||
|
1. Download the `jar` file from the latest release
|
||||||
|
2. Type `mkdir data/`.
|
||||||
|
(The data directory is used to store a config file that you can tweak, and for storing the Database)
|
||||||
|
3. Type `java -jar LibreCaptcha.jar`
|
||||||
|
4. Open [localhost:8888/demo/index.html](http://localhost:8888/demo/index.html) in browser
|
||||||
|
|
||||||
|
We recommend a Java 11+ runtime as that's what we compile the code with.
|
||||||
|
|
||||||
|
Alternatively,
|
||||||
|
1. Install [sbt](https://www.scala-sbt.org/)
|
||||||
|
2. Clone this repository
|
||||||
|
3. Type `sbt run` within the repository
|
||||||
|
4. Open [localhost:8888/demo/index.html](http://localhost:8888/demo/index.html) in browser
|
||||||
|
|
||||||
Some sample CAPTCHA generators are included in the distribution. We will continue adding more samples to the list. For quick
|
|
||||||
deployments the samples themselves might be sufficient. Projects with more resources could create their own CAPTCHAs
|
|
||||||
and use the samples as inspiration.
|
|
||||||
|
|
||||||
## Quick start with Docker
|
## Quick start with Docker
|
||||||
Using `docker-compose`:
|
Using `docker-compose`:
|
||||||
|
|
@ -23,17 +47,35 @@ 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.
|
||||||
|
|
||||||
To test the installation, try:
|
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:
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -d '{"media":"image/png","level":"easy","input_type":"text"}' localhost:8888/v1/captcha
|
podman run -p=8888:8888 -v ./lcdata:/lc-core/data docker.io/librecaptcha/lc-core:2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Quick test
|
||||||
|
Open [localhost:8888/demo/index.html](http://localhost:8888/demo/index.html) in browser.
|
||||||
|
|
||||||
|
Alternatively, on the command line, try:
|
||||||
|
|
||||||
|
```
|
||||||
|
> $ curl -d '{"media":"image/png","level":"easy","input_type":"text","size":"350x100"}' localhost:8888/v2/captcha
|
||||||
|
{"id":"3bf928ce-a1e7-4616-b34f-8252d777855d"}
|
||||||
|
|
||||||
|
> $ curl "localhost:8888/v1/media?id=3bf928ce-a1e7-4616-b34f-8252d777855d" -o sample.png
|
||||||
|
|
||||||
|
> $ file sample.png
|
||||||
|
sample.png: PNG image data, 350 x 100, 8-bit/color RGB, non-interlaced
|
||||||
|
```
|
||||||
|
|
||||||
|
The API endpoints are described at the end of this file.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
If a `config.json` file is not present in the `data/` folder, the app creates one, and this can be modified
|
If a `config.json` file is not present in the `data/` folder, the app creates one, and this can be modified
|
||||||
to customize the app features, such as which CAPTCHAs are enabled and their difficulty settings.
|
to customize the app features, such as which CAPTCHAs are enabled and their difficulty settings.
|
||||||
|
|
@ -56,68 +98,115 @@ 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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
An image of a random string of alphabets is created. Then a series of image filters that add effecs such as Smear, Diffuse, and Ripple are applied to the image to make it less readable.
|
An image of a random string of alphabets is created. Then a series of image filters that add effects such as Smear, Diffuse, and Ripple are applied to the image to make it less readable.
|
||||||
|
|
||||||
### RainDropsCaptcha
|
### RainDropsCaptcha
|
||||||

|

|
||||||
|
|
||||||
### BlurCaptcha
|
### PoppingCharactersCaptcha
|
||||||
An image of a word is blurred before being shown to the user.
|

|
||||||
|
|
||||||
### LabelCaptcha
|
### LabelCaptcha
|
||||||
An image that has a pair of words is created. The answer to one of the words is known and to that of the other is unknown. The user is tested on the known word, and their answer to the unknown word is recorded. If a sufficient number of users agree on their answer to the unknown word, it is transferred to the list of known words.
|
This CAPTCHA provider takes in two sets of images. One with known labels, and the other unknown.
|
||||||
|
The created image has a pair of words one from each set.
|
||||||
|
The user is tested on the known word, and their answer to the unknown word is recorded.
|
||||||
|
If a sufficient number of users agree on their answer to the unknown word, it is transferred to the list of known words.
|
||||||
|
|
||||||
|
(There is a known issue with this provider; see issue #68 )
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
## HTTP API
|
## HTTP API
|
||||||
|
|
||||||
|
The service can be accessed using a simple HTTP API.
|
||||||
|
|
||||||
### - `/v1/captcha`: `POST`
|
### - `/v1/captcha`: `POST`
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `level`: `String` -
|
- `level`: `String` -
|
||||||
The difficulty level of a captcha
|
The difficulty level of a captcha
|
||||||
- easy
|
- easy
|
||||||
- medium
|
- medium
|
||||||
- hard
|
- hard
|
||||||
- `input_type`: `String` -
|
- `input_type`: `String` -
|
||||||
The type of input option for a captcha
|
The type of input option for a captcha
|
||||||
- text
|
- text
|
||||||
- (More to come)
|
- (More to come)
|
||||||
- `media`: `String` -
|
- `media`: `String` -
|
||||||
The type of media of a captcha
|
The type of media of a captcha
|
||||||
- image/png
|
- image/png
|
||||||
- image/gif
|
- image/gif
|
||||||
- (More to come)
|
- (More to come)
|
||||||
- `size`: `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.
|
||||||
|
|
||||||
- Return type:
|
- Returns:
|
||||||
- `id`: `String` - The uuid of the captcha generated
|
- `id`: `String` - The uuid of the captcha generated
|
||||||
|
|
||||||
|
|
||||||
### - `/v1/media`: `GET`
|
### - `/v1/media`: `GET`
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `id`: `String` - The uuid of the captcha
|
- `id`: `String` - The uuid of the captcha
|
||||||
|
|
||||||
- Return type:
|
- Returns:
|
||||||
- `image`: `Array[Byte]` - The requested media as bytes
|
- `image`: `Array[Byte]` - The requested media as bytes
|
||||||
|
|
||||||
|
|
||||||
### - `/v1/answer`: `POST`
|
### - `/v1/answer`: `POST`
|
||||||
- Parameter:
|
- Parameter:
|
||||||
- `id`: `String` - The uuid of the captcha that needs to be solved
|
- `id`: `String` - The uuid of the captcha that needs to be solved
|
||||||
- `answer`: `String` - The answer to the captcha that needs to be validated
|
- `answer`: `String` - The answer to the captcha that needs to be validated
|
||||||
|
|
||||||
- Return Type:
|
- Returns:
|
||||||
- `result`: `String` - The result after validation/checking of the answer
|
- `result`: `String` - The result after validation/checking of the answer
|
||||||
- True - If the answer is correct
|
- True - If the answer is correct
|
||||||
- False - If the answer is incorrect
|
- False - If the answer is incorrect
|
||||||
- Expired - If the time limit to solve the captcha exceeds
|
- Expired - If the time limit to solve the captcha exceeds
|
||||||
|
|
||||||
|
|
||||||
|
## Example usage
|
||||||
|
|
||||||
|
In javascript:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const resp = await fetch("/v2/captcha", {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({level: "easy", media: "image/png", "input_type" : "text", size: "350x100"})
|
||||||
|
})
|
||||||
|
|
||||||
|
const respJson = await resp.json();
|
||||||
|
|
||||||
|
let captchaId = null;
|
||||||
|
|
||||||
|
if (resp.ok) {
|
||||||
|
// The CAPTCHA can be displayed using the data in respJson.
|
||||||
|
console.log(respJson);
|
||||||
|
// Store the id somewhere so that it can be used later for answer verification
|
||||||
|
captchaId = respJson.id;
|
||||||
|
} else {
|
||||||
|
console.err(respJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// When user submits an answer it can be sent to the server for verification thusly:
|
||||||
|
const resp = await fetch("/v2/answer", {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({id: captchaId, answer: "user input"})
|
||||||
|
});
|
||||||
|
const respJson = await resp.json();
|
||||||
|
console.log(respJson.result);
|
||||||
|
```
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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/
|
||||||
|
|
||||||
|
|
|
||||||
37
build.sbt
37
build.sbt
|
|
@ -2,29 +2,38 @@ lazy val root = (project in file(".")).settings(
|
||||||
inThisBuild(
|
inThisBuild(
|
||||||
List(
|
List(
|
||||||
organization := "com.example",
|
organization := "com.example",
|
||||||
scalaVersion := "2.13.5",
|
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.12",
|
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.3.0",
|
||||||
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.0.12",
|
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
unmanagedResourceDirectories in Compile += { baseDirectory.value / "lib" }
|
Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" }
|
||||||
scalacOptions ++= List(
|
scalacOptions ++= List(
|
||||||
"-Yrangepos",
|
|
||||||
"-Ywarn-unused",
|
|
||||||
"-deprecation"
|
"-deprecation"
|
||||||
)
|
)
|
||||||
javacOptions += "-g:none"
|
javacOptions += "-g:none"
|
||||||
compileOrder := CompileOrder.JavaThenScala
|
compileOrder := CompileOrder.JavaThenScala
|
||||||
mainClass in assembly := Some("lc.LCFramework")
|
javafmtOnCompile := false
|
||||||
mainClass in (Compile, run) := Some("lc.LCFramework")
|
assembly / mainClass := Some("lc.LCFramework")
|
||||||
assemblyJarName in assembly := "LibreCaptcha.jar"
|
Compile / run / mainClass := Some("lc.LCFramework")
|
||||||
|
assembly / assemblyJarName := "LibreCaptcha.jar"
|
||||||
|
|
||||||
fork in run := true
|
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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Ignore everything in this directory
|
||||||
|
*
|
||||||
|
# Except this file
|
||||||
|
!.gitignore
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
{
|
|
||||||
"randomSeed" : 20,
|
|
||||||
"port" : 8888,
|
|
||||||
"captchaExpiryTimeLimit" : 5,
|
|
||||||
"throttle" : 10,
|
|
||||||
"threadDelay" : 2,
|
|
||||||
"captchas" : [ {
|
|
||||||
"name" : "FilterChallenge",
|
|
||||||
"allowedLevels" : [ "medium", "hard" ],
|
|
||||||
"allowedMedia" : [ "image/png" ],
|
|
||||||
"allowedInputType" : [ "text" ],
|
|
||||||
"config" : { }
|
|
||||||
}, {
|
|
||||||
"name" : "GifCaptcha",
|
|
||||||
"allowedLevels" : [ "hard" ],
|
|
||||||
"allowedMedia" : [ "image/gif" ],
|
|
||||||
"allowedInputType" : [ "text" ],
|
|
||||||
"config" : { }
|
|
||||||
}, {
|
|
||||||
"name" : "ShadowTextCaptcha",
|
|
||||||
"allowedLevels" : [ "easy" ],
|
|
||||||
"allowedMedia" : [ "image/png" ],
|
|
||||||
"allowedInputType" : [ "text" ],
|
|
||||||
"config" : { }
|
|
||||||
}, {
|
|
||||||
"name" : "RainDropsCaptcha",
|
|
||||||
"allowedLevels" : [ "easy", "medium" ],
|
|
||||||
"allowedMedia" : [ "image/gif" ],
|
|
||||||
"allowedInputType" : [ "text" ],
|
|
||||||
"config" : { }
|
|
||||||
} ]
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
version: "3.8"
|
version: "3.6"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
lc-core:
|
lc-core:
|
||||||
container_name: "libre-captcha"
|
container_name: "libre-captcha"
|
||||||
image: librecaptcha/lc-core:latest
|
image: librecaptcha/lc-core: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.4.9
|
sbt.version=1.10.6
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.27")
|
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0")
|
||||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2")
|
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
|
||||||
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.6.0")
|
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0")
|
||||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")
|
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0")
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -1,27 +1,38 @@
|
||||||
import http.client
|
import http.client
|
||||||
import json
|
import json
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
tempDir = os.getenv('XDG_RUNTIME_DIR', '.')
|
||||||
conn = http.client.HTTPConnection('localhost', 8888)
|
conn = http.client.HTTPConnection('localhost', 8888)
|
||||||
conn.request("GET", "/v1/token?email=test")
|
|
||||||
response = conn.getresponse()
|
|
||||||
responseStr = response.read()
|
|
||||||
user = json.loads(responseStr)
|
|
||||||
token = user["token"]
|
|
||||||
|
|
||||||
params = """{
|
params = """{
|
||||||
"level": "medium",
|
"level": "debug",
|
||||||
"media": "image/png",
|
"media": "image/png",
|
||||||
"input_type": "text"
|
"input_type": "text"
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
def getCaptcha():
|
def getCaptcha():
|
||||||
conn.request("POST", "/v1/captcha", body=params, headers={'access-token': user["token"]})
|
conn.request("POST", "/v1/captcha", body=params)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
responseStr = response.read()
|
responseStr = response.read()
|
||||||
return json.loads(responseStr)
|
return json.loads(responseStr)
|
||||||
|
|
||||||
|
def getAndSolve(idStr):
|
||||||
|
conn.request("GET", "/v1/media?id=" + idStr)
|
||||||
|
response = conn.getresponse()
|
||||||
|
|
||||||
|
if response:
|
||||||
|
responseBytes = response.read()
|
||||||
|
fileName = tempDir + "/captcha.png"
|
||||||
|
with open(fileName, "wb") as f:
|
||||||
|
f.write(responseBytes)
|
||||||
|
ocrResult = subprocess.Popen("gocr " + fileName, shell=True, stdout=subprocess.PIPE)
|
||||||
|
ocrAnswer = ocrResult.stdout.readlines()[0].strip().decode()
|
||||||
|
return ocrAnswer
|
||||||
|
|
||||||
def postAnswer(captchaId, ans):
|
def postAnswer(captchaId, ans):
|
||||||
reply = {"answer": ans, "id" : captchaId}
|
reply = {"answer": ans, "id" : captchaId}
|
||||||
conn.request("POST", "/v1/answer", json.dumps(reply))
|
conn.request("POST", "/v1/answer", json.dumps(reply))
|
||||||
|
|
@ -33,6 +44,6 @@ def postAnswer(captchaId, ans):
|
||||||
|
|
||||||
for i in range(0, 10000):
|
for i in range(0, 10000):
|
||||||
captcha = getCaptcha()
|
captcha = getCaptcha()
|
||||||
#print(captcha)
|
|
||||||
captchaId = captcha["id"]
|
captchaId = captcha["id"]
|
||||||
print(i, postAnswer(captchaId, "xyz"))
|
ans = getAndSolve(captchaId)
|
||||||
|
print(i, postAnswer(captchaId, ans))
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
package lc.captchas;
|
package lc.captchas;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
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.HashMap;
|
|
||||||
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.HelperFunctions;
|
import lc.misc.HelperFunctions;
|
||||||
|
import lc.misc.PngImageWriter;
|
||||||
|
|
||||||
public class FontFunCaptcha implements ChallengeProvider {
|
public class FontFunCaptcha implements ChallengeProvider {
|
||||||
|
|
||||||
|
|
@ -18,13 +18,11 @@ public class FontFunCaptcha implements ChallengeProvider {
|
||||||
return "FontFunCaptcha";
|
return "FontFunCaptcha";
|
||||||
}
|
}
|
||||||
|
|
||||||
public HashMap<String, List<String>> supportedParameters() {
|
public Map<String, List<String>> supportedParameters() {
|
||||||
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
|
return Map.of(
|
||||||
supportedParams.put("supportedLevels", List.of("medium"));
|
"supportedLevels", List.of("medium"),
|
||||||
supportedParams.put("supportedMedia", List.of("image/png"));
|
"supportedMedia", List.of("image/png"),
|
||||||
supportedParams.put("supportedInputType", List.of("text"));
|
"supportedInputType", List.of("text"));
|
||||||
|
|
||||||
return supportedParams;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void configure(String config) {
|
public void configure(String config) {
|
||||||
|
|
@ -60,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);
|
||||||
|
|
@ -76,17 +75,21 @@ public class FontFunCaptcha implements ChallengeProvider {
|
||||||
graphics2D.dispose();
|
graphics2D.dispose();
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
try {
|
try {
|
||||||
ImageIO.write(img, "png", baos);
|
PngImageWriter.write(baos, img);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
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,79 +0,0 @@
|
||||||
package lc.captchas;
|
|
||||||
|
|
||||||
import java.awt.Font;
|
|
||||||
import java.awt.Graphics2D;
|
|
||||||
import java.awt.RenderingHints;
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.imageio.stream.ImageOutputStream;
|
|
||||||
import javax.imageio.stream.MemoryCacheImageOutputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import lc.captchas.interfaces.Challenge;
|
|
||||||
import lc.captchas.interfaces.ChallengeProvider;
|
|
||||||
import lc.misc.HelperFunctions;
|
|
||||||
import lc.misc.GifSequenceWriter;
|
|
||||||
|
|
||||||
public class GifCaptcha implements ChallengeProvider {
|
|
||||||
|
|
||||||
private BufferedImage charToImg(String text) {
|
|
||||||
BufferedImage img = new BufferedImage(250, 100, BufferedImage.TYPE_INT_RGB);
|
|
||||||
Font font = new Font("Bradley Hand", Font.ROMAN_BASELINE, 48);
|
|
||||||
Graphics2D graphics2D = img.createGraphics();
|
|
||||||
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
||||||
graphics2D.setRenderingHint(
|
|
||||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
|
||||||
graphics2D.setFont(font);
|
|
||||||
graphics2D.setColor(new Color((int) (Math.random() * 0x1000000)));
|
|
||||||
graphics2D.drawString(text, 45, 45);
|
|
||||||
graphics2D.dispose();
|
|
||||||
return img;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] gifCaptcha(String text) {
|
|
||||||
try {
|
|
||||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
|
||||||
ImageOutputStream output = new MemoryCacheImageOutputStream(byteArrayOutputStream);
|
|
||||||
GifSequenceWriter writer = new GifSequenceWriter(output, 1, 1000, true);
|
|
||||||
for (int i = 0; i < text.length(); i++) {
|
|
||||||
BufferedImage nextImage = charToImg(String.valueOf(text.charAt(i)));
|
|
||||||
writer.writeToSequence(nextImage);
|
|
||||||
}
|
|
||||||
writer.close();
|
|
||||||
output.close();
|
|
||||||
return byteArrayOutputStream.toByteArray();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void configure(String config) {
|
|
||||||
// TODO: Add custom config
|
|
||||||
}
|
|
||||||
|
|
||||||
public HashMap<String, List<String>> supportedParameters() {
|
|
||||||
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
|
|
||||||
supportedParams.put("supportedLevels", List.of("hard"));
|
|
||||||
supportedParams.put("supportedMedia", List.of("image/gif"));
|
|
||||||
supportedParams.put("supportedInputType", List.of("text"));
|
|
||||||
|
|
||||||
return supportedParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Challenge returnChallenge() {
|
|
||||||
String secret = HelperFunctions.randomString(6);
|
|
||||||
return new Challenge(gifCaptcha(secret), "image/gif", secret.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean checkAnswer(String secret, String answer) {
|
|
||||||
return answer.toLowerCase().equals(secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getId() {
|
|
||||||
return "GifCaptcha";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
package lc.captchas;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import javax.imageio.stream.MemoryCacheImageOutputStream;
|
||||||
|
import lc.captchas.interfaces.Challenge;
|
||||||
|
import lc.captchas.interfaces.ChallengeProvider;
|
||||||
|
import lc.misc.GifSequenceWriter;
|
||||||
|
import lc.misc.HelperFunctions;
|
||||||
|
|
||||||
|
public class PoppingCharactersCaptcha implements ChallengeProvider {
|
||||||
|
|
||||||
|
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 graphics2D = img.createGraphics();
|
||||||
|
final var frc = graphics2D.getFontRenderContext();
|
||||||
|
final var advances = new int[text.length() + 1];
|
||||||
|
final var spacing = font.getStringBounds(" ", frc).getWidth() / 3;
|
||||||
|
var currX = 0;
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
final var c = text.charAt(i);
|
||||||
|
advances[i] = currX;
|
||||||
|
currX += font.getStringBounds(String.valueOf(c), frc).getWidth();
|
||||||
|
currX += spacing;
|
||||||
|
}
|
||||||
|
;
|
||||||
|
advances[text.length()] = currX;
|
||||||
|
graphics2D.dispose();
|
||||||
|
return advances;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 graphics2D = img.createGraphics();
|
||||||
|
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
graphics2D.setRenderingHint(
|
||||||
|
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
graphics2D.setFont(font);
|
||||||
|
f.accept(graphics2D);
|
||||||
|
graphics2D.dispose();
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int jitter() {
|
||||||
|
return HelperFunctions.randomNumber(-2, +2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] gifCaptcha(final int width, final int height, final String text) {
|
||||||
|
try {
|
||||||
|
final var fontHeight = (int) (height * 0.5);
|
||||||
|
final Font font = new Font("Arial", Font.ROMAN_BASELINE, fontHeight);
|
||||||
|
final var byteArrayOutputStream = new ByteArrayOutputStream();
|
||||||
|
final var output = new MemoryCacheImageOutputStream(byteArrayOutputStream);
|
||||||
|
final var writer = new GifSequenceWriter(output, 1, 900, true);
|
||||||
|
final var advances = computeOffsets(font, width, height, text);
|
||||||
|
final var expectedWidth = advances[advances.length - 1];
|
||||||
|
final var scale = width / (float) expectedWidth;
|
||||||
|
final var prevColor = Color.getHSBColor(0f, 0f, 0.1f);
|
||||||
|
IntStream.range(0, text.length())
|
||||||
|
.forEach(
|
||||||
|
i -> {
|
||||||
|
final var color =
|
||||||
|
Color.getHSBColor(HelperFunctions.randomNumber(0, 100) / 100.0f, 0.6f, 1.0f);
|
||||||
|
final var nextImage =
|
||||||
|
makeImage(
|
||||||
|
font,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
(g) -> {
|
||||||
|
g.scale(scale, 1);
|
||||||
|
if (i > 0) {
|
||||||
|
final var prevI = (i - 1) % text.length();
|
||||||
|
g.setColor(prevColor);
|
||||||
|
g.drawString(
|
||||||
|
String.valueOf(text.charAt(prevI)),
|
||||||
|
advances[prevI] + jitter(),
|
||||||
|
fontHeight * 1.1f + jitter());
|
||||||
|
}
|
||||||
|
g.setColor(color);
|
||||||
|
g.drawString(
|
||||||
|
String.valueOf(text.charAt(i)),
|
||||||
|
advances[i] + jitter(),
|
||||||
|
fontHeight * 1.1f + jitter());
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
writer.writeToSequence(nextImage);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
writer.close();
|
||||||
|
output.close();
|
||||||
|
return byteArrayOutputStream.toByteArray();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void configure(final String config) {
|
||||||
|
// TODO: Add custom config
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, List<String>> supportedParameters() {
|
||||||
|
return Map.of(
|
||||||
|
"supportedLevels", List.of("hard"),
|
||||||
|
"supportedMedia", List.of("image/gif"),
|
||||||
|
"supportedInputType", List.of("text"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Challenge returnChallenge(String level, String size) {
|
||||||
|
final var secret = HelperFunctions.randomString(6);
|
||||||
|
final int[] size2D = HelperFunctions.parseSize2D(size);
|
||||||
|
final int width = size2D[0];
|
||||||
|
final int height = size2D[1];
|
||||||
|
return new Challenge(gifCaptcha(width, height, secret), "image/gif", secret.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkAnswer(String secret, String answer) {
|
||||||
|
return answer.toLowerCase().equals(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return "PoppingCharactersCaptcha";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,18 @@
|
||||||
package lc.captchas;
|
package lc.captchas;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
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.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import lc.misc.HelperFunctions;
|
|
||||||
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 {
|
||||||
|
|
||||||
|
|
@ -27,57 +24,69 @@ public class ShadowTextCaptcha implements ChallengeProvider {
|
||||||
// TODO: Add custom config
|
// TODO: Add custom config
|
||||||
}
|
}
|
||||||
|
|
||||||
public HashMap<String, List<String>> supportedParameters() {
|
public Map<String, List<String>> supportedParameters() {
|
||||||
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
|
return Map.of(
|
||||||
supportedParams.put("supportedLevels", List.of("easy"));
|
"supportedLevels", List.of("easy"),
|
||||||
supportedParams.put("supportedMedia", List.of("image/png"));
|
"supportedMedia", List.of("image/png"),
|
||||||
supportedParams.put("supportedInputType", List.of("text"));
|
"supportedInputType", List.of("text"));
|
||||||
|
|
||||||
return supportedParams;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkAnswer(String secret, String answer) {
|
public boolean checkAnswer(String secret, String answer) {
|
||||||
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 {
|
||||||
ImageIO.write(img2, "png", baos);
|
PngImageWriter.write(baos, img2);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,56 @@
|
||||||
package lc.misc;
|
package lc.misc;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
public class HelperFunctions {
|
public class HelperFunctions {
|
||||||
|
|
||||||
|
private static Random random = new Random();
|
||||||
|
|
||||||
|
public static synchronized void setSeed(long 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(
|
||||||
RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String randomString(int n) {
|
public static final String safeAlphabets = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||||
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz23456789$#%@&?";
|
public static final String allAlphabets = safeAlphabets + "ILlO";
|
||||||
StringBuilder stringBuilder = new StringBuilder();
|
public static final String safeNumbers = "23456789";
|
||||||
|
public static final String allNumbers = safeNumbers + "10";
|
||||||
|
public static final String specialCharacters = "$#%@&?";
|
||||||
|
public static final String safeAlphaNum = safeAlphabets + safeNumbers;
|
||||||
|
public static final String safeCharacters = safeAlphaNum + specialCharacters;
|
||||||
|
|
||||||
|
public static String randomString(final int n) {
|
||||||
|
return randomString(n, safeCharacters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String randomString(final int n, final String characters) {
|
||||||
|
final StringBuilder stringBuilder = new StringBuilder();
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
int index = (int) (characters.length() * Math.random());
|
int index = randomNumber(characters.length());
|
||||||
stringBuilder.append(characters.charAt(index));
|
stringBuilder.append(characters.charAt(index));
|
||||||
}
|
}
|
||||||
return stringBuilder.toString();
|
return stringBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int randomNumber(int min, int max) {
|
public static synchronized int randomNumber(int min, int max) {
|
||||||
return (int) (Math.random() * ((max - min) + 1)) + min;
|
return random.nextInt((max - min) + 1) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized int randomNumber(int bound) {
|
||||||
|
return random.nextInt(bound);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package lc.misc;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import javax.imageio.IIOImage;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.imageio.ImageTypeSpecifier;
|
||||||
|
import javax.imageio.ImageWriteParam;
|
||||||
|
import javax.imageio.ImageWriter;
|
||||||
|
import javax.imageio.metadata.IIOInvalidTreeException;
|
||||||
|
import javax.imageio.metadata.IIOMetadata;
|
||||||
|
import javax.imageio.metadata.IIOMetadataNode;
|
||||||
|
import javax.imageio.stream.ImageOutputStream;
|
||||||
|
|
||||||
|
public class PngImageWriter {
|
||||||
|
|
||||||
|
static final int DPI = 245;
|
||||||
|
static final double INCH_2_CM = 2.54;
|
||||||
|
|
||||||
|
public static void write(ByteArrayOutputStream boas, BufferedImage gridImage) throws IOException {
|
||||||
|
final String formatName = "png";
|
||||||
|
for (Iterator<ImageWriter> iw = ImageIO.getImageWritersByFormatName(formatName);
|
||||||
|
iw.hasNext(); ) {
|
||||||
|
ImageWriter writer = iw.next();
|
||||||
|
ImageWriteParam writeParam = writer.getDefaultWriteParam();
|
||||||
|
ImageTypeSpecifier typeSpecifier =
|
||||||
|
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
|
||||||
|
IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam);
|
||||||
|
if (metadata.isReadOnly() || !metadata.isStandardMetadataFormatSupported()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDPIMeta(metadata);
|
||||||
|
|
||||||
|
final ImageOutputStream stream = ImageIO.createImageOutputStream(boas);
|
||||||
|
try {
|
||||||
|
writer.setOutput(stream);
|
||||||
|
writer.write(metadata, new IIOImage(gridImage, null, metadata), writeParam);
|
||||||
|
} finally {
|
||||||
|
stream.close();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setDPIMeta(IIOMetadata metadata) throws IIOInvalidTreeException {
|
||||||
|
|
||||||
|
// for PNG, it's dots per millimeter
|
||||||
|
double dotsPerMilli = 1.0 * DPI / 10 / INCH_2_CM;
|
||||||
|
|
||||||
|
IIOMetadataNode horiz = new IIOMetadataNode("HorizontalPixelSize");
|
||||||
|
horiz.setAttribute("value", Double.toString(dotsPerMilli));
|
||||||
|
|
||||||
|
IIOMetadataNode vert = new IIOMetadataNode("VerticalPixelSize");
|
||||||
|
vert.setAttribute("value", Double.toString(dotsPerMilli));
|
||||||
|
|
||||||
|
IIOMetadataNode dim = new IIOMetadataNode("Dimension");
|
||||||
|
dim.appendChild(horiz);
|
||||||
|
dim.appendChild(vert);
|
||||||
|
|
||||||
|
IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0");
|
||||||
|
root.appendChild(dim);
|
||||||
|
|
||||||
|
metadata.mergeTree("javax_imageio_1.0", root);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,289 @@
|
||||||
|
// Distributed under Apache 2 license
|
||||||
|
// Copyright 2021 github.com/hrj
|
||||||
|
|
||||||
|
package org.limium.picoserve;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import com.sun.net.httpserver.HttpHandler;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.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 {
|
||||||
|
private final HttpServer server;
|
||||||
|
|
||||||
|
public static interface Response {
|
||||||
|
public int getCode();
|
||||||
|
|
||||||
|
public byte[] getBytes();
|
||||||
|
|
||||||
|
public Map<String, List<String>> getResponseHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ByteResponse implements Response {
|
||||||
|
private final int code;
|
||||||
|
private final byte[] bytes;
|
||||||
|
private final Map<String, List<String>> responseHeaders;
|
||||||
|
|
||||||
|
public ByteResponse(final int code, final byte[] bytes) {
|
||||||
|
this.code = code;
|
||||||
|
this.bytes = bytes;
|
||||||
|
this.responseHeaders = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteResponse(
|
||||||
|
final int code, final byte[] bytes, final Map<String, List<String>> responseHeaders) {
|
||||||
|
this.code = code;
|
||||||
|
this.bytes = bytes;
|
||||||
|
this.responseHeaders = responseHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return this.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBytes() {
|
||||||
|
return this.bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, List<String>> getResponseHeaders() {
|
||||||
|
return this.responseHeaders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class StringResponse extends ByteResponse {
|
||||||
|
public StringResponse(final int code, final String msg) {
|
||||||
|
super(code, msg.getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringResponse(
|
||||||
|
final int code, final String msg, final Map<String, List<String>> responseHeaders) {
|
||||||
|
super(code, msg.getBytes(), responseHeaders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class Request {
|
||||||
|
final HttpExchange exchange;
|
||||||
|
|
||||||
|
Request(final HttpExchange exchange) {
|
||||||
|
this.exchange = exchange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMethod() {
|
||||||
|
return exchange.getRequestMethod();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, List<String>> getQueryParams() {
|
||||||
|
final var query = exchange.getRequestURI().getQuery();
|
||||||
|
final var params = parseParams(query);
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBody() {
|
||||||
|
try (final var bodyIS = exchange.getRequestBody()) {
|
||||||
|
final var bytes = bodyIS.readAllBytes();
|
||||||
|
bodyIS.close();
|
||||||
|
return bytes;
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBodyString() {
|
||||||
|
return new String(getBody());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public static interface Processor {
|
||||||
|
public Response process(final Request request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Handler {
|
||||||
|
public final String path;
|
||||||
|
public final Processor processor;
|
||||||
|
public final String[] methods;
|
||||||
|
|
||||||
|
public Handler(final String path, final Processor processor) {
|
||||||
|
this.path = path;
|
||||||
|
this.processor = processor;
|
||||||
|
this.methods = new String[] {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Handler(final String path, final String methods, final Processor processor) {
|
||||||
|
this.path = path;
|
||||||
|
this.processor = processor;
|
||||||
|
this.methods = methods.split(",");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Server(
|
||||||
|
final InetSocketAddress addr,
|
||||||
|
final int backlog,
|
||||||
|
final List<Handler> handlers,
|
||||||
|
final Executor executor)
|
||||||
|
throws IOException {
|
||||||
|
this.server = HttpServer.create(addr, backlog);
|
||||||
|
this.server.setExecutor(executor);
|
||||||
|
for (final var handler : handlers) {
|
||||||
|
// System.out.println("Registering handler for " + handler.path);
|
||||||
|
this.server.createContext(
|
||||||
|
handler.path,
|
||||||
|
new HttpHandler() {
|
||||||
|
public void handle(final HttpExchange exchange) {
|
||||||
|
final var method = exchange.getRequestMethod();
|
||||||
|
final Response errorResponse = checkMethods(handler.methods, method);
|
||||||
|
try (final var os = exchange.getResponseBody()) {
|
||||||
|
Response response;
|
||||||
|
if (errorResponse != null) {
|
||||||
|
response = errorResponse;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
response = handler.processor.process(new Request(exchange));
|
||||||
|
} catch (final Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
response = new StringResponse(500, "Error: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final var headersToSend = response.getResponseHeaders();
|
||||||
|
if (headersToSend != null) {
|
||||||
|
final var responseHeaders = exchange.getResponseHeaders();
|
||||||
|
responseHeaders.putAll(headersToSend);
|
||||||
|
}
|
||||||
|
final var bytes = response.getBytes();
|
||||||
|
final var code = response.getCode();
|
||||||
|
exchange.sendResponseHeaders(code, bytes.length);
|
||||||
|
os.write(bytes);
|
||||||
|
os.close();
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
System.out.println("Error: " + ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Response checkMethods(final String[] methods, final String method) {
|
||||||
|
if (methods.length > 0) {
|
||||||
|
var found = false;
|
||||||
|
for (var m : methods) {
|
||||||
|
if (m.equals(method)) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
return new StringResponse(404, "Method Not Accepted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
this.server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop(int delay) {
|
||||||
|
this.server.stop(delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerBuilder builder() {
|
||||||
|
return new ServerBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapted from https://stackoverflow.com/a/37368660
|
||||||
|
private static final Pattern ampersandPattern = Pattern.compile("&");
|
||||||
|
private static final Pattern equalPattern = Pattern.compile("=");
|
||||||
|
private static final Map<String, List<String>> emptyMap = Map.of();
|
||||||
|
|
||||||
|
private static Map<String, List<String>> parseParams(final String query) {
|
||||||
|
if (query == null) {
|
||||||
|
return emptyMap;
|
||||||
|
}
|
||||||
|
final var params =
|
||||||
|
ampersandPattern
|
||||||
|
.splitAsStream(query)
|
||||||
|
.map(s -> Arrays.copyOf(equalPattern.split(s, 2), 2))
|
||||||
|
.collect(
|
||||||
|
Collectors.groupingBy(
|
||||||
|
s -> decode(s[0]), Collectors.mapping(s -> decode(s[1]), Collectors.toList())));
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String decode(final String encoded) {
|
||||||
|
return Optional.ofNullable(encoded)
|
||||||
|
.map(e -> URLDecoder.decode(e, StandardCharsets.UTF_8))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ServerBuilder {
|
||||||
|
private InetSocketAddress mAddress = new InetSocketAddress(9000);
|
||||||
|
private int backlog = 5;
|
||||||
|
private List<Handler> handlers = new LinkedList<Handler>();
|
||||||
|
private Executor executor = null;
|
||||||
|
|
||||||
|
public ServerBuilder port(final int port) {
|
||||||
|
mAddress = new InetSocketAddress(port);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerBuilder backlog(final int backlog) {
|
||||||
|
this.backlog = backlog;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerBuilder address(final InetSocketAddress addr) {
|
||||||
|
mAddress = addr;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerBuilder handle(final Handler handler) {
|
||||||
|
handlers.add(handler);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerBuilder GET(final String path, final Processor processor) {
|
||||||
|
handlers.add(new Handler(path, "GET", request -> processor.process(request)));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerBuilder POST(final String path, final Processor processor) {
|
||||||
|
handlers.add(new Handler(path, "POST", request -> processor.process(request)));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerBuilder PUT(final String path, final Processor processor) {
|
||||||
|
handlers.add(new Handler(path, "PUT", request -> processor.process(request)));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerBuilder DELETE(final String path, final Processor processor) {
|
||||||
|
handlers.add(new Handler(path, "DELETE", request -> processor.process(request)));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerBuilder HEAD(final String path, final Processor processor) {
|
||||||
|
handlers.add(new Handler(path, "HEAD", request -> processor.process(request)));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerBuilder executor(final Executor executor) {
|
||||||
|
this.executor = executor;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Server build() throws IOException {
|
||||||
|
return new Server(mAddress, backlog, handlers, executor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
<html>
|
||||||
|
<style>
|
||||||
|
body {background-color: #e7f7f7;}
|
||||||
|
button {background-color: #b4ffff; font-size:1.66em;}
|
||||||
|
input[type=text] {font-size:1.66em;}
|
||||||
|
div.inputGroup {margin: 1em}
|
||||||
|
div.section {border: .33em solid #0b6767; margin:1em; padding: 1em}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showError(errMessage) {
|
||||||
|
const resultDiv = document.getElementById("result")
|
||||||
|
const result = `
|
||||||
|
<p>Error: ${errMessage}</p>
|
||||||
|
`
|
||||||
|
resultDiv.innerHTML = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCaptcha() {
|
||||||
|
const levelInput = document.getElementById("levelInput").value
|
||||||
|
const mediaInput = document.getElementById("mediaInput").value
|
||||||
|
const typeInput = document.getElementById("typeInput").value
|
||||||
|
const sizeInput = document.getElementById("sizeInput").value
|
||||||
|
fetch("/v2/captcha", {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput, "size": sizeInput})
|
||||||
|
}).then(async function(resp) {
|
||||||
|
const respJson = await resp.json()
|
||||||
|
if (resp.ok) {
|
||||||
|
const id = respJson.id
|
||||||
|
const resultDiv = document.getElementById("result")
|
||||||
|
const result = `
|
||||||
|
<p>Id: ${id}</p>
|
||||||
|
<p><img src="/v2/media?id=${id}" /> </p>
|
||||||
|
<input type="text" id="answerInput" />
|
||||||
|
<button onClick="submitAnswer('${id}')">Submit</button>
|
||||||
|
<div id="answerResult" />
|
||||||
|
`
|
||||||
|
resultDiv.innerHTML = result;
|
||||||
|
} else {
|
||||||
|
showError("Failed with response code: " + resp.status + " response: " + JSON.stringify(respJson))
|
||||||
|
}
|
||||||
|
}).catch(showError)
|
||||||
|
}
|
||||||
|
async function submitAnswer(id) {
|
||||||
|
const ans = document.getElementById("answerInput").value;
|
||||||
|
const resp = await fetch("/v2/answer", {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({id: id, answer: ans})
|
||||||
|
})
|
||||||
|
const respJson = await resp.json()
|
||||||
|
const resultDiv = document.getElementById("answerResult")
|
||||||
|
const result = `
|
||||||
|
<p>${JSON.stringify(respJson)}</p>
|
||||||
|
`
|
||||||
|
resultDiv.innerHTML = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<body>
|
||||||
|
<div class="section">
|
||||||
|
<div class="inputGroup">
|
||||||
|
<span>Level</span>
|
||||||
|
<input type="text" id="levelInput" value="easy"/>
|
||||||
|
</div>
|
||||||
|
<div class="inputGroup">
|
||||||
|
<span>Media</span>
|
||||||
|
<input type="text" id="mediaInput" value="image/png" />
|
||||||
|
</div>
|
||||||
|
<div class="inputGroup">
|
||||||
|
<span>Input Type</span>
|
||||||
|
<input type="text" id="typeInput" value="text" />
|
||||||
|
</div>
|
||||||
|
<div class="inputGroup">
|
||||||
|
<span>Input Size</span>
|
||||||
|
<input type="text" id="sizeInput" value="350x100" />
|
||||||
|
</div>
|
||||||
|
<div class="inputGroup">
|
||||||
|
<button onClick="loadCaptcha()">Get New CAPTCHA</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div id="result">...</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -1,35 +1,59 @@
|
||||||
package lc
|
package lc
|
||||||
|
|
||||||
import lc.core.{Captcha, CaptchaProviders}
|
import lc.core.{CaptchaProviders, CaptchaManager, Config}
|
||||||
import lc.server.Server
|
import lc.server.Server
|
||||||
import lc.background.BackgroundTask
|
import lc.background.BackgroundTask
|
||||||
import lc.core.Config
|
import lc.database.Statements
|
||||||
|
|
||||||
object LCFramework {
|
object LCFramework {
|
||||||
def main(args: scala.Array[String]): Unit = {
|
def main(args: scala.Array[String]): Unit = {
|
||||||
val captcha = new Captcha()
|
val configFilePath = if (args.length > 0) {
|
||||||
val server = new Server(port = Config.port, captcha = captcha)
|
args(0)
|
||||||
val backgroundTask = new BackgroundTask(
|
} else {
|
||||||
captcha = captcha,
|
"data/config.json"
|
||||||
throttle = Config.throttle,
|
}
|
||||||
timeLimit = Config.captchaExpiryTimeLimit
|
val config = new Config(configFilePath)
|
||||||
|
Statements.maxAttempts = config.maxAttempts
|
||||||
|
val captchaProviders = new CaptchaProviders(config = config)
|
||||||
|
val captchaManager = new CaptchaManager(config = config, captchaProviders = captchaProviders)
|
||||||
|
val backgroundTask = new BackgroundTask(config = config, captchaManager = captchaManager)
|
||||||
|
backgroundTask.beginThread(delay = config.threadDelay)
|
||||||
|
val server = new Server(
|
||||||
|
address = config.address,
|
||||||
|
port = config.port,
|
||||||
|
captchaManager = captchaManager,
|
||||||
|
playgroundEnabled = config.playgroundEnabled,
|
||||||
|
corsHeader = config.corsHeader
|
||||||
)
|
)
|
||||||
backgroundTask.beginThread(delay = Config.threadDelay)
|
|
||||||
|
Runtime.getRuntime.addShutdownHook(new Thread {
|
||||||
|
override def run(): Unit = {
|
||||||
|
println("Shutting down gracefully...")
|
||||||
|
backgroundTask.shutdown()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.start()
|
server.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object MakeSamples {
|
object MakeSamples {
|
||||||
def main(args: scala.Array[String]): Unit = {
|
def main(args: scala.Array[String]): Unit = {
|
||||||
val samples = CaptchaProviders.generateChallengeSamples()
|
val configFilePath = if (args.length > 0) {
|
||||||
samples.foreach {
|
args(0)
|
||||||
case (key, sample) =>
|
} else {
|
||||||
val extensionMap = Map("image/png" -> "png", "image/gif" -> "gif")
|
"data/config.json"
|
||||||
println(key + ": " + sample)
|
}
|
||||||
|
val config = new Config(configFilePath)
|
||||||
|
val captchaProviders = new CaptchaProviders(config = config)
|
||||||
|
val samples = captchaProviders.generateChallengeSamples()
|
||||||
|
samples.foreach { case (key, sample) =>
|
||||||
|
val extensionMap = Map("image/png" -> "png", "image/gif" -> "gif")
|
||||||
|
println(key + ": " + sample)
|
||||||
|
|
||||||
val outStream = new java.io.FileOutputStream("samples/" + key + "." + extensionMap(sample.contentType))
|
val outStream = new java.io.FileOutputStream("samples/" + key + "." + extensionMap(sample.contentType))
|
||||||
outStream.write(sample.content)
|
outStream.write(sample.content)
|
||||||
outStream.close
|
outStream.close
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,37 +2,89 @@ 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
|
import lc.core.{CaptchaManager, Config}
|
||||||
import lc.core.{Parameters, Size}
|
import lc.core.{Parameters, Size}
|
||||||
|
import lc.misc.HelperFunctions
|
||||||
|
|
||||||
class BackgroundTask(captcha: Captcha, throttle: Int, timeLimit: Int) {
|
class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
|
||||||
|
|
||||||
private val task = new Runnable {
|
private val task = new Runnable {
|
||||||
def run(): Unit = {
|
def run(): Unit = {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
val mapIdGCPstmt = Statements.tlStmts.get.mapIdGCPstmt
|
val mapIdGCPstmt = Statements.tlStmts.get.mapIdGCPstmt
|
||||||
mapIdGCPstmt.setInt(1, timeLimit)
|
mapIdGCPstmt.setInt(1, config.captchaExpiryTimeLimit)
|
||||||
mapIdGCPstmt.executeUpdate()
|
mapIdGCPstmt.executeUpdate()
|
||||||
|
|
||||||
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 = (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(Parameters("medium", "image/png", "text", Option(Size(0, 0))))
|
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 e: Exception => println(e) }
|
} 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 = {
|
||||||
|
val captcha = pickRandom(config.captchaConfig)
|
||||||
|
val level = pickRandom(captcha.allowedLevels)
|
||||||
|
val media = pickRandom(captcha.allowedMedia)
|
||||||
|
val inputType = pickRandom(captcha.allowedInputType)
|
||||||
|
val size = pickRandom(captcha.allowedSizes)
|
||||||
|
|
||||||
|
Parameters(level, media, inputType, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def pickRandom[T](list: List[T]): T = {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package lc.captchas
|
||||||
|
|
||||||
|
import java.awt.Color
|
||||||
|
import java.awt.Font
|
||||||
|
import java.awt.font.TextLayout
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.util.Map
|
||||||
|
import java.util.List
|
||||||
|
|
||||||
|
import lc.misc.HelperFunctions
|
||||||
|
import lc.captchas.interfaces.Challenge
|
||||||
|
import lc.captchas.interfaces.ChallengeProvider
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
class DebugCaptcha extends ChallengeProvider {
|
||||||
|
|
||||||
|
def getId(): String = {
|
||||||
|
"DebugCaptcha"
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure(config: String): Unit = {
|
||||||
|
// TODO: Add custom config
|
||||||
|
}
|
||||||
|
|
||||||
|
def supportedParameters(): Map[String, List[String]] = {
|
||||||
|
Map.of(
|
||||||
|
"supportedLevels",
|
||||||
|
List.of("debug"),
|
||||||
|
"supportedMedia",
|
||||||
|
List.of("image/png"),
|
||||||
|
"supportedInputType",
|
||||||
|
List.of("text")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def checkAnswer(secret: String, answer: String): Boolean = {
|
||||||
|
val matches = answer.toLowerCase().replaceAll(" ", "").equals(secret)
|
||||||
|
if (!matches) {
|
||||||
|
println(s"Didn't match, answer: '$answer' to secret '$secret'")
|
||||||
|
}
|
||||||
|
matches
|
||||||
|
}
|
||||||
|
|
||||||
|
private def simpleText(width: Int, height: Int, text: String): Array[Byte] = {
|
||||||
|
val img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
|
||||||
|
val font = new Font("Arial", Font.ROMAN_BASELINE, 56)
|
||||||
|
val graphics2D = img.createGraphics()
|
||||||
|
val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext())
|
||||||
|
HelperFunctions.setRenderingHints(graphics2D)
|
||||||
|
graphics2D.setPaint(Color.WHITE)
|
||||||
|
graphics2D.fillRect(0, 0, width, height)
|
||||||
|
graphics2D.setPaint(Color.BLACK)
|
||||||
|
textLayout.draw(graphics2D, 15, 50)
|
||||||
|
graphics2D.dispose()
|
||||||
|
val baos = new ByteArrayOutputStream()
|
||||||
|
try {
|
||||||
|
PngImageWriter.write(baos, img);
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
baos.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
def returnChallenge(level: String, size: String): Challenge = {
|
||||||
|
val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets)
|
||||||
|
val size2D = HelperFunctions.parseSize2D(size)
|
||||||
|
val width = size2D(0)
|
||||||
|
val height = size2D(1)
|
||||||
|
new Challenge(simpleText(width, height, secret), "image/png", secret.toLowerCase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,10 @@ import java.awt.Font
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import lc.captchas.interfaces.ChallengeProvider
|
import lc.captchas.interfaces.ChallengeProvider
|
||||||
import lc.captchas.interfaces.Challenge
|
import lc.captchas.interfaces.Challenge
|
||||||
import scala.jdk.CollectionConverters.MapHasAsJava
|
|
||||||
import java.util.{List => JavaList, Map => JavaMap}
|
import java.util.{List => JavaList, Map => JavaMap}
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import lc.misc.PngImageWriter
|
||||||
|
import lc.misc.HelperFunctions
|
||||||
|
|
||||||
class FilterChallenge extends ChallengeProvider {
|
class FilterChallenge extends ChallengeProvider {
|
||||||
def getId = "FilterChallenge"
|
def getId = "FilterChallenge"
|
||||||
|
|
@ -18,33 +20,49 @@ class FilterChallenge extends ChallengeProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
||||||
val supportedParams = Map(
|
JavaMap.of(
|
||||||
"supportedLevels" -> JavaList.of("medium", "hard"),
|
"supportedLevels",
|
||||||
"supportedMedia" -> JavaList.of("image/png"),
|
JavaList.of("medium", "hard"),
|
||||||
"supportedInputType" -> JavaList.of("text")
|
"supportedMedia",
|
||||||
).asJava
|
JavaList.of("image/png"),
|
||||||
|
"supportedInputType",
|
||||||
supportedParams
|
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)
|
||||||
new Challenge(image.bytes(new nio.PngWriter()), "image/png", secret)
|
val img = image.awt()
|
||||||
|
val baos = new ByteArrayOutputStream()
|
||||||
|
PngImageWriter.write(baos, img);
|
||||||
|
new Challenge(baos.toByteArray, "image/png", secret)
|
||||||
}
|
}
|
||||||
def checkAnswer(secret: String, answer: String): Boolean = {
|
def checkAnswer(secret: String, answer: String): Boolean = {
|
||||||
secret == answer
|
secret == answer
|
||||||
|
|
@ -52,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)
|
||||||
|
|
@ -68,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)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import java.awt.image.BufferedImage
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import lc.captchas.interfaces.ChallengeProvider
|
import lc.captchas.interfaces.ChallengeProvider
|
||||||
import lc.captchas.interfaces.Challenge
|
import lc.captchas.interfaces.Challenge
|
||||||
import scala.jdk.CollectionConverters.MapHasAsJava
|
|
||||||
import java.util.{List => JavaList, Map => JavaMap}
|
import java.util.{List => JavaList, Map => JavaMap}
|
||||||
|
import lc.misc.PngImageWriter
|
||||||
|
|
||||||
class LabelCaptcha extends ChallengeProvider {
|
class LabelCaptcha extends ChallengeProvider {
|
||||||
private var knownFiles = new File("known").list.toList
|
private var knownFiles = new File("known").list.toList
|
||||||
|
|
@ -30,16 +30,17 @@ class LabelCaptcha extends ChallengeProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
||||||
val supportedParams = Map(
|
JavaMap.of(
|
||||||
"supportedLevels" -> JavaList.of("hard"),
|
"supportedLevels",
|
||||||
"supportedMedia" -> JavaList.of("image/png"),
|
JavaList.of("hard"),
|
||||||
"supportedInputType" -> JavaList.of("text")
|
"supportedMedia",
|
||||||
).asJava
|
JavaList.of("image/png"),
|
||||||
|
"supportedInputType",
|
||||||
supportedParams
|
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)
|
||||||
|
|
@ -52,7 +53,7 @@ class LabelCaptcha extends ChallengeProvider {
|
||||||
|
|
||||||
val token = encrypt(knownImageFile + "," + unknownImageFile)
|
val token = encrypt(knownImageFile + "," + unknownImageFile)
|
||||||
val baos = new ByteArrayOutputStream()
|
val baos = new ByteArrayOutputStream()
|
||||||
ImageIO.write(mergedImage, "png", baos)
|
PngImageWriter.write(baos, mergedImage);
|
||||||
|
|
||||||
new Challenge(baos.toByteArray(), "image/png", token)
|
new Challenge(baos.toByteArray(), "image/png", token)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import javax.imageio.stream.MemoryCacheImageOutputStream;
|
||||||
import lc.captchas.interfaces.ChallengeProvider
|
import lc.captchas.interfaces.ChallengeProvider
|
||||||
import lc.captchas.interfaces.Challenge
|
import lc.captchas.interfaces.Challenge
|
||||||
import lc.misc.GifSequenceWriter
|
import lc.misc.GifSequenceWriter
|
||||||
import scala.jdk.CollectionConverters.MapHasAsJava
|
|
||||||
import java.util.{List => JavaList, Map => JavaMap}
|
import java.util.{List => JavaList, Map => JavaMap}
|
||||||
|
import lc.misc.HelperFunctions
|
||||||
|
|
||||||
class Drop {
|
class Drop {
|
||||||
var x = 0
|
var x = 0
|
||||||
|
|
@ -25,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,13 +36,14 @@ class RainDropsCP extends ChallengeProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
||||||
val supportedParams = Map(
|
JavaMap.of(
|
||||||
"supportedLevels" -> JavaList.of("medium", "easy"),
|
"supportedLevels",
|
||||||
"supportedMedia" -> JavaList.of("image/gif"),
|
JavaList.of("medium", "easy"),
|
||||||
"supportedInputType" -> JavaList.of("text")
|
"supportedMedia",
|
||||||
).asJava
|
JavaList.of("image/gif"),
|
||||||
|
"supportedInputType",
|
||||||
supportedParams
|
JavaList.of("text")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def extendDrops(drops: Array[Drop], steps: Int, xOffset: Int) = {
|
private def extendDrops(drops: Array[Drop], steps: Int, xOffset: Int) = {
|
||||||
|
|
@ -56,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
|
||||||
|
|
@ -80,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)
|
||||||
|
|
@ -117,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)
|
||||||
|
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
package lc.core
|
|
||||||
|
|
||||||
import java.sql.{Blob, ResultSet}
|
|
||||||
import java.util.UUID
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import lc.database.Statements
|
|
||||||
import lc.core.CaptchaProviders
|
|
||||||
import lc.captchas.interfaces.ChallengeProvider
|
|
||||||
|
|
||||||
class Captcha {
|
|
||||||
|
|
||||||
def getCaptcha(id: Id): Array[Byte] = {
|
|
||||||
var image: Array[Byte] = null
|
|
||||||
var blob: Blob = null
|
|
||||||
try {
|
|
||||||
val imagePstmt = Statements.tlStmts.get.imagePstmt
|
|
||||||
imagePstmt.setString(1, id.id)
|
|
||||||
val rs: ResultSet = imagePstmt.executeQuery()
|
|
||||||
if (rs.next()) {
|
|
||||||
blob = rs.getBlob("image")
|
|
||||||
if (blob != null) {
|
|
||||||
image = blob.getBytes(1, blob.length().toInt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
image
|
|
||||||
} catch {
|
|
||||||
case e: Exception =>
|
|
||||||
println(e)
|
|
||||||
image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def generateChallenge(param: Parameters): Int = {
|
|
||||||
val provider = CaptchaProviders.getProvider(param)
|
|
||||||
if (!provider.isInstanceOf[ChallengeProvider]) return -1
|
|
||||||
val providerId = provider.getId()
|
|
||||||
val challenge = provider.returnChallenge()
|
|
||||||
val blob = new ByteArrayInputStream(challenge.content)
|
|
||||||
val insertPstmt = Statements.tlStmts.get.insertPstmt
|
|
||||||
insertPstmt.setString(1, provider.getId)
|
|
||||||
insertPstmt.setString(2, challenge.secret)
|
|
||||||
insertPstmt.setString(3, providerId)
|
|
||||||
insertPstmt.setString(4, challenge.contentType)
|
|
||||||
insertPstmt.setString(5, param.level)
|
|
||||||
insertPstmt.setString(6, param.input_type)
|
|
||||||
insertPstmt.setBlob(7, blob)
|
|
||||||
insertPstmt.executeUpdate()
|
|
||||||
val rs: ResultSet = insertPstmt.getGeneratedKeys()
|
|
||||||
val token = if (rs.next()) {
|
|
||||||
rs.getInt("token")
|
|
||||||
}
|
|
||||||
println("Added new challenge: " + token.toString)
|
|
||||||
token.asInstanceOf[Int]
|
|
||||||
}
|
|
||||||
|
|
||||||
val allowedInputType = Config.allowedInputType
|
|
||||||
val allowedLevels = Config.allowedLevels
|
|
||||||
val allowedMedia = Config.allowedMedia
|
|
||||||
|
|
||||||
private def validateParam(param: Parameters): Boolean = {
|
|
||||||
if (
|
|
||||||
allowedLevels.contains(param.level) &&
|
|
||||||
allowedMedia.contains(param.media) &&
|
|
||||||
allowedInputType.contains(param.input_type)
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
def getChallenge(param: Parameters): ChallengeResult = {
|
|
||||||
try {
|
|
||||||
val validParam = validateParam(param)
|
|
||||||
if (validParam) {
|
|
||||||
val tokenPstmt = Statements.tlStmts.get.tokenPstmt
|
|
||||||
tokenPstmt.setString(1, param.level)
|
|
||||||
tokenPstmt.setString(2, param.media)
|
|
||||||
tokenPstmt.setString(3, param.input_type)
|
|
||||||
val rs = tokenPstmt.executeQuery()
|
|
||||||
val tokenOpt = if (rs.next()) {
|
|
||||||
Some(rs.getInt("token"))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
val updateAttemptedPstmt = Statements.tlStmts.get.updateAttemptedPstmt
|
|
||||||
val token = tokenOpt.getOrElse(generateChallenge(param))
|
|
||||||
val result = if (token != -1) {
|
|
||||||
val uuid = getUUID(token)
|
|
||||||
updateAttemptedPstmt.setString(1, uuid)
|
|
||||||
updateAttemptedPstmt.executeUpdate()
|
|
||||||
Id(uuid)
|
|
||||||
} else {
|
|
||||||
Error(ErrorMessageEnum.NO_CAPTCHA.toString)
|
|
||||||
}
|
|
||||||
result
|
|
||||||
} else {
|
|
||||||
Error(ErrorMessageEnum.INVALID_PARAM.toString)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
case e: Exception =>
|
|
||||||
println(e)
|
|
||||||
Error(ErrorMessageEnum.SMW.toString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def getUUID(id: Int): String = {
|
|
||||||
val uuid = UUID.randomUUID().toString
|
|
||||||
val mapPstmt = Statements.tlStmts.get.mapPstmt
|
|
||||||
mapPstmt.setString(1, uuid)
|
|
||||||
mapPstmt.setInt(2, id)
|
|
||||||
mapPstmt.executeUpdate()
|
|
||||||
uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
def checkAnswer(answer: Answer): Result = {
|
|
||||||
val selectPstmt = Statements.tlStmts.get.selectPstmt
|
|
||||||
selectPstmt.setInt(1, Config.captchaExpiryTimeLimit)
|
|
||||||
selectPstmt.setString(2, answer.id)
|
|
||||||
val rs: ResultSet = selectPstmt.executeQuery()
|
|
||||||
val psOpt = if (rs.first()) {
|
|
||||||
val secret = rs.getString("secret")
|
|
||||||
val provider = rs.getString("provider")
|
|
||||||
val check = CaptchaProviders.getProviderById(provider).checkAnswer(secret, answer.answer)
|
|
||||||
val result = if (check) ResultEnum.TRUE.toString else ResultEnum.FALSE.toString
|
|
||||||
result
|
|
||||||
} else {
|
|
||||||
ResultEnum.EXPIRED.toString
|
|
||||||
}
|
|
||||||
val deleteAnswerPstmt = Statements.tlStmts.get.deleteAnswerPstmt
|
|
||||||
deleteAnswerPstmt.setString(1, answer.id)
|
|
||||||
deleteAnswerPstmt.executeUpdate()
|
|
||||||
Result(psOpt)
|
|
||||||
}
|
|
||||||
|
|
||||||
def display(): Unit = {
|
|
||||||
val rs: ResultSet = Statements.tlStmts.get.getChallengeTable.executeQuery()
|
|
||||||
println("token\t\tid\t\tsecret\t\tattempted")
|
|
||||||
while (rs.next()) {
|
|
||||||
val token = rs.getInt("token")
|
|
||||||
val id = rs.getString("id")
|
|
||||||
val secret = rs.getString("secret")
|
|
||||||
val attempted = rs.getString("attempted")
|
|
||||||
println(s"${token}\t\t${id}\t\t${secret}\t\t${attempted}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
val rss: ResultSet = Statements.tlStmts.get.getMapIdTable.executeQuery()
|
|
||||||
println("uuid\t\ttoken\t\tlastServed")
|
|
||||||
while (rss.next()) {
|
|
||||||
val uuid = rss.getString("uuid")
|
|
||||||
val token = rss.getInt("token")
|
|
||||||
val lastServed = rss.getTimestamp("lastServed")
|
|
||||||
println(s"${uuid}\t\t${token}\t\t${lastServed}\n\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -19,10 +19,14 @@ object AttributesEnum extends Enumeration {
|
||||||
val NAME: Value = Value("name")
|
val NAME: Value = Value("name")
|
||||||
val RANDOM_SEED: Value = Value("randomSeed")
|
val RANDOM_SEED: Value = Value("randomSeed")
|
||||||
val PORT: Value = Value("port")
|
val PORT: Value = Value("port")
|
||||||
|
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 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 {
|
||||||
|
|
@ -37,6 +41,9 @@ object ErrorMessageEnum extends Enumeration {
|
||||||
type ErrorMessage = Value
|
type ErrorMessage = Value
|
||||||
|
|
||||||
val SMW: Value = Value("Oops, something went worng!")
|
val SMW: Value = Value("Oops, something went worng!")
|
||||||
val INVALID_PARAM: Value = Value("Invalid Pramaters")
|
val INVALID_PARAM: Value = Value("Parameters invalid or missing")
|
||||||
val NO_CAPTCHA: Value = Value("No captcha for the provided parameters")
|
val IMG_MISSING: Value = Value("Image missing")
|
||||||
|
val IMG_NOT_FOUND: Value = Value("Image not found")
|
||||||
|
val NO_CAPTCHA: Value = Value("No captcha for the provided parameters. Change config options.")
|
||||||
|
val BAD_METHOD: Value = Value("Bad request method")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
package lc.core
|
||||||
|
|
||||||
|
import lc.captchas.interfaces.{Challenge, ChallengeProvider}
|
||||||
|
import lc.database.Statements
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.sql.{Blob, ResultSet}
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
|
||||||
|
|
||||||
|
def getCaptcha(id: Id): Either[Error, Image] = {
|
||||||
|
val blob = getImage(id.id)
|
||||||
|
blob match {
|
||||||
|
case Some(value) => {
|
||||||
|
if (blob != null) {
|
||||||
|
Right(Image(value.getBytes(1, value.length().toInt)))
|
||||||
|
} else {
|
||||||
|
Left(Error(ErrorMessageEnum.IMG_MISSING.toString))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case None => Left(Error(ErrorMessageEnum.IMG_NOT_FOUND.toString))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getImage(id: String): Option[Blob] = {
|
||||||
|
val imagePstmt = Statements.tlStmts.get.imagePstmt
|
||||||
|
imagePstmt.setString(1, id)
|
||||||
|
val rs: ResultSet = imagePstmt.executeQuery()
|
||||||
|
if (rs.next()) {
|
||||||
|
Some(rs.getBlob("image"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def generateChallenge(param: Parameters): Option[Int] = {
|
||||||
|
try {
|
||||||
|
captchaProviders.getProvider(param).flatMap { provider =>
|
||||||
|
val providerId = provider.getId()
|
||||||
|
val challenge = provider.returnChallenge(param.level, param.size)
|
||||||
|
val blob = new ByteArrayInputStream(challenge.content)
|
||||||
|
val token = insertCaptcha(provider, challenge, providerId, param, blob)
|
||||||
|
// println("Added new challenge: " + token.toString)
|
||||||
|
token.map(_.toInt)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case e: Exception =>
|
||||||
|
e.printStackTrace()
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def insertCaptcha(
|
||||||
|
provider: ChallengeProvider,
|
||||||
|
challenge: Challenge,
|
||||||
|
providerId: String,
|
||||||
|
param: Parameters,
|
||||||
|
blob: ByteArrayInputStream
|
||||||
|
): Option[Int] = {
|
||||||
|
val insertPstmt = Statements.tlStmts.get.insertPstmt
|
||||||
|
insertPstmt.setString(1, provider.getId)
|
||||||
|
insertPstmt.setString(2, challenge.secret)
|
||||||
|
insertPstmt.setString(3, providerId)
|
||||||
|
insertPstmt.setString(4, challenge.contentType)
|
||||||
|
insertPstmt.setString(5, param.level)
|
||||||
|
insertPstmt.setString(6, param.input_type)
|
||||||
|
insertPstmt.setString(7, param.size)
|
||||||
|
insertPstmt.setBlob(8, blob)
|
||||||
|
insertPstmt.executeUpdate()
|
||||||
|
val rs: ResultSet = insertPstmt.getGeneratedKeys()
|
||||||
|
if (rs.next()) {
|
||||||
|
Some(rs.getInt("token"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val allowedInputType = config.allowedInputType
|
||||||
|
val allowedLevels = config.allowedLevels
|
||||||
|
val allowedMedia = config.allowedMedia
|
||||||
|
|
||||||
|
private def validateParam(param: Parameters): Array[String] = {
|
||||||
|
var invalid_params = Array[String]()
|
||||||
|
if (!allowedLevels.contains(param.level)) invalid_params :+= "level"
|
||||||
|
if (!allowedMedia.contains(param.media)) invalid_params :+= "media"
|
||||||
|
if (!allowedInputType.contains(param.input_type)) invalid_params :+= "input_type"
|
||||||
|
|
||||||
|
invalid_params
|
||||||
|
}
|
||||||
|
|
||||||
|
def getChallenge(param: Parameters): Either[Error, Id] = {
|
||||||
|
val validParam = validateParam(param)
|
||||||
|
if (validParam.isEmpty) {
|
||||||
|
val tokenOpt = getToken(param)
|
||||||
|
val token = tokenOpt.orElse(generateChallenge(param))
|
||||||
|
token match {
|
||||||
|
case Some(value) => {
|
||||||
|
val uuid = getUUID(value)
|
||||||
|
updateAttempted(value)
|
||||||
|
Right(Id(uuid))
|
||||||
|
}
|
||||||
|
case None => {
|
||||||
|
Left(Error(ErrorMessageEnum.NO_CAPTCHA.toString))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Left(Error(ErrorMessageEnum.INVALID_PARAM.toString + " => " + validParam.mkString(", ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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] = {
|
||||||
|
val count = getCount(param).getOrElse(0)
|
||||||
|
if (count == 0) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
val tokenPstmt = Statements.tlStmts.get.tokenPstmt
|
||||||
|
tokenPstmt.setString(1, param.level)
|
||||||
|
tokenPstmt.setString(2, param.media)
|
||||||
|
tokenPstmt.setString(3, param.input_type)
|
||||||
|
tokenPstmt.setString(4, param.size)
|
||||||
|
tokenPstmt.setInt(5, count)
|
||||||
|
val rs = tokenPstmt.executeQuery()
|
||||||
|
if (rs.next()) {
|
||||||
|
Some(rs.getInt("token"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def updateAttempted(token: Int): Unit = {
|
||||||
|
val updateAttemptedPstmt = Statements.tlStmts.get.updateAttemptedPstmt
|
||||||
|
updateAttemptedPstmt.setInt(1, token)
|
||||||
|
updateAttemptedPstmt.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getUUID(id: Int): String = {
|
||||||
|
val uuid = UUID.randomUUID().toString
|
||||||
|
val mapPstmt = Statements.tlStmts.get.mapPstmt
|
||||||
|
mapPstmt.setString(1, uuid)
|
||||||
|
mapPstmt.setInt(2, id)
|
||||||
|
mapPstmt.executeUpdate()
|
||||||
|
uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
def checkAnswer(answer: Answer): Either[Error, Success] = {
|
||||||
|
val challenge = getSecret(answer.id)
|
||||||
|
challenge match {
|
||||||
|
case None => Right(Success(ResultEnum.EXPIRED.toString))
|
||||||
|
case Some(value) => {
|
||||||
|
val (provider, secret) = value
|
||||||
|
val check = captchaProviders.getProviderById(provider).checkAnswer(secret, answer.answer)
|
||||||
|
deleteCaptcha(answer.id)
|
||||||
|
val result = if (check) ResultEnum.TRUE.toString else ResultEnum.FALSE.toString
|
||||||
|
Right(Success(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getSecret(id: String): Option[(String, String)] = {
|
||||||
|
val selectPstmt = Statements.tlStmts.get.selectPstmt
|
||||||
|
selectPstmt.setInt(1, config.captchaExpiryTimeLimit)
|
||||||
|
selectPstmt.setString(2, id)
|
||||||
|
val rs: ResultSet = selectPstmt.executeQuery()
|
||||||
|
if (rs.first()) {
|
||||||
|
val secret = rs.getString("secret")
|
||||||
|
val provider = rs.getString("provider")
|
||||||
|
Some(provider, secret)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def deleteCaptcha(id: String): Unit = {
|
||||||
|
val deleteAnswerPstmt = Statements.tlStmts.get.deleteAnswerPstmt
|
||||||
|
deleteAnswerPstmt.setString(1, id)
|
||||||
|
deleteAnswerPstmt.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
def display(): Unit = {
|
||||||
|
val rs: ResultSet = Statements.tlStmts.get.getChallengeTable.executeQuery()
|
||||||
|
println("token\t\tid\t\tsecret\t\tattempted")
|
||||||
|
while (rs.next()) {
|
||||||
|
val token = rs.getInt("token")
|
||||||
|
val id = rs.getString("id")
|
||||||
|
val secret = rs.getString("secret")
|
||||||
|
val attempted = rs.getString("attempted")
|
||||||
|
println(s"${token}\t\t${id}\t\t${secret}\t\t${attempted}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
val rss: ResultSet = Statements.tlStmts.get.getMapIdTable.executeQuery()
|
||||||
|
println("uuid\t\ttoken\t\tlastServed")
|
||||||
|
while (rss.next()) {
|
||||||
|
val uuid = rss.getString("uuid")
|
||||||
|
val token = rss.getInt("token")
|
||||||
|
val lastServed = rss.getTimestamp("lastServed")
|
||||||
|
println(s"${uuid}\t\t${token}\t\t${lastServed}\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,35 +1,29 @@
|
||||||
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
|
||||||
|
import lc.misc.HelperFunctions
|
||||||
|
|
||||||
object CaptchaProviders {
|
class CaptchaProviders(config: Config) {
|
||||||
private val providers = Map(
|
private val providers = Map(
|
||||||
"FilterChallenge" -> new FilterChallenge,
|
"FilterChallenge" -> new FilterChallenge,
|
||||||
//"FontFunCaptcha" -> new FontFunCaptcha,
|
// "FontFunCaptcha" -> new FontFunCaptcha,
|
||||||
"GifCaptcha" -> new GifCaptcha,
|
"PoppingCharactersCaptcha" -> new PoppingCharactersCaptcha,
|
||||||
"ShadowTextCaptcha" -> new ShadowTextCaptcha,
|
"ShadowTextCaptcha" -> new ShadowTextCaptcha,
|
||||||
"RainDropsCaptcha" -> new RainDropsCP
|
"RainDropsCaptcha" -> new RainDropsCP,
|
||||||
//"LabelCaptcha" -> new LabelCaptcha
|
"DebugCaptcha" -> new DebugCaptcha
|
||||||
|
// "LabelCaptcha" -> new LabelCaptcha
|
||||||
)
|
)
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val seed = Config.seed
|
private val captchaConfig = config.captchaConfig
|
||||||
private val random = new scala.util.Random(seed)
|
|
||||||
private val config = Config.captchaConfig
|
|
||||||
|
|
||||||
private def getNextRandomInt(max: Int): Int =
|
|
||||||
random.synchronized {
|
|
||||||
random.nextInt(max)
|
|
||||||
}
|
|
||||||
|
|
||||||
def getProviderById(id: String): ChallengeProvider = {
|
def getProviderById(id: String): ChallengeProvider = {
|
||||||
return providers(id)
|
return providers(id)
|
||||||
|
|
@ -37,10 +31,11 @@ object CaptchaProviders {
|
||||||
|
|
||||||
private def filterProviderByParam(param: Parameters): Iterable[(String, String)] = {
|
private def filterProviderByParam(param: Parameters): Iterable[(String, String)] = {
|
||||||
val configFilter = for {
|
val configFilter = for {
|
||||||
configValue <- config
|
configValue <- captchaConfig
|
||||||
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 {
|
||||||
|
|
@ -54,12 +49,16 @@ object CaptchaProviders {
|
||||||
providerFilter
|
providerFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
def getProvider(param: Parameters): ChallengeProvider = {
|
def getProvider(param: Parameters): Option[ChallengeProvider] = {
|
||||||
val providerConfig = filterProviderByParam(param).toList
|
val providerConfig = filterProviderByParam(param).toList
|
||||||
val randomIndex = getNextRandomInt(providerConfig.length)
|
if (providerConfig.nonEmpty) {
|
||||||
val providerIndex = providerConfig(randomIndex)._1
|
val randomIndex = HelperFunctions.randomNumber(providerConfig.length)
|
||||||
val selectedProvider = providers(providerIndex)
|
val providerIndex = providerConfig(randomIndex)._1
|
||||||
selectedProvider.configure(providerConfig(randomIndex)._2)
|
val selectedProvider = providers(providerIndex)
|
||||||
selectedProvider
|
selectedProvider.configure(providerConfig(randomIndex)._2)
|
||||||
|
Some(selectedProvider)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,17 @@ 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 lc.misc.HelperFunctions
|
||||||
|
|
||||||
object Config {
|
class Config(configFilePath: String) {
|
||||||
|
|
||||||
implicit val formats: DefaultFormats.type = DefaultFormats
|
import Config.formats
|
||||||
|
|
||||||
private val configFilePath = "data/config.json"
|
|
||||||
private val configString =
|
private val configString =
|
||||||
try {
|
try {
|
||||||
val configFile = fromFile(configFilePath)
|
val configFile = fromFile(configFilePath)
|
||||||
|
|
@ -18,86 +22,99 @@ object Config {
|
||||||
configFile.close
|
configFile.close
|
||||||
configFileContent
|
configFileContent
|
||||||
} catch {
|
} catch {
|
||||||
case _: FileNotFoundException =>
|
case _: FileNotFoundException => {
|
||||||
val configFileContent = getDefaultConfig()
|
val configFileContent = getDefaultConfig()
|
||||||
val configFile = new PrintWriter(new File(configFilePath))
|
val file = if (new File(configFilePath).isDirectory) {
|
||||||
|
new File(configFilePath.concat("/config.json"))
|
||||||
|
} else {
|
||||||
|
new File(configFilePath)
|
||||||
|
}
|
||||||
|
val configFile = new PrintWriter(file)
|
||||||
configFile.write(configFileContent)
|
configFile.write(configFileContent)
|
||||||
configFile.close
|
configFile.close
|
||||||
configFileContent
|
configFileContent
|
||||||
|
}
|
||||||
|
case exception: Exception => {
|
||||||
|
println(exception.getStackTrace)
|
||||||
|
throw new Exception(exception.getMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 throttle: Int = (configJson \ AttributesEnum.THROTTLE.toString).extract[Int]
|
val address: String = configFields.address.getOrElse("0.0.0.0")
|
||||||
val seed: Int = (configJson \ AttributesEnum.RANDOM_SEED.toString).extract[Int]
|
val bufferCount: Int = configFields.bufferCountInt.getOrElse(1000)
|
||||||
val captchaExpiryTimeLimit: Int = (configJson \ AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString).extract[Int]
|
val seed: Int = configFields.seedInt.getOrElse(375264328)
|
||||||
val threadDelay: Int = (configJson \ AttributesEnum.THREAD_DELAY.toString).extract[Int]
|
val captchaExpiryTimeLimit: Int = configFields.captchaExpiryTimeLimitInt.getOrElse(5)
|
||||||
|
val threadDelay: Int = configFields.threadDelayInt.getOrElse(2)
|
||||||
|
val playgroundEnabled: Boolean = configFields.playgroundEnabledBool.getOrElse(true)
|
||||||
|
val corsHeader: String = configFields.corsHeader.getOrElse("")
|
||||||
|
val maxAttempts: Int = Math.max(1, (configFields.maxAttemptsRatioFloat.getOrElse(0.01f) * bufferCount).toInt)
|
||||||
|
|
||||||
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] = getAllValues(configJson, ParametersEnum.ALLOWEDLEVELS.toString)
|
val allowedLevels: Set[String] = captchaConfig.flatMap(_.allowedLevels).toSet
|
||||||
val allowedMedia: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDMEDIA.toString)
|
val allowedMedia: Set[String] = captchaConfig.flatMap(_.allowedMedia).toSet
|
||||||
val allowedInputType: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDINPUTTYPE.toString)
|
val allowedInputType: Set[String] = captchaConfig.flatMap(_.allowedInputType).toSet
|
||||||
|
|
||||||
private def getAllValues(config: JValue, param: String): Set[String] = {
|
HelperFunctions.setSeed(seed)
|
||||||
val configValues = (config \\ param)
|
|
||||||
val result = for {
|
|
||||||
JObject(child) <- configValues
|
|
||||||
JField(param) <- child
|
|
||||||
} yield (param)
|
|
||||||
|
|
||||||
var valueSet = Set[String]()
|
|
||||||
for (valueList <- result) {
|
|
||||||
for (value <- valueList._2.children) {
|
|
||||||
valueSet += value.values.toString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
valueSet
|
|
||||||
}
|
|
||||||
|
|
||||||
private def getDefaultConfig(): String = {
|
private def getDefaultConfig(): String = {
|
||||||
val defaultConfigMap =
|
val defaultConfigMap =
|
||||||
(AttributesEnum.RANDOM_SEED.toString -> 20) ~
|
(AttributesEnum.RANDOM_SEED.toString -> new ju.Random().nextInt()) ~
|
||||||
(AttributesEnum.PORT.toString -> 8888) ~
|
(AttributesEnum.PORT.toString -> 8888) ~
|
||||||
(AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString -> 5) ~
|
(AttributesEnum.ADDRESS.toString -> "0.0.0.0") ~
|
||||||
(AttributesEnum.THROTTLE.toString -> 10) ~
|
(AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString -> 5) ~
|
||||||
(AttributesEnum.THREAD_DELAY.toString -> 2) ~
|
(AttributesEnum.BUFFER_COUNT.toString -> 1000) ~
|
||||||
("captchas" -> List(
|
(AttributesEnum.THREAD_DELAY.toString -> 2) ~
|
||||||
(
|
(AttributesEnum.PLAYGROUND_ENABLED.toString -> true) ~
|
||||||
(AttributesEnum.NAME.toString -> "FilterChallenge") ~
|
(AttributesEnum.CORS_HEADER.toString -> "") ~
|
||||||
(ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~
|
(AttributesEnum.MAX_ATTEMPTS_RATIO.toString -> 0.01f) ~
|
||||||
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
|
("captchas" -> List(
|
||||||
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
(
|
||||||
(AttributesEnum.CONFIG.toString -> JObject())
|
(AttributesEnum.NAME.toString -> "FilterChallenge") ~
|
||||||
),
|
(ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~
|
||||||
(
|
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
|
||||||
(AttributesEnum.NAME.toString -> "GifCaptcha") ~
|
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
||||||
(ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~
|
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
|
||||||
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
|
(AttributesEnum.CONFIG.toString -> JObject())
|
||||||
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
),
|
||||||
(AttributesEnum.CONFIG.toString -> JObject())
|
(
|
||||||
),
|
(AttributesEnum.NAME.toString -> "PoppingCharactersCaptcha") ~
|
||||||
(
|
(ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~
|
||||||
(AttributesEnum.NAME.toString -> "ShadowTextCaptcha") ~
|
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
|
||||||
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~
|
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
||||||
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
|
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
|
||||||
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
(AttributesEnum.CONFIG.toString -> JObject())
|
||||||
(AttributesEnum.CONFIG.toString -> JObject())
|
),
|
||||||
),
|
(
|
||||||
(
|
(AttributesEnum.NAME.toString -> "ShadowTextCaptcha") ~
|
||||||
(AttributesEnum.NAME.toString -> "RainDropsCaptcha") ~
|
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~
|
||||||
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~
|
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
|
||||||
(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())
|
||||||
)
|
),
|
||||||
))
|
(
|
||||||
|
(AttributesEnum.NAME.toString -> "RainDropsCaptcha") ~
|
||||||
|
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~
|
||||||
|
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
|
||||||
|
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
|
||||||
|
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
|
||||||
|
(AttributesEnum.CONFIG.toString -> JObject())
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
pretty(render(defaultConfigMap))
|
pretty(render(defaultConfigMap))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object Config {
|
||||||
|
implicit val formats: DefaultFormats.type = DefaultFormats
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,47 @@
|
||||||
package lc.core
|
package lc.core
|
||||||
|
|
||||||
sealed trait ChallengeResult
|
import org.json4s.jackson.Serialization.write
|
||||||
|
import lc.core.Config.formats
|
||||||
|
|
||||||
|
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 ChallengeResult
|
case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } }
|
||||||
case class Error(message: String) extends ChallengeResult
|
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)
|
||||||
case class Result(result: String)
|
case class Success(result: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } }
|
||||||
|
case class Error(message: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } }
|
||||||
case class CaptchaConfig(
|
case class CaptchaConfig(
|
||||||
name: String,
|
name: String,
|
||||||
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/captcha", "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()
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import lc.database.DBConn
|
||||||
import java.sql.Statement
|
import java.sql.Statement
|
||||||
import java.sql.PreparedStatement
|
import java.sql.PreparedStatement
|
||||||
|
|
||||||
class Statements(dbConn: DBConn) {
|
class Statements(dbConn: DBConn, maxAttempts: Int) {
|
||||||
|
|
||||||
private val stmt = dbConn.getStatement()
|
private val stmt = dbConn.getStatement()
|
||||||
|
|
||||||
|
|
@ -17,9 +17,13 @@ class Statements(dbConn: DBConn) {
|
||||||
"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));" +
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS attempted ON challenge(attempted);
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
stmt.execute(
|
stmt.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS mapId" +
|
"CREATE TABLE IF NOT EXISTS mapId" +
|
||||||
|
|
@ -34,8 +38,8 @@ class Statements(dbConn: DBConn) {
|
||||||
|
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -64,20 +68,33 @@ class Statements(dbConn: DBConn) {
|
||||||
val updateAttemptedPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
val updateAttemptedPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
||||||
"UPDATE challenge " +
|
"UPDATE challenge " +
|
||||||
"SET attempted = attempted+1 " +
|
"SET attempted = attempted+1 " +
|
||||||
"WHERE token = (SELECT m.token " +
|
"WHERE token = ?;"
|
||||||
"FROM mapId m, challenge c " +
|
)
|
||||||
"WHERE m.token=c.token AND " +
|
|
||||||
"m.uuid = ?)"
|
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(
|
||||||
"SELECT token " +
|
s"""
|
||||||
"FROM challenge " +
|
SELECT token, attempted
|
||||||
"WHERE attempted < 10 AND " +
|
FROM challenge
|
||||||
"contentLevel = ? AND " +
|
WHERE attempted < $maxAttempts AND
|
||||||
"contentType = ? AND " +
|
contentLevel = ? AND
|
||||||
"contentInput = ? " +
|
contentType = ? AND
|
||||||
"ORDER BY RAND() LIMIT 1"
|
contentInput = ? AND
|
||||||
|
size = ?
|
||||||
|
LIMIT 1
|
||||||
|
OFFSET FLOOR(RAND()*?)
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
val deleteAnswerPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
val deleteAnswerPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
||||||
|
|
@ -85,9 +102,9 @@ class Statements(dbConn: DBConn) {
|
||||||
)
|
)
|
||||||
|
|
||||||
val challengeGCPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
val challengeGCPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
||||||
"DELETE FROM challenge " +
|
s"""DELETE FROM challenge
|
||||||
"WHERE attempted >= 10 AND " +
|
WHERE attempted >= $maxAttempts AND
|
||||||
"token NOT IN (SELECT token FROM mapId)"
|
token NOT IN (SELECT token FROM mapId)"""
|
||||||
)
|
)
|
||||||
|
|
||||||
val mapIdGCPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
val mapIdGCPstmt: PreparedStatement = dbConn.con.prepareStatement(
|
||||||
|
|
@ -106,9 +123,25 @@ class Statements(dbConn: DBConn) {
|
||||||
"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 {
|
||||||
|
/* Note: h2 documentation recommends using a separate DB connection per thread
|
||||||
|
But in practice, as of version 1.4.200, multiple connections occassionally shows error on the console of the form
|
||||||
|
```
|
||||||
|
org.h2.jdbc.JdbcSQLNonTransientException: General error: "java.lang.NullPointerException"; SQL statement:
|
||||||
|
SELECT image FROM challenge c, mapId m WHERE c.token=m.token AND m.uuid = ? [50000-200]
|
||||||
|
```
|
||||||
|
*/
|
||||||
private val dbConn: DBConn = new DBConn()
|
private val dbConn: DBConn = new DBConn()
|
||||||
val tlStmts: ThreadLocal[Statements] = ThreadLocal.withInitial(() => new Statements(dbConn))
|
var maxAttempts: Int = 10
|
||||||
|
val tlStmts: ThreadLocal[Statements] = ThreadLocal.withInitial(() => new Statements(dbConn, maxAttempts))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,108 @@
|
||||||
package lc.server
|
package lc.server
|
||||||
|
|
||||||
import org.json4s.DefaultFormats
|
|
||||||
import org.json4s.jackson.JsonMethods.parse
|
import org.json4s.jackson.JsonMethods.parse
|
||||||
import org.json4s.jackson.Serialization.write
|
import org.json4s.jvalue2extractable
|
||||||
import lc.core.Captcha
|
import lc.core.CaptchaManager
|
||||||
import lc.core.{Parameters, Id, Answer}
|
import lc.core.ErrorMessageEnum
|
||||||
import lc.server.HTTPServer
|
import lc.core.{Answer, ByteConvert, Error, Id, Parameters}
|
||||||
|
import lc.core.Config.formats
|
||||||
|
import org.limium.picoserve
|
||||||
|
import org.limium.picoserve.Server.{ByteResponse, ServerBuilder, StringResponse}
|
||||||
|
import scala.io.Source
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.util
|
||||||
|
import scala.jdk.CollectionConverters._
|
||||||
|
|
||||||
class Server(port: Int, captcha: Captcha) {
|
class Server(
|
||||||
val server = new HTTPServer(port)
|
address: String,
|
||||||
val host: HTTPServer.VirtualHost = server.getVirtualHost(null)
|
port: Int,
|
||||||
|
captchaManager: CaptchaManager,
|
||||||
implicit val formats: DefaultFormats.type = DefaultFormats
|
playgroundEnabled: Boolean,
|
||||||
|
corsHeader: String
|
||||||
host.addContext(
|
) {
|
||||||
"/v1/captcha",
|
var headerMap: util.Map[String, util.List[String]] = _
|
||||||
(req, resp) => {
|
if (corsHeader.nonEmpty) {
|
||||||
val body = req.getJson()
|
headerMap = Map("Access-Control-Allow-Origin" -> List(corsHeader).asJava).asJava
|
||||||
val json = parse(body)
|
}
|
||||||
val param = json.extract[Parameters]
|
val serverBuilder: ServerBuilder = picoserve.Server
|
||||||
val id = captcha.getChallenge(param)
|
.builder()
|
||||||
resp.getHeaders().add("Content-Type", "application/json")
|
.address(new InetSocketAddress(address, port))
|
||||||
resp.send(200, write(id))
|
.backlog(32)
|
||||||
0
|
.POST(
|
||||||
},
|
"/v2/captcha",
|
||||||
"POST"
|
(request) => {
|
||||||
)
|
val json = parse(request.getBodyString())
|
||||||
|
val param = json.extract[Parameters]
|
||||||
host.addContext(
|
val id = captchaManager.getChallenge(param)
|
||||||
"/v1/media",
|
getResponse(id, headerMap)
|
||||||
(req, resp) => {
|
}
|
||||||
val params = req.getParams()
|
)
|
||||||
val id = Id(params.get("id"))
|
.GET(
|
||||||
val image = captcha.getCaptcha(id)
|
"/v2/media",
|
||||||
resp.getHeaders().add("Content-Type", "image/png")
|
(request) => {
|
||||||
resp.send(200, image)
|
val params = request.getQueryParams()
|
||||||
0
|
val result = if (params.containsKey("id")) {
|
||||||
},
|
val paramId = params.get("id").get(0)
|
||||||
"GET"
|
val id = Id(paramId)
|
||||||
)
|
captchaManager.getCaptcha(id)
|
||||||
|
} else {
|
||||||
host.addContext(
|
Left(Error(ErrorMessageEnum.INVALID_PARAM.toString + "=> id"))
|
||||||
"/v1/answer",
|
}
|
||||||
(req, resp) => {
|
getResponse(result, headerMap)
|
||||||
val body = req.getJson()
|
}
|
||||||
val json = parse(body)
|
)
|
||||||
val answer = json.extract[Answer]
|
.POST(
|
||||||
val result = captcha.checkAnswer(answer)
|
"/v2/answer",
|
||||||
resp.getHeaders().add("Content-Type", "application/json")
|
(request) => {
|
||||||
resp.send(200, write(result))
|
val json = parse(request.getBodyString())
|
||||||
0
|
val answer = json.extract[Answer]
|
||||||
},
|
val result = captchaManager.checkAnswer(answer)
|
||||||
"POST"
|
getResponse(result, headerMap)
|
||||||
)
|
}
|
||||||
|
)
|
||||||
def start(): Unit = {
|
if (playgroundEnabled) {
|
||||||
println("Starting server on port:" + port)
|
serverBuilder.GET(
|
||||||
server.start()
|
"/demo/index.html",
|
||||||
|
(_) => {
|
||||||
|
val resStream = getClass().getResourceAsStream("/index.html")
|
||||||
|
val str = Source.fromInputStream(resStream).mkString
|
||||||
|
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()
|
||||||
|
|
||||||
|
private def getResponse(
|
||||||
|
response: Either[Error, ByteConvert],
|
||||||
|
responseHeaders: util.Map[String, util.List[String]]
|
||||||
|
): ByteResponse = {
|
||||||
|
response match {
|
||||||
|
case Right(value) => {
|
||||||
|
new ByteResponse(200, value.toBytes(), responseHeaders)
|
||||||
|
}
|
||||||
|
case Left(value) => {
|
||||||
|
new ByteResponse(500, value.toBytes(), responseHeaders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def start(): Unit = {
|
||||||
|
println("Starting server on " + address + ":" + port)
|
||||||
|
server.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"randomSeed" : 20,
|
||||||
|
"port" : 8888,
|
||||||
|
"address" : "0.0.0.0",
|
||||||
|
"captchaExpiryTimeLimit" : 5,
|
||||||
|
"bufferCount" : 10,
|
||||||
|
"threadDelay" : 2,
|
||||||
|
"playgroundEnabled" : false,
|
||||||
|
"corsHeader" : "*",
|
||||||
|
"maxAttemptsRatio" : 0.01,
|
||||||
|
"captchas" : [ {
|
||||||
|
"name" : "DebugCaptcha",
|
||||||
|
"allowedLevels" : [ "debug" ],
|
||||||
|
"allowedMedia" : [ "image/png" ],
|
||||||
|
"allowedInputType" : [ "text" ],
|
||||||
|
"allowedSizes" : [ "350x100" ],
|
||||||
|
"config" : { }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
from locust import task, between, SequentialTaskSet
|
||||||
|
from locust.contrib.fasthttp import FastHttpUser
|
||||||
|
from locust import events
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
@events.quitting.add_listener
|
||||||
|
def _(environment, **kw):
|
||||||
|
totalStats = environment.stats.total
|
||||||
|
if totalStats.fail_ratio > 0.20:
|
||||||
|
logging.error("Test failed due to failure ratio " + totalStats.fail_ratio + " > 20%")
|
||||||
|
environment.process_exit_code = 1
|
||||||
|
elif totalStats.get_response_time_percentile(0.80) > 800:
|
||||||
|
logging.error("Test failed due to 80th percentile response time > 800 ms")
|
||||||
|
environment.process_exit_code = 1
|
||||||
|
else:
|
||||||
|
environment.process_exit_code = 0
|
||||||
|
|
||||||
|
class QuickStartUser(SequentialTaskSet):
|
||||||
|
wait_time = between(0.1,0.2)
|
||||||
|
|
||||||
|
@task
|
||||||
|
def captcha(self):
|
||||||
|
captcha_params = {"level":"debug","media":"image/png","input_type":"text", "size":"350x100"}
|
||||||
|
|
||||||
|
with self.client.post(path="/v2/captcha", json=captcha_params, name="/captcha", catch_response = True) as resp:
|
||||||
|
if resp.status_code != 200:
|
||||||
|
resp.failure("Status was not 200: " + resp.text)
|
||||||
|
captchaJson = resp.json()
|
||||||
|
uuid = captchaJson.get("id")
|
||||||
|
if not uuid:
|
||||||
|
resp.failure("uuid not returned on /captcha endpoint: " + resp.text)
|
||||||
|
|
||||||
|
with self.client.get(path="/v2/media?id=%s" % uuid, name="/media", stream=True, catch_response = True) as resp:
|
||||||
|
if resp.status_code != 200:
|
||||||
|
resp.failure("Status was not 200: " + resp.text)
|
||||||
|
|
||||||
|
media = resp.content
|
||||||
|
|
||||||
|
ocrAnswer = self.solve(uuid, media)
|
||||||
|
|
||||||
|
answerBody = {"answer": ocrAnswer,"id": uuid}
|
||||||
|
with self.client.post(path='/v2/answer', json=answerBody, name="/answer", catch_response=True) as resp:
|
||||||
|
if resp.status_code != 200:
|
||||||
|
resp.failure("Status was not 200: " + resp.text)
|
||||||
|
else:
|
||||||
|
if resp.json().get("result") != "True":
|
||||||
|
resp.failure("Answer was not accepted: " + ocrAnswer)
|
||||||
|
|
||||||
|
def solve(self, uuid, media):
|
||||||
|
mediaFileName = "tests/test-%s.png" % uuid
|
||||||
|
with open(mediaFileName, "wb") as f:
|
||||||
|
f.write(media)
|
||||||
|
#ocrResult = subprocess.Popen("gocr %s" % mediaFileName, shell=True, stdout=subprocess.PIPE)
|
||||||
|
ocrResult = subprocess.Popen("tesseract %s stdout -l eng" % mediaFileName, shell=True, stdout=subprocess.PIPE)
|
||||||
|
ocrAnswer = ocrResult.stdout.readlines()[0].strip().decode()
|
||||||
|
return ocrAnswer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class User(FastHttpUser):
|
||||||
|
wait_time = between(0.1,0.2)
|
||||||
|
tasks = [QuickStartUser]
|
||||||
|
host = "http://localhost:8888"
|
||||||
|
|
@ -1,16 +1,32 @@
|
||||||
from locust import task, between, SequentialTaskSet
|
from locust import task, between, SequentialTaskSet
|
||||||
from locust.contrib.fasthttp import FastHttpUser
|
from locust.contrib.fasthttp import FastHttpUser
|
||||||
|
from locust import events
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
@events.quitting.add_listener
|
||||||
|
def _(environment, **kw):
|
||||||
|
if environment.stats.total.fail_ratio > 0.02:
|
||||||
|
logging.error("Test failed due to failure ratio > 2%")
|
||||||
|
environment.process_exit_code = 1
|
||||||
|
elif environment.stats.total.avg_response_time > 300:
|
||||||
|
logging.error("Test failed due to average response time ratio > 300 ms")
|
||||||
|
environment.process_exit_code = 1
|
||||||
|
elif environment.stats.total.get_response_time_percentile(0.95) > 800:
|
||||||
|
logging.error("Test failed due to 95th percentile response time > 800 ms")
|
||||||
|
environment.process_exit_code = 1
|
||||||
|
else:
|
||||||
|
environment.process_exit_code = 0
|
||||||
|
|
||||||
class QuickStartUser(SequentialTaskSet):
|
class QuickStartUser(SequentialTaskSet):
|
||||||
wait_time = between(0.1,1)
|
wait_time = between(0.1,0.2)
|
||||||
|
|
||||||
@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)
|
||||||
|
|
@ -20,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)
|
||||||
|
|
@ -36,6 +52,6 @@ class QuickStartUser(SequentialTaskSet):
|
||||||
|
|
||||||
|
|
||||||
class User(FastHttpUser):
|
class User(FastHttpUser):
|
||||||
wait_time = between(0.1,1)
|
wait_time = between(0.1,0.2)
|
||||||
tasks = [QuickStartUser]
|
tasks = [QuickStartUser]
|
||||||
host = "http://localhost:8888"
|
host = "http://localhost:8888"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
python3 -m venv testEnv
|
||||||
|
source ./testEnv/bin/activate
|
||||||
|
pip install locust
|
||||||
|
mkdir -p data/
|
||||||
|
java -jar target/scala-3.6.2/LibreCaptcha.jar &
|
||||||
|
JAVA_PID=$!
|
||||||
|
sleep 4
|
||||||
|
|
||||||
|
locust --only-summary --headless -u 300 -r 100 --run-time 4m --stop-timeout 30 -f tests/locustfile.py
|
||||||
|
status=$?
|
||||||
|
|
||||||
|
if [ $status != 0 ]; then
|
||||||
|
exit $status
|
||||||
|
fi
|
||||||
|
|
||||||
|
kill $JAVA_PID
|
||||||
|
sleep 4
|
||||||
|
|
||||||
|
echo Run functional test
|
||||||
|
cp data/config.json data/config.json.bak
|
||||||
|
cp tests/debug-config.json data/config.json
|
||||||
|
|
||||||
|
java -jar target/scala-3.6.2/LibreCaptcha.jar &
|
||||||
|
JAVA_PID=$!
|
||||||
|
sleep 4
|
||||||
|
|
||||||
|
locust --only-summary --headless -u 1 -r 1 --run-time 1m --stop-timeout 30 -f tests/locustfile-functional.py
|
||||||
|
status=$?
|
||||||
|
mv data/config.json.bak data/config.json
|
||||||
|
|
||||||
|
kill $JAVA_PID
|
||||||
|
exit $status
|
||||||
Loading…
Reference in New Issue