Compare commits

..

271 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
40 changed files with 477 additions and 261 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

@ -16,6 +16,7 @@ jobs:
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 1.11 java-version: 1.11
- uses: sbt/setup-sbt@v1
- name: Run tests - name: Run tests
run: sbt test assembly run: sbt test assembly
- name: Run linter - name: Run linter

View File

@ -54,6 +54,10 @@ jobs:
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: |
linux/amd64
linux/arm64
linux/arm/v7
- name: Image digest - name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }} run: echo ${{ steps.docker_build.outputs.digest }}

View File

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

View File

@ -1,12 +1,13 @@
FROM adoptopenjdk/openjdk16:alpine AS base-builder FROM eclipse-temurin:17-jre-jammy AS base-builder
ARG SBT_VERSION=1.3.13 ARG SBT_VERSION=1.7.1
RUN apk add --no-cache bash
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/" ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
ENV PATH=$PATH:${JAVA_HOME}/bin ENV PATH=$PATH:${JAVA_HOME}/bin
RUN \ RUN \
apt update && \
apt install -y wget && \
wget -O sbt-$SBT_VERSION.tgz https://github.com/sbt/sbt/releases/download/v$SBT_VERSION/sbt-$SBT_VERSION.tgz && \ wget -O sbt-$SBT_VERSION.tgz https://github.com/sbt/sbt/releases/download/v$SBT_VERSION/sbt-$SBT_VERSION.tgz && \
tar -xzvf sbt-$SBT_VERSION.tgz && \ tar -xzvf sbt-$SBT_VERSION.tgz && \
rm sbt-$SBT_VERSION.tgz rm sbt-$SBT_VERSION.tgz
ENV PATH=$PATH:/sbt/bin/ ENV PATH=$PATH:/sbt/bin/
@ -22,15 +23,15 @@ FROM sbt-builder as builder
COPY src/ src/ COPY src/ src/
RUN sbt assembly RUN sbt assembly
FROM adoptopenjdk/openjdk16:alpine-jre AS base-core FROM eclipse-temurin:17-jre-jammy AS base-core
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/" ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
RUN apk add --update ttf-dejavu RUN apt update && apt install -y fonts-dejavu
ENV PATH=$PATH:${JAVA_HOME}/bin ENV PATH=$PATH:${JAVA_HOME}/bin
FROM base-core FROM base-core
WORKDIR /lc-core WORKDIR /lc-core
COPY --from=builder /build/target/scala-2.13/LibreCaptcha.jar . COPY --from=builder /build/target/scala-3.6.2/LibreCaptcha.jar .
RUN mkdir data/ RUN mkdir data/
EXPOSE 8888 EXPOSE 8888

View File

@ -2,12 +2,12 @@
LibreCaptcha is a framework that allows developers to create their own [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA)s. LibreCaptcha is a framework that allows developers to create their own [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA)s.
The framework defines the API for a CAPTCHA generator and takes care of mundane details The framework defines the API for a CAPTCHA generator and takes care of mundane details
such as: such as:
* An HTTP interface for serving CAPTCHAs * An HTTP interface for serving CAPTCHAs
* Background workers to pre-compute CAPTCHAs and to store them in a database * Background workers to pre-compute CAPTCHAs and to store them in a database
* Managing secrets for the CAPTCHAs (tokens, expected answers, etc) * Managing secrets for the CAPTCHAs (tokens, expected answers, etc)
* Safe re-impressions of CAPTCHA images (by creating unique tokens for every impression) * Safe re-impressions of CAPTCHA images (by creating unique tokens for every impression)
* Garbage collection of stale CAPTCHAs * Garbage collection of stale CAPTCHAs
* Sandboxed plugin architecture (TBD) * Sandboxed plugin architecture (TBD)
Some sample CAPTCHA generators are included in the distribution (see below). We will continue adding more samples to the list. For quick Some sample CAPTCHA generators are included in the distribution (see below). We will continue adding more samples to the list. For quick
deployments the samples themselves might be sufficient. Projects with more resources might want create their own CAPTCHAs deployments the samples themselves might be sufficient. Projects with more resources might want create their own CAPTCHAs
@ -47,18 +47,25 @@ docker-compose up
Using `docker`: Using `docker`:
``` ```
docker run -v lcdata:/lc-core/data librecaptcha/lc-core:1.1.0-stable docker run -p=8888:8888 -v ./lcdata:/lc-core/data librecaptcha/lc-core:2.0
``` ```
A default `config.json` is automatically created in the mounted volume. A default `config.json` is automatically created in the mounted volume.
The above commands should work with `podman` as well, if docker.io registry is pre-configured. Otherwise,
you can manually specify the repository like so:
```
podman run -p=8888:8888 -v ./lcdata:/lc-core/data docker.io/librecaptcha/lc-core:2.0
```
## Quick test ## Quick test
Open [localhost:8888/demo/index.html](http://localhost:8888/demo/index.html) in browser. Open [localhost:8888/demo/index.html](http://localhost:8888/demo/index.html) in browser.
Alternatively, on the command line, try: Alternatively, on the command line, try:
``` ```
> $ curl -d '{"media":"image/png","level":"easy","input_type":"text"}' localhost:8888/v1/captcha > $ curl -d '{"media":"image/png","level":"easy","input_type":"text","size":"350x100"}' localhost:8888/v2/captcha
{"id":"3bf928ce-a1e7-4616-b34f-8252d777855d"} {"id":"3bf928ce-a1e7-4616-b34f-8252d777855d"}
> $ curl "localhost:8888/v1/media?id=3bf928ce-a1e7-4616-b34f-8252d777855d" -o sample.png > $ curl "localhost:8888/v1/media?id=3bf928ce-a1e7-4616-b34f-8252d777855d" -o sample.png
@ -124,48 +131,48 @@ If a sufficient number of users agree on their answer to the unknown word, it is
The service can be accessed using a simple HTTP API. The service can be accessed using a simple HTTP API.
### - `/v1/captcha`: `POST` ### - `/v1/captcha`: `POST`
- Parameters: - Parameters:
- `level`: `String` - - `level`: `String` -
The difficulty level of a captcha The difficulty level of a captcha
- easy - easy
- medium - medium
- hard - hard
- `input_type`: `String` - - `input_type`: `String` -
The type of input option for a captcha The type of input option for a captcha
- text - text
- (More to come) - (More to come)
- `media`: `String` - - `media`: `String` -
The type of media of a captcha The type of media of a captcha
- image/png - image/png
- image/gif - image/gif
- (More to come) - (More to come)
- `size`: `Map` - - `size`: String -
The dimensions of a captcha (Optional). It needs two more fields nested in this parameter The dimensions of a captcha. It needs to be a string in the format `"widthxheight"` in pixels, and will be matched
- `height`: `Int` with the `allowedSizes` config setting. Example: `size: "450x200"` which requests an image of width 450 and height
- `width`: `Int` 200 pixels.
- Returns: - Returns:
- `id`: `String` - The uuid of the captcha generated - `id`: `String` - The uuid of the captcha generated
### - `/v1/media`: `GET` ### - `/v1/media`: `GET`
- Parameters: - Parameters:
- `id`: `String` - The uuid of the captcha - `id`: `String` - The uuid of the captcha
- Returns: - Returns:
- `image`: `Array[Byte]` - The requested media as bytes - `image`: `Array[Byte]` - The requested media as bytes
### - `/v1/answer`: `POST` ### - `/v1/answer`: `POST`
- Parameter: - Parameter:
- `id`: `String` - The uuid of the captcha that needs to be solved - `id`: `String` - The uuid of the captcha that needs to be solved
- `answer`: `String` - The answer to the captcha that needs to be validated - `answer`: `String` - The answer to the captcha that needs to be validated
- Returns: - Returns:
- `result`: `String` - The result after validation/checking of the answer - `result`: `String` - The result after validation/checking of the answer
- True - If the answer is correct - True - If the answer is correct
- False - If the answer is incorrect - False - If the answer is incorrect
- Expired - If the time limit to solve the captcha exceeds - Expired - If the time limit to solve the captcha exceeds
## Example usage ## Example usage
@ -173,9 +180,9 @@ The service can be accessed using a simple HTTP API.
In javascript: In javascript:
```js ```js
const resp = await fetch("/v1/captcha", { const resp = await fetch("/v2/captcha", {
method: 'POST', method: 'POST',
body: JSON.stringify({level: "easy", media: "image/png", "input_type" : "text"}) body: JSON.stringify({level: "easy", media: "image/png", "input_type" : "text", size: "350x100"})
}) })
const respJson = await resp.json(); const respJson = await resp.json();
@ -183,19 +190,19 @@ const respJson = await resp.json();
let captchaId = null; let captchaId = null;
if (resp.ok) { if (resp.ok) {
// The CAPTCHA can be displayed using the data in respJson. // The CAPTCHA can be displayed using the data in respJson.
console.log(respJson); console.log(respJson);
// Store the id somewhere so that it can be used later for answer verification // Store the id somewhere so that it can be used later for answer verification
captchaId = respJson.id; captchaId = respJson.id;
} else { } else {
console.err(respJson); console.err(respJson);
} }
// When user submits an answer it can be sent to the server for verification thusly: // When user submits an answer it can be sent to the server for verification thusly:
const resp = await fetch("/v1/answer", { const resp = await fetch("/v2/answer", {
method: 'POST', method: 'POST',
body: JSON.stringify({id: captchaId, answer: "user input"}) body: JSON.stringify({id: captchaId, answer: "user input"})
}); });
const respJson = await resp.json(); const respJson = await resp.json();
console.log(respJson.result); console.log(respJson.result);

View File

@ -1,12 +1,12 @@
FROM adoptopenjdk/openjdk16:alpine-jre AS base-core FROM eclipse-temurin:17-jre-jammy AS base-core
ENV JAVA_HOME="/usr/lib/jvm/default-jvm/" ENV JAVA_HOME="/usr/lib/jvm/default-jvm/"
RUN apk add --update ttf-dejavu RUN apt update && apt install -y fonts-dejavu
ENV PATH=$PATH:${JAVA_HOME}/bin ENV PATH=$PATH:${JAVA_HOME}/bin
FROM base-core FROM base-core
RUN mkdir /lc-core RUN mkdir /lc-core
COPY target/scala-3.1.1/LibreCaptcha.jar /lc-core COPY target/scala-3.6.2/LibreCaptcha.jar /lc-core
WORKDIR /lc-core WORKDIR /lc-core
RUN mkdir data/ RUN mkdir data/

View File

@ -2,8 +2,8 @@ lazy val root = (project in file(".")).settings(
inThisBuild( inThisBuild(
List( List(
organization := "com.example", organization := "com.example",
scalaVersion := "3.1.1", scalaVersion := "3.6.2",
version := "0.1.0-SNAPSHOT", version := "0.2.1-snapshot",
semanticdbEnabled := true, semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision semanticdbVersion := scalafixSemanticdb.revision
@ -12,9 +12,9 @@ lazy val root = (project in file(".")).settings(
) )
), ),
name := "LibreCaptcha", name := "LibreCaptcha",
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.0.30", libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.3.0",
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.0.30", libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.3.0",
libraryDependencies += "org.json4s" %% "json4s-jackson" % "4.0.4" libraryDependencies += "org.json4s" %% "json4s-jackson" % "4.0.7"
) )
Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" } Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" }

