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.io.IOException; 020import java.util.logging.Level; 021import java.util.logging.Logger; 022 023import javax.net.ssl.SSLSession; 024import javax.xml.namespace.QName; 025 026import org.jivesoftware.smack.SmackFuture; 027import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; 028import org.jivesoftware.smack.debugger.SmackDebugger; 029import org.jivesoftware.smack.packet.TopLevelStreamElement; 030import org.jivesoftware.smack.packet.XmlEnvironment; 031import org.jivesoftware.smack.util.PacketParserUtils; 032import org.jivesoftware.smack.websocket.WebSocketException; 033import org.jivesoftware.smack.websocket.elements.WebSocketCloseElement; 034import org.jivesoftware.smack.websocket.elements.WebSocketOpenElement; 035import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; 036import org.jivesoftware.smack.xml.XmlPullParser; 037import org.jivesoftware.smack.xml.XmlPullParserException; 038 039public abstract class AbstractWebSocket { 040 041 protected static final Logger LOGGER = Logger.getLogger(AbstractWebSocket.class.getName()); 042 043 protected static final String SEC_WEBSOCKET_PROTOCOL_HEADER_FILED_NAME = "Sec-WebSocket-Protocol"; 044 protected static final String SEC_WEBSOCKET_PROTOCOL_HEADER_FILED_VALUE_XMPP = "xmpp"; 045 046 protected final SmackFuture.InternalSmackFuture<AbstractWebSocket, Exception> future = new SmackFuture.InternalSmackFuture<>(); 047 048 protected final ModularXmppClientToServerConnectionInternal connectionInternal; 049 050 protected final WebSocketRemoteConnectionEndpoint endpoint; 051 052 private final SmackWebSocketDebugger debugger; 053 054 protected AbstractWebSocket(WebSocketRemoteConnectionEndpoint endpoint, 055 ModularXmppClientToServerConnectionInternal connectionInternal) { 056 this.endpoint = endpoint; 057 this.connectionInternal = connectionInternal; 058 059 final SmackDebugger smackDebugger = connectionInternal.smackDebugger; 060 if (smackDebugger != null) { 061 debugger = new SmackWebSocketDebugger(smackDebugger); 062 } else { 063 debugger = null; 064 } 065 } 066 067 public final WebSocketRemoteConnectionEndpoint getEndpoint() { 068 return endpoint; 069 } 070 071 private String streamOpen; 072 private String streamClose; 073 074 protected final void onIncomingWebSocketElement(String element) { 075 if (debugger != null) { 076 debugger.incoming(element); 077 } 078 079 // TODO: Once smack-websocket-java15 is there, we have to re-evaluate if the async operation here is still 080 // required, or if it should only be performed if OkHTTP is used. 081 if (isOpenElement(element)) { 082 // Transform the XMPP WebSocket <open/> element to a RFC 6120 <stream> open tag. 083 streamOpen = getStreamFromOpenElement(element); 084 streamClose = connectionInternal.onStreamOpen(streamOpen); 085 return; 086 } 087 088 if (isCloseElement(element)) { 089 connectionInternal.onStreamClosed(); 090 return; 091 } 092 093 connectionInternal.withSmackDebugger(debugger -> debugger.onIncomingElementCompleted()); 094 095 // TODO: Do we need to wrap the element again in the stream open to get the 096 // correct XML scoping (just like the modular TCP connection does)? It appears 097 // that this not really required, as onStreamOpen() will set the incomingStreamEnvironment, which is used for 098 // parsing. 099 String wrappedCompleteElement = streamOpen + element + streamClose; 100 connectionInternal.parseAndProcessElement(wrappedCompleteElement); 101 } 102 103 static String getStreamFromOpenElement(String openElement) { 104 String streamElement = openElement.replaceFirst("\\A<open ", "<stream:stream ") 105 .replace("urn:ietf:params:xml:ns:xmpp-framing", "jabber:client") 106 .replaceFirst("/>\\s*\\z", " xmlns:stream='http://etherx.jabber.org/streams'>") 107 .replaceFirst("></open>\\s*\\z", " xmlns:stream='http://etherx.jabber.org/streams'>"); 108 109 return streamElement; 110 } 111 112 static boolean isOpenElement(String text) { 113 XmlPullParser parser; 114 try { 115 parser = PacketParserUtils.getParserFor(text); 116 QName qname = parser.getQName(); 117 return qname.equals(WebSocketOpenElement.QNAME); 118 } catch (XmlPullParserException | IOException e) { 119 LOGGER.log(Level.WARNING, "Could not inspect \"" + text + "\" for open element", e); 120 return false; 121 } 122 } 123 124 static boolean isCloseElement(String text) { 125 XmlPullParser parser; 126 try { 127 parser = PacketParserUtils.getParserFor(text); 128 QName qname = parser.getQName(); 129 return qname.equals(WebSocketCloseElement.QNAME); 130 } catch (XmlPullParserException | IOException e) { 131 LOGGER.log(Level.WARNING, "Could not inspect \"" + text + "\" for close element", e); 132 return false; 133 } 134 } 135 136 protected void onWebSocketFailure(Throwable throwable) { 137 WebSocketException websocketException = new WebSocketException(throwable); 138 139 // If we are already connected, then we need to notify the connection that it got tear down. Otherwise we 140 // need to notify the thread calling connect() that the connection failed. 141 if (future.wasSuccessful()) { 142 connectionInternal.notifyConnectionError(websocketException); 143 } else { 144 future.setException(websocketException); 145 } 146 } 147 148 public final SmackFuture<AbstractWebSocket, Exception> getFuture() { 149 return future; 150 } 151 152 public final void send(TopLevelStreamElement element) { 153 XmlEnvironment outgoingStreamXmlEnvironment = connectionInternal.getOutgoingStreamXmlEnvironment(); 154 String elementString = element.toXML(outgoingStreamXmlEnvironment).toString(); 155 156 // TODO: We could make use of Java 11's WebSocket (is)last feature when sending 157 if (debugger != null) { 158 debugger.outgoing(elementString); 159 } 160 161 send(elementString); 162 } 163 164 protected abstract void send(String element); 165 166 public abstract void disconnect(int code, String message); 167 168 public boolean isConnectionSecure() { 169 return endpoint.isSecureEndpoint(); 170 } 171 172 public abstract SSLSession getSSLSession(); 173 174 @Override 175 public final String toString() { 176 return getClass().getSimpleName() + "[" + connectionInternal.connection + "]"; 177 } 178}