Skip to content

Conversation

@oxdjww
Copy link
Contributor

@oxdjww oxdjww commented May 25, 2025

Resolved Issue #6

Summary by CodeRabbit

  • 신규 기능

    • JWT 인증 기반의 WebSocket 시그널링 엔드포인트(/ws/signaling) 추가로 실시간 통신 지원.
    • WebSocket 연결 시 토큰 검증 및 사용자 식별 기능 제공.
    • 다중 사용자 룸 기반 시그널링 메시지 처리(입장, 퇴장, offer, answer, candidate 등) 지원.
  • 버그 수정

    • WebSocket 핸드셰이크 요청에 대해 JWT 인증 필터가 적용되지 않도록 예외 처리.
  • 문서

    • 테스트용 HTTP 요청 예시의 이메일 및 이름 정보 변경.

@oxdjww oxdjww self-assigned this May 25, 2025
@oxdjww oxdjww added the feature New feature or request label May 25, 2025
@oxdjww oxdjww linked an issue May 25, 2025 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link

coderabbitai bot commented May 25, 2025

Walkthrough

이 변경 사항은 백엔드에 WebSocket 기반 실시간 시그널링 기능을 추가하고, JWT 인증을 WebSocket 핸드셰이크에 적용합니다. 새로운 핸들러, 메시지 모델, 인터셉터, 보안 및 WebSocket 설정 클래스가 도입되었으며, 기존 인증 필터와 JWT 유틸리티에 WebSocket 지원을 위한 예외 처리가 추가되었습니다.

Changes

파일/경로 요약 변경 내용 요약
backend/build.gradle WebSocket 및 Jackson Databind 라이브러리 의존성 추가
backend/src/main/java/com/focussu/backend/auth/filter/JwtAuthenticationFilter.java WebSocket 핸드셰이크(GET, Upgrade: websocket, /ws/signaling) 요청은 필터 제외
backend/src/main/java/com/focussu/backend/auth/util/JwtTokenUtil.java UserDetails 없이 토큰만 검증하는 validateToken(String token) 메서드 추가
backend/src/main/java/com/focussu/backend/config/SecurityConfig.java "/ws/signaling" 경로를 인증 없이 허용하도록 보안 설정 수정
backend/src/main/java/com/focussu/backend/signalling/JwtHandshakeInterceptor.java JWT 기반 WebSocket 핸드셰이크 인터셉터 신규 추가
backend/src/main/java/com/focussu/backend/signalling/SignalingHandler.java WebSocket 시그널링 메시지 처리 핸들러 신규 추가
backend/src/main/java/com/focussu/backend/signalling/SignalingMessage.java 시그널링 메시지 모델 클래스 신규 추가
backend/src/main/java/com/focussu/backend/signalling/WebSecurityIgnoreConfig.java "/ws/signaling" 경로 Spring Security 무시 설정 신규 추가
backend/src/main/java/com/focussu/backend/signalling/WebSocketConfig.java WebSocket 활성화 및 시그널링 핸들러, JWT 인터셉터 등록 신규 추가
backend/src/main/resources/http/auth.http,
backend/src/main/resources/http/member.http
테스트용 이메일, 이름 등 일부 필드 값 변경

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant WebSocketConfig
    participant JwtHandshakeInterceptor
    participant JwtTokenUtil
    participant SignalingHandler

    Client->>WebSocketConfig: /ws/signaling으로 WebSocket 연결 요청
    WebSocketConfig->>JwtHandshakeInterceptor: 핸드셰이크 인터셉트
    JwtHandshakeInterceptor->>JwtTokenUtil: JWT 토큰 추출 및 검증
    JwtTokenUtil-->>JwtHandshakeInterceptor: 토큰 유효 여부 반환
    JwtHandshakeInterceptor-->>WebSocketConfig: 허용/거부 결정
    WebSocketConfig->>SignalingHandler: 연결 승인 시 세션 등록
    Client->>SignalingHandler: 시그널링 메시지 전송(join/offer/answer 등)
    SignalingHandler->>SignalingHandler: 메시지 파싱 및 처리, 브로드캐스트/전달
