Skip to content

Commit 0160342

Browse files
committed
Initial implementation of WebSocket
1 parent d8059be commit 0160342

File tree

60 files changed

+2138
-339
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+2138
-339
lines changed

authorization/src/main/java/eu/solven/kumite/oauth2/authorizationserver/KumiteTokenService.java

+6-7
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import com.nimbusds.jwt.JWTClaimsSet;
2323
import com.nimbusds.jwt.SignedJWT;
2424

25-
import eu.solven.kumite.account.internal.KumiteUserRaw;
2625
import eu.solven.kumite.login.AccessTokenWrapper;
2726
import eu.solven.kumite.login.RefreshTokenWrapper;
2827
import eu.solven.kumite.oauth2.IKumiteOAuth2Constants;
@@ -76,7 +75,7 @@ public static JWK generateSignatureSecret(IUuidGenerator uuidGenerator) {
7675
return jwk;
7776
}
7877

79-
public String generateAccessToken(KumiteUserRaw user,
78+
public String generateAccessToken(UUID accountId,
8079
Set<UUID> playerIds,
8180
Duration accessTokenValidity,
8281
boolean isRefreshToken) {
@@ -95,7 +94,7 @@ public String generateAccessToken(KumiteUserRaw user,
9594
Instant now = Instant.now();
9695

9796
String issuer = getIssuer();
98-
JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder().subject(user.getAccountId().toString())
97+
JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder().subject(accountId.toString())
9998
.audience("Kumite-Server")
10099
// https://connect2id.com/products/server/docs/api/token#url
101100
.issuer(issuer)
@@ -143,11 +142,11 @@ private String getIssuer() {
143142
* @return The generated JWT access token.
144143
* @throws IllegalStateException
145144
*/
146-
public AccessTokenWrapper wrapInJwtAccessToken(KumiteUserRaw user, UUID playerId) {
145+
public AccessTokenWrapper wrapInJwtAccessToken(UUID accountId, UUID playerId) {
147146
// access_token are short-lived
148147
Duration accessTokenValidity = Duration.parse("PT1H");
149148

150-
String accessToken = generateAccessToken(user, Set.of(playerId), accessTokenValidity, false);
149+
String accessToken = generateAccessToken(accountId, Set.of(playerId), accessTokenValidity, false);
151150

152151
// https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/
153152
return AccessTokenWrapper.builder()
@@ -161,11 +160,11 @@ public AccessTokenWrapper wrapInJwtAccessToken(KumiteUserRaw user, UUID playerId
161160

162161
// https://stackoverflow.com/questions/38986005/what-is-the-purpose-of-a-refresh-token
163162
// https://stackoverflow.com/questions/40555855/does-the-refresh-token-expire-and-if-so-when
164-
public RefreshTokenWrapper wrapInJwtRefreshToken(KumiteUserRaw user, Set<UUID> playerIds) {
163+
public RefreshTokenWrapper wrapInJwtRefreshToken(UUID accountId, Set<UUID> playerIds) {
165164
// refresh_token are long-lived
166165
Duration refreshTokenValidity = Duration.parse("P365D");
167166

168-
String accessToken = generateAccessToken(user, playerIds, refreshTokenValidity, true);
167+
String accessToken = generateAccessToken(accountId, playerIds, refreshTokenValidity, true);
169168

170169
// https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/
171170
return RefreshTokenWrapper.builder()

authorization/src/main/java/eu/solven/kumite/oauth2/resourceserver/KumiteResourceServerConfiguration.java

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package eu.solven.kumite.oauth2.resourceserver;
22

33
import java.text.ParseException;
4+
import java.util.concurrent.atomic.AtomicReference;
45

56
import javax.crypto.SecretKey;
67

@@ -27,6 +28,8 @@ public class KumiteResourceServerConfiguration {
2728

2829
public static final MacAlgorithm MAC_ALGORITHM = MacAlgorithm.HS256;
2930

31+
private static final AtomicReference<String> GENERATED_SIGNINGKEY = new AtomicReference<>();
32+
3033
// https://stackoverflow.com/questions/64758305/how-to-configure-a-reactive-resource-server-to-use-a-jwt-with-a-symmetric-key
3134
// https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/jwt.html
3235
// The browser will get an JWT given `/api/login/v1/token`. This route is protected by oauth2Login, and will
@@ -55,8 +58,15 @@ public static OctetSequenceKey loadOAuth2SigningKey(Environment env, IUuidGenera
5558
if (env.acceptsProfiles(Profiles.of(IKumiteSpringProfiles.P_PRDMODE))) {
5659
throw new IllegalStateException("Can not GENERATE oauth2 signingKey in `prodmode`");
5760
}
58-
log.warn("We generate a random signingKey");
59-
secretKeySpec = KumiteTokenService.generateSignatureSecret(uuidGenerator).toJSONString();
61+
synchronized (secretKeySpec) {
62+
if (GENERATED_SIGNINGKEY.get() == null) {
63+
log.warn("We generate a random signingKey");
64+
secretKeySpec = KumiteTokenService.generateSignatureSecret(uuidGenerator).toJSONString();
65+
GENERATED_SIGNINGKEY.set(secretKeySpec);
66+
} else {
67+
secretKeySpec = GENERATED_SIGNINGKEY.get();
68+
}
69+
}
6070
}
6171

6272
OctetSequenceKey octetSequenceKey = OctetSequenceKey.parse(secretKeySpec);

authorization/src/main/resources/application-unsafe_oauth2.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
kumite.oauth2:
44
# This key is used to sign refresh_token and access_token
5-
# We hardcode one in resource to make development easier
5+
# We hardcode the JWK in resource to make development easier
66
# Else, on each reboot, all access_token would be invalid
77
signing-key: '{"kty":"oct","kid":"d6ff447e-2a6e-4ef6-9e3d-792f2d23f11e","k":"bJpLdV8t1P_Pv9nay4zTVAU7C9VaBsR5pBtuAsAPkOU","alg":"HS256"}'
88
issuer-base-url: https://unsafe.oauth2.kumite

contest-core/src/main/java/eu/solven/kumite/app/KumiteServerComponentsConfiguration.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.concurrent.Executor;
44
import java.util.concurrent.Executors;
5+
import java.util.random.RandomGenerator;
56

67
import org.greenrobot.eventbus.EventBus;
78
import org.greenrobot.eventbus.Logger;
@@ -58,12 +59,19 @@
5859
@Slf4j
5960
public class KumiteServerComponentsConfiguration {
6061
@Bean
61-
public BoardLifecycleManager boardLifecycleManager(BoardsRegistry boardRegistry,
62+
public BoardLifecycleManager boardLifecycleManager(ContestsRegistry contestsRegistry,
63+
BoardsRegistry boardRegistry,
6264
ContestPlayersRegistry contestPlayersRegistry,
63-
EventBus eventBus) {
65+
EventBus eventBus,
66+
RandomGenerator randomGenerator) {
6467
final Executor boardEvolutionExecutor = Executors.newFixedThreadPool(4);
6568

66-
return new BoardLifecycleManager(boardRegistry, contestPlayersRegistry, boardEvolutionExecutor, eventBus);
69+
return new BoardLifecycleManager(contestsRegistry,
70+
boardRegistry,
71+
contestPlayersRegistry,
72+
boardEvolutionExecutor,
73+
eventBus,
74+
randomGenerator);
6775
}
6876

6977
@Bean
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
package eu.solven.kumite.board;
22

3-
import java.util.UUID;
4-
53
import org.greenrobot.eventbus.EventBus;
6-
import org.greenrobot.eventbus.Subscribe;
74
import org.springframework.beans.factory.InitializingBean;
85

9-
import eu.solven.kumite.contest.Contest;
106
import eu.solven.kumite.contest.ContestsRegistry;
11-
import eu.solven.kumite.events.BoardIsUpdated;
12-
import eu.solven.kumite.events.ContestIsGameover;
137
import lombok.AllArgsConstructor;
148

159
@AllArgsConstructor
@@ -22,14 +16,15 @@ public void afterPropertiesSet() {
2216
eventBus.register(this);
2317
}
2418

25-
@Subscribe
26-
public void onBoardUpdate(BoardIsUpdated boardIsUpdated) {
27-
UUID contestId = boardIsUpdated.getContestId();
28-
29-
Contest contest = contestsRegistry.getContest(contestId);
30-
if (contest.getGame().makeDynamicGameover(contest.getBoard()).isGameOver()) {
31-
eventBus.post(ContestIsGameover.builder().contestId(contestId).build());
32-
}
33-
}
19+
// BoardIsUpdated->ContestIsGameover is managed by BoardLifecycleManager
20+
// @Subscribe
21+
// public void onBoardUpdate(BoardIsUpdated boardIsUpdated) {
22+
// UUID contestId = boardIsUpdated.getContestId();
23+
//
24+
// Contest contest = contestsRegistry.getContest(contestId);
25+
// if (contest.getGame().makeDynamicGameover(contest.getBoard()).isGameOver()) {
26+
// eventBus.post(ContestIsGameover.builder().contestId(contestId).build());
27+
// }
28+
// }
3429

3530
}

contest-core/src/main/java/eu/solven/kumite/board/BoardLifecycleManager.java

+93-10
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
package eu.solven.kumite.board;
22

3+
import java.util.HashSet;
34
import java.util.List;
5+
import java.util.Map;
6+
import java.util.Set;
47
import java.util.UUID;
58
import java.util.concurrent.CountDownLatch;
69
import java.util.concurrent.Executor;
710
import java.util.concurrent.TimeUnit;
811
import java.util.concurrent.atomic.AtomicReference;
12+
import java.util.random.RandomGenerator;
913
import java.util.stream.Collectors;
1014

1115
import org.greenrobot.eventbus.EventBus;
1216

1317
import eu.solven.kumite.contest.Contest;
18+
import eu.solven.kumite.contest.ContestsRegistry;
19+
import eu.solven.kumite.events.ContestIsGameover;
20+
import eu.solven.kumite.events.PlayerCanMove;
1421
import eu.solven.kumite.events.PlayerJoinedBoard;
1522
import eu.solven.kumite.events.PlayerMoved;
23+
import eu.solven.kumite.move.IKumiteMove;
24+
import eu.solven.kumite.move.INoOpKumiteMove;
1625
import eu.solven.kumite.move.PlayerMoveRaw;
1726
import eu.solven.kumite.player.ContestPlayersRegistry;
1827
import eu.solven.kumite.player.PlayerJoinRaw;
@@ -22,6 +31,7 @@
2231
@RequiredArgsConstructor
2332
@Slf4j
2433
public class BoardLifecycleManager {
34+
final ContestsRegistry contestsRegistry;
2535
final BoardsRegistry boardRegistry;
2636

2737
final ContestPlayersRegistry contestPlayersRegistry;
@@ -31,13 +41,15 @@ public class BoardLifecycleManager {
3141

3242
final EventBus eventBus;
3343

44+
final RandomGenerator randomGenerator;
45+
3446
/**
3547
* When this returns, the caller is guaranteed its change has been executed
3648
*
3749
* @param contestId
3850
* @param runnable
3951
*/
40-
public void executeBoardChange(UUID contestId, Runnable runnable) {
52+
protected void executeBoardChange(UUID contestId, Runnable runnable) {
4153
if (isDirect(boardEvolutionExecutor)) {
4254
boardEvolutionExecutor.execute(runnable);
4355
} else {
@@ -94,29 +106,83 @@ protected static boolean isDirect(Executor executor) {
94106
return false;
95107
}
96108

97-
public void registerPlayer(Contest contest, PlayerJoinRaw playerRegistrationRaw) {
109+
/**
110+
*
111+
* @param contest
112+
* @param playerRegistrationRaw
113+
* @return
114+
*/
115+
public IKumiteBoardView registerPlayer(Contest contest, PlayerJoinRaw playerRegistrationRaw) {
98116
UUID contestId = contest.getContestId();
117+
UUID playerId = playerRegistrationRaw.getPlayerId();
118+
119+
AtomicReference<IKumiteBoardView> refBoardView = new AtomicReference<>();
120+
121+
Set<UUID> enabledPlayersIds = new HashSet<>();
122+
99123
executeBoardChange(contestId, () -> {
124+
IKumiteBoard boardBefore = boardRegistry.makeDynamicBoardHolder(contestId).get();
125+
126+
Set<UUID> playerCanMoveBefore = playersCanMove(contestId, boardBefore);
127+
100128
// The registry takes in charge the registration in the board
101129
contestPlayersRegistry.registerPlayer(contest, playerRegistrationRaw);
130+
131+
IKumiteBoard boardAfter = boardRegistry.makeDynamicBoardHolder(contestId).get();
132+
133+
Set<UUID> playerCanMoveAfter = playersCanMove(contestId, boardBefore);
134+
135+
// This does an intersection: players turned movable can now move, through they could not move before
136+
enabledPlayersIds.addAll(playerCanMoveAfter);
137+
enabledPlayersIds.removeAll(playerCanMoveBefore);
138+
139+
refBoardView.set(boardAfter.asView(playerId));
102140
});
103141

142+
IKumiteBoardView boardViewPostMove = refBoardView.get();
143+
144+
if (boardViewPostMove == null) {
145+
throw new IllegalStateException("Should have failed, or have produced a view");
146+
}
147+
104148
// We submit the event out of threadPool.
105149
// Hence we are guaranteed the event is fully processed.
106150
// The event subscriber can process it synchronously (through beware of deep-stack in case of long event-chains)
107151
// Hence we do not guarantee other events interleaved when the event is processed
108-
eventBus.post(
109-
PlayerJoinedBoard.builder().contestId(contestId).playerId(playerRegistrationRaw.getPlayerId()).build());
152+
eventBus.post(PlayerJoinedBoard.builder().contestId(contestId).playerId(playerId).build());
153+
154+
enabledPlayersIds.forEach(enabledPlayerId -> {
155+
eventBus.post(PlayerCanMove.builder().contestId(contestId).playerId(playerId).build());
156+
});
157+
158+
return boardViewPostMove;
159+
}
160+
161+
private Set<UUID> playersCanMove(UUID contestId, IKumiteBoard board) {
162+
Contest contest = contestsRegistry.getContest(contestId);
163+
164+
Set<UUID> movablePlayerIds = board.snapshotPlayers()
165+
.stream()
166+
.filter(playerId -> canPlay(
167+
contest.getGame().exampleMoves(randomGenerator, board.asView(playerId), playerId)))
168+
.collect(Collectors.toSet());
169+
170+
return movablePlayerIds;
171+
}
172+
173+
private boolean canPlay(Map<String, IKumiteMove> exampleMoves) {
174+
return exampleMoves.values().stream().anyMatch(move -> !(move instanceof INoOpKumiteMove));
110175
}
111176

112177
public IKumiteBoardView onPlayerMove(Contest contest, PlayerMoveRaw playerMove) {
113178
UUID contestId = contest.getContestId();
179+
UUID playerId = playerMove.getPlayerId();
114180

115-
AtomicReference<IKumiteBoardView> refBoardView = new AtomicReference<>();
181+
AtomicReference<IKumiteBoard> refBoard = new AtomicReference<>();
116182

117-
executeBoardChange(contestId, () -> {
118-
UUID playerId = playerMove.getPlayerId();
183+
Set<UUID> enabledPlayersIds = new HashSet<>();
119184

185+
executeBoardChange(contestId, () -> {
120186
if (!contestPlayersRegistry.isRegisteredPlayer(contestId, playerId)) {
121187
List<UUID> contestPlayers = contestPlayersRegistry.makeDynamicHasPlayers(contestId)
122188
.getPlayers()
@@ -132,6 +198,8 @@ public IKumiteBoardView onPlayerMove(Contest contest, PlayerMoveRaw playerMove)
132198

133199
IKumiteBoard currentBoard = boardRegistry.makeDynamicBoardHolder(contestId).get();
134200

201+
Set<UUID> playerCanMoveBefore = playersCanMove(contestId, currentBoard);
202+
135203
// First `.checkMove`: these are generic checks (e.g. is the gamerOver?)
136204
try {
137205
contest.checkValidMove(playerMove);
@@ -144,20 +212,35 @@ public IKumiteBoardView onPlayerMove(Contest contest, PlayerMoveRaw playerMove)
144212
// This may still fail (e.g. the move is illegal given game rules)
145213
currentBoard.registerMove(playerMove);
146214

215+
Set<UUID> playerCanMoveAfter = playersCanMove(contestId, currentBoard);
216+
217+
// This does an intersection: players turned movable can now move, through they could not move before
218+
enabledPlayersIds.addAll(playerCanMoveAfter);
219+
enabledPlayersIds.removeAll(playerCanMoveBefore);
220+
147221
// Persist the board (e.g. for concurrent changes)
148222
boardRegistry.updateBoard(contestId, currentBoard);
149223

150-
refBoardView.set(currentBoard.asView(playerId));
224+
refBoard.set(currentBoard);
151225

152226
});
153227

154-
IKumiteBoardView boardViewPostMove = refBoardView.get();
228+
IKumiteBoard boardAfter = refBoard.get();
229+
IKumiteBoardView boardViewPostMove = boardAfter.asView(playerId);
155230

156231
if (boardViewPostMove == null) {
157232
throw new IllegalStateException("Should have failed, or have produced a view");
158233
}
159234

160-
eventBus.post(PlayerMoved.builder().contestId(contestId).playerId(playerMove.getPlayerId()).build());
235+
eventBus.post(PlayerMoved.builder().contestId(contestId).playerId(playerId).build());
236+
237+
enabledPlayersIds.forEach(enabledPlayerId -> {
238+
eventBus.post(PlayerCanMove.builder().contestId(contestId).playerId(playerId).build());
239+
});
240+
241+
if (contest.getGame().makeDynamicGameover(() -> boardAfter).isGameOver()) {
242+
eventBus.post(ContestIsGameover.builder().contestId(contestId).build());
243+
}
161244

162245
return boardViewPostMove;
163246
}

0 commit comments

Comments
 (0)