Skip to content

Commit

Permalink
Add logging settings to soap methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Forbiddensequence committed Oct 13, 2024
1 parent eca4e63 commit 34f136e
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 0 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ public class MyServiceImpl implements MyService {

* Configure max log size of you soap-client `play.soap.services.<SERVICE_CLASS>.log-size` (Default value 48 kB). When you use log-size more than 128 kbytes CXF creates temporary files that contains messages, and CXF deletes files by itself.

* Configure logging shadowing properties with `play.soap.services.<SERVICE_CLASS>.fields-shadowing`. This settings are used when `play.soap.services.<SERVICE_CLASS>.debugLog=true` (Logging interceptors are enabled). Custom LoggingInterceptors embed necessary string in a soap request and response when it finds a message with a desired soapAction message.

```HOCON
lagom.circuit-breaker {
default.exception-whitelist = [
Expand All @@ -157,6 +159,19 @@ play.soap.services {
browser-type = "Lagom MyService"
singleton: false
log-size : 1MB
fields-shadowing : [
{
soap-method = "\"soapAction\""
replacing-symbols = "********"
request-patterns = [
"//books/title",
]
response-patterns = [
"//books/book/date",
"//books/book/author/name"
]
}
]
breaker = {
call-timeout = 10s
}
Expand Down
17 changes: 17 additions & 0 deletions java/src/main/kotlin/org/taymyr/lagom/soap/ServiceProviderImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import javassist.util.proxy.ProxyFactory
import mu.KotlinLogging
import org.apache.cxf.interceptor.AbstractLoggingInterceptor
import org.apache.cxf.transport.http.HTTPConduit
import org.taymyr.lagom.soap.interceptor.PostShadowingLoggingInInterceptor
import org.taymyr.lagom.soap.interceptor.PreShadowingLoggingInInterceptor
import org.taymyr.lagom.soap.interceptor.ShadowingLoggingOutInterceptor
import org.taymyr.lagom.soap.interceptor.ShadowingSettings
import play.soap.PlayJaxWsClientProxy
import play.soap.PlaySoapClient
import java.lang.String.format
Expand Down Expand Up @@ -88,6 +92,7 @@ constructor(
private val port: P
private val isSingleton: Boolean
private val logSize: Int?
private val fieldsShadowing: List<ShadowingSettings>

init {
val globalConfig = configProvider.get()
Expand All @@ -101,6 +106,9 @@ constructor(
this.invokeHandlers = this@ServiceProviderImpl.invokeHandlers.plus(invokeMethodHandlers)
this.isSingleton = if (config.hasPath("singleton")) config.getBoolean("singleton") else false
this.logSize = if (config.hasPath("log-size")) config.getBytes("log-size").toInt() else null
this.fieldsShadowing = if (config.hasPath("fields-shadowing"))
config.extract<List<ShadowingSettings>>("fields-shadowing")
else emptyList()
this.port = if (isSingleton) createPort() else Unit as P
}

Expand Down Expand Up @@ -173,6 +181,15 @@ constructor(
proxy.client.inInterceptors.filterIsInstance<AbstractLoggingInterceptor>().forEach { it.limit = logSize }
proxy.client.outInterceptors.filterIsInstance<AbstractLoggingInterceptor>().forEach { it.limit = logSize }
}
if (config.hasPath("debugLog") && fieldsShadowing.isNotEmpty()) {
// proxy.client.outInterceptors += LoggingShadowingOutInterceptor(shadowingSettings = fieldsShadowing)
// proxy.client.outInterceptors += PreOutLoggingMessageInterceptor(fieldsShadowing)
proxy.client.outInterceptors += ShadowingLoggingOutInterceptor(fieldsShadowing, logSize)
proxy.client.inInterceptors += listOf(
PreShadowingLoggingInInterceptor(fieldsShadowing),
PostShadowingLoggingInInterceptor(fieldsShadowing, logSize)
)
}
httpClientPolicy.browserType = config.extract("browser-type") ?: "lagom"
afterInit(port)
return port
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.taymyr.lagom.soap.interceptor

import org.apache.cxf.helpers.XPathUtils
import org.apache.cxf.message.Message
import org.apache.cxf.message.Message.PROTOCOL_HEADERS
import org.apache.cxf.phase.AbstractPhaseInterceptor
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import java.io.ByteArrayInputStream
import java.io.StringWriter
import javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD
import javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA
import javax.xml.XMLConstants.ACCESS_EXTERNAL_STYLESHEET
import javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import javax.xml.xpath.XPathConstants.NODE
import javax.xml.xpath.XPathConstants.NODESET

const val SOAP_ACTION = "org.taymyr.lagom.soap.interceptor.soapAction"

@Suppress("unchecked_cast")
val Message.soapAction: String? get() = (this[PROTOCOL_HEADERS] as Map<String, List<String>>)["SOAPAction"]?.firstOrNull()

private val DOCUMENT_BUILDER: DocumentBuilder = DocumentBuilderFactory.newInstance().apply {
setAttribute(ACCESS_EXTERNAL_DTD, "")
setAttribute(ACCESS_EXTERNAL_SCHEMA, "")
setFeature(FEATURE_SECURE_PROCESSING, true)
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
}.newDocumentBuilder()

private val TRANSFORMER = TransformerFactory.newInstance().apply {
setAttribute(ACCESS_EXTERNAL_DTD, "")
setAttribute(ACCESS_EXTERNAL_STYLESHEET, "")
}.newTransformer()

private val xPathUtils: XPathUtils = XPathUtils()

abstract class AbstractLoggingShadowingInterceptor(
phase: String,
protected val shadowingSettings: List<ShadowingSettings>
) : AbstractPhaseInterceptor<Message>(phase) {

companion object {
fun shadowStringXml(
sourceXml: String,
shadowingConfig: ShadowingSettings?,
configSelector: (ShadowingSettings) -> List<String>
): String =
if (shadowingConfig != null) {
replaceAllEntrance(
xmlString = sourceXml,
replacingValue = shadowingConfig.replacingSymbols,
regexps = configSelector(shadowingConfig)
)
} else sourceXml

private tailrec fun replaceAllEntrance(
xmlString: String,
regexps: List<String>,
replacingValue: String
): String =
if (regexps.isNotEmpty())
replaceAllEntrance(
replaceXml(xmlString, regexps.first(), replacingValue),
regexps.drop(1),
replacingValue
)
else
xmlString

private fun replaceXml(xmlString: String, xmlPathExp: String, replacingValue: String): String =
runCatching {
val doc = DOCUMENT_BUILDER.parse(ByteArrayInputStream(xmlString.toByteArray()))
// node and nodes are mutual exclusive in common
val node = xPathUtils.getValue(xmlPathExp, doc, NODE) as? Element
val nodes = xPathUtils.getValue(xmlPathExp, doc, NODESET) as? NodeList
when {
nodes != null -> {
(0..nodes.length).mapNotNull { nodes.item(it) as? Element }.forEach {
replaceNode(replacingValue = replacingValue, sourceDocument = doc, foundNode = it)
}
getXmlWithReplacing(doc)
}

(node != null) -> {
replaceNode(replacingValue = replacingValue, sourceDocument = doc, foundNode = node)
getXmlWithReplacing(sourceDocument = doc)
}

else -> xmlString
}
}.recover { xmlString }
.getOrThrow()

private fun replaceNode(replacingValue: String, sourceDocument: Document, foundNode: Node) {
val fieldName = foundNode.nodeName
val fragmentDoc =
DOCUMENT_BUILDER.parse(ByteArrayInputStream("<$fieldName>$replacingValue</$fieldName>".toByteArray()))
val injectedNode = sourceDocument.adoptNode(fragmentDoc.firstChild)
val parentNode = foundNode.parentNode
parentNode.removeChild(foundNode)
parentNode.appendChild(injectedNode)
}

private fun getXmlWithReplacing(sourceDocument: Document): String {
val result = StreamResult(StringWriter())
TRANSFORMER.transform(DOMSource(sourceDocument), result)
return result.writer.toString()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.taymyr.lagom.soap.interceptor

import org.apache.cxf.helpers.IOUtils
import org.apache.cxf.interceptor.LoggingInInterceptor
import org.apache.cxf.io.CachedOutputStream
import org.apache.cxf.message.Message
import org.apache.cxf.phase.Phase.RECEIVE
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.io.SequenceInputStream
import kotlin.Int.Companion.MAX_VALUE

private const val SOURCE_INPUT_STREAM = "org.taymyr.lagom.soap.interceptor.sourceInputStream"

/**
* Wraps logging income inputstream to overwrite data before logging
*/
class PreShadowingLoggingInInterceptor(
shadowingSettings: List<ShadowingSettings>,
) : AbstractLoggingShadowingInterceptor(phase = RECEIVE, shadowingSettings = shadowingSettings) {

init {
addBefore(LoggingInInterceptor::class.java.name)
}

override fun handleMessage(message: Message) {
val soapAction = message.exchange[SOAP_ACTION]
if (soapAction != null) {
val bis = message.getContent(InputStream::class.java)
val bos = CachedOutputStream()
// copy all to process all xml string
IOUtils.copyAtLeast(bis, bos, MAX_VALUE)
bos.flush()
val result = bos.inputStream.toString()
message[SOURCE_INPUT_STREAM] = ByteArrayInputStream(result.toByteArray())
val bais = ByteArrayInputStream(
(
shadowStringXml(
sourceXml = result,
shadowingConfig = shadowingSettings.firstOrNull { it.soapMethod == soapAction }
) { it.responsePatterns }
).toByteArray()
)
message.setContent(InputStream::class.java, bais)
}
}
}

/**
* Wraps logging income inputstream to overwrite data after logging
*/
class PostShadowingLoggingInInterceptor(
shadowingSettings: List<ShadowingSettings>,
maxSize: Int?
) : AbstractLoggingShadowingInterceptor(phase = RECEIVE, shadowingSettings = shadowingSettings) {
private val maxSize = maxSize ?: (1024 * 48)
init {
addAfter(LoggingInInterceptor::class.java.name)
}

override fun handleMessage(message: Message) {
val soapAction = message.exchange[SOAP_ACTION]
if (soapAction != null) {
val bis = message[SOURCE_INPUT_STREAM] as InputStream
val bos = CachedOutputStream()
IOUtils.copyAtLeast(bis, bos, if (maxSize == -1) MAX_VALUE else maxSize)
bos.flush()
val bais = SequenceInputStream(bos.getInputStream(), bis)
message.setContent(InputStream::class.java, bais)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.taymyr.lagom.soap.interceptor

import org.apache.cxf.interceptor.LoggingOutInterceptor
import org.apache.cxf.io.CacheAndWriteOutputStream
import org.apache.cxf.io.CachedOutputStream
import org.apache.cxf.io.CachedOutputStreamCallback
import org.apache.cxf.message.Message
import org.apache.cxf.phase.Phase.PRE_STREAM
import org.taymyr.lagom.soap.interceptor.AbstractLoggingShadowingInterceptor.Companion.shadowStringXml
import java.io.ByteArrayOutputStream
import java.io.OutputStream

/**
* Wraps logging outcome callback for logging fields with shadowing
*/
class ShadowingLoggingOutInterceptor(
shadowingSettings: List<ShadowingSettings>,
maxSize: Int?,
) : AbstractLoggingShadowingInterceptor(phase = PRE_STREAM, shadowingSettings = shadowingSettings) {
private val maxSize = maxSize ?: (1024 * 48)

init {
addAfter(LoggingOutInterceptor::class.java.name)
}

override fun handleMessage(message: Message) {
val os = message.getContent(OutputStream::class.java) as CacheAndWriteOutputStream
// Here we have one org.apache.cxf.interceptor.LoggingOutInterceptor.LoggingCallback that we are going to wrap
val callbacks = os.callbacks.toMutableList().onEach {
os.deregisterCallback(it)
}
message.exchange[SOAP_ACTION] = message.soapAction
os.registerCallback(
NameShadowingCallback(
originalMessage = message,
shadowingSettings = shadowingSettings,
loggingCallback = callbacks,
maxSize = maxSize
)
)
}
}

class NameShadowingCallback(
private val originalMessage: Message,
private val loggingCallback: List<CachedOutputStreamCallback>,
private val shadowingSettings: List<ShadowingSettings>,
private val maxSize: Int,
) : CachedOutputStreamCallback {

override fun onClose(cos: CachedOutputStream) {
val sb = StringBuilder()
cos.writeCacheTo(sb)
val result = sb.toString()
val baos = ByteArrayOutputStream(maxSize)
baos.write(
(
shadowStringXml(
result,
shadowingSettings.firstOrNull { it.soapMethod == originalMessage.soapAction }
) { it.requestPatterns }
).toByteArray()
)
val shadowedCos = CacheAndWriteOutputStream(baos)
shadowedCos.resetOut(baos, true)
loggingCallback.forEach {
it.onClose(shadowedCos)
}
}

override fun onFlush(os: CachedOutputStream?) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.taymyr.lagom.soap.interceptor

data class ShadowingSettings(
val soapMethod: String,
val replacingSymbols: String,
val requestPatterns: List<String>,
val responsePatterns: List<String>
)

0 comments on commit 34f136e

Please sign in to comment.