diff --git a/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java index a12e5da..5993b53 100644 --- a/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java +++ b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java @@ -23,6 +23,7 @@ import com.eactive.eai.common.message.EAIMessage; import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.util.Logger; import com.eactive.eai.common.util.MessageUtil; +import com.eactive.eai.common.util.MessageUtil; import com.eactive.eai.message.StandardItem; import com.eactive.eai.message.StandardMessage; import com.google.gson.JsonElement; @@ -171,7 +172,10 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle } else { // ${path} / ${callprop.키} / ${exception.필드} 스칼라 변수 String expr = matcher.group(3).trim(); - replacement = resolveScalar(expr, msg, callProp, exception); + // 치환값에 제어문자가 섞이면 렌더 결과가 깨진 JSON/XML 이 된다. + // (개행이 든 값 → {"outpMsgDesc":"오류상세<개행>..."} → 수신측 파싱 실패) + // 템플릿 포맷을 알 수 없으므로 이스케이프 대신 제어문자를 걸러낸다. + replacement = MessageUtil.stripControlChars(resolveScalar(expr, msg, callProp, exception)); } matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); } @@ -400,7 +404,8 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle String value = ""; StandardItem item = row.get(fieldName); if (item != null) { - value = StringUtils.defaultString(item.getValue()); + // render() 의 스칼라 치환과 동일한 이유로 제어문자를 걸러낸다 + value = MessageUtil.stripControlChars(StringUtils.defaultString(item.getValue())); } varMatcher.appendReplacement(result, Matcher.quoteReplacement(value)); } diff --git a/src/main/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandler.java b/src/main/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandler.java index 43a32c5..56d5e1e 100644 --- a/src/main/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandler.java +++ b/src/main/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandler.java @@ -60,7 +60,9 @@ public class TemplateCodeConvertAdapterErrorMsgHandler extends TemplateAdapterEr return responseMsessage; } - JsonNode rootNode = OBJECT_MAPPER.readTree(jsonStr); + // 상대 시스템이 개행 등 제어문자를 이스케이프하지 않고 보내는 경우가 있어 정규화 후 파싱한다. + // (이미 표준을 지킨 JSON 이면 escapeControlChars 는 원본을 그대로 반환한다) + JsonNode rootNode = OBJECT_MAPPER.readTree(JacksonUtil.escapeControlChars(jsonStr)); boolean modified = false; for (String rawField : fieldsValue.split(",")) { diff --git a/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceBody.java b/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceBody.java index a651da0..845e7e0 100644 --- a/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceBody.java +++ b/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceBody.java @@ -64,7 +64,7 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp // 파싱 후 재직렬화하므로 숫자 자릿수가 유실되지 않는 mapper 를 쓴다. // 기본 ObjectMapper 는 100000000.00 을 1.0E8 로 바꿔버린다. ObjectMapper mapper = JacksonUtil.newNumberSafeMapper(); - ObjectNode jsonNode = (ObjectNode) mapper.readTree(sendData); + ObjectNode jsonNode = (ObjectNode) mapper.readTree(JacksonUtil.escapeControlChars(sendData)); ObjectNode headerPart = (ObjectNode) jsonNode.get("header_part"); if( headerPart.get("mciIntfId") != null && headerPart.get("mciIntfId").asText().trim().length() > 0 ) { diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/JsonToSetStatusFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/JsonToSetStatusFilter.java index 678af70..944f169 100644 --- a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/JsonToSetStatusFilter.java +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/JsonToSetStatusFilter.java @@ -6,6 +6,7 @@ import java.util.Properties; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import com.eactive.eai.common.util.JacksonUtil; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; import org.apache.commons.lang3.StringUtils; @@ -66,6 +67,6 @@ public class JsonToSetStatusFilter implements HttpAdapterFilter { orgMessageString = (String) message; } - return mapper.readTree(orgMessageString); + return mapper.readTree(JacksonUtil.escapeControlChars(orgMessageString)); } } diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/JsonToStdConverterFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/JsonToStdConverterFilter.java index f02e309..6328641 100644 --- a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/JsonToStdConverterFilter.java +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/JsonToStdConverterFilter.java @@ -107,7 +107,7 @@ public class JsonToStdConverterFilter implements HttpAdapterFilter { orgMessageString = (String) message; } - return mapper.readTree(orgMessageString); + return mapper.readTree(JacksonUtil.escapeControlChars(orgMessageString)); } } diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankEaiJsonParseFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankEaiJsonParseFilter.java index b1d0027..15f021c 100644 --- a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankEaiJsonParseFilter.java +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankEaiJsonParseFilter.java @@ -33,7 +33,7 @@ public class KbankEaiJsonParseFilter implements HttpAdapterFilter { for(Iterator it = rootNode.fieldNames(); it.hasNext();) { String fieldName = it.next(); String value = rootNode.get(fieldName).asText(); - JsonNode jsonNode = mapper.readTree(value); + JsonNode jsonNode = mapper.readTree(JacksonUtil.escapeControlChars(value)); replacedJson.set(fieldName, jsonNode); } @@ -65,7 +65,7 @@ public class KbankEaiJsonParseFilter implements HttpAdapterFilter { orgMessageString = (String) message; } - return mapper.readTree(orgMessageString); + return mapper.readTree(JacksonUtil.escapeControlChars(orgMessageString)); } } diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankHmacSha256VerifyFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankHmacSha256VerifyFilter.java index e68cb90..6e56f4e 100644 --- a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankHmacSha256VerifyFilter.java +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/custom/KbankHmacSha256VerifyFilter.java @@ -16,6 +16,7 @@ import javax.servlet.http.HttpServletResponse; import com.eactive.eai.adapter.AdapterManager; import com.eactive.eai.common.property.PropManager; +import com.eactive.eai.common.util.JacksonUtil; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; @@ -203,7 +204,7 @@ public class KbankHmacSha256VerifyFilter implements HttpAdapterFilter { orgMessageString = (String) message; } - return mapper.readTree(orgMessageString); + return mapper.readTree(JacksonUtil.escapeControlChars(orgMessageString)); } @Override diff --git a/src/main/java/com/eactive/eai/common/util/JacksonUtil.java b/src/main/java/com/eactive/eai/common/util/JacksonUtil.java index f1d0756..9b08048 100644 --- a/src/main/java/com/eactive/eai/common/util/JacksonUtil.java +++ b/src/main/java/com/eactive/eai/common/util/JacksonUtil.java @@ -191,7 +191,9 @@ public final class JacksonUtil { return null; } - return objectMapper.readTree(jsonStr); + // 상대 시스템이 제어문자를 이스케이프하지 않고 보내는 경우가 있어 정규화 후 파싱한다. + // 이미 표준을 지킨 JSON 이면 원본을 그대로 반환하므로 사실상 무해하다. + return objectMapper.readTree(escapeControlChars(jsonStr)); } public static JsonNode readTree(Object jsonData) throws JsonMappingException, JsonProcessingException { diff --git a/src/main/java/com/eactive/eai/common/util/MessageUtil.java b/src/main/java/com/eactive/eai/common/util/MessageUtil.java index 4bf4423..0bc5286 100644 --- a/src/main/java/com/eactive/eai/common/util/MessageUtil.java +++ b/src/main/java/com/eactive/eai/common/util/MessageUtil.java @@ -47,6 +47,61 @@ public final class MessageUtil { } + /** + * 문자열에서 제어문자(0x00~0x1F, 0x7F)를 걸러낸다. + * + * 값이 JSON / XML / 고정길이 전문 중 어디로 나갈지 모르는 자리 - 대표적으로 템플릿 치환 - + * 에서 쓴다. 출력 포맷마다 이스케이프 방식이 달라 포맷을 알아야 하는데, 제어문자를 아예 + * 걷어내면 포맷과 무관하게 안전해진다. + * + * 각 포맷에서 제어문자가 일으키는 문제: + * - JSON : raw 제어문자는 문자열 안에 올 수 없다 + * ("Illegal unquoted character ((CTRL-CHAR, code 10))") + * - XML : 0x09/0x0A/0x0D 를 제외한 제어문자는 문자 참조로도 표현할 수 없어 + * 수신측 파서가 거부한다 + * - 전문 : 제어문자도 1바이트를 차지해 고정길이 자리수가 어긋난다 + * + * 처리 규칙 + * - 탭/개행/캐리지리턴(0x09/0x0A/0x0D) : 구분 의미가 있으므로 공백 1칸으로 치환 + * - 그 외 제어문자 및 DEL(0x7F) : 제거 + * + * 연속 공백을 합치지는 않는다(CRLF 는 공백 2칸이 된다). 값 변형을 최소화하기 위함이다. + * + * @param s 원본 문자열. null/빈 문자열이면 그대로 반환 + * @return 제어문자가 걸러진 문자열. 걸러낼 게 없으면 원본을 그대로 반환 + */ + public static String stripControlChars(String s) { + if (s == null || s.isEmpty()) { + return s; + } + + // 빠른 경로: 제어문자가 없으면 원본 그대로 (대부분의 값이 여기 해당) + boolean found = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c < 0x20 || c == 0x7F) { + found = true; + break; + } + } + if (!found) { + return s; + } + + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\t' || c == '\n' || c == '\r') { + sb.append(' '); + } else if (c < 0x20 || c == 0x7F) { + continue; + } else { + sb.append(c); + } + } + return sb.toString(); + } + // ASCII Bytes에서 특정길이의 값을 추출하는 Method public static String getAscBytes(byte[] message, int startPos, int length) { if (message == null || message.length < startPos) {