Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Slf4j
Expand All @@ -26,6 +31,15 @@ public class WebRTCApiController {

private final WebRTCProperties webRTCProperties;

@Value("${webrtc.turn.shared-secret}")
private String turnSharedSecret;

@Value("${webrtc.turn.server-ip}")
private String turnServerIp;

@Value("${webrtc.turn.ttl-seconds}")
private long turnTtlSeconds;

@GetMapping("/ice-servers")
@Operation(summary = "ICE 서버 설정 조회")
public ResponseEntity<RsData<IceServerConfig>> getIceServers(
Expand All @@ -34,13 +48,35 @@ public ResponseEntity<RsData<IceServerConfig>> getIceServers(

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

List<IceServer> iceServers =
List<IceServer> iceServers = new ArrayList<>(
webRTCProperties.iceServers().stream()
.map(s -> new IceServer(s.urls(), s.username(), s.credential()))
.toList();
.toList()
);

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

// HMAC-SHA1 알고리즘과 공유 비밀키를 사용하여 비밀번호(credential) 생성
Mac sha1Hmac = Mac.getInstance("HmacSHA1");
SecretKeySpec secretKey = new SecretKeySpec(turnSharedSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA1");
sha1Hmac.init(secretKey);
byte[] hmacBytes = sha1Hmac.doFinal(username.getBytes(StandardCharsets.UTF_8));
String credential = Base64.getEncoder().encodeToString(hmacBytes);

// 생성된 TURN 서버 정보를 리스트에 추가
String turnUrl = "turn:" + turnServerIp + ":3478";
iceServers.add(IceServer.turn(turnUrl, username, credential));

} catch (Exception e) {
log.error("TURN 서버 동적 인증 정보 생성에 실패했습니다. STUN 서버만으로 응답합니다.", e);
// 인증 정보 생성에 실패하더라도, STUN 서버만으로라도 서비스가 동작하도록 예외를 던지지 않음
}

IceServerConfig config = new IceServerConfig(iceServers);
log.info("ICE 서버 설정 제공 완료 - STUN/TURN 서버 {}개", config.iceServers().size());

return ResponseEntity
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ webrtc:
- urls: stun:stun.l.google.com:19302
- urls: stun:stun1.l.google.com:19302
- urls: stun:stun2.l.google.com:19302
turn:
shared-secret: "${WEBRTC_TURN_SHARED_SECRET}"
server-ip: "${WEBRTC_TURN_SERVER_IP}"
ttl-seconds: 3600

# 스터디룸 설정
studyroom:
Expand Down
6 changes: 0 additions & 6 deletions src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,6 @@ jwt:
frontend:
base-url: http://localhost:3000

webrtc:
ice-servers:
- urls: stun:stun.l.google.com:19302
- urls: stun:stun1.l.google.com:19302
- urls: stun:stun2.l.google.com:19302

# AWS S3
cloud:
aws:
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ webrtc:
- urls: stun:stun.l.google.com:19302
- urls: stun:stun1.l.google.com:19302
- urls: stun:stun2.l.google.com:19302
turn:
shared-secret: "${WEBRTC_TURN_SHARED_SECRET}"
server-ip: "${WEBRTC_TURN_SERVER_IP}"
ttl-seconds: 3600

# 스터디룸 설정
studyroom:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

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

Expand All @@ -34,7 +40,7 @@ class WebRTCApiControllerTest {
class GetIceServersTest {

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

// 응답 본문 검증
String content = result.getResponse().getContentAsString();
assertThat(content).contains("stun:");
assertThat(content).contains("turn:");
}


@Test
@DisplayName("userId, roomId 파라미터")
void t2() throws Exception {
Expand Down