From 986c2799d75d9ad5edd8de093e2d39e01d29766a Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Thu, 4 Aug 2022 14:57:01 -0400 Subject: [PATCH] fixes #1317 convert the audit handler to interceptor for logging request and response bodies (#1318) --- .../java/com/networknt/audit/AuditConfig.java | 30 +++- .../com/networknt/audit/AuditHandler.java | 98 ++++++------ audit/src/main/resources/config/audit.yml | 4 - .../com/networknt/audit/AuditConfigTest.java | 2 +- .../java/com/networknt/body/BodyConfig.java | 10 ++ .../java/com/networknt/body/BodyHandler.java | 2 +- ...eptor.java => RequestBodyInterceptor.java} | 22 +-- .../body/ResponseBodyInterceptor.java | 140 ++++++++++++++++++ body/src/main/resources/config/body.yml | 4 +- ...t.java => RequestBodyInterceptorTest.java} | 10 +- .../handler/ResponseInterceptor.java | 16 ++ .../httpstring/AttachmentConstants.java | 3 + .../ResponseTransformerInterceptor.java | 8 - 13 files changed, 264 insertions(+), 85 deletions(-) rename body/src/main/java/com/networknt/body/{ProxyBodyInterceptor.java => RequestBodyInterceptor.java} (87%) create mode 100644 body/src/main/java/com/networknt/body/ResponseBodyInterceptor.java rename body/src/test/java/com/networknt/body/{ProxyBodyInterceptorTest.java => RequestBodyInterceptorTest.java} (97%) diff --git a/audit/src/main/java/com/networknt/audit/AuditConfig.java b/audit/src/main/java/com/networknt/audit/AuditConfig.java index bc623a2298..7361ba51eb 100644 --- a/audit/src/main/java/com/networknt/audit/AuditConfig.java +++ b/audit/src/main/java/com/networknt/audit/AuditConfig.java @@ -34,14 +34,20 @@ class AuditConfig { private static final Logger logger = LoggerFactory.getLogger(AuditConfig.class); + public static final String REQUEST_BODY = "requestBody"; + public static final String RESPONSE_BODY = "responseBody"; + private static final String HEADERS = "headers"; private static final String AUDIT = "audit"; private static final String STATUS_CODE = "statusCode"; private static final String RESPONSE_TIME = "responseTime"; private static final String AUDIT_ON_ERROR = "auditOnError"; - private static final String IS_LOG_LEVEL_ERROR = "logLevelIsError"; - private static final String IS_MASK_ENABLED = "mask"; + private static final String LOG_LEVEL_IS_ERROR = "logLevelIsError"; + private static final String MASK = "mask"; private static final String TIMESTAMP_FORMAT = "timestampFormat"; + + private static final String ENABLED = "enabled"; + private Map mappedConfig; public static final String CONFIG_NAME = "audit"; private List headerList; @@ -53,9 +59,11 @@ class AuditConfig { private boolean statusCode; private boolean responseTime; private boolean auditOnError; - private boolean isMaskEnabled; + private boolean mask; private String timestampFormat; + private boolean enabled; + private AuditConfig() { this(CONFIG_NAME); } @@ -101,10 +109,12 @@ public boolean isAuditOnError() { return auditOnError; } - public boolean isMaskEnabled() { - return isMaskEnabled; + public boolean isMask() { + return mask; } + public boolean isEnabled() { return enabled; } + public boolean isResponseTime() { return responseTime; } @@ -134,7 +144,7 @@ Config getConfig() { } private void setLogLevel() { - Object object = getMappedConfig().get(IS_LOG_LEVEL_ERROR); + Object object = getMappedConfig().get(LOG_LEVEL_IS_ERROR); auditFunc = (object != null && (Boolean) object) ? LoggerFactory.getLogger(Constants.AUDIT_LOGGER)::error : LoggerFactory.getLogger(Constants.AUDIT_LOGGER)::info; } @@ -197,9 +207,13 @@ private void setConfigData() { if(object != null && (Boolean) object) { auditOnError = true; } - object = getMappedConfig().get(IS_MASK_ENABLED); + object = getMappedConfig().get(MASK); + if(object != null && (Boolean) object) { + mask = true; + } + object = getMappedConfig().get(ENABLED); if(object != null && (Boolean) object) { - isMaskEnabled = true; + } timestampFormat = (String)getMappedConfig().get(TIMESTAMP_FORMAT); } diff --git a/audit/src/main/java/com/networknt/audit/AuditHandler.java b/audit/src/main/java/com/networknt/audit/AuditHandler.java index ad241ea88e..6f9a009161 100644 --- a/audit/src/main/java/com/networknt/audit/AuditHandler.java +++ b/audit/src/main/java/com/networknt/audit/AuditHandler.java @@ -70,6 +70,7 @@ * responseTime * * Created by steve on 17/09/16. + * This handler is replaced by the AuditInterceptor for logging request body and response body. */ public class AuditHandler implements MiddlewareHandler { static final Logger logger = LoggerFactory.getLogger(AuditHandler.class); @@ -86,10 +87,10 @@ public class AuditHandler implements MiddlewareHandler { static final String REQUEST_COOKIES_KEY = "requestCookies"; static final String STATUS_KEY = "status"; static final String SERVER_CONFIG = "server"; - static final String SERVICEID_KEY = "serviceId"; + static final String SERVICE_ID_KEY = "serviceId"; static final String INVALID_CONFIG_VALUE_CODE = "ERR10060"; - private AuditConfig auditConfig; + private AuditConfig config; private volatile HttpHandler next; @@ -99,12 +100,12 @@ public class AuditHandler implements MiddlewareHandler { public AuditHandler() { if (logger.isInfoEnabled()) logger.info("AuditHandler is loaded."); - auditConfig = AuditConfig.load(); + config = AuditConfig.load(); Map serverConfig = Config.getInstance().getJsonMapConfigNoCache(SERVER_CONFIG); if (serverConfig != null) { - serviceId = (String) serverConfig.get(SERVICEID_KEY); + serviceId = (String) serverConfig.get(SERVICE_ID_KEY); } - String timestampFormat = auditConfig.getTimestampFormat(); + String timestampFormat = config.getTimestampFormat(); if (!StringUtils.isBlank(timestampFormat)) { try { DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(timestampFormat) @@ -126,35 +127,36 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { auditMap.put(TIMESTAMP, DATE_TIME_FORMATTER == null ? System.currentTimeMillis() : DATE_TIME_FORMATTER.format(Instant.now())); // dump audit info fields according to config - boolean needAuditData = auditInfo != null && auditConfig.hasAuditList(); + boolean needAuditData = auditInfo != null && config.hasAuditList(); if (needAuditData) { auditFields(auditInfo, auditMap); } // dump request header, request body, path parameters, query parameters and request cookies according to config - auditRequest(exchange, auditMap, auditConfig); + auditRequest(exchange, auditMap, config); // dump serviceId from server.yml - if (auditConfig.hasAuditList() && auditConfig.getAuditList().contains(SERVICEID_KEY)) { + if (config.hasAuditList() && config.getAuditList().contains(SERVICE_ID_KEY)) { auditServiceId(auditMap); } - if (auditConfig.isStatusCode() || auditConfig.isResponseTime()) { + if (config.isStatusCode() || config.isResponseTime()) { exchange.addExchangeCompleteListener((exchange1, nextListener) -> { - if (auditConfig.isStatusCode()) { + // response status code and response time. + if (config.isStatusCode()) { auditMap.put(STATUS_CODE, exchange1.getStatusCode()); } - if (auditConfig.isResponseTime()) { + if (config.isResponseTime()) { auditMap.put(RESPONSE_TIME, System.currentTimeMillis() - start); } // add additional fields accumulated during the microservice execution // according to the config Map auditInfo1 = exchange.getAttachment(AttachmentConstants.AUDIT_INFO); if (auditInfo1 != null) { - if (auditConfig.getAuditList() != null && auditConfig.getAuditList().size() > 0) { - for (String name : auditConfig.getAuditList()) { + if (config.getAuditList() != null && config.getAuditList().size() > 0) { + for (String name : config.getAuditList()) { if (name.equals(RESPONSE_BODY_KEY)) { - auditResponseOnError(exchange, auditMap); + auditResponseBody(exchange, auditMap); } auditMap.putIfAbsent(name, auditInfo1.get(name)); } @@ -163,11 +165,11 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { try { // audit entries only is it is an error, if auditOnError flag is set - if (auditConfig.isAuditOnError()) { + if (config.isAuditOnError()) { if (exchange1.getStatusCode() >= 400) - auditConfig.getAuditFunc().accept(Config.getInstance().getMapper().writeValueAsString(auditMap)); + config.getAuditFunc().accept(Config.getInstance().getMapper().writeValueAsString(auditMap)); } else { - auditConfig.getAuditFunc().accept(Config.getInstance().getMapper().writeValueAsString(auditMap)); + config.getAuditFunc().accept(Config.getInstance().getMapper().writeValueAsString(auditMap)); } } catch (JsonProcessingException e) { throw new RuntimeException(e); @@ -176,15 +178,15 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { nextListener.proceed(); }); } else { - auditConfig.getAuditFunc().accept(auditConfig.getConfig().getMapper().writeValueAsString(auditMap)); + config.getAuditFunc().accept(config.getConfig().getMapper().writeValueAsString(auditMap)); } next(exchange); } private void auditHeader(HttpServerExchange exchange, Map auditMap) { - for (String name : auditConfig.getHeaderList()) { + for (String name : config.getHeaderList()) { String value = exchange.getRequestHeaders().getFirst(name); - auditMap.put(name, auditConfig.isMaskEnabled() ? Mask.maskRegex(value, "requestHeader", name) : value); + auditMap.put(name, config.isMask() ? Mask.maskRegex(value, "requestHeader", name) : value); } } @@ -193,21 +195,21 @@ protected void next(HttpServerExchange exchange) throws Exception { } private void auditFields(Map auditInfo, Map auditMap) { - for (String name : auditConfig.getAuditList()) { + for (String name : config.getAuditList()) { Object value = auditInfo.get(name); - boolean needApplyMask = auditConfig.isMaskEnabled() && value instanceof String; + boolean needApplyMask = config.isMask() && value instanceof String; auditMap.put(name, needApplyMask ? Mask.maskRegex((String) value, MASK_KEY, name) : value); } } - private void auditRequest(HttpServerExchange exchange, Map auditMap, AuditConfig auditConfig) { - if (auditConfig.hasHeaderList()) { + private void auditRequest(HttpServerExchange exchange, Map auditMap, AuditConfig config) { + if (config.hasHeaderList()) { auditHeader(exchange, auditMap); } - if (!auditConfig.hasAuditList()) { + if (!config.hasAuditList()) { return; } - for (String key : auditConfig.getAuditList()) { + for (String key : config.getAuditList()) { switch (key) { case REQUEST_BODY_KEY: auditRequestBody(exchange, auditMap); @@ -239,22 +241,24 @@ private void auditRequestBody(HttpServerExchange exchange, Map a } // Mask requestBody json string if mask enabled if (requestBodyString != null) { - auditMap.put(REQUEST_BODY_KEY, auditConfig.isMaskEnabled() ? Mask.maskJson(requestBodyString, REQUEST_BODY_KEY) : requestBodyString); + auditMap.put(REQUEST_BODY_KEY, config.isMask() ? Mask.maskJson(requestBodyString, REQUEST_BODY_KEY) : requestBodyString); } } - // Audit response body only if auditOnError is enabled - private void auditResponseOnError(HttpServerExchange exchange, Map auditMap) { - if (!auditOnError) { - return; - } - String responseBodyString = null; - Map auditInfo = exchange.getAttachment(AttachmentConstants.AUDIT_INFO); - if (auditInfo != null && auditInfo.get(STATUS_KEY) != null) { - responseBodyString = auditInfo.get(STATUS_KEY).toString(); + // Audit response body + private void auditResponseBody(HttpServerExchange exchange, Map auditMap) { + String responseBodyString = exchange.getAttachment(AttachmentConstants.RESPONSE_BODY_STRING); + if(responseBodyString == null && exchange.getAttachment(AttachmentConstants.RESPONSE_BODY) != null) { + // try to convert the response body to JSON if possible. Fallback to String(). + try { + responseBodyString = Config.getInstance().getMapper().writeValueAsString(exchange.getAttachment(AttachmentConstants.RESPONSE_BODY)); + } catch (JsonProcessingException e) { + responseBodyString = exchange.getAttachment(AttachmentConstants.RESPONSE_BODY).toString(); + } } - if (responseBodyString != null) { - auditMap.put(RESPONSE_BODY_KEY, auditConfig.isMaskEnabled() ? Mask.maskJson(responseBodyString, RESPONSE_BODY_KEY) : responseBodyString); + // mask the response body json string if mask is enabled. + if(responseBodyString != null) { + auditMap.put(RESPONSE_BODY_KEY, config.isMask() ? Mask.maskJson(responseBodyString, RESPONSE_BODY_KEY) : responseBodyString); } } @@ -265,7 +269,7 @@ private void auditQueryParameters(HttpServerExchange exchange, Map 0) { for (String query : queryParameters.keySet()) { String value = queryParameters.get(query).toString(); - String mask = auditConfig.isMaskEnabled() ? Mask.maskRegex(value, QUERY_PARAMETERS_KEY, query) : value; + String mask = config.isMask() ? Mask.maskRegex(value, QUERY_PARAMETERS_KEY, query) : value; res.put(query, mask); } auditMap.put(QUERY_PARAMETERS_KEY, res.toString()); @@ -278,7 +282,7 @@ private void auditPathParameters(HttpServerExchange exchange, Map 0) { for (String name : pathParameters.keySet()) { String value = pathParameters.get(name).toString(); - String mask = auditConfig.isMaskEnabled() ? Mask.maskRegex(value, PATH_PARAMETERS_KEY, name) : value; + String mask = config.isMask() ? Mask.maskRegex(value, PATH_PARAMETERS_KEY, name) : value; res.put(name, mask); } auditMap.put(PATH_PARAMETERS_KEY, res.toString()); @@ -291,7 +295,7 @@ private void auditRequestCookies(HttpServerExchange exchange, Map 0) { for (String name : cookieMap.keySet()) { String cookieString = cookieMap.get(name).getValue(); - String mask = auditConfig.isMaskEnabled() ? Mask.maskRegex(cookieString, REQUEST_COOKIES_KEY, name) : cookieString; + String mask = config.isMask() ? Mask.maskRegex(cookieString, REQUEST_COOKIES_KEY, name) : cookieString; res.put(name, mask); } auditMap.put(REQUEST_COOKIES_KEY, res.toString()); @@ -300,7 +304,7 @@ private void auditRequestCookies(HttpServerExchange exchange, Map auditMap) { if (!StringUtils.isBlank(serviceId)) { - auditMap.put(SERVICEID_KEY, serviceId); + auditMap.put(SERVICE_ID_KEY, serviceId); } } @@ -318,21 +322,21 @@ public MiddlewareHandler setNext(final HttpHandler next) { @Override public boolean isEnabled() { - Object object = auditConfig.getMappedConfig().get(ENABLED); + Object object = config.getMappedConfig().get(ENABLED); return object != null && (Boolean) object; } @Override public void register() { - ModuleRegistry.registerModule(AuditHandler.class.getName(), auditConfig.getMappedConfig(), null); + ModuleRegistry.registerModule(AuditHandler.class.getName(), config.getMappedConfig(), null); } @Override public void reload() { - if (auditConfig==null) { - auditConfig = AuditConfig.load(); + if (config==null) { + config = AuditConfig.load(); } else { - auditConfig.reload(); + config.reload(); } } } diff --git a/audit/src/main/resources/config/audit.yml b/audit/src/main/resources/config/audit.yml index a3cbf012b0..0be17f4bd8 100644 --- a/audit/src/main/resources/config/audit.yml +++ b/audit/src/main/resources/config/audit.yml @@ -15,12 +15,8 @@ responseTime: ${audit.responseTime:true} # when auditOnError is true: # - it will only log when status code >= 400 -# - response body will be only logged when auditOnError is true -# - status detail will be only logged when auditOnError is true # when auditOnError is false: # - it will log on every request -# - no response body will be logged. -# - no status detail will be logged. # log level is controlled by logLevel auditOnError: ${audit.auditOnError:false} diff --git a/audit/src/test/java/com/networknt/audit/AuditConfigTest.java b/audit/src/test/java/com/networknt/audit/AuditConfigTest.java index f81809658e..e3c8c8ed82 100644 --- a/audit/src/test/java/com/networknt/audit/AuditConfigTest.java +++ b/audit/src/test/java/com/networknt/audit/AuditConfigTest.java @@ -41,7 +41,7 @@ public void shouldLoadEmptyConfig() { Assert.assertTrue(config.isStatusCode()); Assert.assertTrue(config.isResponseTime()); Assert.assertFalse(config.isAuditOnError()); - Assert.assertFalse(config.isMaskEnabled()); + Assert.assertFalse(config.isMask()); Assert.assertNotNull(config.getTimestampFormat()); } diff --git a/body/src/main/java/com/networknt/body/BodyConfig.java b/body/src/main/java/com/networknt/body/BodyConfig.java index b14dd44a27..44dfb02f89 100644 --- a/body/src/main/java/com/networknt/body/BodyConfig.java +++ b/body/src/main/java/com/networknt/body/BodyConfig.java @@ -36,10 +36,13 @@ public class BodyConfig { public static final String CONFIG_NAME = "body"; private static final String ENABLED = "enabled"; private static final String CACHE_REQUEST_BODY = "cacheRequestBody"; + + private static final String CACHE_RESPONSE_BODY = "cacheResponseBody"; private static final String APPLIED_PATH_PREFIXES = "appliedPathPrefixes"; boolean enabled; boolean cacheRequestBody; + boolean cacheResponseBody; List appliedPathPrefixes; private Config config; @@ -86,6 +89,9 @@ public boolean isEnabled() { public boolean isCacheRequestBody() { return cacheRequestBody; } + public boolean isCacheResponseBody() { + return cacheResponseBody; + } public List getAppliedPathPrefixes() { return appliedPathPrefixes; @@ -100,6 +106,10 @@ private void setConfigData() { if (object != null && (Boolean) object) { cacheRequestBody = (Boolean)object; } + object = mappedConfig.get(CACHE_RESPONSE_BODY); + if (object != null && (Boolean) object) { + cacheResponseBody = (Boolean)object; + } } private void setConfigList() { diff --git a/body/src/main/java/com/networknt/body/BodyHandler.java b/body/src/main/java/com/networknt/body/BodyHandler.java index 61aefe412a..e72f58b9e8 100644 --- a/body/src/main/java/com/networknt/body/BodyHandler.java +++ b/body/src/main/java/com/networknt/body/BodyHandler.java @@ -59,7 +59,7 @@ public class BodyHandler implements MiddlewareHandler { static final Logger logger = LoggerFactory.getLogger(BodyHandler.class); static final String CONTENT_TYPE_MISMATCH = "ERR10015"; - // request body will be parse during validation and it is attached to the exchange, in JSON, + // request body will be parsed during validation and it is attached to the exchange, in JSON, // it could be a map or list. So treat it as Object in the attachment. public static final AttachmentKey REQUEST_BODY = AttachmentConstants.REQUEST_BODY; diff --git a/body/src/main/java/com/networknt/body/ProxyBodyInterceptor.java b/body/src/main/java/com/networknt/body/RequestBodyInterceptor.java similarity index 87% rename from body/src/main/java/com/networknt/body/ProxyBodyInterceptor.java rename to body/src/main/java/com/networknt/body/RequestBodyInterceptor.java index 2b2e56eb35..0a1c67c27a 100644 --- a/body/src/main/java/com/networknt/body/ProxyBodyInterceptor.java +++ b/body/src/main/java/com/networknt/body/RequestBodyInterceptor.java @@ -27,11 +27,11 @@ /** * Note: With RequestInterceptorInjectionHandler implemented, this handler is changed from a - * pure middleware handler to RequestInterceptorHandler implementation. + * pure middleware handler to RequestInterceptor implementation. * - * This is the Body Parser handler used by the light-proxy and http-sidecar to not only parse - * the body into an attachment in the exchange but also keep the stream to be forwarded to the - * backend API. If the normal BodyHandler is used, once the stream is consumed, it is gone and + * This is the Body Parser interceptor used by the light-4j, light-proxy and http-sidecar to + * not only parse the body into an attachment in the exchange but also to keep the stream to + * be forwarded to the subsequent middleware handlers or backend API. If the normal BodyHandler is used, once the stream is consumed, it is gone and * cannot be transfer/forward to the backend with socket to socket transfer. *

