Skip to content

Commit

Permalink
Merge branch 'develop' of github.com:FAForever/downlords-faf-client i…
Browse files Browse the repository at this point in the history
…nto bugfix/#3262_fix_links
  • Loading branch information
obydog002 committed Dec 4, 2024
2 parents 532a18e + d732ff0 commit a147400
Show file tree
Hide file tree
Showing 15 changed files with 116 additions and 9 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ dependencies {
implementation("org.apache.commons:commons-compress:1.27.1")
implementation("net.java.dev.jna:jna:5.14.0")
implementation("net.java.dev.jna:jna-platform:5.14.0")
implementation("org.jetbrains:annotations:24.1.0")
implementation("org.jetbrains:annotations:26.0.1")
implementation("com.neovisionaries:nv-i18n:1.29")
implementation("com.nativelibs4java:bridj:0.7.0")
implementation("org.luaj:luaj-jse:3.0.1")
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
version=unspecified
javafxPlatform=unspecified
iceAdapterVersion=3.3.9
iceAdapterVersion=3.3.10
7 changes: 5 additions & 2 deletions src/main/java/com/faforever/client/api/TokenRetriever.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ public class TokenRetriever implements InitializingBean {

private final Mono<String> refreshedTokenMono = Mono.defer(this::refreshAccess)
.cacheInvalidateWhen(this::getExpirationMono)

.map(OAuth2AccessToken::getTokenValue);
.map(OAuth2AccessToken::getTokenValue);

@Override
public void afterPropertiesSet() throws Exception {
Expand Down Expand Up @@ -130,4 +129,8 @@ public void invalidateToken() {
public Flux<Long> invalidationFlux() {
return invalidateFlux;
}

public Mono<String> getAccessToken() {
return refreshedTokenMono;
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/faforever/client/game/GameRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ private boolean waitingForMatchMakerGame() {

private Process launchOnlineGame(GameParameters gameParameters, Integer gpgPort, Integer replayPort) {
fafServerAccessor.setPingIntervalSeconds(5);
fafServerAccessor.setTimeoutLoginReconnectSeconds(5);
gameKilled = false;
return forgedAllianceLaunchService.launchOnlineGame(gameParameters, gpgPort, replayPort);
}
Expand Down Expand Up @@ -400,6 +401,7 @@ private Mono<League> getDivisionInfo(String leaderboard) {

private void handleTermination(Process finishedProcess) {
fafServerAccessor.setPingIntervalSeconds(25);
fafServerAccessor.setTimeoutLoginReconnectSeconds(30);
int exitCode = finishedProcess.exitValue();
log.info("Forged Alliance terminated with exit code {}", exitCode);
Optional<Path> logFile = loggingService.getMostRecentGameLogFile();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
package com.faforever.client.headerbar;

import com.faforever.client.api.TokenRetriever;
import com.faforever.client.domain.server.PlayerInfo;
import com.faforever.client.fx.FxApplicationThreadExecutor;
import com.faforever.client.fx.NodeController;
import com.faforever.client.player.PlayerInfoWindowController;
import com.faforever.client.player.PlayerService;
import com.faforever.client.reporting.ReportDialogController;
import com.faforever.client.theme.UiService;
import com.faforever.client.user.LoginService;
import com.faforever.client.util.ClipboardUtil;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.MenuButton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Slf4j
@RequiredArgsConstructor
public class UserButtonController extends NodeController<Node> {

private final FxApplicationThreadExecutor fxApplicationThreadExecutor;
private final PlayerService playerService;
private final UiService uiService;
private final LoginService loginService;
private final TokenRetriever tokenRetriever;

public MenuButton userMenuButtonRoot;

Expand Down Expand Up @@ -57,6 +64,13 @@ public void onReport() {
reportDialogController.show();
}

public void onCopyAccessToken() {
tokenRetriever.getAccessToken().publishOn(fxApplicationThreadExecutor.asScheduler()).subscribe(accessToken -> {
log.info("Copied access token to clipboard");
ClipboardUtil.copyToClipboard(accessToken);
});
}

public void onLogOut() {
loginService.logOut();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.faforever.client.login;

import lombok.Getter;

@Getter
public class KnownLoginErrorException extends RuntimeException {
private final String i18nKey;

public KnownLoginErrorException(String message, String i18nKey) {
super(message);
this.i18nKey = i18nKey;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
@Slf4j
@RequiredArgsConstructor
public class LoginController extends NodeController<Pane> {

private final OperatingSystem operatingSystem;
private final GameRunner gameRunner;
private final LoginService loginService;
Expand Down Expand Up @@ -292,6 +291,8 @@ private Void onLoginFailed(Throwable throwable) {
notificationService.addNotification(
new ServerNotification(i18n.get("login.failed"), loginException.getMessage(), Severity.ERROR,
List.of(new DismissAction(i18n))));
} else if (throwable instanceof KnownLoginErrorException loginException) {
notificationService.addImmediateErrorNotification(throwable, loginException.getI18nKey());
} else {
log.error("Could not log in", throwable);
notificationService.addImmediateErrorNotification(throwable, "login.failed");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
Expand All @@ -33,6 +34,9 @@ public class OAuthValuesReceiver {

private static final Pattern CODE_PATTERN = Pattern.compile("code=([^ &]+)");
private static final Pattern STATE_PATTERN = Pattern.compile("state=([^ &]+)");
private static final Pattern ERROR_PATTERN = Pattern.compile("error=([^ &]+)");
private static final Pattern ERROR_SCOPE_DENIED = Pattern.compile("scope_denied");
private static final Pattern ERROR_NO_CSRF = Pattern.compile("No\\+CSRF\\+value");

private final PlatformService platformService;
private final LoginService loginService;
Expand Down Expand Up @@ -89,6 +93,7 @@ private Values readValues(String state, String codeVerifier) {

// Do not try with resources as the socket needs to stay open.
try {
checkForError(request);
Values values = readValues(request, redirectUri);
success = true;
return values;
Expand Down Expand Up @@ -139,13 +144,32 @@ private Values readValues(String request, URI redirectUri) {
return new Values(code, state, redirectUri);
}

private String formatRequest(String request) {
return URLDecoder.decode(request, StandardCharsets.UTF_8);
}

private String extractValue(String request, Pattern pattern) {
Matcher matcher = pattern.matcher(request);
if (!matcher.find()) {
throw new IllegalStateException("Could not extract value with pattern '" + pattern + "' from: " + request);
throw new IllegalStateException("Could not extract value with pattern '" + pattern + "' from: " + formatRequest(request));
}
return matcher.group(1);
}

private void checkForError(String request) {
Matcher matcher = ERROR_PATTERN.matcher(request);
if (matcher.find()) {
String errorMessage = "Login failed with error '" + matcher.group(1) + "'. The full request is: " + formatRequest(request);
if (ERROR_SCOPE_DENIED.matcher(request).find()) {
throw new KnownLoginErrorException(errorMessage, "login.scopeDenied");
}

if (ERROR_NO_CSRF.matcher(request).find()) {
throw new KnownLoginErrorException(errorMessage, "login.noCSRF");
}
throw new IllegalStateException(errorMessage);
}
}

public record Values(String code, String state, URI redirectUri) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import javafx.beans.property.ReadOnlyObjectWrapper;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
Expand Down Expand Up @@ -83,6 +84,9 @@ public class FafServerAccessor implements InitializingBean, DisposableBean, Life
private boolean autoReconnect;
@Getter
private boolean running;
@Getter
@Setter
private int timeoutLoginReconnectSeconds;

@Override
public void afterPropertiesSet() throws Exception {
Expand All @@ -98,6 +102,7 @@ public void start() {
.subscribe();

setPingIntervalSeconds(25);
setTimeoutLoginReconnectSeconds(30);

lobbyClient.getConnectionStatus()
.map(connectionStatus -> switch (connectionStatus) {
Expand Down Expand Up @@ -161,7 +166,7 @@ public Mono<Player> connectAndLogIn() {
clientProperties.getUserAgent(), lobbyUrl,
this::tryGenerateUid, 1024 * 1024, false)))
.flatMap(lobbyClient::connectAndLogin)
.timeout(Duration.ofSeconds(30))
.timeout(Duration.ofSeconds(timeoutLoginReconnectSeconds))
.retryWhen(createRetrySpec(clientProperties.getServer()));
}

Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ view.tiles = Tiles
view.table = Table
userMenu.logOut = Log out
userMenu.showProfile = Show profile
userMenu.copyAccessToken=Copy access token
menu.feedback = Feedback
menu.exit = Exit
menu.settings = Settings
Expand Down Expand Up @@ -1088,6 +1089,8 @@ map.all = All Versions
map.current = Current Version
login.remember = Remember Me
login.failed = Error occurred during login
login.scopeDenied = Login failed. You did not accept the scopes on the login page.
login.noCSRF = Login failed. Likely your login timed out, please try again.
login.badState = State returned by user service does not match initial state if this continues to occur please reach out to technical help on the forum or discord channel
login.oauthBaseUrl = OAuth base URL
session.expired.title = Session Expired
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/theme/headerbar/user_button.fxml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<items>
<MenuItem mnemonicParsing="false" onAction="#onShowProfile" text="%userMenu.showProfile"/>
<MenuItem mnemonicParsing="false" onAction="#onReport" text="%userMenu.moderationReport"/>
<MenuItem mnemonicParsing="false" onAction="#onCopyAccessToken" text="%userMenu.copyAccessToken"/>
<MenuItem mnemonicParsing="false" onAction="#onLogOut" text="%userMenu.logOut"/>
</items>
</MenuButton>
12 changes: 12 additions & 0 deletions src/test/java/com/faforever/client/api/TokenRetrieverTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,16 @@ public void testGetRefreshToken() throws Exception {

assertEquals(tokenProperties.get(REFRESH_TOKEN), loginPrefs.getRefreshToken());
}

@Test
public void testGetAccessToken() throws Exception {
loginPrefs.setRememberMe(true);
Map<String, String> tokenProperties = Map.of(EXPIRES_IN, "3600", REFRESH_TOKEN, "refresh", ACCESS_TOKEN, "test",
TOKEN_TYPE, "bearer");
prepareTokenResponse(tokenProperties);

StepVerifier.create(instance.getAccessToken())
.assertNext(accessToken -> assertEquals(accessToken, "test"))
.verifyComplete();
}
}
2 changes: 2 additions & 0 deletions src/test/java/com/faforever/client/game/GameRunnerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ public void testStartOnlineGlobalGame() throws Exception {
Integer uid = gameParameters.uid();

verify(fafServerAccessor).setPingIntervalSeconds(5);
verify(fafServerAccessor).setTimeoutLoginReconnectSeconds(5);
verify(leaderboardService, never()).getActiveLeagueEntryForPlayer(any(), any());
verify(mapService, never()).downloadIfNecessary(any());
verify(replayServer).start(uid);
Expand All @@ -234,6 +235,7 @@ public void testStartOnlineGlobalGame() throws Exception {
verify(replayServer).stop();
verify(fafServerAccessor).notifyGameEnded();
verify(fafServerAccessor).setPingIntervalSeconds(25);
verify(fafServerAccessor).setTimeoutLoginReconnectSeconds(30);
verify(notificationService).addNotification(any(PersistentNotification.class));
assertFalse(instance.isRunning());
assertNull(instance.getRunningProcessId());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.faforever.client.headerbar;

import com.faforever.client.api.TokenRetriever;
import com.faforever.client.builders.PlayerInfoBuilder;
import com.faforever.client.domain.server.PlayerInfo;
import com.faforever.client.player.PlayerInfoWindowController;
Expand All @@ -13,9 +14,11 @@
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import reactor.core.publisher.Mono;

import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class UserButtonControllerTest extends PlatformTest {
private static final String TEST_USER_NAME = "junit";
Expand All @@ -30,6 +33,8 @@ public class UserButtonControllerTest extends PlatformTest {
private ReportDialogController reportDialogController;
@Mock
private PlayerInfoWindowController playerInfoWindowController;
@Mock
private TokenRetriever tokenRetriever;


@InjectMocks
Expand Down Expand Up @@ -64,6 +69,15 @@ public void testReport() {
verify(reportDialogController).show();
}

@Test
public void testCopyAccessToken() {
when(tokenRetriever.getAccessToken()).thenReturn(Mono.just("someToken"));

instance.onCopyAccessToken();

verify(tokenRetriever).getAccessToken();
}

@Test
public void testLogOut() {
instance.onLogOut();
Expand Down
17 changes: 15 additions & 2 deletions src/test/java/com/faforever/client/login/LoginControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public void testLoginFails() throws Exception {
@Test
public void testLoginFailsNoPorts() throws Exception {
when(oAuthValuesReceiver.receiveValues(anyString(), anyString()))
.thenReturn(CompletableFuture.failedFuture(new IllegalStateException()));
.thenReturn(CompletableFuture.failedFuture(new IllegalStateException("")));

instance.onLoginButtonClicked();
WaitForAsyncUtils.waitForFxEvents();
Expand All @@ -170,6 +170,19 @@ public void testLoginFailsNoPorts() throws Exception {
assertTrue(instance.loginFormPane.isVisible());
}

@Test
public void testLoginFailsKnownError() throws Exception {
when(oAuthValuesReceiver.receiveValues(anyString(), anyString()))
.thenReturn(CompletableFuture.failedFuture(new KnownLoginErrorException("", "login.known")));

instance.onLoginButtonClicked();
WaitForAsyncUtils.waitForFxEvents();

verify(notificationService).addImmediateErrorNotification(any(), eq("login.known"));
assertFalse(instance.loginProgressPane.isVisible());
assertTrue(instance.loginFormPane.isVisible());
}

@Test
public void testLoginFailsTimeout() throws Exception {
when(oAuthValuesReceiver.receiveValues(anyString(), anyString()))
Expand Down Expand Up @@ -236,7 +249,7 @@ public void testLoginRefreshFails() {
.thenReturn(CompletableFuture.completedFuture(ClientConfigurationBuilder.create().defaultValues().get()));
loginPrefs.setRememberMe(true);
loginPrefs.setRefreshToken("abc");
when(loginService.loginWithRefreshToken()).thenReturn(Mono.error(new Exception()));
when(loginService.loginWithRefreshToken()).thenReturn(Mono.error(new Exception("")));
runOnFxThreadAndWait(() -> reinitialize(instance));
verify(loginService).loginWithRefreshToken();
assertFalse(instance.loginProgressPane.isVisible());
Expand Down

0 comments on commit a147400

Please sign in to comment.