package com.faforever.client.relay;
import com.faforever.client.connectivity.DatagramGateway;
import com.faforever.client.connectivity.TurnServerAccessor;
import com.faforever.client.game.GameLaunchMessageBuilder;
import com.faforever.client.game.GameType;
import com.faforever.client.i18n.I18n;
import com.faforever.client.net.SocketAddressUtil;
import com.faforever.client.notification.NotificationService;
import com.faforever.client.preferences.ForgedAlliancePrefs;
import com.faforever.client.preferences.Preferences;
import com.faforever.client.preferences.PreferencesService;
import com.faforever.client.relay.event.GameFullEvent;
import com.faforever.client.remote.FafService;
import com.faforever.client.remote.domain.GameLaunchMessage;
import com.faforever.client.test.AbstractPlainJavaFxTest;
import com.faforever.client.user.UserService;
import com.google.common.eventbus.EventBus;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import org.apache.commons.compress.utils.IOUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.testfx.util.WaitForAsyncUtils;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.startsWith;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.util.SocketUtils.PORT_RANGE_MAX;
import static org.springframework.util.SocketUtils.PORT_RANGE_MIN;
public class LocalRelayServerImplTest extends AbstractPlainJavaFxTest {
private static final int TIMEOUT = 5000;
private static final TimeUnit TIMEOUT_UNIT = TimeUnit.MILLISECONDS;
private static final InetAddress LOOPBACK_ADDRESS = InetAddress.getLoopbackAddress();
private static final long SESSION_ID = 1234;
private static final double USER_ID = 872348.0;
private static final int GAME_PORT = 6112;
@Rule
public TemporaryFolder cacheDirectory = new TemporaryFolder();
private BlockingQueue<GpgClientMessage> messagesReceivedByFafServer;
private BlockingQueue<GpgServerMessage> messagesReceivedByGame;
private LocalRelayServerImpl instance;
private FaDataOutputStream gameToRelayOutputStream;
private FaDataInputStream gameFromRelayInputStream;
private Socket gameToRelaySocket;
private boolean stopped;
@Mock
private TurnServerAccessor turnServerAccessor;
@Mock
private UserService userService;
@Mock
private PreferencesService preferencesService;
@Mock
private FafService fafService;
@Mock
private ThreadPoolExecutor threadPoolExecutor;
@Mock
private DatagramGateway datagramGateway;
@Mock
private NotificationService notificationService;
@Mock
private I18n i18n;
@Mock
private EventBus eventBus;
@Captor
private ArgumentCaptor<Consumer<GameLaunchMessage>> gameLaunchMessageListenerCaptor;
@Captor
private ArgumentCaptor<Consumer<HostGameMessage>> hostGameMessageListenerCaptor;
@Captor
private ArgumentCaptor<Consumer<JoinGameMessage>> joinGameMessageListenerCaptor;
@Captor
private ArgumentCaptor<Consumer<ConnectToPeerMessage>> connectToPeerMessageListenerCaptor;
@Captor
private ArgumentCaptor<Consumer<DisconnectFromPeerMessage>> disconnectFromPeerMessageListenerCaptor;
@Before
public void setUp() throws Exception {
messagesReceivedByFafServer = new ArrayBlockingQueue<>(10);
messagesReceivedByGame = new ArrayBlockingQueue<>(10);
IntegerProperty portProperty = new SimpleIntegerProperty(GAME_PORT);
CountDownLatch gameConnectedLatch = new CountDownLatch(1);
instance = new LocalRelayServerImpl();
instance.userService = userService;
instance.preferencesService = preferencesService;
instance.fafService = fafService;
instance.threadPoolExecutor = threadPoolExecutor;
instance.notificationService = notificationService;
instance.i18n = i18n;
instance.eventBus = eventBus;
ForgedAlliancePrefs forgedAlliancePrefs = mock(ForgedAlliancePrefs.class);
Preferences preferences = mock(Preferences.class);
instance.addOnGameConnectedListener(gameConnectedLatch::countDown);
doAnswer(
invocation -> WaitForAsyncUtils.async(invocation.getArgumentAt(0, Runnable.class))
).when(threadPoolExecutor).execute(any(Runnable.class));
when(forgedAlliancePrefs.getPort()).thenReturn(GAME_PORT);
when(preferences.getForgedAlliance()).thenReturn(forgedAlliancePrefs);
when(preferencesService.getPreferences()).thenReturn(preferences);
when(preferencesService.getCacheDirectory()).thenReturn(cacheDirectory.getRoot().toPath());
when(forgedAlliancePrefs.portProperty()).thenReturn(portProperty);
when(userService.getUid()).thenReturn((int) USER_ID);
when(userService.getUsername()).thenReturn("junit");
when(fafService.getSessionId()).thenReturn(SESSION_ID);
doAnswer(invocation -> {
messagesReceivedByFafServer.put(invocation.getArgumentAt(0, GpgClientMessage.class));
return null;
}).when(fafService).sendGpgMessage(any());
instance.postConstruct();
verify(fafService).addOnMessageListener(eq(GameLaunchMessage.class), gameLaunchMessageListenerCaptor.capture());
verify(fafService).addOnMessageListener(eq(HostGameMessage.class), hostGameMessageListenerCaptor.capture());
verify(fafService).addOnMessageListener(eq(JoinGameMessage.class), joinGameMessageListenerCaptor.capture());
verify(fafService).addOnMessageListener(eq(ConnectToPeerMessage.class), connectToPeerMessageListenerCaptor.capture());
verify(fafService).addOnMessageListener(eq(DisconnectFromPeerMessage.class), disconnectFromPeerMessageListenerCaptor.capture());
verify(fafService, never()).addOnMessageListener(eq(CreateLobbyServerMessage.class), any());
GameLaunchMessage gameLaunchMessage = GameLaunchMessageBuilder.create().defaultValues().get();
gameLaunchMessage.setMod(GameType.DEFAULT.getString());
gameLaunchMessageListenerCaptor.getValue().accept(gameLaunchMessage);
instance.start(datagramGateway).toCompletableFuture().get(2, TimeUnit.SECONDS);
startFakeGameProcess();
gameConnectedLatch.await(TIMEOUT, TIMEOUT_UNIT);
assertTrue("Fake game did not connect within timeout", gameConnectedLatch.getCount() == 0);
}
private void startFakeGameProcess() throws IOException {
gameToRelaySocket = new Socket(LOOPBACK_ADDRESS, instance.getPort());
this.gameToRelayOutputStream = new FaDataOutputStream(gameToRelaySocket.getOutputStream());
this.gameFromRelayInputStream = new FaDataInputStream(gameToRelaySocket.getInputStream());
WaitForAsyncUtils.async(() -> {
while (!stopped) {
try {
GpgServerMessageType command = GpgServerMessageType.fromString(gameFromRelayInputStream.readString());
List<Object> args = gameFromRelayInputStream.readChunks();
GpgServerMessage message = new GpgServerMessage(command, args);
messagesReceivedByGame.add(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
@After
public void tearDown() {
stopped = true;
IOUtils.closeQuietly(gameToRelaySocket);
instance.close();
threadPoolExecutor.shutdownNow();
}
@Test
public void testIdle() throws Exception {
sendFromGame(new GpgClientMessage(GpgClientCommand.GAME_STATE, singletonList("Idle")));
GpgClientMessage gpgClientMessage = messagesReceivedByFafServer.poll(TIMEOUT, TIMEOUT_UNIT);
assertThat(gpgClientMessage.getCommand(), is(GpgClientCommand.GAME_STATE));
assertThat(gpgClientMessage.getArgs().get(0), is("Idle"));
}
/**
* Writes the specified message to the local relay server as if it was sent by the game.
*/
private void sendFromGame(GpgClientMessage message) throws IOException {
String action = message.getCommand().getString();
int headerSize = action.length();
String headerField = action.replace("\t", "/t").replace("\n", "/n");
gameToRelayOutputStream.writeInt(headerSize);
gameToRelayOutputStream.writeString(headerField);
gameToRelayOutputStream.writeArgs(message.getArgs());
gameToRelayOutputStream.flush();
}
@Test
public void testCreateLobbyUponIdle() throws Exception {
sendFromGame(new GpgClientMessage(GpgClientCommand.GAME_STATE, singletonList("Idle")));
GpgServerMessage relayMessage = messagesReceivedByGame.poll(TIMEOUT, TIMEOUT_UNIT);
assertThat(relayMessage.getMessageType(), is(GpgServerMessageType.CREATE_LOBBY));
List<Object> args = relayMessage.getArgs();
assertThat(args.get(0), is(LobbyMode.DEFAULT_LOBBY.getMode()));
assertThat((Integer) args.get(1), is(both(greaterThan(PORT_RANGE_MIN)).and(lessThan(PORT_RANGE_MAX))));
assertThat(args.get(2), is("junit"));
assertThat(args.get(3), is((int) USER_ID));
assertThat(args.get(4), is(1));
}
@Test
public void testHostGame() throws Exception {
HostGameMessage hostGameMessage = new HostGameMessage();
hostGameMessage.setArgs(singletonList("3v3 sand box.v0001"));
hostGameMessageListenerCaptor.getValue().accept(hostGameMessage);
GpgServerMessage receivedMessage = messagesReceivedByGame.poll(TIMEOUT, TIMEOUT_UNIT);
assertThat(receivedMessage.getMessageType(), is(GpgServerMessageType.HOST_GAME));
assertThat(receivedMessage.getArgs(), contains("3v3 sand box.v0001"));
}
@Test
public void testJoinGame() throws Exception {
enterIdleState();
try (DatagramSocket fakePeer = new DatagramSocket(new InetSocketAddress(InetAddress.getLocalHost(), 0))) {
JoinGameMessage joinGameMessage = new JoinGameMessage();
joinGameMessage.setArgs(Arrays.asList(Arrays.asList(fakePeer.getLocalAddress().getHostAddress(), fakePeer.getLocalPort()), "TechMonkey", 81655));
joinGameMessageListenerCaptor.getValue().accept(joinGameMessage);
GpgServerMessage receivedMessage = messagesReceivedByGame.poll(TIMEOUT, TIMEOUT_UNIT);
assertThat(receivedMessage.getMessageType(), is(GpgServerMessageType.JOIN_GAME));
List<Object> args = receivedMessage.getArgs();
assertThat(args, hasSize(3));
assertThat((String) args.get(0), startsWith("127.0.0.1:"));
assertThat(args.get(1), is("TechMonkey"));
assertThat(args.get(2), is(81655));
// Imitate the game sending a UDP packet to the joined peer
CompletableFuture<DatagramPacket> pingPacketFuture = new CompletableFuture<>();
doAnswer(invocation -> {
pingPacketFuture.complete(invocation.getArgumentAt(0, DatagramPacket.class));
return null;
}).when(datagramGateway).send(any());
byte[] data = new byte[]{0x05, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
DatagramPacket pingPacket = new DatagramPacket(data, data.length);
pingPacket.setSocketAddress(SocketAddressUtil.fromString((String) args.get(0)));
try (DatagramSocket fakeGameSocket = new DatagramSocket(instance.getGameSocketAddress())) {
fakeGameSocket.send(pingPacket);
}
assertThat(pingPacketFuture.get(3, TimeUnit.SECONDS).getSocketAddress(), is(fakePeer.getLocalSocketAddress()));
}
}
private void enterIdleState() throws IOException, InterruptedException {
sendFromGame(new GpgClientMessage(GpgClientCommand.GAME_STATE, singletonList("Idle")));
messagesReceivedByGame.poll(TIMEOUT, TIMEOUT_UNIT);
}
@Test
public void testConnectToPeer() throws Exception {
enterIdleState();
ConnectToPeerMessage connectToPeerMessage = new ConnectToPeerMessage();
connectToPeerMessage.setArgs(Arrays.asList(Arrays.asList("86.128.102.173", GAME_PORT), "Cadet", 79359));
connectToPeerMessageListenerCaptor.getValue().accept(connectToPeerMessage);
GpgServerMessage receivedMessage = messagesReceivedByGame.poll(TIMEOUT, TIMEOUT_UNIT);
assertThat(receivedMessage.getMessageType(), is(GpgServerMessageType.CONNECT_TO_PEER));
List<Object> args = receivedMessage.getArgs();
assertThat(args, hasSize(3));
assertThat((String) args.get(0), startsWith("127.0.0.1:"));
assertThat(args.get(1), is("Cadet"));
assertThat(args.get(2), is(79359));
}
@Test
public void testDisconnectFromPeer() throws Exception {
enterIdleState();
DisconnectFromPeerMessage disconnectFromPeerMessage = new DisconnectFromPeerMessage();
disconnectFromPeerMessage.setUid(79359);
disconnectFromPeerMessageListenerCaptor.getValue().accept(disconnectFromPeerMessage);
GpgServerMessage receivedMessage = messagesReceivedByGame.poll(TIMEOUT, TIMEOUT_UNIT);
assertThat(receivedMessage.getMessageType(), is(GpgServerMessageType.DISCONNECT_FROM_PEER));
assertThat(receivedMessage.getArgs(), contains(79359));
}
@Test
public void testGameFull() throws Exception {
when(i18n.get(anyString())).thenReturn("test");
sendFromGame(new GpgClientMessage(GpgClientCommand.GAME_FULL, emptyList()));
verify(eventBus, timeout(1000)).post(any(GameFullEvent.class));
verify(fafService, never()).sendGpgMessage(any(GpgClientMessage.class));
}
}