package com.faforever.client.replay; import com.faforever.client.game.GameInfoBean; import com.faforever.client.game.GameService; import com.faforever.client.i18n.I18n; import com.faforever.client.notification.Action; import com.faforever.client.notification.NotificationService; import com.faforever.client.notification.PersistentNotification; import com.faforever.client.notification.Severity; import com.faforever.client.remote.domain.GameState; import com.faforever.client.update.ClientUpdateService; import com.faforever.client.user.UserService; import com.google.common.primitives.Bytes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import javax.annotation.Resource; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.invoke.MethodHandles; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.util.Collections; import java.util.HashMap; import static com.github.nocatch.NoCatch.noCatch; public class ReplayServerImpl implements ReplayServer { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final int REPLAY_BUFFER_SIZE = 0x200; /** * This is a prefix used in the FA live replay protocol that needs to be stripped away when storing to a file. */ private static final byte[] LIVE_REPLAY_PREFIX = new byte[]{'P', '/'}; @Resource Environment environment; @Resource NotificationService notificationService; @Resource I18n i18n; @Resource GameService gameService; @Resource UserService userService; @Resource ReplayFileWriter replayFileWriter; @Resource ClientUpdateService clientUpdateService; private LocalReplayInfo replayInfo; private ServerSocket serverSocket; private boolean stoppedGracefully; /** * Returns the current millis the same way as python does since this is what's stored in the replay files *yay*. */ private static double pythonTime() { return System.currentTimeMillis() / 1000; } @Override public void stop() { if (serverSocket == null) { throw new IllegalStateException("Server has never been started"); } stoppedGracefully = true; noCatch(() -> serverSocket.close()); } @Override public void start(int uid) { stoppedGracefully = false; new Thread(() -> { Integer localReplayServerPort = environment.getProperty("localReplayServer.port", Integer.class); String fafReplayServerHost = environment.getProperty("fafReplayServer.host"); Integer fafReplayServerPort = environment.getProperty("fafReplayServer.port", Integer.class); logger.debug("Opening local replay server on port {}", localReplayServerPort); try (ServerSocket serverSocket = new ServerSocket(localReplayServerPort); Socket fafReplayServerSocket = new Socket(fafReplayServerHost, fafReplayServerPort)) { this.serverSocket = serverSocket; recordAndRelay(uid, serverSocket, new BufferedOutputStream(fafReplayServerSocket.getOutputStream())); } catch (IOException e) { if (stoppedGracefully) { return; } logger.warn("Error in replay server", e); notificationService.addNotification(new PersistentNotification( i18n.get("replayServer.listeningFailed", localReplayServerPort), Severity.WARN, Collections.singletonList(new Action(i18n.get("replayServer.retry"), event -> start(uid))) ) ); } }).start(); } private void initReplayInfo(int uid) { replayInfo = new LocalReplayInfo(); replayInfo.setUid(uid); replayInfo.setLaunchedAt(pythonTime()); replayInfo.setVersionInfo(new HashMap<>()); replayInfo.getVersionInfo().put("lobby", String.format("dfaf-%s", clientUpdateService.getCurrentVersion().getCanonical()) ); } private void recordAndRelay(int uid, ServerSocket serverSocket, OutputStream fafReplayOutputStream) throws IOException { Socket socket = serverSocket.accept(); logger.debug("Accepted connection from {}", socket.getRemoteSocketAddress()); initReplayInfo(uid); ByteArrayOutputStream replayData = new ByteArrayOutputStream(); boolean connectionToServerLost = false; byte[] buffer = new byte[REPLAY_BUFFER_SIZE]; try (InputStream inputStream = socket.getInputStream()) { int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { if (replayData.size() == 0 && Bytes.indexOf(buffer, LIVE_REPLAY_PREFIX) != -1) { int dataBeginIndex = Bytes.indexOf(buffer, (byte) 0x00) + 1; replayData.write(buffer, dataBeginIndex, bytesRead - dataBeginIndex); } else { replayData.write(buffer, 0, bytesRead); } if (!connectionToServerLost) { try { fafReplayOutputStream.write(buffer, 0, bytesRead); } catch (SocketException e) { // In case we lose connection to the replay server, just stop writing to it logger.warn("Connection to replay server lost ({})", e.getMessage()); connectionToServerLost = true; } } } } catch (Exception e) { logger.warn("Error while recording replay", e); throw e; } finally { try { fafReplayOutputStream.flush(); } catch (IOException e) { logger.warn("Could not flush FAF replay output stream", e); } } logger.debug("FAF has disconnected, writing replay data to file"); finishReplayInfo(); replayFileWriter.writeReplayDataToFile(replayData, replayInfo); } private void finishReplayInfo() { GameInfoBean gameInfoBean = gameService.getByUid(replayInfo.getUid()); replayInfo.setGameEnd(pythonTime()); replayInfo.setRecorder(userService.getUsername()); replayInfo.setComplete(true); replayInfo.setState(GameState.CLOSED); replayInfo.updateFromGameInfoBean(gameInfoBean); } }