Коммит a8132bde создал по автору rsinukov's avatar rsinukov Зафиксировано автором Rustam
Просмотр файлов

KTOR-503 Add request validation plugin

владелец c7823f18
public final class io/ktor/server/plugins/requestvalidation/RequestValidationConfig {
public fun <init> ()V
public final fun validate (Lio/ktor/server/plugins/requestvalidation/Validator;)V
public final fun validate (Lkotlin/jvm/functions/Function1;)V
public final fun validate (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;)V
}
public final class io/ktor/server/plugins/requestvalidation/RequestValidationConfig$ValidatorBuilder {
public fun <init> ()V
public final fun filter (Lkotlin/jvm/functions/Function1;)V
public final fun validation (Lkotlin/jvm/functions/Function2;)V
}
public final class io/ktor/server/plugins/requestvalidation/RequestValidationException : java/lang/IllegalArgumentException {
public fun <init> (Ljava/lang/Object;Ljava/util/List;)V
public final fun getReasons ()Ljava/util/List;
public final fun getValue ()Ljava/lang/Object;
}
public final class io/ktor/server/plugins/requestvalidation/RequestValidationKt {
public static final fun getRequestValidation ()Lio/ktor/server/application/RouteScopedPlugin;
}
public abstract class io/ktor/server/plugins/requestvalidation/ValidationResult {
}
public final class io/ktor/server/plugins/requestvalidation/ValidationResult$Invalid : io/ktor/server/plugins/requestvalidation/ValidationResult {
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/util/List;)V
public final fun getReasons ()Ljava/util/List;
}
public final class io/ktor/server/plugins/requestvalidation/ValidationResult$Valid : io/ktor/server/plugins/requestvalidation/ValidationResult {
public static final field INSTANCE Lio/ktor/server/plugins/requestvalidation/ValidationResult$Valid;
}
public abstract interface class io/ktor/server/plugins/requestvalidation/Validator {
public abstract fun filter (Ljava/lang/Object;)Z
public abstract fun validate (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
/*
* Copyright 2014-2020 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
description = ""
kotlin {
sourceSets {
jvmAndNixTest {
dependencies {
implementation(project(":ktor-server:ktor-server-plugins:ktor-server-status-pages"))
}
}
}
}
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.server.plugins.requestvalidation
import io.ktor.server.application.*
import io.ktor.server.request.*
/**
* A result of validation.
*/
public sealed class ValidationResult {
/**
* Represents successful result of validation
*/
public object Valid : ValidationResult()
/**
* Represents not successful result of validation
*/
public class Invalid(public val reasons: List<String>) : ValidationResult() {
public constructor (reason: String) : this(listOf(reason))
}
}
/**
* A validator that should be registered with [RequestValidation] plugin
*/
public interface Validator {
/**
* Validates [value]
*/
public suspend fun validate(value: Any): ValidationResult
/**
* Check if [value] should be checked by this validator
*/
public fun filter(value: Any): Boolean
}
/**
* A plugin that checks request body using [Validator]
* Example:
* ```
* install(RequestValidation) {
* validate<String> {
* if (!it.startsWith("+")) ValidationResult.Invalid("$it should start with \"+\"")
* else ValidationResult.Valid
* }
* }
* install(StatusPages) {
* exception<RequestValidationException> { call, cause ->
* call.respond(HttpStatusCode.BadRequest, cause.reasons.joinToString())
* }
* }
* ```
*/
public val RequestValidation: RouteScopedPlugin<RequestValidationConfig> = createRouteScopedPlugin(
"RequestValidation",
::RequestValidationConfig
) {
val validators = pluginConfig.validators
on(RequestBodyTransformed) { content ->
@Suppress("UNCHECKED_CAST")
val failures = validators.filter { it.filter(content) }
.map { it.validate(content) }
.filterIsInstance<ValidationResult.Invalid>()
if (failures.isNotEmpty()) {
throw RequestValidationException(content, failures.flatMap { it.reasons })
}
}
}
/**
* Thrown when validation fails.
* @property value - invalid request body
* @property reasons - combined reasons of all validation failures for this request
*/
public class RequestValidationException(public val value: Any, public val reasons: List<String>) :
IllegalArgumentException(
"Validation failed for $value. Reasons: ${reasons.joinToString(".")}"
)
private object RequestBodyTransformed : Hook<suspend (content: Any) -> Unit> {
override fun install(
pipeline: ApplicationCallPipeline,
handler: suspend (content: Any) -> Unit
) {
pipeline.receivePipeline.intercept(ApplicationReceivePipeline.After) {
handler(subject)
}
}
}
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.server.plugins.requestvalidation
import kotlin.reflect.*
/**
* A config for [RequestValidation] plugin
*/
public class RequestValidationConfig {
internal val validators: MutableList<Validator> = mutableListOf()
/**
* Registers [validator]
*/
public fun validate(validator: Validator) {
validators.add(validator)
}
/**
* Registers [Validator] that should check instances of a [kClass] using [block]
*/
public fun <T : Any> validate(kClass: KClass<T>, block: suspend (T) -> ValidationResult) {
val validator = object : Validator {
@Suppress("UNCHECKED_CAST")
override suspend fun validate(value: Any): ValidationResult = block(value as T)
override fun filter(value: Any): Boolean = kClass.isInstance(value)
}
validate(validator)
}
/**
* Registers [Validator] that should check instances of a [T] using [block]
*/
public inline fun <reified T : Any> validate(noinline block: suspend (T) -> ValidationResult) {
validate(T::class, block)
}
/**
* Registers [Validator] using DSL
* ```
* validate {
* filter { it is Int }
* validation { check(it is Int); ... }
* }
* ```
*/
public fun validate(block: ValidatorBuilder.() -> Unit) {
val builder = ValidatorBuilder().apply(block)
validate(builder.build())
}
public class ValidatorBuilder {
private lateinit var validationBlock: suspend (Any) -> ValidationResult
private lateinit var filterBlock: (Any) -> Boolean
public fun filter(block: (Any) -> Boolean) {
filterBlock = block
}
public fun validation(block: suspend (Any) -> ValidationResult) {
validationBlock = block
}
internal fun build(): Validator {
check(::validationBlock.isInitialized) { "`validation { ... } block is not ser`" }
check(::filterBlock.isInitialized) { "`filter { ... } block is not set`" }
return object : Validator {
override suspend fun validate(value: Any) = validationBlock(value)
override fun filter(value: Any): Boolean = filterBlock(value)
}
}
}
}
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.server.plugins.requestvalidation
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import io.ktor.utils.io.*
import io.ktor.utils.io.core.*
import kotlin.test.*
class RequestValidationTest {
@Test
fun testSimpleValidationByClass() = testApplication {
install(RequestValidation) {
validate<CharSequence> {
if (!it.startsWith("+")) ValidationResult.Invalid(listOf("$it should start with \"+\""))
else ValidationResult.Valid
}
validate<String> {
if (!it.endsWith("!")) ValidationResult.Invalid(listOf("$it should end with \"!\""))
else ValidationResult.Valid
}
}
install(StatusPages) {
exception<RequestValidationException> { call, cause ->
call.respond(HttpStatusCode.BadRequest, "${cause.value}\n${cause.reasons.joinToString()}")
}
}
routing {
get("/text") {
val body = call.receive<String>()
call.respond(body)
}
get("/channel") {
call.receive<ByteReadChannel>().discard()
call.respond("OK")
}
}
client.get("/text") {
setBody("1")
}.let {
val body = it.bodyAsText()
assertEquals(HttpStatusCode.BadRequest, it.status)
assertEquals("1\n1 should start with \"+\", 1 should end with \"!\"", body)
}
client.get("/text") {
setBody("+1")
}.let {
val body = it.bodyAsText()
assertEquals(HttpStatusCode.BadRequest, it.status)
assertEquals("+1\n+1 should end with \"!\"", body)
}
client.get("/text") {
setBody("1!")
}.let {
val body = it.bodyAsText()
assertEquals(HttpStatusCode.BadRequest, it.status)
assertEquals("1!\n1! should start with \"+\"", body)
}
client.get("/text") {
setBody("+1!")
}.let {
val body = it.bodyAsText()
assertEquals(HttpStatusCode.OK, it.status)
assertEquals("+1!", body)
}
client.get("/channel") {
setBody("1")
}.let {
assertEquals(HttpStatusCode.OK, it.status)
}
}
@Test
fun testValidatorDsl() = testApplication {
install(RequestValidation) {
validate {
filter { it is ByteArray }
validation {
check(it is ByteArray)
val intValue = String(it).toInt()
if (intValue < 0) ValidationResult.Invalid("Value is negative")
else ValidationResult.Valid
}
}
}
install(StatusPages) {
exception<RequestValidationException> { call, cause ->
call.respond(HttpStatusCode.BadRequest, "${cause.value}\n${cause.reasons.joinToString()}")
}
}
routing {
get("/text") {
val body = call.receive<String>()
call.respond(body)
}
get("/array") {
val body = call.receive<ByteArray>()
call.respond(String(body))
}
}
client.get("/text") {
setBody("1")
}.let {
val body = it.bodyAsText()
assertEquals(HttpStatusCode.OK, it.status)
assertEquals("1", body)
}
client.get("/array") {
setBody("1")
}.let {
val body = it.bodyAsText()
assertEquals(HttpStatusCode.OK, it.status)
assertEquals("1", body)
}
client.get("/array") {
setBody("-1")
}.let {
val body = it.bodyAsText()
assertEquals(HttpStatusCode.BadRequest, it.status)
assertTrue(body.endsWith("Value is negative"), body)
}
}
}
......@@ -115,6 +115,7 @@ include(":ktor-server:ktor-server-plugins:ktor-server-metrics-micrometer")
include(":ktor-server:ktor-server-plugins:ktor-server-mustache")
include(":ktor-server:ktor-server-plugins:ktor-server-partial-content")
include(":ktor-server:ktor-server-plugins:ktor-server-pebble")
include(":ktor-server:ktor-server-plugins:ktor-server-request-validation")
include(":ktor-server:ktor-server-plugins:ktor-server-resources")
include(":ktor-server:ktor-server-plugins:ktor-server-sessions")
include(":ktor-server:ktor-server-plugins:ktor-server-status-pages")
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать