diff --git a/engine.io-server-test/pom.xml b/engine.io-server-test/pom.xml index db22fbf..ec2146b 100644 --- a/engine.io-server-test/pom.xml +++ b/engine.io-server-test/pom.xml @@ -42,6 +42,18 @@ 9.4.10.v20180503 test + + org.seleniumhq.selenium + selenium-server + 2.52.0 + test + + + org.seleniumhq.selenium + selenium-htmlunit-driver + 2.52.0 + test + io.socket diff --git a/engine.io-server-test/src/test/java/io/socket/engineio/server/PollingJsonpTest.java b/engine.io-server-test/src/test/java/io/socket/engineio/server/PollingJsonpTest.java new file mode 100644 index 0000000..739a4ba --- /dev/null +++ b/engine.io-server-test/src/test/java/io/socket/engineio/server/PollingJsonpTest.java @@ -0,0 +1,121 @@ +package io.socket.engineio.server; + +import io.socket.engineio.parser.Packet; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.openqa.jetty.log.LogFactory; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.htmlunit.HtmlUnitDriver; +import org.openqa.selenium.remote.server.SeleniumServer; +import org.openqa.selenium.support.events.AbstractWebDriverEventListener; +import org.openqa.selenium.support.events.EventFiringWebDriver; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; + +import static org.junit.Assert.*; + +public final class PollingJsonpTest { + + private static SeleniumServer sSeleniumServer; + + @Before + public void setup() { + LogFactory.getFactory().setAttribute("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.NoOpLog"); + + java.util.logging.Logger.getLogger("com.gargoylesoftware.htmlunit").setLevel(Level.OFF); + java.util.logging.Logger.getLogger("org.apache.commons.httpclient").setLevel(Level.OFF); + + sSeleniumServer = new SeleniumServer(4444); + sSeleniumServer.boot(); + } + + @After + public void teardown() { + sSeleniumServer.stop(); + } + + @Test + public void echoTest_string() throws Exception { + final ServerWrapper serverWrapper = new ServerWrapper(); + + try { + serverWrapper.startServer(); + serverWrapper.getEngineIoServer().on("connection", args -> { + final EngineIoSocket socket = (EngineIoSocket) args[0]; + socket.on("message", args1 -> { + Packet packet = new Packet(Packet.MESSAGE); + packet.data = args1[0]; + socket.send(packet); + }); + }); + + assertEquals(0, executeScriptInBrowser(serverWrapper, "src/test/resources/testPollingJsonp_echo_string.js")); + } finally { + serverWrapper.stopServer(); + } + } + + @Test + public void reverseEchoTest() throws Exception { + final ServerWrapper serverWrapper = new ServerWrapper(); + try { + serverWrapper.startServer(); + serverWrapper.getEngineIoServer().on("connection", args -> { + final EngineIoSocket socket = (EngineIoSocket) args[0]; + final String echoMessage = PollingTest.class.getSimpleName() + System.currentTimeMillis(); + socket.on("message", args1 -> assertEquals(echoMessage, args1[0])); + + Packet packet = new Packet(Packet.MESSAGE); + packet.data = echoMessage; + socket.send(packet); + }); + + assertEquals(0, executeScriptInBrowser(serverWrapper, "src/test/resources/testPollingJsonp_reverseEcho.js")); + } finally { + serverWrapper.stopServer(); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private int executeScriptInBrowser(ServerWrapper serverWrapper, String script) throws IOException { + class ScriptResult { + int result = -1; + } + + final HtmlUnitDriver webDriver = new HtmlUnitDriver(true); + final EventFiringWebDriver eventFiringWebDriver = new EventFiringWebDriver(webDriver); + final ScriptResult scriptResult = new ScriptResult(); + + try (FileInputStream fis = new FileInputStream(script)) { + final byte[] scriptBytes = new byte[fis.available()]; + fis.read(scriptBytes); + final String scriptContent = new String(scriptBytes, StandardCharsets.UTF_8); + + eventFiringWebDriver.get("http://127.0.0.1:" + serverWrapper.getPort() + "/testPollingJsonp.html"); + eventFiringWebDriver.register(new AbstractWebDriverEventListener() { + + @Override + public void afterScript(String script, WebDriver driver) { + final String url = eventFiringWebDriver.getCurrentUrl(); + if (url.startsWith("http://www.example.com/?result=")) { + scriptResult.result = Integer.parseInt(url.substring("http://www.example.com/?result=".length())); + } + } + }); + eventFiringWebDriver.executeScript(scriptContent, serverWrapper.getPort()); + + try { + Thread.sleep(3000); + } catch (InterruptedException ignore) { + } + + return scriptResult.result; + } finally { + webDriver.close(); + } + } +} diff --git a/engine.io-server-test/src/test/java/io/socket/engineio/server/ServerWrapper.java b/engine.io-server-test/src/test/java/io/socket/engineio/server/ServerWrapper.java index 7bfebe2..fc94731 100644 --- a/engine.io-server-test/src/test/java/io/socket/engineio/server/ServerWrapper.java +++ b/engine.io-server-test/src/test/java/io/socket/engineio/server/ServerWrapper.java @@ -1,5 +1,6 @@ package io.socket.engineio.server; +import org.apache.commons.io.IOUtils; import org.eclipse.jetty.http.pathmap.ServletPathSpec; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; @@ -14,7 +15,11 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; import java.util.concurrent.atomic.AtomicInteger; final class ServerWrapper { @@ -43,6 +48,26 @@ protected void service(HttpServletRequest request, HttpServletResponse response) mEngineIoServer.handleRequest(request, response); } }), "/engine.io/*"); + servletContextHandler.addServlet(new ServletHolder(new HttpServlet() { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { + final String path = request.getPathInfo(); + final File file = new File("src/test/resources", path); + if (file.exists()) { + response.setContentType(Files.probeContentType(file.toPath())); + + try (FileInputStream fis = new FileInputStream(file)) { + IOUtils.copy(fis, response.getOutputStream()); + } + } else { + response.setStatus(404); + try (PrintWriter writer = response.getWriter()) { + writer.print("Not Found"); + writer.flush(); + } + } + } + }), "/*"); try { WebSocketUpgradeFilter webSocketUpgradeFilter = WebSocketUpgradeFilter.configureContext(servletContextHandler); diff --git a/engine.io-server-test/src/test/resources/testPollingJsonp.html b/engine.io-server-test/src/test/resources/testPollingJsonp.html new file mode 100644 index 0000000..a3d3e6c --- /dev/null +++ b/engine.io-server-test/src/test/resources/testPollingJsonp.html @@ -0,0 +1,17 @@ + + + + + + Polling Test - JSONP + + + + + + + \ No newline at end of file diff --git a/engine.io-server-test/src/test/resources/testPollingJsonp_echo_string.js b/engine.io-server-test/src/test/resources/testPollingJsonp_echo_string.js new file mode 100644 index 0000000..c2d6d21 --- /dev/null +++ b/engine.io-server-test/src/test/resources/testPollingJsonp_echo_string.js @@ -0,0 +1,23 @@ +var returnError = function () { + window.setResult(1); +}; + +var port = arguments[0]; +socket = eio('http://127.0.0.1:' + port, { + transports: ['polling'], + jsonp: true, + forceJSONP: true +}); +socket.on('open', function () { + var echoMessage = "Hello World"; + socket.on('message', function (message) { + if(message === echoMessage) { + window.setResult(0); + } else { + returnError(); + } + }); + socket.send(echoMessage); +}); +socket.on('error', returnError); +setTimeout(returnError, 750); diff --git a/engine.io-server-test/src/test/resources/testPollingJsonp_reverseEcho.js b/engine.io-server-test/src/test/resources/testPollingJsonp_reverseEcho.js new file mode 100644 index 0000000..d187f18 --- /dev/null +++ b/engine.io-server-test/src/test/resources/testPollingJsonp_reverseEcho.js @@ -0,0 +1,19 @@ +var returnError = function () { + window.setResult(1); +}; + +var port = arguments[0]; +socket = eio('http://127.0.0.1:' + port, { + transports: ['polling'], + jsonp: true, + forceJSONP: true +}); +socket.on("message", function (message) { + socket.sendPacket("message", message); + + setTimeout(function () { + window.setResult(0); + }, 100); +}); +socket.on('error', returnError); +setTimeout(returnError, 750); \ No newline at end of file diff --git a/engine.io-server/src/main/java/io/socket/engineio/server/transport/Polling.java b/engine.io-server/src/main/java/io/socket/engineio/server/transport/Polling.java index bcef88d..d803fbd 100644 --- a/engine.io-server/src/main/java/io/socket/engineio/server/transport/Polling.java +++ b/engine.io-server/src/main/java/io/socket/engineio/server/transport/Polling.java @@ -3,6 +3,9 @@ import io.socket.engineio.parser.Packet; import io.socket.engineio.parser.ServerParser; import io.socket.engineio.server.Transport; +import io.socket.parseqs.ParseQS; +import org.json.JSONArray; +import org.json.JSONObject; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; @@ -69,23 +72,41 @@ public synchronized void send(List packets) { packets.add(new Packet(Packet.CLOSE)); } - @SuppressWarnings("unchecked") - final boolean supportsBinary = (!((Map) mRequest.getAttribute("query")).containsKey("b64")); + //noinspection unchecked + final Map query = (Map) mRequest.getAttribute("query"); + + final boolean supportsBinary = !query.containsKey("b64"); + final boolean jsonp = query.containsKey("j"); if(packets.size() == 0) { throw new IllegalArgumentException("No packets to send."); } ServerParser.encodePayload(packets.toArray(new Packet[0]), supportsBinary, data -> { - final String contentType = (data instanceof String)? "text/plain; charset=UTF-8" : "application/octet-stream"; - final int contentLength = (data instanceof String)? ((String) data).length() : ((byte[]) data).length; + final String contentType; + final byte[] contentBytes; + + if (jsonp) { + final String jsonpIndex = query.get("j").replaceAll("[^0-9]", ""); + final String jsonContentString = (data instanceof String)? + JSONObject.quote((String)data) : + JSONObject.valueToString(new JSONArray(data)); + final String jsContentString = jsonContentString + .replace("\u2028", "\\u2028") + .replace("\u2029", "\\u2029"); + final String contentString = "___eio[" + jsonpIndex + "](" + jsContentString + ")"; + + contentType = "text/javascript; charset=UTF-8"; + contentBytes = contentString.getBytes(StandardCharsets.UTF_8); + } else { + contentType = (data instanceof String)? "text/plain; charset=UTF-8" : "application/octet-stream"; + contentBytes = (data instanceof String)? ((String)data).getBytes(StandardCharsets.UTF_8) : ((byte[])data); + } mResponse.setContentType(contentType); - mResponse.setContentLength(contentLength); + mResponse.setContentLength(contentBytes.length); try (OutputStream outputStream = mResponse.getOutputStream()) { - // TODO: Support JSONP - byte[] writeBytes = (data instanceof String)? ((String) data).getBytes(StandardCharsets.UTF_8) : ((byte[]) data); - outputStream.write(writeBytes); + outputStream.write(contentBytes); } catch (IOException ex) { onError("write failure", ex.getMessage()); } @@ -148,7 +169,11 @@ private void onPollRequest(@SuppressWarnings("unused") HttpServletRequest reques } private void onDataRequest(final HttpServletRequest request, final HttpServletResponse response) throws IOException { + //noinspection unchecked + final Map query = (Map) mRequest.getAttribute("query"); + final boolean isBinary = request.getContentType().equals("application/octet-stream"); + final boolean jsonp = query.containsKey("j"); try(final ServletInputStream inputStream = request.getInputStream()) { final byte[] mReadBuffer = new byte[request.getContentLength()]; @@ -156,7 +181,13 @@ private void onDataRequest(final HttpServletRequest request, final HttpServletRe //noinspection ResultOfMethodCallIgnored inputStream.read(mReadBuffer, 0, mReadBuffer.length); - onData((isBinary)? mReadBuffer : (new String(mReadBuffer, StandardCharsets.UTF_8))); + if (jsonp) { + final String packetPayloadRaw = ParseQS.decode(new String(mReadBuffer, StandardCharsets.UTF_8)).get("d"); + final String packetPayload = packetPayloadRaw.replace("\\n", "\n"); + onData(packetPayload); + } else { + onData((isBinary)? mReadBuffer : (new String(mReadBuffer, StandardCharsets.UTF_8))); + } } response.setContentType("text/html"); diff --git a/engine.io-server/src/test/java/io/socket/engineio/server/transport/PollingTest.java b/engine.io-server/src/test/java/io/socket/engineio/server/transport/PollingTest.java index ab8372c..016b093 100644 --- a/engine.io-server/src/test/java/io/socket/engineio/server/transport/PollingTest.java +++ b/engine.io-server/src/test/java/io/socket/engineio/server/transport/PollingTest.java @@ -1,5 +1,6 @@ package io.socket.engineio.server.transport; +import io.socket.emitter.Emitter; import io.socket.engineio.parser.Packet; import io.socket.engineio.parser.ServerParser; import io.socket.engineio.server.HttpServletResponseImpl; @@ -11,11 +12,12 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import static org.junit.Assert.*; +@SuppressWarnings("unchecked") public final class PollingTest { @Test @@ -65,15 +67,64 @@ public void testOnRequest_poll() throws IOException { final HttpServletResponseImpl response = new HttpServletResponseImpl(); + final Emitter.Listener drainListener = Mockito.spy(Emitter.Listener.class); + Mockito.doAnswer(invocation -> { + polling.send(new ArrayList(){{ + add(new Packet(Packet.MESSAGE, "Test Data")); + }}); + return null; + }).when(drainListener).call(); + polling.on("drain", drainListener); + polling.onRequest(request, response); - Mockito.verify(polling, Mockito.times(1)) - .emit(Mockito.eq("drain")); + Mockito.verify(drainListener, Mockito.times(1)).call(); final String responseString = new String(response.getByteOutputStream().toByteArray(), StandardCharsets.UTF_8); ServerParser.decodePayload(responseString, (packet, index, total) -> { assertEquals(1, total); - assertEquals(Packet.NOOP, packet.type); + assertEquals(Packet.MESSAGE, packet.type); + assertEquals("Test Data", packet.data); + return true; + }); + } + + @Test + public void testOnRequest_poll_jsonp() throws IOException { + final Polling polling = Mockito.spy(new Polling()); + + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + Mockito.doAnswer(invocationOnMock -> "GET").when(request).getMethod(); + Mockito.doAnswer(invocationOnMock -> { + final HashMap queryMap = new HashMap<>(); + queryMap.put("transport", Polling.NAME); + queryMap.put("j", "100"); + return queryMap; + }).when(request).getAttribute("query"); + + final HttpServletResponseImpl response = new HttpServletResponseImpl(); + + final Emitter.Listener drainListener = Mockito.spy(Emitter.Listener.class); + Mockito.doAnswer(invocation -> { + polling.send(new ArrayList(){{ + add(new Packet(Packet.MESSAGE, "Test Data")); + }}); + return null; + }).when(drainListener).call(); + polling.on("drain", drainListener); + + polling.onRequest(request, response); + + Mockito.verify(drainListener, Mockito.times(1)).call(); + + final String responseString = new String(response.getByteOutputStream().toByteArray(), StandardCharsets.UTF_8); + assertTrue(responseString.startsWith("___eio[100](")); + + final String payloadString = responseString.substring("___eio[100](".length() + 1, responseString.length() - 2); + ServerParser.decodePayload(payloadString, (packet, index, total) -> { + assertEquals(1, total); + assertEquals(Packet.MESSAGE, packet.type); + assertEquals("Test Data", packet.data); return true; }); } @@ -118,6 +169,45 @@ public void testOnRequest_data() { }); } + @Test + public void testOnRequest_data_jsonp() { + final String messageData = "Test Data"; + + final Polling polling = Mockito.spy(new Polling()); + polling.on("packet", args -> { + final Packet packet = (Packet) args[0]; + assertEquals(Packet.MESSAGE, packet.type); + assertEquals(messageData, packet.data); + }); + + final byte[] data = "d=10:4Test Data".getBytes(StandardCharsets.UTF_8); + final ByteArrayInputStream requestInputStream = new ByteArrayInputStream(data); + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + Mockito.doAnswer(invocationOnMock -> "POST").when(request).getMethod(); + Mockito.doAnswer(invocationOnMock -> { + final HashMap queryMap = new HashMap<>(); + queryMap.put("transport", Polling.NAME); + queryMap.put("j", "100"); + return queryMap; + }).when(request).getAttribute("query"); + Mockito.doAnswer(invocationOnMock -> "application/octet-stream").when(request).getContentType(); + Mockito.doAnswer(invocationOnMock -> data.length).when(request).getContentLength(); + try { + Mockito.doAnswer(invocationOnMock -> new ServletInputStreamWrapper(requestInputStream)).when(request).getInputStream(); + } catch (IOException ignore) { + } + + final HttpServletResponseImpl response = new HttpServletResponseImpl(); + + try { + polling.onRequest(request, response); + } catch (IOException ignore) { + } + + Mockito.verify(polling, Mockito.times(1)) + .emit(Mockito.eq("packet"), Mockito.any(Packet.class)); + } + @Test public void testClose_client() { final Polling polling = Mockito.spy(new Polling());