(Untitled)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MRCP 1.0 서버 에뮬레이터 + 클라이언트 테스트
실제 MRCP 서버를 시뮬레이션하여 클라이언트 테스트 가능
"""

import socket
import time
import threading
import random
import re
from enum import Enum
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass


class RequestState(Enum):
    """MRCP 요청 상태"""
    IDLE = "IDLE"
    IN_PROGRESS = "IN-PROGRESS"
    COMPLETE = "COMPLETE"
    FAILED = "FAILED"


class CompletionCause(Enum):
    """완료 원인 코드"""
    NORMAL = "000 normal"
    TTS_ERROR = "006 tts-error"
    NO_INPUT_TIMEOUT = "001 no-input-timeout"
    CONNECTION_ERROR = "999 connection-error"


@dataclass
class MRCPRequest:
    """MRCP 요청 구조"""
    method: str
    url: str
    cseq: int
    content_type: str
    content_length: int
    headers: Dict[str, str]
    body: str
    
    def __str__(self):
        """RTSP 형식의 요청 패킷 생성"""
        lines = [f"{self.method} {self.url} RTSP/1.0"]
        lines.append(f"CSeq: {self.cseq}")
        lines.append(f"Content-Type: {self.content_type}")
        lines.append(f"Content-Length: {self.content_length}")
        
        for key, value in self.headers.items():
            lines.append(f"{key}: {value}")
        
        lines.append("")  # 빈 줄
        lines.append(self.body)
        
        return "\r\n".join(lines)


@dataclass
class MRCPResponse:
    """MRCP 응답 구조"""
    status_code: int
    status_message: str
    cseq: int
    content_length: int
    request_state: RequestState
    completion_cause: Optional[CompletionCause] = None
    headers: Optional[Dict[str, str]] = None
    body: str = ""
    
    def __str__(self):
        """RTSP 형식의 응답 패킷 생성"""
        lines = [f"RTSP/1.0 {self.status_code} {self.status_message}"]
        lines.append(f"CSeq: {self.cseq}")
        lines.append(f"Content-Length: {self.content_length}")
        lines.append(f"Request-State: {self.request_state.value}")
        
        if self.completion_cause:
            lines.append(f"Completion-Cause: {self.completion_cause.value}")
        
        if self.headers:
            for key, value in self.headers.items():
                lines.append(f"{key}: {value}")
        
        lines.append("")  # 빈 줄
        if self.body:
            lines.append(self.body)
            
        return "\r\n".join(lines)

    @classmethod
    def parse(cls, response_text: str) -> 'MRCPResponse':
        """RTSP 응답 텍스트를 파싱"""
        lines = response_text.strip().split('\r\n')
        if not lines:
            raise ValueError("빈 응답")
        
        # 첫 번째 줄: RTSP/1.0 200 OK
        status_line = lines[0]
        status_parts = status_line.split(' ', 2)
        if len(status_parts) < 2:
            raise ValueError(f"잘못된 상태 라인: {status_line}")
            
        status_code = int(status_parts[1])
        status_message = status_parts[2] if len(status_parts) > 2 else ""
        
        # 헤더 파싱
        headers = {}
        body_start = 0
        cseq = 0
        content_length = 0
        request_state = RequestState.IDLE
        completion_cause = None
        
        for i, line in enumerate(lines[1:], 1):
            if line == '':  # 빈 줄이면 헤더 끝
                body_start = i + 1
                break
            if ':' in line:
                key, value = line.split(':', 1)
                key = key.strip()
                value = value.strip()
                headers[key] = value
                
                # 중요 헤더들 추출
                if key.lower() == 'cseq':
                    cseq = int(value)
                elif key.lower() == 'content-length':
                    content_length = int(value)
                elif key.lower() == 'request-state':
                    try:
                        request_state = RequestState(value)
                    except ValueError:
                        request_state = RequestState.IDLE
                elif key.lower() == 'completion-cause':
                    try:
                        completion_cause = CompletionCause(value)
                    except ValueError:
                        completion_cause = None
        
        # 바디 추출
        body = '\r\n'.join(lines[body_start:]) if body_start < len(lines) else ""
        
        return cls(
            status_code=status_code,
            status_message=status_message,
            cseq=cseq,
            content_length=content_length,
            request_state=request_state,
            completion_cause=completion_cause,
            headers=headers,
            body=body
        )


class MRCPServerEmulator:
    """MRCP 1.0 서버 에뮬레이터"""
    
    def __init__(self, host: str = "127.0.0.1", port: int = 8000):
        self.host = host
        self.port = port
        self.server_socket: Optional[socket.socket] = None
        self.is_running = False
        self.clients = {}  # 클라이언트 연결 관리
        
    def start(self):
        """서버 시작"""
        try:
            self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.server_socket.bind((self.host, self.port))
            self.server_socket.listen(5)
            
            self.is_running = True
            print(f"[SERVER] MRCP 서버 시작: {self.host}:{self.port}")
            
            # 클라이언트 연결 대기 스레드
            accept_thread = threading.Thread(target=self._accept_connections)
            accept_thread.daemon = True
            accept_thread.start()
            
            return True
            
        except Exception as e:
            print(f"[SERVER] 서버 시작 실패: {e}")
            return False
    
    def stop(self):
        """서버 중지"""
        self.is_running = False
        if self.server_socket:
            try:
                self.server_socket.close()
            except:
                pass
            finally:
                self.server_socket = None
        print(f"[SERVER] MRCP 서버 중지")
    
    def _accept_connections(self):
        """클라이언트 연결 수락"""
        while self.is_running:
            try:
                if not self.server_socket:
                    break
                    
                client_socket, client_address = self.server_socket.accept()
                print(f"[SERVER] 클라이언트 연결: {client_address}")
                
                # 클라이언트 처리 스레드
                client_thread = threading.Thread(
                    target=self._handle_client,
                    args=(client_socket, client_address)
                )
                client_thread.daemon = True
                client_thread.start()
                
            except Exception as e:
                if self.is_running:  # 서버가 실행 중일 때만 오류 출력
                    print(f"[SERVER] 클라이언트 수락 오류: {e}")
                break
    
    def _handle_client(self, client_socket: socket.socket, client_address: Tuple[str, int]):
        """개별 클라이언트 처리"""
        client_id = f"{client_address[0]}:{client_address[1]}"
        
        try:
            client_socket.settimeout(30)  # 30초 타임아웃
            
            while self.is_running:
                # 요청 수신
                request_data = b""
                while True:
                    try:
                        chunk = client_socket.recv(4096)
                        if not chunk:
                            break
                        request_data += chunk
                        
                        # RTSP 요청 끝 확인
                        request_text = request_data.decode('utf-8', errors='ignore')
                        if '\r\n\r\n' in request_text:
                            # Content-Length 체크
                            if 'Content-Length:' in request_text:
                                try:
                                    headers_end = request_text.find('\r\n\r\n')
                                    headers_part = request_text[:headers_end]
                                    
                                    content_length = 0
                                    for line in headers_part.split('\r\n'):
                                        if line.lower().startswith('content-length:'):
                                            content_length = int(line.split(':', 1)[1].strip())
                                            break
                                    
                                    body_start = headers_end + 4
                                    if len(request_text[body_start:]) >= content_length:
                                        break
                                except (ValueError, IndexError):
                                    break
                            else:
                                break
                    except socket.timeout:
                        print(f"[SERVER] 클라이언트 {client_id} 수신 타임아웃")
                        return
                
                if not request_data:
                    break
                
                request_text = request_data.decode('utf-8', errors='ignore')
                
                print(f"\n[SERVER] 클라이언트 {client_id}로부터 요청 수신:")
                print("-" * 40)
                print(request_text)
                print("-" * 40)
                
                # 요청 처리 및 응답 생성
                response = self._process_request(request_text, client_id)
                
                if response:
                    response_text = str(response)
                    
                    print(f"\n[SERVER] 클라이언트 {client_id}에게 응답 전송:")
                    print("-" * 40)
                    print(response_text)
                    print("-" * 40)
                    
                    # 응답 전송
                    try:
                        client_socket.sendall(response_text.encode('utf-8'))
                    except Exception as send_error:
                        print(f"[SERVER] 응답 전송 실패: {send_error}")
                        break
                        
        except Exception as e:
            print(f"[SERVER] 클라이언트 {client_id} 처리 오류: {e}")
        finally:
            try:
                client_socket.close()
            except:
                pass
            print(f"[SERVER] 클라이언트 {client_id} 연결 해제")
    
    def _process_request(self, request_text: str, client_id: str) -> Optional[MRCPResponse]:
        """RTSP/MRCP 요청 처리"""
        lines = request_text.strip().split('\r\n')
        if not lines:
            return None
        
        # 첫 번째 줄 파싱: METHOD URL RTSP/1.0
        first_line = lines[0]
        parts = first_line.split(' ')
        if len(parts) < 3:
            return None
        
        method = parts[0]
        url = parts[1]
        protocol = parts[2]
        
        # 헤더 파싱
        headers = {}
        cseq = 0
        body_start = 0
        
        for i, line in enumerate(lines[1:], 1):
            if line == '':  # 빈 줄이면 헤더 끝
                body_start = i + 1
                break
            if ':' in line:
                key, value = line.split(':', 1)
                headers[key.strip()] = value.strip()
                if key.strip().lower() == 'cseq':
                    cseq = int(value.strip())
        
        # 바디 추출
        body = '\r\n'.join(lines[body_start:]) if body_start < len(lines) else ""
        
        # 메소드별 처리
        if method == "ANNOUNCE":
            return self._handle_announce(cseq, body, client_id)
        elif method == "SETUP":
            return self._handle_setup(cseq, url, client_id)
        elif method == "DESCRIBE":
            return self._handle_describe(cseq, client_id)
        else:
            # 지원하지 않는 메소드
            return MRCPResponse(
                status_code=405,
                status_message="Method Not Allowed",
                cseq=cseq,
                content_length=0,
                request_state=RequestState.FAILED
            )
    
    def _handle_announce(self, cseq: int, body: str, client_id: str) -> MRCPResponse:
        """ANNOUNCE 요청 처리 (MRCP 메시지 포함)"""
        print(f"[SERVER] ANNOUNCE 처리 - 클라이언트: {client_id}")
        
        # MRCP 메시지 파싱
        if "SPEAK" in body:
            return self._handle_tts_speak(cseq, body, client_id)
        else:
            # 알 수 없는 MRCP 요청
            return MRCPResponse(
                status_code=400,
                status_message="Bad Request",
                cseq=cseq,
                content_length=0,
                request_state=RequestState.FAILED,
                completion_cause=CompletionCause.TTS_ERROR
            )
    
    def _handle_tts_speak(self, cseq: int, mrcp_body: str, client_id: str) -> MRCPResponse:
        """TTS SPEAK 요청 처리"""
        print(f"[SERVER] TTS SPEAK 처리 - 클라이언트: {client_id}")
        
        # MRCP 바디에서 텍스트 추출
        lines = mrcp_body.strip().split('\r\n')
        text_content = ""
        voice_info = {}
        
        # MRCP 헤더와 바디 파싱
        in_headers = True
        for line in lines:
            if in_headers:
                if line == '':
                    in_headers = False
                    continue
                elif ':' in line:
                    key, value = line.split(':', 1)
                    key = key.strip()
                    value = value.strip()
                    
                    if key == "Voice-Gender":
                        voice_info['gender'] = value
                    elif key == "Speech-Language":
                        voice_info['language'] = value
                    elif key == "Voice-Age":
                        voice_info['age'] = value
            else:
                if line.strip():
                    text_content = line.strip()
                    break
        
        print(f"[SERVER] TTS 요청 내용:")
        print(f"  - 텍스트: {text_content}")
        print(f"  - 음성 정보: {voice_info}")
        
        # TTS 처리 시뮬레이션
        processing_time = random.uniform(1, 3)  # 1-3초 처리 시간
        success_rate = 0.9  # 90% 성공률
        
        # 처리 시간 시뮬레이션
        time.sleep(processing_time)
        
        # 성공/실패 결정
        if random.random() < success_rate:
            # 성공 시나리오
            response = MRCPResponse(
                status_code=200,
                status_message="OK",
                cseq=cseq,
                content_length=0,
                request_state=RequestState.COMPLETE,
                completion_cause=CompletionCause.NORMAL,
                headers={
                    "Speech-Marker": f"mark1={int(processing_time * 1000)}",
                    "Content-Duration": f"{processing_time:.1f}",
                    "Voice-Used": voice_info.get('gender', 'female')
                }
            )
        else:
            # 실패 시나리오
            response = MRCPResponse(
                status_code=500,
                status_message="Internal Server Error",
                cseq=cseq,
                content_length=0,
                request_state=RequestState.FAILED,
                completion_cause=CompletionCause.TTS_ERROR
            )
        
        return response
    
    def _handle_setup(self, cseq: int, url: str, client_id: str) -> MRCPResponse:
        """SETUP 요청 처리"""
        print(f"[SERVER] SETUP 처리 - 클라이언트: {client_id}")
        
        # 세션 ID 생성
        session_id = f"session_{int(time.time())}_{random.randint(1000, 9999)}"
        
        return MRCPResponse(
            status_code=200,
            status_message="OK",
            cseq=cseq,
            content_length=0,
            request_state=RequestState.COMPLETE,
            headers={
                "Session": session_id,
                "Transport": "RTP/AVP;unicast;server_port=8000-8001"
            }
        )
    
    def _handle_describe(self, cseq: int, client_id: str) -> MRCPResponse:
        """DESCRIBE 요청 처리"""
        print(f"[SERVER] DESCRIBE 처리 - 클라이언트: {client_id}")
        
        # SDP 형태의 서버 능력 정보
        sdp_body = """v=0
