/** * * Copyright 2019-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smack.tcp; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.jivesoftware.smack.SmackException.EndpointConnectionException; import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; import org.jivesoftware.smack.fsm.StateTransitionResult; import org.jivesoftware.smack.tcp.XmppTcpTransportModule.EstablishingTcpConnectionState; import org.jivesoftware.smack.tcp.rce.Rfc6120TcpRemoteConnectionEndpoint; import org.jivesoftware.smack.util.Async; import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint; import org.jivesoftware.smack.util.rce.RemoteConnectionException; public final class ConnectionAttemptState { private final ModularXmppClientToServerConnectionInternal connectionInternal; private final XmppTcpTransportModule.XmppTcpNioTransport.DiscoveredTcpEndpoints discoveredEndpoints; private final EstablishingTcpConnectionState establishingTcpConnectionState; // TODO: Check if we can re-use the socket channel in case some InetSocketAddress fail to connect to. final SocketChannel socketChannel; final List> connectionExceptions; EndpointConnectionException connectionException; boolean connected; long deadline; final Iterator connectionEndpointIterator; /** The current connection endpoint we are trying */ Rfc6120TcpRemoteConnectionEndpoint connectionEndpoint; Iterator inetAddressIterator; ConnectionAttemptState(ModularXmppClientToServerConnectionInternal connectionInternal, XmppTcpTransportModule.XmppTcpNioTransport.DiscoveredTcpEndpoints discoveredEndpoints, EstablishingTcpConnectionState establishingTcpConnectionState) throws IOException { this.connectionInternal = connectionInternal; this.discoveredEndpoints = discoveredEndpoints; this.establishingTcpConnectionState = establishingTcpConnectionState; socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); List endpoints = discoveredEndpoints.result.discoveredRemoteConnectionEndpoints; connectionEndpointIterator = endpoints.iterator(); connectionExceptions = new ArrayList<>(endpoints.size()); } StateTransitionResult.Failure establishTcpConnection() throws InterruptedException { RemoteConnectionEndpoint.InetSocketAddressCoupling address = nextAddress(); establishTcpConnection(address); synchronized (this) { while (!connected && connectionException == null) { final long now = System.currentTimeMillis(); if (now >= deadline) { return new StateTransitionResult.FailureCausedByTimeout("Timeout waiting to establish connection"); } wait (deadline - now); } } if (connected) { assert connectionException == null; // Success case: we have been able to establish a connection to one remote endpoint. return null; } return new StateTransitionResult.FailureCausedByException(connectionException); } private void establishTcpConnection( RemoteConnectionEndpoint.InetSocketAddressCoupling address) { TcpHostEvent.ConnectingToHostEvent connectingToHostEvent = new TcpHostEvent.ConnectingToHostEvent( establishingTcpConnectionState, address); connectionInternal.invokeConnectionStateMachineListener(connectingToHostEvent); final InetSocketAddress inetSocketAddress = address.getInetSocketAddress(); // TODO: Should use "connect timeout" instead of reply timeout. But first connect timeout needs to be moved from // XMPPTCPConnectionConfiguration. into XMPPConnectionConfiguration. deadline = System.currentTimeMillis() + connectionInternal.connection.getReplyTimeout(); try { connected = socketChannel.connect(inetSocketAddress); } catch (IOException e) { onIOExceptionWhenEstablishingTcpConnection(e, address); return; } if (connected) { TcpHostEvent.ConnectedToHostEvent connectedToHostEvent = new TcpHostEvent.ConnectedToHostEvent( establishingTcpConnectionState, address, true); connectionInternal.invokeConnectionStateMachineListener(connectedToHostEvent); synchronized (this) { notifyAll(); } return; } try { connectionInternal.registerWithSelector(socketChannel, SelectionKey.OP_CONNECT, (selectedChannel, selectedSelectionKey) -> { SocketChannel selectedSocketChannel = (SocketChannel) selectedChannel; boolean finishConnected; try { finishConnected = selectedSocketChannel.finishConnect(); } catch (IOException e) { Async.go(() -> onIOExceptionWhenEstablishingTcpConnection(e, address)); return; } if (!finishConnected) { Async.go(() -> onIOExceptionWhenEstablishingTcpConnection(new IOException("finishConnect() failed"), address)); return; } TcpHostEvent.ConnectedToHostEvent connectedToHostEvent = new TcpHostEvent.ConnectedToHostEvent( establishingTcpConnectionState, address, false); connectionInternal.invokeConnectionStateMachineListener(connectedToHostEvent); connected = true; synchronized (ConnectionAttemptState.this) { notifyAll(); } }); } catch (ClosedChannelException e) { onIOExceptionWhenEstablishingTcpConnection(e, address); } } private void onIOExceptionWhenEstablishingTcpConnection(IOException exception, RemoteConnectionEndpoint.InetSocketAddressCoupling failedAddress) { RemoteConnectionEndpoint.InetSocketAddressCoupling nextInetSocketAddress = nextAddress(); if (nextInetSocketAddress == null) { connectionException = EndpointConnectionException.from( discoveredEndpoints.result.lookupFailures, connectionExceptions); synchronized (this) { notifyAll(); } return; } RemoteConnectionException rce = new RemoteConnectionException<>( failedAddress, exception); connectionExceptions.add(rce); TcpHostEvent.ConnectionToHostFailedEvent connectionToHostFailedEvent = new TcpHostEvent.ConnectionToHostFailedEvent( establishingTcpConnectionState, nextInetSocketAddress, exception); connectionInternal.invokeConnectionStateMachineListener(connectionToHostFailedEvent); establishTcpConnection(nextInetSocketAddress); } private RemoteConnectionEndpoint.InetSocketAddressCoupling nextAddress() { if (inetAddressIterator == null || !inetAddressIterator.hasNext()) { if (!connectionEndpointIterator.hasNext()) { return null; } connectionEndpoint = connectionEndpointIterator.next(); inetAddressIterator = connectionEndpoint.getInetAddresses().iterator(); // Every valid connection addresspoint must have a non-empty collection of inet addresses. assert inetAddressIterator.hasNext(); } InetAddress inetAddress = inetAddressIterator.next(); return new RemoteConnectionEndpoint.InetSocketAddressCoupling<>(connectionEndpoint, inetAddress); } }