Compare commits

...

486 Commits

Author SHA1 Message Date
hrj a53903a757 update scala version in scripts 2024-12-29 18:59:53 +05:30
hrj 12ddf59bd1
Merge pull request #260 from scala-steward/update/scala3-library-3.6.2
Update scala3-library to 3.6.2
2024-12-18 13:31:22 +05:30
hrj 5a5c9a73d9 setup sbt explicitly 2024-12-17 21:14:34 +05:30
Scala Steward a8f50f6098
Update scala3-library to 3.6.2
Signed-off-by: Scala Steward <scala_steward@virtuslab.com>
2024-12-11 17:38:27 +00:00
hrj ab1387814c
Merge pull request #259 from scala-steward/update/scrimage-core-4.3.0
Update scrimage-core, scrimage-filters to 4.3.0
2024-12-04 08:47:38 +05:30
Scala Steward f0d2ddda44
Update scrimage-core, scrimage-filters to 4.3.0
Signed-off-by: Scala Steward <scala_steward@virtuslab.com>
2024-12-03 20:03:13 +00:00
hrj 2fefc62ba2
Merge pull request #258 from scala-steward/update/sbt-1.10.6
Update sbt, scripted-plugin to 1.10.6
2024-12-02 09:56:05 +05:30
Scala Steward 2ec93c4b95
Update sbt, scripted-plugin to 1.10.6
Signed-off-by: Scala Steward <scala_steward@virtuslab.com>
2024-12-01 20:13:15 +00:00
hrj f60cb8dd05 Merge remote-tracking branch 'origin/master' 2024-11-06 06:54:59 +05:30
hrj 5c73a0fea0 scala steward config to signoff commits 2024-11-06 06:54:17 +05:30
hrj 45299fb8eb
Merge pull request #257 from scala-steward/update/sbt-1.10.5
Update sbt, scripted-plugin to 1.10.5
2024-11-06 06:50:50 +05:30
Scala Steward 3c6bbe8c9f
Update sbt, scripted-plugin to 1.10.5 2024-11-05 19:38:15 +00:00
hrj e122976878
Merge pull request #256 from scala-steward/update/sbt-1.10.4
Update sbt, scripted-plugin to 1.10.4
2024-10-31 11:32:55 +05:30
Scala Steward f702a22dcd
Update sbt, scripted-plugin to 1.10.4 2024-10-28 17:16:37 +00:00
hrj b9bd305599 update scala version in test script 2024-10-25 10:45:44 +05:30
hrj f4737424f7 update scala version in scripts 2024-10-25 10:03:18 +05:30
hrj 355e8c553a
Merge pull request #255 from scala-steward/update/scala3-library-3.5.2
Update scala3-library to 3.5.2
2024-10-25 09:52:14 +05:30
Scala Steward 777be7cf08
Update scala3-library to 3.5.2 2024-10-24 18:24:36 +00:00
hrj c7c89a6396
Merge pull request #252 from scala-steward/update/sbt-scalafix-0.13.0
Update sbt-scalafix to 0.13.0
2024-10-24 16:46:43 +05:30
hrj 471a5828cd
Merge pull request #254 from scala-steward/update/sbt-1.10.3
Update sbt, scripted-plugin to 1.10.3
2024-10-22 09:13:04 +05:30
Scala Steward c7327f07ee
Update sbt, scripted-plugin to 1.10.3 2024-10-20 17:55:01 +00:00
hrj aa31b2d9cf
Merge pull request #253 from scala-steward/update/sbt-assembly-2.3.0
Update sbt-assembly to 2.3.0
2024-10-20 22:36:51 +05:30
Scala Steward 592a85257e
Update sbt-assembly to 2.3.0 2024-10-08 17:02:29 +00:00
Scala Steward 5c3160157a
Update sbt-scalafix to 0.13.0 2024-09-28 19:03:49 +00:00
hrj 24ac9c5ee7
Merge pull request #249 from scala-steward/update/sbt-1.10.2
Update sbt to 1.10.2
2024-09-20 22:55:14 +05:30
Scala Steward 6cf0b322fc
Update sbt to 1.10.2 2024-09-16 16:11:09 +00:00
hrj 967145b73b update scala in scripts 2024-08-24 11:17:08 +05:30
hrj 332aac57f0
Merge pull request #248 from scala-steward/update/scala3-library-3.5.0
Update scala3-library to 3.5.0
2024-08-24 11:12:34 +05:30
Scala Steward f6e0130a53
Update scala3-library to 3.5.0 2024-08-23 15:33:05 +00:00
hrj 20f32bdabb update scripts for scala 3.4.3 2024-08-16 10:55:07 +05:30
hrj 660d18d6f2
Merge pull request #247 from scala-steward/update/scala3-library-3.4.3
Update scala3-library to 3.4.3
2024-08-16 10:53:11 +05:30
Scala Steward b786330bff
Update scala3-library to 3.4.3 2024-08-15 16:10:43 +00:00
hrj dbb675b975
Merge pull request #244 from scala-steward/update/scalafmt-core-3.8.3
Update scalafmt-core to 3.8.3
2024-08-15 15:00:11 +05:30
hrj 220eec02b0
Merge pull request #245 from scala-steward/update/scrimage-core-4.2.0
Update scrimage-core, scrimage-filters to 4.2.0
2024-08-13 11:47:38 +05:30
Scala Steward 3e822ef02a
Update scrimage-core, scrimage-filters to 4.2.0 2024-08-12 23:02:17 +00:00
Scala Steward 1c273c1996
Update scalafmt-core to 3.8.3 2024-07-29 18:24:04 +00:00
hrj 7887622040
Merge pull request #243 from scala-steward/update/sbt-1.10.1
Update sbt to 1.10.1
2024-07-11 08:38:27 +05:30
Scala Steward bf303108aa
Update sbt to 1.10.1 2024-07-10 16:58:41 +00:00
hrj cc7e1232f2
Merge pull request #242 from scala-steward/update/scalafmt-core-3.8.2
Update scalafmt-core to 3.8.2
2024-06-14 23:13:16 +05:30
Scala Steward a9b148c1a9
Update scalafmt-core to 3.8.2 2024-06-14 16:56:30 +00:00
hrj 13459fbf21 update scala version in scripts 2024-06-01 16:07:15 +05:30
hrj fa366be315
Merge pull request #241 from scala-steward/update/scala3-library-3.4.2
Update scala3-library to 3.4.2
2024-06-01 15:47:13 +05:30
Scala Steward 9e39f6d293
Update scala3-library to 3.4.2 2024-05-17 16:20:08 +00:00
hrj f0050494a1
Merge pull request #239 from scala-steward/update/sbt-scalafix-0.12.1
Update sbt-scalafix to 0.12.1
2024-05-08 12:24:38 +05:30
hrj 52156b1677
Merge pull request #240 from scala-steward/update/sbt-1.10.0
Update sbt to 1.10.0
2024-05-08 11:33:41 +05:30
Scala Steward 4749486112
Update sbt to 1.10.0 2024-05-07 21:49:03 +00:00
Scala Steward 7c7d7f6633
Update sbt-scalafix to 0.12.1 2024-05-05 16:32:23 +00:00
hrj 17db5767f4
Merge pull request #238 from scala-steward/update/scrimage-core-4.1.3
Update scrimage-core, scrimage-filters to 4.1.3
2024-04-27 21:57:21 +05:30
Scala Steward 3d7b7e8bd9
Update scrimage-core, scrimage-filters to 4.1.3 2024-04-27 16:16:50 +00:00
hrj 1991375d6b
Merge pull request #236 from scala-steward/update/scalafmt-core-3.8.1
Update scalafmt-core to 3.8.1
2024-04-26 08:45:39 +05:30
hrj b77bd94ae6
Merge pull request #237 from scala-steward/update/scrimage-core-4.1.2
Update scrimage-core, scrimage-filters to 4.1.2
2024-04-26 08:45:17 +05:30
Scala Steward b14415add5
Update scrimage-core, scrimage-filters to 4.1.2 2024-04-25 16:16:42 +00:00
hrj ac44980f50 update scripts to scala 3.4.1 2024-04-17 22:59:58 +05:30
hrj fba5b10e77
Merge pull request #235 from scala-steward/update/scala3-library-3.4.1
Update scala3-library to 3.4.1
2024-04-17 22:57:30 +05:30
Scala Steward c4515d3f77
Update scalafmt-core to 3.8.1 2024-03-30 19:11:05 +00:00
Scala Steward 7c394b4359
Update scala3-library to 3.4.1 2024-03-28 17:44:28 +00:00
hrj 6db9934bec
Merge pull request #234 from scala-steward/update/sbt-assembly-2.2.0
Update sbt-assembly to 2.2.0
2024-03-15 09:03:31 +05:30
Scala Steward 39b776fde8
Update sbt-assembly to 2.2.0 2024-03-14 17:32:47 +00:00
hrj 4c3a7ad0fa update dejava font package name 2024-03-04 09:32:39 +05:30
hrj 7f70d60dbb try 17 jre jammy docker image 2024-03-04 09:27:07 +05:30
hrj 4d79567227 use eclipse-temurin containers from adoptium 2024-03-04 09:12:57 +05:30
hrj f6b7278ed1 update scripts to scala 3.3.3 2024-03-04 08:32:35 +05:30
hrj 984bbacb69
Merge pull request #233 from scala-steward/update/scala3-library-3.3.3
Update scala3-library to 3.3.3
2024-03-04 08:27:44 +05:30
hrj 0dd00f5d3e
Merge pull request #232 from scala-steward/update/sbt-scalafix-0.12.0
Update sbt-scalafix to 0.12.0
2024-03-04 08:27:14 +05:30
Scala Steward d355333bd4
Update scala3-library to 3.3.3 2024-03-02 18:55:20 +00:00
Scala Steward 55064c8532
Update sbt-scalafix to 0.12.0 2024-03-02 18:55:14 +00:00
hrj 1742772de1 update jdk version in dockerfile 2024-02-24 09:13:16 +05:30
hrj 1234bd4acb
Merge pull request #230 from scala-steward/update/scalafmt-core-3.8.0
Update scalafmt-core to 3.8.0
2024-02-24 08:49:23 +05:30
hrj df82593d90
Merge pull request #231 from scala-steward/update/sbt-1.9.9
Update sbt to 1.9.9
2024-02-24 08:49:05 +05:30
Scala Steward b782ded765
Update sbt to 1.9.9 2024-02-23 19:34:34 +00:00
Scala Steward aa6a6a1015
Update scalafmt-core to 3.8.0 2024-02-19 23:16:43 +00:00
hrj 157054d9d8
Merge pull request #229 from scala-steward/update/sbt-1.9.8
Update sbt to 1.9.8
2024-01-14 17:03:50 +05:30
Scala Steward daa8903933
Update sbt to 1.9.8 2024-01-12 23:23:08 +00:00
hrj de3a2bad6f
Merge pull request #228 from scala-steward/update/sbt-1.9.7
Update sbt to 1.9.7
2023-12-04 08:03:30 +05:30
hrj 121b190c32
Merge pull request #227 from scala-steward/update/json4s-jackson-4.0.7
Update json4s-jackson to 4.0.7
2023-12-04 08:02:53 +05:30
Scala Steward b95350b1e2
Update sbt to 1.9.7 2023-12-03 16:35:11 +00:00
Scala Steward 26bc159342
Update json4s-jackson to 4.0.7 2023-12-03 16:35:07 +00:00
hrj 476ef499e3
Merge pull request #226 from scala-steward/update/sbt-scalafmt-2.5.2
Update sbt-scalafmt to 2.5.2
2023-12-02 08:26:37 +05:30
hrj 3b925adfde
Merge branch 'master' into update/sbt-scalafmt-2.5.2 2023-12-02 08:15:55 +05:30
hrj 16a8282206
Merge pull request #225 from scala-steward/update/sbt-1.8.3
Update sbt to 1.8.3
2023-12-02 08:14:13 +05:30
hrj 4cedff874d
Merge pull request #224 from scala-steward/update/sbt-assembly-2.1.5
Update sbt-assembly to 2.1.5
2023-12-02 08:13:38 +05:30
hrj d8360c6192
Merge pull request #223 from scala-steward/update/sbt-scalafix-0.11.1
Update sbt-scalafix to 0.11.1
2023-12-02 08:12:46 +05:30
Scala Steward e81e8a77e8
Update sbt-scalafmt to 2.5.2 2023-12-01 17:39:20 +00:00
Scala Steward c045682708
Update sbt to 1.8.3 2023-12-01 17:39:15 +00:00
Scala Steward 414c07beaf
Update sbt-assembly to 2.1.5 2023-12-01 17:39:02 +00:00
Scala Steward aa6607237a
Update sbt-scalafix to 0.11.1 2023-12-01 17:38:57 +00:00
hrj 931ab0a086
Merge pull request #222 from scala-steward/update/scalafmt-core-3.7.17
Update scalafmt-core to 3.7.17
2023-11-19 08:07:43 +05:30
Scala Steward bc4acb1bae
Update scalafmt-core to 3.7.17 2023-11-17 20:29:03 +00:00
hrj 2dc2066ad8
Merge pull request #220 from scala-steward/update/scalafmt-core-3.7.15
Update scalafmt-core to 3.7.15
2023-10-25 07:50:44 +05:30
Scala Steward bc8c6ad227
Update scalafmt-core to 3.7.15 2023-10-24 21:15:37 +00:00
hrj be0612b94d
Merge pull request #219 from scala-steward/update/scrimage-core-4.1.1
Update scrimage-core, scrimage-filters to 4.1.1
2023-10-17 06:59:21 +05:30
Scala Steward fe2315fc89
Update scrimage-core, scrimage-filters to 4.1.1 2023-10-16 22:35:56 +00:00
hrj 1696050702
Merge pull request #218 from scala-steward/update/scrimage-core-4.1.0
Update scrimage-core, scrimage-filters to 4.1.0
2023-09-21 13:47:45 +05:30
Scala Steward 5ddfab95d1
Update scrimage-core, scrimage-filters to 4.1.0 2023-09-20 16:34:22 +00:00
hrj 15c95747e8
Merge pull request #217 from scala-steward/update/scrimage-core-4.0.42
Update scrimage-core, scrimage-filters to 4.0.42
2023-09-20 08:16:41 +05:30
Scala Steward 12bb1dd1e7
Update scrimage-core, scrimage-filters to 4.0.42 2023-09-18 18:01:05 +00:00
hrj 1de046362d update scala version in scripts 2023-09-09 08:31:18 +05:30
hrj f9a089bed1
Merge pull request #216 from scala-steward/update/scalafmt-core-3.7.14
Update scalafmt-core to 3.7.14
2023-09-09 08:27:40 +05:30
hrj caac0552c3
Merge pull request #215 from scala-steward/update/scala3-library-3.3.1
Update scala3-library to 3.3.1
2023-09-09 08:27:02 +05:30
Scala Steward 1701a497b6
Update scalafmt-core to 3.7.14 2023-09-08 22:21:34 +00:00
Scala Steward ecabb6baae
Update scala3-library to 3.3.1 2023-09-08 22:21:25 +00:00
hrj 065748e339
Merge pull request #213 from scala-steward/update/scalafmt-core-3.7.12
Update scalafmt-core to 3.7.12
2023-08-07 09:19:35 +05:30
Scala Steward 707d02a71f
Update scalafmt-core to 3.7.12 2023-08-05 16:15:44 +00:00
hrj a69bd1afaf
Merge pull request #212 from scala-steward/update/scalafmt-core-3.7.11
Update scalafmt-core to 3.7.11
2023-07-31 23:23:56 +05:30
Scala Steward f0f62eb4fc
Update scalafmt-core to 3.7.11 2023-07-30 16:52:14 +00:00
hrj fbaa0f24a8
Merge pull request #211 from scala-steward/update/scalafmt-core-3.7.10
Update scalafmt-core to 3.7.10
2023-07-17 21:47:17 +05:30
hrj ddd4054db1
Merge pull request #210 from scala-steward/update/scrimage-core-4.0.38
Update scrimage-core, scrimage-filters to 4.0.38
2023-07-17 21:46:57 +05:30
Scala Steward 66c93c5069
Update scalafmt-core to 3.7.10 2023-07-16 16:43:51 +00:00
Scala Steward 75ac7b2f88
Update scrimage-core, scrimage-filters to 4.0.38 2023-07-16 16:43:44 +00:00
hrj 367eef011f
Merge pull request #209 from scala-steward/update/scalafmt-core-3.7.8
Update scalafmt-core to 3.7.8
2023-07-13 20:06:36 +05:30
Scala Steward 8be4ad2be8
Update scalafmt-core to 3.7.8 2023-07-12 18:34:15 +00:00
hrj f7568ffd5f
Merge pull request #208 from scala-steward/update/scalafmt-core-3.7.7
Update scalafmt-core to 3.7.7
2023-07-09 18:00:07 +05:30
Scala Steward 061dc82c68
Update scalafmt-core to 3.7.7 2023-07-08 20:45:06 +00:00
hrj 6ef321c418
Merge pull request #207 from scala-steward/update/scalafmt-core-3.7.6
Update scalafmt-core to 3.7.6
2023-07-06 09:09:47 +05:30
Scala Steward d3048b3b4a
Update scalafmt-core to 3.7.6 2023-07-04 17:50:54 +00:00
hrj 33fbd9a49b
Merge pull request #206 from scala-steward/update/scalafmt-core-3.7.5
Update scalafmt-core to 3.7.5
2023-07-01 10:23:01 +05:30
Scala Steward f9e626415d
Update scalafmt-core to 3.7.5 2023-06-30 19:19:09 +00:00
hrj ddb333a1df
Merge pull request #205 from scala-steward/update/scrimage-core-4.0.37
Update scrimage-core, scrimage-filters to 4.0.37
2023-06-22 11:10:23 +05:30
Scala Steward 62c0c5c363
Update scrimage-core, scrimage-filters to 4.0.37 2023-06-20 16:18:54 +00:00
hrj cff297a48f
Merge pull request #204 from scala-steward/update/scrimage-core-4.0.36
Update scrimage-core, scrimage-filters to 4.0.36
2023-06-14 08:01:52 +05:30
Scala Steward 10ca28ba07
Update scrimage-core, scrimage-filters to 4.0.36 2023-06-12 16:41:50 +00:00
hrj afab1b4803 update scala version in scripts 2023-06-02 17:34:13 +05:30
hrj 205e652e1e
Merge pull request #203 from scala-steward/update/scalafmt-core-3.7.4
Update scalafmt-core to 3.7.4
2023-06-02 17:27:53 +05:30
hrj 03ac5e5e83
Merge pull request #202 from scala-steward/update/scala3-library-3.3.0
Update scala3-library to 3.3.0
2023-06-02 17:26:50 +05:30
Scala Steward ce71407d74
Update scalafmt-core to 3.7.4 2023-05-31 19:36:09 +00:00
Scala Steward 2a33fe1e90
Update scala3-library to 3.3.0 2023-05-31 19:36:00 +00:00
hrj 9399adf2c3
Merge pull request #201 from scala-steward/update/scalafmt-core-3.7.3
Update scalafmt-core to 3.7.3
2023-04-01 07:35:30 +05:30
Scala Steward ef19bbc0c1
Update scalafmt-core to 3.7.3 2023-03-30 18:55:16 +00:00
hrj 1c95665f67
Merge pull request #200 from scala-steward/update/scrimage-core-4.0.34
Update scrimage-core, scrimage-filters to 4.0.34
2023-03-27 22:36:48 +05:30
Scala Steward 6bb9fbd7ea
Update scrimage-core, scrimage-filters to 4.0.34 2023-03-26 15:20:14 +00:00
hrj 6e6daf6c36
Merge pull request #198 from scala-steward/update/scalafmt-core-3.7.2
Update scalafmt-core to 3.7.2
2023-02-27 16:06:13 +05:30
Scala Steward daa6c49f8a
Update scalafmt-core to 3.7.2 2023-02-26 16:53:56 +00:00
hrj cb9b0d361b
Merge pull request #197 from scala-steward/update/sbt-assembly-2.1.1
Update sbt-assembly to 2.1.1
2023-02-15 06:41:07 +05:30
Scala Steward ae9163d106
Update sbt-assembly to 2.1.1 2023-02-14 16:36:59 +00:00
hrj ba21b133fc update scala to 3.2.2 2023-02-01 08:41:21 +05:30
hrj 3b0b464109
Merge pull request #196 from scala-steward/update/scala3-library-3.2.2
Update scala3-library to 3.2.2
2023-02-01 08:39:41 +05:30
Scala Steward 1663e2b306
Update scala3-library to 3.2.2 2023-01-31 18:39:59 +00:00
hrj d77bc658a0
Merge pull request #195 from scala-steward/update/scalafmt-core-3.7.1
Update scalafmt-core to 3.7.1
2023-01-26 08:04:51 +05:30
Scala Steward 33ad07c768
Update scalafmt-core to 3.7.1 2023-01-25 18:06:58 +00:00
hrj beccf4dff6 Merge remote-tracking branch 'origin/master' 2023-01-22 22:52:09 +05:30
hrj e2688f7e51
Merge pull request #194 from scala-steward/update/scalafmt-core-3.7.0
Update scalafmt-core to 3.7.0
2023-01-22 14:25:33 +05:30
Scala Steward ef4d1690e1
Update scalafmt-core to 3.7.0 2023-01-21 16:51:30 +00:00
hrj 43291222a2 README: explain podman usage and publish ports in example 2023-01-18 20:07:01 +05:30
hrj 044d39ad44
Merge pull request #191 from scala-steward/update/scrimage-core-4.0.33
Update scrimage-core, scrimage-filters to 4.0.33
2023-01-18 07:40:53 +05:30
Scala Steward b0f75677c9
Update scrimage-core, scrimage-filters to 4.0.33 2023-01-17 18:16:23 +00:00
hrj cda8bf6aa5 In Runner, use apt instead of apk 2023-01-15 08:21:03 +05:30
hrj 57b43e0f31 In Runner dockerfile use the same docker base as normal Dockerfile 2023-01-15 08:15:04 +05:30
hrj bfa2cfdc88
Merge pull request #190 from gheorghiuradu/feature/multi-platform-docker-support
Add multi platform docker support
2023-01-14 22:44:28 +05:30
gheorghiuradu 5cbb23b714
add arm platforms to build 2023-01-13 15:37:43 +02:00
gheorghiuradu a1739e5288
Update to use multi-platform images 2023-01-13 15:35:27 +02:00
hrj d5f7fdcad1
Merge pull request #189 from scala-steward/update/sbt-1.8.2
Update sbt to 1.8.2
2023-01-08 17:54:16 +05:30
Scala Steward 5950e42973
Update sbt to 1.8.2 2023-01-07 20:14:36 +00:00
hrj 59934539a7
Merge pull request #188 from scala-steward/update/sbt-assembly-2.1.0
Update sbt-assembly to 2.1.0
2022-12-11 07:39:50 +05:30
Scala Steward 399ee06b62
Update sbt-assembly to 2.1.0 2022-12-10 16:41:58 +00:00
hrj 5d7ccbb1c5 update scala version in scripts 2022-11-30 09:51:52 +05:30
hrj 25fb901e9e bump scala to 3.2.1 2022-11-30 09:49:04 +05:30
hrj dd48e9e4e7
Merge pull request #187 from scala-steward/update/sbt-scalafmt-2.5.0
Update sbt-scalafmt to 2.5.0
2022-11-15 06:20:15 +05:30
hrj 43cb991e16
Merge pull request #186 from scala-steward/update/sbt-1.8.0
Update sbt to 1.8.0
2022-11-15 06:19:48 +05:30
Scala Steward 737ebd76ae
Update sbt-scalafmt to 2.5.0 2022-11-14 22:59:30 +00:00
Scala Steward 30c978ea1c
Update sbt to 1.8.0 2022-11-14 22:59:24 +00:00
hrj 7721df414b
Merge pull request #184 from scala-steward/update/scalafmt-core-3.6.1
Update scalafmt-core to 3.6.1
2022-11-03 09:22:49 +05:30
hrj 901759e06e
Merge pull request #183 from scala-steward/update/sbt-1.7.3
Update sbt to 1.7.3
2022-11-03 09:22:06 +05:30
Scala Steward 2dab3e0638
Add 'Reformat with scalafmt 3.6.1' to .git-blame-ignore-revs 2022-11-02 18:33:05 +00:00
Scala Steward f2b19baca8
Reformat with scalafmt 3.6.1
Executed command: scalafmt --non-interactive
2022-11-02 18:33:05 +00:00
Scala Steward 56b42801d9
Update scalafmt-core to 3.6.1 2022-11-02 18:32:55 +00:00
Scala Steward 448bb5fe9f
Update sbt to 1.7.3 2022-11-02 18:32:47 +00:00
hrj fadfffafb0
Merge pull request #182 from scala-steward/update/scalafmt-core-3.6.0
Update scalafmt-core to 3.6.0
2022-10-24 10:45:01 +05:30
hrj f7d38f5f88
Merge pull request #181 from scala-steward/update/sbt-1.7.2
Update sbt to 1.7.2
2022-10-24 10:43:20 +05:30
hrj 85cfdf642c
Merge pull request #180 from scala-steward/update/json4s-jackson-4.0.6
Update json4s-jackson to 4.0.6
2022-10-24 10:42:55 +05:30
hrj 7dea087e2d
Merge pull request #179 from scala-steward/update/sbt-java-formatter-0.8.0
Update sbt-java-formatter to 0.8.0
2022-10-24 10:42:30 +05:30
hrj f7d03ac5cf
Merge branch 'master' into update/sbt-java-formatter-0.8.0 2022-10-24 10:12:59 +05:30
hrj a40b33d492
Merge pull request #178 from scala-steward/update/sbt-assembly-2.0.0
Update sbt-assembly to 2.0.0
2022-10-24 10:11:01 +05:30
Scala Steward 6eb38929d2
Update scalafmt-core to 3.6.0 2022-10-23 18:04:51 +00:00
Scala Steward a88068b865
Update sbt to 1.7.2 2022-10-23 18:04:45 +00:00
Scala Steward 5e9c9e2d5d
Update json4s-jackson to 4.0.6 2022-10-23 18:04:37 +00:00
Scala Steward f9ae1e3970
Add 'Reformat with sbt-java-formatter 0.8.0' to .git-blame-ignore-revs 2022-10-23 18:04:16 +00:00
Scala Steward 57ce691a00
Reformat with sbt-java-formatter 0.8.0
Executed command: sbt javafmtAll
2022-10-23 18:04:16 +00:00
Scala Steward 55e68d23f4
Update sbt-java-formatter to 0.8.0 2022-10-23 18:03:11 +00:00
Scala Steward b27bcc8e52
Update sbt-assembly to 2.0.0 2022-10-23 18:03:00 +00:00
hrj 6ce5bff8c5
Merge pull request #177 from scala-steward/update/sbt-scalafix-0.10.4
Update sbt-scalafix to 0.10.4
2022-10-15 07:14:49 +05:30
Scala Steward 9ef3d162f2
Update sbt-scalafix to 0.10.4 2022-10-15 00:52:24 +00:00
hrj 515782ea8b update docker image version in readme 2022-10-11 10:52:16 +05:30
hrj 70ca5673d1 fix #162: update h2 2022-10-11 10:23:01 +05:30
hrj 2a092b777e fix jar path in Runner.Dockerfile 2022-10-11 07:38:09 +05:30
hrj a2fc659eda fix jar paths in dockerfile and tests 2022-10-11 07:36:35 +05:30
hrj a020426329 update sbt in dockerfile 2022-10-11 07:36:21 +05:30
hrj 907210f229 show a message when playground is enabled 2022-10-11 07:30:58 +05:30
hrj 58c6e96dc9 fix #175: playgroundEnabled config option was not effective 2022-10-11 07:30:46 +05:30
hrj 7beecd7b80
Merge pull request #174 from scala-steward/update/scala3-library-3.2.0
Update scala3-library to 3.2.0
2022-09-08 10:13:57 +05:30
Scala Steward 2f18edef10
Update scala3-library to 3.2.0 2022-09-07 17:37:10 +00:00
hrj 76dcd1b41e
Merge pull request #173 from scala-steward/update/scrimage-core-4.0.32
Update scrimage-core, scrimage-filters to 4.0.32
2022-08-29 09:06:37 +05:30
Scala Steward 32ac8f5baa
Update scrimage-core, scrimage-filters to 4.0.32 2022-08-28 14:32:06 +00:00
hrj 5794693f51
Merge pull request #172 from scala-steward/update/scalafmt-core-3.5.9
Update scalafmt-core to 3.5.9
2022-08-12 22:13:39 +05:30
Scala Steward c02f284cc9
Update scalafmt-core to 3.5.9 2022-08-12 16:09:22 +00:00
hrj 055525bd74
Merge pull request #171 from scala-steward/update/sbt-1.7.1
Update sbt to 1.7.1
2022-07-14 18:44:49 +05:30
Scala Steward dc5d9d86ff
Update sbt to 1.7.1 2022-07-13 22:14:53 +00:00
hrj ffe3142fbf
Merge pull request #168 from scala-steward/update/scala3-library-3.1.3
Update scala3-library to 3.1.3
2022-07-10 12:50:48 +05:30
hrj 5537d907a1
Merge pull request #169 from scala-steward/update/scalafmt-core-3.5.8
Update scalafmt-core to 3.5.8
2022-07-10 12:18:34 +05:30
hrj 15dfc1576e
Merge pull request #167 from scala-steward/update/sbt-scalafix-0.10.1
Update sbt-scalafix to 0.10.1
2022-07-10 09:20:46 +05:30
Scala Steward 603e6eda22
Update scalafmt-core to 3.5.8 2022-07-09 22:40:38 +00:00
Scala Steward 3532f2ec29
Update scala3-library to 3.1.3 2022-07-09 22:40:33 +00:00
Scala Steward 0a4ca07ed4
Update sbt-scalafix to 0.10.1 2022-07-09 22:40:23 +00:00
hrj 95a7ee1710
Merge pull request #166 from scala-steward/update/scalafmt-core-3.5.3
Update scalafmt-core to 3.5.3
2022-05-15 06:53:46 +05:30
Scala Steward aa484e4bc0
Update scalafmt-core to 3.5.3 2022-05-15 00:38:11 +02:00
hrj 0549d9cfcf
Merge pull request #165 from scala-steward/update/scalafmt-core-3.5.2
Update scalafmt-core to 3.5.2
2022-05-01 06:04:55 +05:30
Scala Steward 94dddd99c0
Update scalafmt-core to 3.5.2 2022-04-30 21:44:09 +02:00
hrj 353ecaa1eb
Merge pull request #164 from scala-steward/update/scalafmt-core-3.5.1
Update scalafmt-core to 3.5.1
2022-04-17 00:20:09 +05:30
Scala Steward b402d34be8
Reformat with scalafmt 3.5.1 2022-04-16 20:42:16 +02:00
Scala Steward 37d840b365
Update scalafmt-core to 3.5.1 2022-04-16 20:42:09 +02:00
hrj a9b630724d update to scala 3.1.2 2022-04-13 07:34:11 +05:30
hrj b50df7ec2f
Merge pull request #161 from scala-steward/update/sbt-scalafix-0.10.0
Update sbt-scalafix to 0.10.0
2022-04-08 08:38:55 +05:30
Scala Steward b3c0a83594
Update sbt-scalafix to 0.10.0 2022-04-08 02:47:20 +02:00
hrj 35909261ce
Merge pull request #160 from vinceh121/patch-1
Update curl example for v2 changes
2022-04-07 21:48:01 +05:30
Vincent 073740b98b
Update curl example for v2 changes
Signed-off-by: vinceh121 <contact@vinceh121.me>
2022-04-07 16:08:40 +02:00
hrj c7726edc1c set next release version 2022-04-06 20:29:04 +05:30
hrj e35d569cbb
Merge pull request #155 from scala-steward/update/json4s-jackson-4.0.5
Update json4s-jackson to 4.0.5
2022-04-05 23:17:17 +05:30
hrj e6b09b8d33
Merge pull request #158 from JacobPozaic/master
Updated javascript example usage in README.md for 2.0.0 changes.
2022-04-05 22:48:49 +05:30
JacobPozaic a43b18587e Updated javascript example usage in README.md for 2.0.0 changes.
Signed-off-by: jacobp <jacobpozaic@gmail.com>
2022-04-05 10:30:31 -04:00
Scala Steward fb1bfbd881
Update json4s-jackson to 4.0.5 2022-04-05 14:45:59 +02:00
hrj 41afb10cb5 update version in sbt 2022-04-05 10:41:15 +05:30
hrj e1a88735e3 update test config to match newest config options 2022-04-05 10:15:21 +05:30
hrj 9b56759c80 match size in demo to size in default config 2022-04-05 10:14:28 +05:30
hrj 8df6039a7d define maxAttemptsRatio instead of maxAttempts 2022-04-05 10:13:58 +05:30
hrj a4df7a346d rename throttle to `bufferCount` in captcha fields 2022-04-05 10:13:25 +05:30
hrj 5218653f56 shadow text: center text 2022-04-05 10:12:38 +05:30
hrj 8c38ba2a21 minor cleanup 2022-04-05 09:54:03 +05:30
hrj cd6c8d790a renamed throttle config option to `bufferCount` 2022-04-05 09:53:58 +05:30
hrj 44432cbd5a Update `size` description 2022-04-04 21:47:49 +05:30
hrj c71fdbb8de
Merge pull request #153 from librecaptcha/fix98SizeParam
Respect size param
2022-04-04 21:42:11 +05:30
hrj 7ef308e556 tests: specify size parameter 2022-04-04 20:30:28 +05:30
hrj e26bd32b2f set size in test config 2022-04-04 20:28:13 +05:30
hrj cbb8abd352 functional tests: use new api path 2022-04-04 20:27:35 +05:30
hrj ba796fddf7 locust tests: use new api path 2022-04-04 19:48:28 +05:30
hrj c809872b8d Merge remote-tracking branch 'origin/master' into fix98SizeParam 2022-04-04 19:27:04 +05:30
hrj caf03d7a48 raindrops: vary length based on level, and use safe alphaNumeric characters 2022-04-04 11:13:25 +05:30
hrj 3aeb258851 rain drops: scale text to fit image size 2022-04-04 11:07:50 +05:30
hrj f8626e3670 shadow text: adapt text to size dynamically 2022-04-03 23:22:45 +05:30
hrj 83b0eb069e shadow text: dynamic kernel size 2022-04-03 23:09:45 +05:30
hrj 832053f6e9 minor, simplification 2022-04-03 23:01:40 +05:30
hrj 6480da09ff helper setRenderingHints: set antialiasing on 2022-04-03 23:01:26 +05:30
hrj 23a6a43d2d shadow text: simplify text drawing 2022-04-03 22:58:12 +05:30
hrj 7f77f819dd popping characters: adjust height to size 2022-04-03 22:49:35 +05:30
hrj bfc7174e2a FilterChallenge: use bold font 2022-04-03 16:45:53 +05:30
hrj 0c48f8fbd1 filter challenge: ensure that text fits within image width 2022-04-03 16:45:33 +05:30
hrj 595815921f
Merge pull request #152 from scala-steward/update/scalafmt-core-3.5.0
Update scalafmt-core to 3.5.0
2022-04-03 07:15:01 +05:30
hrj b33dd8adcf filter challenge: adjust filter effect based on difficult level 2022-04-02 23:18:46 +05:30
hrj d797acdcac FilterChallenge: adjust difficult based on parameter 2022-04-02 23:07:38 +05:30
hrj 444982e65f added HelperFunctions.safeAlphaNum 2022-04-02 23:06:57 +05:30
Scala Steward c396fe5c4a
Reformat with scalafmt 3.5.0 2022-04-02 19:26:52 +02:00
Scala Steward 2ee331e6b5
Update scalafmt-core to 3.5.0 2022-04-02 19:26:44 +02:00
hrj cc8addb0c7 filter challenge: scale font height based on image size 2022-04-02 22:51:45 +05:30
hrj d7c1f9a4cc handle exceptions thrown by captcha provider 2022-04-02 22:51:19 +05:30
hrj 07cce460b7
Merge pull request #151 from Korkman/patch-2
Point docker-compose.yml to "latest", add "build"
2022-03-02 17:50:52 +05:30
Korkman 7da5a5b9af
Quotes 2022-03-02 12:57:34 +01:00
Korkman 9f069e1402
Commented 2022-03-02 12:57:12 +01:00
hrj 70f8e42d93
Merge pull request #150 from Korkman/patch-1
Bump version numbers, fixes #149
2022-03-02 17:04:14 +05:30
Korkman c3eb078635
Point docker-compose.yml to "latest"
More sane to point docker-compose.yml to "latest" instead of ageing version numbers. Also added commented-out build option which should be obvious but still helps.
2022-03-02 12:25:24 +01:00
Korkman 8f34d58521
Bump version numbers, fixes #149 2022-03-02 12:19:00 +01:00
hrj 4f3bec0bc6 each challenge provider now respects the size parameter when construction image 2022-03-01 23:03:43 +05:30
hrj edd211fb52 respect size and level parameters
* Add size and level parameters to ChallengeProvider.returnChallenge()
* Add size column to challenge table
* Pass size and level parameters to all relevant calls
* Add size field to demo app
2022-03-01 22:51:38 +05:30
hrj f0659da3eb add a welcome message to root path 2022-03-01 19:11:13 +05:30
hrj c8db914cfd minor simplification 2022-03-01 17:21:30 +05:30
hrj a54947cfc2 shutdown server gracefully 2022-03-01 13:23:29 +05:30
hrj 2d9fd3c68e update h2 db to 2.1.210 2022-03-01 13:21:06 +05:30
hrj 226d3bfa74
Merge pull request #148 from librecaptcha/fix145
Fix for #145: frequent repetition of CAPTCHA challenge
2022-03-01 11:12:55 +05:30
hrj cb0c244779 in background task, divide total required by number of combinations, and create in multiple iterations
Signed-off-by: hrj <harshad.rj@gmail.com>
2022-03-01 09:37:33 +05:30
hrj 8316084ee7 create equal number of captchas for each parameter combination in background task
Signed-off-by: hrj <harshad.rj@gmail.com>
2022-03-01 09:37:33 +05:30
hrj e44a1df22e select random captchas
Signed-off-by: hrj <harshad.rj@gmail.com>
2022-03-01 09:37:33 +05:30
hrj d2a4a92463
Merge pull request #147 from scala-steward/update/scrimage-core-4.0.31
Update scrimage-core, scrimage-filters to 4.0.31
2022-02-27 15:21:03 +05:30
Scala Steward d3056699eb
Update scrimage-core, scrimage-filters to 4.0.31 2022-02-27 00:19:16 +01:00
hrj 3b6a09649d
Merge pull request #144 from librecaptcha/simplification
Simplification and minor refactoring
2022-02-24 20:48:12 +05:30
hrj 52b071d680 renamed Captcha to CaptchaManager
Signed-off-by: hrj <harshad.rj@gmail.com>
2022-02-24 20:12:58 +05:30
hrj 497b88f0d3 simplify logic for background task
Signed-off-by: hrj <harshad.rj@gmail.com>
2022-02-24 20:12:58 +05:30
hrj 6758ec3acf
Merge pull request #143 from scala-steward/update/scrimage-core-4.0.30
Update scrimage-core, scrimage-filters to 4.0.30
2022-02-24 08:41:05 +05:30
Scala Steward 10e0ff08ca
Update scrimage-core, scrimage-filters to 4.0.30 2022-02-24 00:27:06 +01:00
hrj 50b5e00ddf
Merge pull request #142 from scala-steward/update/scrimage-core-4.0.29
Update scrimage-core, scrimage-filters to 4.0.29
2022-02-23 22:12:08 +05:30
Scala Steward 34556a8e93
Update scrimage-core, scrimage-filters to 4.0.29 2022-02-23 16:28:50 +01:00
hrj 836ee0f7f1
Merge pull request #141 from scala-steward/update/scrimage-core-4.0.28
Update scrimage-core, scrimage-filters to 4.0.28
2022-02-23 13:44:52 +05:30
Scala Steward 0f8782b6d3
Update scrimage-core, scrimage-filters to 4.0.28 2022-02-23 08:24:11 +01:00
hrj 44d0464830
Merge pull request #139 from scala-steward/update/scrimage-core-4.0.27
Update scrimage-core, scrimage-filters to 4.0.27
2022-02-22 08:04:39 +05:30
Scala Steward 33a4411689
Update scrimage-core, scrimage-filters to 4.0.27 2022-02-21 20:40:24 +01:00
hrj 86e11a9075
Merge pull request #138 from scala-steward/update/sbt-assembly-1.2.0
Update sbt-assembly to 1.2.0
2022-02-13 21:06:39 +05:30
Scala Steward 25bab9eeeb
Update sbt-assembly to 1.2.0 2022-02-13 10:26:49 +01:00
hrj 7405707250
Merge pull request #137 from scala-steward/update/sbt-assembly-1.1.1
Update sbt-assembly to 1.1.1
2022-02-13 14:34:10 +05:30
Scala Steward 181bcdae0c
Update sbt-assembly to 1.1.1 2022-02-13 08:27:44 +01:00
hrj 02ac9456e1
Merge pull request #136 from scala-steward/update/scalafmt-core-3.4.3
Update scalafmt-core to 3.4.3
2022-02-12 22:55:24 +05:30
Scala Steward a74c7a8ed0
Update scalafmt-core to 3.4.3 2022-02-12 17:26:27 +01:00
hrj 9c83170bf3
Merge pull request #135 from scala-steward/update/scalafmt-core-3.4.2
Update scalafmt-core to 3.4.2
2022-02-06 19:02:54 +05:30
Scala Steward f24ede129c
Update scalafmt-core to 3.4.2 2022-02-06 12:07:47 +01:00
hrj 2f2d54a513
Merge pull request #134 from scala-steward/update/scalafmt-core-3.4.1
Update scalafmt-core to 3.4.1
2022-02-06 09:46:57 +05:30
Scala Steward 756ae8c2a7
Update scalafmt-core to 3.4.1 2022-02-05 17:51:26 +01:00
hrj 3c7c47e0d8 update lib path in docker file 2022-02-02 09:47:03 +05:30
hrj a3fbbf836b update jar file path for tests 2022-02-02 09:46:39 +05:30
hrj 300a04d47f
Merge pull request #133 from scala-steward/update/scala3-library-3.1.1
Update scala3-library to 3.1.1
2022-02-02 09:45:28 +05:30
hrj 3a7cb16286
Merge pull request #132 from scala-steward/update/sbt-1.6.2
Update sbt to 1.6.2
2022-02-02 09:42:26 +05:30
Scala Steward d0145cebe6
Update scala3-library to 3.1.1 2022-02-02 01:36:43 +01:00
Scala Steward ba9e125f9a
Update sbt to 1.6.2 2022-02-01 17:17:50 +01:00
hrj 6f22131162
Merge pull request #131 from scala-steward/update/scalafmt-core-3.4.0
Update scalafmt-core to 3.4.0
2022-01-29 20:54:28 +05:30
Scala Steward ade9a41b14
Update scalafmt-core to 3.4.0 2022-01-29 13:24:49 +01:00
hrj 8c70c53fde
Merge pull request #130 from scala-steward/update/scrimage-core-4.0.26
Update scrimage-core, scrimage-filters to 4.0.26
2022-01-28 08:35:45 +05:30
Scala Steward 250eea2e3f
Update scrimage-core, scrimage-filters to 4.0.26 2022-01-27 23:21:41 +01:00
hrj 1bf19d352e
Merge pull request #129 from scala-steward/update/json4s-jackson-4.0.4
Update json4s-jackson to 4.0.4
2022-01-23 15:58:43 +05:30
Scala Steward 925b45d6f6
Update json4s-jackson to 4.0.4 2022-01-23 08:15:38 +01:00
hrj b8eb7223e2
Merge pull request #128 from scala-steward/update/scalafmt-core-3.3.3
Update scalafmt-core to 3.3.3
2022-01-23 07:45:12 +05:30
Scala Steward 1840ec4f59
Update scalafmt-core to 3.3.3 2022-01-23 02:15:04 +01:00
hrj 7cb2d28968
Merge pull request #127 from scala-steward/update/scalafmt-core-3.3.2
Update scalafmt-core to 3.3.2
2022-01-20 10:27:02 +05:30
Scala Steward dbe0692b8b
Reformat with scalafmt 3.3.2 2022-01-20 05:36:07 +01:00
Scala Steward 46031ce2e4
Update scalafmt-core to 3.3.2 2022-01-20 05:35:57 +01:00
hrj b7ea87a629
Merge pull request #125 from scala-steward/update/sbt-scalafix-0.9.34
Update sbt-scalafix to 0.9.34
2022-01-12 07:52:55 +05:30
hrj c01139d37e
Merge pull request #124 from scala-steward/update/scrimage-core-4.0.25
Update scrimage-core, scrimage-filters to 4.0.25
2022-01-12 07:51:54 +05:30
Scala Steward a2a91d8f45
Update sbt-scalafix to 0.9.34 2022-01-11 23:20:26 +01:00
Scala Steward 85d20d8fff
Update scrimage-core, scrimage-filters to 4.0.25 2022-01-11 19:35:58 +01:00
hrj 715758ad48
Merge pull request #122 from rr83019/opt-config-param
Make config parameters optional
2022-01-09 22:42:51 +05:30
Rahul Rudragoudar 1bd342e825 Merge remote-tracking branch 'origin/opt-config-param' into opt-config-param 2022-01-09 22:34:43 +05:30
Rahul Rudragoudar 5072926bfc Refactor: lambda to fn
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2022-01-09 22:34:33 +05:30
Rahul Rudragoudar f286ef1741 Merge remote-tracking branch 'origin/opt-config-param' into opt-config-param 2022-01-09 21:38:53 +05:30
Rahul Rudragoudar 2a01c176f7 Reconvert playgroundEnabled to bool in debug-config.json
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2022-01-09 21:38:44 +05:30
Rahul Rudragoudar b78331819e Merge remote-tracking branch 'origin/opt-config-param' into opt-config-param 2022-01-09 21:37:22 +05:30
Rahul Rudragoudar d5cf860629 Extract and read from configField case class
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2022-01-09 21:37:13 +05:30
Rahul Rudragoudar 71350f7105 Merge remote-tracking branch 'origin/opt-config-param' into opt-config-param 2022-01-09 01:18:57 +05:30
Rahul Rudragoudar 8e66f3fa8e WIP: Add config case class to avoid type change
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2022-01-09 01:18:33 +05:30
hrj 8ec72ad397
Merge pull request #123 from librecaptcha/h2Update20206
Update h2 lib to 2.0.206
2022-01-08 15:30:44 +05:30
hrj a649aa7c33 Update h2 lib to 2.0.206
Signed-off-by: hrj <harshad.rj@gmail.com>
2022-01-08 10:23:34 +05:30
Rahul Rudragoudar fabd5dd7ec Convert playgroundEnabled: bool to string
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2022-01-08 02:16:03 +05:30
Rahul Rudragoudar e1505a7573 Convert playgroundEnabled: bool to string
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2022-01-08 02:16:03 +05:30
Rahul Rudragoudar 8e39505644 Make config parameters optional with defaults
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2022-01-08 01:59:22 +05:30
hrj 8a87da88c0
Merge pull request #121 from librecaptcha/scala3
update jar path in docker file
2022-01-03 19:43:08 +05:30
hrj 0ed1f980dd update jar path in docker file
Signed-off-by: hrj <harshad.rj@gmail.com>
2022-01-03 19:26:27 +05:30
hrj d706d91ee1
Merge pull request #120 from librecaptcha/scala3
update to scala 3
2022-01-03 19:08:21 +05:30
hrj e78af73951 update jar path in second test
Signed-off-by: hrj <harshad.rj@gmail.com>
2022-01-03 18:56:06 +05:30
hrj 4853e6bccf change jar path in test
Signed-off-by: hrj <harshad.rj@gmail.com>
2022-01-03 18:56:06 +05:30
hrj 9688387b9b remove unsupported rules in scalafix
Signed-off-by: hrj <harshad.rj@gmail.com>
2022-01-03 18:56:06 +05:30
hrj 0b17419ccf disable unsupported option
Signed-off-by: hrj <harshad.rj@gmail.com>
2022-01-03 18:56:06 +05:30
hrj 3e99c0d38b update to scala 3
Signed-off-by: hrj <harshad.rj@gmail.com>
2022-01-03 18:56:06 +05:30
hrj fe0766320b
Merge pull request #119 from scala-steward/update/scalafmt-core-3.3.1
Update scalafmt-core to 3.3.1
2022-01-02 18:17:38 +05:30
Scala Steward ff9462e9bc
Reformat with scalafmt 3.3.1 2022-01-02 05:26:42 +01:00
Scala Steward 3122206ff7
Update scalafmt-core to 3.3.1 2022-01-02 05:26:36 +01:00
hrj 992b07c365
Merge pull request #118 from scala-steward/update/sbt-1.6.1
Update sbt to 1.6.1
2021-12-29 20:12:53 +05:30
Scala Steward 0f191e69a3
Update sbt to 1.6.1 2021-12-29 14:19:03 +01:00
hrj d4e3fb7002
Merge pull request #109 from scala-steward/update/json4s-jackson_2.13-4.0.3
Update json4s-jackson_2.13 to 4.0.3
2021-12-27 20:28:50 +05:30
hrj 4988e2e856
update assembly merge strategy to use latest conventinon
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-12-27 20:19:20 +05:30
hrj 700bd2d910 assembly merge strategy that discards module-info
This is to fix this error:

