001/**
002 *
003 * Copyright 2020 Aditya Borikar, 2020-2021 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}