Loading

Poem

🐇
실시간 신호가 춤추는 밤,
토큰을 품고 입장해요,
방마다 토끼들이 뛰노는 듯
메시지 건너, 친구를 만나
WebSocket 문이 활짝 열렸네!

(깡총! 실시간의 세상으로)

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@oxdjww oxdjww merged commit 2ae7295 into main May 25, 2025
1 of 2 checks passed
@oxdjww oxdjww deleted the feat/#6 branch May 25, 2025 10:36
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (9)
backend/src/main/resources/http/auth.http (1)

11-11: 샘플 로그인 이메일 업데이트 확인
로그인 예시의 이메일이 [email protected]으로 적절히 변경되었습니다.

샘플 데이터가 여러 곳에 분산될 경우, {{TEST_USER_EMAIL}} 등의 환경 변수 플레이스홀더로 관리하여 일관성과 유지 보수성을 강화하는 것을 고려해 보세요.

backend/src/main/resources/http/member.http (1)

6-7: 회원가입 샘플 데이터 일관성 확인
회원가입 예시의 nameemail이 로그인 예시(auth.http)와 일치하도록 변경되었습니다.

동일한 테스트 유저 정보를 여러 파일에서 사용하면 유지 보수가 어려워질 수 있으니, 환경 변수 또는 전역 플레이스홀더({{TEST_USER_NAME}}, {{TEST_USER_EMAIL}})로 관리하는 방안을 검토해 보세요.

backend/src/main/java/com/focussu/backend/auth/filter/JwtAuthenticationFilter.java (1)

32-40: WebSocket 핸드셰이크 요청 제외 로직이 정확하게 구현되었습니다.

WebSocket 핸드셰이크 요청을 정확히 식별하여 JWT 필터에서 제외하는 로직이 잘 구현되었습니다. HTTP 메서드, Upgrade 헤더, URI를 모두 확인하여 안전합니다.

주석을 더 명확하게 개선할 수 있습니다:

-    // 1) WebSocket 핸드쉐이크는 GET + Upgrade:websocket
-
-    // 2) 이 외에도 필요하다면 특정 경로를 추가로 제외할 수 있습니다.
+    // WebSocket 핸드셰이크 요청(GET + Upgrade:websocket + /ws/signaling)은 
+    // JWT 필터링에서 제외하여 별도의 핸드셰이크 인터셉터에서 처리
backend/src/main/java/com/focussu/backend/signalling/WebSocketConfig.java (1)

28-31: 핸들러를 싱글톤으로 관리하는 것을 고려해보세요.

현재 구현에서는 문제없지만, 향후 상태를 관리해야 하는 경우 싱글톤 패턴이 더 적절할 수 있습니다.

+    private final SignalingHandler signalingHandler = new SignalingHandler();
+
     @Bean
     public WebSocketHandler signalingHandler() {
-        return new SignalingHandler();
+        return signalingHandler;
     }
backend/src/main/java/com/focussu/backend/signalling/JwtHandshakeInterceptor.java (2)

50-56: 토큰 검증과 사용자 정보 추출을 최적화하세요.

현재 validateTokengetUsernameFromToken을 각각 호출하여 토큰을 두 번 파싱합니다. 성능 개선을 위해 한 번에 처리하는 것을 고려해보세요.

         // 4) 검증
