From 0d09ba7f06df8374ee73d4ff3df25f166709f084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 26 Mar 2026 17:42:20 +0100 Subject: [PATCH 1/4] draft of fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../sse/ServerSentEventProxyHandler.java | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java index 76561a975e..9ee967ccfc 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java @@ -15,6 +15,7 @@ import io.netty.handler.ssl.SslContextBuilder; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; @@ -141,11 +142,63 @@ public SseEmitter getEmitter(HttpServletRequest request, HttpServletResponse res return emitter; } + boolean hasEnter(String in) { + return StringUtils.containsAny(in, '\n', '\r'); + } + + boolean hasEnter(ServerSentEvent event) { + return + hasEnter(event.data()) || + hasEnter(event.event()) || + hasEnter(event.comment()) || + hasEnter(event.id()); + } + + ServerSentEvent sanitize(ServerSentEvent event) { + if (!hasEnter(event)) { + return event; + } + + if (hasEnter(event.event())) { + throw new IllegalArgumentException("Illegal character in event content"); + } + + if (hasEnter(event.id())) { + throw new IllegalArgumentException("Illegal character in event content"); + } + + String data = event.data(); + if (hasEnter(data)) { + if (data.endsWith("\n\n")) { + data = data.substring(0, data.length() - 2); + } + + data = data.replaceAll("\r\n", "\ndata:"); + data = data.replaceAll("\n", "\ndata:"); + } + + String comment = event.comment(); + if (hasEnter(comment)) { + if (comment.endsWith("\n")) { + comment = comment.substring(0, comment.length() - 1); + } + comment = comment.replaceAll("\n", "\n:") + '\n'; + } + + return ServerSentEvent.builder() + .comment(comment) + .event(event.event()) + .id(event.id()) + .data(data) + .retry(event.retry()) + .build(); + } + // package protected for unit testing Consumer> consumer(SseEmitter emitter) { return content -> { try { - emitter.send(content.data()); + emitter.send(sanitize(content).data()); } catch (IOException error) { emitter.completeWithError(error); } From 51cf8bad3e4fdb99a7350a5dffb06b756dc6ad6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 26 Mar 2026 22:28:52 +0100 Subject: [PATCH 2/4] draft of fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../sse/ServerSentEventProxyHandler.java | 146 ++++++++++++++++-- 1 file changed, 133 insertions(+), 13 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java index 9ee967ccfc..37c1bdcd32 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java @@ -10,6 +10,8 @@ package org.zowe.apiml.gateway.sse; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; @@ -20,12 +22,16 @@ import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.codec.ServerSentEvent; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; +import org.springframework.util.MimeType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.zowe.apiml.message.core.Message; import org.zowe.apiml.message.core.MessageService; @@ -46,13 +52,11 @@ import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import static org.springframework.http.MediaType.TEXT_PLAIN; import static org.zowe.apiml.security.SecurityUtils.loadKeyStore; @Slf4j @@ -104,7 +108,12 @@ private SslContext getSslContext() throws CertificateException, IOException, NoS @GetMapping({"/sse/**","/*/sse/**"}) public SseEmitter getEmitter(HttpServletRequest request, HttpServletResponse response) throws IOException { - SseEmitter emitter = new SseEmitter(-1L); + SseEmitter emitter = new SseEmitter(-1L) { + @Override + public void send(Object object, MediaType mediaType) throws IOException { + send(new SseEventBuilderFixedImpl().data(object, mediaType)); + } + }; String uri = request.getRequestURI(); List uriParts = getUriParts(uri); @@ -169,20 +178,13 @@ ServerSentEvent sanitize(ServerSentEvent event) { String data = event.data(); if (hasEnter(data)) { - if (data.endsWith("\n\n")) { - data = data.substring(0, data.length() - 2); - } - data = data.replaceAll("\r\n", "\ndata:"); data = data.replaceAll("\n", "\ndata:"); } String comment = event.comment(); if (hasEnter(comment)) { - if (comment.endsWith("\n")) { - comment = comment.substring(0, comment.length() - 1); - } - comment = comment.replaceAll("\n", "\n:") + '\n'; + comment = comment.replaceAll("\n", "\n:"); } return ServerSentEvent.builder() @@ -262,4 +264,122 @@ private void writeError(HttpServletResponse response, SseErrorMessages errorMess public void addRoutedServices(String serviceId, RoutedServices routedServices) { routedServicesMap.put(serviceId, routedServices); } + + private static class SseEventBuilderFixedImpl implements SseEmitter.SseEventBuilder { + + private final Set dataToSend = new LinkedHashSet<>(4); + + private final StringBuilder sb = new StringBuilder(); + + + private boolean hasName; + + @Override + public SseEventBuilderFixedImpl id(String id) { + checkEvent(id); + append("id:").append(id).append('\n'); + return this; + } + + @Override + public SseEventBuilderFixedImpl name(String name) { + checkEvent(name); + this.hasName = true; + append("event:").append(name).append('\n'); + return this; + } + + @Override + public SseEventBuilderFixedImpl reconnectTime(long reconnectTimeMillis) { + append("retry:").append(String.valueOf(reconnectTimeMillis)).append('\n'); + return this; + } + + @Override + public SseEventBuilderFixedImpl comment(String comment) { + append(':').append(StringUtils.replace(comment, "\n", "\n:")).append('\n'); + return this; + } + + @Override + public SseEventBuilderFixedImpl data(Object object) { + return data(object, null); + } + + @Override + public SseEventBuilderFixedImpl data(Object object, @Nullable MediaType mediaType) { + if (object instanceof ModelAndView && !this.hasName && ((ModelAndView) object).getViewName() != null) { + name(((ModelAndView) object).getViewName()); + } + append("data:"); + saveAppendedText(TEXT_PLAIN); + if (object instanceof String) { + writeStringData((String) object, mediaType); + } + else { + this.dataToSend.add(new ResponseBodyEmitter.DataWithMediaType(object, mediaType)); + } + + append('\n'); + return this; + } + + private static void checkEvent(String content) { + Assert.isTrue(content.indexOf('\n') == -1 && content.indexOf('\r') == -1, + "illegal character '\\n' or '\\r' in event content"); + } + + private void writeStringData(String input, @Nullable MediaType mediaType) { + if (input.indexOf('\n') == -1 && input.indexOf('\r') == -1) { + this.dataToSend.add(new ResponseBodyEmitter.DataWithMediaType(input, mediaType)); + } + else { + int length = input.length(); + for (int i = 0; i < length; i++) { + char c = input.charAt(i); + if (c == '\r') { + if (i + 1 < length && input.charAt(i + 1) == '\n') { + i++; + } + this.sb.append("\ndata:"); + } + else if (c == '\n') { + this.sb.append("\ndata:"); + } + else { + this.sb.append(c); + } + } + saveAppendedText(mediaType); + } + } + + SseEventBuilderFixedImpl append(String text) { + this.sb.append(text); + return this; + } + + SseEventBuilderFixedImpl append(char ch) { + this.sb.append(ch); + return this; + } + + @Override + public Set build() { + if (!org.springframework.util.StringUtils.hasLength(this.sb) && this.dataToSend.isEmpty()) { + return Collections.emptySet(); + } + append('\n'); + saveAppendedText(TEXT_PLAIN); + return this.dataToSend; + } + + private void saveAppendedText(@Nullable MediaType mediaType) { + if (org.springframework.util.StringUtils.hasLength(this.sb)) { + this.dataToSend.add(new ResponseBodyEmitter.DataWithMediaType(this.sb.toString(), mediaType)); + this.sb.setLength(0); + } + } + } + } From 446c386cb47dfabea167252b92cebee62865433d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Fri, 27 Mar 2026 00:09:00 +0100 Subject: [PATCH 3/4] tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../sse/ServerSentEventProxyHandler.java | 20 +-- .../sse/ServerSentEventProxyHandlerTest.java | 152 +++++++++++++++++- 2 files changed, 157 insertions(+), 15 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java index 37c1bdcd32..ae33436bfe 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java @@ -10,8 +10,6 @@ package org.zowe.apiml.gateway.sse; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; @@ -25,9 +23,10 @@ import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.codec.ServerSentEvent; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; -import org.springframework.util.MimeType; +import org.springframework.util.Assert; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.servlet.ModelAndView; @@ -111,7 +110,7 @@ public SseEmitter getEmitter(HttpServletRequest request, HttpServletResponse res SseEmitter emitter = new SseEmitter(-1L) { @Override public void send(Object object, MediaType mediaType) throws IOException { - send(new SseEventBuilderFixedImpl().data(object, mediaType)); + super.send(new SseEventBuilderFixedImpl().data(object, mediaType)); } }; @@ -168,17 +167,12 @@ ServerSentEvent sanitize(ServerSentEvent event) { return event; } - if (hasEnter(event.event())) { - throw new IllegalArgumentException("Illegal character in event content"); - } - - if (hasEnter(event.id())) { - throw new IllegalArgumentException("Illegal character in event content"); - } + Assert.isTrue(!hasEnter(event.event()), "Illegal character in event content"); + Assert.isTrue(!hasEnter(event.id()), "Illegal character in event content"); String data = event.data(); if (hasEnter(data)) { - data = data.replaceAll("\r\n", "\ndata:"); + data = data.replaceAll("\r\n", "\n"); data = data.replaceAll("\n", "\ndata:"); } @@ -265,7 +259,7 @@ public void addRoutedServices(String serviceId, RoutedServices routedServices) { routedServicesMap.put(serviceId, routedServices); } - private static class SseEventBuilderFixedImpl implements SseEmitter.SseEventBuilder { + static class SseEventBuilderFixedImpl implements SseEmitter.SseEventBuilder { private final Set dataToSend = new LinkedHashSet<>(4); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandlerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandlerTest.java index b95f9ec39a..5708d916bf 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandlerTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandlerTest.java @@ -10,6 +10,7 @@ package org.zowe.apiml.gateway.sse; +import com.netflix.appinfo.InstanceInfo; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -18,18 +19,27 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.codec.ServerSentEvent; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.yaml.YamlMessageService; import org.zowe.apiml.product.routing.RoutedService; import org.zowe.apiml.product.routing.RoutedServices; +import reactor.core.publisher.Flux; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -39,15 +49,17 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Stream; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -281,4 +293,140 @@ private static Stream endpoints() { Arguments.of("/serviceid/sse/v1/", "/") ); } + + @Nested + class Emitter { + + @Nested + class Consumer { + + @Mock + private SseEmitter emitter; + + @Test + void givenValidData_whenConsumer_thenEmitData() throws IOException { + ServerSentEvent event = ServerSentEvent.builder() + .event("event") + .data("data") + .comment("comment") + .id("id") + .build(); + + new ServerSentEventProxyHandler(null, null) + .consumer(emitter).accept(event); + + verify(emitter).send("data"); + verify(emitter, never()).completeWithError(any()); + } + + @Test + void givenEmptyEvent_whenConsumer_thenEmitData() throws IOException { + ServerSentEvent event = ServerSentEvent.builder().build(); + + new ServerSentEventProxyHandler(null, null) + .consumer(emitter).accept(event); + + verify(emitter).send(isNull(Object.class)); + } + + @Test + void givenInvalidId_whenConsumer_thenThrowException() { + ServerSentEvent event = ServerSentEvent.builder() + .id("invalid\nid") + .build(); + + java.util.function.Consumer> consumer = new ServerSentEventProxyHandler(null, null) + .consumer(emitter); + + assertThrows(IllegalArgumentException.class, () -> consumer.accept(event)); + } + + @Test + void givenInvalidEvent_whenConsumer_thenThrowException() { + ServerSentEvent event = ServerSentEvent.builder() + .event("invalid\revent") + .build(); + + java.util.function.Consumer> consumer = new ServerSentEventProxyHandler(null, null) + .consumer(emitter); + + assertThrows(IllegalArgumentException.class, () -> consumer.accept(event)); + } + + @Test + void givenInvalidData_whenConsumer_thenSanitizeAndEmit() throws IOException { + ServerSentEvent event = ServerSentEvent.builder() + .data("invalid\r\ndata\nto\rbe\n\nsanitized") + .build(); + + new ServerSentEventProxyHandler(null, null) + .consumer(emitter).accept(event); + + verify(emitter).send("invalid\ndata:data\ndata:to\rbe\ndata:\ndata:sanitized"); + } + + } + + @Nested + class BuilderImpl { + + @Captor + private ArgumentCaptor builderCaptor; + + @Mock + private DiscoveryClient discoveryClient; + + private ServerSentEventProxyHandler serverSentEventProxyHandler; + + @BeforeEach + void init() { + doReturn(Collections.singletonList(new EurekaServiceInstance(mock(InstanceInfo.class)))) + .when(discoveryClient).getInstances(any()); + + RoutedServices routedServices = mock(RoutedServices.class); + doReturn(mock(RoutedService.class)).when(routedServices).findServiceByGatewayUrl("sse/v1"); + + serverSentEventProxyHandler = spy(new ServerSentEventProxyHandler(discoveryClient, null)); + serverSentEventProxyHandler.addRoutedServices("service", routedServices); + + doReturn(Flux.empty()).when(serverSentEventProxyHandler).getSseStream(any()); + } + + @Test + void givenJson_whenGetEmitter_thenDataAreEmitted() throws IOException { + SseEmitter emitter = spy(serverSentEventProxyHandler.getEmitter(new MockHttpServletRequest("GET", "sse/service/api/v1/path"), new MockHttpServletResponse())); + doNothing().when(emitter).send(any(SseEmitter.SseEventBuilder.class)); + ReflectionTestUtils.setField(emitter, "complete", false); + Object json = new Object(); + + emitter.send(json, MediaType.APPLICATION_JSON); + verify(emitter).send(builderCaptor.capture()); + + Set data = builderCaptor.getValue().build(); + assertEquals(3, data.size()); + ResponseBodyEmitter.DataWithMediaType second = new ArrayList<>(data).get(1); + assertSame(json, second.getData()); + assertEquals(MediaType.APPLICATION_JSON, second.getMediaType()); + } + + @Test + void givenPlainText_whenGetEmitter_thenDataAreSanitized() throws IOException { + SseEmitter emitter = spy(serverSentEventProxyHandler.getEmitter(new MockHttpServletRequest("GET", "sse/service/api/v1/path"), new MockHttpServletResponse())); + doNothing().when(emitter).send(any(SseEmitter.SseEventBuilder.class)); + ReflectionTestUtils.setField(emitter, "complete", false); + + emitter.send("corrupted\ndata\r\non\rinput", MediaType.TEXT_PLAIN); + verify(emitter).send(builderCaptor.capture()); + + Set data = builderCaptor.getValue().build(); + assertEquals(3, data.size()); + ResponseBodyEmitter.DataWithMediaType second = new ArrayList<>(data).get(1); + assertEquals("corrupted\ndata:data\ndata:on\ndata:input", second.getData()); + assertEquals(MediaType.TEXT_PLAIN, second.getMediaType()); + } + + } + + } + } From c834af5b5f7afaceb5d3612c145fd06f2f52e840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Fri, 27 Mar 2026 09:22:37 +0100 Subject: [PATCH 4/4] fix Sonar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../gateway/sse/ServerSentEventProxyHandler.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java index ae33436bfe..cc6c3c369d 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/sse/ServerSentEventProxyHandler.java @@ -16,6 +16,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; @@ -64,6 +65,8 @@ @Component("ServerSentEventProxyHandler") public class ServerSentEventProxyHandler implements RoutedServicesUser { + private static final String N_DATA = "\ndata:"; + @Value("${server.ssl.trustStore:#{null}}") private String trustStore; @@ -162,6 +165,7 @@ boolean hasEnter(ServerSentEvent event) { hasEnter(event.id()); } + @SuppressWarnings("squid:S4449") ServerSentEvent sanitize(ServerSentEvent event) { if (!hasEnter(event)) { return event; @@ -172,13 +176,13 @@ ServerSentEvent sanitize(ServerSentEvent event) { String data = event.data(); if (hasEnter(data)) { - data = data.replaceAll("\r\n", "\n"); - data = data.replaceAll("\n", "\ndata:"); + data = data.replace("\r\n", "\n"); + data = data.replace("\n", N_DATA); } String comment = event.comment(); if (hasEnter(comment)) { - comment = comment.replaceAll("\n", "\n:"); + comment = comment.replace("\n", "\n:"); } return ServerSentEvent.builder() @@ -291,7 +295,7 @@ public SseEventBuilderFixedImpl reconnectTime(long reconnectTimeMillis) { @Override public SseEventBuilderFixedImpl comment(String comment) { - append(':').append(StringUtils.replace(comment, "\n", "\n:")).append('\n'); + append(':').append(Strings.CS.replace(comment, "\n", "\n:")).append('\n'); return this; } @@ -335,10 +339,10 @@ private void writeStringData(String input, @Nullable MediaType mediaType) { if (i + 1 < length && input.charAt(i + 1) == '\n') { i++; } - this.sb.append("\ndata:"); + this.sb.append(N_DATA); } else if (c == '\n') { - this.sb.append("\ndata:"); + this.sb.append(N_DATA); } else { this.sb.append(c);