From 89fde1afc99f5a630e15761c3f3d50e1bd7ed118 Mon Sep 17 00:00:00 2001 From: alterdekim Date: Mon, 19 Feb 2024 15:05:20 +0300 Subject: [PATCH] longpoll api update, token auth --- .../alterdekim/game/component/LongPoll.java | 30 +++++++++++ .../game/component/LongPollingSession.java | 14 +++++ .../game/controller/APIController.java | 47 +++++++++++++---- .../game/controller/AuthController.java | 24 ++------- .../game/controller/StaticController.java | 51 +++++++++++++++++++ .../alterdekim/game/dto/AuthApiObject.java | 13 +++++ .../alterdekim/game/dto/LongPollResult.java | 1 + .../com/alterdekim/game/dto/RoomResult.java | 6 ++- .../game/repository/RoomRepository.java | 6 +++ .../game/security/SpringSecurity.java | 3 ++ .../game/service/RoomServiceImpl.java | 4 ++ .../java/com/alterdekim/game/util/Hash.java | 22 ++++++++ src/main/resources/static/javascript/games.js | 13 +++-- src/main/resources/templates/games.html | 1 + 14 files changed, 199 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/alterdekim/game/component/LongPoll.java create mode 100644 src/main/java/com/alterdekim/game/component/LongPollingSession.java create mode 100644 src/main/java/com/alterdekim/game/controller/StaticController.java create mode 100644 src/main/java/com/alterdekim/game/dto/AuthApiObject.java create mode 100644 src/main/java/com/alterdekim/game/util/Hash.java diff --git a/src/main/java/com/alterdekim/game/component/LongPoll.java b/src/main/java/com/alterdekim/game/component/LongPoll.java new file mode 100644 index 0000000..60028ce --- /dev/null +++ b/src/main/java/com/alterdekim/game/component/LongPoll.java @@ -0,0 +1,30 @@ +package com.alterdekim.game.component; + +import com.alterdekim.game.dto.LongPollResult; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +@Component +@Getter +@Slf4j +public class LongPoll { + + private final BlockingQueue longPollingQueue = new ArrayBlockingQueue<>(100); + + @Scheduled(fixedRate = 5000) + private void longPoll() { + getLongPollingQueue().forEach(longPollingSession -> { + try { + longPollingSession.getDeferredResult().setResult(new LongPollResult()); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + }); + getLongPollingQueue().removeIf(e -> e.getDeferredResult().isSetOrExpired()); + } +} diff --git a/src/main/java/com/alterdekim/game/component/LongPollingSession.java b/src/main/java/com/alterdekim/game/component/LongPollingSession.java new file mode 100644 index 0000000..5599a2b --- /dev/null +++ b/src/main/java/com/alterdekim/game/component/LongPollingSession.java @@ -0,0 +1,14 @@ +package com.alterdekim.game.component; + +import com.alterdekim.game.dto.LongPollResult; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.web.context.request.async.DeferredResult; + +@AllArgsConstructor +@Getter + +public class LongPollingSession { + private Long last_chat_id; + private DeferredResult deferredResult; +} diff --git a/src/main/java/com/alterdekim/game/controller/APIController.java b/src/main/java/com/alterdekim/game/controller/APIController.java index 0f93ccd..fc03868 100644 --- a/src/main/java/com/alterdekim/game/controller/APIController.java +++ b/src/main/java/com/alterdekim/game/controller/APIController.java @@ -1,20 +1,21 @@ package com.alterdekim.game.controller; +import com.alterdekim.game.component.LongPoll; +import com.alterdekim.game.component.LongPollingSession; import com.alterdekim.game.dto.*; import com.alterdekim.game.entities.Chat; +import com.alterdekim.game.entities.Room; import com.alterdekim.game.entities.User; import com.alterdekim.game.service.*; +import com.alterdekim.game.util.Hash; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.async.DeferredResult; import java.io.UnsupportedEncodingException; @@ -47,11 +48,16 @@ public class APIController { @Autowired private UserServiceImpl userService; + @Autowired + private LongPoll longPoll; + @GetMapping("/api/v1/games/list") public ResponseEntity> gamesList() { List results = roomService.getAll() .stream() - .map(room -> new RoomResult(room, roomPlayerService.findByRoomId(room.getId()))) + .map(room -> new RoomResult(room.getId(), room.getPlayerCount(), roomPlayerService.findByRoomId(room.getId()).stream() + .map(p -> userService.findById(p.getId())) + .map(p -> new UserResult(p.getId(), p.getUsername())).collect(Collectors.toList()))) .collect(Collectors.toList()); return ResponseEntity.ok(results); } @@ -126,10 +132,22 @@ public class APIController { return ResponseEntity.accepted().build(); } - @GetMapping("/api/v1/notify/get/{last_chat_id}/") - public ResponseEntity getNotify(@PathVariable Long last_chat_id ) { - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + @GetMapping("/async/notify/get/") + @ResponseBody + public DeferredResult getNotify(@RequestParam("last_chat_id") Long last_chat_id, + @RequestParam("accessToken") String accessToken, + @RequestParam("uid") Long userId) { + try { + User u = userService.findById(userId); + if (u == null) return null; + if (!Hash.sha256((u.getId() + u.getUsername() + u.getPassword()).getBytes()).equals(accessToken)) + return null; + } catch ( Exception e ) { + log.error(e.getMessage(), e); + } + final DeferredResult deferredResult = new DeferredResult<>(); + longPoll.getLongPollingQueue().add(new LongPollingSession(last_chat_id, deferredResult)); + /*Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Long userId = userService.findByUsername(((org.springframework.security.core.userdetails.User) authentication.getPrincipal()).getUsername()).getId(); userService.updateOnline(userId); @@ -160,6 +178,15 @@ public class APIController { c.setMessage(message); return c; }).collect(Collectors.toList()); - return ResponseEntity.ok(new LongPollResult(onlineCount, results, users)); + // Room stuff + + List rooms = roomService.getAllActive(); + List roomResults = rooms.stream() + .map( r -> new RoomResult(r.getId(), r.getPlayerCount(), roomPlayerService.findByRoomId(r.getId()).stream() + .map(p -> userService.findById(p.getId())) + .map(p -> new UserResult(p.getId(), p.getUsername())).collect(Collectors.toList()))) + .collect(Collectors.toList()); + return ResponseEntity.ok(new LongPollResult(onlineCount, results, users, roomResults));*/ + return deferredResult; } } diff --git a/src/main/java/com/alterdekim/game/controller/AuthController.java b/src/main/java/com/alterdekim/game/controller/AuthController.java index be5e029..9f8fc39 100644 --- a/src/main/java/com/alterdekim/game/controller/AuthController.java +++ b/src/main/java/com/alterdekim/game/controller/AuthController.java @@ -5,8 +5,11 @@ import com.alterdekim.game.entities.Invite; import com.alterdekim.game.entities.User; import com.alterdekim.game.service.InviteService; import com.alterdekim.game.service.UserService; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; @@ -33,33 +36,12 @@ public class AuthController { return "game"; } - @GetMapping("/rules") - public String rulesPage(Model model) { - return "rules"; - } - @GetMapping("/login") public String loginPage(Model model) { model.addAttribute("title", "Login" + base_title); return "login"; } - @GetMapping("/games") - public String gamesPage(Model model) { - return "games"; - } - - @GetMapping("/") - public String homePage(Model model) { - return "index"; - } - - @GetMapping("/access-denied") - public String accessDenied(Model model) { - model.addAttribute("title", "Access denied"); - return "access-denied"; - } - @GetMapping("/signup") public String showRegistrationForm(Model model) { UserDTO userDto = new UserDTO(); diff --git a/src/main/java/com/alterdekim/game/controller/StaticController.java b/src/main/java/com/alterdekim/game/controller/StaticController.java new file mode 100644 index 0000000..0f80c1b --- /dev/null +++ b/src/main/java/com/alterdekim/game/controller/StaticController.java @@ -0,0 +1,51 @@ +package com.alterdekim.game.controller; + +import com.alterdekim.game.dto.AuthApiObject; +import com.alterdekim.game.entities.User; +import com.alterdekim.game.service.UserServiceImpl; +import com.alterdekim.game.util.Hash; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Slf4j +@Controller +public class StaticController { + + @Autowired + private UserServiceImpl userService; + + @GetMapping("/rules") + public String rulesPage(Model model) { + return "rules"; + } + + @GetMapping("/games") + public String gamesPage(Model model) { + try { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + User u = userService.findByUsername(((org.springframework.security.core.userdetails.User) authentication.getPrincipal()).getUsername()); + Long userId = u.getId(); + String apiKey = Hash.sha256((u.getId() + u.getUsername() + u.getPassword()).getBytes()); + model.addAttribute("auth_obj", new AuthApiObject(apiKey, userId)); + } catch ( Exception e ) { + log.error(e.getMessage(), e); + } + return "games"; + } + + @GetMapping("/") + public String homePage(Model model) { + return "index"; + } + + @GetMapping("/access-denied") + public String accessDenied(Model model) { + model.addAttribute("title", "Access denied"); + return "access-denied"; + } +} diff --git a/src/main/java/com/alterdekim/game/dto/AuthApiObject.java b/src/main/java/com/alterdekim/game/dto/AuthApiObject.java new file mode 100644 index 0000000..f04f681 --- /dev/null +++ b/src/main/java/com/alterdekim/game/dto/AuthApiObject.java @@ -0,0 +1,13 @@ +package com.alterdekim.game.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class AuthApiObject { + private String accessToken; + private Long uid; +} diff --git a/src/main/java/com/alterdekim/game/dto/LongPollResult.java b/src/main/java/com/alterdekim/game/dto/LongPollResult.java index 9c57156..de218c2 100644 --- a/src/main/java/com/alterdekim/game/dto/LongPollResult.java +++ b/src/main/java/com/alterdekim/game/dto/LongPollResult.java @@ -14,4 +14,5 @@ public class LongPollResult { private Integer onlineCount; private List messages; private List users; + private List rooms; } diff --git a/src/main/java/com/alterdekim/game/dto/RoomResult.java b/src/main/java/com/alterdekim/game/dto/RoomResult.java index f6b9a1d..64d0040 100644 --- a/src/main/java/com/alterdekim/game/dto/RoomResult.java +++ b/src/main/java/com/alterdekim/game/dto/RoomResult.java @@ -2,6 +2,7 @@ package com.alterdekim.game.dto; import com.alterdekim.game.entities.Room; import com.alterdekim.game.entities.RoomPlayer; +import com.alterdekim.game.entities.User; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,6 +13,7 @@ import java.util.List; @NoArgsConstructor @Getter public class RoomResult { - private Room room; - private List players; + private Long id; + private Integer playerCount; + private List players; } diff --git a/src/main/java/com/alterdekim/game/repository/RoomRepository.java b/src/main/java/com/alterdekim/game/repository/RoomRepository.java index 40a7a74..a8a319e 100644 --- a/src/main/java/com/alterdekim/game/repository/RoomRepository.java +++ b/src/main/java/com/alterdekim/game/repository/RoomRepository.java @@ -3,8 +3,14 @@ package com.alterdekim.game.repository; import com.alterdekim.game.entities.Room; import com.alterdekim.game.entities.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface RoomRepository extends JpaRepository { + + @Query(value = "SELECT r FROM Room r WHERE r.isPrivate = false") + List findAllByActiveTrue(); } \ No newline at end of file diff --git a/src/main/java/com/alterdekim/game/security/SpringSecurity.java b/src/main/java/com/alterdekim/game/security/SpringSecurity.java index 2ddae27..11c35cb 100644 --- a/src/main/java/com/alterdekim/game/security/SpringSecurity.java +++ b/src/main/java/com/alterdekim/game/security/SpringSecurity.java @@ -1,6 +1,7 @@ package com.alterdekim.game.security; import com.alterdekim.game.handler.CustomAccessDeniedHandler; +import jakarta.servlet.DispatcherType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -13,6 +14,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.context.SecurityContextHolderFilter; import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -31,6 +33,7 @@ public class SpringSecurity { http.csrf().disable() .authorizeHttpRequests((authorize) -> authorize + .requestMatchers("/async/**").permitAll() .requestMatchers("/game").hasAnyAuthority("ROLE_ADMIN") .requestMatchers("/games").hasAnyAuthority("ROLE_ADMIN") .requestMatchers("/profile/**").hasAnyAuthority("ROLE_ADMIN") diff --git a/src/main/java/com/alterdekim/game/service/RoomServiceImpl.java b/src/main/java/com/alterdekim/game/service/RoomServiceImpl.java index e0039f8..98e9690 100644 --- a/src/main/java/com/alterdekim/game/service/RoomServiceImpl.java +++ b/src/main/java/com/alterdekim/game/service/RoomServiceImpl.java @@ -18,4 +18,8 @@ public class RoomServiceImpl implements RoomService { public List getAll() { return roomRepository.findAll(); } + + public List getAllActive() { + return roomRepository.findAllByActiveTrue(); + } } diff --git a/src/main/java/com/alterdekim/game/util/Hash.java b/src/main/java/com/alterdekim/game/util/Hash.java new file mode 100644 index 0000000..c854697 --- /dev/null +++ b/src/main/java/com/alterdekim/game/util/Hash.java @@ -0,0 +1,22 @@ +package com.alterdekim.game.util; + +import java.security.MessageDigest; + +public class Hash { + public static String sha256( byte[] b ) throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return bytesToHex(digest.digest(b)); + } + + private static String bytesToHex(byte[] hash) { + StringBuilder hexString = new StringBuilder(2 * hash.length); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } +} diff --git a/src/main/resources/static/javascript/games.js b/src/main/resources/static/javascript/games.js index 16eec97..a8407bf 100644 --- a/src/main/resources/static/javascript/games.js +++ b/src/main/resources/static/javascript/games.js @@ -23,9 +23,9 @@ function findUser(users, id) { function parseTime(unix_timestamp) { var date = new Date(unix_timestamp * 1000); - var hours = date.getHours(); + var hours = "0" + date.getHours(); var minutes = "0" + date.getMinutes(); - return hours + ':' + minutes.substr(-2); + return hours.substr(-2) + ':' + minutes.substr(-2); } function replyButtonClicked(obj) { @@ -35,8 +35,15 @@ function replyButtonClicked(obj) { } setInterval(function() { + let accessToken = $("api-tag").attr("data-access-token"); + let uid = $("api-tag").attr("data-uid"); $.ajax({ - url: "/api/v1/notify/get/" + last_chat_id + "/", + url: "/async/notify/get/", + data: { + last_chat_id: last_chat_id, + accessToken: accessToken, + uid: uid + }, method: "GET" }).done(function(data) { console.log(data); diff --git a/src/main/resources/templates/games.html b/src/main/resources/templates/games.html index 7642778..0ec05db 100644 --- a/src/main/resources/templates/games.html +++ b/src/main/resources/templates/games.html @@ -401,6 +401,7 @@ cursor: pointer; } +