-        if (token == null || !jwtTokenUtil.validateToken(token)) {
+        if (token == null) {
             response.setStatusCode(HttpStatus.UNAUTHORIZED);
             return false;
         }
 
-        attrs.put("userId", jwtTokenUtil.getUsernameFromToken(token));
+        try {
+            String userId = jwtTokenUtil.getUsernameFromToken(token);
+            if (userId == null || !jwtTokenUtil.validateToken(token)) {
+                response.setStatusCode(HttpStatus.UNAUTHORIZED);
+                return false;
+            }
+            attrs.put("userId", userId);
+        } catch (Exception e) {
+            log.warn("[HandshakeInterceptor] 토큰 검증 실패: {}", e.getMessage());
+            response.setStatusCode(HttpStatus.UNAUTHORIZED);
+            return false;
+        }

60-68: 일관된 로깅 방식을 사용하세요.

System.out.println 대신 SLF4J logger를 사용하여 일관된 로깅 레벨과 포맷을 유지하세요.

     @Override
     public void afterHandshake(ServerHttpRequest request,
                                ServerHttpResponse response,
                                WebSocketHandler wsHandler,
                                Exception exception) {
-        // 아무 처리 필요 없으면 빈 바디로 둡니다.
-        // // 또는 디버깅용:
-        System.out.println("WebSocket Handshake 완료: " + request.getRemoteAddress());
+        log.debug("[HandshakeInterceptor] WebSocket Handshake 완료: {}", request.getRemoteAddress());
+        if (exception != null) {
+            log.error("[HandshakeInterceptor] Handshake 중 오류 발생: {}", exception.getMessage());
+        }
     }
backend/src/main/java/com/focussu/backend/signalling/SignalingHandler.java (3)

19-26: 클래스 설계가 잘 되어있지만 ObjectMapper 설정을 고려해보세요.

동시성을 고려한 ConcurrentHashMap 사용이 적절합니다. 다만 ObjectMapper의 보안 설정을 추가하는 것을 권장합니다.

ObjectMapper의 보안을 강화하려면 다음과 같이 설정하는 것을 고려해보세요:

-    private final ObjectMapper mapper = new ObjectMapper();
+    private final ObjectMapper mapper = new ObjectMapper()
+            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+            .configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true);

116-121: 방 멤버십 검증 로직이 개선 가능합니다.

현재 조건문이 복잡하고 가독성이 떨어집니다.

다음과 같이 가독성을 개선하세요:

         WebSocketSession ws = sessions.get(to);
-        if (ws != null && ws.isOpen() && rooms.getOrDefault(roomId, Collections.emptySet()).contains(to)) {
-            ws.sendMessage(new TextMessage(mapper.writeValueAsString(msg)));
+        Set<String> roomMembers = rooms.getOrDefault(roomId, Collections.emptySet());
+        
+        if (ws == null || !ws.isOpen()) {
+            log.warn("[Signaling] Target user {} session not available", to);
+            return;
+        }
+        
+        if (!roomMembers.contains(to)) {
+            log.warn("[Signaling] Target user {} not in room {}", to, roomId);
+            return;
+        }
+        
+        try {
+            ws.sendMessage(new TextMessage(mapper.writeValueAsString(msgCopy)));
             log.info("[Signaling] SEND to {} in room {}: {}", to, roomId, msg.getType());
-        } else {
-            log.warn("[Signaling] Cannot send to {} (not in room or closed)", to);
-        }
+        } catch (IOException e) {
+            log.error("[Signaling] Failed to send message to {}: {}", to, e.getMessage());
+        }

19-123: 전반적인 아키텍처 개선 제안사항들입니다.

코드의 기본 구조는 좋지만 몇 가지 개선할 부분이 있습니다.

다음 사항들을 고려해보세요:

  1. 메모리 관리: 빈 방(room) 정리 로직 추가
  2. 성능 최적화: 대용량 방에 대한 브로드캐스트 최적화
  3. 보안: 방 접근 권한 검증
  4. 모니터링: 메트릭 수집 (연결 수, 메시지 수 등)
  5. 설정: 최대 방 크기, 사용자당 최대 방 수 등의 제한 설정
// 예시: 빈 방 정리 로직
private void cleanupEmptyRooms() {
    rooms.entrySet().removeIf(entry -> entry.getValue().isEmpty());
}
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce5772d and eb9b5c5.

