feat: add support for Engine.IO v4

Reference: https://github.com/socketio/engine.io-protocol#difference-between-v3-and-v4
This commit is contained in:
Damien Arrachequesne
2020-12-09 01:57:52 +01:00
parent b1b7002691
commit 41f89a38b7
14 changed files with 829 additions and 864 deletions

View File

@@ -128,7 +128,6 @@ public class Socket extends Emitter {
/*package*/ LinkedList<Packet> writeBuffer = new LinkedList<Packet>();
/*package*/ Transport transport;
private Future pingTimeoutTimer;
private Future pingIntervalTimer;
private okhttp3.WebSocket.Factory webSocketFactory;
private okhttp3.Call.Factory callFactory;
@@ -137,7 +136,7 @@ public class Socket extends Emitter {
private final Listener onHeartbeatAsListener = new Listener() {
@Override
public void call(Object... args) {
Socket.this.onHeartbeat(args.length > 0 ? (Long)args[0]: 0);
Socket.this.onHeartbeat();
}
};
@@ -540,9 +539,14 @@ public class Socket extends Emitter {
} catch (JSONException e) {
this.emit(EVENT_ERROR, new EngineIOException(e));
}
} else if (Packet.PONG.equals(packet.type)) {
this.setPing();
this.emit(EVENT_PONG);
} else if (Packet.PING.equals(packet.type)) {
this.emit(EVENT_PING);
EventThread.exec(new Runnable() {
@Override
public void run() {
Socket.this.sendPacket(Packet.PONG, null);
}
});
} else if (Packet.ERROR.equals(packet.type)) {
EngineIOException err = new EngineIOException("server error");
err.code = packet.data;
@@ -568,20 +572,18 @@ public class Socket extends Emitter {
this.onOpen();
// In case open handler closes socket
if (ReadyState.CLOSED == this.readyState) return;
this.setPing();
this.onHeartbeat();
this.off(EVENT_HEARTBEAT, this.onHeartbeatAsListener);
this.on(EVENT_HEARTBEAT, this.onHeartbeatAsListener);
}
private void onHeartbeat(long timeout) {
private void onHeartbeat() {
if (this.pingTimeoutTimer != null) {
pingTimeoutTimer.cancel(false);
}
if (timeout <= 0) {
timeout = this.pingInterval + this.pingTimeout;
}
long timeout = this.pingInterval + this.pingTimeout;
final Socket self = this;
this.pingTimeoutTimer = this.getHeartbeatScheduler().schedule(new Runnable() {
@@ -598,46 +600,6 @@ public class Socket extends Emitter {
}, timeout, TimeUnit.MILLISECONDS);
}
private void setPing() {
if (this.pingIntervalTimer != null) {
pingIntervalTimer.cancel(false);
}
final Socket self = this;
this.pingIntervalTimer = this.getHeartbeatScheduler().schedule(new Runnable() {
@Override
public void run() {
EventThread.exec(new Runnable() {
@Override
public void run() {
if (logger.isLoggable(Level.FINE)) {
logger.fine(String.format("writing ping packet - expecting pong within %sms", self.pingTimeout));
}
self.ping();
self.onHeartbeat(self.pingTimeout);
}
});
}
}, this.pingInterval, TimeUnit.MILLISECONDS);
}
/**
* Sends a ping packet.
*/
private void ping() {
EventThread.exec(new Runnable() {
@Override
public void run() {
Socket.this.sendPacket(Packet.PING, new Runnable() {
@Override
public void run() {
Socket.this.emit(EVENT_PING);
}
});
}
});
}
private void onDrain() {
for (int i = 0; i < this.prevBufferLen; i++) {
this.writeBuffer.poll();
@@ -833,9 +795,6 @@ public class Socket extends Emitter {
final Socket self = this;
// clear timers
if (this.pingIntervalTimer != null) {
this.pingIntervalTimer.cancel(false);
}
if (this.pingTimeoutTimer != null) {
this.pingTimeoutTimer.cancel(false);
}

View File

@@ -7,7 +7,6 @@ import io.socket.emitter.Emitter;
import io.socket.engineio.parser.Packet;
import io.socket.engineio.parser.Parser;
import io.socket.thread.EventThread;
import io.socket.utf8.UTF8Exception;
import okhttp3.Call;
import okhttp3.WebSocket;
@@ -96,11 +95,7 @@ public abstract class Transport extends Emitter {
@Override
public void run() {
if (Transport.this.readyState == ReadyState.OPEN) {
try {
Transport.this.write(packets);
} catch (UTF8Exception err) {
throw new RuntimeException(err);
}
Transport.this.write(packets);
} else {
throw new RuntimeException("Transport not open");
}
@@ -131,7 +126,7 @@ public abstract class Transport extends Emitter {
this.emit(EVENT_CLOSE);
}
abstract protected void write(Packet[] packets) throws UTF8Exception;
abstract protected void write(Packet[] packets);
abstract protected void doOpen();

View File

@@ -7,7 +7,6 @@ import io.socket.engineio.parser.Packet;
import io.socket.engineio.parser.Parser;
import io.socket.parseqs.ParseQS;
import io.socket.thread.EventThread;
import io.socket.utf8.UTF8Exception;
import io.socket.yeast.Yeast;
import java.util.HashMap;
@@ -129,13 +128,7 @@ abstract public class Polling extends Transport {
}
};
if (data instanceof String) {
@SuppressWarnings("unchecked")
Parser.DecodePayloadCallback<String> tempCallback = callback;
Parser.decodePayload((String)data, tempCallback);
} else if (data instanceof byte[]) {
Parser.decodePayload((byte[])data, callback);
}
Parser.decodePayload((String) data, callback);
if (this.readyState != ReadyState.CLOSED) {
this.polling = false;
@@ -158,11 +151,7 @@ abstract public class Polling extends Transport {
@Override
public void call(Object... args) {
logger.fine("writing close packet");
try {
self.write(new Packet[]{new Packet(Packet.CLOSE)});
} catch (UTF8Exception err) {
throw new RuntimeException(err);
}
self.write(new Packet[]{new Packet(Packet.CLOSE)});
}
};
@@ -177,7 +166,7 @@ abstract public class Polling extends Transport {
}
}
protected void write(Packet[] packets) throws UTF8Exception {
protected void write(Packet[] packets) {
final Polling self = this;
this.writable = false;
final Runnable callbackfn = new Runnable() {
@@ -188,16 +177,10 @@ abstract public class Polling extends Transport {
}
};
Parser.encodePayload(packets, new Parser.EncodeCallback() {
Parser.encodePayload(packets, new Parser.EncodeCallback<String>() {
@Override
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);
}
public void call(String data) {
self.doWrite(data, callbackfn);
}
});
}
@@ -229,8 +212,6 @@ abstract public class Polling extends Transport {
return schema + "://" + (ipv6 ? "[" + this.hostname + "]" : this.hostname) + port + this.path + derivedQuery;
}
abstract protected void doWrite(byte[] data, Runnable fn);
abstract protected void doWrite(String data, Runnable fn);
abstract protected void doPoll();

View File

@@ -67,17 +67,8 @@ public class PollingXHR extends Polling {
return req;
}
@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;
@@ -121,11 +112,7 @@ public class PollingXHR extends Polling {
@Override
public void run() {
Object arg = args.length > 0 ? args[0] : null;
if (arg instanceof String) {
self.onData((String)arg);
} else if (arg instanceof byte[]) {
self.onData((byte[])arg);
}
self.onData((String)arg);
}
});
}
@@ -153,16 +140,14 @@ public class PollingXHR extends Polling {
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;
private Object data;
private String data;
private Call.Factory callFactory;
private Response response;
@@ -181,11 +166,7 @@ public class PollingXHR extends Polling {
Map<String, List<String>> headers = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
if ("POST".equals(this.method)) {
if (this.data instanceof byte[]) {
headers.put("Content-type", new LinkedList<String>(Collections.singletonList(BINARY_CONTENT_TYPE)));
} else {
headers.put("Content-type", new LinkedList<String>(Collections.singletonList(TEXT_CONTENT_TYPE)));
}
headers.put("Content-type", new LinkedList<String>(Collections.singletonList(TEXT_CONTENT_TYPE)));
}
headers.put("Accept", new LinkedList<String>(Collections.singletonList("*/*")));
@@ -193,8 +174,7 @@ public class PollingXHR extends Polling {
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, this.data));
}
okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder();
@@ -204,10 +184,8 @@ public class PollingXHR extends Polling {
}
}
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);
if (this.data != null) {
body = RequestBody.create(TEXT_MEDIA_TYPE, this.data);
}
okhttp3.Request request = requestBuilder
@@ -249,11 +227,6 @@ public class PollingXHR extends Polling {
this.onSuccess();
}
private void onData(byte[] data) {
this.emit(EVENT_DATA, data);
this.onSuccess();
}
private void onError(Exception err) {
this.emit(EVENT_ERROR, err);
}
@@ -268,14 +241,9 @@ public class PollingXHR extends Polling {
private void onLoad() {
ResponseBody body = response.body();
MediaType mediaType = body.contentType();
try {
if (mediaType != null && BINARY_CONTENT_TYPE.equalsIgnoreCase(mediaType.toString())) {
this.onData(body.bytes());
} else {
this.onData(body.string());
}
this.onData(body.string());
} catch (IOException e) {
this.onError(e);
}
@@ -285,7 +253,7 @@ public class PollingXHR extends Polling {
public String uri;
public String method;
public Object data;
public String data;
public Call.Factory callFactory;
}
}

View File

@@ -1,18 +1,11 @@
package io.socket.engineio.client.transports;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;
import io.socket.engineio.client.Transport;
import io.socket.engineio.parser.Packet;
import io.socket.engineio.parser.Parser;
import io.socket.parseqs.ParseQS;
import io.socket.thread.EventThread;
import io.socket.utf8.UTF8Exception;
import io.socket.yeast.Yeast;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -20,6 +13,12 @@ import okhttp3.Response;
import okhttp3.WebSocketListener;
import okio.ByteString;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;
public class WebSocket extends Transport {
@@ -111,7 +110,7 @@ public class WebSocket extends Transport {
});
}
protected void write(Packet[] packets) throws UTF8Exception {
protected void write(Packet[] packets) {
final WebSocket self = this;
this.writable = false;

View File

@@ -0,0 +1,665 @@
// from https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/util/Base64.java
package io.socket.engineio.parser;
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.io.UnsupportedEncodingException;
/**
* Utilities for encoding and decoding the Base64 representation of
* binary data. See RFCs <a
* href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
* href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
*/
public class Base64 {
/**
* Default values for encoder/decoder flags.
*/
public static final int DEFAULT = 0;
/**
* Encoder flag bit to omit the padding '=' characters at the end
* of the output (if any).
*/
public static final int NO_PADDING = 1;
/**
* Encoder flag bit to omit all line terminators (i.e., the output
* will be on one long line).
*/
public static final int NO_WRAP = 2;
/**
* Encoder flag bit to indicate lines should be terminated with a
* CRLF pair instead of just an LF. Has no effect if {@code
* NO_WRAP} is specified as well.
*/
public static final int CRLF = 4;
/**
* Encoder/decoder flag bit to indicate using the "URL and
* filename safe" variant of Base64 (see RFC 3548 section 4) where
* {@code -} and {@code _} are used in place of {@code +} and
* {@code /}.
*/
public static final int URL_SAFE = 8;
/**
* Flag to pass to Base64OutputStream to indicate that it
* should not close the output stream it is wrapping when it
* itself is closed.
*/
public static final int NO_CLOSE = 16;
// --------------------------------------------------------
// shared code
// --------------------------------------------------------
/* package */ static abstract class Coder {
public byte[] output;
public int op;
/**
* Encode/decode another block of input data. this.output is
* provided by the caller, and must be big enough to hold all
* the coded data. On exit, this.opwill be set to the length
* of the coded data.
*
* @param finish true if this is the final call to process for
* this object. Will finalize the coder state and
* include any final bytes in the output.
*
* @return true if the input so far is good; false if some
* error has been detected in the input stream..
*/
public abstract boolean process(byte[] input, int offset, int len, boolean finish);
/**
* @return the maximum number of bytes a call to process()
* could produce for the given number of input bytes. This may
* be an overestimate.
*/
public abstract int maxOutputSize(int len);
}
// --------------------------------------------------------
// decoding
// --------------------------------------------------------
/**
* Decode the Base64-encoded data in input and return the data in
* a new byte array.
*
* <p>The padding '=' characters at the end are considered optional, but
* if any are present, there must be the correct number of them.
*
* @param str the input String to decode, which is converted to
* bytes using the default charset
* @param flags controls certain features of the decoded output.
* Pass {@code DEFAULT} to decode standard Base64.
*
* @throws IllegalArgumentException if the input contains
* incorrect padding
*/
public static byte[] decode(String str, int flags) {
return decode(str.getBytes(), flags);
}
/**
* Decode the Base64-encoded data in input and return the data in
* a new byte array.
*
* <p>The padding '=' characters at the end are considered optional, but
* if any are present, there must be the correct number of them.
*
* @param input the input array to decode
* @param flags controls certain features of the decoded output.
* Pass {@code DEFAULT} to decode standard Base64.
*
* @throws IllegalArgumentException if the input contains
* incorrect padding
*/
public static byte[] decode(byte[] input, int flags) {
return decode(input, 0, input.length, flags);
}
/**
* Decode the Base64-encoded data in input and return the data in
* a new byte array.
*
* <p>The padding '=' characters at the end are considered optional, but
* if any are present, there must be the correct number of them.
*
* @param input the data to decode
* @param offset the position within the input array at which to start
* @param len the number of bytes of input to decode
* @param flags controls certain features of the decoded output.
* Pass {@code DEFAULT} to decode standard Base64.
*
* @throws IllegalArgumentException if the input contains
* incorrect padding
*/
public static byte[] decode(byte[] input, int offset, int len, int flags) {
// Allocate space for the most data the input could represent.
// (It could contain less if it contains whitespace, etc.)
Decoder decoder = new Decoder(flags, new byte[len*3/4]);
if (!decoder.process(input, offset, len, true)) {
throw new IllegalArgumentException("bad base-64");
}
// Maybe we got lucky and allocated exactly enough output space.
if (decoder.op == decoder.output.length) {
return decoder.output;
}
// Need to shorten the array, so allocate a new one of the
// right size and copy.
byte[] temp = new byte[decoder.op];
System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
return temp;
}
/* package */ static class Decoder extends Coder {
/**
* Lookup table for turning bytes into their position in the
* Base64 alphabet.
*/
private static final int DECODE[] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
};
/**
* Decode lookup table for the "web safe" variant (RFC 3548
* sec. 4) where - and _ replace + and /.
*/
private static final int DECODE_WEBSAFE[] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
};
/** Non-data values in the DECODE arrays. */
private static final int SKIP = -1;
private static final int EQUALS = -2;
/**
* States 0-3 are reading through the next input tuple.
* State 4 is having read one '=' and expecting exactly
* one more.
* State 5 is expecting no more data or padding characters
* in the input.
* State 6 is the error state; an error has been detected
* in the input and no future input can "fix" it.
*/
private int state; // state number (0 to 6)
private int value;
final private int[] alphabet;
public Decoder(int flags, byte[] output) {
this.output = output;
alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
state = 0;
value = 0;
}
/**
* @return an overestimate for the number of bytes {@code
* len} bytes could decode to.
*/
public int maxOutputSize(int len) {
return len * 3/4 + 10;
}
/**
* Decode another block of input data.
*
* @return true if the state machine is still healthy. false if
* bad base-64 data has been detected in the input stream.
*/
public boolean process(byte[] input, int offset, int len, boolean finish) {
if (this.state == 6) return false;
int p = offset;
len += offset;
// Using local variables makes the decoder about 12%
// faster than if we manipulate the member variables in
// the loop. (Even alphabet makes a measurable
// difference, which is somewhat surprising to me since
// the member variable is final.)
int state = this.state;
int value = this.value;
int op = 0;
final byte[] output = this.output;
final int[] alphabet = this.alphabet;
while (p < len) {
// Try the fast path: we're starting a new tuple and the
// next four bytes of the input stream are all data
// bytes. This corresponds to going through states
// 0-1-2-3-0. We expect to use this method for most of
// the data.
//
// If any of the next four bytes of input are non-data
// (whitespace, etc.), value will end up negative. (All
// the non-data values in decode are small negative
// numbers, so shifting any of them up and or'ing them
// together will result in a value with its top bit set.)
//
// You can remove this whole block and the output should
// be the same, just slower.
if (state == 0) {
while (p+4 <= len &&
(value = ((alphabet[input[p] & 0xff] << 18) |
(alphabet[input[p+1] & 0xff] << 12) |
(alphabet[input[p+2] & 0xff] << 6) |
(alphabet[input[p+3] & 0xff]))) >= 0) {
output[op+2] = (byte) value;
output[op+1] = (byte) (value >> 8);
output[op] = (byte) (value >> 16);
op += 3;
p += 4;
}
if (p >= len) break;
}
// The fast path isn't available -- either we've read a
// partial tuple, or the next four input bytes aren't all
// data, or whatever. Fall back to the slower state
// machine implementation.
int d = alphabet[input[p++] & 0xff];
switch (state) {
case 0:
if (d >= 0) {
value = d;
++state;
} else if (d != SKIP) {
this.state = 6;
return false;
}
break;
case 1:
if (d >= 0) {
value = (value << 6) | d;
++state;
} else if (d != SKIP) {
this.state = 6;
return false;
}
break;
case 2:
if (d >= 0) {
value = (value << 6) | d;
++state;
} else if (d == EQUALS) {
// Emit the last (partial) output tuple;
// expect exactly one more padding character.
output[op++] = (byte) (value >> 4);
state = 4;
} else if (d != SKIP) {
this.state = 6;
return false;
}
break;
case 3:
if (d >= 0) {
// Emit the output triple and return to state 0.
value = (value << 6) | d;
output[op+2] = (byte) value;
output[op+1] = (byte) (value >> 8);
output[op] = (byte) (value >> 16);
op += 3;
state = 0;
} else if (d == EQUALS) {
// Emit the last (partial) output tuple;
// expect no further data or padding characters.
output[op+1] = (byte) (value >> 2);
output[op] = (byte) (value >> 10);
op += 2;
state = 5;
} else if (d != SKIP) {
this.state = 6;
return false;
}
break;
case 4:
if (d == EQUALS) {
++state;
} else if (d != SKIP) {
this.state = 6;
return false;
}
break;
case 5:
if (d != SKIP) {
this.state = 6;
return false;
}
break;
}
}
if (!finish) {
// We're out of input, but a future call could provide
// more.
this.state = state;
this.value = value;
this.op = op;
return true;
}
// Done reading input. Now figure out where we are left in
// the state machine and finish up.
switch (state) {
case 0:
// Output length is a multiple of three. Fine.
break;
case 1:
// Read one extra input byte, which isn't enough to
// make another output byte. Illegal.
this.state = 6;
return false;
case 2:
// Read two extra input bytes, enough to emit 1 more
// output byte. Fine.
output[op++] = (byte) (value >> 4);
break;
case 3:
// Read three extra input bytes, enough to emit 2 more
// output bytes. Fine.
output[op++] = (byte) (value >> 10);
output[op++] = (byte) (value >> 2);
break;
case 4:
// Read one padding '=' when we expected 2. Illegal.
this.state = 6;
return false;
case 5:
// Read all the padding '='s we expected and no more.
// Fine.
break;
}
this.state = state;
this.op = op;
return true;
}
}
// --------------------------------------------------------
// encoding
// --------------------------------------------------------
/**
* Base64-encode the given data and return a newly allocated
* String with the result.
*
* @param input the data to encode
* @param flags controls certain features of the encoded output.
* Passing {@code DEFAULT} results in output that
* adheres to RFC 2045.
*/
public static String encodeToString(byte[] input, int flags) {
try {
return new String(encode(input, flags), "US-ASCII");
} catch (UnsupportedEncodingException e) {
// US-ASCII is guaranteed to be available.
throw new AssertionError(e);
}
}
/**
* Base64-encode the given data and return a newly allocated
* String with the result.
*
* @param input the data to encode
* @param offset the position within the input array at which to
* start
* @param len the number of bytes of input to encode
* @param flags controls certain features of the encoded output.
* Passing {@code DEFAULT} results in output that
* adheres to RFC 2045.
*/
public static String encodeToString(byte[] input, int offset, int len, int flags) {
try {
return new String(encode(input, offset, len, flags), "US-ASCII");
} catch (UnsupportedEncodingException e) {
// US-ASCII is guaranteed to be available.
throw new AssertionError(e);
}
}
/**
* Base64-encode the given data and return a newly allocated
* byte[] with the result.
*
* @param input the data to encode
* @param flags controls certain features of the encoded output.
* Passing {@code DEFAULT} results in output that
* adheres to RFC 2045.
*/
public static byte[] encode(byte[] input, int flags) {
return encode(input, 0, input.length, flags);
}
/**
* Base64-encode the given data and return a newly allocated
* byte[] with the result.
*
* @param input the data to encode
* @param offset the position within the input array at which to
* start
* @param len the number of bytes of input to encode
* @param flags controls certain features of the encoded output.
* Passing {@code DEFAULT} results in output that
* adheres to RFC 2045.
*/
public static byte[] encode(byte[] input, int offset, int len, int flags) {
Encoder encoder = new Encoder(flags, null);
// Compute the exact length of the array we will produce.
int output_len = len / 3 * 4;
// Account for the tail of the data and the padding bytes, if any.
if (encoder.do_padding) {
if (len % 3 > 0) {
output_len += 4;
}
} else {
switch (len % 3) {
case 0: break;
case 1: output_len += 2; break;
case 2: output_len += 3; break;
}
}
// Account for the newlines, if any.
if (encoder.do_newline && len > 0) {
output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) *
(encoder.do_cr ? 2 : 1);
}
encoder.output = new byte[output_len];
encoder.process(input, offset, len, true);
assert encoder.op == output_len;
return encoder.output;
}
/* package */ static class Encoder extends Coder {
/**
* Emit a new line every this many output tuples. Corresponds to
* a 76-character line length (the maximum allowable according to
* <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>).
*/
public static final int LINE_GROUPS = 19;
/**
* Lookup table for turning Base64 alphabet positions (6 bits)
* into output bytes.
*/
private static final byte ENCODE[] = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
};
/**
* Lookup table for turning Base64 alphabet positions (6 bits)
* into output bytes.
*/
private static final byte ENCODE_WEBSAFE[] = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
};
final private byte[] tail;
/* package */ int tailLen;
private int count;
final public boolean do_padding;
final public boolean do_newline;
final public boolean do_cr;
final private byte[] alphabet;
public Encoder(int flags, byte[] output) {
this.output = output;
do_padding = (flags & NO_PADDING) == 0;
do_newline = (flags & NO_WRAP) == 0;
do_cr = (flags & CRLF) != 0;
alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
tail = new byte[2];
tailLen = 0;
count = do_newline ? LINE_GROUPS : -1;
}
/**
* @return an overestimate for the number of bytes {@code
* len} bytes could encode to.
*/
public int maxOutputSize(int len) {
return len * 8/5 + 10;
}
public boolean process(byte[] input, int offset, int len, boolean finish) {
// Using local variables makes the encoder about 9% faster.
final byte[] alphabet = this.alphabet;
final byte[] output = this.output;
int op = 0;
int count = this.count;
int p = offset;
len += offset;
int v = -1;
// First we need to concatenate the tail of the previous call
// with any input bytes available now and see if we can empty
// the tail.
switch (tailLen) {
case 0:
// There was no tail.
break;
case 1:
if (p+2 <= len) {
// A 1-byte tail with at least 2 bytes of
// input available now.
v = ((tail[0] & 0xff) << 16) |
((input[p++] & 0xff) << 8) |
(input[p++] & 0xff);
tailLen = 0;
};
break;
case 2:
if (p+1 <= len) {
// A 2-byte tail with at least 1 byte of input.
v = ((tail[0] & 0xff) << 16) |
((tail[1] & 0xff) << 8) |
(input[p++] & 0xff);
tailLen = 0;
}
break;
}
if (v != -1) {
output[op++] = alphabet[(v >> 18) & 0x3f];
output[op++] = alphabet[(v >> 12) & 0x3f];
output[op++] = alphabet[(v >> 6) & 0x3f];
output[op++] = alphabet[v & 0x3f];
if (--count == 0) {
if (do_cr) output[op++] = '\r';
output[op++] = '\n';
count = LINE_GROUPS;
}
}
// At this point either there is no tail, or there are fewer
// than 3 bytes of input available.
// The main loop, turning 3 input bytes into 4 output bytes on
// each iteration.
while (p+3 <= len) {
v = ((input[p] & 0xff) << 16) |
((input[p+1] & 0xff) << 8) |
(input[p+2] & 0xff);
output[op] = alphabet[(v >> 18) & 0x3f];
output[op+1] = alphabet[(v >> 12) & 0x3f];
output[op+2] = alphabet[(v >> 6) & 0x3f];
output[op+3] = alphabet[v & 0x3f];
p += 3;
op += 4;
if (--count == 0) {
if (do_cr) output[op++] = '\r';
output[op++] = '\n';
count = LINE_GROUPS;
}
}
if (finish) {
// Finish up the tail of the input. Note that we need to
// consume any bytes in tail before any bytes
// remaining in input; there should be at most two bytes
// total.
if (p-tailLen == len-1) {
int t = 0;
v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
tailLen -= t;
output[op++] = alphabet[(v >> 6) & 0x3f];
output[op++] = alphabet[v & 0x3f];
if (do_padding) {
output[op++] = '=';
output[op++] = '=';
}
if (do_newline) {
if (do_cr) output[op++] = '\r';
output[op++] = '\n';
}
} else if (p-tailLen == len-2) {
int t = 0;
v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
(((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
tailLen -= t;
output[op++] = alphabet[(v >> 12) & 0x3f];
output[op++] = alphabet[(v >> 6) & 0x3f];
output[op++] = alphabet[v & 0x3f];
if (do_padding) {
output[op++] = '=';
}
if (do_newline) {
if (do_cr) output[op++] = '\r';
output[op++] = '\n';
}
} else if (do_newline && op > 0 && count != LINE_GROUPS) {
if (do_cr) output[op++] = '\r';
output[op++] = '\n';
}
assert tailLen == 0;
assert p == len;
} else {
// Save the leftovers in tail to be consumed on the next
// call to encodeInternal.
if (p == len-1) {
tail[tailLen++] = input[p];
} else if (p == len-2) {
tail[tailLen++] = input[p];
tail[tailLen++] = input[p+1];
}
}
this.op = op;
this.count = count;
return true;
}
}
private Base64() { } // don't instantiate
}

View File

@@ -1,20 +1,13 @@
package io.socket.engineio.parser;
import io.socket.utf8.UTF8;
import io.socket.utf8.UTF8Exception;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Parser {
private static final int MAX_INT_CHAR_LENGTH = String.valueOf(Integer.MAX_VALUE).length();
public static final int PROTOCOL = 4;
public static final int PROTOCOL = 3;
private static final char SEPARATOR = '\u001e';
private static final Map<String, Integer> packets = new HashMap<String, Integer>() {{
put(Packet.OPEN, 0);
@@ -26,61 +19,38 @@ public class Parser {
put(Packet.NOOP, 6);
}};
private static final Map<Integer, String> packetslist = new HashMap<Integer, String>();
private static final Map<Integer, String> packetslist = new HashMap<>();
static {
for (Map.Entry<String, Integer> entry : packets.entrySet()) {
packetslist.put(entry.getValue(), entry.getKey());
}
}
private static Packet<String> err = new Packet<String>(Packet.ERROR, "parser error");
private static UTF8.Options utf8Options = new UTF8.Options();
static {
utf8Options.strict = false;
}
private static final Packet<String> err = new Packet<String>(Packet.ERROR, "parser error");
private Parser() {}
public static void encodePacket(Packet packet, EncodeCallback callback) throws UTF8Exception {
encodePacket(packet, false, callback);
}
public static void encodePacket(Packet packet, boolean utf8encode, EncodeCallback callback) throws UTF8Exception {
public static void encodePacket(Packet packet, EncodeCallback callback) {
if (packet.data instanceof byte[]) {
@SuppressWarnings("unchecked")
Packet<byte[]> packetToEncode = packet;
@SuppressWarnings("unchecked")
EncodeCallback<byte[]> callbackToEncode = callback;
encodeByteArray(packetToEncode, callbackToEncode);
return;
((EncodeCallback<byte[]>) callback).call(((Packet<byte[]>) packet).data);
} else {
String type = String.valueOf(packets.get(packet.type));
String content = packet.data != null ? String.valueOf(packet.data) : "";
((EncodeCallback<String>) callback).call(type + content);
}
String encoded = String.valueOf(packets.get(packet.type));
if (null != packet.data) {
encoded += utf8encode ? UTF8.encode(String.valueOf(packet.data), utf8Options) : String.valueOf(packet.data);
}
@SuppressWarnings("unchecked")
EncodeCallback<String> tempCallback = callback;
tempCallback.call(encoded);
}
private static void encodeByteArray(Packet<byte[]> packet, EncodeCallback<byte[]> callback) {
byte[] data = packet.data;
byte[] resultArray = new byte[1 + data.length];
resultArray[0] = packets.get(packet.type).byteValue();
System.arraycopy(data, 0, resultArray, 1, data.length);
callback.call(resultArray);
private static void encodePacketAsBase64(Packet packet, EncodeCallback<String> callback) {
if (packet.data instanceof byte[]) {
byte[] data = ((Packet<byte[]>) packet).data;
String value = "b" + Base64.encodeToString(data, Base64.DEFAULT);
callback.call(value);
} else {
encodePacket(packet, callback);
}
}
public static Packet<String> decodePacket(String data) {
return decodePacket(data, false);
}
public static Packet<String> decodePacket(String data, boolean utf8decode) {
if (data == null) {
return err;
}
@@ -92,14 +62,6 @@ public class Parser {
type = -1;
}
if (utf8decode) {
try {
data = UTF8.decode(data, utf8Options);
} catch (UTF8Exception e) {
return err;
}
}
if (type < 0 || type >= packetslist.size()) {
return err;
}
@@ -111,23 +73,23 @@ public class Parser {
}
}
public static Packet<byte[]> decodePacket(byte[] data) {
int type = data[0];
byte[] intArray = new byte[data.length - 1];
System.arraycopy(data, 1, intArray, 0, intArray.length);
return new Packet<byte[]>(packetslist.get(type), intArray);
}
public static void encodePayload(Packet[] packets, EncodeCallback callback) throws UTF8Exception {
for (Packet packet : packets) {
if (packet.data instanceof byte[]) {
@SuppressWarnings("unchecked")
EncodeCallback<byte[]> _callback = (EncodeCallback<byte[]>) callback;
encodePayloadAsBinary(packets, _callback);
return;
}
public static Packet decodeBase64Packet(String data) {
if (data == null) {
return err;
}
if (data.charAt(0) == 'b') {
return new Packet(Packet.MESSAGE, Base64.decode(data.substring(1), Base64.DEFAULT));
} else {
return decodePacket(data);
}
}
public static Packet<byte[]> decodePacket(byte[] data) {
return new Packet<>(Packet.MESSAGE, data);
}
public static void encodePayload(Packet[] packets, EncodeCallback<String> callback) {
if (packets.length == 0) {
callback.call("0:");
return;
@@ -135,11 +97,15 @@ public class Parser {
final StringBuilder result = new StringBuilder();
for (Packet packet : packets) {
encodePacket(packet, false, new EncodeCallback() {
for (int i = 0, l = packets.length; i < l; i++) {
final boolean isLast = i == l - 1;
encodePacketAsBase64(packets[i], new EncodeCallback<String>() {
@Override
public void call(Object message) {
result.append(setLengthHeader((String)message));
public void call(String message) {
result.append(message);
if (!isLast) {
result.append(SEPARATOR);
}
}
});
}
@@ -147,218 +113,36 @@ public class Parser {
callback.call(result.toString());
}
private static String setLengthHeader(String message) {
return message.length() + ":" + message;
}
private static void encodePayloadAsBinary(Packet[] packets, EncodeCallback<byte[]> callback) throws UTF8Exception {
if (packets.length == 0) {
callback.call(new byte[0]);
return;
}
final ArrayList<byte[]> results = new ArrayList<byte[]>(packets.length);
for (Packet packet : packets) {
encodeOneBinaryPacket(packet, new EncodeCallback<byte[]>() {
@Override
public void call(byte[] data) {
results.add(data);
}
});
}
callback.call(Buffer.concat(results.toArray(new byte[results.size()][])));
}
private static void encodeOneBinaryPacket(Packet p, final EncodeCallback<byte[]> 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<String> callback) {
if (data == null || data.length() == 0) {
callback.call(err, 0, 1);
return;
}
StringBuilder length = new StringBuilder();
for (int i = 0, l = data.length(); i < l; i++) {
char chr = data.charAt(i);
String[] messages = data.split(String.valueOf(SEPARATOR));
if (':' != chr) {
length.append(chr);
continue;
}
int n;
try {
n = Integer.parseInt(length.toString());
} catch (NumberFormatException e) {
for (int i = 0, l = messages.length; i < l; i++) {
Packet<String> packet = decodeBase64Packet(messages[i]);
if (err.type.equals(packet.type) && err.data.equals(packet.data)) {
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);
boolean ret = callback.call(packet, i, l);
if (!ret) {
return;
}
if (msg.length() != 0) {
Packet<String> 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) {
callback.call(err, 0, 1);
}
}
public static void decodePayload(byte[] data, DecodePayloadCallback callback) {
ByteBuffer bufferTail = ByteBuffer.wrap(data);
List<Object> buffers = new ArrayList<Object>();
public interface EncodeCallback<T> {
while (bufferTail.capacity() > 0) {
StringBuilder strLen = new StringBuilder();
boolean isString = (bufferTail.get(0) & 0xFF) == 0;
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) {
callback.call(err, 0, 1);
return;
}
strLen.append(b);
}
bufferTail.position(strLen.length() + 1);
bufferTail = bufferTail.slice();
int msgLength = Integer.parseInt(strLen.toString());
bufferTail.position(1);
bufferTail.limit(msgLength + 1);
byte[] msg = new byte[bufferTail.remaining()];
bufferTail.get(msg);
if (isString) {
buffers.add(byteArrayToString(msg));
} else {
buffers.add(msg);
}
bufferTail.clear();
bufferTail.position(msgLength + 1);
bufferTail = bufferTail.slice();
}
int total = buffers.size();
for (int i = 0; i < total; i++) {
Object buffer = buffers.get(i);
if (buffer instanceof String) {
@SuppressWarnings("unchecked")
DecodePayloadCallback<String> tempCallback = callback;
tempCallback.call(decodePacket((String)buffer, true), i, total);
} else if (buffer instanceof byte[]) {
@SuppressWarnings("unchecked")
DecodePayloadCallback<byte[]> tempCallback = callback;
tempCallback.call(decodePacket((byte[])buffer), i, total);
}
}
}
private static String byteArrayToString(byte[] bytes) {
StringBuilder builder = new StringBuilder();
for (byte b : bytes) {
builder.appendCodePoint(b & 0xFF);
}
return builder.toString();
}
private static byte[] stringToByteArray(String string) {
int len = string.length();
byte[] bytes = new byte[len];
for (int i = 0; i < len; i++) {
bytes[i] = (byte)Character.codePointAt(string, i);
}
return bytes;
}
public static interface EncodeCallback<T> {
public void call(T data);
void call(T data);
}
public static interface DecodePayloadCallback<T> {
public interface DecodePayloadCallback<T> {
public boolean call(Packet<T> packet, int index, int total);
}
}
class Buffer {
private Buffer() {}
public static byte[] concat(byte[][] list) {
int length = 0;
for (byte[] buf : list) {
length += buf.length;
}
return concat(list, length);
}
public static byte[] concat(byte[][] list, int length) {
if (list.length == 0) {
return new byte[0];
} else if (list.length == 1) {
return list[0];
}
ByteBuffer buffer = ByteBuffer.allocate(length);
for (byte[] buf : list) {
buffer.put(buf);
}
return buffer.array();
boolean call(Packet<T> packet, int index, int total);
}
}

View File

@@ -1,199 +0,0 @@
package io.socket.utf8;
import java.util.ArrayList;
import java.util.List;
/**
* UTF-8 encoder/decoder ported from utf8.js.
*
* @see <a href="https://github.com/mathiasbynens/utf8.js">https://github.com/mathiasbynens/utf8.js</a>
*/
public final class UTF8 {
private static final String INVALID_CONTINUATION_BYTE = "Invalid continuation byte";
private static int[] byteArray;
private static int byteCount;
private static int byteIndex;
private UTF8 () {}
public static String encode(String string) throws UTF8Exception {
return encode(string, new Options());
}
public static String encode(String string, Options opts) throws UTF8Exception {
boolean strict = opts.strict;
int[] codePoints = ucs2decode(string);
int length = codePoints.length;
int index = -1;
int codePoint;
StringBuilder byteString = new StringBuilder();
while (++index < length) {
codePoint = codePoints[index];
byteString.append(encodeCodePoint(codePoint, strict));
}
return byteString.toString();
}
public static String decode(String byteString) throws UTF8Exception {
return decode(byteString, new Options());
}
public static String decode(String byteString, Options opts) throws UTF8Exception {
boolean strict = opts.strict;
byteArray = ucs2decode(byteString);
byteCount = byteArray.length;
byteIndex = 0;
List<Integer> codePoints = new ArrayList<Integer>();
int tmp;
while ((tmp = decodeSymbol(strict)) != -1) {
codePoints.add(tmp);
}
return ucs2encode(listToArray(codePoints));
}
private static int[] ucs2decode(String string) {
int length = string.length();
int[] output = new int[string.codePointCount(0, length)];
int counter = 0;
int value;
for (int i = 0; i < length; i += Character.charCount(value)) {
value = string.codePointAt(i);
output[counter++] = value;
}
return output;
}
private static String encodeCodePoint(int codePoint, boolean strict) throws UTF8Exception {
StringBuilder symbol = new StringBuilder();
if ((codePoint & 0xFFFFFF80) == 0) {
return symbol.append(Character.toChars(codePoint)).toString();
}
if ((codePoint & 0xFFFFF800) == 0) {
symbol.append(Character.toChars(((codePoint >> 6) & 0x1F) | 0xC0));
} else if ((codePoint & 0xFFFF0000) == 0) {
if (!checkScalarValue(codePoint, strict)) {
codePoint = 0xFFFD;
}
symbol.append(Character.toChars(((codePoint >> 12) & 0x0F) | 0xE0));
symbol.append(createByte(codePoint, 6));
} else if ((codePoint & 0xFFE00000) == 0) {
symbol.append(Character.toChars(((codePoint >> 18) & 0x07) | 0xF0));
symbol.append(createByte(codePoint, 12));
symbol.append(createByte(codePoint, 6));
}
symbol.append(Character.toChars((codePoint & 0x3F) | 0x80));
return symbol.toString();
}
private static char[] createByte(int codePoint, int shift) {
return Character.toChars(((codePoint >> shift) & 0x3F) | 0x80);
}
private static int decodeSymbol(boolean strict) throws UTF8Exception {
int byte1;
int byte2;
int byte3;
int byte4;
int codePoint;
if (byteIndex > byteCount) {
throw new UTF8Exception("Invalid byte index");
}
if (byteIndex == byteCount) {
return -1;
}
byte1 = byteArray[byteIndex] & 0xFF;
byteIndex++;
if ((byte1 & 0x80) == 0) {
return byte1;
}
if ((byte1 & 0xE0) == 0xC0) {
byte2 = readContinuationByte();
codePoint = ((byte1 & 0x1F) << 6) | byte2;
if (codePoint >= 0x80) {
return codePoint;
} else {
throw new UTF8Exception(INVALID_CONTINUATION_BYTE);
}
}
if ((byte1 & 0xF0) == 0xE0) {
byte2 = readContinuationByte();
byte3 = readContinuationByte();
codePoint = ((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3;
if (codePoint >= 0x0800) {
return checkScalarValue(codePoint, strict) ? codePoint : 0xFFFD;
} else {
throw new UTF8Exception(INVALID_CONTINUATION_BYTE);
}
}
if ((byte1 & 0xF8) == 0xF0) {
byte2 = readContinuationByte();
byte3 = readContinuationByte();
byte4 = readContinuationByte();
codePoint = ((byte1 & 0x0F) << 0x12) | (byte2 << 0x0C) | (byte3 << 0x06) | byte4;
if (codePoint >= 0x010000 && codePoint <= 0x10FFFF) {
return codePoint;
}
}
throw new UTF8Exception(INVALID_CONTINUATION_BYTE);
}
private static int readContinuationByte() throws UTF8Exception {
if (byteIndex >= byteCount) {
throw new UTF8Exception("Invalid byte index");
}
int continuationByte = byteArray[byteIndex] & 0xFF;
byteIndex++;
if ((continuationByte & 0xC0) == 0x80) {
return continuationByte & 0x3F;
}
throw new UTF8Exception(INVALID_CONTINUATION_BYTE);
}
private static String ucs2encode(int[] array) {
StringBuilder output = new StringBuilder();
for (int value : array) {
output.appendCodePoint(value);
}
return output.toString();
}
private static boolean checkScalarValue(int codePoint, boolean strict) throws UTF8Exception {
if (codePoint >= 0xD800 && codePoint <= 0xDFFF) {
if (strict) {
throw new UTF8Exception(
"Lone surrogate U+" + Integer.toHexString(codePoint).toUpperCase() +
" is not a scalar value"
);
}
return false;
}
return true;
}
private static int[] listToArray(List<Integer> list) {
int size = list.size();
int[] array = new int[size];
for (int i = 0; i < size; i++) {
array[i] = list.get(i);
}
return array;
}
public static class Options {
public boolean strict = true;
}
}

View File

@@ -1,22 +0,0 @@
package io.socket.utf8;
import java.io.IOException;
public class UTF8Exception extends IOException {
public UTF8Exception() {
super();
}
public UTF8Exception(String message) {
super(message);
}
public UTF8Exception(String message, Throwable cause) {
super(message, cause);
}
public UTF8Exception(Throwable cause) {
super(cause);
}
}

View File

@@ -242,4 +242,20 @@ public class ConnectionTest extends Connection {
socket.open();
assertThat((Integer) values.take(), is(0));
}
@Test(timeout = TIMEOUT)
public void receivePing() throws InterruptedException {
final BlockingQueue<String> values = new LinkedBlockingQueue<>();
socket = new Socket(createOptions());
socket.on(Socket.EVENT_PING, new Emitter.Listener() {
@Override
public void call(Object... args) {
values.offer("end");
socket.close();
}
});
socket.open();
assertThat(values.take(), is("end"));
}
}

View File

@@ -1,6 +1,5 @@
package io.socket.engineio.parser;
import io.socket.utf8.UTF8Exception;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -15,7 +14,7 @@ public class ParserTest {
static final String ERROR_DATA = "parser error";
@Test
public void encodeAsString() throws UTF8Exception {
public void encodeAsString() {
encodePacket(new Packet<String>(Packet.MESSAGE, "test"), new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -25,7 +24,7 @@ public class ParserTest {
}
@Test
public void decodeAsPacket() throws UTF8Exception {
public void decodeAsPacket() {
encodePacket(new Packet<String>(Packet.MESSAGE, "test"), new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -35,7 +34,7 @@ public class ParserTest {
}
@Test
public void noData() throws UTF8Exception {
public void noData() {
encodePacket(new Packet(Packet.MESSAGE), new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -47,7 +46,7 @@ public class ParserTest {
}
@Test
public void encodeOpenPacket() throws UTF8Exception {
public void encodeOpenPacket() {
encodePacket(new Packet<String>(Packet.OPEN, "{\"some\":\"json\"}"), new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -59,7 +58,7 @@ public class ParserTest {
}
@Test
public void encodeClosePacket() throws UTF8Exception {
public void encodeClosePacket() {
encodePacket(new Packet<String>(Packet.CLOSE), new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -70,7 +69,7 @@ public class ParserTest {
}
@Test
public void encodePingPacket() throws UTF8Exception {
public void encodePingPacket() {
encodePacket(new Packet<String>(Packet.PING, "1"), new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -82,7 +81,7 @@ public class ParserTest {
}
@Test
public void encodePongPacket() throws UTF8Exception {
public void encodePongPacket() {
encodePacket(new Packet<String>(Packet.PONG, "1"), new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -94,7 +93,7 @@ public class ParserTest {
}
@Test
public void encodeMessagePacket() throws UTF8Exception {
public void encodeMessagePacket() {
encodePacket(new Packet<String>(Packet.MESSAGE, "aaa"), new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -106,7 +105,7 @@ public class ParserTest {
}
@Test
public void encodeUTF8SpecialCharsMessagePacket() throws UTF8Exception {
public void encodeUTF8SpecialCharsMessagePacket() {
encodePacket(new Packet<String>(Packet.MESSAGE, "utf8 — string"), new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -118,7 +117,7 @@ public class ParserTest {
}
@Test
public void encodeMessagePacketCoercingToString() throws UTF8Exception {
public void encodeMessagePacketCoercingToString() {
encodePacket(new Packet<Integer>(Packet.MESSAGE, 1), new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -130,7 +129,7 @@ public class ParserTest {
}
@Test
public void encodeUpgradePacket() throws UTF8Exception {
public void encodeUpgradePacket() {
encodePacket(new Packet<String>(Packet.UPGRADE), new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -141,7 +140,7 @@ public class ParserTest {
}
@Test
public void encodingFormat() throws UTF8Exception {
public void encodingFormat() {
encodePacket(new Packet<String>(Packet.MESSAGE, "test"), new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -156,19 +155,6 @@ public class ParserTest {
});
}
@Test
public void encodingStringMessageWithLoneSurrogatesReplacedByUFFFD() throws UTF8Exception {
String data = "\uDC00\uD834\uDF06\uDC00 \uD800\uD835\uDF07\uD800";
encodePacket(new Packet<String>(Packet.MESSAGE, data), true, new EncodeCallback<String>() {
@Override
public void call(String encoded) {
Packet<String> 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<String> p = decodePacket((String)null);
@@ -191,14 +177,7 @@ public class ParserTest {
}
@Test
public void decodeInvalidUTF8() {
Packet<String> p = decodePacket("4\uffff", true);
assertThat(p.type, is(Packet.ERROR));
assertThat(p.data, is(ERROR_DATA));
}
@Test
public void encodePayloads() throws UTF8Exception {
public void encodePayloads() {
encodePayload(new Packet[]{new Packet(Packet.PING), new Packet(Packet.PONG)}, new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -208,7 +187,7 @@ public class ParserTest {
}
@Test
public void encodeAndDecodePayloads() throws UTF8Exception {
public void encodeAndDecodePayloads() {
encodePayload(new Packet[] {new Packet<String>(Packet.MESSAGE, "a")}, new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -242,7 +221,7 @@ public class ParserTest {
}
@Test
public void encodeAndDecodeEmptyPayloads() throws UTF8Exception {
public void encodeAndDecodeEmptyPayloads() {
encodePayload(new Packet[] {}, new EncodeCallback<String>() {
@Override
public void call(String data) {
@@ -260,30 +239,20 @@ public class ParserTest {
}
@Test
public void notUTF8EncodeWhenDealingWithStringsOnly() throws UTF8Exception {
public void notUTF8EncodeWhenDealingWithStringsOnly() {
encodePayload(new Packet[] {
new Packet(Packet.MESSAGE, "€€€"),
new Packet(Packet.MESSAGE, "α")
}, new EncodeCallback<String>() {
@Override
public void call(String data) {
assertThat(data, is("4:4€€€2:4α"));
assertThat(data, is("4€€€\u001e4α"));
}
});
}
@Test
public void decodePayloadBadFormat() {
decodePayload("1!", new DecodePayloadCallback<String>() {
@Override
public boolean call(Packet<String> 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;
}
});
decodePayload("", new DecodePayloadCallback<String>() {
@Override
public boolean call(Packet<String> packet, int index, int total) {
@@ -306,23 +275,9 @@ public class ParserTest {
});
}
@Test
public void decodePayloadBadLength() {
decodePayload("1:", new DecodePayloadCallback<String>() {
@Override
public boolean call(Packet<String> 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 decodePayloadBadPacketFormat() {
decodePayload("3:99:", new DecodePayloadCallback<String>() {
decodePayload("99:", new DecodePayloadCallback<String>() {
@Override
public boolean call(Packet<String> packet, int index, int total) {
boolean isLast = index + 1 == total;
@@ -332,17 +287,7 @@ public class ParserTest {
return true;
}
});
decodePayload("1:aa", new DecodePayloadCallback<String>() {
@Override
public boolean call(Packet<String> 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;
}
});
decodePayload("1:a2:b", new DecodePayloadCallback<String>() {
decodePayload("aa", new DecodePayloadCallback<String>() {
@Override
public boolean call(Packet<String> packet, int index, int total) {
boolean isLast = index + 1 == total;
@@ -355,7 +300,7 @@ public class ParserTest {
}
@Test
public void encodeBinaryMessage() throws UTF8Exception {
public void encodeBinaryMessage() {
final byte[] data = new byte[5];
for (int i = 0; i < data.length; i++) {
data[0] = (byte)i;
@@ -371,7 +316,7 @@ public class ParserTest {
}
@Test
public void encodeBinaryContents() throws UTF8Exception {
public void encodeBinaryContents() {
final byte[] firstBuffer = new byte[5];
for (int i = 0 ; i < firstBuffer.length; i++) {
firstBuffer[0] = (byte)i;
@@ -384,9 +329,9 @@ public class ParserTest {
encodePayload(new Packet[]{
new Packet<byte[]>(Packet.MESSAGE, firstBuffer),
new Packet<byte[]>(Packet.MESSAGE, secondBuffer),
}, new EncodeCallback<byte[]>() {
}, new EncodeCallback<String>() {
@Override
public void call(byte[] data) {
public void call(String data) {
decodePayload(data, new DecodePayloadCallback() {
@Override
public boolean call(Packet packet, int index, int total) {
@@ -405,7 +350,7 @@ public class ParserTest {
}
@Test
public void encodeMixedBinaryAndStringContents() throws UTF8Exception {
public void encodeMixedBinaryAndStringContents() {
final byte[] firstBuffer = new byte[123];
for (int i = 0 ; i < firstBuffer.length; i++) {
firstBuffer[0] = (byte)i;
@@ -414,9 +359,9 @@ public class ParserTest {
new Packet<byte[]>(Packet.MESSAGE, firstBuffer),
new Packet<String>(Packet.MESSAGE, "hello"),
new Packet<String>(Packet.CLOSE),
}, new EncodeCallback<byte[]>() {
}, new EncodeCallback<String>() {
@Override
public void call(byte[] encoded) {
public void call(String encoded) {
decodePayload(encoded, new DecodePayloadCallback() {
@Override
public boolean call(Packet packet, int index, int total) {

View File

@@ -1,114 +0,0 @@
package io.socket.utf8;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
@RunWith(JUnit4.class)
public class UTF8Test {
private static final Data[] DATA = new Data[] {
// 1-byte
new Data(0x0000, "\u0000", "\u0000"),
new Data(0x005c, "\u005C\u005C", "\u005C\u005C"), // = backslash
new Data(0x007f, "\u007F", "\u007F"),
// 2-byte
new Data(0x0080, "\u0080", "\u00C2\u0080"),
new Data(0x05CA, "\u05CA", "\u00D7\u008A"),
new Data(0x07FF, "\u07FF", "\u00DF\u00BF"),
// 3-byte
new Data(0x0800, "\u0800", "\u00E0\u00A0\u0080"),
new Data(0x2C3C, "\u2C3C", "\u00E2\u00B0\u00BC"),
new Data(0x07FF, "\uFFFF", "\u00EF\u00BF\u00BF"),
// unmatched surrogate halves
// high surrogates: 0xD800 to 0xDBFF
new Data(0xD800, "\uD800", "\u00ED\u00A0\u0080", true),
new Data("High surrogate followed by another high surrogate",
"\uD800\uD800", "\u00ED\u00A0\u0080\u00ED\u00A0\u0080", true),
new Data("High surrogate followed by a symbol that is not a surrogate",
"\uD800A", "\u00ED\u00A0\u0080A", true),
new Data("Unmatched high surrogate, followed by a surrogate pair, followed by an unmatched high surrogate",
"\uD800\uD834\uDF06\uD800", "\u00ED\u00A0\u0080\u00F0\u009D\u008C\u0086\u00ED\u00A0\u0080", true),
new Data(0xD9AF, "\uD9AF", "\u00ED\u00A6\u00AF", true),
new Data(0xDBFF, "\uDBFF", "\u00ED\u00AF\u00BF", true),
// low surrogates: 0xDC00 to 0xDFFF
new Data(0xDC00, "\uDC00", "\u00ED\u00B0\u0080", true),
new Data("Low surrogate followed by another low surrogate",
"\uDC00\uDC00", "\u00ED\u00B0\u0080\u00ED\u00B0\u0080", true),
new Data("Low surrogate followed by a symbol that is not a surrogate",
"\uDC00A", "\u00ED\u00B0\u0080A", true),
new Data("Unmatched low surrogate, followed by a surrogate pair, followed by an unmatched low surrogate",
"\uDC00\uD834\uDF06\uDC00", "\u00ED\u00B0\u0080\u00F0\u009D\u008C\u0086\u00ED\u00B0\u0080", true),
new Data(0xDEEE, "\uDEEE", "\u00ED\u00BB\u00AE", true),
new Data(0xDFFF, "\uDFFF", "\u00ED\u00BF\u00BF", true),
// 4-byte
new Data(0x010000, "\uD800\uDC00", "\u00F0\u0090\u0080\u0080"),
new Data(0x01D306, "\uD834\uDF06", "\u00F0\u009D\u008C\u0086"),
new Data(0x010FFF, "\uDBFF\uDFFF", "\u00F4\u008F\u00BF\u00BF"),
};
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void encodeAndDecode() throws UTF8Exception {
for (Data data : DATA) {
String reason = data.description != null? data.description : "U+" + Integer.toHexString(data.codePoint).toUpperCase();
if (data.error) {
exception.expect(UTF8Exception.class);
UTF8.decode(data.encoded);
exception.expect(UTF8Exception.class);
UTF8.encode(data.decoded);
} else {
assertThat("Encoding: " + reason, data.encoded, is(UTF8.encode(data.decoded)));
assertThat("Decoding: " + reason, data.decoded, is(UTF8.decode(data.encoded)));
}
}
exception.expect(UTF8Exception.class);
UTF8.decode("\uFFFF");
exception.expect(UTF8Exception.class);
UTF8.decode("\u00E9\u0000\u0000");
exception.expect(UTF8Exception.class);
UTF8.decode("\u00C2\uFFFF");
exception.expect(UTF8Exception.class);
UTF8.decode("\u00F0\u009D");
}
private static class Data {
public int codePoint = -1;
public String description;
public String decoded;
public String encoded;
public boolean error;
public Data(int codePoint, String decoded, String encoded) {
this(codePoint, decoded, encoded, false);
}
public Data(int codePoint, String decoded, String encoded, boolean error) {
this.codePoint = codePoint;
this.decoded = decoded;
this.encoded = encoded;
this.error = error;
}
public Data(String description, String decoded, String encoded) {
this(description, decoded, encoded, false);
}
public Data(String description, String decoded, String encoded, boolean error) {
this.description = description;
this.decoded = decoded;
this.encoded = encoded;
this.error = error;
}
}
}

View File

@@ -11,16 +11,6 @@
"negotiator": "0.6.2"
}
},
"after": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
"integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
},
"arraybuffer.slice": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
},
"base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
@@ -31,15 +21,19 @@
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
},
"blob": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
"integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
},
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
},
"cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"requires": {
"object-assign": "^4",
"vary": "^1"
}
},
"debug": {
"version": "4.1.1",
@@ -50,43 +44,27 @@
}
},
"engine.io": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz",
"integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.0.5.tgz",
"integrity": "sha512-Ri+whTNr2PKklxQkfbGjwEo+kCBUM4Qxk4wtLqLrhH+b1up2NFL9g9pjYWiCV/oazwB0rArnvF/ZmZN2ab5Hpg==",
"requires": {
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "0.3.1",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.1.0",
"engine.io-parser": "~2.2.0",
"engine.io-parser": "~4.0.0",
"ws": "^7.1.2"
}
},
"engine.io-parser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz",
"integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz",
"integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==",
"requires": {
"after": "0.8.2",
"arraybuffer.slice": "~0.0.7",
"base64-arraybuffer": "0.1.4",
"blob": "0.0.5",
"has-binary2": "~1.0.2"
"base64-arraybuffer": "0.1.4"
}
},
"has-binary2": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
"integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
"requires": {
"isarray": "2.0.1"
}
},
"isarray": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
},
"mime-db": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
@@ -101,15 +79,25 @@
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"ws": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"private": true,
"dependencies": {
"engine.io": "^3.4.2"
"engine.io": "^4.0.5"
}
}