deduplicate: different file contents found in the following:
[error] [snip] jackson-annotations/2.12.4/jackson-annotations-2.12.4.jar:module-info.class
[error] [snip] jackson-core/2.12.4/jackson-core-2.12.4.jar:module-info.class
2021-12-27 20:05:30 +05:30
Scala Steward f4187720d2 Update json4s-jackson_2.13 to 4.0.3 2021-12-27 20:05:30 +05:30
hrj 4fc2d4ebb0
Merge pull request #116 from rr83019/max-attempts-config
Max attempts config
2021-12-27 19:50:03 +05:30
Rahul Rudragoudar dff305ae14 Add maxAttempts in debug-config.json
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-12-27 10:40:47 +05:30
hrj e17e0cab36
Merge pull request #117 from scala-steward/update/sbt-1.6.0
Update sbt to 1.6.0
2021-12-27 07:53:57 +05:30
Scala Steward 56d04ba722
Update sbt to 1.6.0 2021-12-26 22:26:09 +01:00
Rahul Rudragoudar 5e15ca6c35 Improve imports
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-12-26 22:04:02 +05:30
Rahul Rudragoudar 632eb49fd3 Add config option for maxAttempts field
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-12-26 22:04:02 +05:30
hrj 72cbbcde1f
Merge pull request #115 from scala-steward/update/scalafmt-core-3.3.0
Update scalafmt-core to 3.3.0
2021-12-25 18:38:01 +05:30
hrj e878353b96
Merge pull request #114 from scala-steward/update/sbt-scalafmt-2.4.6
Update sbt-scalafmt to 2.4.6
2021-12-25 18:37:26 +05:30
Scala Steward 9d0ed85f56
Update scalafmt-core to 3.3.0 2021-12-25 11:41:28 +01:00
Scala Steward 018a52a6fd
Update sbt-scalafmt to 2.4.6 2021-12-25 11:41:20 +01:00
hrj b7deb03b02
Merge pull request #113 from scala-steward/update/scalafmt-core-3.2.2
Update scalafmt-core to 3.2.2
2021-12-24 09:58:01 +05:30
Scala Steward d60eee1d13
Update scalafmt-core to 3.2.2 2021-12-24 02:08:09 +01:00
hrj 81ddfbdd4b
Merge pull request #112 from librecaptcha/updateH2-2.0.204
update H2 lib to 2.0.204
2021-12-22 10:00:42 +05:30
hrj 5e7a668016 update H2 lib to 2.0.204
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-12-22 09:26:22 +05:30
hrj f08daf0026
Merge pull request #111 from scala-steward/update/sbt-1.5.8
Update sbt to 1.5.8
2021-12-21 12:20:54 +05:30
Scala Steward c78b1c96bc
Update sbt to 1.5.8 2021-12-21 05:19:25 +01:00
hrj e4e3ed413d
Merge pull request #110 from scala-steward/update/scalafmt-core-3.2.1
Update scalafmt-core to 3.2.1
2021-12-16 15:45:07 +05:30
Scala Steward 6a0ffd4ad6
Update scalafmt-core to 3.2.1 2021-12-16 09:50:30 +01:00
hrj 6cb10f409b
Merge pull request #108 from scala-steward/update/scalafmt-core-3.0.8
Update scalafmt-core to 3.0.8
2021-12-16 07:17:22 +05:30
hrj d8c2f43989
Merge pull request #107 from scala-steward/update/sbt-1.5.7
Update sbt to 1.5.7
2021-12-16 07:14:59 +05:30
hrj e3494de0bd
Merge pull request #106 from scala-steward/update/json4s-jackson_2.13-3.6.12
Update json4s-jackson_2.13 to 3.6.12
2021-12-16 07:04:48 +05:30
Scala Steward f463b487eb
Reformat with scalafmt 3.0.8 2021-12-15 22:54:41 +01:00
Scala Steward cbe26a6097
Update scalafmt-core to 3.0.8 2021-12-15 22:54:32 +01:00
Scala Steward 1c71f2ae68
Update sbt to 1.5.7 2021-12-15 22:54:24 +01:00
Scala Steward 83dcd65da3
Update json4s-jackson_2.13 to 3.6.12 2021-12-15 22:54:00 +01:00
hrj 46e084095c add shadow text sample 2021-12-10 22:12:18 +05:30
hrj 74df5a8b7b update sbt 2021-12-10 22:11:56 +05:30
hrj 0ce5e23e2f update docker tag in README and docker-compose 2021-12-10 09:52:50 +05:30
hrj 6ee4aa596d fix docker action 2021-12-10 09:28:05 +05:30
hrj 3717454f89 add tags to docker images 2021-12-10 09:27:07 +05:30
hrj b8679fb5cc protect docker actions on pull requests 2021-12-10 09:26:50 +05:30
hrj 51a1f0a710 update h2 to 2.0.202
Note that this version is NOT backwards compatible with 1.x version

