Skip to content

Commit 07bf9c9

Browse files
authored
Merge pull request #283 from prgrms-web-devcourse-final-project/Feat/255
Feat: TURN 서버 지원 추가 (#255)
2 parents 58236df + 5453063 commit 07bf9c9

File tree

5 files changed

+60
-12
lines changed

5 files changed

+60
-12
lines changed

src/main/java/com/back/global/websocket/webrtc/controller/WebRTCApiController.java

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99
import io.swagger.v3.oas.annotations.tags.Tag;
1010
import lombok.RequiredArgsConstructor;
1111
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.beans.factory.annotation.Value;
13+
import javax.crypto.Mac;
14+
import javax.crypto.spec.SecretKeySpec;
15+
import java.nio.charset.StandardCharsets;
16+
import java.util.ArrayList;
17+
import java.util.Base64;
1218
import org.springframework.boot.context.properties.EnableConfigurationProperties;
1319
import org.springframework.http.HttpStatus;
1420
import org.springframework.http.ResponseEntity;
1521
import org.springframework.web.bind.annotation.*;
16-
1722
import java.util.List;
1823

1924
@Slf4j
@@ -26,6 +31,15 @@ public class WebRTCApiController {
2631

2732
private final WebRTCProperties webRTCProperties;
2833

34+
@Value("${webrtc.turn.shared-secret}")
35+
private String turnSharedSecret;
36+
37+
@Value("${webrtc.turn.server-ip}")
38+
private String turnServerIp;
39+
40+
@Value("${webrtc.turn.ttl-seconds}")
41+
private long turnTtlSeconds;
42+
2943
@GetMapping("/ice-servers")
3044
@Operation(summary = "ICE 서버 설정 조회")
3145
public ResponseEntity<RsData<IceServerConfig>> getIceServers(
@@ -34,13 +48,35 @@ public ResponseEntity<RsData<IceServerConfig>> getIceServers(
3448

3549
log.info("ICE 서버 설정 요청 - userId: {}, roomId: {}", userId, roomId);
3650

37-
List<IceServer> iceServers =
51+
List<IceServer> iceServers = new ArrayList<>(
3852
webRTCProperties.iceServers().stream()
3953
.map(s -> new IceServer(s.urls(), s.username(), s.credential()))
40-
.toList();
54+
.toList()
55+
);
4156

42-
IceServerConfig config = new IceServerConfig(iceServers);
57+
// 동적으로 시간제한이 있는 TURN 서버 인증 정보 생성
58+
try {
59+
// 유효기간 타임스탬프를 생성 (현재 시간 + TTL)
60+
long expiry = (System.currentTimeMillis() / 1000) + turnTtlSeconds;
61+
String username = String.valueOf(expiry);
4362

63+
// HMAC-SHA1 알고리즘과 공유 비밀키를 사용하여 비밀번호(credential) 생성
64+
Mac sha1Hmac = Mac.getInstance("HmacSHA1");
65+
SecretKeySpec secretKey = new SecretKeySpec(turnSharedSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA1");
66+
sha1Hmac.init(secretKey);
67+
byte[] hmacBytes = sha1Hmac.doFinal(username.getBytes(StandardCharsets.UTF_8));
68+
String credential = Base64.getEncoder().encodeToString(hmacBytes);
69+
70+
// 생성된 TURN 서버 정보를 리스트에 추가
71+
String turnUrl = "turn:" + turnServerIp + ":3478";
72+
iceServers.add(IceServer.turn(turnUrl, username, credential));
73+
74+
} catch (Exception e) {
75+
log.error("TURN 서버 동적 인증 정보 생성에 실패했습니다. STUN 서버만으로 응답합니다.", e);
76+
// 인증 정보 생성에 실패하더라도, STUN 서버만으로라도 서비스가 동작하도록 예외를 던지지 않음
77+
}
78+
79+
IceServerConfig config = new IceServerConfig(iceServers);
4480
log.info("ICE 서버 설정 제공 완료 - STUN/TURN 서버 {}개", config.iceServers().size());
4581

4682
return ResponseEntity

src/main/resources/application-prod.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ webrtc:
115115
- urls: stun:stun.l.google.com:19302
116116
- urls: stun:stun1.l.google.com:19302
117117
- urls: stun:stun2.l.google.com:19302
118+
turn:
119+
shared-secret: "${WEBRTC_TURN_SHARED_SECRET}"
120+
server-ip: "${WEBRTC_TURN_SERVER_IP}"
121+
ttl-seconds: 3600
118122

119123
# 스터디룸 설정
120124
studyroom:

src/main/resources/application-test.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,6 @@ jwt:
8989
frontend:
9090
base-url: http://localhost:3000
9191

92-
webrtc:
93-
ice-servers:
94-
- urls: stun:stun.l.google.com:19302
95-
- urls: stun:stun1.l.google.com:19302
96-
- urls: stun:stun2.l.google.com:19302
97-
9892
# AWS S3
9993
cloud:
10094
aws:

src/main/resources/application.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ webrtc:
122122
- urls: stun:stun.l.google.com:19302
123123
- urls: stun:stun1.l.google.com:19302
124124
- urls: stun:stun2.l.google.com:19302
125+
turn:
126+
shared-secret: "${WEBRTC_TURN_SHARED_SECRET}"
127+
server-ip: "${WEBRTC_TURN_SERVER_IP}"
128+
ttl-seconds: 3600
125129

126130
# 스터디룸 설정
127131
studyroom:

src/test/java/com/back/global/websocket/webrtc/controller/WebRTCApiControllerTest.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.springframework.boot.test.context.SpringBootTest;
1010
import org.springframework.http.MediaType;
1111
import org.springframework.security.test.context.support.WithMockUser;
12+
import org.springframework.test.context.TestPropertySource;
1213
import org.springframework.test.web.servlet.MockMvc;
1314
import org.springframework.test.web.servlet.MvcResult;
1415

@@ -20,6 +21,11 @@
2021
@SpringBootTest
2122
@AutoConfigureMockMvc
2223
@WithMockUser
24+
@TestPropertySource(properties = {
25+
"webrtc.turn.shared-secret=test-secret-key-for-junit-12345",
26+
"webrtc.turn.server-ip=127.0.0.1"
27+
// ttl-seconds는 application.yml의 기본값 사용
28+
})
2329
@DisplayName("WebRTC API 컨트롤러")
2430
class WebRTCApiControllerTest {
2531

@@ -34,7 +40,7 @@ class WebRTCApiControllerTest {
3440
class GetIceServersTest {
3541

3642
@Test
37-
@DisplayName("기본 조회")
43+
@DisplayName("STUN 서버와 동적으로 생성된 TURN 서버 정보를 모두 포함하여 반환")
3844
void t1() throws Exception {
3945
// when & then
4046
MvcResult result = mockMvc.perform(get("/api/webrtc/ice-servers")
@@ -47,13 +53,17 @@ void t1() throws Exception {
4753
.andExpect(jsonPath("$.data").exists())
4854
.andExpect(jsonPath("$.data.iceServers").isArray())
4955
.andExpect(jsonPath("$.data.iceServers").isNotEmpty())
56+
.andExpect(jsonPath("$.data.iceServers[?(@.urls contains 'turn:')].urls").exists())
57+
.andExpect(jsonPath("$.data.iceServers[?(@.urls contains 'turn:')].username").exists())
58+
.andExpect(jsonPath("$.data.iceServers[?(@.urls contains 'turn:')].credential").exists())
5059
.andReturn();
5160

52-
// 응답 본문 검증
5361
String content = result.getResponse().getContentAsString();
5462
assertThat(content).contains("stun:");
63+
assertThat(content).contains("turn:");
5564
}
5665

66+
5767
@Test
5868
@DisplayName("userId, roomId 파라미터")
5969
void t2() throws Exception {

0 commit comments

Comments
 (0)