diff --git a/build.gradle b/build.gradle index 948bfbc26c..7609ccc972 100644 --- a/build.gradle +++ b/build.gradle @@ -292,8 +292,9 @@ dependencies { implementation("org.springframework:spring-websocket") implementation("io.projectreactor.addons:reactor-extra") + implementation("io.projectreactor:reactor-tools") - def commonsVersion = "4a3facc824" + def commonsVersion = "c060e973aa54e1fa215df15c1b8bad22806d84aa" implementation("com.github.FAForever.faf-java-commons:faf-commons-data:${commonsVersion}") { exclude module: 'guava' @@ -363,4 +364,4 @@ dependencies { testCompileOnly("org.projectlombok:lombok") codacy("com.github.codacy:codacy-coverage-reporter:-SNAPSHOT") -} +} \ No newline at end of file diff --git a/src/lombok.config b/src/lombok.config index 9b2c34ed11..afe923bc9f 100644 --- a/src/lombok.config +++ b/src/lombok.config @@ -1,3 +1,4 @@ lombok.accessors.chain=true config.stopBubbling=true lombok.anyConstructor.addConstructorProperties=true +lombok.copyableAnnotations+=org.springframework.beans.factory.annotation.Qualifier diff --git a/src/main/java/com/faforever/client/api/FafApiAccessor.java b/src/main/java/com/faforever/client/api/FafApiAccessor.java index 92716deda2..7c1c7c5eb6 100644 --- a/src/main/java/com/faforever/client/api/FafApiAccessor.java +++ b/src/main/java/com/faforever/client/api/FafApiAccessor.java @@ -35,6 +35,7 @@ import com.github.jasminb.jsonapi.exceptions.ResourceParseException; import com.github.rutledgepaulv.qbuilders.builders.QBuilder; import com.github.rutledgepaulv.qbuilders.conditions.Condition; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.VisibleForTesting; @@ -74,6 +75,7 @@ @Slf4j @Component @Profile("!offline") +@RequiredArgsConstructor public class FafApiAccessor implements InitializingBean { @VisibleForTesting @@ -119,6 +121,7 @@ public class FafApiAccessor implements InitializingBean { private static final String JSONAPI_MEDIA_TYPE = "application/vnd.api+json;charset=utf-8"; private final ClientProperties clientProperties; + @Qualifier("apiWebClient") private final ObjectFactory apiWebClientFactory; private WebClient apiWebClient; @@ -126,12 +129,6 @@ public class FafApiAccessor implements InitializingBean { private CountDownLatch authorizedLatch = new CountDownLatch(1); - public FafApiAccessor(ClientProperties clientProperties, - @Qualifier("apiWebClient") ObjectFactory apiWebClientFactory) { - this.clientProperties = clientProperties; - this.apiWebClientFactory = apiWebClientFactory; - } - @Override public void afterPropertiesSet() { Api api = clientProperties.getApi(); diff --git a/src/main/java/com/faforever/client/config/ClientProperties.java b/src/main/java/com/faforever/client/config/ClientProperties.java index 893541edc4..e66af420eb 100644 --- a/src/main/java/com/faforever/client/config/ClientProperties.java +++ b/src/main/java/com/faforever/client/config/ClientProperties.java @@ -28,6 +28,7 @@ public class ClientProperties { private Imgur imgur = new Imgur(); private TrueSkill trueSkill = new TrueSkill(); private Api api = new Api(); + private User user = new User(); private Oauth oauth = new Oauth(); private UnitDatabase unitDatabase = new UnitDatabase(); private FAFDebugger fafDebugger = new FAFDebugger(); @@ -80,8 +81,7 @@ public static class Irc { @Data public static class Server { - private String host; - private int port; + private String url; private int retryDelaySeconds = 30; private int retryAttempts = 10; } @@ -148,11 +148,15 @@ public static class Api { private int maxPageSize = 10000; } + @Data + public static class User { + private String baseUrl; + } + public void updateFromEndpoint(ServerEndpoints serverEndpoints) { - SocketEndpoint lobby = serverEndpoints.getLobby(); - if (lobby != null) { - server.setHost(lobby.getHost()); - server.setPort(lobby.getPort()); + UrlEndpoint user = serverEndpoints.getUser(); + if (user != null) { + this.user.setBaseUrl(user.getUrl()); } SocketEndpoint liveReplay = serverEndpoints.getLiveReplay(); diff --git a/src/main/java/com/faforever/client/config/WebClientConfig.java b/src/main/java/com/faforever/client/config/WebClientConfig.java index ca5b8addc4..6c708cf0f9 100644 --- a/src/main/java/com/faforever/client/config/WebClientConfig.java +++ b/src/main/java/com/faforever/client/config/WebClientConfig.java @@ -22,4 +22,11 @@ public WebClient apiWebClient(WebClient.Builder webClientBuilder, OAuthTokenFilt return webClientBuilder.baseUrl(clientProperties.getApi().getBaseUrl()).filter(oAuthTokenFilter).build(); } + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + public WebClient userWebClient(WebClient.Builder webClientBuilder, OAuthTokenFilter oAuthTokenFilter, + ClientProperties clientProperties) { + return webClientBuilder.baseUrl(clientProperties.getUser().getBaseUrl()).filter(oAuthTokenFilter).build(); + } + } diff --git a/src/main/java/com/faforever/client/login/LoginController.java b/src/main/java/com/faforever/client/login/LoginController.java index 472874f438..b784af12ee 100644 --- a/src/main/java/com/faforever/client/login/LoginController.java +++ b/src/main/java/com/faforever/client/login/LoginController.java @@ -3,7 +3,7 @@ import com.faforever.client.config.ClientProperties; import com.faforever.client.config.ClientProperties.Irc; import com.faforever.client.config.ClientProperties.Replay; -import com.faforever.client.config.ClientProperties.Server; +import com.faforever.client.config.ClientProperties.User; import com.faforever.client.fx.Controller; import com.faforever.client.fx.FxApplicationThreadExecutor; import com.faforever.client.fx.JavaFxUtil; @@ -78,8 +78,7 @@ public class LoginController implements Controller { public Label loginErrorLabel; public Pane loginRoot; public GridPane serverConfigPane; - public TextField serverHostField; - public TextField serverPortField; + public TextField userUrlField; public TextField replayServerHostField; public TextField replayServerPortField; public TextField ircServerHostField; @@ -201,9 +200,8 @@ private void showClientOutdatedPane(String minimumVersion) { private void populateEndpointFields() { fxApplicationThreadExecutor.execute(() -> { - Server server = clientProperties.getServer(); - serverHostField.setText(server.getHost()); - serverPortField.setText(String.valueOf(server.getPort())); + User user = clientProperties.getUser(); + userUrlField.setText(user.getBaseUrl()); Replay replay = clientProperties.getReplay(); replayServerHostField.setText(replay.getRemoteHost()); replayServerPortField.setText(String.valueOf(replay.getRemotePort())); @@ -236,9 +234,8 @@ public void onDownloadUpdateButtonClicked() { public CompletableFuture onLoginButtonClicked() { initializeFuture.join(); - clientProperties.getServer() - .setHost(serverHostField.getText()) - .setPort(Integer.parseInt(serverPortField.getText())); + clientProperties.getUser() + .setBaseUrl(userUrlField.getText()); clientProperties.getReplay() .setRemoteHost(replayServerHostField.getText()) diff --git a/src/main/java/com/faforever/client/remote/FafServerAccessor.java b/src/main/java/com/faforever/client/remote/FafServerAccessor.java index 57a4c8181c..7bc8da408d 100644 --- a/src/main/java/com/faforever/client/remote/FafServerAccessor.java +++ b/src/main/java/com/faforever/client/remote/FafServerAccessor.java @@ -37,9 +37,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -70,16 +73,16 @@ public class FafServerAccessor implements InitializingBean, DisposableBean { private final EventBus eventBus; private final ClientProperties clientProperties; private final FafLobbyClient lobbyClient; + @Qualifier("userWebClient") + private final ObjectFactory userWebClientFactory; @Override public void afterPropertiesSet() throws Exception { eventBus.register(this); - getEvents(IrcPasswordInfo.class) - .doOnError(throwable -> log.error("Error processing irc password", throwable)) + getEvents(IrcPasswordInfo.class).doOnError(throwable -> log.error("Error processing irc password", throwable)) .retry() .subscribe(this::onIrcPassword); - getEvents(NoticeInfo.class) - .doOnError(throwable -> log.error("Error processing notice", throwable)) + getEvents(NoticeInfo.class).doOnError(throwable -> log.error("Error processing notice", throwable)) .retry() .subscribe(this::onNotice); @@ -119,16 +122,22 @@ public ReadOnlyObjectProperty connectionStateProperty() { } public Mono connectAndLogIn() { - Config config = new Config(tokenRetriever.getRefreshedTokenValue(), Version.getCurrentVersion(), clientProperties.getUserAgent(), clientProperties.getServer() - .getHost(), clientProperties.getServer().getPort() + 1, sessionId -> { - try { - return uidService.generate(String.valueOf(sessionId)); - } catch (IOException e) { - throw new UIDException("Cannot generate UID", e, "uid.generate.error"); - } - }, 1024 * 1024, false, clientProperties.getServer().getRetryAttempts(), clientProperties.getServer() - .getRetryDelaySeconds()); - return lobbyClient.connectAndLogin(config); + return userWebClientFactory.getObject() + .get() + .uri("/lobby/access") + .retrieve() + .bodyToMono(LobbyAccess.class) + .map(lobbyAccess -> new Config(tokenRetriever.getRefreshedTokenValue(), Version.getCurrentVersion(), clientProperties.getUserAgent(), lobbyAccess.accessUrl(), this::tryGenerateUid, 1024 * 1024, false, clientProperties.getServer() + .getRetryAttempts(), clientProperties.getServer().getRetryDelaySeconds())) + .flatMap(lobbyClient::connectAndLogin); + } + + private String tryGenerateUid(Long sessionId) { + try { + return uidService.generate(String.valueOf(sessionId)); + } catch (IOException e) { + throw new UIDException("Cannot generate UID", e, "uid.generate.error"); + } } public CompletableFuture requestHostGame(NewGameInfo newGameInfo) { diff --git a/src/main/java/com/faforever/client/remote/LobbyAccess.java b/src/main/java/com/faforever/client/remote/LobbyAccess.java new file mode 100644 index 0000000000..00592c9e5e --- /dev/null +++ b/src/main/java/com/faforever/client/remote/LobbyAccess.java @@ -0,0 +1,4 @@ +package com.faforever.client.remote; + +public record LobbyAccess(String accessUrl) { +} diff --git a/src/main/java/com/faforever/client/update/ClientConfiguration.java b/src/main/java/com/faforever/client/update/ClientConfiguration.java index d411c13be4..b3fa29d570 100644 --- a/src/main/java/com/faforever/client/update/ClientConfiguration.java +++ b/src/main/java/com/faforever/client/update/ClientConfiguration.java @@ -32,6 +32,8 @@ public static class GitHubRepo { public static class ServerEndpoints { private String name; private SocketEndpoint lobby; + private UrlEndpoint lobbyWebsocket; + private UrlEndpoint user; private SocketEndpoint irc; private SocketEndpoint liveReplay; private UrlEndpoint api; diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index fa59d369c3..4e26899b56 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,7 +1,6 @@ faf-client: server: - host: localhost - port: 8001 + url: ws://localhost:8003 irc: host: localhost diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index e3978570d9..bf565af817 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,7 +1,6 @@ faf-client: server: - host: lobby.faforever.com - port: 8001 + url: wss://ws.faforever.com irc: host: irc.faforever.com diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 8862c7bb0c..392b602c8a 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -1,7 +1,6 @@ faf-client: server: - host: lobby.test.faforever.com - port: 8001 + url: wss://lobby.test.faforever.com irc: host: irc.test.faforever.com diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 002774d8dd..9bd74b2122 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -574,9 +574,10 @@ mapVault.ladder = Ladder maps news.showLadderMaps = Show ladder maps ranked1v1.showMaps = Show ladder maps port = Port +login.userBaseUrl=User API base URL login.serverHost = Server host login.replayServerHost = Replay server host -login.apiBaseUrl = API base URL +login.apiBaseUrl=Game API base URL games.showModdedGames = Show modded games chat.userContext.copyUsername = Copy username userInfo.idleTimeFormat = Last seen\: {0} diff --git a/src/main/resources/theme/login/login.fxml b/src/main/resources/theme/login/login.fxml index 05e7500c7a..1ea0bec087 100644 --- a/src/main/resources/theme/login/login.fxml +++ b/src/main/resources/theme/login/login.fxml @@ -1,9 +1,20 @@ - - - - + + + + + + + + + + + + + + + @@ -76,9 +87,8 @@ - - + serverSentSink = Sinks.many().unicast().onBackpressureBuffer(); private DisposableServer disposableServer; + private MockWebServer mockApi; + @BeforeEach public void setUp() throws Exception { + mockApi = new MockWebServer(); + mockApi.start(); + objectMapper.registerModule(new Builder().build()) .registerModule(new JavaTimeModule()) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) @@ -149,12 +157,19 @@ public void setUp() throws Exception { startFakeFafLobbyServer(); - clientProperties.getServer() - .setHost(disposableServer.host()) - .setPort(disposableServer.port() - 1); + clientProperties.getUser() + .setBaseUrl("http://localhost:%d".formatted(mockApi.getPort())); clientProperties.setUserAgent("downlords-faf-client"); - instance = new FafServerAccessor(notificationService, i18n, taskScheduler, tokenRetriever, uidService, eventBus, clientProperties, new FafLobbyClient(objectMapper)); + WebClient webClient = WebClient.builder() + .baseUrl(String.format("http://localhost:%s", mockApi.getPort())) + .build(); + + mockApi.enqueue(new MockResponse() + .setBody(objectMapper.writeValueAsString(new LobbyAccess("http://localhost:%d".formatted(disposableServer.port())))) + .addHeader("Content-Type", "application/json;charset=utf-8")); + + instance = new FafServerAccessor(notificationService, i18n, taskScheduler, tokenRetriever, uidService, eventBus, clientProperties, new FafLobbyClient(objectMapper), () -> webClient); instance.afterPropertiesSet(); instance.addEventListener(ServerMessage.class, serverMessage -> { @@ -172,16 +187,16 @@ private ServerMessage parseServerString(String json) throws JsonProcessingExcept } private void startFakeFafLobbyServer() { - this.disposableServer = TcpServer.create() + this.disposableServer = HttpServer.create() .doOnConnection(connection -> { log.info("New Client connected to server"); - connection.addHandler(new LineEncoder(LineSeparator.UNIX)) // TODO: This is not working. Raise a bug ticket! Workaround below - .addHandler(new LineBasedFrameDecoder(1024 * 1024)); + connection.addHandlerFirst(new LineEncoder(LineSeparator.UNIX)) // TODO: This is not working. Raise a bug ticket! Workaround below + .addHandlerLast(new LineBasedFrameDecoder(1024 * 1024)); }) .doOnBound(disposableServer -> log.info("Fake server listening at {} on port {}", disposableServer.host(), disposableServer.port())) .noSSL() .host(LOOPBACK_ADDRESS.getHostAddress()) - .handle((inbound, outbound) -> { + .route(routes -> routes.ws("/", (inbound, outbound) -> { Mono inboundMono = inbound.receive() .asString(StandardCharsets.UTF_8) .doOnNext(message -> { @@ -195,8 +210,8 @@ private void startFakeFafLobbyServer() { return message + "\n"; }), StandardCharsets.UTF_8).then(); - return inboundMono.mergeWith(outboundMono); - }) + return Flux.firstWithSignal(inboundMono, outboundMono); + })) .bindNow(); } @@ -212,7 +227,8 @@ private void assertMessageContainsComponents(String command, String... values) { } @AfterEach - public void tearDown() { + public void tearDown() throws IOException { + mockApi.shutdown(); disposableServer.disposeNow(); instance.disconnect(); }