Hence, we use a new DB path "captcha2". The contents in the old DB can be reused
by following instructions in
https://github.com/h2database/h2database/releases/tag/version-2.0.202
2021-12-10 00:01:35 +05:30
hrj 5f77eee1ac Allow scala 3 syntax 2021-12-09 23:25:03 +05:30
hrj ab1e09142a update scrimage version 2021-12-09 23:24:38 +05:30
hrj 529e482a17 update sbt plugins 2021-12-09 23:14:06 +05:30
hrj abc9f5440f update scala version 2021-12-09 23:13:48 +05:30
hrj 6b2d565601 update sbt version 2021-12-09 23:13:34 +05:30
hrj 928fc786cf
Merge pull request #102 from JacobPozaic/master
CORS configuration and disable playground
2021-12-09 22:00:31 +05:30
jacobp 99ee7c55b5 Added configuration to disable serving the playground webpage and configuration to set Access-Control-Allow-Origin header value on responses.
Signed-off-by: JacobPozaic <jacobpozaic@gmail.com>
2021-12-09 11:02:43 -05:00
hrj 8f606da92f
Merge pull request #96 from rr83019/image-dpi
Set Image DPI for captcha providers
2021-09-02 16:37:00 +05:30
Rahul Rudragoudar a0afba6fa5
Remove print stmts
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-09-02 14:54:20 +05:30
Rahul Rudragoudar b46f6795ce
Set DPI for FilterCaptcha provider
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-09-02 14:53:58 +05:30
Rahul Rudragoudar f83289514b
Rename DPISetter to PngImageWritter
Make fn static

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-09-01 22:57:26 +05:30
Rahul Rudragoudar 26d86bca4c
Set DPI for captcha providers
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-08-31 15:43:58 +05:30
Rahul Rudragoudar d30249a89f
Add DPI setter
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-08-31 15:42:57 +05:30
hrj 1d26b76b01 added example usage for API 2021-08-04 11:45:03 +05:30
hrj 6f6c6eda66 added address to debug config 2021-05-19 20:19:23 +05:30
hrj ab9c0e6247
Merge pull request #94 from vinceh121/master
Listen address in config
2021-05-19 20:16:01 +05:30
vinceh121 bbe7c8b858
Log listen address
Signed-off-by: vinceh121 <contact@vinceh121.me>
2021-05-19 14:32:06 +02:00
vinceh121 b5a33d2e42
Listen address in config
Signed-off-by: vinceh121 <contact@vinceh121.me>
2021-05-19 14:32:02 +02:00
hrj c914484b42
Merge pull request #92 from rr83019/Config-file-path
Specify config file path
2021-05-02 20:13:17 +05:30
Rahul Rudragoudar ecff4087ad
Convert objects to classes
Update references

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-05-02 16:58:14 +05:30
Rahul Rudragoudar 6f38a77c2f
Support to specify config file
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-05-02 16:57:12 +05:30
hrj 1a41724fdc popping characters captcha: don't show prev char for the first char
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-24 22:36:25 +05:30
hrj e3241ff7ac fix bug in randomNumber(min, max)
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-24 22:35:50 +05:30
hrj 6196a34aae
Merge pull request #90 from rr83019/BT-captcha
Background Thread - Random Captcha Generation
2021-04-24 21:52:25 +05:30
Rahul Rudragoudar 1708347504
Remove seed member var
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-24 20:53:28 +05:30
Rahul Rudragoudar 04755c0a07
Add method to set seed
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-24 20:44:20 +05:30
Rahul Rudragoudar 30bb26473e
Update random generator reference
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-24 19:37:44 +05:30
Rahul Rudragoudar 0d33f51f9e
Move random number generator to HelperFunctions
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-24 19:36:48 +05:30
Rahul Rudragoudar d62951fa51
Minor fix
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-21 14:56:58 +05:30
Rahul Rudragoudar aa5e7da0bc
Merge branch 'BT-captcha' of github.com:rr83019/lc-core into BT-captcha 2021-04-21 14:45:42 +05:30
Rahul Rudragoudar e38c3b680a
Reformat:Scalafmt
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-21 14:43:00 +05:30
Rahul Rudragoudar 98c304ccd4
Pick a random captcha provider instead of params
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-21 14:39:20 +05:30
Rahul Rudragoudar d3d5296ccd
Disable JavaFmt on compile
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-21 14:27:19 +05:30
Rahul Rudragoudar 241be1631c
Minor fix
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-21 13:53:35 +05:30
Rahul Rudragoudar 8f0f29c579
Minor fix
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-20 13:53:08 +05:30
Rahul Rudragoudar 8ea0652331
Java formatter
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-20 03:07:35 +05:30
Rahul Rudragoudar 3682b2cb7d
Randomize captcha generation
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-20 03:06:47 +05:30
Rahul Rudragoudar ef31ee8a57
Move random number generator to config
Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-20 03:05:13 +05:30
hrj 9e7efc1cf3 readme: removed reference to unimplemented provider
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 23:35:27 +05:30
hrj f5a262cf50 downgrade docker compose version
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 19:26:05 +05:30
hrj ed3afbc47e ensure data directory is present in the repo
to ease running the app on checkout and in CI
2021-04-19 18:51:20 +05:30
hrj 9b978212dc show errors in demo
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 18:05:59 +05:30
hrj 8595b2cc79 added popping characters sample
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 18:05:43 +05:30
hrj c362ed5cb0 renamed GifCaptcha to PoppingCharactersCaptcha 2021-04-19 17:26:09 +05:30
hrj d81cf17a08 tune GifCaptcha: decrease character spacing, add jitter, slower frame rate 2021-04-19 17:17:24 +05:30
hrj 7ad164e3c2 GifCaptcha: show letters at different offsets
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 16:59:14 +05:30
hrj d32d836475 Gif Captcha: Simplify code
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 15:26:03 +05:30
hrj d2ef8c5259 Use port number from config file (fixes regression)
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 15:13:52 +05:30
hrj 46012b2ce3 default config: randomize seed used by randomizer
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 13:29:41 +05:30
hrj 523805263a Default config: larger throttle setting
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 13:29:15 +05:30
hrj da1b7eee23 don't track data/config.json
Auto-generating it is better as random seed can be made unique upon
first run

Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 13:28:45 +05:30
hrj 6189ffce89 add a simple browser based demo
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 13:14:36 +05:30
hrj f38d6ee191
Merge pull request #88 from UprootStaging/rotateCaptchas
Ensure fresh Captchas are served by sorting on attempted count
2021-04-19 11:49:42 +05:30
hrj 2885decb56 ensure fresh captchas are served by sorting on attempted
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 09:26:49 +05:30
hrj 7684b68efd optimisation: faster update of attempted column
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 09:26:49 +05:30
hrj 4335740bfc remove O (Capital O) from set of safe alphabets
since it is easily confused with 0 (zero)

Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-19 09:26:49 +05:30
hrj a942cd2a93
Merge pull request #87 from UprootStaging/usePicoServer
Use pico server
2021-04-18 20:16:59 +05:30
hrj d02a3504b7 Restore backlog value
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-18 18:54:32 +05:30
hrj 32169dbe80 scala-fix changes
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-18 18:37:37 +05:30
hrj 55288d3346 use picoServe library
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-18 18:37:37 +05:30
Rahul Rudragoudar 43331f8dd7
Improve Error handling/messages (#82)
* Add image error fields

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Update models

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Improve error handling/messages

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Minor reformat

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Improve error handling

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Add base trait

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Decouple data access methods
Improve error handling

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Minor reformat

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Add pattern matching to handle error
Remove try except blocks

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-16 23:22:21 +05:30
hrj 3a1b01688a
Merge pull request #85 from UprootStaging/simplifiedConstructor
Code simplifications and updated test script
2021-04-14 10:47:18 +05:30
hrj 660447798f Debug Captcha: Print mismatches
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-14 10:08:33 +05:30
hrj 8fd294f0cf simpleTest.py : use debug level of difficulty
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-14 10:08:33 +05:30
hrj 352424e8f5 simpleTest: use XDG_RUNTIME_DIR for storing temp files
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-14 10:08:33 +05:30
hrj 433621f046 Update simpleTest to latest API and solve before answering
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-14 10:08:33 +05:30
hrj d3a2c6fa35 Minor: use Map.of()
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-14 10:08:33 +05:30
hrj 321bc67897 update readme with more details and clarifications 2021-04-13 17:58:21 +05:30
hrj ab64bb217c
Merge pull request #83 from UprootStaging/debugCaptcha
Add Debug Captcha and functional tests
2021-04-13 17:44:49 +05:30
hrj caf3669bd9 functional tests: show failed answers
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj 1d746f7655 functional test: reduce failure criteria
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj 96b5808628 correctly parse parameter values from config
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj 1ff4a30da7 use tesseract instead of gocr
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj b765399f68 debug captcha: use safe alphabets only
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj 41bdbc7fbf debug captcha: use a larger font
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj 954399042c debug catpcha: only use alphabets
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj 332bb2113b minor, typos in comment
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj 328f046379 make locust output less verbose
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj 3845645f9a use debug config
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj c0ac570746 simplify locust file
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj 68dcfb1e49 CI: fix permission error
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj a51defd2c7 install gocr during CI
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj 3cfba7a08e run functional test
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj d9fefca841 minor, spacing
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj dd1129b484 added a debug captcha
Signed-off-by: hrj <harshad.rj@gmail.com>
2021-04-13 14:54:16 +05:30
hrj 2d7f229d30 bump to latest sbt version 2021-04-12 19:55:27 +05:30
hrj f8de5a5932 minor: added comment about per thread connections 2021-04-12 17:01:24 +05:30
hrj 79428bc5e7 reduce number of users in locust stress test
by reducing wait time, we can generate same load with fewer users
2021-04-12 16:56:59 +05:30
hrj e8416ff70e internal change: make maxAttempts easier to specify 2021-04-12 16:37:09 +05:30
hrj 8840a13a8f minor: moved blob variable to inner scope 2021-04-12 09:25:33 +05:30
hrj 43e1a379ac use an unbounded thread pool
mimics the performance with JLHttpServer
2021-04-12 07:22:53 +05:30
hrj fb400a6aea remove debug print 2021-04-12 07:10:00 +05:30
hrj 6fd34c16ac optimise string operations 2021-04-12 07:06:44 +05:30
hrj 89eeb76c46 Use a fork join pool executor with parallelism = 4 2021-04-12 07:00:53 +05:30
Rahul Rudragoudar 4612dfa1cd
Migrate to HttpServer (#76)
* Migrate to Fibry

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Improve error handling

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Update models and fields

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Enable fibry server

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Update .gitignore

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Convert captcha class to object

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Rollback error handling

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Update models

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Migrate to sun http server

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Refactor: Linter and formatter

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>

* Remove redundant dependancy

Signed-off-by: Rahul Rudragoudar <rr83019@gmail.com>
2021-04-11 17:57:15 +05:30
hrj ff666396b8 readme: added a note on current status 2021-04-02 18:40:33 +05:30
hrj 12668d3785 readme: add quick start instructions with Java 2021-04-02 18:38:00 +05:30
hrj 02e9115641
Merge pull request #72 from UprootStaging/testing
Run locust tests in CI
2021-04-02 18:37:23 +05:30
hrj 7e87f6721b test script: fix path to activate 2021-04-02 16:51:48 +05:30
hrj c9abe141ec fail early in test script 2021-04-02 16:42:00 +05:30
hrj 30f5705aa5 exit test script with locust's exit code 2021-04-02 16:41:33 +05:30
hrj 5226e0032e check for failure rate in locust test 2021-04-02 16:17:07 +05:30
hrj f02eadb945 specify run time of locust tests 2021-04-02 15:51:55 +05:30
hrj e79da3b881 start locust in headless mode 2021-04-02 15:48:09 +05:30
hrj 6ea5691c5e run locust tests in CI 2021-04-02 15:42:47 +05:30
hrj 72f092e6b5 run sbt assembly in CI 2021-04-02 15:35:52 +05:30
hrj 19fc5f98f9 removed older ci file 2021-04-02 15:35:40 +05:30
hrj e9ff454be5 fix syntax error 2021-04-02 15:34:05 +05:30
hrj 5aad8d2135 run CI for all branches 2021-04-02 15:32:37 +05:30
hrj 87f1bdd72e Update readme with an API test example 2021-04-01 20:39:21 +05:30
hrj f4154de561 Updated readme 2021-04-01 20:10:09 +05:30
51 changed files with 1856 additions and 3732 deletions

5
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,5 @@
# Scala Steward: Reformat with sbt-java-formatter 0.8.0
57ce691a00babb03e0cae03a26fe56d63fc609af
# Scala Steward: Reformat with scalafmt 3.6.1
f2b19baca828a4d88b46bc009aef6d7115e63924

3
.github/scala-steward.conf vendored Normal file
View File

@ -0,0 +1,3 @@
# If true, Scala Steward will sign off all commits (e.g. `git --signoff`).
# Default: false
signoffCommits = true

View File

@ -1,8 +1,7 @@
name: Scala CI
name: Core CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
@ -17,7 +16,10 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 1.11
- uses: sbt/setup-sbt@v1
- name: Run tests
run: sbt test
run: sbt test assembly
- name: Run linter
run: sbt "scalafixAll --check"
- name: Run locust tests
run: sudo apt-get install -y tesseract-ocr && ./tests/run.sh

View File

@ -3,6 +3,8 @@ name: Update docker image
on:
push:
branches: [ master ]
tags:
- 'v*'
jobs:
build:
@ -20,7 +22,20 @@ jobs:
- name: Assemble Jar
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
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@ -36,8 +51,13 @@ jobs:
with:
context: ./
file: ./Runner.Dockerfile
push: true
tags: librecaptcha/lc-core:latest
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: |
linux/amd64
linux/arm64
linux/arm/v7
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

6
.gitignore vendored
View File

@ -1,6 +1,5 @@
/*.log
/*.png
**/*.db
/bin/
/project/**
/target/
@ -8,8 +7,13 @@
.bloop
.metals
.vscode
.bsp
# for python test env
/testEnv/
# for various captcha
/known/
/unknown/
/lib/fonts/

View File

@ -1,8 +1,5 @@
rules=[
ExplicitResultTypes,
RemoveUnused,
DisableSyntax,
LeakingImplicitClassVal,
NoValInForComprehension,
ProcedureSyntax
]

View File

@ -1,2 +1,3 @@
version=2.5.2
version="3.8.3"
maxColumn = 120
runner.dialect = scala3

View File

@ -1,9 +1,10 @@
FROM adoptopenjdk/openjdk16:alpine AS base-builder
ARG SBT_VERSION=1.3.13
RUN apk add --no-cache bash
FROM eclipse-temurin:17-jre-jammy AS base-builder
ARG SBT_VERSION=1.7.1
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
ENV PATH=$PATH:${JAVA_HOME}/bin
RUN \
apt update && \
apt install -y wget && \
wget -O sbt-$SBT_VERSION.tgz https://github.com/sbt/sbt/releases/download/v$SBT_VERSION/sbt-$SBT_VERSION.tgz && \
tar -xzvf sbt-$SBT_VERSION.tgz && \
rm sbt-$SBT_VERSION.tgz
@ -22,15 +23,15 @@ FROM sbt-builder as builder
COPY src/ src/
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/"
RUN apk add --update ttf-dejavu
RUN apt update && apt install -y fonts-dejavu
ENV PATH=$PATH:${JAVA_HOME}/bin
FROM base-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/
EXPOSE 8888

143
README.md
View File

@ -1,16 +1,40 @@
# LibreCaptcha
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
framework itself. Details such as:
* Background workers to render CAPTCHAs and to store them in a database
* Providing an HTTP interface for serving CAPTCHAs
* Managing secrets for the CAPTCHAs (tokens, expected answers, etc)
* Safe re-impressions of CAPTCHA images (by creating unique tokens for every impression)
* Sandboxed plugin architecture (To be done)
The framework defines the API for a CAPTCHA generator and takes care of mundane details
such as:
* An HTTP interface for serving CAPTCHAs
* Background workers to pre-compute CAPTCHAs and to store them in a database
* Managing secrets for the CAPTCHAs (tokens, expected answers, etc)
* Safe re-impressions of CAPTCHA images (by creating unique tokens for every impression)
* Garbage collection of stale CAPTCHAs
* Sandboxed plugin architecture (TBD)
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
Using `docker-compose`:
@ -23,17 +47,35 @@ docker-compose up
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.
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
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.
@ -56,27 +98,40 @@ 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.
## Sample CAPTCHAs
These are included in this server.
### ShadowText
![ShadowText Sample](./samples/shadowText.png)
### FilterCaptcha
![FilterCaptcha Sample](./samples/FilterChallenge.png)
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
![RaindDrops Sample](./samples/RainDropsCaptcha.gif)
### BlurCaptcha
An image of a word is blurred before being shown to the user.
### PoppingCharactersCaptcha
![PoppingCharacters Sample](./samples/popping.gif)
### 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
The service can be accessed using a simple HTTP API.
### - `/v1/captcha`: `POST`
- Parameters:
- Parameters:
- `level`: `String` -
The difficulty level of a captcha
- easy
@ -91,34 +146,68 @@ An image that has a pair of words is created. The answer to one of the words is
- image/png
- image/gif
- (More to come)
- `size`: `Map` -
The dimensions of a captcha (Optional). It needs two more fields nested in this parameter
- `height`: `Int`
- `width`: `Int`
- `size`: String -
The dimensions of a captcha. It needs to be a string in the format `"widthxheight"` in pixels, and will be matched
with the `allowedSizes` config setting. Example: `size: "450x200"` which requests an image of width 450 and height
200 pixels.
- Return type:
- Returns:
- `id`: `String` - The uuid of the captcha generated
### - `/v1/media`: `GET`
- Parameters:
- Parameters:
- `id`: `String` - The uuid of the captcha
- Return type:
- Returns:
- `image`: `Array[Byte]` - The requested media as bytes
### - `/v1/answer`: `POST`
- Parameter:
- Parameter:
- `id`: `String` - The uuid of the captcha that needs to be solved
- `answer`: `String` - The answer to the captcha that needs to be validated
- Return Type:
- Returns:
- `result`: `String` - The result after validation/checking of the answer
- True - If the answer is correct
- False - If the answer is incorrect
- Expired - If the time limit to solve the captcha exceeds
## 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);
```
***
## Roadmap

View File

@ -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/"
RUN apk add --update ttf-dejavu
RUN apt update && apt install -y fonts-dejavu
ENV PATH=$PATH:${JAVA_HOME}/bin
FROM base-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
RUN mkdir data/

View File

@ -2,29 +2,38 @@ lazy val root = (project in file(".")).settings(
inThisBuild(
List(
organization := "com.example",
scalaVersion := "2.13.5",
version := "0.1.0-SNAPSHOT",
scalaVersion := "3.6.2",
version := "0.2.1-snapshot",
semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision,
scalafixScalaBinaryVersion := "2.13"
semanticdbVersion := scalafixSemanticdb.revision
// This is apparently not supported on Scala 3 currently
// scalafixScalaBinaryVersion := "3.1"
)
),
name := "LibreCaptcha",
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.0.12",
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.0.12",
libraryDependencies += "org.json4s" % "json4s-jackson_2.13" % "3.6.11"
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.3.0",
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.3.0",
libraryDependencies += "org.json4s" %% "json4s-jackson" % "4.0.7"
)
unmanagedResourceDirectories in Compile += { baseDirectory.value / "lib" }
Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" }
scalacOptions ++= List(
"-Yrangepos",
"-Ywarn-unused",
"-deprecation"
)
javacOptions += "-g:none"
compileOrder := CompileOrder.JavaThenScala
mainClass in assembly := Some("lc.LCFramework")
mainClass in (Compile, run) := Some("lc.LCFramework")
assemblyJarName in assembly := "LibreCaptcha.jar"
javafmtOnCompile := false
assembly / mainClass := Some("lc.LCFramework")
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

4
data/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@ -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" : { }
} ]
}

View File

@ -1,9 +1,11 @@
version: "3.8"
version: "3.6"
services:
lc-core:
container_name: "libre-captcha"
image: librecaptcha/lc-core:latest
# Comment "image" & uncomment "build" if you intend to build from source
#build: .
volumes:
- "./docker-data:/lc-core/data"
ports:

Binary file not shown.

BIN
lib/h2-2.1.214.jar Normal file

Binary file not shown.

View File

@ -1 +1 @@
sbt.version=1.4.9
sbt.version=1.10.6

View File

@ -1,4 +1,4 @@
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.27")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2")
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.6.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0")

BIN
samples/popping.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
samples/shadowText.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,27 +1,38 @@
import http.client
import json
import subprocess
import os
tempDir = os.getenv('XDG_RUNTIME_DIR', '.')
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 = """{
"level": "medium",
"level": "debug",
"media": "image/png",
"input_type": "text"
}"""
def getCaptcha():
conn.request("POST", "/v1/captcha", body=params, headers={'access-token': user["token"]})
conn.request("POST", "/v1/captcha", body=params)
response = conn.getresponse()
if response:
responseStr = response.read()
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):
reply = {"answer": ans, "id" : captchaId}
conn.request("POST", "/v1/answer", json.dumps(reply))
@ -33,6 +44,6 @@ def postAnswer(captchaId, ans):
for i in range(0, 10000):
captcha = getCaptcha()
#print(captcha)
captchaId = captcha["id"]
print(i, postAnswer(captchaId, "xyz"))
ans = getAndSolve(captchaId)
print(i, postAnswer(captchaId, ans))

View File

@ -1,16 +1,16 @@
package lc.captchas;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lc.captchas.interfaces.Challenge;
import lc.captchas.interfaces.ChallengeProvider;
import lc.misc.HelperFunctions;
import lc.misc.PngImageWriter;
public class FontFunCaptcha implements ChallengeProvider {
@ -18,13 +18,11 @@ public class FontFunCaptcha implements ChallengeProvider {
return "FontFunCaptcha";
}
public HashMap<String, List<String>> supportedParameters() {
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
supportedParams.put("supportedLevels", List.of("medium"));
supportedParams.put("supportedMedia", List.of("image/png"));
supportedParams.put("supportedInputType", List.of("text"));
return supportedParams;
public Map<String, List<String>> supportedParameters() {
return Map.of(
"supportedLevels", List.of("medium"),
"supportedMedia", List.of("image/png"),
"supportedInputType", List.of("text"));
}
public void configure(String config) {
@ -60,9 +58,10 @@ public class FontFunCaptcha implements ChallengeProvider {
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"};
BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB);
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics2D = img.createGraphics();
for (int i = 0; i < captchaText.length(); i++) {
Font font = loadCustomFont(level, path);
@ -76,17 +75,21 @@ public class FontFunCaptcha implements ChallengeProvider {
graphics2D.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ImageIO.write(img, "png", baos);
PngImageWriter.write(baos, img);
} catch (Exception e) {
e.printStackTrace();
}
return baos.toByteArray();
}
public Challenge returnChallenge() {
public Challenge returnChallenge(String level, String size) {
String secret = HelperFunctions.randomString(7);
final int[] size2D = HelperFunctions.parseSize2D(size);
final int width = size2D[0];
final int height = size2D[1];
String path = "./lib/fonts/";
return new Challenge(fontFun(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) {

View File

@ -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";
}
}

View File

@ -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";
}
}

View File

@ -1,21 +1,18 @@
package lc.captchas;
import javax.imageio.ImageIO;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Color;
import java.awt.Font;
import java.awt.font.TextLayout;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.List;
import lc.misc.HelperFunctions;
import java.util.Map;
import lc.captchas.interfaces.Challenge;
import lc.captchas.interfaces.ChallengeProvider;
import lc.misc.HelperFunctions;
import lc.misc.PngImageWriter;
public class ShadowTextCaptcha implements ChallengeProvider {
@ -27,57 +24,69 @@ public class ShadowTextCaptcha implements ChallengeProvider {
// 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("easy"));
supportedParams.put("supportedMedia", List.of("image/png"));
supportedParams.put("supportedInputType", List.of("text"));
return supportedParams;
public Map<String, List<String>> supportedParameters() {
return Map.of(
"supportedLevels", List.of("easy"),
"supportedMedia", List.of("image/png"),
"supportedInputType", List.of("text"));
}
public boolean checkAnswer(String secret, String answer) {
return answer.toLowerCase().equals(secret);
}
private byte[] shadowText(String text) {
BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB);
Font font = new Font("Arial", Font.ROMAN_BASELINE, 48);
Graphics2D graphics2D = img.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
private float[] makeKernel(int size) {
final int N = size * size;
final float weight = 1.0f / (N);
final float[] kernel = new float[N];
java.util.Arrays.fill(kernel, weight);
return kernel;
};
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);
graphics2D.setPaint(Color.WHITE);
graphics2D.fillRect(0, 0, 350, 100);
graphics2D.fillRect(0, 0, width, height);
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();
float[] kernel = {
1f / 9f, 1f / 9f, 1f / 9f,
1f / 9f, 1f / 9f, 1f / 9f,
1f / 9f, 1f / 9f, 1f / 9f
};
ConvolveOp op = new ConvolveOp(new Kernel(3, 3, kernel), ConvolveOp.EDGE_NO_OP, null);
final int kernelSize = (int) Math.ceil((Math.min(width, height) / 50.0));
ConvolveOp op =
new ConvolveOp(
new Kernel(kernelSize, kernelSize, makeKernel(kernelSize)),
ConvolveOp.EDGE_NO_OP,
null);
BufferedImage img2 = op.filter(img, null);
Graphics2D g2d = img2.createGraphics();
HelperFunctions.setRenderingHints(g2d);
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();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ImageIO.write(img2, "png", baos);
PngImageWriter.write(baos, img2);
} catch (Exception e) {
e.printStackTrace();
}
return baos.toByteArray();
}
public Challenge returnChallenge() {
public Challenge returnChallenge(String level, String size) {
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());
}
}

View File

@ -1,12 +1,12 @@
package lc.captchas.interfaces;
import java.util.Map;
import java.util.List;
import java.util.Map;
public interface ChallengeProvider {
public String getId();
public Challenge returnChallenge();
public Challenge returnChallenge(String level, String size);
public boolean checkAnswer(String secret, String answer);

View File

@ -3,12 +3,12 @@
package lc.misc;
import javax.imageio.*;
import javax.imageio.metadata.*;
import javax.imageio.stream.*;
import java.awt.image.*;
import java.io.*;
import java.util.Iterator;
import javax.imageio.*;
import javax.imageio.metadata.*;
import javax.imageio.stream.*;
public class GifSequenceWriter {
protected ImageWriter gifWriter;

View File

@ -1,27 +1,56 @@
package lc.misc;
import java.awt.*;
import java.util.Random;
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) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(
RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
}
public static String randomString(int n) {
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz23456789$#%@&?";
StringBuilder stringBuilder = new StringBuilder();
public static final String safeAlphabets = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
public static final String allAlphabets = safeAlphabets + "ILlO";
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++) {
int index = (int) (characters.length() * Math.random());
int index = randomNumber(characters.length());
stringBuilder.append(characters.charAt(index));
}
return stringBuilder.toString();
}
public static int randomNumber(int min, int max) {
return (int) (Math.random() * ((max - min) + 1)) + min;
public static synchronized int randomNumber(int min, int max) {
return random.nextInt((max - min) + 1) + min;
}
public static synchronized int randomNumber(int bound) {
return random.nextInt(bound);
}
}

View File

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

View File

@ -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);
}
}
}

View File

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

View File

@ -1,29 +1,53 @@
package lc
import lc.core.{Captcha, CaptchaProviders}
import lc.core.{CaptchaProviders, CaptchaManager, Config}
import lc.server.Server
import lc.background.BackgroundTask
import lc.core.Config
import lc.database.Statements
object LCFramework {
def main(args: scala.Array[String]): Unit = {
val captcha = new Captcha()
val server = new Server(port = Config.port, captcha = captcha)
val backgroundTask = new BackgroundTask(
captcha = captcha,
throttle = Config.throttle,
timeLimit = Config.captchaExpiryTimeLimit
val configFilePath = if (args.length > 0) {
args(0)
} else {
"data/config.json"
}
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()
}
}
object MakeSamples {
def main(args: scala.Array[String]): Unit = {
val samples = CaptchaProviders.generateChallengeSamples()
samples.foreach {
case (key, sample) =>
val configFilePath = if (args.length > 0) {
args(0)
} else {
"data/config.json"
}
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)

View File

@ -2,37 +2,89 @@ package lc.background
import lc.database.Statements
import java.util.concurrent.{ScheduledThreadPoolExecutor, TimeUnit}
import lc.core.Captcha
import lc.core.{CaptchaManager, Config}
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 {
def run(): Unit = {
try {
val mapIdGCPstmt = Statements.tlStmts.get.mapIdGCPstmt
mapIdGCPstmt.setInt(1, timeLimit)
mapIdGCPstmt.setInt(1, config.captchaExpiryTimeLimit)
mapIdGCPstmt.executeUpdate()
val challengeGCPstmt = Statements.tlStmts.get.challengeGCPstmt
challengeGCPstmt.executeUpdate()
val imageNum = Statements.tlStmts.get.getCountChallengeTable.executeQuery()
var throttleIn = (throttle * 1.1).toInt
if (imageNum.next())
throttleIn = (throttleIn - imageNum.getInt("total"))
while (0 < throttleIn) {
captcha.generateChallenge(Parameters("medium", "image/png", "text", Option(Size(0, 0))))
throttleIn -= 1
val allCombinations = allParameterCombinations()
val requiredCountPerCombination = Math.max(1, (config.bufferCount * 1.01) / allCombinations.size).toInt
for (param <- allCombinations) {
if (!shutdownInProgress) {
val countExisting = captchaManager.getCount(param).getOrElse(0)
val countRequired = requiredCountPerCombination - countExisting
if (countRequired > 0) {
val countCreate = Math.min(1.0 + requiredCountPerCombination / 10.0, countRequired).toInt
println(s"Creating $countCreate of $countRequired captchas for $param")
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 = {
val ex = new ScheduledThreadPoolExecutor(1)
ex.scheduleWithFixedDelay(task, 1, delay, TimeUnit.SECONDS)
}
@volatile var shutdownInProgress = false
def shutdown(): Unit = {
println(" Shutting down background task...")
shutdownInProgress = true
ex.shutdown()
println(" Finished Shutting background task")
println(" Shutting down DB...")
Statements.tlStmts.get.shutdown.execute()
println(" Finished shutting down db")
}
}

View File

@ -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())
}
}

View File

@ -7,8 +7,10 @@ import java.awt.Font
import java.awt.Color
import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import scala.jdk.CollectionConverters.MapHasAsJava
import java.util.{List => JavaList, Map => JavaMap}
import java.io.ByteArrayOutputStream
import lc.misc.PngImageWriter
import lc.misc.HelperFunctions
class FilterChallenge extends ChallengeProvider {
def getId = "FilterChallenge"
@ -18,33 +20,49 @@ class FilterChallenge extends ChallengeProvider {
}
def supportedParameters(): JavaMap[String, JavaList[String]] = {
val supportedParams = Map(
"supportedLevels" -> JavaList.of("medium", "hard"),
"supportedMedia" -> JavaList.of("image/png"),
"supportedInputType" -> JavaList.of("text")
).asJava
supportedParams
JavaMap.of(
"supportedLevels",
JavaList.of("medium", "hard"),
"supportedMedia",
JavaList.of("image/png"),
"supportedInputType",
JavaList.of("text")
)
}
def returnChallenge(): Challenge = {
val filterTypes = List(new FilterType1, new FilterType2)
private 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 alphabet = "abcdefghijklmnopqrstuvwxyz"
val n = 8
val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString
val canvas = new BufferedImage(225, 50, BufferedImage.TYPE_INT_RGB)
val characters = if (mediumLevel) HelperFunctions.safeAlphaNum else HelperFunctions.safeCharacters
val n = if (mediumLevel) 5 else 7
val secret = LazyList.continually(r.nextInt(characters.size)).map(characters).take(n).mkString
val size2D = HelperFunctions.parseSize2D(size)
val width = size2D(0)
val height = size2D(1)
val canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
val g = canvas.createGraphics()
val fontHeight = (height * 0.6).toInt
g.setColor(Color.WHITE)
g.fillRect(0, 0, canvas.getWidth, canvas.getHeight)
g.setColor(Color.BLACK)
g.setFont(new Font("Serif", Font.PLAIN, 30))
g.drawString(secret, 5, 30)
val font = new Font("Serif", Font.BOLD, fontHeight)
g.setFont(font)
val stringWidth = g.getFontMetrics().stringWidth(secret)
val scaleX = if (stringWidth > width) width / (stringWidth.toDouble) else 1d
val margin = if (stringWidth > width) 0 else (width - stringWidth)
val xOffset = (margin * r.nextDouble).toInt
g.scale(scaleX, 1d)
g.drawString(secret, xOffset, fontHeight)
g.dispose()
var image = ImmutableImage.fromAwt(canvas)
val s = scala.util.Random.nextInt(2)
image = filterTypes(s).applyFilter(image)
new Challenge(image.bytes(new nio.PngWriter()), "image/png", secret)
val s = r.nextInt(2)
image = filterTypes(s).applyFilter(image, !mediumLevel)
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 = {
secret == answer
@ -52,14 +70,15 @@ class FilterChallenge extends ChallengeProvider {
}
trait FilterType {
def applyFilter(image: ImmutableImage): ImmutableImage
def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage
}
class FilterType1 extends FilterType {
override def applyFilter(image: ImmutableImage): ImmutableImage = {
val blur = new GaussianBlurFilter(2)
override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = {
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 diffuse = new DiffuseFilter(2)
val diffuse = new DiffuseFilter(radius.toFloat)
blur.apply(image)
diffuse.apply(image)
smear.apply(image)
@ -68,9 +87,10 @@ class FilterType1 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 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)
diffuse.apply(image)
ripple.apply(image)

View File

@ -9,8 +9,8 @@ import java.awt.image.BufferedImage
import java.awt.Color
import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import scala.jdk.CollectionConverters.MapHasAsJava
import java.util.{List => JavaList, Map => JavaMap}
import lc.misc.PngImageWriter
class LabelCaptcha extends ChallengeProvider {
private var knownFiles = new File("known").list.toList
@ -30,16 +30,17 @@ class LabelCaptcha extends ChallengeProvider {
}
def supportedParameters(): JavaMap[String, JavaList[String]] = {
val supportedParams = Map(
"supportedLevels" -> JavaList.of("hard"),
"supportedMedia" -> JavaList.of("image/png"),
"supportedInputType" -> JavaList.of("text")
).asJava
supportedParams
JavaMap.of(
"supportedLevels",
JavaList.of("hard"),
"supportedMedia",
JavaList.of("image/png"),
"supportedInputType",
JavaList.of("text")
)
}
def returnChallenge(): Challenge =
def returnChallenge(level: String, size: String): Challenge =
synchronized {
val r = scala.util.Random.nextInt(knownFiles.length)
val s = scala.util.Random.nextInt(unknownFiles.length)
@ -52,7 +53,7 @@ class LabelCaptcha extends ChallengeProvider {
val token = encrypt(knownImageFile + "," + unknownImageFile)
val baos = new ByteArrayOutputStream()
ImageIO.write(mergedImage, "png", baos)
PngImageWriter.write(baos, mergedImage);
new Challenge(baos.toByteArray(), "image/png", token)
}

View File

@ -10,8 +10,8 @@ import javax.imageio.stream.MemoryCacheImageOutputStream;
import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import lc.misc.GifSequenceWriter
import scala.jdk.CollectionConverters.MapHasAsJava
import java.util.{List => JavaList, Map => JavaMap}
import lc.misc.HelperFunctions
class Drop {
var x = 0
@ -25,8 +25,6 @@ class Drop {
}
class RainDropsCP extends ChallengeProvider {
private val alphabet = "abcdefghijklmnopqrstuvwxyz"
private val n = 6
private val bgColor = new Color(200, 200, 200)
private val textColor = new Color(208, 208, 218)
private val textHighlightColor = new Color(100, 100, 125)
@ -38,13 +36,14 @@ class RainDropsCP extends ChallengeProvider {
}
def supportedParameters(): JavaMap[String, JavaList[String]] = {
val supportedParams = Map(
"supportedLevels" -> JavaList.of("medium", "easy"),
"supportedMedia" -> JavaList.of("image/gif"),
"supportedInputType" -> JavaList.of("text")
).asJava
supportedParams
JavaMap.of(
"supportedLevels",
JavaList.of("medium", "easy"),
"supportedMedia",
JavaList.of("image/gif"),
"supportedInputType",
JavaList.of("text")
)
}
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 secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString
val width = 450
val height = 100
val n = if (level == "easy") 4 else 6
val secret = HelperFunctions.randomString(n, HelperFunctions.safeAlphaNum)
val size2D = HelperFunctions.parseSize2D(size)
val width = size2D(0)
val height = size2D(1)
val imgType = BufferedImage.TYPE_INT_RGB
val xOffset = 2 + r.nextInt(3)
val xBias = (height / 10) - 2
@ -80,7 +81,8 @@ class RainDropsCP extends ChallengeProvider {
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]()
attributes.put(TextAttribute.TRACKING, Double.box(0.2))
attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_EXTRABOLD)
@ -117,17 +119,22 @@ class RainDropsCP extends ChallengeProvider {
}
}
// center the text
g.setFont(spacedFont)
val textWidth = g.getFontMetrics().charsWidth(secret.toCharArray, 0, secret.toCharArray.length)
val textX = (width - textWidth) / 2
val textWidth = g.getFontMetrics().stringWidth(secret)
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.drawString(secret, textX, 69)
g.drawString(secret, textX, (fontHeight * 1.1).toInt - yOffset)
// paint the text
g.setColor(textColor)
g.drawString(secret, textX, 70)
g.drawString(secret, textX, (fontHeight * 1.1).toInt)
g.dispose()
writer.writeToSequence(canvas)

View File

@ -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")
}
}
}

View File

@ -10,7 +10,7 @@ object ParametersEnum extends Enumeration {
val ALLOWEDLEVELS: Value = Value("allowedLevels")
val ALLOWEDMEDIA: Value = Value("allowedMedia")
val ALLOWEDINPUTTYPE: Value = Value("allowedInputType")
val ALLOWEDSIZES: Value = Value("allowedSizes")
}
object AttributesEnum extends Enumeration {
@ -19,10 +19,14 @@ object AttributesEnum extends Enumeration {
val NAME: Value = Value("name")
val RANDOM_SEED: Value = Value("randomSeed")
val PORT: Value = Value("port")
val ADDRESS: Value = Value("address")
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 PLAYGROUND_ENABLED: Value = Value("playgroundEnabled")
val CORS_HEADER: Value = Value("corsHeader")
val CONFIG: Value = Value("config")
val MAX_ATTEMPTS_RATIO: Value = Value("maxAttemptsRatio")
}
object ResultEnum extends Enumeration {
@ -37,6 +41,9 @@ object ErrorMessageEnum extends Enumeration {
type ErrorMessage = Value
val SMW: Value = Value("Oops, something went worng!")
val INVALID_PARAM: Value = Value("Invalid Pramaters")
val NO_CAPTCHA: Value = Value("No captcha for the provided parameters")
val INVALID_PARAM: Value = Value("Parameters invalid or missing")
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")
}

View File

@ -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")
}
}
}

View File

@ -1,35 +1,29 @@
package lc.core
import lc.captchas._
import lc.captchas.*
import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import scala.collection.mutable.Map
import lc.misc.HelperFunctions
object CaptchaProviders {
class CaptchaProviders(config: Config) {
private val providers = Map(
"FilterChallenge" -> new FilterChallenge,
//"FontFunCaptcha" -> new FontFunCaptcha,
"GifCaptcha" -> new GifCaptcha,
// "FontFunCaptcha" -> new FontFunCaptcha,
"PoppingCharactersCaptcha" -> new PoppingCharactersCaptcha,
"ShadowTextCaptcha" -> new ShadowTextCaptcha,
"RainDropsCaptcha" -> new RainDropsCP
//"LabelCaptcha" -> new LabelCaptcha
"RainDropsCaptcha" -> new RainDropsCP,
"DebugCaptcha" -> new DebugCaptcha
// "LabelCaptcha" -> new LabelCaptcha
)
def generateChallengeSamples(): Map[String, Challenge] = {
providers.map {
case (key, provider) =>
(key, provider.returnChallenge())
providers.map { case (key, provider) =>
(key, provider.returnChallenge("easy", "350x100"))
}
}
private val seed = Config.seed
private val random = new scala.util.Random(seed)
private val config = Config.captchaConfig
private def getNextRandomInt(max: Int): Int =
random.synchronized {
random.nextInt(max)
}
private val captchaConfig = config.captchaConfig
def getProviderById(id: String): ChallengeProvider = {
return providers(id)
@ -37,10 +31,11 @@ object CaptchaProviders {
private def filterProviderByParam(param: Parameters): Iterable[(String, String)] = {
val configFilter = for {
configValue <- config
configValue <- captchaConfig
if configValue.allowedLevels.contains(param.level)
if configValue.allowedMedia.contains(param.media)
if configValue.allowedInputType.contains(param.input_type)
if configValue.allowedSizes.contains(param.size)
} yield (configValue.name, configValue.config)
val providerFilter = for {
@ -54,12 +49,16 @@ object CaptchaProviders {
providerFilter
}
def getProvider(param: Parameters): ChallengeProvider = {
def getProvider(param: Parameters): Option[ChallengeProvider] = {
val providerConfig = filterProviderByParam(param).toList
val randomIndex = getNextRandomInt(providerConfig.length)
if (providerConfig.nonEmpty) {
val randomIndex = HelperFunctions.randomNumber(providerConfig.length)
val providerIndex = providerConfig(randomIndex)._1
val selectedProvider = providers(providerIndex)
selectedProvider.configure(providerConfig(randomIndex)._2)
selectedProvider
Some(selectedProvider)
} else {
None
}
}
}

View File

@ -4,13 +4,17 @@ import scala.io.Source.fromFile
import org.json4s.{DefaultFormats, JValue, JObject, JField, JString}
import org.json4s.jackson.JsonMethods.{parse, render, pretty}
import org.json4s.JsonDSL._
import org.json4s.StringInput
import org.json4s.jvalue2monadic
import org.json4s.jvalue2extractable
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 =
try {
val configFile = fromFile(configFilePath)
@ -18,67 +22,74 @@ object Config {
configFile.close
configFileContent
} catch {
case _: FileNotFoundException =>
case _: FileNotFoundException => {
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.close
configFileContent
}
case exception: Exception => {
println(exception.getStackTrace)
throw new Exception(exception.getMessage)
}
}
private val configJson = parse(configString)
private val configFields: ConfigField = configJson.extract[ConfigField]
val port: Int = (configJson \ AttributesEnum.PORT.toString).extract[Int]
val throttle: Int = (configJson \ AttributesEnum.THROTTLE.toString).extract[Int]
val seed: Int = (configJson \ AttributesEnum.RANDOM_SEED.toString).extract[Int]
val captchaExpiryTimeLimit: Int = (configJson \ AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString).extract[Int]
val threadDelay: Int = (configJson \ AttributesEnum.THREAD_DELAY.toString).extract[Int]
val port: Int = configFields.portInt.getOrElse(8888)
val address: String = configFields.address.getOrElse("0.0.0.0")
val bufferCount: Int = configFields.bufferCountInt.getOrElse(1000)
val seed: Int = configFields.seedInt.getOrElse(375264328)
val captchaExpiryTimeLimit: Int = configFields.captchaExpiryTimeLimitInt.getOrElse(5)
val threadDelay: Int = configFields.threadDelayInt.getOrElse(2)
val playgroundEnabled: Boolean = configFields.playgroundEnabledBool.getOrElse(true)
val corsHeader: String = configFields.corsHeader.getOrElse("")
val maxAttempts: Int = Math.max(1, (configFields.maxAttemptsRatioFloat.getOrElse(0.01f) * bufferCount).toInt)
private val captchaConfigJson = (configJson \ "captchas")
val captchaConfigTransform: JValue = captchaConfigJson transformField {
case JField("config", JObject(config)) => ("config", JString(config.toString))
val captchaConfigTransform: JValue = captchaConfigJson transformField { case JField("config", JObject(config)) =>
("config", JString(config.toString))
}
val captchaConfig: List[CaptchaConfig] = captchaConfigTransform.extract[List[CaptchaConfig]]
val allowedLevels: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDLEVELS.toString)
val allowedMedia: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDMEDIA.toString)
val allowedInputType: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDINPUTTYPE.toString)
val allowedLevels: Set[String] = captchaConfig.flatMap(_.allowedLevels).toSet
val allowedMedia: Set[String] = captchaConfig.flatMap(_.allowedMedia).toSet
val allowedInputType: Set[String] = captchaConfig.flatMap(_.allowedInputType).toSet
private def getAllValues(config: JValue, param: String): Set[String] = {
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
}
HelperFunctions.setSeed(seed)
private def getDefaultConfig(): String = {
val defaultConfigMap =
(AttributesEnum.RANDOM_SEED.toString -> 20) ~
(AttributesEnum.RANDOM_SEED.toString -> new ju.Random().nextInt()) ~
(AttributesEnum.PORT.toString -> 8888) ~
(AttributesEnum.ADDRESS.toString -> "0.0.0.0") ~
(AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString -> 5) ~
(AttributesEnum.THROTTLE.toString -> 10) ~
(AttributesEnum.BUFFER_COUNT.toString -> 1000) ~
(AttributesEnum.THREAD_DELAY.toString -> 2) ~
(AttributesEnum.PLAYGROUND_ENABLED.toString -> true) ~
(AttributesEnum.CORS_HEADER.toString -> "") ~
(AttributesEnum.MAX_ATTEMPTS_RATIO.toString -> 0.01f) ~
("captchas" -> List(
(
(AttributesEnum.NAME.toString -> "FilterChallenge") ~
(ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject())
),
(
(AttributesEnum.NAME.toString -> "GifCaptcha") ~
(AttributesEnum.NAME.toString -> "PoppingCharactersCaptcha") ~
(ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject())
),
(
@ -86,6 +97,7 @@ object Config {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject())
),
(
@ -93,6 +105,7 @@ object Config {
(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())
)
))
@ -101,3 +114,7 @@ object Config {
}
}
object Config {
implicit val formats: DefaultFormats.type = DefaultFormats
}

View File

@ -1,16 +1,47 @@
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 Parameters(level: String, media: String, input_type: String, size: Option[Size])
case class Id(id: String) extends ChallengeResult
case class Error(message: String) extends ChallengeResult
case class Parameters(level: String, media: String, input_type: String, size: String)
case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } }
case class Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[Byte] = { image } }
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(
name: String,
allowedLevels: List[String],
allowedMedia: List[String],
allowedInputType: List[String],
allowedSizes: List[String],
config: String
)
case class ConfigField(
port: Option[Integer],
address: Option[String],
bufferCount: Option[Integer],
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)
}
}

View File

@ -1,9 +1,10 @@
package lc.database
import java.sql._
import java.sql.{Connection, DriverManager, Statement}
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 = {
con.createStatement()

View File

@ -4,7 +4,7 @@ import lc.database.DBConn
import java.sql.Statement
import java.sql.PreparedStatement
class Statements(dbConn: DBConn) {
class Statements(dbConn: DBConn, maxAttempts: Int) {
private val stmt = dbConn.getStatement()
@ -17,9 +17,13 @@ class Statements(dbConn: DBConn) {
"contentType varchar, " +
"contentLevel varchar, " +
"contentInput varchar, " +
"size varchar, " +
"image blob, " +
"attempted int default 0, " +
"PRIMARY KEY(token))"
"PRIMARY KEY(token));" +
"""
CREATE INDEX IF NOT EXISTS attempted ON challenge(attempted);
"""
)
stmt.execute(
"CREATE TABLE IF NOT EXISTS mapId" +
@ -34,8 +38,8 @@ class Statements(dbConn: DBConn) {
val insertPstmt: PreparedStatement = dbConn.con.prepareStatement(
"INSERT INTO " +
"challenge(id, secret, provider, contentType, contentLevel, contentInput, image) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)",
"challenge(id, secret, provider, contentType, contentLevel, contentInput, size, image) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS
)
@ -64,20 +68,33 @@ class Statements(dbConn: DBConn) {
val updateAttemptedPstmt: PreparedStatement = dbConn.con.prepareStatement(
"UPDATE challenge " +
"SET attempted = attempted+1 " +
"WHERE token = (SELECT m.token " +
"FROM mapId m, challenge c " +
"WHERE m.token=c.token AND " +
"m.uuid = ?)"
"WHERE token = ?;"
)
val countForParameterPstmt: PreparedStatement = dbConn.con.prepareStatement(
s"""
SELECT count(*) as count
FROM challenge
WHERE attempted < $maxAttempts AND
contentLevel = ? AND
contentType = ? AND
contentInput = ? AND
size = ?
"""
)
val tokenPstmt: PreparedStatement = dbConn.con.prepareStatement(
"SELECT token " +
"FROM challenge " +
"WHERE attempted < 10 AND " +
"contentLevel = ? AND " +
"contentType = ? AND " +
"contentInput = ? " +
"ORDER BY RAND() LIMIT 1"
s"""
SELECT token, attempted
FROM challenge
WHERE attempted < $maxAttempts AND
contentLevel = ? AND
contentType = ? AND
contentInput = ? AND
size = ?
LIMIT 1
OFFSET FLOOR(RAND()*?)
"""
)
val deleteAnswerPstmt: PreparedStatement = dbConn.con.prepareStatement(
@ -85,9 +102,9 @@ class Statements(dbConn: DBConn) {
)
val challengeGCPstmt: PreparedStatement = dbConn.con.prepareStatement(
"DELETE FROM challenge " +
"WHERE attempted >= 10 AND " +
"token NOT IN (SELECT token FROM mapId)"
s"""DELETE FROM challenge
WHERE attempted >= $maxAttempts AND
token NOT IN (SELECT token FROM mapId)"""
)
val mapIdGCPstmt: PreparedStatement = dbConn.con.prepareStatement(
@ -106,9 +123,25 @@ class Statements(dbConn: DBConn) {
"SELECT * FROM mapId"
)
val shutdown: PreparedStatement = dbConn.con.prepareStatement(
"SHUTDOWN"
)
val shutdownCompact: PreparedStatement = dbConn.con.prepareStatement(
"SHUTDOWN COMPACT"
)
}
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()
val tlStmts: ThreadLocal[Statements] = ThreadLocal.withInitial(() => new Statements(dbConn))
var maxAttempts: Int = 10
val tlStmts: ThreadLocal[Statements] = ThreadLocal.withInitial(() => new Statements(dbConn, maxAttempts))
}

View File

@ -1,62 +1,108 @@
package lc.server
import org.json4s.DefaultFormats
import org.json4s.jackson.JsonMethods.parse
import org.json4s.jackson.Serialization.write
import lc.core.Captcha
import lc.core.{Parameters, Id, Answer}
import lc.server.HTTPServer
import org.json4s.jvalue2extractable
import lc.core.CaptchaManager
import lc.core.ErrorMessageEnum
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) {
val server = new HTTPServer(port)
val host: HTTPServer.VirtualHost = server.getVirtualHost(null)
implicit val formats: DefaultFormats.type = DefaultFormats
host.addContext(
"/v1/captcha",
(req, resp) => {
val body = req.getJson()
val json = parse(body)
class Server(
address: String,
port: Int,
captchaManager: CaptchaManager,
playgroundEnabled: Boolean,
corsHeader: String
) {
var headerMap: util.Map[String, util.List[String]] = _
if (corsHeader.nonEmpty) {
headerMap = Map("Access-Control-Allow-Origin" -> List(corsHeader).asJava).asJava
}
val serverBuilder: ServerBuilder = picoserve.Server
.builder()
.address(new InetSocketAddress(address, port))
.backlog(32)
.POST(
"/v2/captcha",
(request) => {
val json = parse(request.getBodyString())
val param = json.extract[Parameters]
val id = captcha.getChallenge(param)
resp.getHeaders().add("Content-Type", "application/json")
resp.send(200, write(id))
0
},
"POST"
val id = captchaManager.getChallenge(param)
getResponse(id, headerMap)
}
)
host.addContext(
"/v1/media",
(req, resp) => {
val params = req.getParams()
val id = Id(params.get("id"))
val image = captcha.getCaptcha(id)
resp.getHeaders().add("Content-Type", "image/png")
resp.send(200, image)
0
},
"GET"
.GET(
"/v2/media",
(request) => {
val params = request.getQueryParams()
val result = if (params.containsKey("id")) {
val paramId = params.get("id").get(0)
val id = Id(paramId)
captchaManager.getCaptcha(id)
} else {
Left(Error(ErrorMessageEnum.INVALID_PARAM.toString + "=> id"))
}
getResponse(result, headerMap)
}
)
host.addContext(
"/v1/answer",
(req, resp) => {
val body = req.getJson()
val json = parse(body)
.POST(
"/v2/answer",
(request) => {
val json = parse(request.getBodyString())
val answer = json.extract[Answer]
val result = captcha.checkAnswer(answer)
resp.getHeaders().add("Content-Type", "application/json")
resp.send(200, write(result))
0
},
"POST"
val result = captchaManager.checkAnswer(answer)
getResponse(result, headerMap)
}
)
def start(): Unit = {
println("Starting server on port:" + port)
server.start()
if (playgroundEnabled) {
serverBuilder.GET(
"/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()
}
}

19
tests/debug-config.json Normal file
View File

@ -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" : { }
}]
}

View File

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

View File

@ -1,16 +1,32 @@
from locust import task, between, SequentialTaskSet
from locust.contrib.fasthttp import FastHttpUser
from locust import events
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):
wait_time = between(0.1,1)
wait_time = between(0.1,0.2)
@task
def captcha(self):
# 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:
print("\nError on /captcha endpoint: ")
print(resp)
@ -20,14 +36,14 @@ class QuickStartUser(SequentialTaskSet):
uuid = json.loads(resp.text).get("id")
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:
print("\nError on /media endpoint: ")
print(resp)
print(resp.text)
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:
print("\nError on /answer endpoint: ")
print(resp)
@ -36,6 +52,6 @@ class QuickStartUser(SequentialTaskSet):
class User(FastHttpUser):
wait_time = between(0.1,1)
wait_time = between(0.1,0.2)
tasks = [QuickStartUser]
host = "http://localhost:8888"

34
tests/run.sh Executable file
View File

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