📒 Files selected for processing (11)
  • backend/build.gradle (1 hunks)
  • backend/src/main/java/com/focussu/backend/auth/filter/JwtAuthenticationFilter.java (1 hunks)
  • backend/src/main/java/com/focussu/backend/auth/util/JwtTokenUtil.java (1 hunks)
  • backend/src/main/java/com/focussu/backend/config/SecurityConfig.java (1 hunks)
  • backend/src/main/java/com/focussu/backend/signalling/JwtHandshakeInterceptor.java (1 hunks)
  • backend/src/main/java/com/focussu/backend/signalling/SignalingHandler.java (1 hunks)
  • backend/src/main/java/com/focussu/backend/signalling/SignalingMessage.java (1 hunks)
  • backend/src/main/java/com/focussu/backend/signalling/WebSecurityIgnoreConfig.java (1 hunks)
  • backend/src/main/java/com/focussu/backend/signalling/WebSocketConfig.java (1 hunks)
  • backend/src/main/resources/http/auth.http (1 hunks)
  • backend/src/main/resources/http/member.http (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
backend/src/main/java/com/focussu/backend/signalling/WebSecurityIgnoreConfig.java (1)
backend/src/main/java/com/focussu/backend/signalling/WebSocketConfig.java (1)
  • Configuration (11-32)
backend/src/main/java/com/focussu/backend/signalling/WebSocketConfig.java (1)
backend/src/main/java/com/focussu/backend/signalling/WebSecurityIgnoreConfig.java (1)
  • Configuration (7-14)
🔇 Additional comments (5)
backend/build.gradle (1)

50-52:

Details

❓ Verification inconclusive

중복 의존성 확인이 필요합니다.

Jackson Databind 의존성을 명시적으로 추가했지만, Spring Boot Web 스타터에 이미 포함되어 있을 수 있습니다. 불필요한 중복 의존성을 방지하기 위해 확인이 필요합니다.

다음 스크립트를 실행하여 의존성 중복을 확인해주세요:


🏁 Script executed:

#!/bin/bash
# 설명: Spring Boot에 포함된 Jackson 의존성 확인
# Jackson이 이미 포함되어 있는지 확인

# Spring Boot 스타터들이 가져오는 의존성 확인
echo "Checking if Jackson is already included in Spring Boot starters..."

# gradle dependencies로 의존성 트리 확인 (실제 프로젝트에서 실행 필요)
echo "Run 'gradle dependencies' to check dependency tree"
echo "Look for 'com.fasterxml.jackson.core:jackson-databind' in the output"

Length of output: 412


의존성 중복 여부 검토 필요

build.gradle에 명시한 com.fasterxml.jackson.core:jackson-databind이 Spring Boot 스타터 웹(WebSocket 포함)을 통해 이미 포함될 수 있으니 중복 여부를 확인해 주세요.

  • 위치: backend/build.gradle Lines 50-52
  • 확인 방법:
    ./gradlew dependencies --configuration compileClasspath \
      | grep com.fasterxml.jackson.core:jackson-databind

중복으로 확인되면 불필요한 의존성을 제거하거나 버전을 통일해 주세요.

backend/src/main/java/com/focussu/backend/auth/util/JwtTokenUtil.java (1)

86-95: WebSocket 인증을 위한 간소화된 토큰 검증 메서드가 잘 구현되었습니다.

UserDetails 없이 토큰만으로 유효성을 검증하는 메서드가 WebSocket 핸드셰이크 시나리오에 적합하게 구현되었습니다. 기존 메서드들을 재사용하여 일관성을 유지하고 있으며, 예외 처리도 적절합니다.

backend/src/main/java/com/focussu/backend/config/SecurityConfig.java (1)

106-106:

Details

✅ Verification successful

WebSocket 엔드포인트 보안 설정을 확인해주세요.

/ws/signaling 경로를 permitAll()로 설정했는데, 이는 HTTP 핸드셰이크 단계에서는 인증을 요구하지 않겠다는 의미입니다. 실제 WebSocket 인증은 핸드셰이크 인터셉터에서 처리되는 것으로 보이지만, 보안상 이 접근법이 적절한지 확인이 필요합니다.

다음 사항들을 확인해주세요:

  1. JwtHandshakeInterceptor가 제대로 구현되어 있는지
  2. 인증되지 않은 사용자의 WebSocket 연결 시도가 차단되는지
  3. 취약점이 없는지 보안 테스트가 필요합니다

🏁 Script executed:

#!/bin/bash
# WebSocket 핸드셰이크 인터셉터 구현 확인
rg -A 10 "JwtHandshakeInterceptor" --type java

Length of output: 2616


🏁 Script executed:

#!/bin/bash
# JWT 핸드셰이크 인터셉터 전체 구현 확인
cat backend/src/main/java/com/focussu/backend/signalling/JwtHandshakeInterceptor.java

Length of output: 2597


WebSocket 핸드셰이크 인증 처리 확인 완료
JwtHandshakeInterceptor의 beforeHandshake() 메서드에서 Authorization 헤더 또는 쿼리 파라미터로 전달된 토큰을 검증하고, 유효하지 않을 경우 HTTP 401 응답과 함께 false를 반환하므로
/ws/signalingpermitAll()로 설정한 뒤에도 인증되지 않은 연결은 모두 차단됩니다.
현재 설정은 적절하니 추가 조치가 필요 없습니다.

backend/src/main/java/com/focussu/backend/signalling/WebSecurityIgnoreConfig.java (1)

7-14: WebSocket 엔드포인트를 위한 적절한 보안 설정입니다.

WebSocket 핸드셰이크 요청이 Spring Security 필터 체인을 우회하도록 하는 설정이 올바르게 구현되었습니다. JwtHandshakeInterceptor에서 실제 JWT 인증을 처리하므로 이 구성이 적절합니다.

backend/src/main/java/com/focussu/backend/signalling/SignalingMessage.java (1)

8-21: WebRTC 시그널링을 위한 잘 설계된 메시지 모델입니다.

Lombok 어노테이션을 적절히 활용하여 간결한 코드를 작성했고, JsonNode를 사용한 유연한 페이로드 처리와 부분 생성자 제공이 좋습니다. WebRTC 시그널링 요구사항에 적합한 구조입니다.

Comment on lines +22 to +26
registry
.addHandler(signalingHandler(), "/ws/signaling")
.setAllowedOrigins("*")
.addInterceptors(new JwtHandshakeInterceptor(jwtTokenUtil));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

CORS 설정을 프로덕션 환경에 맞게 제한하세요.

setAllowedOrigins("*")는 개발 환경에서는 편리하지만 프로덕션에서는 보안 위험이 있습니다. 실제 프론트엔드 도메인으로 제한하는 것을 권장합니다.

-                .setAllowedOrigins("*")
+                .setAllowedOrigins("https://yourdomain.com", "https://localhost:3000")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
registry
.addHandler(signalingHandler(), "/ws/signaling")
.setAllowedOrigins("*")
.addInterceptors(new JwtHandshakeInterceptor(jwtTokenUtil));
}
registry
.addHandler(signalingHandler(), "/ws/signaling")
.setAllowedOrigins("https://yourdomain.com", "https://localhost:3000")
.addInterceptors(new JwtHandshakeInterceptor(jwtTokenUtil));
}
🤖 Prompt for AI Agents
In backend/src/main/java/com/focussu/backend/signalling/WebSocketConfig.java
around lines 22 to 26, the CORS setting uses setAllowedOrigins("*"), which is
insecure for production. Replace the wildcard "*" with the specific frontend
domain(s) allowed to connect, ensuring only trusted origins can access the
WebSocket endpoint.

