package com.faforever.client.connectivity; import com.faforever.client.i18n.I18n; import com.faforever.client.relay.ConnectivityStateMessage; import com.faforever.client.relay.GpgServerMessage; import com.faforever.client.relay.ProcessNatPacketMessage; import com.faforever.client.remote.FafService; import com.faforever.client.remote.domain.MessageTarget; import com.faforever.client.task.CompletableTask; import com.faforever.client.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import javax.annotation.Resource; import java.lang.invoke.MethodHandles; import java.net.DatagramPacket; import java.net.InetSocketAddress; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import static java.nio.charset.StandardCharsets.US_ASCII; /** * Detects the connectivity state in cooperation with the FAF server. <p> <ol> <li>Step: </li> </ol> </p> */ public class ConnectivityCheckTask extends CompletableTask<ConnectivityStateMessage> { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @Resource I18n i18n; @Resource FafService fafService; @Value("${connectivityCheck.timeout}") int connectivityCheckTimeout; private DatagramGateway datagramGateway; private CompletableFuture<DatagramPacket> gamePortPacketFuture; private CompletableFuture<ConnectivityStateMessage> connectivityStateFuture; private Integer publicPort; public ConnectivityCheckTask() { super(Priority.LOW); } public void setDatagramGateway(DatagramGateway datagramGateway) { this.datagramGateway = datagramGateway; } public int getPublicPort() { return publicPort; } public void setPublicPort(int publicPort) { this.publicPort = publicPort; } private void onConnectivityStateMessage(GpgServerMessage message) { if (message.getTarget() != MessageTarget.CONNECTIVITY) { return; } switch (message.getMessageType()) { case SEND_NAT_PACKET: // The server did not receive the expected response and wants us to send a UDP packet in order hole punch the // NAT. This is done by connectivity service. gamePortPacketFuture.cancel(true); break; case CONNECTIVITY_STATE: // The server tells us what our connectivity state is, we're done gamePortPacketFuture.cancel(true); connectivityStateFuture.complete((ConnectivityStateMessage) message); break; } } @Override protected ConnectivityStateMessage call() throws Exception { Assert.checkNullIllegalState(publicPort, "publicPort has not been set"); updateTitle(i18n.get("portCheckTask.title")); connectivityStateFuture = new CompletableFuture<>(); Consumer<GpgServerMessage> connectivityStateMessageListener = this::onConnectivityStateMessage; fafService.addOnMessageListener(GpgServerMessage.class, connectivityStateMessageListener); try { runTestForPort(publicPort); return connectivityStateFuture.get(connectivityCheckTimeout, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { logger.warn("Connectivity state not received from server within " + connectivityCheckTimeout + "ms"); throw new RuntimeException(e); } finally { fafService.removeOnMessageListener(GpgServerMessage.class, connectivityStateMessageListener); } } private void runTestForPort(int port) { logger.info("Testing public connectivity of game port: {}", port); try { gamePortPacketFuture = listenForPackage(); fafService.initConnectivityTest(port); DatagramPacket udpPacket = gamePortPacketFuture.get(connectivityCheckTimeout, TimeUnit.MILLISECONDS); String message = new String(udpPacket.getData(), 1, udpPacket.getLength() - 1, US_ASCII); logger.info("Received UDP package on port {}: ", port, message); InetSocketAddress address = (InetSocketAddress) udpPacket.getSocketAddress(); ProcessNatPacketMessage processNatPacketMessage = new ProcessNatPacketMessage(address, message); processNatPacketMessage.setTarget(MessageTarget.CONNECTIVITY); fafService.sendGpgMessage(processNatPacketMessage); } catch (CancellationException e) { logger.debug("Waiting for UDP package on public game port has been cancelled"); } catch (TimeoutException e) { logger.debug("Waiting for UDP package on public game port timed out"); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } private CompletableFuture<DatagramPacket> listenForPackage() throws ExecutionException, InterruptedException { CompletableFuture<DatagramPacket> future = new CompletableFuture<>(); Consumer<DatagramPacket> complete = future::complete; datagramGateway.addOnPacketListener(complete); return future.thenComposeAsync(datagramPacket -> { datagramGateway.removeOnPacketListener(complete); return CompletableFuture.completedFuture(datagramPacket); }); } }