001/** 002 * 003 * Copyright 2020 Aditya Borikar, 2020-2023 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smack.websocket.impl; 018 019import java.util.logging.Logger; 020 021import javax.net.ssl.SSLSession; 022 023import org.jivesoftware.smack.SmackFuture; 024import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; 025import org.jivesoftware.smack.debugger.SmackDebugger; 026import org.jivesoftware.smack.packet.TopLevelStreamElement; 027import org.jivesoftware.smack.packet.XmlEnvironment; 028import org.jivesoftware.smack.websocket.WebSocketException; 029import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; 030 031public abstract class AbstractWebSocket { 032 033 protected static final Logger LOGGER = Logger.getLogger(AbstractWebSocket.class.getName()); 034 035 protected static final String SEC_WEBSOCKET_PROTOCOL_HEADER_FILED_NAME = "Sec-WebSocket-Protocol"; 036 protected static final String SEC_WEBSOCKET_PROTOCOL_HEADER_FILED_VALUE_XMPP = "xmpp"; 037 038 protected final SmackFuture.InternalSmackFuture<AbstractWebSocket, Exception> future = new SmackFuture.InternalSmackFuture<>(); 039 040 protected final ModularXmppClientToServerConnectionInternal connectionInternal; 041 042 protected final WebSocketRemoteConnectionEndpoint endpoint; 043 044 private final SmackWebSocketDebugger debugger; 045 046 protected AbstractWebSocket(WebSocketRemoteConnectionEndpoint endpoint, 047 ModularXmppClientToServerConnectionInternal connectionInternal) { 048 this.endpoint = endpoint; 049 this.connectionInternal = connectionInternal; 050 051 final SmackDebugger smackDebugger = connectionInternal.smackDebugger; 052 if (smackDebugger != null) { 053 debugger = new SmackWebSocketDebugger(smackDebugger); 054 } else { 055 debugger = null; 056 } 057 } 058 059 public final WebSocketRemoteConnectionEndpoint getEndpoint() { 060 return endpoint; 061 } 062 063 private String streamOpen; 064 private String streamClose; 065 066 protected final void onIncomingWebSocketElement(String element) { 067 if (debugger != null) { 068 debugger.incoming(element); 069 } 070 071 // TODO: Once smack-websocket-java15 is there, we have to re-evaluate if the async operation here is still 072 // required, or if it should only be performed if OkHTTP is used. 073 if (isOpenElement(element)) { 074 // Transform the XMPP WebSocket <open/> element to a RFC 6120 <stream> open tag. 075 streamOpen = getStreamFromOpenElement(element); 076 streamClose = connectionInternal.onStreamOpen(streamOpen); 077 return; 078 } 079 080 if (isCloseElement(element)) { 081 connectionInternal.onStreamClosed(); 082 return; 083 } 084 085 connectionInternal.withSmackDebugger(debugger -> debugger.onIncomingElementCompleted()); 086 087 // TODO: Do we need to wrap the element again in the stream open to get the 088 // correct XML scoping (just like the modular TCP connection does)? It appears 089 // that this not really required, as onStreamOpen() will set the incomingStreamEnvironment, which is used for 090 // parsing. 091 String wrappedCompleteElement = streamOpen + element + streamClose; 092 connectionInternal.parseAndProcessElement(wrappedCompleteElement); 093 } 094 095 static String getStreamFromOpenElement(String openElement) { 096 String streamElement = openElement.replaceFirst("\\A<open ", "<stream:stream ") 097 .replace("urn:ietf:params:xml:ns:xmpp-framing", "jabber:client") 098 .replaceFirst("/>\\s*\\z", " xmlns:stream='http://etherx.jabber.org/streams'>"); 099 return streamElement; 100 } 101 102 // TODO: Make this method less fragile, e.g. by parsing a little bit into the element to ensure that this is an 103 // <open/> element qualified by the correct namespace. 104 static boolean isOpenElement(String text) { 105 if (text.startsWith("<open ")) { 106 return true; 107 } 108 return false; 109 } 110 111 // TODO: Make this method less fragile, e.g. by parsing a little bit into the element to ensure that this is an 112 // <close/> element qualified by the correct namespace. The fragility comes due the fact that the element could, 113 // inter alia, be specified as 114 // <close:close xmlns:close="urn:ietf:params:xml:ns:xmpp-framing"/> 115 static boolean isCloseElement(String text) { 116 if (text.startsWith("<close xmlns='urn:ietf:params:xml:ns:xmpp-framing'/>")) { 117 return true; 118 } 119 return false; 120 } 121 122 protected void onWebSocketFailure(Throwable throwable) { 123 WebSocketException websocketException = new WebSocketException(throwable); 124 125 // If we are already connected, then we need to notify the connection that it got tear down. Otherwise we 126 // need to notify the thread calling connect() that the connection failed. 127 if (future.wasSuccessful()) { 128 connectionInternal.notifyConnectionError(websocketException); 129 } else { 130 future.setException(websocketException); 131 } 132 } 133 134 public final SmackFuture<AbstractWebSocket, Exception> getFuture() { 135 return future; 136 } 137 138 public final void send(TopLevelStreamElement element) { 139 XmlEnvironment outgoingStreamXmlEnvironment = connectionInternal.getOutgoingStreamXmlEnvironment(); 140 String elementString = element.toXML(outgoingStreamXmlEnvironment).toString(); 141 142 // TODO: We could make use of Java 11's WebSocket (is)last feature when sending 143 if (debugger != null) { 144 debugger.outgoing(elementString); 145 } 146 147 send(elementString); 148 } 149 150 protected abstract void send(String element); 151 152 public abstract void disconnect(int code, String message); 153 154 public boolean isConnectionSecure() { 155 return endpoint.isSecureEndpoint(); 156 } 157 158 public abstract SSLSession getSSLSession(); 159 160 @Override 161 public final String toString() { 162 return getClass().getSimpleName() + "[" + connectionInternal.connection + "]"; 163 } 164}