* The body validation will only support smaller size JSON request body, so we check the method @@ -50,8 +50,8 @@ * * @author Steve Hu */ -public class ProxyBodyInterceptor implements RequestInterceptor { - static final Logger logger = LoggerFactory.getLogger(ProxyBodyInterceptor.class); +public class RequestBodyInterceptor implements RequestInterceptor { + static final Logger logger = LoggerFactory.getLogger(RequestBodyInterceptor.class); static final String CONTENT_TYPE_MISMATCH = "ERR10015"; static final String PAYLOAD_TOO_LARGE = "ERR10068"; static final String GENERIC_EXCEPTION = "ERR10014"; @@ -60,7 +60,7 @@ public class ProxyBodyInterceptor implements RequestInterceptor { private volatile HttpHandler next; - public ProxyBodyInterceptor() { + public RequestBodyInterceptor() { if (logger.isInfoEnabled()) logger.info("ProxyBodyHandler is loaded."); config = BodyConfig.load(); } @@ -86,7 +86,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { } boolean attached = this.attachJsonBody(exchange, completeBody.toString()); if(!attached) { - return; + if(logger.isInfoEnabled()) logger.info("Failed to attached the request body to the exchange"); } } Handler.next(exchange, next); @@ -148,9 +148,9 @@ private boolean attachJsonBody(final HttpServerExchange exchange, String string) return false; } if (config.isCacheRequestBody()) { - exchange.putAttachment(REQUEST_BODY_STRING, string); + exchange.putAttachment(AttachmentConstants.REQUEST_BODY_STRING, string); } - exchange.putAttachment(REQUEST_BODY, body); + exchange.putAttachment(AttachmentConstants.REQUEST_BODY, body); return true; } @@ -173,7 +173,7 @@ public boolean isEnabled() { @Override public void register() { - ModuleRegistry.registerModule(ProxyBodyInterceptor.class.getName(), config.getMappedConfig(), null); + ModuleRegistry.registerModule(RequestBodyInterceptor.class.getName(), config.getMappedConfig(), null); } @Override diff --git a/body/src/main/java/com/networknt/body/ResponseBodyInterceptor.java b/body/src/main/java/com/networknt/body/ResponseBodyInterceptor.java new file mode 100644 index 0000000000..00c7b692a4 --- /dev/null +++ b/body/src/main/java/com/networknt/body/ResponseBodyInterceptor.java @@ -0,0 +1,140 @@ +package com.networknt.body; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.networknt.config.Config; +import com.networknt.handler.BuffersUtils; +import com.networknt.handler.Handler; +import com.networknt.handler.MiddlewareHandler; +import com.networknt.handler.ResponseInterceptor; +import com.networknt.httpstring.AttachmentConstants; +import com.networknt.utility.ModuleRegistry; +import io.undertow.Handlers; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import static com.networknt.body.BodyHandler.REQUEST_BODY; +import static com.networknt.body.BodyHandler.REQUEST_BODY_STRING; + +public class ResponseBodyInterceptor implements ResponseInterceptor { + static final Logger logger = LoggerFactory.getLogger(ResponseBodyInterceptor.class); + static final String CONTENT_TYPE_MISMATCH = "ERR10015"; + static final String PAYLOAD_TOO_LARGE = "ERR10068"; + + public static int MAX_BUFFERS = 1024; + private BodyConfig config; + private volatile HttpHandler next; + + public ResponseBodyInterceptor() { + if(logger.isInfoEnabled()) logger.info("ResponseBodyInterceptor is loaded"); + config = BodyConfig.load(); + } + + @Override + public HttpHandler getNext() { + return next; + } + + @Override + public MiddlewareHandler setNext(HttpHandler next) { + Handlers.handlerNotNull(next); + this.next = next; + return this; + } + + @Override + public boolean isEnabled() { + return config.isEnabled(); + } + + @Override + public void register() { + ModuleRegistry.registerModule(ResponseBodyInterceptor.class.getName(), config.getMappedConfig(), null); + } + + @Override + public void reload() { + config.reload(); + } + + @Override + public boolean isRequiredContent() { + return true; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.startBlocking(); + if(shouldParseBody(exchange)) { + String s = BuffersUtils.toString(getBuffer(exchange), StandardCharsets.UTF_8); + if(logger.isTraceEnabled()) logger.trace("original response body = " + s); + // put the response body in the attachment for auditing and validation. + boolean attached = this.attachJsonBody(exchange, s); + if(!attached) { + if(logger.isInfoEnabled()) logger.info("Failed to attached the response body to the exchange"); + } + } + Handler.next(exchange, next); + } + + private boolean shouldParseBody(final HttpServerExchange exchange) { + String requestPath = exchange.getRequestPath(); + boolean isPathConfigured = config.getAppliedPathPrefixes() == null ? true : config.getAppliedPathPrefixes().stream().anyMatch(s -> requestPath.startsWith(s)); + return isPathConfigured && + exchange.getResponseHeaders().getFirst(Headers.CONTENT_TYPE) != null && + exchange.getResponseHeaders().getFirst(Headers.CONTENT_TYPE).startsWith("application/json"); + } + + /** + * Method used to parse the body into a Map or a List and attach it into exchange + * + * @param exchange exchange to be attached + * @param string raw request body + */ + private boolean attachJsonBody(final HttpServerExchange exchange, String string) { + Object body; + string = string.trim(); + if (string.startsWith("{")) { + try { + body = Config.getInstance().getMapper().readValue(string, new TypeReference>() { + }); + } catch (JsonProcessingException e) { + if(exchange.getConnection().getBufferSize() <= string.length()) { + setExchangeStatus(exchange, PAYLOAD_TOO_LARGE, "application/json"); + } else { + setExchangeStatus(exchange, CONTENT_TYPE_MISMATCH, "application/json"); + } + return false; + } + } else if (string.startsWith("[")) { + try { + body = Config.getInstance().getMapper().readValue(string, new TypeReference>() { + }); + } catch (JsonProcessingException e) { + if(exchange.getConnection().getBufferSize() <= string.length()) { + setExchangeStatus(exchange, PAYLOAD_TOO_LARGE, "application/json"); + } else { + setExchangeStatus(exchange, CONTENT_TYPE_MISMATCH, "application/json"); + } + return false; + } + } else { + // error here. The content type in head doesn't match the body. + setExchangeStatus(exchange, CONTENT_TYPE_MISMATCH, "application/json"); + return false; + } + if (config.isCacheRequestBody()) { + exchange.putAttachment(AttachmentConstants.RESPONSE_BODY_STRING, string); + } + exchange.putAttachment(AttachmentConstants.RESPONSE_BODY, body); + return true; + } + +} diff --git a/body/src/main/resources/config/body.yml b/body/src/main/resources/config/body.yml index 0022812ad5..23ca398230 100644 --- a/body/src/main/resources/config/body.yml +++ b/body/src/main/resources/config/body.yml @@ -1,7 +1,9 @@ # Enable body parse flag enabled: ${body.enabled:true} -# cache request body as a String along with JSON object +# cache request body as a string along with JSON object cacheRequestBody: ${body.cacheRequestBody:false} +# cache response body as a string along with JSON object +cacheResponseBody: ${body.cacheResponseBody:false} # A list of applied request path prefixes, other requests will skip this handler. The value can be a string # if there is only one request path prefix needs this handler. or a list of strings if there are multiple. appliedPathPrefixes: ${body.appliedPathPrefixes:} diff --git a/body/src/test/java/com/networknt/body/ProxyBodyInterceptorTest.java b/body/src/test/java/com/networknt/body/RequestBodyInterceptorTest.java similarity index 97% rename from body/src/test/java/com/networknt/body/ProxyBodyInterceptorTest.java rename to body/src/test/java/com/networknt/body/RequestBodyInterceptorTest.java index 1796626dcb..665aece90d 100644 --- a/body/src/test/java/com/networknt/body/ProxyBodyInterceptorTest.java +++ b/body/src/test/java/com/networknt/body/RequestBodyInterceptorTest.java @@ -37,8 +37,8 @@ * * @author Steve Hu */ -public class ProxyBodyInterceptorTest { - static final Logger logger = LoggerFactory.getLogger(ProxyBodyInterceptorTest.class); +public class RequestBodyInterceptorTest { + static final Logger logger = LoggerFactory.getLogger(RequestBodyInterceptorTest.class); static Undertow server = null; @@ -47,7 +47,7 @@ public static void setUp() { if (server == null) { logger.info("starting server"); HttpHandler handler = getTestHandler(); - ProxyBodyInterceptor bodyHandler = new ProxyBodyInterceptor(); + RequestBodyInterceptor bodyHandler = new RequestBodyInterceptor(); bodyHandler.setNext(handler); handler = bodyHandler; server = Undertow.builder() @@ -431,6 +431,8 @@ public void run() { } finally { IoUtils.safeClose(connection); } - Assert.assertEquals("nobody", reference.get().getAttachment(Http2Client.RESPONSE_BODY)); + String responseBody = reference.get().getAttachment(Http2Client.RESPONSE_BODY); + System.out.println("response body = " + responseBody); + Assert.assertEquals("nobody", responseBody); } } diff --git a/handler/src/main/java/com/networknt/handler/ResponseInterceptor.java b/handler/src/main/java/com/networknt/handler/ResponseInterceptor.java index feba4d7d5d..fa9c1b2ed7 100644 --- a/handler/src/main/java/com/networknt/handler/ResponseInterceptor.java +++ b/handler/src/main/java/com/networknt/handler/ResponseInterceptor.java @@ -1,5 +1,9 @@ package com.networknt.handler; +import com.networknt.httpstring.AttachmentConstants; +import io.undertow.connector.PooledByteBuffer; +import io.undertow.server.HttpServerExchange; + /** * This handler is special middleware handler, and it is used to inject response interceptors in the request/response * chain to modify/transform the response before calling the next middleware handler. @@ -20,4 +24,16 @@ public interface ResponseInterceptor extends MiddlewareHandler { */ default boolean isAsync() {return false;}; + /** + * A default interface method to get the buffer from the exchange attachment for response body. + * @param exchange HttpServerExchange + * @return PooledByteBuffer[] array + */ + default PooledByteBuffer[] getBuffer(HttpServerExchange exchange) { + PooledByteBuffer[] buffer = exchange.getAttachment(AttachmentConstants.BUFFERED_RESPONSE_DATA_KEY); + if (buffer == null) { + throw new IllegalStateException("Response content is not available in exchange attachment as there is no interceptors."); + } + return buffer; + } } diff --git a/http-string/src/main/java/com/networknt/httpstring/AttachmentConstants.java b/http-string/src/main/java/com/networknt/httpstring/AttachmentConstants.java index e745d6e102..acde0d3546 100644 --- a/http-string/src/main/java/com/networknt/httpstring/AttachmentConstants.java +++ b/http-string/src/main/java/com/networknt/httpstring/AttachmentConstants.java @@ -21,6 +21,9 @@ public class AttachmentConstants { public static final AttachmentKey AUDIT_INFO = AttachmentKey.create(Map.class); public static final AttachmentKey REQUEST_BODY = AttachmentKey.create(Object.class); public static final AttachmentKey REQUEST_BODY_STRING = AttachmentKey.create(String.class); + public static final AttachmentKey RESPONSE_BODY = AttachmentKey.create(Object.class); + public static final AttachmentKey RESPONSE_BODY_STRING = AttachmentKey.create(String.class); + public static final AttachmentKey BUFFERED_RESPONSE_DATA_KEY = AttachmentKey.create(PooledByteBuffer[].class); public static final AttachmentKey BUFFERED_REQUEST_DATA_KEY = AttachmentKey.create(PooledByteBuffer[].class); diff --git a/response-transformer/src/main/java/com/networknt/restrans/ResponseTransformerInterceptor.java b/response-transformer/src/main/java/com/networknt/restrans/ResponseTransformerInterceptor.java index 80b8297b58..37a589cbb3 100644 --- a/response-transformer/src/main/java/com/networknt/restrans/ResponseTransformerInterceptor.java +++ b/response-transformer/src/main/java/com/networknt/restrans/ResponseTransformerInterceptor.java @@ -157,14 +157,6 @@ public boolean isRequiredContent() { return config.isRequiredContent(); } - public PooledByteBuffer[] getBuffer(HttpServerExchange exchange) { - PooledByteBuffer[] buffer = exchange.getAttachment(AttachmentConstants.BUFFERED_RESPONSE_DATA_KEY); - if (buffer == null) { - throw new IllegalStateException("Response content is not available in exchange attachment as there is no interceptors."); - } - return buffer; - } - public void setBuffer(HttpServerExchange exchange, PooledByteBuffer[] raw) { // close the current buffer pool PooledByteBuffer[] oldBuffers = exchange.getAttachment(AttachmentConstants.BUFFERED_RESPONSE_DATA_KEY);