o=- 123456789 123456789 IN IP4 127.0.0.1
s=MRCP Server
c=IN IP4 127.0.0.1
t=0 0
m=application 9 TCP/MRCPv1 1
a=resource:speechsynth
a=cmid:1
"""
        
        return MRCPResponse(
            status_code=200,
            status_message="OK",
            cseq=cseq,
            content_length=len(sdp_body.encode('utf-8')),
            request_state=RequestState.COMPLETE,
            headers={
                "Content-Type": "application/sdp"
            },
            body=sdp_body
        )


class MRCPTTSClient:
    """MRCP 1.0 TTS 전용 실제 클라이언트"""
    
    def __init__(self, server_host: str = "127.0.0.1", server_port: int = 554, timeout: int = 30):
        self.server_host = server_host
        self.server_port = server_port
        self.timeout = timeout
        self.session_id = f"session_{int(time.time())}"
        self.cseq = 0
        self.socket: Optional[socket.socket] = None
        self.is_connected = False
        
    def connect(self) -> bool:
        """실제 MRCP 서버에 연결"""
        try:
            print(f"[CLIENT] MRCP 서버 연결 시도: {self.server_host}:{self.server_port}")
            
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.settimeout(self.timeout)
            self.socket.connect((self.server_host, self.server_port))
            
            self.is_connected = True
            print(f"[CLIENT] 서버 연결 성공! 세션 ID: {self.session_id}")
            return True
            
        except socket.timeout:
            print(f"[CLIENT] 연결 타임아웃: {self.server_host}:{self.server_port}")
            return False
        except ConnectionRefusedError:
            print(f"[CLIENT] 연결 거부됨: {self.server_host}:{self.server_port} (서버가 실행 중인지 확인)")
            return False
        except socket.gaierror:
            print(f"[CLIENT] 호스트명 해석 실패: {self.server_host}")
            return False
        except Exception as e:
            print(f"[CLIENT] 연결 실패: {e}")
            return False
        
    def disconnect(self):
        """서버 연결 해제"""
        if self.socket:
            try:
                self.socket.close()
            except:
                pass
            finally:
                self.socket = None
                self.is_connected = False
                print(f"[CLIENT] 서버 연결 해제")
        
    def _get_next_cseq(self) -> int:
        """다음 시퀀스 번호 생성"""
        self.cseq += 1
        return self.cseq
    
    def _send_request(self, request: str) -> str:
        """RTSP 요청 전송 및 응답 수신"""
        if not self.is_connected or not self.socket:
            raise ConnectionError("서버에 연결되지 않았습니다.")
        
        try:
            # 요청 전송
            request_bytes = request.encode('utf-8')
            self.socket.sendall(request_bytes)
            
            print(f"\n[CLIENT] TTS 요청 전송:")
            print("=" * 50)
            print(request)
            print("=" * 50)
            
            # 응답 수신
            response_data = b""
            while True:
                try:
                    chunk = self.socket.recv(4096)
                    if not chunk:
                        break
                    response_data += chunk
                    
                    # RTSP 응답 끝 확인
                    response_text = response_data.decode('utf-8', errors='ignore')
                    if '\r\n\r\n' in response_text:
                        # Content-Length 체크
                        if 'Content-Length:' in response_text:
                            try:
                                headers_end = response_text.find('\r\n\r\n')
                                headers_part = response_text[:headers_end]
                                
                                content_length = 0
                                for line in headers_part.split('\r\n'):
                                    if line.lower().startswith('content-length:'):
                                        content_length = int(line.split(':', 1)[1].strip())
                                        break
                                
                                body_start = headers_end + 4
                                if len(response_text[body_start:]) >= content_length:
                                    break
                            except (ValueError, IndexError):
                                # Content-Length 파싱 실패 시 응답 종료로 간주
                                break
                        else:
                            break
                except socket.timeout:
                    print("[CLIENT] 응답 수신 타임아웃")
                    break
            
            response_text = response_data.decode('utf-8', errors='ignore')
            
            print(f"\n[SERVER] TTS 응답 수신:")
            print("-" * 50)
            print(response_text if response_text else "응답 없음")
            print("-" * 50)
            
            return response_text
            
        except Exception as e:
            raise ConnectionError(f"요청 전송 실패: {e}")
    
    def speak(self, text: str, voice_gender: str = "female", 
              language: str = "ko-KR", voice_age: str = "25") -> List[MRCPResponse]:
        """TTS SPEAK 요청 - 실제 서버와 통신"""
        if not self.is_connected:
            raise ConnectionError("서버에 연결되지 않았습니다.")
            
        responses = []
        
        try:
            # MRCP SPEAK 요청 생성
            url = f"rtsp://{self.server_host}:{self.server_port}/{self.session_id}"
            cseq = self._get_next_cseq()
            
            # MRCP 메시지 바디 생성
            mrcp_body = f"""SPEAK {cseq} SPEAK 100\r
