001/**
002 *
003 * Copyright 2019-2020 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.tcp;
018
019import java.io.IOException;
020import java.net.InetAddress;
021import java.net.InetSocketAddress;
022import java.nio.channels.ClosedChannelException;
023import java.nio.channels.SelectionKey;
024import java.nio.channels.SocketChannel;
025import java.util.ArrayList;
026import java.util.Iterator;
027import java.util.List;
028
029import org.jivesoftware.smack.SmackException.EndpointConnectionException;
030import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
031import org.jivesoftware.smack.fsm.StateTransitionResult;
032import org.jivesoftware.smack.tcp.XmppTcpTransportModule.EstablishingTcpConnectionState;
033import org.jivesoftware.smack.tcp.rce.Rfc6120TcpRemoteConnectionEndpoint;
034import org.jivesoftware.smack.util.Async;
035import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint;
036import org.jivesoftware.smack.util.rce.RemoteConnectionException;
037
038public final class ConnectionAttemptState {
039
040    private final ModularXmppClientToServerConnectionInternal connectionInternal;
041
042    private final XmppTcpTransportModule.XmppTcpNioTransport.DiscoveredTcpEndpoints discoveredEndpoints;
043
044    private final EstablishingTcpConnectionState establishingTcpConnectionState;
045
046    // TODO: Check if we can re-use the socket channel in case some InetSocketAddress fail to connect to.
047    final SocketChannel socketChannel;
048
049    final List<RemoteConnectionException<?>> connectionExceptions;
050
051    EndpointConnectionException connectionException;
052    boolean connected;
053    long deadline;
054
055    final Iterator<Rfc6120TcpRemoteConnectionEndpoint> connectionEndpointIterator;
056    /** The current connection endpoint we are trying */
057    Rfc6120TcpRemoteConnectionEndpoint connectionEndpoint;
058    Iterator<? extends InetAddress> inetAddressIterator;
059
060    ConnectionAttemptState(ModularXmppClientToServerConnectionInternal connectionInternal,
061                    XmppTcpTransportModule.XmppTcpNioTransport.DiscoveredTcpEndpoints discoveredEndpoints,
062                    EstablishingTcpConnectionState establishingTcpConnectionState) throws IOException {
063        this.connectionInternal = connectionInternal;
064        this.discoveredEndpoints = discoveredEndpoints;
065        this.establishingTcpConnectionState = establishingTcpConnectionState;
066
067        socketChannel = SocketChannel.open();
068        socketChannel.configureBlocking(false);
069
070        List<Rfc6120TcpRemoteConnectionEndpoint> endpoints = discoveredEndpoints.result.discoveredRemoteConnectionEndpoints;
071        connectionEndpointIterator = endpoints.iterator();
072        connectionExceptions = new ArrayList<>(endpoints.size());
073    }
074
075    StateTransitionResult.Failure establishTcpConnection() throws InterruptedException {
076        RemoteConnectionEndpoint.InetSocketAddressCoupling<Rfc6120TcpRemoteConnectionEndpoint> address = nextAddress();
077        establishTcpConnection(address);
078
079        synchronized (this) {
080            while (!connected && connectionException == null) {
081                final long now = System.currentTimeMillis();
082                if (now >= deadline) {
083                    return new StateTransitionResult.FailureCausedByTimeout("Timeout waiting to establish connection");
084                }
085                wait (deadline - now);
086            }
087        }
088        if (connected) {
089            assert connectionException == null;
090            // Success case: we have been able to establish a connection to one remote endpoint.
091            return null;
092        }
093
094        return new StateTransitionResult.FailureCausedByException<Exception>(connectionException);
095    }
096
097    private void establishTcpConnection(
098                    RemoteConnectionEndpoint.InetSocketAddressCoupling<Rfc6120TcpRemoteConnectionEndpoint> address) {
099        TcpHostEvent.ConnectingToHostEvent connectingToHostEvent = new TcpHostEvent.ConnectingToHostEvent(
100                        establishingTcpConnectionState, address);
101        connectionInternal.invokeConnectionStateMachineListener(connectingToHostEvent);
102
103        final InetSocketAddress inetSocketAddress = address.getInetSocketAddress();
104        // TODO: Should use "connect timeout" instead of reply timeout. But first connect timeout needs to be moved from
105        // XMPPTCPConnectionConfiguration. into XMPPConnectionConfiguration.
106        deadline = System.currentTimeMillis() + connectionInternal.connection.getReplyTimeout();
107        try {
108            connected = socketChannel.connect(inetSocketAddress);
109        } catch (IOException e) {
110            onIOExceptionWhenEstablishingTcpConnection(e, address);
111            return;
112        }
113
114        if (connected) {
115            TcpHostEvent.ConnectedToHostEvent connectedToHostEvent = new TcpHostEvent.ConnectedToHostEvent(
116                            establishingTcpConnectionState, address, true);
117            connectionInternal.invokeConnectionStateMachineListener(connectedToHostEvent);
118
119            synchronized (this) {
120                notifyAll();
121            }
122            return;
123        }
124
125        try {
126            connectionInternal.registerWithSelector(socketChannel, SelectionKey.OP_CONNECT,
127                    (selectedChannel, selectedSelectionKey) -> {
128                        SocketChannel selectedSocketChannel = (SocketChannel) selectedChannel;
129
130                        boolean finishConnected;
131                        try {
132                            finishConnected = selectedSocketChannel.finishConnect();
133                        } catch (IOException e) {
134                            Async.go(() -> onIOExceptionWhenEstablishingTcpConnection(e, address));
135                            return;
136                        }
137
138                        if (!finishConnected) {
139                            Async.go(() -> onIOExceptionWhenEstablishingTcpConnection(new IOException("finishConnect() failed"), address));
140                            return;
141                        }
142
143                        TcpHostEvent.ConnectedToHostEvent connectedToHostEvent = new TcpHostEvent.ConnectedToHostEvent(
144                                        establishingTcpConnectionState, address, false);
145                        connectionInternal.invokeConnectionStateMachineListener(connectedToHostEvent);
146
147                        connected = true;
148                        synchronized (ConnectionAttemptState.this) {
149                            notifyAll();
150                        }
151                    });
152        } catch (ClosedChannelException e) {
153            onIOExceptionWhenEstablishingTcpConnection(e, address);
154        }
155    }
156
157    private void onIOExceptionWhenEstablishingTcpConnection(IOException exception,
158                    RemoteConnectionEndpoint.InetSocketAddressCoupling<Rfc6120TcpRemoteConnectionEndpoint> failedAddress) {
159        RemoteConnectionEndpoint.InetSocketAddressCoupling<Rfc6120TcpRemoteConnectionEndpoint> nextInetSocketAddress = nextAddress();
160        if (nextInetSocketAddress == null) {
161            connectionException = EndpointConnectionException.from(
162                            discoveredEndpoints.result.lookupFailures, connectionExceptions);
163            synchronized (this) {
164                notifyAll();
165            }
166            return;
167        }
168
169        RemoteConnectionException<Rfc6120TcpRemoteConnectionEndpoint> rce = new RemoteConnectionException<>(
170                        failedAddress, exception);
171        connectionExceptions.add(rce);
172
173        TcpHostEvent.ConnectionToHostFailedEvent connectionToHostFailedEvent = new TcpHostEvent.ConnectionToHostFailedEvent(
174                        establishingTcpConnectionState, nextInetSocketAddress, exception);
175        connectionInternal.invokeConnectionStateMachineListener(connectionToHostFailedEvent);
176
177        establishTcpConnection(nextInetSocketAddress);
178    }
179
180    private RemoteConnectionEndpoint.InetSocketAddressCoupling<Rfc6120TcpRemoteConnectionEndpoint> nextAddress() {
181        if (inetAddressIterator == null || !inetAddressIterator.hasNext()) {
182            if (!connectionEndpointIterator.hasNext()) {
183                return null;
184            }
185
186            connectionEndpoint = connectionEndpointIterator.next();
187            inetAddressIterator = connectionEndpoint.getInetAddresses().iterator();
188            // Every valid connection addresspoint must have a non-empty collection of inet addresses.
189            assert inetAddressIterator.hasNext();
190        }
191
192        InetAddress inetAddress = inetAddressIterator.next();
193
194        return new RemoteConnectionEndpoint.InetSocketAddressCoupling<>(connectionEndpoint, inetAddress);
195    }
196}