Comment on lines +45 to +48
// 3) 로깅
log.info("[HandshakeInterceptor] 요청 URI: {}", request.getURI());
log.info("[HandshakeInterceptor] 토큰: {}", token);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

보안상 민감한 토큰 정보를 로그에 기록하지 마세요.

JWT 토큰을 로그에 기록하는 것은 보안 위험을 초래할 수 있습니다. 토큰 존재 여부만 로깅하거나 마스킹 처리를 권장합니다.

         // 3) 로깅
         log.info("[HandshakeInterceptor] 요청 URI: {}", request.getURI());
-        log.info("[HandshakeInterceptor] 토큰: {}", token);
+        log.info("[HandshakeInterceptor] 토큰 존재 여부: {}", token != null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 3) 로깅
log.info("[HandshakeInterceptor] 요청 URI: {}", request.getURI());
log.info("[HandshakeInterceptor] 토큰: {}", token);
// 3) 로깅
log.info("[HandshakeInterceptor] 요청 URI: {}", request.getURI());
log.info("[HandshakeInterceptor] 토큰 존재 여부: {}", token != null);
🤖 Prompt for AI Agents
In
backend/src/main/java/com/focussu/backend/signalling/JwtHandshakeInterceptor.java
around lines 45 to 48, the JWT token is being logged in plain text, which poses
a security risk. Modify the logging to avoid printing the full token; instead,
log only whether the token exists or mask part of the token to prevent exposing
sensitive information.