View File

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

View File

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

View File

@ -1,4 +1,4 @@
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.34") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.7.0") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0")

View File

@ -5,12 +5,12 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.util.Map;
import java.util.List; import java.util.List;
import java.util.Map;
import lc.captchas.interfaces.Challenge; import lc.captchas.interfaces.Challenge;
import lc.captchas.interfaces.ChallengeProvider; import lc.captchas.interfaces.ChallengeProvider;
import lc.misc.PngImageWriter;
import lc.misc.HelperFunctions; import lc.misc.HelperFunctions;
import lc.misc.PngImageWriter;
public class FontFunCaptcha implements ChallengeProvider { public class FontFunCaptcha implements ChallengeProvider {
@ -58,9 +58,10 @@ public class FontFunCaptcha implements ChallengeProvider {
return null; return null;
} }
private byte[] fontFun(String captchaText, String level, String path) { private byte[] fontFun(
final int width, final int height, String captchaText, String level, String path) {
String[] colors = {"#f68787", "#f8a978", "#f1eb9a", "#a4f6a5"}; String[] colors = {"#f68787", "#f8a978", "#f1eb9a", "#a4f6a5"};
BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB); BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics2D = img.createGraphics(); Graphics2D graphics2D = img.createGraphics();
for (int i = 0; i < captchaText.length(); i++) { for (int i = 0; i < captchaText.length(); i++) {
Font font = loadCustomFont(level, path); Font font = loadCustomFont(level, path);
@ -81,10 +82,14 @@ public class FontFunCaptcha implements ChallengeProvider {
return baos.toByteArray(); return baos.toByteArray();
} }
public Challenge returnChallenge() { public Challenge returnChallenge(String level, String size) {
String secret = HelperFunctions.randomString(7); String secret = HelperFunctions.randomString(7);
final int[] size2D = HelperFunctions.parseSize2D(size);
final int width = size2D[0];
final int height = size2D[1];
String path = "./lib/fonts/"; String path = "./lib/fonts/";
return new Challenge(fontFun(secret, "medium", path), "image/png", secret.toLowerCase()); return new Challenge(
fontFun(width, height, secret, "medium", path), "image/png", secret.toLowerCase());
} }
public boolean checkAnswer(String secret, String answer) { public boolean checkAnswer(String secret, String answer) {

View File

@ -1,51 +1,51 @@
package lc.captchas; package lc.captchas;
import java.awt.Font;
import java.awt.RenderingHints;
import java.awt.Color; import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import java.util.LinkedList;
import java.util.List;
import javax.imageio.stream.MemoryCacheImageOutputStream; import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.io.ByteArrayOutputStream;
import lc.captchas.interfaces.Challenge; import lc.captchas.interfaces.Challenge;
import lc.captchas.interfaces.ChallengeProvider; import lc.captchas.interfaces.ChallengeProvider;
import lc.misc.HelperFunctions;
import lc.misc.GifSequenceWriter; import lc.misc.GifSequenceWriter;
import lc.misc.HelperFunctions;
public class PoppingCharactersCaptcha implements ChallengeProvider { public class PoppingCharactersCaptcha implements ChallengeProvider {
private final Font font = new Font("Arial", Font.ROMAN_BASELINE, 48);
private final int width = 250;
private final int height = 100;
private Integer[] computeOffsets(final String text) { private int[] computeOffsets(
final Font font, final int width, final int height, final String text) {
final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final var graphics2D = img.createGraphics(); final var graphics2D = img.createGraphics();
final var frc = graphics2D.getFontRenderContext(); final var frc = graphics2D.getFontRenderContext();
final var advances = new LinkedList<Integer>(); final var advances = new int[text.length() + 1];
final var spacing = font.getStringBounds(" ", frc).getWidth() / 3; final var spacing = font.getStringBounds(" ", frc).getWidth() / 3;
var currX = 0; var currX = 0;
for (int i = 0; i < text.length(); i++) { for (int i = 0; i < text.length(); i++) {
final var c = text.charAt(i); final var c = text.charAt(i);
advances.add(currX); advances[i] = currX;
currX += font.getStringBounds(String.valueOf(c), frc).getWidth(); currX += font.getStringBounds(String.valueOf(c), frc).getWidth();
currX += spacing; currX += spacing;
}; }
;
advances[text.length()] = currX;
graphics2D.dispose(); graphics2D.dispose();
return advances.toArray(new Integer[]{}); return advances;
} }
private BufferedImage makeImage(final Consumer<Graphics2D> f) { private BufferedImage makeImage(
final Font font, final int width, final int height, final Consumer<Graphics2D> f) {
final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); final var img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final var graphics2D = img.createGraphics(); final var graphics2D = img.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); graphics2D.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
graphics2D.setFont(font); graphics2D.setFont(font);
f.accept(graphics2D); f.accept(graphics2D);
graphics2D.dispose(); graphics2D.dispose();
@ -56,30 +56,49 @@ public class PoppingCharactersCaptcha implements ChallengeProvider {
return HelperFunctions.randomNumber(-2, +2); return HelperFunctions.randomNumber(-2, +2);
} }
private byte[] gifCaptcha(final String text) { private byte[] gifCaptcha(final int width, final int height, final String text) {
try { try {
final var fontHeight = (int) (height * 0.5);
final Font font = new Font("Arial", Font.ROMAN_BASELINE, fontHeight);
final var byteArrayOutputStream = new ByteArrayOutputStream(); final var byteArrayOutputStream = new ByteArrayOutputStream();
final var output = new MemoryCacheImageOutputStream(byteArrayOutputStream); final var output = new MemoryCacheImageOutputStream(byteArrayOutputStream);
final var writer = new GifSequenceWriter(output, 1, 900, true); final var writer = new GifSequenceWriter(output, 1, 900, true);
final var advances = computeOffsets(text); final var advances = computeOffsets(font, width, height, text);
final var expectedWidth = advances[advances.length - 1];
final var scale = width / (float) expectedWidth;
final var prevColor = Color.getHSBColor(0f, 0f, 0.1f); final var prevColor = Color.getHSBColor(0f, 0f, 0.1f);
IntStream.range(0, text.length()).forEach(i -> { IntStream.range(0, text.length())
final var color = Color.getHSBColor(HelperFunctions.randomNumber(0, 100)/100.0f, 0.6f, 1.0f); .forEach(
final var nextImage = makeImage((g) -> { i -> {
if (i > 0) { final var color =
final var prevI = (i - 1) % text.length(); Color.getHSBColor(HelperFunctions.randomNumber(0, 100) / 100.0f, 0.6f, 1.0f);
g.setColor(prevColor); final var nextImage =
g.drawString(String.valueOf(text.charAt(prevI)), advances[prevI] + jitter(), 45 + jitter()); makeImage(
} font,
g.setColor(color); width,
g.drawString(String.valueOf(text.charAt(i)), advances[i] + jitter(), 45 + jitter()); height,
}); (g) -> {
try { g.scale(scale, 1);
writer.writeToSequence(nextImage); if (i > 0) {
} catch (final IOException e) { final var prevI = (i - 1) % text.length();
e.printStackTrace(); 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(); writer.close();
output.close(); output.close();
return byteArrayOutputStream.toByteArray(); return byteArrayOutputStream.toByteArray();
@ -100,9 +119,12 @@ public class PoppingCharactersCaptcha implements ChallengeProvider {
"supportedInputType", List.of("text")); "supportedInputType", List.of("text"));
} }
public Challenge returnChallenge() { public Challenge returnChallenge(String level, String size) {
final var secret = HelperFunctions.randomString(6); final var secret = HelperFunctions.randomString(6);
return new Challenge(gifCaptcha(secret), "image/gif", secret.toLowerCase()); final int[] size2D = HelperFunctions.parseSize2D(size);
final int width = size2D[0];
final int height = size2D[1];
return new Challenge(gifCaptcha(width, height, secret), "image/gif", secret.toLowerCase());
} }
public boolean checkAnswer(String secret, String answer) { public boolean checkAnswer(String secret, String answer) {

View File

@ -1,21 +1,18 @@
package lc.captchas; package lc.captchas;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Color; import java.awt.Color;
import java.awt.Font; import java.awt.Font;
import java.awt.font.TextLayout; import java.awt.Graphics2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp; import java.awt.image.ConvolveOp;
import java.awt.image.Kernel; import java.awt.image.Kernel;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.util.Map;
import java.util.List; import java.util.List;
import java.util.Map;
import lc.misc.HelperFunctions;
import lc.misc.PngImageWriter;
import lc.captchas.interfaces.Challenge; import lc.captchas.interfaces.Challenge;
import lc.captchas.interfaces.ChallengeProvider; import lc.captchas.interfaces.ChallengeProvider;
import lc.misc.HelperFunctions;
import lc.misc.PngImageWriter;
public class ShadowTextCaptcha implements ChallengeProvider { public class ShadowTextCaptcha implements ChallengeProvider {
@ -38,32 +35,43 @@ public class ShadowTextCaptcha implements ChallengeProvider {
return answer.toLowerCase().equals(secret); return answer.toLowerCase().equals(secret);
} }
private byte[] shadowText(String text) { private float[] makeKernel(int size) {
BufferedImage img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB); final int N = size * size;
Font font = new Font("Arial", Font.ROMAN_BASELINE, 48); final float weight = 1.0f / (N);
Graphics2D graphics2D = img.createGraphics(); final float[] kernel = new float[N];
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); java.util.Arrays.fill(kernel, weight);
graphics2D.setRenderingHint( return kernel;
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); };
TextLayout textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext()); private byte[] shadowText(final int width, final int height, String text) {
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final int fontHeight = (int) (height * 0.5f);
Font font = new Font("Arial", Font.PLAIN, fontHeight);
Graphics2D graphics2D = img.createGraphics();
HelperFunctions.setRenderingHints(graphics2D); HelperFunctions.setRenderingHints(graphics2D);
graphics2D.setPaint(Color.WHITE); graphics2D.setPaint(Color.WHITE);
graphics2D.fillRect(0, 0, 350, 100); graphics2D.fillRect(0, 0, width, height);
graphics2D.setPaint(Color.BLACK); graphics2D.setPaint(Color.BLACK);
textLayout.draw(graphics2D, 15, 50); graphics2D.setFont(font);
final var stringWidth = graphics2D.getFontMetrics().stringWidth(text);
final var padding = (stringWidth > width) ? 0 : (width - stringWidth) / 2;
final var scaleX = (stringWidth > width) ? width / ((double) stringWidth) : 1d;
graphics2D.scale(scaleX, 1d);
graphics2D.drawString(text, padding, fontHeight * 1.1f);
graphics2D.dispose(); graphics2D.dispose();
float[] kernel = { final int kernelSize = (int) Math.ceil((Math.min(width, height) / 50.0));
1f / 9f, 1f / 9f, 1f / 9f, ConvolveOp op =
1f / 9f, 1f / 9f, 1f / 9f, new ConvolveOp(
1f / 9f, 1f / 9f, 1f / 9f new Kernel(kernelSize, kernelSize, makeKernel(kernelSize)),
}; ConvolveOp.EDGE_NO_OP,
ConvolveOp op = new ConvolveOp(new Kernel(3, 3, kernel), ConvolveOp.EDGE_NO_OP, null); null);
BufferedImage img2 = op.filter(img, null); BufferedImage img2 = op.filter(img, null);
Graphics2D g2d = img2.createGraphics(); Graphics2D g2d = img2.createGraphics();
HelperFunctions.setRenderingHints(g2d); HelperFunctions.setRenderingHints(g2d);
g2d.setPaint(Color.WHITE); g2d.setPaint(Color.WHITE);
textLayout.draw(g2d, 13, 50); g2d.scale(scaleX, 1d);
g2d.setFont(font);
g2d.drawString(text, padding - kernelSize, fontHeight * 1.1f);
g2d.dispose(); g2d.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
try { try {
@ -74,8 +82,11 @@ public class ShadowTextCaptcha implements ChallengeProvider {
return baos.toByteArray(); return baos.toByteArray();
} }
public Challenge returnChallenge() { public Challenge returnChallenge(String level, String size) {
String secret = HelperFunctions.randomString(6); String secret = HelperFunctions.randomString(6);
return new Challenge(shadowText(secret), "image/png", secret.toLowerCase()); final int[] size2D = HelperFunctions.parseSize2D(size);
final int width = size2D[0];
final int height = size2D[1];
return new Challenge(shadowText(width, height, secret), "image/png", secret.toLowerCase());
} }
} }

View File

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

View File

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

View File

@ -11,7 +11,14 @@ public class HelperFunctions {
random.setSeed(seed); random.setSeed(seed);
} }
public static int[] parseSize2D(final String size) {
final String[] fields = size.split("x");
final int[] result = {Integer.parseInt(fields[0]), Integer.parseInt(fields[1])};
return result;
}
public static void setRenderingHints(Graphics2D g2d) { public static void setRenderingHints(Graphics2D g2d) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint( g2d.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint( g2d.setRenderingHint(
@ -23,7 +30,8 @@ public class HelperFunctions {
public static final String safeNumbers = "23456789"; public static final String safeNumbers = "23456789";
public static final String allNumbers = safeNumbers + "10"; public static final String allNumbers = safeNumbers + "10";
public static final String specialCharacters = "$#%@&?"; public static final String specialCharacters = "$#%@&?";
public static final String safeCharacters = safeAlphabets + safeNumbers + specialCharacters; public static final String safeAlphaNum = safeAlphabets + safeNumbers;
public static final String safeCharacters = safeAlphaNum + specialCharacters;
public static String randomString(final int n) { public static String randomString(final int n) {
return randomString(n, safeCharacters); return randomString(n, safeCharacters);

View File

@ -1,9 +1,9 @@
package lc.misc; package lc.misc;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Iterator; import java.util.Iterator;
import javax.imageio.IIOImage; import javax.imageio.IIOImage;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageTypeSpecifier;
@ -13,7 +13,6 @@ import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
public class PngImageWriter { public class PngImageWriter {
@ -26,7 +25,8 @@ public class PngImageWriter {
iw.hasNext(); ) { iw.hasNext(); ) {
ImageWriter writer = iw.next(); ImageWriter writer = iw.next();
ImageWriteParam writeParam = writer.getDefaultWriteParam(); ImageWriteParam writeParam = writer.getDefaultWriteParam();
ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB); ImageTypeSpecifier typeSpecifier =
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam); IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam);
if (metadata.isReadOnly() || !metadata.isStandardMetadataFormatSupported()) { if (metadata.isReadOnly() || !metadata.isStandardMetadataFormatSupported()) {
continue; continue;

View File

@ -3,21 +3,21 @@
package org.limium.picoserve; package org.limium.picoserve;
import java.net.InetSocketAddress; import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.net.InetSocketAddress;
import java.util.Map;
import java.util.LinkedList;
import java.util.concurrent.Executor;
import java.util.regex.Pattern;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import com.sun.net.httpserver.HttpServer; import java.util.Arrays;
import com.sun.net.httpserver.HttpHandler; import java.util.LinkedList;
import com.sun.net.httpserver.HttpExchange; import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public final class Server { public final class Server {
private final HttpServer server; private final HttpServer server;

View File

@ -20,9 +20,10 @@
const levelInput = document.getElementById("levelInput").value const levelInput = document.getElementById("levelInput").value
const mediaInput = document.getElementById("mediaInput").value const mediaInput = document.getElementById("mediaInput").value
const typeInput = document.getElementById("typeInput").value const typeInput = document.getElementById("typeInput").value
fetch("/v1/captcha", { const sizeInput = document.getElementById("sizeInput").value
fetch("/v2/captcha", {
method: 'POST', method: 'POST',
body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput}) body: JSON.stringify({level: levelInput, media: mediaInput, "input_type" : typeInput, "size": sizeInput})
}).then(async function(resp) { }).then(async function(resp) {
const respJson = await resp.json() const respJson = await resp.json()
if (resp.ok) { if (resp.ok) {
@ -30,7 +31,7 @@
const resultDiv = document.getElementById("result") const resultDiv = document.getElementById("result")
const result = ` const result = `
<p>Id: ${id}</p> <p>Id: ${id}</p>
<p><img src="/v1/media?id=${id}" /> </p> <p><img src="/v2/media?id=${id}" /> </p>
<input type="text" id="answerInput" /> <input type="text" id="answerInput" />
<button onClick="submitAnswer('${id}')">Submit</button> <button onClick="submitAnswer('${id}')">Submit</button>
<div id="answerResult" /> <div id="answerResult" />
@ -43,7 +44,7 @@
} }
async function submitAnswer(id) { async function submitAnswer(id) {
const ans = document.getElementById("answerInput").value; const ans = document.getElementById("answerInput").value;
const resp = await fetch("/v1/answer", { const resp = await fetch("/v2/answer", {
method: 'POST', method: 'POST',
body: JSON.stringify({id: id, answer: ans}) body: JSON.stringify({id: id, answer: ans})
}) })
@ -70,6 +71,10 @@
<span>Input Type</span> <span>Input Type</span>
<input type="text" id="typeInput" value="text" /> <input type="text" id="typeInput" value="text" />
</div> </div>
<div class="inputGroup">
<span>Input Size</span>
<input type="text" id="sizeInput" value="350x100" />
</div>
<div class="inputGroup"> <div class="inputGroup">
<button onClick="loadCaptcha()">Get New CAPTCHA</button> <button onClick="loadCaptcha()">Get New CAPTCHA</button>
</div> </div>
@ -79,4 +84,4 @@
</div> </div>
</body> </body>
</html> </html>

View File

@ -25,6 +25,14 @@ object LCFramework {
playgroundEnabled = config.playgroundEnabled, playgroundEnabled = config.playgroundEnabled,
corsHeader = config.corsHeader corsHeader = config.corsHeader
) )
Runtime.getRuntime.addShutdownHook(new Thread {
override def run(): Unit = {
println("Shutting down gracefully...")
backgroundTask.shutdown()
}
})
server.start() server.start()
} }
} }

View File

@ -18,38 +18,73 @@ class BackgroundTask(config: Config, captchaManager: CaptchaManager) {
val challengeGCPstmt = Statements.tlStmts.get.challengeGCPstmt val challengeGCPstmt = Statements.tlStmts.get.challengeGCPstmt
challengeGCPstmt.executeUpdate() challengeGCPstmt.executeUpdate()
val imageNumResult = Statements.tlStmts.get.getCountChallengeTable.executeQuery() val allCombinations = allParameterCombinations()
val imageNum = if (imageNumResult.next()) { val requiredCountPerCombination = Math.max(1, (config.bufferCount * 1.01) / allCombinations.size).toInt
imageNumResult.getInt("total")
} else {
0
}
val throttle = (config.throttle * 1.1).toInt - imageNum 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 throttle) { for (i <- 0 until countCreate) {
captchaManager.generateChallenge(getRandomParam()) if (!shutdownInProgress) {
captchaManager.generateChallenge(param)
}
}
}
}
} }
} catch { case exception: Exception => println(exception) } } catch { case exception: Exception => println(exception) }
} }
} }
private def allParameterCombinations(): List[Parameters] = {
(config.captchaConfig).flatMap { captcha =>
(captcha.allowedLevels).flatMap { level =>
(captcha.allowedMedia).flatMap { media =>
(captcha.allowedInputType).flatMap { inputType =>
(captcha.allowedSizes).map { size =>
Parameters(level, media, inputType, size)
}
}
}
}
}
}
private def getRandomParam(): Parameters = { private def getRandomParam(): Parameters = {
val captcha = pickRandom(config.captchaConfig) val captcha = pickRandom(config.captchaConfig)
val level = pickRandom(captcha.allowedLevels) val level = pickRandom(captcha.allowedLevels)
val media = pickRandom(captcha.allowedMedia) val media = pickRandom(captcha.allowedMedia)
val inputType = pickRandom(captcha.allowedInputType) val inputType = pickRandom(captcha.allowedInputType)
val size = pickRandom(captcha.allowedSizes)
Parameters(level, media, inputType, Some(Size(0, 0))) Parameters(level, media, inputType, size)
} }
private def pickRandom[T](list: List[T]): T = { private def pickRandom[T](list: List[T]): T = {
list(HelperFunctions.randomNumber(list.size)) list(HelperFunctions.randomNumber(list.size))
} }
private val ex = new ScheduledThreadPoolExecutor(1)
def beginThread(delay: Int): Unit = { def beginThread(delay: Int): Unit = {
val ex = new ScheduledThreadPoolExecutor(1)
ex.scheduleWithFixedDelay(task, 1, delay, TimeUnit.SECONDS) ex.scheduleWithFixedDelay(task, 1, delay, TimeUnit.SECONDS)
} }
@volatile var shutdownInProgress = false
def shutdown(): Unit = {
println(" Shutting down background task...")
shutdownInProgress = true
ex.shutdown()
println(" Finished Shutting background task")
println(" Shutting down DB...")
Statements.tlStmts.get.shutdown.execute()
println(" Finished shutting down db")
}
} }

View File

@ -45,14 +45,14 @@ class DebugCaptcha extends ChallengeProvider {
matches matches
} }
private def simpleText(text: String): Array[Byte] = { private def simpleText(width: Int, height: Int, text: String): Array[Byte] = {
val img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB) val img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
val font = new Font("Arial", Font.ROMAN_BASELINE, 56) val font = new Font("Arial", Font.ROMAN_BASELINE, 56)
val graphics2D = img.createGraphics() val graphics2D = img.createGraphics()
val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext()) val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext())
HelperFunctions.setRenderingHints(graphics2D) HelperFunctions.setRenderingHints(graphics2D)
graphics2D.setPaint(Color.WHITE) graphics2D.setPaint(Color.WHITE)
graphics2D.fillRect(0, 0, 350, 100) graphics2D.fillRect(0, 0, width, height)
graphics2D.setPaint(Color.BLACK) graphics2D.setPaint(Color.BLACK)
textLayout.draw(graphics2D, 15, 50) textLayout.draw(graphics2D, 15, 50)
graphics2D.dispose() graphics2D.dispose()
@ -66,8 +66,11 @@ class DebugCaptcha extends ChallengeProvider {
baos.toByteArray() baos.toByteArray()
} }
def returnChallenge(): Challenge = { def returnChallenge(level: String, size: String): Challenge = {
val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets) val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets)
new Challenge(simpleText(secret), "image/png", secret.toLowerCase()) val size2D = HelperFunctions.parseSize2D(size)
val width = size2D(0)
val height = size2D(1)
new Challenge(simpleText(width, height, secret), "image/png", secret.toLowerCase())
} }
} }

View File

@ -10,6 +10,7 @@ import lc.captchas.interfaces.Challenge
import java.util.{List => JavaList, Map => JavaMap} import java.util.{List => JavaList, Map => JavaMap}
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import lc.misc.PngImageWriter import lc.misc.PngImageWriter
import lc.misc.HelperFunctions
class FilterChallenge extends ChallengeProvider { class FilterChallenge extends ChallengeProvider {
def getId = "FilterChallenge" def getId = "FilterChallenge"
@ -29,31 +30,38 @@ class FilterChallenge extends ChallengeProvider {
) )
} }
def returnChallenge(): Challenge = { private val filterTypes = List(new FilterType1, new FilterType2)
val filterTypes = List(new FilterType1, new FilterType2)
def returnChallenge(level: String, size: String): Challenge = {
val mediumLevel = level == "medium"
val r = new scala.util.Random val r = new scala.util.Random
val alphabet = "abcdefghijklmnopqrstuvwxyz" val characters = if (mediumLevel) HelperFunctions.safeAlphaNum else HelperFunctions.safeCharacters
val n = 8 val n = if (mediumLevel) 5 else 7
val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString val secret = LazyList.continually(r.nextInt(characters.size)).map(characters).take(n).mkString
val canvas = new BufferedImage(225, 50, BufferedImage.TYPE_INT_RGB) val size2D = HelperFunctions.parseSize2D(size)
val width = size2D(0)
val height = size2D(1)
val canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
val g = canvas.createGraphics() val g = canvas.createGraphics()
val fontHeight = (height * 0.6).toInt
g.setColor(Color.WHITE) g.setColor(Color.WHITE)
g.fillRect(0, 0, canvas.getWidth, canvas.getHeight) g.fillRect(0, 0, canvas.getWidth, canvas.getHeight)
g.setColor(Color.BLACK) g.setColor(Color.BLACK)
g.setFont(new Font("Serif", Font.PLAIN, 30)) val font = new Font("Serif", Font.BOLD, fontHeight)
g.drawString(secret, 5, 30) g.setFont(font)
val stringWidth = g.getFontMetrics().stringWidth(secret)
val scaleX = if (stringWidth > width) width / (stringWidth.toDouble) else 1d
val margin = if (stringWidth > width) 0 else (width - stringWidth)
val xOffset = (margin * r.nextDouble).toInt
g.scale(scaleX, 1d)
g.drawString(secret, xOffset, fontHeight)
g.dispose() g.dispose()
var image = ImmutableImage.fromAwt(canvas) var image = ImmutableImage.fromAwt(canvas)
val s = scala.util.Random.nextInt(2) val s = r.nextInt(2)
image = filterTypes(s).applyFilter(image) image = filterTypes(s).applyFilter(image, !mediumLevel)
val img = image.awt() val img = image.awt()
val baos = new ByteArrayOutputStream() val baos = new ByteArrayOutputStream()
try { PngImageWriter.write(baos, img);
PngImageWriter.write(baos, img);
} catch {
case e: Exception =>
e.printStackTrace()
}
new Challenge(baos.toByteArray, "image/png", secret) new Challenge(baos.toByteArray, "image/png", secret)
} }
def checkAnswer(secret: String, answer: String): Boolean = { def checkAnswer(secret: String, answer: String): Boolean = {
@ -62,14 +70,15 @@ class FilterChallenge extends ChallengeProvider {
} }
trait FilterType { trait FilterType {
def applyFilter(image: ImmutableImage): ImmutableImage def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage
} }
class FilterType1 extends FilterType { class FilterType1 extends FilterType {
override def applyFilter(image: ImmutableImage): ImmutableImage = { override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = {
val blur = new GaussianBlurFilter(2) val radius = if (hardLevel) 3 else 2
val blur = new GaussianBlurFilter(radius)
val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1) val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1)
val diffuse = new DiffuseFilter(2) val diffuse = new DiffuseFilter(radius.toFloat)
blur.apply(image) blur.apply(image)
diffuse.apply(image) diffuse.apply(image)
smear.apply(image) smear.apply(image)
@ -78,9 +87,10 @@ class FilterType1 extends FilterType {
} }
class FilterType2 extends FilterType { class FilterType2 extends FilterType {
override def applyFilter(image: ImmutableImage): ImmutableImage = { override def applyFilter(image: ImmutableImage, hardLevel: Boolean): ImmutableImage = {
val radius = if (hardLevel) 2f else 1f
val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1) val smear = new SmearFilter(com.sksamuel.scrimage.filter.SmearType.Circles, 10, 10, 10, 0, 1)
val diffuse = new DiffuseFilter(1) val diffuse = new DiffuseFilter(radius)
val ripple = new RippleFilter(com.sksamuel.scrimage.filter.RippleType.Noise, 1, 1, 0.005.toFloat, 0.005.toFloat) val ripple = new RippleFilter(com.sksamuel.scrimage.filter.RippleType.Noise, 1, 1, 0.005.toFloat, 0.005.toFloat)
diffuse.apply(image) diffuse.apply(image)
ripple.apply(image) ripple.apply(image)

View File

@ -40,7 +40,7 @@ class LabelCaptcha extends ChallengeProvider {
) )
} }
def returnChallenge(): Challenge = def returnChallenge(level: String, size: String): Challenge =
synchronized { synchronized {
val r = scala.util.Random.nextInt(knownFiles.length) val r = scala.util.Random.nextInt(knownFiles.length)
val s = scala.util.Random.nextInt(unknownFiles.length) val s = scala.util.Random.nextInt(unknownFiles.length)

View File

@ -11,6 +11,7 @@ import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge import lc.captchas.interfaces.Challenge
import lc.misc.GifSequenceWriter import lc.misc.GifSequenceWriter
import java.util.{List => JavaList, Map => JavaMap} import java.util.{List => JavaList, Map => JavaMap}
import lc.misc.HelperFunctions
class Drop { class Drop {
var x = 0 var x = 0
@ -24,8 +25,6 @@ class Drop {
} }
class RainDropsCP extends ChallengeProvider { class RainDropsCP extends ChallengeProvider {
private val alphabet = "abcdefghijklmnopqrstuvwxyz"
private val n = 6
private val bgColor = new Color(200, 200, 200) private val bgColor = new Color(200, 200, 200)
private val textColor = new Color(208, 208, 218) private val textColor = new Color(208, 208, 218)
private val textHighlightColor = new Color(100, 100, 125) private val textHighlightColor = new Color(100, 100, 125)
@ -56,11 +55,13 @@ class RainDropsCP extends ChallengeProvider {
}) })
} }
def returnChallenge(): Challenge = { def returnChallenge(level: String, size: String): Challenge = {
val r = new scala.util.Random val r = new scala.util.Random
val secret = LazyList.continually(r.nextInt(alphabet.size)).map(alphabet).take(n).mkString val n = if (level == "easy") 4 else 6
val width = 450 val secret = HelperFunctions.randomString(n, HelperFunctions.safeAlphaNum)
val height = 100 val size2D = HelperFunctions.parseSize2D(size)
val width = size2D(0)
val height = size2D(1)
val imgType = BufferedImage.TYPE_INT_RGB val imgType = BufferedImage.TYPE_INT_RGB
val xOffset = 2 + r.nextInt(3) val xOffset = 2 + r.nextInt(3)
val xBias = (height / 10) - 2 val xBias = (height / 10) - 2
@ -80,7 +81,8 @@ class RainDropsCP extends ChallengeProvider {
xOffset xOffset
) )
val baseFont = new Font(Font.MONOSPACED, Font.BOLD, 80) val fontHeight = (height * 0.5f).toInt
val baseFont = new Font(Font.MONOSPACED, Font.BOLD, fontHeight)
val attributes = new java.util.HashMap[TextAttribute, Object]() val attributes = new java.util.HashMap[TextAttribute, Object]()
attributes.put(TextAttribute.TRACKING, Double.box(0.2)) attributes.put(TextAttribute.TRACKING, Double.box(0.2))
attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_EXTRABOLD) attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_EXTRABOLD)
@ -117,17 +119,22 @@ class RainDropsCP extends ChallengeProvider {
} }
} }
// center the text
g.setFont(spacedFont) g.setFont(spacedFont)
val textWidth = g.getFontMetrics().charsWidth(secret.toCharArray, 0, secret.toCharArray.length) val textWidth = g.getFontMetrics().stringWidth(secret)
val textX = (width - textWidth) / 2 val scaleX = if (textWidth > width) width / textWidth.toDouble else 1.0d
g.scale(scaleX, 1)
// paint the top outline // center the text
val textX = if (textWidth > width) 0 else ((width - textWidth) / 2)
// this will be overlapped by the following text to show the top outline because of the offset
val yOffset = (fontHeight * 0.01).ceil.toInt
g.setColor(textHighlightColor) g.setColor(textHighlightColor)
g.drawString(secret, textX, 69) g.drawString(secret, textX, (fontHeight * 1.1).toInt - yOffset)
// paint the text // paint the text
g.setColor(textColor) g.setColor(textColor)
g.drawString(secret, textX, 70) g.drawString(secret, textX, (fontHeight * 1.1).toInt)
g.dispose() g.dispose()
writer.writeToSequence(canvas) writer.writeToSequence(canvas)

View File

@ -10,7 +10,7 @@ object ParametersEnum extends Enumeration {
val ALLOWEDLEVELS: Value = Value("allowedLevels") val ALLOWEDLEVELS: Value = Value("allowedLevels")
val ALLOWEDMEDIA: Value = Value("allowedMedia") val ALLOWEDMEDIA: Value = Value("allowedMedia")
val ALLOWEDINPUTTYPE: Value = Value("allowedInputType") val ALLOWEDINPUTTYPE: Value = Value("allowedInputType")
val ALLOWEDSIZES: Value = Value("allowedSizes")
} }
object AttributesEnum extends Enumeration { object AttributesEnum extends Enumeration {
@ -21,12 +21,12 @@ object AttributesEnum extends Enumeration {
val PORT: Value = Value("port") val PORT: Value = Value("port")
val ADDRESS: Value = Value("address") val ADDRESS: Value = Value("address")
val CAPTCHA_EXPIRY_TIME_LIMIT: Value = Value("captchaExpiryTimeLimit") val CAPTCHA_EXPIRY_TIME_LIMIT: Value = Value("captchaExpiryTimeLimit")
val THROTTLE: Value = Value("throttle") val BUFFER_COUNT: Value = Value("bufferCount")
val THREAD_DELAY: Value = Value("threadDelay") val THREAD_DELAY: Value = Value("threadDelay")
val PLAYGROUND_ENABLED: Value = Value("playgroundEnabled") val PLAYGROUND_ENABLED: Value = Value("playgroundEnabled")
val CORS_HEADER: Value = Value("corsHeader") val CORS_HEADER: Value = Value("corsHeader")
val CONFIG: Value = Value("config") val CONFIG: Value = Value("config")
val MAX_ATTEMPTS: Value = Value("maxAttempts") val MAX_ATTEMPTS_RATIO: Value = Value("maxAttemptsRatio")
} }
object ResultEnum extends Enumeration { object ResultEnum extends Enumeration {

View File

@ -34,17 +34,19 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
} }
def generateChallenge(param: Parameters): Option[Int] = { def generateChallenge(param: Parameters): Option[Int] = {
val provider = captchaProviders.getProvider(param) try {
provider match { captchaProviders.getProvider(param).flatMap { provider =>
case Some(value) => { val providerId = provider.getId()
val providerId = value.getId() val challenge = provider.returnChallenge(param.level, param.size)
val challenge = value.returnChallenge()
val blob = new ByteArrayInputStream(challenge.content) val blob = new ByteArrayInputStream(challenge.content)
val token = insertCaptcha(value, challenge, providerId, param, blob) val token = insertCaptcha(provider, challenge, providerId, param, blob)
// println("Added new challenge: " + token.toString) // println("Added new challenge: " + token.toString)
token.map(_.toInt) token.map(_.toInt)
} }
case None => None } catch {
case e: Exception =>
e.printStackTrace()
None
} }
} }
@ -62,7 +64,8 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
insertPstmt.setString(4, challenge.contentType) insertPstmt.setString(4, challenge.contentType)
insertPstmt.setString(5, param.level) insertPstmt.setString(5, param.level)
insertPstmt.setString(6, param.input_type) insertPstmt.setString(6, param.input_type)
insertPstmt.setBlob(7, blob) insertPstmt.setString(7, param.size)
insertPstmt.setBlob(8, blob)
insertPstmt.executeUpdate() insertPstmt.executeUpdate()
val rs: ResultSet = insertPstmt.getGeneratedKeys() val rs: ResultSet = insertPstmt.getGeneratedKeys()
if (rs.next()) { if (rs.next()) {
@ -105,19 +108,40 @@ class CaptchaManager(config: Config, captchaProviders: CaptchaProviders) {
} }
} }
private def getToken(param: Parameters): Option[Int] = { def getCount(param: Parameters): Option[Int] = {
val tokenPstmt = Statements.tlStmts.get.tokenPstmt val countPstmt = Statements.tlStmts.get.countForParameterPstmt
tokenPstmt.setString(1, param.level) countPstmt.setString(1, param.level)
tokenPstmt.setString(2, param.media) countPstmt.setString(2, param.media)
tokenPstmt.setString(3, param.input_type) countPstmt.setString(3, param.input_type)
val rs = tokenPstmt.executeQuery() countPstmt.setString(4, param.size.toString())
val rs = countPstmt.executeQuery()
if (rs.next()) { if (rs.next()) {
Some(rs.getInt("token")) Some(rs.getInt("count"))
} else { } else {
None 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 = { private def updateAttempted(token: Int): Unit = {
val updateAttemptedPstmt = Statements.tlStmts.get.updateAttemptedPstmt val updateAttemptedPstmt = Statements.tlStmts.get.updateAttemptedPstmt
updateAttemptedPstmt.setInt(1, token) updateAttemptedPstmt.setInt(1, token)

View File

@ -19,7 +19,7 @@ class CaptchaProviders(config: Config) {
def generateChallengeSamples(): Map[String, Challenge] = { def generateChallengeSamples(): Map[String, Challenge] = {
providers.map { case (key, provider) => providers.map { case (key, provider) =>
(key, provider.returnChallenge()) (key, provider.returnChallenge("easy", "350x100"))
} }
} }
@ -35,6 +35,7 @@ class CaptchaProviders(config: Config) {
if configValue.allowedLevels.contains(param.level) if configValue.allowedLevels.contains(param.level)
if configValue.allowedMedia.contains(param.media) if configValue.allowedMedia.contains(param.media)
if configValue.allowedInputType.contains(param.input_type) if configValue.allowedInputType.contains(param.input_type)
if configValue.allowedSizes.contains(param.size)
} yield (configValue.name, configValue.config) } yield (configValue.name, configValue.config)
val providerFilter = for { val providerFilter = for {

View File

@ -45,13 +45,13 @@ class Config(configFilePath: String) {
val port: Int = configFields.portInt.getOrElse(8888) val port: Int = configFields.portInt.getOrElse(8888)
val address: String = configFields.address.getOrElse("0.0.0.0") val address: String = configFields.address.getOrElse("0.0.0.0")
val throttle: Int = configFields.throttleInt.getOrElse(1000) val bufferCount: Int = configFields.bufferCountInt.getOrElse(1000)
val seed: Int = configFields.seedInt.getOrElse(375264328) val seed: Int = configFields.seedInt.getOrElse(375264328)
val captchaExpiryTimeLimit: Int = configFields.captchaExpiryTimeLimitInt.getOrElse(5) val captchaExpiryTimeLimit: Int = configFields.captchaExpiryTimeLimitInt.getOrElse(5)
val threadDelay: Int = configFields.threadDelayInt.getOrElse(2) val threadDelay: Int = configFields.threadDelayInt.getOrElse(2)
val playgroundEnabled: Boolean = configFields.playgroundEnabledBool.getOrElse(true) val playgroundEnabled: Boolean = configFields.playgroundEnabledBool.getOrElse(true)
val corsHeader: String = configFields.corsHeader.getOrElse("") val corsHeader: String = configFields.corsHeader.getOrElse("")
val maxAttempts: Int = configFields.maxAttemptsInt.getOrElse(10) val maxAttempts: Int = Math.max(1, (configFields.maxAttemptsRatioFloat.getOrElse(0.01f) * bufferCount).toInt)
private val captchaConfigJson = (configJson \ "captchas") private val captchaConfigJson = (configJson \ "captchas")
val captchaConfigTransform: JValue = captchaConfigJson transformField { case JField("config", JObject(config)) => val captchaConfigTransform: JValue = captchaConfigJson transformField { case JField("config", JObject(config)) =>
@ -70,17 +70,18 @@ class Config(configFilePath: String) {
(AttributesEnum.PORT.toString -> 8888) ~ (AttributesEnum.PORT.toString -> 8888) ~
(AttributesEnum.ADDRESS.toString -> "0.0.0.0") ~ (AttributesEnum.ADDRESS.toString -> "0.0.0.0") ~
(AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString -> 5) ~ (AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString -> 5) ~
(AttributesEnum.THROTTLE.toString -> 1000) ~ (AttributesEnum.BUFFER_COUNT.toString -> 1000) ~
(AttributesEnum.THREAD_DELAY.toString -> 2) ~ (AttributesEnum.THREAD_DELAY.toString -> 2) ~
(AttributesEnum.PLAYGROUND_ENABLED.toString -> true) ~ (AttributesEnum.PLAYGROUND_ENABLED.toString -> true) ~
(AttributesEnum.CORS_HEADER.toString -> "") ~ (AttributesEnum.CORS_HEADER.toString -> "") ~
(AttributesEnum.MAX_ATTEMPTS.toString -> 10) ~ (AttributesEnum.MAX_ATTEMPTS_RATIO.toString -> 0.01f) ~
("captchas" -> List( ("captchas" -> List(
( (
(AttributesEnum.NAME.toString -> "FilterChallenge") ~ (AttributesEnum.NAME.toString -> "FilterChallenge") ~
(ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~ (ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject()) (AttributesEnum.CONFIG.toString -> JObject())
), ),
( (
@ -88,6 +89,7 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~ (ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject()) (AttributesEnum.CONFIG.toString -> JObject())
), ),
( (
@ -95,6 +97,7 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~ (ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject()) (AttributesEnum.CONFIG.toString -> JObject())
), ),
( (
@ -102,6 +105,7 @@ class Config(configFilePath: String) {
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~ (ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~ (ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~ (ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
(AttributesEnum.CONFIG.toString -> JObject()) (AttributesEnum.CONFIG.toString -> JObject())
) )
)) ))

View File

@ -5,7 +5,7 @@ import lc.core.Config.formats
trait ByteConvert { def toBytes(): Array[Byte] } trait ByteConvert { def toBytes(): Array[Byte] }
case class Size(height: Int, width: Int) case class Size(height: Int, width: Int)
case class Parameters(level: String, media: String, input_type: String, size: Option[Size]) case class Parameters(level: String, media: String, input_type: String, size: String)
case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } } case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } }
case class Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[Byte] = { image } } case class Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[Byte] = { image } }
case class Answer(answer: String, id: String) case class Answer(answer: String, id: String)
@ -16,28 +16,32 @@ case class CaptchaConfig(
allowedLevels: List[String], allowedLevels: List[String],
allowedMedia: List[String], allowedMedia: List[String],
allowedInputType: List[String], allowedInputType: List[String],
allowedSizes: List[String],
config: String config: String
) )
case class ConfigField( case class ConfigField(
port: Option[Integer], port: Option[Integer],
address: Option[String], address: Option[String],
throttle: Option[Integer], bufferCount: Option[Integer],
seed: Option[Integer], seed: Option[Integer],
captchaExpiryTimeLimit: Option[Integer], captchaExpiryTimeLimit: Option[Integer],
threadDelay: Option[Integer], threadDelay: Option[Integer],
playgroundEnabled: Option[java.lang.Boolean], playgroundEnabled: Option[java.lang.Boolean],
corsHeader: Option[String], corsHeader: Option[String],
maxAttempts: Option[Integer] maxAttemptsRatio: Option[java.lang.Float]
) { ) {
lazy val portInt: Option[Int] = mapInt(port) lazy val portInt: Option[Int] = mapInt(port)
lazy val throttleInt: Option[Int] = mapInt(throttle) lazy val bufferCountInt: Option[Int] = mapInt(bufferCount)
lazy val seedInt: Option[Int] = mapInt(seed) lazy val seedInt: Option[Int] = mapInt(seed)
lazy val captchaExpiryTimeLimitInt: Option[Int] = mapInt(captchaExpiryTimeLimit) lazy val captchaExpiryTimeLimitInt: Option[Int] = mapInt(captchaExpiryTimeLimit)
lazy val threadDelayInt: Option[Int] = mapInt(threadDelay) lazy val threadDelayInt: Option[Int] = mapInt(threadDelay)
lazy val maxAttemptsInt: Option[Int] = mapInt(maxAttempts) lazy val maxAttemptsRatioFloat: Option[Float] = mapFloat(maxAttemptsRatio)
lazy val playgroundEnabledBool: Option[Boolean] = playgroundEnabled.map(_ || true) lazy val playgroundEnabledBool: Option[Boolean] = playgroundEnabled.map(_ || false)
private def mapInt(x: Option[Integer]): Option[Int] = { private def mapInt(x: Option[Integer]): Option[Int] = {
x.map(_ + 0) x.map(_ + 0)
} }
private def mapFloat(x: Option[java.lang.Float]): Option[Float] = {
x.map(_ + 0.0f)
}
} }

View File

@ -3,7 +3,8 @@ package lc.database
import java.sql.{Connection, DriverManager, Statement} import java.sql.{Connection, DriverManager, Statement}
class DBConn() { class DBConn() {
val con: Connection = DriverManager.getConnection("jdbc:h2:./data/H2/captcha2", "sa", "") val con: Connection =
DriverManager.getConnection("jdbc:h2:./data/H2/captcha3;MAX_COMPACT_TIME=8000;DB_CLOSE_ON_EXIT=FALSE", "sa", "")
def getStatement(): Statement = { def getStatement(): Statement = {
con.createStatement() con.createStatement()

View File

@ -17,6 +17,7 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
"contentType varchar, " + "contentType varchar, " +
"contentLevel varchar, " + "contentLevel varchar, " +
"contentInput varchar, " + "contentInput varchar, " +
"size varchar, " +
"image blob, " + "image blob, " +
"attempted int default 0, " + "attempted int default 0, " +
"PRIMARY KEY(token));" + "PRIMARY KEY(token));" +
@ -37,8 +38,8 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
val insertPstmt: PreparedStatement = dbConn.con.prepareStatement( val insertPstmt: PreparedStatement = dbConn.con.prepareStatement(
"INSERT INTO " + "INSERT INTO " +
"challenge(id, secret, provider, contentType, contentLevel, contentInput, image) " + "challenge(id, secret, provider, contentType, contentLevel, contentInput, size, image) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)", "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS Statement.RETURN_GENERATED_KEYS
) )
@ -70,6 +71,18 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
"WHERE token = ?;" "WHERE token = ?;"
) )
val countForParameterPstmt: PreparedStatement = dbConn.con.prepareStatement(
s"""
SELECT count(*) as count
FROM challenge
WHERE attempted < $maxAttempts AND
contentLevel = ? AND
contentType = ? AND
contentInput = ? AND
size = ?
"""
)
val tokenPstmt: PreparedStatement = dbConn.con.prepareStatement( val tokenPstmt: PreparedStatement = dbConn.con.prepareStatement(
s""" s"""
SELECT token, attempted SELECT token, attempted
@ -77,8 +90,11 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
WHERE attempted < $maxAttempts AND WHERE attempted < $maxAttempts AND
contentLevel = ? AND contentLevel = ? AND
contentType = ? AND contentType = ? AND
contentInput = ? contentInput = ? AND
ORDER BY attempted ASC LIMIT 1""" size = ?
LIMIT 1
OFFSET FLOOR(RAND()*?)
"""
) )
val deleteAnswerPstmt: PreparedStatement = dbConn.con.prepareStatement( val deleteAnswerPstmt: PreparedStatement = dbConn.con.prepareStatement(
@ -107,6 +123,14 @@ class Statements(dbConn: DBConn, maxAttempts: Int) {
"SELECT * FROM mapId" "SELECT * FROM mapId"
) )
val shutdown: PreparedStatement = dbConn.con.prepareStatement(
"SHUTDOWN"
)
val shutdownCompact: PreparedStatement = dbConn.con.prepareStatement(
"SHUTDOWN COMPACT"
)
} }
object Statements { object Statements {

View File

@ -13,7 +13,13 @@ import java.net.InetSocketAddress
import java.util import java.util
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
class Server(address: String, port: Int, captchaManager: CaptchaManager, playgroundEnabled: Boolean, corsHeader: String) { class Server(
address: String,
port: Int,
captchaManager: CaptchaManager,
playgroundEnabled: Boolean,
corsHeader: String
) {
var headerMap: util.Map[String, util.List[String]] = _ var headerMap: util.Map[String, util.List[String]] = _
if (corsHeader.nonEmpty) { if (corsHeader.nonEmpty) {
headerMap = Map("Access-Control-Allow-Origin" -> List(corsHeader).asJava).asJava headerMap = Map("Access-Control-Allow-Origin" -> List(corsHeader).asJava).asJava
@ -23,7 +29,7 @@ class Server(address: String, port: Int, captchaManager: CaptchaManager, playgro
.address(new InetSocketAddress(address, port)) .address(new InetSocketAddress(address, port))
.backlog(32) .backlog(32)
.POST( .POST(
"/v1/captcha", "/v2/captcha",
(request) => { (request) => {
val json = parse(request.getBodyString()) val json = parse(request.getBodyString())
val param = json.extract[Parameters] val param = json.extract[Parameters]
@ -32,7 +38,7 @@ class Server(address: String, port: Int, captchaManager: CaptchaManager, playgro
} }
) )
.GET( .GET(
"/v1/media", "/v2/media",
(request) => { (request) => {
val params = request.getQueryParams() val params = request.getQueryParams()
val result = if (params.containsKey("id")) { val result = if (params.containsKey("id")) {
@ -46,7 +52,7 @@ class Server(address: String, port: Int, captchaManager: CaptchaManager, playgro
} }
) )
.POST( .POST(
"/v1/answer", "/v2/answer",
(request) => { (request) => {
val json = parse(request.getBodyString()) val json = parse(request.getBodyString())
val answer = json.extract[Answer] val answer = json.extract[Answer]
@ -63,6 +69,20 @@ class Server(address: String, port: Int, captchaManager: CaptchaManager, playgro
new StringResponse(200, str) new StringResponse(200, str)
} }
) )
serverBuilder.GET(
"/",
(_) => {
val str = """
<html>
<h2>Welcome to LibreCaptcha server</h2>
<h3><a href="/demo/index.html">Link to Demo</a></h3>
<h3>API is served at <b>/v2/</b></h3>
</html>
"""
new StringResponse(200, str)
}
)
println("Playground enabled on /demo/index.html")
} }
val server: picoserve.Server = serverBuilder.build() val server: picoserve.Server = serverBuilder.build()

View File

@ -3,16 +3,17 @@
"port" : 8888, "port" : 8888,
"address" : "0.0.0.0", "address" : "0.0.0.0",
"captchaExpiryTimeLimit" : 5, "captchaExpiryTimeLimit" : 5,
"throttle" : 10, "bufferCount" : 10,
"threadDelay" : 2, "threadDelay" : 2,
"playgroundEnabled" : false, "playgroundEnabled" : false,
"corsHeader" : "*", "corsHeader" : "*",
"maxAttempts" : 20, "maxAttemptsRatio" : 0.01,
"captchas" : [ { "captchas" : [ {
"name" : "DebugCaptcha", "name" : "DebugCaptcha",
"allowedLevels" : [ "debug" ], "allowedLevels" : [ "debug" ],
"allowedMedia" : [ "image/png" ], "allowedMedia" : [ "image/png" ],
"allowedInputType" : [ "text" ], "allowedInputType" : [ "text" ],
"allowedSizes" : [ "350x100" ],
"config" : { } "config" : { }
}] }]
} }

View File

@ -22,9 +22,9 @@ class QuickStartUser(SequentialTaskSet):
@task @task
def captcha(self): def captcha(self):
captcha_params = {"level":"debug","media":"image/png","input_type":"text"} captcha_params = {"level":"debug","media":"image/png","input_type":"text", "size":"350x100"}
with self.client.post(path="/v1/captcha", json=captcha_params, name="/captcha", catch_response = True) as resp: with self.client.post(path="/v2/captcha", json=captcha_params, name="/captcha", catch_response = True) as resp:
if resp.status_code != 200: if resp.status_code != 200:
resp.failure("Status was not 200: " + resp.text) resp.failure("Status was not 200: " + resp.text)
captchaJson = resp.json() captchaJson = resp.json()
@ -32,7 +32,7 @@ class QuickStartUser(SequentialTaskSet):
if not uuid: if not uuid:
resp.failure("uuid not returned on /captcha endpoint: " + resp.text) resp.failure("uuid not returned on /captcha endpoint: " + resp.text)
with self.client.get(path="/v1/media?id=%s" % uuid, name="/media", stream=True, catch_response = True) as resp: with self.client.get(path="/v2/media?id=%s" % uuid, name="/media", stream=True, catch_response = True) as resp:
if resp.status_code != 200: if resp.status_code != 200:
resp.failure("Status was not 200: " + resp.text) resp.failure("Status was not 200: " + resp.text)
@ -41,7 +41,7 @@ class QuickStartUser(SequentialTaskSet):
ocrAnswer = self.solve(uuid, media) ocrAnswer = self.solve(uuid, media)
answerBody = {"answer": ocrAnswer,"id": uuid} answerBody = {"answer": ocrAnswer,"id": uuid}
with self.client.post(path='/v1/answer', json=answerBody, name="/answer", catch_response=True) as resp: with self.client.post(path='/v2/answer', json=answerBody, name="/answer", catch_response=True) as resp:
if resp.status_code != 200: if resp.status_code != 200:
resp.failure("Status was not 200: " + resp.text) resp.failure("Status was not 200: " + resp.text)
else: else:

View File

@ -24,9 +24,9 @@ class QuickStartUser(SequentialTaskSet):
@task @task
def captcha(self): def captcha(self):
# TODO: Iterate over parameters for a more comprehensive test # TODO: Iterate over parameters for a more comprehensive test
captcha_params = {"level":"easy","media":"image/png","input_type":"text"} captcha_params = {"level":"easy","media":"image/png","input_type":"text", "size":"350x100"}
resp = self.client.post(path="/v1/captcha", json=captcha_params, name="/captcha") resp = self.client.post(path="/v2/captcha", json=captcha_params, name="/captcha")
if resp.status_code != 200: if resp.status_code != 200:
print("\nError on /captcha endpoint: ") print("\nError on /captcha endpoint: ")
print(resp) print(resp)
@ -36,14 +36,14 @@ class QuickStartUser(SequentialTaskSet):
uuid = json.loads(resp.text).get("id") uuid = json.loads(resp.text).get("id")
answerBody = {"answer": "qwer123","id": uuid} answerBody = {"answer": "qwer123","id": uuid}
resp = self.client.get(path="/v1/media?id=%s" % uuid, name="/media") resp = self.client.get(path="/v2/media?id=%s" % uuid, name="/media")
if resp.status_code != 200: if resp.status_code != 200:
print("\nError on /media endpoint: ") print("\nError on /media endpoint: ")
print(resp) print(resp)
print(resp.text) print(resp.text)
print("----------------END.MEDIA-------------------\n\n") print("----------------END.MEDIA-------------------\n\n")
resp = self.client.post(path='/v1/answer', json=answerBody, name="/answer") resp = self.client.post(path='/v2/answer', json=answerBody, name="/answer")
if resp.status_code != 200: if resp.status_code != 200:
print("\nError on /answer endpoint: ") print("\nError on /answer endpoint: ")
print(resp) print(resp)

View File

@ -4,7 +4,7 @@ python3 -m venv testEnv
source ./testEnv/bin/activate source ./testEnv/bin/activate
pip install locust pip install locust
mkdir -p data/ mkdir -p data/
java -jar target/scala-3.1.1/LibreCaptcha.jar & java -jar target/scala-3.6.2/LibreCaptcha.jar &
JAVA_PID=$! JAVA_PID=$!
sleep 4 sleep 4
@ -22,7 +22,7 @@ echo Run functional test
cp data/config.json data/config.json.bak cp data/config.json data/config.json.bak
cp tests/debug-config.json data/config.json cp tests/debug-config.json data/config.json
java -jar target/scala-3.1.1/LibreCaptcha.jar & java -jar target/scala-3.6.2/LibreCaptcha.jar &
JAVA_PID=$! JAVA_PID=$!
sleep 4 sleep 4