Voice-Gender: {voice_gender}\r
Voice-Age: {voice_age}\r
Speech-Language: {language}\r
Content-Type: text/plain\r
Content-Length: {len(text.encode('utf-8'))}\r
\r
{text}"""
            
            # RTSP ANNOUNCE 요청 생성
            announce_request = f"""ANNOUNCE {url} RTSP/1.0\r
CSeq: {cseq}\r
Content-Type: application/mrcp\r
Content-Length: {len(mrcp_body.encode('utf-8'))}\r
\r
{mrcp_body}"""
            
            # 요청 전송 및 응답 수신
            response_text = self._send_request(announce_request)
            
            if response_text:
                # 응답 파싱
                try:
                    response = MRCPResponse.parse(response_text)
                    responses.append(response)
                    
                    print(f"\n[SERVER] TTS 응답 분석:")
                    print(f"상태 코드: {response.status_code}")
                    print(f"상태 메시지: {response.status_message}")
                    print(f"요청 상태: {response.request_state.value}")
                    if response.completion_cause:
                        print(f"완료 원인: {response.completion_cause.value}")
                    
                    # 성공/실패 판정
                    if response.status_code == 200:
                        if response.request_state == RequestState.COMPLETE:
                            if response.completion_cause == CompletionCause.NORMAL:
                                print("✅ TTS 성공! 음성이 생성되었습니다.")
                            else:
                                print(f"❌ TTS 완료되었지만 오류: {response.completion_cause}")
                        elif response.request_state == RequestState.IN_PROGRESS:
                            print("🔄 TTS 처리 중...")
                        else:
                            print(f"⚠️ TTS 상태: {response.request_state.value}")
                    else:
                        print(f"❌ TTS 요청 실패: {response.status_code} {response.status_message}")
                        
                except Exception as parse_error:
                    print(f"❌ 응답 파싱 실패: {parse_error}")
                    error_response = MRCPResponse(
                        status_code=500,
                        status_message="Parse Error",
                        cseq=cseq,
                        content_length=0,
                        request_state=RequestState.FAILED,
                        completion_cause=CompletionCause.TTS_ERROR
                    )
                    responses.append(error_response)
            else:
                print("❌ 서버로부터 응답을 받지 못했습니다.")
                no_response = MRCPResponse(
                    status_code=408,
                    status_message="No Response",
                    cseq=cseq,
                    content_length=0,
                    request_state=RequestState.FAILED,
                    completion_cause=CompletionCause.CONNECTION_ERROR
                )
                responses.append(no_response)
                
        except Exception as e:
            print(f"❌ TTS 요청 중 오류 발생: {e}")
            error_response = MRCPResponse(
                status_code=500,
                status_message="Internal Error",
                cseq=self.cseq,
                content_length=0,
                request_state=RequestState.FAILED,
                completion_cause=CompletionCause.CONNECTION_ERROR
            )
            responses.append(error_response)
        
        return responses


def demo_server_client_test():
    """서버와 클라이언트 통합 테스트"""
    print("=" * 60)
    print("MRCP 1.0 서버-클라이언트 통합 테스트")
    print("=" * 60)
    
    # 서버 시작
    server = MRCPServerEmulator("127.0.0.1", 8000)
    
    if not server.start():
        print("❌ 서버 시작 실패")
        return
    
    try:
        # 서버가 완전히 시작될 때까지 잠시 대기
        time.sleep(1)
        
        # 클라이언트 테스트
        print("\n" + "=" * 40)
        print("클라이언트 테스트 시작")
        print("=" * 40)
        
        client = MRCPTTSClient("127.0.0.1", 8000, timeout=10)
        
        try:
            # 1. 서버 연결
            if not client.connect():
                print("❌ 클라이언트 연결 실패")
                return
            
            # 2. 첫 번째 TTS 테스트
            print("\n[테스트 1] 기본 TTS 테스트")
            responses1 = client.speak(
                text="안녕하세요, MRCP 서버 에뮬레이터 테스트입니다.",
                voice_gender="female",
                language="ko-KR"
            )
            
            if responses1 and responses1[-1].status_code == 200:
                print("✅ 테스트 1 성공!")
            else:
                print("❌ 테스트 1 실패!")
            
            time.sleep(1)
            
            # 3. 두 번째 TTS 테스트 (짧은 텍스트)
            print("\n[테스트 2] 짧은 텍스트 TTS 테스트")
            responses2 = client.speak(
                text="안녕",
                voice_gender="male",
                language="ko-KR"
            )
            
            if responses2 and responses2[-1].status_code == 200:
                print("✅ 테스트 2 성공!")
            else:
                print("❌ 테스트 2 실패!")
            
            time.sleep(1)
            
            # 4. 긴 텍스트 TTS 테스트
            print("\n[테스트 3] 긴 텍스트 TTS 테스트")
            long_text = "이것은 긴 텍스트 테스트입니다. " * 10  # 긴 텍스트 생성
            responses3 = client.speak(
                text=long_text,
                voice_gender="female",
                language="ko-KR"
            )
            
            if responses3 and responses3[-1].status_code == 200:
                print("✅ 테스트 3 성공!")
            else:
                print("❌ 테스트 3 실패!")
            
        finally:
            client.disconnect()
        
    finally:
        # 서버 중지
        print("\n" + "=" * 40)
        print("서버 중지 중...")
        server.stop()
        time.sleep(1)
    
    print("\n" + "=" * 60)
    print("통합 테스트 완료!")
    print("=" * 60)


def demo_multiple_clients_test():
    """다중 클라이언트 동시 접속 테스트"""
    print("\n" + "=" * 60)
    print("MRCP 1.0 다중 클라이언트 테스트")
    print("=" * 60)
    
    # 서버 시작
    server = MRCPServerEmulator("127.0.0.1", 8001)  # 다른 포트 사용
    
    if not server.start():
        print("❌ 서버 시작 실패")
        return
    
    try:
        time.sleep(1)  # 서버 시작 대기
        
        def client_worker(client_id: int):
            """개별 클라이언트 작업"""
            client = MRCPTTSClient("127.0.0.1", 8001, timeout=15)
            
            try:
                if client.connect():
                    # 각 클라이언트마다 다른 텍스트로 TTS 요청
                    text = f"클라이언트 {client_id} TTS 테스트입니다."
                    responses = client.speak(
                        text=text,
                        voice_gender="female" if client_id % 2 == 0 else "male",
                        language="ko-KR"
                    )
                    
                    if responses and responses[-1].status_code == 200:
                        print(f"✅ 클라이언트 {client_id} 성공!")
                    else:
                        print(f"❌ 클라이언트 {client_id} 실패!")
                else:
                    print(f"❌ 클라이언트 {client_id} 연결 실패")
                    
            except Exception as e:
                print(f"❌ 클라이언트 {client_id} 오류: {e}")
            finally:
                client.disconnect()
        
        # 5개의 클라이언트 동시 실행
        threads = []
        for i in range(1, 6):
            thread = threading.Thread(target=client_worker, args=(i,))
            threads.append(thread)
            thread.start()
        
        # 모든 클라이언트 완료 대기
        for thread in threads:
            thread.join()
        
    finally:
        print("\n다중 클라이언트 테스트 완료, 서버 중지 중...")
        server.stop()
        time.sleep(1)


def demo_error_scenarios():
    """오류 시나리오 테스트"""
    print("\n" + "=" * 60)
    print("MRCP 1.0 오류 시나리오 테스트")
    print("=" * 60)
    
    server = MRCPServerEmulator("127.0.0.1", 8002)  # 또 다른 포트
    
    if not server.start():
        print("❌ 서버 시작 실패")
        return
    
    try:
        time.sleep(1)
        
        client = MRCPTTSClient("127.0.0.1", 8002, timeout=5)
        
        try:
            if client.connect():
                print("✅ 서버 연결 성공")
                
                # 1. 빈 텍스트 테스트
                print("\n[오류 테스트 1] 빈 텍스트")
                responses1 = client.speak(text="")
                print(f"결과: {responses1[-1].status_code if responses1 else 'No Response'}")
                
                time.sleep(0.5)
                
                # 2. 매우 긴 텍스트 테스트
                print("\n[오류 테스트 2] 매우 긴 텍스트")
                very_long_text = "매우 긴 텍스트입니다. " * 50
                responses2 = client.speak(text=very_long_text)
                print(f"결과: {responses2[-1].status_code if responses2 else 'No Response'}")
                
                time.sleep(0.5)
                
                # 3. 특수문자 테스트
                print("\n[오류 테스트 3] 특수문자 포함 텍스트")
                special_text = "특수문자: !@#$%^&*()_+[]{}|;':\",./<>?"
                responses3 = client.speak(text=special_text)
                print(f"결과: {responses3[-1].status_code if responses3 else 'No Response'}")
                
            else:
                print("❌ 서버 연결 실패")
                
        finally:
            client.disconnect()
        
        time.sleep(1)
        
        # 4. 서버 중지 후 연결 시도
        print("\n[오류 테스트 4] 서버 중지 후 연결 시도")
        server.stop()
        time.sleep(1)
        
        client2 = MRCPTTSClient("127.0.0.1", 8002, timeout=3)
        if client2.connect():
            print("❌ 예상과 다름: 서버가 중지되었는데 연결됨")
        else:
            print("✅ 예상대로: 서버 중지 후 연결 실패")
        client2.disconnect()
        
    except Exception as e:
        print(f"❌ 오류 시나리오 테스트 중 예외: {e}")
        server.stop()


def demo_performance_test():
    """성능 테스트"""
    print("\n" + "=" * 60)
    print("MRCP 1.0 성능 테스트")
    print("=" * 60)
    
    server = MRCPServerEmulator("127.0.0.1", 8003)
    
    if not server.start():
        print("❌ 서버 시작 실패")
        return
    
    try:
        time.sleep(1)
        
        client = MRCPTTSClient("127.0.0.1", 8003, timeout=30)
        
        try:
            if client.connect():
                print("✅ 성능 테스트 서버 연결 성공")
                
                # 연속 요청 성능 테스트
                num_requests = 10
                start_time = time.time()
                success_count = 0
                
                print(f"\n{num_requests}개의 연속 TTS 요청 테스트 시작...")
                
                for i in range(1, num_requests + 1):
                    print(f"  [{i}/{num_requests}] TTS 요청 전송...")
                    
                    text = f"성능 테스트 {i}번째 요청입니다."
                    responses = client.speak(text=text)
                    
                    if responses and responses[-1].status_code == 200:
                        success_count += 1
                        print(f"    ✅ 요청 {i} 성공")
                    else:
                        print(f"    ❌ 요청 {i} 실패")
                    
                    # 요청 간 짧은 간격
                    time.sleep(0.2)
                
                end_time = time.time()
                total_time = end_time - start_time
                
                print(f"\n📊 성능 테스트 결과:")
                print(f"  - 총 요청 수: {num_requests}")
                print(f"  - 성공 요청 수: {success_count}")
                print(f"  - 실패 요청 수: {num_requests - success_count}")
                print(f"  - 성공률: {(success_count / num_requests * 100):.1f}%")
                print(f"  - 총 소요 시간: {total_time:.2f}초")
                print(f"  - 평균 요청 시간: {(total_time / num_requests):.2f}초")
                print(f"  - 초당 처리량: {(num_requests / total_time):.2f} req/s")
                
            else:
                print("❌ 성능 테스트 서버 연결 실패")
                
        finally:
            client.disconnect()
        
    finally:
        print("\n성능 테스트 완료, 서버 중지 중...")
        server.stop()
        time.sleep(1)


def demo_connection_test():
    """기본 연결 테스트"""
    print("\n" + "=" * 60)
    print("MRCP 1.0 기본 연결 테스트")
    print("=" * 60)
    
    # 다양한 포트에서 서버 시작 테스트
    test_ports = [8004, 8005, 8006]
    
    for port in test_ports:
        print(f"\n[테스트] 포트 {port}에서 서버 시작...")
        
        server = MRCPServerEmulator("127.0.0.1", port)
        
        if server.start():
            print(f"✅ 포트 {port} 서버 시작 성공")
            
            time.sleep(1)
            
            # 클라이언트 연결 테스트
            client = MRCPTTSClient("127.0.0.1", port, timeout=5)
            
            if client.connect():
                print(f"✅ 포트 {port} 클라이언트 연결 성공")
                client.disconnect()
            else:
                print(f"❌ 포트 {port} 클라이언트 연결 실패")
            
            server.stop()
            time.sleep(1)
        else:
            print(f"❌ 포트 {port} 서버 시작 실패")


if __name__ == "__main__":
    print("MRCP 1.0 서버 에뮬레이터 + 클라이언트 테스트")
    print("실제 MRCP 서버를 시뮬레이션하여 클라이언트 코드를 테스트합니다.")
    print()
    
    try:
        # 1. 기본 연결 테스트
        demo_connection_test()
        
        time.sleep(2)
        
        # 2. 기본 서버-클라이언트 테스트
        demo_server_client_test()
        
        time.sleep(2)
        
        # 3. 다중 클라이언트 테스트
        demo_multiple_clients_test()
        
        time.sleep(2)
        
        # 4. 오류 시나리오 테스트
        demo_error_scenarios()
        
        time.sleep(2)
        
        # 5. 성능 테스트
        demo_performance_test()
        
        print("\n" + "🎉" * 20)
        print("모든 테스트 완료!")
        print("🎉" * 20)
        
    except KeyboardInterrupt:
        print("\n\n⚠️ 사용자에 의해 테스트가 중단되었습니다.")
    except Exception as e:
        print(f"\n\n❌ 테스트 실행 중 오류 발생: {e}")
    
    print("\n프로그램을 종료합니다.")

Read more

CSTI

좋은 질문이에요 👍 CSTI(Client-Side Template Injection)와 SSTI(Server-Side Template Injection)는 모두 템플릿 엔진을 통한 코드 실행 취약점이라는 공통점이 있지만, 동작 환경(클라이언트/서버)에 따라 대응 방식이 달라집니다. 보안 전문가 관점에서 기술적 대응 + 운영적 대응을 함께 정리해드릴게요. ✅ CSTI 및 SSTI 보안 취약점 대응방안 1. 공통 대응방안 템플릿 엔진의

By ByteGaze