Comment on lines +80 to +83
String userId = (String) session.getAttributes().get("userId");
sessions.remove(userId);
rooms.values().forEach(m -> m.remove(userId));
log.info("[Signaling] DISCONNECTED: {}", userId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

연결 종료 시 userId null 체크가 필요합니다.

연결 종료 시에도 userId가 null일 가능성을 고려해야 합니다.

다음과 같이 null 체크를 추가하세요:

         String userId = (String) session.getAttributes().get("userId");
+        if (userId == null) {
+            log.warn("[Signaling] Cannot cleanup: userId not found in session attributes");
+            return;
+        }
         sessions.remove(userId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String userId = (String) session.getAttributes().get("userId");
sessions.remove(userId);
rooms.values().forEach(m -> m.remove(userId));
log.info("[Signaling] DISCONNECTED: {}", userId);
String userId = (String) session.getAttributes().get("userId");
if (userId == null) {
log.warn("[Signaling] Cannot cleanup: userId not found in session attributes");
return;
}
sessions.remove(userId);
rooms.values().forEach(m -> m.remove(userId));
log.info("[Signaling] DISCONNECTED: {}", userId);
🤖 Prompt for AI Agents
In backend/src/main/java/com/focussu/backend/signalling/SignalingHandler.java
around lines 80 to 83, add a null check for userId before using it. The current
code assumes userId is always non-null, but it can be null on connection
termination. Fix this by checking if userId is not null before removing it from
sessions and rooms, and before logging the disconnection event.

Comment on lines +29 to +30
String userId = (String) session.getAttributes().get("userId");
sessions.put(userId, session);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

userId null 체크 추가가 필요합니다.

세션 속성에서 userId를 가져올 때 null일 가능성을 고려해야 합니다.

다음과 같이 null 체크를 추가하세요:

-        String userId = (String) session.getAttributes().get("userId");
-        sessions.put(userId, session);
+        String userId = (String) session.getAttributes().get("userId");
+        if (userId == null) {
+            log.warn("[Signaling] Connection rejected: userId not found in session attributes");
+            session.close();
+            return;
+        }
+        sessions.put(userId, session);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String userId = (String) session.getAttributes().get("userId");
sessions.put(userId, session);
String userId = (String) session.getAttributes().get("userId");
if (userId == null) {
log.warn("[Signaling] Connection rejected: userId not found in session attributes");
session.close();
return;
}
sessions.put(userId, session);
🤖 Prompt for AI Agents
In backend/src/main/java/com/focussu/backend/signalling/SignalingHandler.java
around lines 29 to 30, add a null check for userId after retrieving it from
session attributes to prevent potential NullPointerException. If userId is null,
handle it appropriately, such as by skipping adding to sessions or logging a
warning, before calling sessions.put(userId, session).

Comment on lines +35 to +39
protected void handleTextMessage(WebSocketSession session, TextMessage msg) throws IOException {
SignalingMessage in = mapper.readValue(msg.getPayload(), SignalingMessage.class);
String from = (String) session.getAttributes().get("userId");
String roomId = in.getRoomId();
log.info("[Signaling] [{}] {} ▶ {}", roomId, from, in.getType());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

JSON 파싱 예외 처리 및 입력 검증이 필요합니다.

메시지 파싱 시 예외 처리와 기본적인 검증이 부족합니다.

다음과 같이 예외 처리와 검증을 추가하세요:

-        SignalingMessage in = mapper.readValue(msg.getPayload(), SignalingMessage.class);
-        String from = (String) session.getAttributes().get("userId");
-        String roomId = in.getRoomId();
+        SignalingMessage in;
+        try {
+            in = mapper.readValue(msg.getPayload(), SignalingMessage.class);
+        } catch (IOException e) {
+            log.warn("[Signaling] Invalid message format from {}: {}", session.getId(), e.getMessage());
+            return;
+        }
+        
+        String from = (String) session.getAttributes().get("userId");
+        if (from == null) {
+            log.warn("[Signaling] No userId in session attributes");
+            return;
+        }
+        
+        String roomId = in.getRoomId();
+        if (roomId == null || roomId.trim().isEmpty()) {
+            log.warn("[Signaling] Invalid roomId from {}", from);
+            return;
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected void handleTextMessage(WebSocketSession session, TextMessage msg) throws IOException {
SignalingMessage in = mapper.readValue(msg.getPayload(), SignalingMessage.class);
String from = (String) session.getAttributes().get("userId");
String roomId = in.getRoomId();
log.info("[Signaling] [{}] {} ▶ {}", roomId, from, in.getType());
protected void handleTextMessage(WebSocketSession session, TextMessage msg) throws IOException {
- SignalingMessage in = mapper.readValue(msg.getPayload(), SignalingMessage.class);
- String from = (String) session.getAttributes().get("userId");
- String roomId = in.getRoomId();
+ SignalingMessage in;
+ try {
+ in = mapper.readValue(msg.getPayload(), SignalingMessage.class);
+ } catch (IOException e) {
+ log.warn("[Signaling] Invalid message format from {}: {}", session.getId(), e.getMessage());
+ return;
+ }
+
+ String from = (String) session.getAttributes().get("userId");
+ if (from == null) {
+ log.warn("[Signaling] No userId in session attributes");
+ return;
+ }
+
+ String roomId = in.getRoomId();
+ if (roomId == null || roomId.trim().isEmpty()) {
+ log.warn("[Signaling] Invalid roomId from {}", from);
+ return;
+ }
log.info("[Signaling] [{}] {} ▶ {}", roomId, from, in.getType());
🤖 Prompt for AI Agents
In backend/src/main/java/com/focussu/backend/signalling/SignalingHandler.java
around lines 35 to 39, the JSON parsing of the incoming message lacks exception
handling and input validation. Wrap the mapper.readValue call in a try-catch
block to handle potential parsing exceptions, and add checks to validate that
the parsed SignalingMessage and its critical fields like roomId and type are not
null or invalid before proceeding. Log or handle errors appropriately to prevent
the method from failing silently or crashing.

Comment on lines +110 to +113
if (msg.getPayload() == null) {
msg.setPayload(mapper.createObjectNode());
}
((ObjectNode) msg.getPayload()).put("from", from);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

sendTo 메서드에서도 동일한 메시지 변조 문제가 있습니다.

broadcast 메서드와 동일한 문제입니다.

동일하게 메시지를 복사한 후 수정하도록 변경하세요:

-        if (msg.getPayload() == null) {
-            msg.setPayload(mapper.createObjectNode());
-        }
-        ((ObjectNode) msg.getPayload()).put("from", from);
+        SignalingMessage msgCopy = new SignalingMessage(msg.getType(), msg.getRoomId(), msg.getTo(), 
+                msg.getPayload() != null ? msg.getPayload().deepCopy() : mapper.createObjectNode());
+        ((ObjectNode) msgCopy.getPayload()).put("from", from);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (msg.getPayload() == null) {
msg.setPayload(mapper.createObjectNode());
}
((ObjectNode) msg.getPayload()).put("from", from);
SignalingMessage msgCopy = new SignalingMessage(
msg.getType(),
msg.getRoomId(),
msg.getTo(),
msg.getPayload() != null
? msg.getPayload().deepCopy()
: mapper.createObjectNode()
);
((ObjectNode) msgCopy.getPayload()).put("from", from);
🤖 Prompt for AI Agents
In backend/src/main/java/com/focussu/backend/signalling/SignalingHandler.java
around lines 110 to 113, the sendTo method modifies the original message payload
directly, causing unintended side effects similar to the broadcast method. To
fix this, create a copy of the message payload before making any modifications,
then update the copy with the "from" field and use this modified copy instead of
altering the original message.

Comment on lines +90 to +93
if (msg.getPayload() == null) {
msg.setPayload(mapper.createObjectNode());
}
((ObjectNode) msg.getPayload()).put("from", sender);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

메시지 객체 변조 문제가 있습니다.

원본 메시지 객체의 payload를 직접 수정하고 있어 예상치 못한 부작용이 발생할 수 있습니다.

메시지를 복사한 후 수정하도록 변경하세요:

-        if (msg.getPayload() == null) {
-            msg.setPayload(mapper.createObjectNode());
-        }
-        ((ObjectNode) msg.getPayload()).put("from", sender);
+        SignalingMessage msgCopy = new SignalingMessage(msg.getType(), msg.getRoomId(), msg.getTo(), 
+                msg.getPayload() != null ? msg.getPayload().deepCopy() : mapper.createObjectNode());
+        ((ObjectNode) msgCopy.getPayload()).put("from", sender);

그리고 이후 코드에서 msg 대신 msgCopy를 사용하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (msg.getPayload() == null) {
msg.setPayload(mapper.createObjectNode());
}
((ObjectNode) msg.getPayload()).put("from", sender);
SignalingMessage msgCopy = new SignalingMessage(
msg.getType(),
msg.getRoomId(),
msg.getTo(),
msg.getPayload() != null
? msg.getPayload().deepCopy()
: mapper.createObjectNode()
);
((ObjectNode) msgCopy.getPayload()).put("from", sender);
🤖 Prompt for AI Agents
In backend/src/main/java/com/focussu/backend/signalling/SignalingHandler.java
around lines 90 to 93, the code directly modifies the original message object's
payload, which can cause unintended side effects. To fix this, create a deep
copy of the message object before making any changes, then modify the payload on
this copied object. Replace all subsequent uses of the original message variable
with the new copied message variable to avoid mutating the original message.

Comment on lines +53 to +58
Set<String> members = rooms.getOrDefault(roomId, Collections.emptySet());
members.remove(from);
// (2) peer-left 방송
ObjectNode leavePayload = mapper.createObjectNode().put("from", from);
SignalingMessage leftMsg = new SignalingMessage("peer-left", roomId, null, leavePayload);
broadcast(roomId, leftMsg, from);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

동시성 문제 가능성이 있습니다.

방에서 사용자를 제거할 때 동시성 문제가 발생할 수 있습니다.

다음과 같이 안전하게 수정하세요:

-                Set<String> members = rooms.getOrDefault(roomId, Collections.emptySet());
-                members.remove(from);
+                rooms.computeIfPresent(roomId, (k, members) -> {
+                    members.remove(from);
+                    return members.isEmpty() ? null : members;
+                });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Set<String> members = rooms.getOrDefault(roomId, Collections.emptySet());
members.remove(from);
// (2) peer-left 방송
ObjectNode leavePayload = mapper.createObjectNode().put("from", from);
SignalingMessage leftMsg = new SignalingMessage("peer-left", roomId, null, leavePayload);
broadcast(roomId, leftMsg, from);
rooms.computeIfPresent(roomId, (k, members) -> {
members.remove(from);
return members.isEmpty() ? null : members;
});
// (2) peer-left 방송
ObjectNode leavePayload = mapper.createObjectNode().put("from", from);
SignalingMessage leftMsg = new SignalingMessage("peer-left", roomId, null, leavePayload);
broadcast(roomId, leftMsg, from);
🤖 Prompt for AI Agents
In backend/src/main/java/com/focussu/backend/signalling/SignalingHandler.java
around lines 53 to 58, the code removes a user from the room's member set
without synchronization, which can cause concurrency issues. To fix this, ensure
thread-safe access by synchronizing on the room's member set or using a
concurrent collection when modifying the members set. This will prevent
concurrent modification exceptions and maintain data consistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 스터디룸 참여

2 participants