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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user