AbstractWebSocket.java

  1. /**
  2.  *
  3.  * Copyright 2020 Aditya Borikar, 2020-2023 Florian Schmaus
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smack.websocket.impl;

  18. import java.io.IOException;
  19. import java.util.logging.Level;
  20. import java.util.logging.Logger;

  21. import javax.net.ssl.SSLSession;
  22. import javax.xml.namespace.QName;

  23. import org.jivesoftware.smack.SmackFuture;
  24. import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
  25. import org.jivesoftware.smack.debugger.SmackDebugger;
  26. import org.jivesoftware.smack.packet.TopLevelStreamElement;
  27. import org.jivesoftware.smack.packet.XmlEnvironment;
  28. import org.jivesoftware.smack.util.PacketParserUtils;
  29. import org.jivesoftware.smack.websocket.WebSocketException;
  30. import org.jivesoftware.smack.websocket.elements.WebSocketCloseElement;
  31. import org.jivesoftware.smack.websocket.elements.WebSocketOpenElement;
  32. import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
  33. import org.jivesoftware.smack.xml.XmlPullParser;
  34. import org.jivesoftware.smack.xml.XmlPullParserException;

  35. public abstract class AbstractWebSocket {

  36.     protected static final Logger LOGGER = Logger.getLogger(AbstractWebSocket.class.getName());

  37.     protected static final String SEC_WEBSOCKET_PROTOCOL_HEADER_FILED_NAME = "Sec-WebSocket-Protocol";
  38.     protected static final String SEC_WEBSOCKET_PROTOCOL_HEADER_FILED_VALUE_XMPP = "xmpp";

  39.     protected final SmackFuture.InternalSmackFuture<AbstractWebSocket, Exception> future = new SmackFuture.InternalSmackFuture<>();

  40.     protected final ModularXmppClientToServerConnectionInternal connectionInternal;

  41.     protected final WebSocketRemoteConnectionEndpoint endpoint;

  42.     private final SmackWebSocketDebugger debugger;

  43.     protected AbstractWebSocket(WebSocketRemoteConnectionEndpoint endpoint,
  44.                     ModularXmppClientToServerConnectionInternal connectionInternal) {
  45.         this.endpoint = endpoint;
  46.         this.connectionInternal = connectionInternal;

  47.         final SmackDebugger smackDebugger = connectionInternal.smackDebugger;
  48.         if (smackDebugger != null) {
  49.             debugger = new SmackWebSocketDebugger(smackDebugger);
  50.         } else {
  51.             debugger = null;
  52.         }
  53.     }

  54.     public final WebSocketRemoteConnectionEndpoint getEndpoint() {
  55.         return endpoint;
  56.     }

  57.     private String streamOpen;
  58.     private String streamClose;

  59.     protected final void onIncomingWebSocketElement(String element) {
  60.         if (debugger != null) {
  61.             debugger.incoming(element);
  62.         }

  63.         // TODO: Once smack-websocket-java15 is there, we have to re-evaluate if the async operation here is still
  64.         // required, or if it should only be performed if OkHTTP is used.
  65.         if (isOpenElement(element)) {
  66.             // Transform the XMPP WebSocket <open/> element to a RFC 6120 <stream> open tag.
  67.             streamOpen = getStreamFromOpenElement(element);
  68.             streamClose = connectionInternal.onStreamOpen(streamOpen);
  69.             return;
  70.         }

  71.         if (isCloseElement(element)) {
  72.             connectionInternal.onStreamClosed();
  73.             return;
  74.         }

  75.         connectionInternal.withSmackDebugger(debugger -> debugger.onIncomingElementCompleted());

  76.         // TODO: Do we need to wrap the element again in the stream open to get the
  77.         // correct XML scoping (just like the modular TCP connection does)? It appears
  78.         // that this not really required, as onStreamOpen() will set the incomingStreamEnvironment, which is used for
  79.         // parsing.
  80.         String wrappedCompleteElement = streamOpen + element + streamClose;
  81.         connectionInternal.parseAndProcessElement(wrappedCompleteElement);
  82.     }

  83.     static String getStreamFromOpenElement(String openElement) {
  84.         String streamElement = openElement.replaceFirst("\\A<open ", "<stream:stream ")
  85.                                           .replace("urn:ietf:params:xml:ns:xmpp-framing", "jabber:client")
  86.                                           .replaceFirst("/>\\s*\\z", " xmlns:stream='http://etherx.jabber.org/streams'>")
  87.                                           .replaceFirst("></open>\\s*\\z", " xmlns:stream='http://etherx.jabber.org/streams'>");

  88.         return streamElement;
  89.     }

  90.     static boolean isOpenElement(String text) {
  91.         XmlPullParser parser;
  92.         try {
  93.             parser = PacketParserUtils.getParserFor(text);
  94.             QName qname = parser.getQName();
  95.             return qname.equals(WebSocketOpenElement.QNAME);
  96.         } catch (XmlPullParserException | IOException e) {
  97.             LOGGER.log(Level.WARNING, "Could not inspect \"" + text + "\" for open element", e);
  98.             return false;
  99.         }
  100.     }

  101.     static boolean isCloseElement(String text) {
  102.         XmlPullParser parser;
  103.         try {
  104.             parser = PacketParserUtils.getParserFor(text);
  105.             QName qname = parser.getQName();
  106.             return qname.equals(WebSocketCloseElement.QNAME);
  107.         } catch (XmlPullParserException | IOException e) {
  108.             LOGGER.log(Level.WARNING, "Could not inspect \"" + text + "\" for close element", e);
  109.             return false;
  110.         }
  111.     }

  112.     protected void onWebSocketFailure(Throwable throwable) {
  113.         WebSocketException websocketException = new WebSocketException(throwable);

  114.         // If we are already connected, then we need to notify the connection that it got tear down. Otherwise we
  115.         // need to notify the thread calling connect() that the connection failed.
  116.         if (future.wasSuccessful()) {
  117.             connectionInternal.notifyConnectionError(websocketException);
  118.         } else {
  119.             future.setException(websocketException);
  120.         }
  121.     }

  122.     public final SmackFuture<AbstractWebSocket, Exception> getFuture() {
  123.         return future;
  124.     }

  125.     public final void send(TopLevelStreamElement element) {
  126.         XmlEnvironment outgoingStreamXmlEnvironment = connectionInternal.getOutgoingStreamXmlEnvironment();
  127.         String elementString = element.toXML(outgoingStreamXmlEnvironment).toString();

  128.         // TODO: We could make use of Java 11's WebSocket (is)last feature when sending
  129.         if (debugger != null) {
  130.             debugger.outgoing(elementString);
  131.         }

  132.         send(elementString);
  133.     }

  134.     protected abstract void send(String element);

  135.     public abstract void disconnect(int code, String message);

  136.     public boolean isConnectionSecure() {
  137.         return endpoint.isSecureEndpoint();
  138.     }

  139.     public abstract SSLSession getSSLSession();

  140.     @Override
  141.     public final String toString() {
  142.         return getClass().getSimpleName() + "[" + connectionInternal.connection + "]";
  143.     }
  144. }