diff --git a/src/main/java/io/socket/engineio/client/transports/Polling.java b/src/main/java/io/socket/engineio/client/transports/Polling.java index b6d577b..a055357 100644 --- a/src/main/java/io/socket/engineio/client/transports/Polling.java +++ b/src/main/java/io/socket/engineio/client/transports/Polling.java @@ -183,10 +183,16 @@ abstract public class Polling extends Transport { } }; - Parser.encodePayload(packets, new Parser.EncodeCallback() { + Parser.encodePayload(packets, new Parser.EncodeCallback() { @Override - public void call(byte[] data) { - self.doWrite(data, callbackfn); + public void call(Object data) { + if (data instanceof byte[]) { + self.doWrite((byte[])data, callbackfn); + } else if (data instanceof String) { + self.doWrite((String)data, callbackfn); + } else { + logger.warning("Unexpected data: " + data); + } } }); } @@ -220,5 +226,7 @@ abstract public class Polling extends Transport { abstract protected void doWrite(byte[] data, Runnable fn); + abstract protected void doWrite(String data, Runnable fn); + abstract protected void doPoll(); } diff --git a/src/main/java/io/socket/engineio/client/transports/PollingXHR.java b/src/main/java/io/socket/engineio/client/transports/PollingXHR.java index d0e2675..ed118eb 100644 --- a/src/main/java/io/socket/engineio/client/transports/PollingXHR.java +++ b/src/main/java/io/socket/engineio/client/transports/PollingXHR.java @@ -8,6 +8,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeMap; +import java.util.logging.Level; import java.util.logging.Logger; import io.socket.emitter.Emitter; @@ -26,6 +27,8 @@ public class PollingXHR extends Polling { private static final Logger logger = Logger.getLogger(PollingXHR.class.getName()); + private static boolean LOGGABLE_FINE = logger.isLoggable(Level.FINE); + public PollingXHR(Transport.Options opts) { super(opts); } @@ -66,6 +69,15 @@ public class PollingXHR extends Polling { @Override protected void doWrite(byte[] data, final Runnable fn) { + this.doWrite((Object) data, fn); + } + + @Override + protected void doWrite(String data, final Runnable fn) { + this.doWrite((Object) data, fn); + } + + private void doWrite(Object data, final Runnable fn) { Request.Options opts = new Request.Options(); opts.method = "POST"; opts.data = data; @@ -140,13 +152,17 @@ public class PollingXHR extends Polling { public static final String EVENT_ERROR = "error"; public static final String EVENT_REQUEST_HEADERS = "requestHeaders"; public static final String EVENT_RESPONSE_HEADERS = "responseHeaders"; + private static final String BINARY_CONTENT_TYPE = "application/octet-stream"; + private static final String TEXT_CONTENT_TYPE = "text/plain;charset=UTF-8"; + + private static final MediaType BINARY_MEDIA_TYPE = MediaType.parse(BINARY_CONTENT_TYPE); + private static final MediaType TEXT_MEDIA_TYPE = MediaType.parse(TEXT_CONTENT_TYPE); private String method; private String uri; - // data is always a binary - private byte[] data; + private Object data; private Call.Factory callFactory; private Response response; @@ -161,28 +177,42 @@ public class PollingXHR extends Polling { public void create() { final Request self = this; - logger.fine(String.format("xhr open %s: %s", this.method, this.uri)); + if (LOGGABLE_FINE) logger.fine(String.format("xhr open %s: %s", this.method, this.uri)); Map> headers = new TreeMap>(String.CASE_INSENSITIVE_ORDER); if ("POST".equals(this.method)) { - headers.put("Content-type", new LinkedList(Collections.singletonList(BINARY_CONTENT_TYPE))); + if (this.data instanceof byte[]) { + headers.put("Content-type", new LinkedList(Collections.singletonList(BINARY_CONTENT_TYPE))); + } else { + headers.put("Content-type", new LinkedList(Collections.singletonList(TEXT_CONTENT_TYPE))); + } } headers.put("Accept", new LinkedList(Collections.singletonList("*/*"))); - self.onRequestHeaders(headers); + this.onRequestHeaders(headers); + + if (LOGGABLE_FINE) { + logger.fine(String.format("sending xhr with url %s | data %s", this.uri, + this.data instanceof byte[] ? Arrays.toString((byte[]) this.data) : this.data)); + } - logger.fine(String.format("sending xhr with url %s | data %s", this.uri, Arrays.toString(this.data))); okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder(); for (Map.Entry> header : headers.entrySet()) { for (String v : header.getValue()){ requestBuilder.addHeader(header.getKey(), v); } } + RequestBody body = null; + if (this.data instanceof byte[]) { + body = RequestBody.create(BINARY_MEDIA_TYPE, (byte[])this.data); + } else if (this.data instanceof String) { + body = RequestBody.create(TEXT_MEDIA_TYPE, (String)this.data); + } + okhttp3.Request request = requestBuilder .url(HttpUrl.parse(self.uri)) - .method(self.method, (self.data != null) ? - RequestBody.create(MediaType.parse(BINARY_CONTENT_TYPE), self.data) : null) + .method(self.method, body) .build(); requestCall = callFactory.newCall(request); @@ -255,7 +285,7 @@ public class PollingXHR extends Polling { public String uri; public String method; - public byte[] data; + public Object data; public Call.Factory callFactory; } } diff --git a/src/main/java/io/socket/engineio/parser/Parser.java b/src/main/java/io/socket/engineio/parser/Parser.java index c34dce5..fe6c362 100644 --- a/src/main/java/io/socket/engineio/parser/Parser.java +++ b/src/main/java/io/socket/engineio/parser/Parser.java @@ -35,6 +35,11 @@ public class Parser { private static Packet err = new Packet(Packet.ERROR, "parser error"); + private static UTF8.Options utf8Options = new UTF8.Options(); + static { + utf8Options.strict = false; + } + private Parser() {} @@ -55,7 +60,7 @@ public class Parser { String encoded = String.valueOf(packets.get(packet.type)); if (null != packet.data) { - encoded += utf8encode ? UTF8.encode(String.valueOf(packet.data)) : String.valueOf(packet.data); + encoded += utf8encode ? UTF8.encode(String.valueOf(packet.data), utf8Options) : String.valueOf(packet.data); } @SuppressWarnings("unchecked") @@ -89,7 +94,7 @@ public class Parser { if (utf8decode) { try { - data = UTF8.decode(data); + data = UTF8.decode(data, utf8Options); } catch (UTF8Exception e) { return err; } @@ -113,7 +118,40 @@ public class Parser { return new Packet(packetslist.get(type), intArray); } - public static void encodePayload(Packet[] packets, EncodeCallback callback) throws UTF8Exception { + public static void encodePayload(Packet[] packets, EncodeCallback callback) throws UTF8Exception { + for (Packet packet : packets) { + if (packet.data instanceof byte[]) { + @SuppressWarnings("unchecked") + EncodeCallback _callback = (EncodeCallback) callback; + encodePayloadAsBinary(packets, _callback); + return; + } + } + + if (packets.length == 0) { + callback.call("0:"); + return; + } + + final StringBuilder result = new StringBuilder(); + + for (Packet packet : packets) { + encodePacket(packet, false, new EncodeCallback() { + @Override + public void call(Object message) { + result.append(setLengthHeader((String)message)); + } + }); + } + + callback.call(result.toString()); + } + + private static String setLengthHeader(String message) { + return message.length() + ":" + message; + } + + private static void encodePayloadAsBinary(Packet[] packets, EncodeCallback callback) throws UTF8Exception { if (packets.length == 0) { callback.call(new byte[0]); return; @@ -122,30 +160,10 @@ public class Parser { final ArrayList results = new ArrayList(packets.length); for (Packet packet : packets) { - encodePacket(packet, true, new EncodeCallback() { + encodeOneBinaryPacket(packet, new EncodeCallback() { @Override - public void call(Object packet) { - if (packet instanceof String) { - String encodingLength = String.valueOf(((String) packet).length()); - byte[] sizeBuffer = new byte[encodingLength.length() + 2]; - - sizeBuffer[0] = (byte)0; // is a string - for (int i = 0; i < encodingLength.length(); i ++) { - sizeBuffer[i + 1] = (byte)Character.getNumericValue(encodingLength.charAt(i)); - } - sizeBuffer[sizeBuffer.length - 1] = (byte)255; - results.add(Buffer.concat(new byte[][] {sizeBuffer, stringToByteArray((String)packet)})); - return; - } - - String encodingLength = String.valueOf(((byte[])packet).length); - byte[] sizeBuffer = new byte[encodingLength.length() + 2]; - sizeBuffer[0] = (byte)1; // is binary - for (int i = 0; i < encodingLength.length(); i ++) { - sizeBuffer[i + 1] = (byte)Character.getNumericValue(encodingLength.charAt(i)); - } - sizeBuffer[sizeBuffer.length - 1] = (byte)255; - results.add(Buffer.concat(new byte[][] {sizeBuffer, (byte[])packet})); + public void call(byte[] data) { + results.add(data); } }); } @@ -153,6 +171,35 @@ public class Parser { callback.call(Buffer.concat(results.toArray(new byte[results.size()][]))); } + private static void encodeOneBinaryPacket(Packet p, final EncodeCallback doneCallback) throws UTF8Exception { + encodePacket(p, true, new EncodeCallback() { + @Override + public void call(Object packet) { + if (packet instanceof String) { + String encodingLength = String.valueOf(((String) packet).length()); + byte[] sizeBuffer = new byte[encodingLength.length() + 2]; + + sizeBuffer[0] = (byte)0; // is a string + for (int i = 0; i < encodingLength.length(); i ++) { + sizeBuffer[i + 1] = (byte)Character.getNumericValue(encodingLength.charAt(i)); + } + sizeBuffer[sizeBuffer.length - 1] = (byte)255; + doneCallback.call(Buffer.concat(new byte[][] {sizeBuffer, stringToByteArray((String)packet)})); + return; + } + + String encodingLength = String.valueOf(((byte[])packet).length); + byte[] sizeBuffer = new byte[encodingLength.length() + 2]; + sizeBuffer[0] = (byte)1; // is binary + for (int i = 0; i < encodingLength.length(); i ++) { + sizeBuffer[i + 1] = (byte)Character.getNumericValue(encodingLength.charAt(i)); + } + sizeBuffer[sizeBuffer.length - 1] = (byte)255; + doneCallback.call(Buffer.concat(new byte[][] {sizeBuffer, (byte[])packet})); + } + }); + } + public static void decodePayload(String data, DecodePayloadCallback callback) { if (data == null || data.length() == 0) { callback.call(err, 0, 1); @@ -165,37 +212,40 @@ public class Parser { if (':' != chr) { length.append(chr); - } else { - int n; - try { - n = Integer.parseInt(length.toString()); - } catch (NumberFormatException e) { - callback.call(err, 0, 1); - return; - } - - String msg; - try { - msg = data.substring(i + 1, i + 1 + n); - } catch (IndexOutOfBoundsException e) { - callback.call(err, 0, 1); - return; - } - - if (msg.length() != 0) { - Packet packet = decodePacket(msg, true); - if (err.type.equals(packet.type) && err.data.equals(packet.data)) { - callback.call(err, 0, 1); - return; - } - - boolean ret = callback.call(packet, i + n, l); - if (!ret) return; - } - - i += n; - length = new StringBuilder(); + continue; } + + int n; + try { + n = Integer.parseInt(length.toString()); + } catch (NumberFormatException e) { + callback.call(err, 0, 1); + return; + } + + String msg; + try { + msg = data.substring(i + 1, i + 1 + n); + } catch (IndexOutOfBoundsException e) { + callback.call(err, 0, 1); + return; + } + + if (msg.length() != 0) { + Packet packet = decodePacket(msg, false); + if (err.type.equals(packet.type) && err.data.equals(packet.data)) { + callback.call(err, 0, 1); + return; + } + + boolean ret = callback.call(packet, i + n, l); + if (!ret) { + return; + } + } + + i += n; + length = new StringBuilder(); } if (length.length() > 0) { @@ -210,23 +260,17 @@ public class Parser { while (bufferTail.capacity() > 0) { StringBuilder strLen = new StringBuilder(); boolean isString = (bufferTail.get(0) & 0xFF) == 0; - boolean numberTooLong = false; for (int i = 1; ; i++) { int b = bufferTail.get(i) & 0xFF; if (b == 255) break; // supports only integer if (strLen.length() > MAX_INT_CHAR_LENGTH) { - numberTooLong = true; - break; + callback.call(err, 0, 1); + return; } strLen.append(b); } - if (numberTooLong) { - @SuppressWarnings("unchecked") - DecodePayloadCallback tempCallback = callback; - tempCallback.call(err, 0, 1); - return; - } + bufferTail.position(strLen.length() + 1); bufferTail = bufferTail.slice(); diff --git a/src/test/java/io/socket/engineio/parser/ParserTest.java b/src/test/java/io/socket/engineio/parser/ParserTest.java index 86b7c7f..d8e7692 100644 --- a/src/test/java/io/socket/engineio/parser/ParserTest.java +++ b/src/test/java/io/socket/engineio/parser/ParserTest.java @@ -156,6 +156,19 @@ public class ParserTest { }); } + @Test + public void encodingStringMessageWithLoneSurrogatesReplacedByUFFFD() throws UTF8Exception { + String data = "\uDC00\uD834\uDF06\uDC00 \uD800\uD835\uDF07\uD800"; + encodePacket(new Packet(Packet.MESSAGE, data), true, new EncodeCallback() { + @Override + public void call(String encoded) { + Packet p = decodePacket(encoded, true); + assertThat(p.type, is(Packet.MESSAGE)); + assertThat(p.data, is("\uFFFD\uD834\uDF06\uFFFD \uFFFD\uD835\uDF07\uFFFD")); + } + }); + } + @Test public void decodeEmptyPayload() { Packet p = decodePacket((String)null); @@ -186,20 +199,20 @@ public class ParserTest { @Test public void encodePayloads() throws UTF8Exception { - encodePayload(new Packet[]{new Packet(Packet.PING), new Packet(Packet.PONG)}, new EncodeCallback() { + encodePayload(new Packet[]{new Packet(Packet.PING), new Packet(Packet.PONG)}, new EncodeCallback() { @Override - public void call(byte[] data) { - assertThat(data, isA(byte[].class)); + public void call(String data) { + assertThat(data, isA(String.class)); } }); } @Test public void encodeAndDecodePayloads() throws UTF8Exception { - encodePayload(new Packet[] {new Packet(Packet.MESSAGE, "a")}, new EncodeCallback() { + encodePayload(new Packet[] {new Packet(Packet.MESSAGE, "a")}, new EncodeCallback() { @Override - public void call(byte[] data) { - decodePayload(data, new DecodePayloadCallback() { + public void call(String data) { + decodePayload(data, new DecodePayloadCallback() { @Override public boolean call(Packet packet, int index, int total) { boolean isLast = index + 1 == total; @@ -209,10 +222,10 @@ public class ParserTest { }); } }); - encodePayload(new Packet[]{new Packet(Packet.MESSAGE, "a"), new Packet(Packet.PING)}, new EncodeCallback() { + encodePayload(new Packet[]{new Packet(Packet.MESSAGE, "a"), new Packet(Packet.PING)}, new EncodeCallback() { @Override - public void call(byte[] data) { - decodePayload(data, new DecodePayloadCallback() { + public void call(String data) { + decodePayload(data, new DecodePayloadCallback() { @Override public boolean call(Packet packet, int index, int total) { boolean isLast = index + 1 == total; @@ -230,10 +243,10 @@ public class ParserTest { @Test public void encodeAndDecodeEmptyPayloads() throws UTF8Exception { - encodePayload(new Packet[] {}, new EncodeCallback() { + encodePayload(new Packet[] {}, new EncodeCallback() { @Override - public void call(byte[] data) { - decodePayload(data, new DecodePayloadCallback() { + public void call(String data) { + decodePayload(data, new DecodePayloadCallback() { @Override public boolean call(Packet packet, int index, int total) { assertThat(packet.type, is(Packet.OPEN)); @@ -246,6 +259,19 @@ public class ParserTest { }); } + @Test + public void notUTF8EncodeWhenDealingWithStringsOnly() throws UTF8Exception { + encodePayload(new Packet[] { + new Packet(Packet.MESSAGE, "€€€"), + new Packet(Packet.MESSAGE, "α") + }, new EncodeCallback() { + @Override + public void call(String data) { + assertThat(data, is("4:4€€€2:4α")); + } + }); + } + @Test public void decodePayloadBadFormat() { decodePayload("1!", new DecodePayloadCallback() { @@ -328,20 +354,6 @@ public class ParserTest { }); } - @Test - public void decodePayloadInvalidUTF8() { - decodePayload("2:4\uffff", new DecodePayloadCallback() { - @Override - public boolean call(Packet packet, int index, int total) { - boolean isLast = index + 1 == total; - assertThat(packet.type, is(Packet.ERROR)); - assertThat(packet.data, is(ERROR_DATA)); - assertThat(isLast, is(true)); - return true; - } - }); - } - @Test public void encodeBinaryMessage() throws UTF8Exception { final byte[] data = new byte[5];