Skip to content

Commit

Permalink
Create record and playback request feature (#34)
Browse files Browse the repository at this point in the history
Create record and playback request feature #33
  • Loading branch information
laviua authored Dec 5, 2018
1 parent b21ac61 commit 2cddcc0
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 10 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=1.9.0
version=1.10.0
18 changes: 18 additions & 0 deletions komock-core/mock_example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ consulAgents:
checkInterval: 30s
checkTimeout: 30s

proxies:
-
enabled: true
httpServer:
name: auth
port: 9090
host: 127.0.0.1
forwardTo: "https://auth.testlab.com:9090"
writeTo: "/tmp/auth.yml"
-
enabled: true
httpServer:
name: esb
port: 9091
host: 127.0.0.1
forwardTo: "https://esb.testlab.com:9091"
writeTo: "/tmp/proxy.yml"

springConfig:
enabled: true
refreshPeriod: 10000
Expand Down
11 changes: 8 additions & 3 deletions komock-core/src/main/kotlin/ua/com/lavi/komock/KomockRunner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ua.com.lavi.komock
import ua.com.lavi.komock.model.config.KomockConfiguration
import ua.com.lavi.komock.registrar.consul.ConsulRegistrar
import ua.com.lavi.komock.registrar.http.HttpServerRegistrar
import ua.com.lavi.komock.registrar.proxy.ProxyForwarder
import ua.com.lavi.komock.registrar.spring.SpringConfigRegistrar

/**
Expand All @@ -26,12 +27,16 @@ class KomockRunner {
springConfigRegistrar.register(springConfigProperties)
}

//Proxy interceptor server
val proxyForwarder = ProxyForwarder()
komockConfiguration.proxies
.filter { it.enabled }
.forEach { proxyForwarder.register(it) }

//Consul registration
val consulRegistrar = ConsulRegistrar()
komockConfiguration.consulAgents
.filter { it.enabled }
.forEach {
consulRegistrar.register(it)
}
.forEach { consulRegistrar.register(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@ class RoutingTable {
if (find(httpMethod, url) != null) {
throw RuntimeException("Route with httpMethod: ${httpMethod.name} and requestedUrl: $url is already exists in the routing table")
}
urlMap.put(httpMethod, Route(url, responseHandler, beforeResponseHandler, afterResponseHandler, callbackHandler))
routeMap.put(url, urlMap)
urlMap[httpMethod] = Route(url, responseHandler, beforeResponseHandler, afterResponseHandler, callbackHandler)
routeMap[url] = urlMap
}

fun find(httpMethod: HttpMethod, requestedUrl: String): Route? {
val httpMethodsMap = routeMap[requestedUrl]
if (httpMethodsMap == null) {
for (routeKey in routeMap.keys) {
if (routeKey == "/**") {
return routeMap[routeKey]?.get(httpMethod)
}
val pattern = routeKey.replace("*", REGEX_URL_WILDCARD)
if (Regex(pattern).matchEntire(requestedUrl) != null) {
return routeMap[routeKey]!![httpMethod]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import javax.servlet.http.HttpServletRequest

class Request(private val httpServletRequest: HttpServletRequest) {

fun getMethod() : String {
return httpServletRequest.method
}

fun getRequestBody(): String {
return httpServletRequest.inputStream.bufferedReader().use { it.readText() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class Response(private val servletResponse: HttpServletResponse) {
servletResponse.status = code
}

fun getCode() : Int {
return servletResponse.status
}

fun setContentType(contentType: String?) {
servletResponse.contentType = contentType
}
Expand All @@ -46,7 +50,7 @@ class Response(private val servletResponse: HttpServletResponse) {
fun getHeaders(): MutableMap<String, String> {
val headers: MutableMap<String, String> = HashMap()
for (headerName in servletResponse.headerNames) {
headers.put(headerName, servletResponse.getHeader(headerName))
headers[headerName] = servletResponse.getHeader(headerName)
}
return headers
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package ua.com.lavi.komock.model.config

import ua.com.lavi.komock.model.config.consul.ConsulAgentProperties
import ua.com.lavi.komock.model.config.http.HttpServerProperties
import ua.com.lavi.komock.model.config.proxy.ProxyConfigProperties
import ua.com.lavi.komock.model.config.spring.SpringConfigProperties
import java.util.*

/**
* Created by Oleksandr Loushkin
Expand All @@ -14,5 +14,6 @@ open class KomockConfiguration {
var httpServers: List<HttpServerProperties> = ArrayList()
var consulAgents: List<ConsulAgentProperties> = ArrayList()
var springConfig: SpringConfigProperties = SpringConfigProperties()
var proxies: List<ProxyConfigProperties> = ArrayList()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ua.com.lavi.komock.model.config.proxy

import ua.com.lavi.komock.model.config.http.HttpServerProperties

open class ProxyConfigProperties {
var enabled: Boolean = false
var httpServer: HttpServerProperties = HttpServerProperties()
var forwardTo: String = ""
var writeTo: String = ""
var connectTimeout: Int = 250000
var connectionRequestTimeout: Int = 30000
var socketTimeout: Int = 25000
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package ua.com.lavi.komock.registrar.proxy

import org.apache.http.client.config.RequestConfig
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpUriRequest
import org.apache.http.conn.ssl.NoopHostnameVerifier
import org.apache.http.entity.InputStreamEntity
import org.apache.http.impl.client.HttpClients
import org.apache.http.ssl.SSLContextBuilder
import org.slf4j.LoggerFactory
import ua.com.lavi.komock.http.handler.response.ResponseHandler
import ua.com.lavi.komock.http.server.SecuredMockServer
import ua.com.lavi.komock.http.server.UnsecuredMockServer
import ua.com.lavi.komock.model.HttpMethod
import ua.com.lavi.komock.model.Request
import ua.com.lavi.komock.model.Response
import ua.com.lavi.komock.model.config.http.CallbackRequest
import ua.com.lavi.komock.model.config.http.HttpServerProperties
import ua.com.lavi.komock.model.config.http.RouteProperties
import ua.com.lavi.komock.model.config.proxy.ProxyConfigProperties
import ua.com.lavi.komock.registrar.Registrar
import java.io.File
import java.net.BindException


/**
* Created by Oleksandr Loushkin
* Register proxy interceptor configuration files as a separate http server with default forward rule
*/

class ProxyForwarder : Registrar<ProxyConfigProperties> {

private val log = LoggerFactory.getLogger(this.javaClass)

private val yamlRouteWriter = YamlRouteWriter()

private val ignoredHeaders = arrayListOf("Content-Length", "Host", "Connection", "Date")

override fun register(properties: ProxyConfigProperties) {
val httpServerProp: HttpServerProperties = properties.httpServer

val router = if (httpServerProp.ssl.enabled) {
SecuredMockServer(httpServerProp)
} else {
UnsecuredMockServer(httpServerProp)
}

try {
router.start()
} catch (e: BindException) {
log.warn(e.message + ": ${httpServerProp.host}, port: ${httpServerProp.port}", e)
return
}

val httpClient = HttpClients.custom()
.setSSLContext(SSLContextBuilder().loadTrustMaterial(null) { _, _ -> true }.build())
.setSSLHostnameVerifier(NoopHostnameVerifier())
.build()

val hitTable = hashMapOf<String, MutableMap<String, RouteProperties>>()

val responseHandler = object : ResponseHandler {
override fun handle(request: Request, response: Response) {
val httpResponse: CloseableHttpResponse = httpClient.execute(buildHttpRequest(request, properties))

//fill the response model
val content = httpResponse.entity.content.bufferedReader().use { it.readText() }
response.setCode(httpResponse.statusLine.statusCode)
response.setContent(content)
for (header in httpResponse.allHeaders) {
response.addHeader(header.name, header.value)
}
log.debug("Received: $httpResponse \r\n $content")

// dump the routes in the komock format
if (properties.writeTo.isNotEmpty()) {

val routeProperties = buildRouteProperties(request, response)

var methodRoutes: MutableMap<String, RouteProperties>? = hitTable[request.getMethod()]
if (methodRoutes == null) {
methodRoutes = hashMapOf()
hitTable[request.getMethod()] = methodRoutes
}
val requestURI = request.getHttpServletRequest().requestURI
methodRoutes[requestURI] = routeProperties

yamlRouteWriter.write(hitTable, File(properties.writeTo))
}

}
}

for (httpMethod in HttpMethod.values()) {
router.addRoute("/**", httpMethod, responseHandler)
}
}

private fun buildRouteProperties(request: Request, response: Response): RouteProperties {
val routeProperties = RouteProperties()
routeProperties.url = request.getHttpServletRequest().requestURI
routeProperties.httpMethod = request.getMethod()
routeProperties.responseBody = "'${response.getContent().replace("\n", "")}'"
routeProperties.code = response.getCode()
routeProperties.responseHeaders = response.getHeaders().filterNot { ignoreHeader(it.key) }
return routeProperties
}

private fun buildHttpRequest(request: Request, properties: ProxyConfigProperties): HttpUriRequest {
val baseUri = "${properties.forwardTo}${request.getHttpServletRequest().requestURI}"
val queryUri = buildQueryUrl(request)
val requestUri = "$baseUri$queryUri"
log.debug("Request uri: ${request.getMethod()}: $requestUri")
val anyRequest = CallbackRequest(request.getMethod(), requestUri)

request.getHeaders()
.filterNot { ignoreHeader(it.key) }
.forEach { header -> anyRequest.addHeader(header.key, header.value) }

// add body to the request. it needs for the POST callback
if (request.getRequestBody().isNotBlank()) {
anyRequest.entity = InputStreamEntity(request.getHttpServletRequest().inputStream)
}
anyRequest.config = RequestConfig.custom()
.setConnectTimeout(properties.connectTimeout)
.setConnectionRequestTimeout(properties.connectionRequestTimeout)
.setSocketTimeout(properties.socketTimeout)
.build()
return anyRequest
}

private fun buildQueryUrl(request: Request): String {
return request.getQueryParametersMap().entries.stream()
.map { p -> p.key + "=" + p.value }
.reduce { p1, p2 -> "$p1&$p2" }
.map { s -> "?$s" }
.orElse("")
}

private fun ignoreHeader(headerName: String): Boolean {
if (ignoredHeaders.contains(headerName)) {
return true
}
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ua.com.lavi.komock.registrar.proxy

import com.google.gson.Gson
import ua.com.lavi.komock.model.config.http.RouteProperties
import java.io.File

class YamlRouteWriter {

private val gson = Gson()

fun write(routeMap: Map<String, MutableMap<String, RouteProperties>>, targetFile: File) {

//check that possible to write
targetFile.writeText("")

val routes: List<RouteProperties> = routeMap.flatMap { it.value.values }

for (route in routes) {

val url = "url: ${route.url}"
val headers = "responseHeaders: ${gson.toJson(route.responseHeaders)}"
val httpMethod = "httpMethod: ${route.httpMethod}"
val httpCode = "code: ${route.code}"
val responseBody = "responseBody: ${route.responseBody.replace("\n", "")}"

val template = "-\r\n" +
" $httpMethod \r\n" +
" $url \r\n" +
" $headers \r\n" +
" $httpCode \r\n" +
" $responseBody \r\n"

targetFile.appendText(template)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ import ua.com.lavi.komock.http.handler.after.EmptyAfterResponseHandlerImpl
import ua.com.lavi.komock.http.handler.before.EmptyBeforeResponseHandlerImpl
import ua.com.lavi.komock.http.handler.callback.EmptyCallbackHandlerImpl
import ua.com.lavi.komock.http.handler.response.EmptyResponseHandler
import ua.com.lavi.komock.http.server.handler.RoutingTable
import ua.com.lavi.komock.model.HttpMethod
import ua.com.lavi.komock.model.config.KomockConfiguration
import ua.com.lavi.komock.http.server.handler.RoutingTable
import ua.com.lavi.komock.registrar.http.HttpServerRegistrar
import java.net.Socket
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.test.assertEquals
Expand Down

0 comments on commit 2cddcc0

Please sign in to comment.