VNC WebSocket 연결 문제 - 브라우저 보안 정책으로 인한 미해결 상태

WebSocket 1006 오류로 인해 브라우저에서 VNC 연결 실패
- 서버 환경에서는 연결 가능하나 브라우저 보안 정책으로 차단
- 역방향 프록시 솔루션 문서화 완료
- 추후 nginx 프록시 구현 필요

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-12 01:44:47 +09:00
parent 1dc09101cc
commit ac620a0e15
5 changed files with 713 additions and 22 deletions

View File

@@ -581,11 +581,7 @@ def create_app(config_name=None):
print(f"🔄 VNC 티켓 새로고침 요청: {node}/{vmid}")
# Proxmox 클라이언트 생성
client = ProxmoxClient(
host=config['proxmox']['host'],
username=config['proxmox']['username'],
password=config['proxmox']['password']
)
client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD)
if not client.login():
return jsonify({'error': 'Proxmox 로그인 실패'}), 500

View File

@@ -74,10 +74,31 @@
// This function is called when we are disconnected
function disconnectedFromServer(e) {
console.log('🔌 VNC 연결 해제 상세 정보:', e.detail);
if (e.detail.clean) {
status("Disconnected");
status("연결이 정상적으로 종료되었습니다");
} else {
status("Something went wrong, connection is closed");
const reason = e.detail.reason || 'Unknown';
status(`연결 실패: ${reason} (Code: ${e.detail.code || 'Unknown'})`);
console.error('❌ VNC 연결 실패 상세:', {
code: e.detail.code,
reason: e.detail.reason,
wasClean: e.detail.clean
});
// WebSocket 에러 코드별 메시지
const errorMessages = {
1006: 'WebSocket 서버에 연결할 수 없습니다. VM이 실행중인지 확인하세요.',
1000: '정상적으로 연결이 종료되었습니다.',
1002: '프로토콜 오류가 발생했습니다.',
1003: '지원하지 않는 데이터를 받았습니다.',
1009: '메시지가 너무 큽니다.',
1011: '서버에서 예상치 못한 오류가 발생했습니다.'
};
const userFriendlyMessage = errorMessages[e.detail.code] || `알 수 없는 오류 (코드: ${e.detail.code})`;
status(`${userFriendlyMessage}`);
}
}
@@ -190,22 +211,103 @@
console.log('WebSocket URL:', websocketUrl);
console.log('VNC Password:', vncPassword);
status("Connecting");
// WebSocket URL 유효성 검사
function validateWebSocketUrl(url) {
if (!url || typeof url !== 'string') {
console.error('❌ WebSocket URL이 비어있습니다');
return false;
}
if (!url.startsWith('wss://') && !url.startsWith('ws://')) {
console.error('❌ 올바르지 않은 WebSocket URL 프로토콜:', url);
return false;
}
console.log('✅ WebSocket URL 유효성 검사 통과');
return true;
}
// Creating a new RFB object will start a new connection
rfb = new RFB(document.getElementById('screen'), websocketUrl,
{ credentials: { password: vncPassword } });
// VNC 연결 함수
function connectToVNC() {
if (!validateWebSocketUrl(websocketUrl)) {
status("❌ 잘못된 WebSocket URL입니다");
return;
}
if (!vncPassword || vncPassword.trim() === '') {
status("❌ VNC 패스워드가 없습니다");
return;
}
status("Connecting to VM...");
console.log('🔄 VNC 연결 시도 시작...');
// Add listeners to important events from the RFB module
rfb.addEventListener("connect", connectedToServer);
rfb.addEventListener("disconnect", disconnectedFromServer);
rfb.addEventListener("credentialsrequired", credentialsAreRequired);
rfb.addEventListener("desktopname", updateDesktopName);
rfb.addEventListener("securityfailure", onSecurityFailure);
try {
// WebSocket 연결 직접 테스트
console.log('🧪 WebSocket 연결 직접 테스트...');
const testWS = new WebSocket(websocketUrl);
testWS.onopen = function(event) {
console.log('✅ WebSocket 연결 테스트 성공');
testWS.close();
// WebSocket이 연결되면 RFB 객체 생성
createRFBConnection();
};
testWS.onerror = function(error) {
console.error('❌ WebSocket 연결 테스트 실패:', error);
status('❌ WebSocket 서버에 연결할 수 없습니다');
// 대안: 티켓 새로고침 시도
setTimeout(() => {
status('🔄 새 티켓으로 재시도 중...');
refreshVNCTicket();
}, 2000);
};
testWS.onclose = function(event) {
console.log('🔌 WebSocket 테스트 연결 종료:', event.code, event.reason);
};
} catch (error) {
console.error('❌ WebSocket 테스트 실패:', error);
status(`❌ 연결 초기화 실패: ${error.message}`);
return;
}
}
// RFB 연결 생성 함수
function createRFBConnection() {
try {
console.log('🔄 RFB 객체 생성 시작...');
// Creating a new RFB object will start a new connection
rfb = new RFB(document.getElementById('screen'), websocketUrl,
{ credentials: { password: vncPassword } });
console.log('✅ RFB 객체 생성 완료');
// Add listeners to important events from the RFB module
rfb.addEventListener("connect", connectedToServer);
rfb.addEventListener("disconnect", disconnectedFromServer);
rfb.addEventListener("credentialsrequired", credentialsAreRequired);
rfb.addEventListener("desktopname", updateDesktopName);
rfb.addEventListener("securityfailure", onSecurityFailure);
// Set parameters that can be changed on an active connection
rfb.viewOnly = false;
rfb.scaleViewport = true;
// Set parameters that can be changed on an active connection
rfb.viewOnly = false;
rfb.scaleViewport = true;
} catch (error) {
console.error('❌ RFB 객체 생성 실패:', error);
status(`❌ VNC 클라이언트 초기화 실패: ${error.message}`);
return;
}
}
// 연결 시작
connectToVNC();
</script>
</head>

