Merge pull request #60 from rr83019/Config

Add config file support
This commit is contained in:
hrj 2021-03-11 22:07:51 +05:30 committed by GitHub
commit 055d999e17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 321 additions and 63 deletions

View File

@ -13,10 +13,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
- name: Set up JDK 1.11
uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 1.11
- name: Run tests
run: sbt test
- name: Run linter

View File

@ -21,7 +21,6 @@ scalacOptions ++= List(
"-Ywarn-unused"
)
javacOptions += "-g:none"
scalafmtOnCompile := true
compileOrder := CompileOrder.JavaThenScala
fork in run := true

View File

@ -1,34 +1,37 @@
{
"randomSeed": 20,
"port": 8888,
"captchaExpiryTimeLimit": 5,
"captchas":{
"FilterChallenge":{
"threadDelay": 2,
"throttle": 10,
"captchas":[
{
"name": "FilterChallenge",
"supportedLevels":["medium", "hard"],
"supportedMedia":["image"],
"supportedinputType":["text"],
"allowedLevels":["medium", "hard"],
"allowedMedia":["image/png"],
"allowedInputType":["text"],
"config":{}
},
"GifCaptcha":{
{
"name": "GifCaptcha",
"supportedLevels":["hard"],
"supportedMedia":["gif"],
"supportedinputType":["text"],
"allowedLevels":["hard"],
"allowedMedia":["image/gif"],
"allowedInputType":["text"],
"config":{}
},
"ShadowTextCaptcha":{
{
"name": "ShadowTextCaptcha",
"supportedLevels":["easy"],
"supportedMedia":["image"],
"supportedinputType":["text"],
"allowedLevels":["easy"],
"allowedMedia":["image/png"],
"allowedInputType":["text"],
"config": {}
},
"RainDropsCaptcha":{
{
"name": "RainDropsCaptcha",
"supportedLevels":["easy","medium"],
"supportedMedia":["image"],
"supportedinputType":["text"],
"allowedLevels":["easy","medium"],
"allowedMedia":["image/gif"],
"allowedInputType":["text"],
"config":{}
}
}
]
}

View File

@ -6,6 +6,8 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.util.HashMap;
import java.util.List;
import lc.captchas.interfaces.Challenge;
import lc.captchas.interfaces.ChallengeProvider;
import lc.misc.HelperFunctions;
@ -16,6 +18,19 @@ public class FontFunCaptcha implements ChallengeProvider {
return "FontFunCaptcha";
}
public HashMap<String, List<String>> supportedParameters() {
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
supportedParams.put("supportedLevels", List.of("medium"));
supportedParams.put("supportedMedia", List.of("image/png"));
supportedParams.put("supportedInputType", List.of("text"));
return supportedParams;
}
public void configure(String config) {
// TODO: Add custom config
}
private String getFontName(String path, String level) {
File file = new File(path + level + "/");
FilenameFilter txtFileFilter =

View File

@ -6,6 +6,9 @@ import java.awt.RenderingHints;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.io.ByteArrayOutputStream;
@ -48,6 +51,19 @@ public class GifCaptcha implements ChallengeProvider {
return null;
}
public void configure(String config) {
// TODO: Add custom config
}
public HashMap<String, List<String>> supportedParameters() {
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
supportedParams.put("supportedLevels", List.of("hard"));
supportedParams.put("supportedMedia", List.of("image/gif"));
supportedParams.put("supportedInputType", List.of("text"));
return supportedParams;
}
public Challenge returnChallenge() {
String secret = HelperFunctions.randomString(6);
return new Challenge(gifCaptcha(secret), "image/gif", secret.toLowerCase());

View File

@ -10,6 +10,9 @@ import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.List;
import lc.misc.HelperFunctions;
import lc.captchas.interfaces.Challenge;
import lc.captchas.interfaces.ChallengeProvider;
@ -20,6 +23,19 @@ public class ShadowTextCaptcha implements ChallengeProvider {
return "ShadowTextCaptcha";
}
public void configure(String config) {
// TODO: Add custom config
}
public HashMap<String, List<String>> supportedParameters() {
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
supportedParams.put("supportedLevels", List.of("easy"));
supportedParams.put("supportedMedia", List.of("image/png"));
supportedParams.put("supportedInputType", List.of("text"));
return supportedParams;
}
public boolean checkAnswer(String secret, String answer) {
return answer.toLowerCase().equals(secret);
}

View File

@ -1,5 +1,8 @@
package lc.captchas.interfaces;
import java.util.Map;
import java.util.List;
public interface ChallengeProvider {
public String getId();
@ -7,5 +10,7 @@ public interface ChallengeProvider {
public boolean checkAnswer(String secret, String answer);
// TODO: def configure(): Unit
public void configure(String config);
public Map<String, List<String>> supportedParameters();
}

View File

@ -3,13 +3,18 @@ package lc
import lc.core.{Captcha, CaptchaProviders}
import lc.server.Server
import lc.background.BackgroundTask
import lc.core.Config
object LCFramework {
def main(args: scala.Array[String]): Unit = {
val captcha = new Captcha()
val server = new Server(8888, captcha)
val backgroudTask = new BackgroundTask(captcha, 10)
backgroudTask.beginThread(2)
val server = new Server(port = Config.port, captcha = captcha)
val backgroundTask = new BackgroundTask(
captcha = captcha,
throttle = Config.throttle,
timeLimit = Config.captchaExpiryTimeLimit
)
backgroundTask.beginThread(delay = Config.threadDelay)
server.start()
}
}

View File

@ -5,13 +5,14 @@ import java.util.concurrent.{ScheduledThreadPoolExecutor, TimeUnit}
import lc.core.Captcha
import lc.core.{Parameters, Size}
class BackgroundTask(captcha: Captcha, throttle: Int) {
class BackgroundTask(captcha: Captcha, throttle: Int, timeLimit: Int) {
private val task = new Runnable {
def run(): Unit = {
try {
val mapIdGCPstmt = Statements.tlStmts.get.mapIdGCPstmt
mapIdGCPstmt.setInt(1, timeLimit)
mapIdGCPstmt.executeUpdate()
val challengeGCPstmt = Statements.tlStmts.get.challengeGCPstmt
@ -22,7 +23,7 @@ class BackgroundTask(captcha: Captcha, throttle: Int) {
if (imageNum.next())
throttleIn = (throttleIn - imageNum.getInt("total"))
while (0 < throttleIn) {
captcha.generateChallenge(Parameters("", "", "", Option(Size(0, 0))))
captcha.generateChallenge(Parameters("medium", "image/png", "text", Option(Size(0, 0))))
throttleIn -= 1
}
} catch { case e: Exception => println(e) }

View File

@ -7,9 +7,26 @@ import java.awt.Font
import java.awt.Color
import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import scala.jdk.CollectionConverters.MapHasAsJava
import java.util.{List => JavaList, Map => JavaMap}
class FilterChallenge extends ChallengeProvider {
def getId = "FilterChallenge"
def configure(config: String): Unit = {
// TODO: add custom config
}
def supportedParameters(): JavaMap[String, JavaList[String]] = {
val supportedParams = Map(
"supportedLevels" -> JavaList.of("medium", "hard"),
"supportedMedia" -> JavaList.of("image/png"),
"supportedInputType" -> JavaList.of("text")
).asJava
supportedParams
}
def returnChallenge(): Challenge = {
val filterTypes = List(new FilterType1, new FilterType2)
val r = new scala.util.Random

View File

@ -9,6 +9,8 @@ import java.awt.image.BufferedImage
import java.awt.Color
import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import scala.jdk.CollectionConverters.MapHasAsJava
import java.util.{List => JavaList, Map => JavaMap}
class LabelCaptcha extends ChallengeProvider {
private var knownFiles = new File("known").list.toList
@ -23,6 +25,20 @@ class LabelCaptcha extends ChallengeProvider {
def getId = "LabelCaptcha"
def configure(config: String): Unit = {
// TODO: add custom config
}
def supportedParameters(): JavaMap[String, JavaList[String]] = {
val supportedParams = Map(
"supportedLevels" -> JavaList.of("hard"),
"supportedMedia" -> JavaList.of("image/png"),
"supportedInputType" -> JavaList.of("text")
).asJava
supportedParams
}
def returnChallenge(): Challenge =
synchronized {
val r = scala.util.Random.nextInt(knownFiles.length)

View File

@ -10,6 +10,8 @@ import javax.imageio.stream.MemoryCacheImageOutputStream;
import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import lc.misc.GifSequenceWriter
import scala.jdk.CollectionConverters.MapHasAsJava
import java.util.{List => JavaList, Map => JavaMap}
class Drop {
var x = 0
@ -31,6 +33,20 @@ class RainDropsCP extends ChallengeProvider {
def getId = "FilterChallenge"
def configure(config: String): Unit = {
// TODO: add custom config
}
def supportedParameters(): JavaMap[String, JavaList[String]] = {
val supportedParams = Map(
"supportedLevels" -> JavaList.of("medium", "easy"),
"supportedMedia" -> JavaList.of("image/gif"),
"supportedInputType" -> JavaList.of("text")
).asJava
supportedParams
}
private def extendDrops(drops: Array[Drop], steps: Int, xOffset: Int) = {
drops.map(d => {
val nd = new Drop()

View File

@ -5,6 +5,7 @@ import java.util.UUID
import java.io.ByteArrayInputStream
import lc.database.Statements
import lc.core.CaptchaProviders
import lc.captchas.interfaces.ChallengeProvider
class Captcha {
@ -30,8 +31,8 @@ class Captcha {
}
def generateChallenge(param: Parameters): Int = {
//TODO: eval params to choose a provider
val provider = CaptchaProviders.getProvider()
val provider = CaptchaProviders.getProvider(param)
if (!provider.isInstanceOf[ChallengeProvider]) return -1
val providerId = provider.getId()
val challenge = provider.returnChallenge()
val blob = new ByteArrayInputStream(challenge.content)
@ -40,7 +41,9 @@ class Captcha {
insertPstmt.setString(2, challenge.secret)
insertPstmt.setString(3, providerId)
insertPstmt.setString(4, challenge.contentType)
insertPstmt.setBlob(5, blob)
insertPstmt.setString(5, param.level)
insertPstmt.setString(6, param.input_type)
insertPstmt.setBlob(7, blob)
insertPstmt.executeUpdate()
val rs: ResultSet = insertPstmt.getGeneratedKeys()
val token = if (rs.next()) {
@ -50,24 +53,53 @@ class Captcha {
token.asInstanceOf[Int]
}
def getChallenge(param: Parameters): Id = {
val allowedInputType = Config.allowedInputType
val allowedLevels = Config.allowedLevels
val allowedMedia = Config.allowedMedia
private def validateParam(param: Parameters): Boolean = {
if (
allowedLevels.contains(param.level) &&
allowedMedia.contains(param.media) &&
allowedInputType.contains(param.input_type)
)
return true
else
return false
}
def getChallenge(param: Parameters): ChallengeResult = {
try {
val tokenPstmt = Statements.tlStmts.get.tokenPstmt
val rs = tokenPstmt.executeQuery()
val tokenOpt = if (rs.next()) {
Some(rs.getInt("token"))
val validParam = validateParam(param)
if (validParam) {
val tokenPstmt = Statements.tlStmts.get.tokenPstmt
tokenPstmt.setString(1, param.level)
tokenPstmt.setString(2, param.media)
tokenPstmt.setString(3, param.input_type)
val rs = tokenPstmt.executeQuery()
val tokenOpt = if (rs.next()) {
Some(rs.getInt("token"))
} else {
None
}
val updateAttemptedPstmt = Statements.tlStmts.get.updateAttemptedPstmt
val token = tokenOpt.getOrElse(generateChallenge(param))
val result = if (token != -1) {
val uuid = getUUID(token)
updateAttemptedPstmt.setString(1, uuid)
updateAttemptedPstmt.executeUpdate()
Id(uuid)
} else {
Error(ErrorMessageEnum.NO_CAPTCHA.toString)
}
result
} else {
None
Error(ErrorMessageEnum.INVALID_PARAM.toString)
}
val updateAttemptedPstmt = Statements.tlStmts.get.updateAttemptedPstmt
val uuid = getUUID(tokenOpt.getOrElse(generateChallenge(param)))
updateAttemptedPstmt.setString(1, uuid)
updateAttemptedPstmt.executeUpdate()
Id(uuid)
} catch {
case e: Exception =>
println(e)
Id(getUUID(-1))
Error(ErrorMessageEnum.SMW.toString)
}
}
@ -82,16 +114,17 @@ class Captcha {
def checkAnswer(answer: Answer): Result = {
val selectPstmt = Statements.tlStmts.get.selectPstmt
selectPstmt.setString(1, answer.id)
selectPstmt.setInt(1, Config.captchaExpiryTimeLimit)
selectPstmt.setString(2, answer.id)
val rs: ResultSet = selectPstmt.executeQuery()
val psOpt = if (rs.first()) {
val secret = rs.getString("secret")
val provider = rs.getString("provider")
val check = CaptchaProviders.getProviderById(provider).checkAnswer(secret, answer.answer)
val result = if (check) "TRUE" else "FALSE"
val result = if (check) ResultEnum.TRUE.toString else ResultEnum.FALSE.toString
result
} else {
"EXPIRED"
ResultEnum.EXPIRED.toString
}
val deleteAnswerPstmt = Statements.tlStmts.get.deleteAnswerPstmt
deleteAnswerPstmt.setString(1, answer.id)

View File

@ -0,0 +1,29 @@
package lc.core
object ParametersEnum extends Enumeration {
type Parameter = Value
val SUPPORTEDLEVEL: Value = Value("supportedLevels")
val SUPPORTEDMEDIA: Value = Value("supportedMedia")
val SUPPORTEDINPUTTYPE: Value = Value("supportedInputType")
val ALLOWEDLEVELS: Value = Value("allowedLevels")
val ALLOWEDMEDIA: Value = Value("allowedMedia")
val ALLOWEDINPUTTYPE: Value = Value("allowedInputType")
}
object ResultEnum extends Enumeration {
type Result = Value
val TRUE: Value = Value("True")
val FALSE: Value = Value("False")
val EXPIRED: Value = Value("Expired")
}
object ErrorMessageEnum extends Enumeration {
type ErrorMessage = Value
val SMW: Value = Value("Oops, something went worng!")
val INVALID_PARAM: Value = Value("Invalid Pramaters")
val NO_CAPTCHA: Value = Value("No captcha for the provided parameters")
}

View File

@ -3,6 +3,7 @@ package lc.core
import lc.captchas._
import lc.captchas.interfaces.ChallengeProvider
import lc.captchas.interfaces.Challenge
import scala.collection.mutable.Map
object CaptchaProviders {
private val providers = Map(
@ -21,10 +22,11 @@ object CaptchaProviders {
}
}
private val seed = System.currentTimeMillis.toString.substring(2, 6).toInt
private val seed = Config.seed
private val random = new scala.util.Random(seed)
private val config = Config.captchaConfig
private def getNextRandomInt(max: Int) =
private def getNextRandomInt(max: Int): Int =
random.synchronized {
random.nextInt(max)
}
@ -33,9 +35,31 @@ object CaptchaProviders {
return providers(id)
}
def getProvider(): ChallengeProvider = {
val keys = providers.keys
val providerIndex = keys.toVector(getNextRandomInt(keys.size))
providers(providerIndex)
private def filterProviderByParam(param: Parameters): Iterable[(String, String)] = {
val configFilter = for {
configValue <- config
if configValue.allowedLevels.contains(param.level)
if configValue.allowedMedia.contains(param.media)
if configValue.allowedInputType.contains(param.input_type)
} yield (configValue.name, configValue.config)
val providerFilter = for {
providerValue <- configFilter
providerConfigMap = providers(providerValue._1).supportedParameters()
if providerConfigMap.get(ParametersEnum.SUPPORTEDLEVEL.toString).contains(param.level)
if providerConfigMap.get(ParametersEnum.SUPPORTEDMEDIA.toString).contains(param.media)
if providerConfigMap.get(ParametersEnum.SUPPORTEDINPUTTYPE.toString).contains(param.input_type)
} yield (providerValue._1, providerValue._2)
providerFilter
}
def getProvider(param: Parameters): ChallengeProvider = {
val providerConfig = filterProviderByParam(param).toList
val randomIndex = getNextRandomInt(providerConfig.length)
val providerIndex = providerConfig(randomIndex)._1
val selectedProvider = providers(providerIndex)
selectedProvider.configure(providerConfig(randomIndex)._2)
selectedProvider
}
}

View File

@ -0,0 +1,48 @@
package lc.core
import scala.io.Source.fromFile
import org.json4s.{DefaultFormats, JValue, JObject, JField, JString}
import org.json4s.jackson.JsonMethods.parse
object Config {
implicit val formats: DefaultFormats.type = DefaultFormats
private val configFile = fromFile("config.json")
private val configString =
try configFile.mkString
finally configFile.close
private val configJson = parse(configString)
val port: Int = (configJson \ "port").extract[Int]
val throttle: Int = (configJson \ "throttle").extract[Int]
val seed: Int = (configJson \ "randomSeed").extract[Int]
val captchaExpiryTimeLimit: Int = (configJson \ "captchaExpiryTimeLimit").extract[Int]
val threadDelay: Int = (configJson \ "threadDelay").extract[Int]
private val captchaConfigJson = (configJson \ "captchas")
val captchaConfigTransform: JValue = captchaConfigJson transformField {
case JField("config", JObject(config)) => ("config", JString(config.toString))
}
val captchaConfig: List[CaptchaConfig] = captchaConfigTransform.extract[List[CaptchaConfig]]
val allowedLevels: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDLEVELS.toString)
val allowedMedia: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDMEDIA.toString)
val allowedInputType: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDINPUTTYPE.toString)
private def getAllValues(config: JValue, param: String): Set[String] = {
val configValues = (config \\ param)
val result = for {
JObject(child) <- configValues
JField(param) <- child
} yield (param)
var valueSet = Set[String]()
for (valueList <- result) {
for (value <- valueList._2.children) {
valueSet += value.values.toString
}
}
valueSet
}
}

View File

@ -1,7 +1,16 @@
package lc.core
sealed trait ChallengeResult
case class Size(height: Int, width: Int)
case class Parameters(level: String, media: String, input_type: String, size: Option[Size])
case class Id(id: String)
case class Id(id: String) extends ChallengeResult
case class Error(message: String) extends ChallengeResult
case class Answer(answer: String, id: String)
case class Result(result: String)
case class CaptchaConfig(
name: String,
allowedLevels: List[String],
allowedMedia: List[String],
allowedInputType: List[String],
config: String
)

View File

@ -15,6 +15,8 @@ class Statements(dbConn: DBConn) {
"secret varchar, " +
"provider varchar, " +
"contentType varchar, " +
"contentLevel varchar, " +
"contentInput varchar, " +
"image blob, " +
"attempted int default 0, " +
"PRIMARY KEY(token))"
@ -32,8 +34,8 @@ class Statements(dbConn: DBConn) {
val insertPstmt: PreparedStatement = dbConn.con.prepareStatement(
"INSERT INTO " +
"challenge(id, secret, provider, contentType, image) " +
"VALUES (?, ?, ?, ?, ?)",
"challenge(id, secret, provider, contentType, contentLevel, contentInput, image) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS
)
@ -48,7 +50,7 @@ class Statements(dbConn: DBConn) {
"SELECT c.secret, c.provider " +
"FROM challenge c, mapId m " +
"WHERE m.token=c.token AND " +
"DATEDIFF(MINUTE, CURRENT_TIMESTAMP, DATEADD(MINUTE, 1, m.lastServed)) > 0 AND " +
"DATEDIFF(MINUTE, CURRENT_TIMESTAMP, DATEADD(MINUTE, ?, m.lastServed)) > 0 AND " +
"m.uuid = ?"
)
@ -71,7 +73,10 @@ class Statements(dbConn: DBConn) {
val tokenPstmt: PreparedStatement = dbConn.con.prepareStatement(
"SELECT token " +
"FROM challenge " +
"WHERE attempted < 10 " +
"WHERE attempted < 10 AND " +
"contentLevel = ? AND " +
"contentType = ? AND " +
"contentInput = ? " +
"ORDER BY RAND() LIMIT 1"
)
@ -86,7 +91,7 @@ class Statements(dbConn: DBConn) {
)
val mapIdGCPstmt: PreparedStatement = dbConn.con.prepareStatement(
"DELETE FROM mapId WHERE DATEDIFF(MINUTE, CURRENT_TIMESTAMP, DATEADD(MINUTE, 1, lastServed)) < 0"
"DELETE FROM mapId WHERE DATEDIFF(MINUTE, CURRENT_TIMESTAMP, DATEADD(MINUTE, ?, lastServed)) < 0"
)
val getCountChallengeTable: PreparedStatement = dbConn.con.prepareStatement(

View File

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