diff --git a/pom.xml b/pom.xml index aeff337aca..f06aaf3d2b 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,6 @@ 5.0.0-SNAPSHOT 5.0.0-SNAPSHOT 5.0.0-SNAPSHOT - 4.21.0 5.0.0-SNAPSHOT 4.5.14 @@ -1356,6 +1355,12 @@ + + org.eclipse.jetty.websocket + jetty-websocket-jetty-client + ${jetty.version} + + org.htmlunit htmlunit-core-js @@ -1381,11 +1386,6 @@ htmlunit-csp ${htmlunitcsp.version} - - org.htmlunit - htmlunit-websocket-client - ${htmlunitwebsocketclient.version} - org.apache.commons diff --git a/src/main/java/org/htmlunit/WebClient.java b/src/main/java/org/htmlunit/WebClient.java index 18e7fd39d0..f530730fe0 100644 --- a/src/main/java/org/htmlunit/WebClient.java +++ b/src/main/java/org/htmlunit/WebClient.java @@ -77,6 +77,7 @@ import org.htmlunit.html.XHtmlPage; import org.htmlunit.html.parser.HTMLParser; import org.htmlunit.html.parser.HTMLParserListener; +import org.htmlunit.http.Cookie; import org.htmlunit.http.HttpStatus; import org.htmlunit.http.HttpUtils; import org.htmlunit.httpclient.HttpClientConverter; @@ -94,7 +95,6 @@ import org.htmlunit.javascript.host.file.Blob; import org.htmlunit.javascript.host.html.HTMLIFrameElement; import org.htmlunit.protocol.data.DataURLConnection; -import org.htmlunit.http.Cookie; import org.htmlunit.util.HeaderUtils; import org.htmlunit.util.MimeType; import org.htmlunit.util.NameValuePair; @@ -3022,8 +3022,9 @@ public XHtmlPage loadXHtmlCodeIntoCurrentWindow(final String xhtmlCode) throws I * * @param webSocketListener the {@link WebSocketListener} * @return a new {@link WebSocketAdapter} + * @throws Exception in case of error */ - public WebSocketAdapter buildWebSocketAdapter(final WebSocketListener webSocketListener) { + public WebSocketAdapter buildWebSocketAdapter(final WebSocketListener webSocketListener) throws Exception { return webSocketAdapterFactory_.buildWebSocketAdapter(this, webSocketListener); } diff --git a/src/main/java/org/htmlunit/javascript/host/WebSocket.java b/src/main/java/org/htmlunit/javascript/host/WebSocket.java index c682115159..be5a8c5bef 100644 --- a/src/main/java/org/htmlunit/javascript/host/WebSocket.java +++ b/src/main/java/org/htmlunit/javascript/host/WebSocket.java @@ -120,7 +120,7 @@ public void onWebSocketConnecting() { } @Override - public void onWebSocketConnect() { + public void onWebSocketOpen() { setReadyState(OPEN); final Event openEvent = new Event(Event.TYPE_OPEN); @@ -159,9 +159,10 @@ public void onWebSocketText(final String message) { } @Override - public void onWebSocketBinary(final byte[] data, final int offset, final int length) { - final NativeArrayBuffer buffer = new NativeArrayBuffer(length); - System.arraycopy(data, offset, buffer.getBuffer(), 0, length); + public void onWebSocketBinary(final byte[] bytes) { + final NativeArrayBuffer buffer = new NativeArrayBuffer(bytes.length); + System.arraycopy(bytes, 0, buffer.getBuffer(), 0, bytes.length); + buffer.setParentScope(getParentScope()); buffer.setPrototype(ScriptableObject.getClassPrototype(getWindow(), buffer.getClassName())); @@ -419,17 +420,10 @@ public void close() throws IOException { public void close(final Object code, final Object reason) { if (readyState_ != CLOSED) { try { - webSocketImpl_.closeIncommingSession(); - } - catch (final Throwable e) { - LOG.error("WS close error - incomingSession_.close() failed", e); - } - - try { - webSocketImpl_.closeOutgoingSession(); + webSocketImpl_.closeSession(); } catch (final Throwable e) { - LOG.error("WS close error - outgoingSession_.close() failed", e); + LOG.error("WS close error - session_.close() failed", e); } } @@ -452,10 +446,10 @@ public void send(final Object content) { if (content instanceof NativeArrayBuffer buffer1) { final byte[] bytes = buffer1.getBuffer(); final ByteBuffer buffer = ByteBuffer.wrap(bytes); - webSocketImpl_.send(buffer); + webSocketImpl_.sendBinary(buffer); return; } - webSocketImpl_.send(content); + webSocketImpl_.sendText(content.toString()); } catch (final IOException e) { LOG.error("WS send error", e); diff --git a/src/main/java/org/htmlunit/websocket/JettyWebSocketAdapter.java b/src/main/java/org/htmlunit/websocket/JettyWebSocketAdapter.java index 71eb29b340..c68a7673df 100644 --- a/src/main/java/org/htmlunit/websocket/JettyWebSocketAdapter.java +++ b/src/main/java/org/htmlunit/websocket/JettyWebSocketAdapter.java @@ -17,14 +17,15 @@ import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; -import java.util.concurrent.Future; +import java.util.function.Consumer; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.websocket.api.Callback; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.client.WebSocketClient; import org.htmlunit.WebClient; import org.htmlunit.WebClientOptions; -import org.htmlunit.jetty.util.ssl.SslContextFactory; -import org.htmlunit.jetty.websocket.api.Session; -import org.htmlunit.jetty.websocket.api.WebSocketPolicy; -import org.htmlunit.jetty.websocket.client.WebSocketClient; /** * Jetty9 based impl of the WebSocketAdapter. @@ -44,7 +45,7 @@ public static final class JettyWebSocketAdapterFactory implements WebSocketAdapt */ @Override public WebSocketAdapter buildWebSocketAdapter(final WebClient webClient, - final WebSocketListener webSocketListener) { + final WebSocketListener webSocketListener) throws Exception { return new JettyWebSocketAdapter(webClient, webSocketListener); } } @@ -52,53 +53,47 @@ public WebSocketAdapter buildWebSocketAdapter(final WebClient webClient, private final Object clientLock_ = new Object(); private WebSocketClient client_; private final WebSocketListener listener_; + private final JettyWebSocketAdapterImpl adapterImpl_; - private volatile Session incomingSession_; - private Session outgoingSession_; + private volatile Session session_; /** * Ctor. * @param webClient the {@link WebClient} * @param listener the {@link WebSocketListener} + * @throws Exception in case of error */ - public JettyWebSocketAdapter(final WebClient webClient, final WebSocketListener listener) { + public JettyWebSocketAdapter(final WebClient webClient, final WebSocketListener listener) throws Exception { super(); final WebClientOptions options = webClient.getOptions(); + final HttpClient httpClient = new HttpClient(); + httpClient.setHttpCookieStore(new WebSocketCookieStore(webClient)); + + // Initialize client if (webClient.getOptions().isUseInsecureSSL()) { - client_ = new WebSocketClient(new SslContextFactory(true), null, null); - // still use the deprecated method here to be backward compatible with older jetty versions - // see https://github.com/HtmlUnit/htmlunit/issues/36 - // client_ = new WebSocketClient(new SslContextFactory.Client(true), null, null); - } - else { - client_ = new WebSocketClient(); - } + final SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(true); + sslContextFactory.setEndpointIdentificationAlgorithm(null); + sslContextFactory.setTrustAll(true); - listener_ = listener; + httpClient.setSslContextFactory(sslContextFactory); + } - // use the same executor as the rest - client_.setExecutor(webClient.getExecutor()); + httpClient.start(); + client_ = new WebSocketClient(httpClient); - client_.getHttpClient().setCookieStore(new WebSocketCookieStore(webClient)); + adapterImpl_ = new JettyWebSocketAdapterImpl(); + listener_ = listener; - final WebSocketPolicy policy = client_.getPolicy(); int size = options.getWebSocketMaxBinaryMessageSize(); if (size > 0) { - policy.setMaxBinaryMessageSize(size); - } - size = options.getWebSocketMaxBinaryMessageBufferSize(); - if (size > 0) { - policy.setMaxBinaryMessageBufferSize(size); + client_.setMaxBinaryMessageSize(size); } size = options.getWebSocketMaxTextMessageSize(); if (size > 0) { - policy.setMaxTextMessageSize(size); - } - size = options.getWebSocketMaxTextMessageBufferSize(); - if (size > 0) { - policy.setMaxTextMessageBufferSize(size); + client_.setMaxTextMessageSize(size); } + } /** @@ -117,16 +112,8 @@ public void start() throws Exception { @Override public void connect(final URI url) throws Exception { synchronized (clientLock_) { - final Future connectFuture = client_.connect(new JettyWebSocketAdapterImpl(), url); - client_.getExecutor().execute(() -> { - try { - listener_.onWebSocketConnecting(); - incomingSession_ = connectFuture.get(); - } - catch (final Exception e) { - listener_.onWebSocketConnectError(e); - } - }); + listener_.onWebSocketConnecting(); + client_.connect(adapterImpl_, url); } } @@ -134,36 +121,26 @@ public void connect(final URI url) throws Exception { * {@inheritDoc} */ @Override - public void send(final Object content) throws IOException { - if (content instanceof String string) { - outgoingSession_.getRemote().sendString(string); - } - else if (content instanceof ByteBuffer buffer) { - outgoingSession_.getRemote().sendBytes(buffer); - } - else { - throw new IllegalStateException( - "Not Yet Implemented: WebSocket.send() was used to send non-string value"); - } + public void sendText(final String message) throws IOException { + session_.sendText(message, Callback.from(session_::demand, adapterImpl_)); } /** * {@inheritDoc} */ @Override - public void closeIncommingSession() { - if (incomingSession_ != null) { - incomingSession_.close(); - } + public void sendBinary(final ByteBuffer buffer) throws IOException { + session_.sendBinary(buffer, Callback.from(session_::demand, adapterImpl_)); } /** * {@inheritDoc} */ @Override - public void closeOutgoingSession() { - if (outgoingSession_ != null) { - outgoingSession_.close(); + public void closeSession() { + if (session_ != null) { + session_.close(); + session_ = null; } } @@ -183,7 +160,10 @@ public void closeClient() throws Exception { } } - private class JettyWebSocketAdapterImpl extends org.htmlunit.jetty.websocket.api.WebSocketAdapter { + /** + * Session.Listener. + */ + public class JettyWebSocketAdapterImpl implements Session.Listener, Consumer { /** * Ctor. @@ -196,53 +176,106 @@ private class JettyWebSocketAdapterImpl extends org.htmlunit.jetty.websocket.api * {@inheritDoc} */ @Override - public void onWebSocketConnect(final Session session) { - super.onWebSocketConnect(session); - outgoingSession_ = session; + public void onWebSocketOpen(final Session session) { + session_ = session; - listener_.onWebSocketConnect(); + listener_.onWebSocketOpen(); } /** * {@inheritDoc} */ @Override - public void onWebSocketClose(final int statusCode, final String reason) { - super.onWebSocketClose(statusCode, reason); - outgoingSession_ = null; + public void onWebSocketText(final String message) { + if (session_ == null) { + return; + } + session_.demand(); - listener_.onWebSocketClose(statusCode, reason); + try { + listener_.onWebSocketText(message); + } + catch (final Exception e) { + // TODO: handle exception + } } /** * {@inheritDoc} */ @Override - public void onWebSocketText(final String message) { - super.onWebSocketText(message); + public void onWebSocketBinary(final ByteBuffer buffer, final Callback callback) { + if (session_ == null) { + return; + } + + byte[] arr = new byte[0]; + try { + arr = new byte[buffer.remaining()]; + buffer.get(arr); + + callback.succeed(); + } + catch (final Exception e) { + callback.fail(e); + return; + } - listener_.onWebSocketText(message); + session_.demand(); + + try { + listener_.onWebSocketBinary(arr); + } + catch (final Exception e) { + // TODO: handle exception + } } /** * {@inheritDoc} */ @Override - public void onWebSocketBinary(final byte[] data, final int offset, final int length) { - super.onWebSocketBinary(data, offset, length); + public void onWebSocketError(final Throwable cause) { - listener_.onWebSocketBinary(data, offset, length); + final String className = cause.getClass().getName(); + if ("java.nio.channels.ClosedChannelException".equals(className) + || "java.io.EOFException".equals(className) + || "java.net.SocketException".equals(className) + || "ClosedChannelException".contains(className) + || (cause.getMessage() != null + && (cause.getMessage().contains("Connection reset") + || cause.getMessage().contains("Broken pipe") + || cause.getMessage().contains("Connection closed")))) { + // TODO + return; + } + + accept(cause); } /** * {@inheritDoc} */ @Override - public void onWebSocketError(final Throwable cause) { - super.onWebSocketError(cause); - outgoingSession_ = null; + public void onWebSocketClose(final int statusCode, final String reason) { + session_ = null; + + try { + listener_.onWebSocketClose(statusCode, reason); + } + catch (final Exception e) { + // TODO: handle exception + } + } - listener_.onWebSocketError(cause); + @Override + public void accept(final Throwable cause) { + try { + listener_.onWebSocketError(cause); + } + catch (final Exception e) { + // TODO: handle exception + } } } } diff --git a/src/main/java/org/htmlunit/websocket/WebSocketAdapter.java b/src/main/java/org/htmlunit/websocket/WebSocketAdapter.java index 4442162856..af37b5b15e 100644 --- a/src/main/java/org/htmlunit/websocket/WebSocketAdapter.java +++ b/src/main/java/org/htmlunit/websocket/WebSocketAdapter.java @@ -16,10 +16,11 @@ import java.io.IOException; import java.net.URI; +import java.nio.ByteBuffer; /** * Helper to have no direct dependency to the WebSockt client - * implementation used by HtmlUnit. + * implementation used by HtmlUnit. This is used from the js code. * * @author Ronald Brill */ @@ -43,24 +44,25 @@ public interface WebSocketAdapter { /** * Sends the provided content. * - * @param content the content to be sent + * @param message the message to be sent * @throws IOException in case of error */ - void send(Object content) throws IOException; + void sendText(String message) throws IOException; /** - * Close the incomming session. + * Sends the provided content. * - * @throws Exception in case of error + * @param buffer the bytes to be sent + * @throws IOException in case of error */ - void closeIncommingSession() throws Exception; + void sendBinary(ByteBuffer buffer) throws IOException; /** - * Close the outgoing session. + * Close the session. * * @throws Exception in case of error */ - void closeOutgoingSession() throws Exception; + void closeSession() throws Exception; /** * Close the client. diff --git a/src/main/java/org/htmlunit/websocket/WebSocketAdapterFactory.java b/src/main/java/org/htmlunit/websocket/WebSocketAdapterFactory.java index 29d816676c..8e42e3a4e6 100644 --- a/src/main/java/org/htmlunit/websocket/WebSocketAdapterFactory.java +++ b/src/main/java/org/htmlunit/websocket/WebSocketAdapterFactory.java @@ -32,6 +32,7 @@ public interface WebSocketAdapterFactory extends Serializable { * @param webClient the {@link WebClient} * @param webSocketListener the {@link WebSocketListener} * @return a new {@link WebSocketAdapter} + * @throws Exception in case of error */ - WebSocketAdapter buildWebSocketAdapter(WebClient webClient, WebSocketListener webSocketListener); + WebSocketAdapter buildWebSocketAdapter(WebClient webClient, WebSocketListener webSocketListener) throws Exception; } diff --git a/src/main/java/org/htmlunit/websocket/WebSocketCookieStore.java b/src/main/java/org/htmlunit/websocket/WebSocketCookieStore.java index 7fcde84ae7..049ec1d6a3 100644 --- a/src/main/java/org/htmlunit/websocket/WebSocketCookieStore.java +++ b/src/main/java/org/htmlunit/websocket/WebSocketCookieStore.java @@ -14,23 +14,22 @@ */ package org.htmlunit.websocket; -import java.net.CookieStore; -import java.net.HttpCookie; import java.net.URI; import java.util.ArrayList; import java.util.List; +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpCookieStore; import org.htmlunit.WebClient; -import org.htmlunit.javascript.host.WebSocket; import org.htmlunit.http.Cookie; +import org.htmlunit.javascript.host.WebSocket; /** * A helper class for {@link WebSocket}. * - * @author Ahmed Ashour * @author Ronald Brill */ -class WebSocketCookieStore implements CookieStore { +class WebSocketCookieStore implements HttpCookieStore { private final WebClient webClient_; @@ -38,27 +37,17 @@ class WebSocketCookieStore implements CookieStore { webClient_ = webClient; } - /** - * {@inheritDoc} - */ @Override - public void add(final URI uri, final HttpCookie cookie) { - throw new UnsupportedOperationException(); + public boolean add(final URI uri, final HttpCookie cookie) { + return false; } - /** - * {@inheritDoc} - */ @Override - public List get(final URI uri) { + public List all() { final List cookies = new ArrayList<>(); try { - final String urlString = uri.toString().replace("ws://", "http://").replace("wss://", "https://"); - final java.net.URL url = new java.net.URL(urlString); - for (final Cookie cookie : webClient_.getCookies(url)) { - final HttpCookie httpCookie = new HttpCookie(cookie.getName(), cookie.getValue()); - httpCookie.setVersion(0); - cookies.add(httpCookie); + for (final Cookie htmlUnitCookie : webClient_.getCookieManager().getCookies()) { + cookies.add(buildHttpCookie(htmlUnitCookie)); } } catch (final Exception e) { @@ -67,35 +56,59 @@ public List get(final URI uri) { return cookies; } - /** - * {@inheritDoc} - */ @Override - public List getCookies() { - throw new UnsupportedOperationException(); - } + public List match(final URI uri) { + final List cookies = new ArrayList<>(); + try { + final String urlString = uri.toString() + .replace("ws://", "http://") + .replace("wss://", "https://"); + final java.net.URL url = new java.net.URL(urlString); - /** - * {@inheritDoc} - */ - @Override - public List getURIs() { - throw new UnsupportedOperationException(); + for (final Cookie htmlUnitCookie : webClient_.getCookies(url)) { + cookies.add(buildHttpCookie(htmlUnitCookie)); + } + } + catch (final Exception e) { + throw new RuntimeException(e); + } + return cookies; } - /** - * {@inheritDoc} - */ @Override public boolean remove(final URI uri, final HttpCookie cookie) { - throw new UnsupportedOperationException(); + return false; } - /** - * {@inheritDoc} - */ @Override - public boolean removeAll() { + public boolean clear() { return false; } + + private static HttpCookie buildHttpCookie(final Cookie htmlUnitCookie) { + final HttpCookie.Builder builder = HttpCookie.build( + htmlUnitCookie.getName(), + htmlUnitCookie.getValue()); + if (htmlUnitCookie.getDomain() != null) { + builder.domain(htmlUnitCookie.getDomain()); + } + + if (htmlUnitCookie.getPath() != null) { + builder.path(htmlUnitCookie.getPath()); + } +// +// if (htmlUnitCookie.getMaxAge() > -1) { +// builder.maxAge(htmlUnitCookie.getMaxAge()); +// } + + if (htmlUnitCookie.isSecure()) { + builder.secure(true); + } + + if (htmlUnitCookie.isHttpOnly()) { + builder.httpOnly(true); + } + + return builder.build(); + } } diff --git a/src/main/java/org/htmlunit/websocket/WebSocketListener.java b/src/main/java/org/htmlunit/websocket/WebSocketListener.java index 327d96e3fe..dca06604e0 100644 --- a/src/main/java/org/htmlunit/websocket/WebSocketListener.java +++ b/src/main/java/org/htmlunit/websocket/WebSocketListener.java @@ -28,9 +28,9 @@ public interface WebSocketListener { void onWebSocketConnecting(); /** - * Callback to be called when connected. + * Callback to be called when opened. */ - void onWebSocketConnect(); + void onWebSocketOpen(); /** * Callback to be called when closed. @@ -50,11 +50,9 @@ public interface WebSocketListener { /** * Callback to be called when binary data retrieved. * - * @param data the bytes - * @param offset start offset - * @param length the length + * @param buffer the bytes */ - void onWebSocketBinary(byte[] data, int offset, int length); + void onWebSocketBinary(byte[] buffer); /** * Callback to be called on connect error. diff --git a/src/test/java/org/htmlunit/javascript/host/WebSocketTest.java b/src/test/java/org/htmlunit/javascript/host/WebSocketTest.java index e8dc0bbed6..d06c2fa470 100644 --- a/src/test/java/org/htmlunit/javascript/host/WebSocketTest.java +++ b/src/test/java/org/htmlunit/javascript/host/WebSocketTest.java @@ -273,6 +273,7 @@ public void onWebSocketText(final String data) { @Override public void onWebSocketClose(final int closeCode, final String message) { +System.out.println("Server close"); webSockets_.remove(this); } }