View File

@@ -106,26 +106,46 @@ class ProxmoxClient:
def get_vnc_ticket(self, node: str, vmid: int) -> Optional[Dict]:
"""VNC 접속 티켓 생성"""
try:
# 먼저 VM 상태 확인
vm_status = self.get_vm_status(node, vmid)
print(f"🔍 VM {vmid} 상태: {vm_status}")
if vm_status.get('status') != 'running':
print(f"❌ VM {vmid}이 실행중이 아닙니다. 현재 상태: {vm_status.get('status', 'unknown')}")
return None
data = {
'websocket': '1',
'generate-password': '1' # 패스워드 생성 활성화
}
print(f"🔄 VNC 티켓 요청: {self.base_url}/nodes/{node}/qemu/{vmid}/vncproxy")
response = self.session.post(
f"{self.base_url}/nodes/{node}/qemu/{vmid}/vncproxy",
data=data,
timeout=10
)
print(f"📡 VNC 티켓 응답 상태: {response.status_code}")
print(f"📄 VNC 티켓 응답 내용: {response.text}")
if response.status_code == 200:
vnc_data = response.json()['data']
print(f"✅ VNC 티켓 생성 성공: {vnc_data}")
# WebSocket URL 생성
# WebSocket URL 생성 (인증 토큰 포함)
encoded_ticket = quote_plus(vnc_data['ticket'])
vnc_data['websocket_url'] = f"wss://{self.host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}"
# Proxmox 세션 쿠키도 함께 포함 (CSRFPreventionToken도 필요할 수 있음)
csrf_token = getattr(self, 'csrf_token', None)
if csrf_token:
vnc_data['websocket_url'] = f"wss://{self.host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}&CSRFPreventionToken={csrf_token}"
else:
vnc_data['websocket_url'] = f"wss://{self.host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}"
# 디버깅 정보 추가
print(f"🔗 WebSocket URL: {vnc_data['websocket_url']}")
print(f"🔑 VNC Password: {vnc_data.get('password', 'N/A')}")
return vnc_data
else:
print(f"❌ VNC 티켓 생성 HTTP 오류: {response.status_code}")