fix: 處理系統代理連線 (Proxy Connection) 狀態落差問題 - 導入「自訂斷線碼 (Custom Close Code)」。
This commit is contained in:
parent
7a49ef9833
commit
8f026aac9e
181
all_code.txt
181
all_code.txt
|
|
@ -3,28 +3,71 @@ PROJECT SOURCE CODE EXPORT (FULL)
|
|||
================================================================================
|
||||
|
||||
|
||||
================================================================================
|
||||
FILE: .env.example
|
||||
================================================================================
|
||||
# --- .env.example ---
|
||||
# Database Configuration (請填入你的本地端或正式機設定)
|
||||
DB_NAME=cmts_nms
|
||||
DB_USER=
|
||||
DB_PASS=
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
|
||||
# God Mode Secret
|
||||
GOD_MODE_SECRET=
|
||||
|
||||
# Default CMTS Device
|
||||
DEFAULT_CMTS_HOST=
|
||||
DEFAULT_CMTS_USER=
|
||||
DEFAULT_CMTS_PASS=
|
||||
|
||||
================================================================================
|
||||
FILE: .env
|
||||
================================================================================
|
||||
# --- .env ---
|
||||
# Database Configuration
|
||||
DB_NAME=cmts_nms
|
||||
DB_USER=swpa
|
||||
DB_PASS=swpa4920
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
|
||||
# God Mode Secret
|
||||
GOD_MODE_SECRET=swpa@Serc0mm
|
||||
|
||||
# Default CMTS Device (本地開發用,正式環境可留空)
|
||||
DEFAULT_CMTS_HOST=10.14.110.4
|
||||
DEFAULT_CMTS_USER=admin
|
||||
DEFAULT_CMTS_PASS=nsgadmin
|
||||
|
||||
================================================================================
|
||||
FILE: database.py
|
||||
================================================================================
|
||||
import asyncpg
|
||||
import json
|
||||
import uuid
|
||||
import os
|
||||
from dotenv import load_dotenv # 🌟 1. 引入 load_dotenv
|
||||
from typing import Dict, List, Optional, Any
|
||||
from logger import get_logger
|
||||
|
||||
# 🌟 2. 明確指示 Python 讀取同目錄下的 .env 檔案
|
||||
load_dotenv()
|
||||
|
||||
# ==========================================
|
||||
# 💡 PostgreSQL 連線與操作 (高可用性版)
|
||||
# ==========================================
|
||||
|
||||
logger = get_logger("app.database")
|
||||
|
||||
# DB 設定 (從您的 init_db.py 中提取)
|
||||
# 🌟 2. 同步改用 os.getenv 讀取環境變數
|
||||
DB_CONFIG = {
|
||||
"database": "cmts_nms",
|
||||
"user": "swpa",
|
||||
"password": "swpa4920",
|
||||
"host": "127.0.0.1",
|
||||
"port": "5432"
|
||||
"database": os.getenv("DB_NAME", "cmts_nms"),
|
||||
"user": os.getenv("DB_USER", "postgres"), # 本地開發常用的預設帳號,或留空 ""
|
||||
"password": os.getenv("DB_PASS", ""), # 🌟 絕對機密:預設留空!
|
||||
"host": os.getenv("DB_HOST", "127.0.0.1"),
|
||||
"port": os.getenv("DB_PORT", "5432")
|
||||
}
|
||||
|
||||
_pool: Optional[asyncpg.Pool] = None
|
||||
|
|
@ -371,12 +414,12 @@ def debug_print(msg: str):
|
|||
if DEBUG_MODE:
|
||||
print(msg)
|
||||
|
||||
# 預設的 CMTS 連線樣板
|
||||
# 🌟 2. 淨化預設的 CMTS 連線樣板
|
||||
CMTS_DEVICE = {
|
||||
'device_type': 'cisco_ios',
|
||||
'host': '10.14.110.4',
|
||||
'username': 'admin',
|
||||
'password': 'nsgadmin',
|
||||
'host': os.getenv("DEFAULT_CMTS_HOST", ""),
|
||||
'username': os.getenv("DEFAULT_CMTS_USER", ""),
|
||||
'password': os.getenv("DEFAULT_CMTS_PASS", ""),
|
||||
'port': 22,
|
||||
'fast_cli': False,
|
||||
'global_delay_factor': 2
|
||||
|
|
@ -646,9 +689,10 @@ FILE: index.html
|
|||
|
||||
<div class="global-settings">
|
||||
<label><strong>🌐 目標設備:</strong></label>
|
||||
<input type="text" id="cmtsHost" placeholder="IP 地址" value="10.14.110.4" style="width: 130px;">
|
||||
<input type="text" id="cmtsUser" placeholder="帳號" value="admin" style="width: 100px;">
|
||||
<input type="password" id="cmtsPass" placeholder="密碼" value="nsgadmin" style="width: 100px;">
|
||||
<!-- 🌟 修改後 (徹底淨化) -->
|
||||
<input type="text" id="cmtsHost" placeholder="IP 地址" style="width: 130px;">
|
||||
<input type="text" id="cmtsUser" placeholder="帳號" style="width: 100px;">
|
||||
<input type="password" id="cmtsPass" placeholder="密碼" style="width: 100px;">
|
||||
|
||||
<div style="margin-left: 15px; display: flex; align-items: center; gap: 10px;">
|
||||
<button onclick="connectWebSocket()" id="btnConnect" class="btn-modern btn-connect">連線至 CMTS</button>
|
||||
|
|
@ -1238,17 +1282,22 @@ import asyncio
|
|||
import asyncpg
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from dotenv import load_dotenv # 🌟 1. 引入 load_dotenv
|
||||
|
||||
# 🌟 2. 明確指示 Python 讀取同目錄下的 .env 檔案
|
||||
load_dotenv()
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# DB 設定
|
||||
# 🌟 2. 改用 os.getenv 讀取環境變數,並保留原本的設定作為安全預設值
|
||||
DB_CONFIG = {
|
||||
"database": "cmts_nms",
|
||||
"user": "swpa",
|
||||
"password": "swpa4920",
|
||||
"host": "127.0.0.1",
|
||||
"port": "5432"
|
||||
"database": os.getenv("DB_NAME", "cmts_nms"),
|
||||
"user": os.getenv("DB_USER", "postgres"), # 本地開發常用的預設帳號,或留空 ""
|
||||
"password": os.getenv("DB_PASS", ""), # 🌟 絕對機密:預設留空!
|
||||
"host": os.getenv("DB_HOST", "127.0.0.1"),
|
||||
"port": os.getenv("DB_PORT", "5432")
|
||||
}
|
||||
|
||||
async def init_database(force_reset: bool = False):
|
||||
|
|
@ -1387,12 +1436,21 @@ def parse_question_mark_output(output: str) -> dict:
|
|||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
|
||||
# 1. 過濾雜訊與 Echo (包含 prompt 和 ?)
|
||||
# ==========================================
|
||||
# 🛡️ 1. 終極雜訊與 Echo 過濾器
|
||||
# ==========================================
|
||||
if not line or line.startswith('%') or line.startswith('^'):
|
||||
continue
|
||||
if '?' in line and ('#' in line or '>' in line):
|
||||
|
||||
# 攔截 Echo 的問號指令 (例如 "logging buffered ?")
|
||||
if line.endswith('?'):
|
||||
continue
|
||||
|
||||
# 攔截終端機 Prompt (例如 "admin@SERCOMM-COS-02(config)# ..." 或 "admin@SERCOMM-COS-02>")
|
||||
if re.search(r"[a-zA-Z0-9_.@-]+\(.*\)#", line) or re.search(r"^[a-zA-Z0-9_.@-]+>", line):
|
||||
continue
|
||||
# ==========================================
|
||||
|
||||
# 2. 無差別收集所有有效行作為 Hint (保留最完整的說明給使用者看)
|
||||
hint_lines.append(line)
|
||||
|
||||
|
|
@ -4375,6 +4433,20 @@ function colorizeTerminalStream(text) {
|
|||
|
||||
// 4. 匯出 (export) 所有需要被外部呼叫的函數
|
||||
export function initTerminal() {
|
||||
const container = document.getElementById('terminal-container');
|
||||
|
||||
// ==========================================
|
||||
// 🛡️ DOM 防火牆:攔截殘缺的假鍵盤事件
|
||||
// ==========================================
|
||||
container.addEventListener('keydown', (e) => {
|
||||
// 如果這個事件物件沒有 getModifierState 函數 (代表它是外掛產生的假事件)
|
||||
if (typeof e.getModifierState !== 'function') {
|
||||
// 🛑 停止事件傳遞!不讓它流進 Xterm.js 裡
|
||||
e.stopPropagation();
|
||||
}
|
||||
}, true); // 🌟 關鍵:傳入 true 啟用「捕獲階段 (Capture Phase)」,確保我們比 Xterm.js 更早拿到事件
|
||||
// ==========================================
|
||||
|
||||
term = new Terminal({ cursorBlink: true, theme: { background: '#1e1e1e', foreground: '#e0e0e0' }, fontFamily: 'Courier New, monospace', fontSize: 15, scrollback: 100000 });
|
||||
fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
|
|
@ -4409,8 +4481,9 @@ export function connectWebSocket(resetCallback) {
|
|||
btnLoadTask.style.backgroundColor = '#c0392b';
|
||||
btnLoadTask.style.cursor = 'pointer';
|
||||
}
|
||||
document.getElementById('wsStatus').textContent = `狀態:已連線`;
|
||||
document.getElementById('wsStatus').style.color = '#27ae60';
|
||||
// 🌟 UX 升級:剛連上 WS 時,顯示「驗證中...」而不是直接顯示「已連線」
|
||||
document.getElementById('wsStatus').textContent = `狀態:連線與驗證中...`;
|
||||
document.getElementById('wsStatus').style.color = '#f39c12'; // 橘色
|
||||
document.getElementById('btnConnect').style.display = 'none';
|
||||
document.getElementById('btnDisconnect').style.display = 'inline-block';
|
||||
document.getElementById('cmtsHost').disabled = true;
|
||||
|
|
@ -4432,6 +4505,12 @@ export function connectWebSocket(resetCallback) {
|
|||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
// 🌟 UX 升級:只要收到設備傳來的第一個字元,就代表 SSH 驗證成功,正式轉為綠色「已連線」
|
||||
const statusEl = document.getElementById('wsStatus');
|
||||
if (statusEl.textContent.includes('驗證中')) {
|
||||
statusEl.textContent = `狀態:已連線`;
|
||||
statusEl.style.color = '#27ae60'; // 綠色
|
||||
}
|
||||
term.write(colorizeTerminalStream(event.data));
|
||||
};
|
||||
|
||||
|
|
@ -4452,6 +4531,11 @@ export function connectWebSocket(resetCallback) {
|
|||
document.getElementById('cmtsPass').disabled = false;
|
||||
term.writeln('\r\n\x1b[31m--- Connection Closed ---\x1b[0m\r\n');
|
||||
|
||||
// 🌟 關鍵修復:攔截後端傳來的 4001 錯誤碼,彈出明確的警告視窗
|
||||
if (event.code === 4001) {
|
||||
alert(`❌ 連線失敗!\n請檢查目標設備 IP、帳號與密碼是否正確。\n\n系統訊息: ${event.reason}`);
|
||||
}
|
||||
|
||||
// 🌟 新增:斷線時,強制觸發配置任務切換,藉此「關閉」SSE 監聽
|
||||
const configTaskSelect = document.getElementById('configTask');
|
||||
if (configTaskSelect) {
|
||||
|
|
@ -4986,6 +5070,14 @@ export async function startEditFolder(path, elementId) {
|
|||
const detailsEl = document.getElementById(`details-${elementId}`);
|
||||
if (detailsEl) detailsEl.open = true;
|
||||
|
||||
// ==========================================
|
||||
// 🌟 完美修復:在抓取葉子節點前,強制將該資料夾底下的所有 HTML 預先渲染出來
|
||||
// 這樣 querySelectorAll 就能一次抓到所有深層的欄位!
|
||||
// ==========================================
|
||||
if (typeof window.forceRenderFolderHTML === 'function') {
|
||||
window.forceRenderFolderHTML(elementId);
|
||||
}
|
||||
|
||||
const contentDiv = document.getElementById(`content-${elementId}`);
|
||||
if (!contentDiv) return;
|
||||
|
||||
|
|
@ -6406,8 +6498,8 @@ export function collapseAll(elementId) {
|
|||
// ==========================================
|
||||
// 🌟 核心函數:生成完整設備配置樹狀圖 (Lazy 版)
|
||||
// ==========================================
|
||||
// 🌟 新增 forceExpand 參數,預設為 false
|
||||
export function buildTree(node, path = '', isParentCommandGroup = false, mode = 'running', forceExpand = false) {
|
||||
// 🌟 修改函數宣告,加入 renderAll 參數
|
||||
export function buildTree(node, path = '', isParentCommandGroup = false, mode = 'running', forceExpand = false, renderAll = false) {
|
||||
const htmlParts = [];
|
||||
if (!node || typeof node !== 'object') return htmlParts.join('');
|
||||
|
||||
|
|
@ -6477,13 +6569,14 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
|
|||
|
||||
// 🚀 核心魔法:如果 forceExpand 為 true,直接在記憶體中遞迴生成子節點 HTML
|
||||
const isOpenAttr = forceExpand ? 'open' : '';
|
||||
const isLoadedAttr = forceExpand ? 'true' : 'false';
|
||||
const isLoadedAttr = (forceExpand || renderAll) ? 'true' : 'false';
|
||||
const displayClosed = forceExpand ? 'none' : 'inline-block';
|
||||
const displayOpen = forceExpand ? 'inline-block' : 'none';
|
||||
|
||||
let innerContent = '<!-- 🌟 延遲渲染 -->';
|
||||
if (forceExpand) {
|
||||
innerContent = buildTree(value, currentPath, isCommandGroup, mode, true);
|
||||
if (forceExpand || renderAll) {
|
||||
// 🌟 遞迴呼叫時,把 renderAll 傳遞下去
|
||||
innerContent = buildTree(value, currentPath, isCommandGroup, mode, forceExpand, renderAll);
|
||||
}
|
||||
|
||||
htmlParts.push(`
|
||||
|
|
@ -6755,6 +6848,25 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa
|
|||
return htmlParts.join('');
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 🌟 輔助函數:強制預先渲染資料夾 HTML (供編輯模式使用)
|
||||
// ==========================================
|
||||
window.forceRenderFolderHTML = function(elementId) {
|
||||
const detailsEl = document.getElementById(`details-${elementId}`);
|
||||
const contentDiv = document.getElementById(`content-${elementId}`);
|
||||
const cache = folderDataCache[elementId];
|
||||
|
||||
if (detailsEl && contentDiv && cache && detailsEl.dataset.loaded !== 'true') {
|
||||
const currentPath = detailsEl.dataset.path;
|
||||
const isCommandGroup = detailsEl.dataset.isGroup === 'true';
|
||||
const mode = detailsEl.dataset.mode;
|
||||
|
||||
// 重新渲染,傳入 renderAll = true (生成 HTML 但不展開)
|
||||
contentDiv.innerHTML = buildTree(cache, currentPath, isCommandGroup, mode, false, true);
|
||||
detailsEl.dataset.loaded = 'true';
|
||||
}
|
||||
};
|
||||
|
||||
================================================================================
|
||||
FILE: routers/backup.py
|
||||
================================================================================
|
||||
|
|
@ -7190,12 +7302,19 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
|
|||
if ssh_task and not ssh_task.done():
|
||||
ssh_task.cancel()
|
||||
except Exception as e:
|
||||
error_msg = f"\r\n\x1b[31mSSH Connection Error: {str(e)}\x1b[0m\r\n"
|
||||
err_str = str(e)
|
||||
error_msg = f"\r\n\x1b[31mSSH Connection Error: {err_str}\x1b[0m\r\n"
|
||||
try:
|
||||
# 先把錯誤印在終端機畫面上
|
||||
await websocket.send_text(error_msg)
|
||||
|
||||
# 🌟 關鍵修復:使用自訂斷線碼 4001,並附上錯誤原因 (WebSocket 規範 reason 最長 123 bytes)
|
||||
safe_reason = err_str[:120] if err_str else "Authentication or Connection Failed"
|
||||
await websocket.close(code=4001, reason=safe_reason)
|
||||
except:
|
||||
pass
|
||||
logger.error("❌ [Connection Error] 建立連線或執行過程中發生錯誤", exc_info=True)
|
||||
return # 🌟 提早結束,避免下方的 finally 再次執行 close 導致報錯
|
||||
finally:
|
||||
# 🌟 確保 process 與 conn 絕對被關閉,防止 Memory Leak
|
||||
if process:
|
||||
|
|
@ -7792,8 +7911,8 @@ class GodModeRequest(BaseModel):
|
|||
|
||||
@router.post("/auth/god-mode", summary="驗證上帝模式進階密碼")
|
||||
async def verify_god_mode(req: GodModeRequest):
|
||||
# 從環境變數讀取正確密碼,預設為 "admin999"
|
||||
expected_password = os.getenv("GOD_MODE_SECRET", "swpa@Serc0mm")
|
||||
# 從環境變數讀取正確密碼,預設為 "19760107@Serc0mm"
|
||||
expected_password = os.getenv("GOD_MODE_SECRET", "19760107@Serc0mm")
|
||||
|
||||
# 使用 compare_digest 防禦計時攻擊
|
||||
if secrets.compare_digest(req.password, expected_password):
|
||||
|
|
|
|||
|
|
@ -80,12 +80,19 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
|
|||
if ssh_task and not ssh_task.done():
|
||||
ssh_task.cancel()
|
||||
except Exception as e:
|
||||
error_msg = f"\r\n\x1b[31mSSH Connection Error: {str(e)}\x1b[0m\r\n"
|
||||
err_str = str(e)
|
||||
error_msg = f"\r\n\x1b[31mSSH Connection Error: {err_str}\x1b[0m\r\n"
|
||||
try:
|
||||
# 先把錯誤印在終端機畫面上
|
||||
await websocket.send_text(error_msg)
|
||||
|
||||
# 🌟 關鍵修復:使用自訂斷線碼 4001,並附上錯誤原因 (WebSocket 規範 reason 最長 123 bytes)
|
||||
safe_reason = err_str[:120] if err_str else "Authentication or Connection Failed"
|
||||
await websocket.close(code=4001, reason=safe_reason)
|
||||
except:
|
||||
pass
|
||||
logger.error("❌ [Connection Error] 建立連線或執行過程中發生錯誤", exc_info=True)
|
||||
return # 🌟 提早結束,避免下方的 finally 再次執行 close 導致報錯
|
||||
finally:
|
||||
# 🌟 確保 process 與 conn 絕對被關閉,防止 Memory Leak
|
||||
if process:
|
||||
|
|
|
|||
|
|
@ -79,8 +79,9 @@ export function connectWebSocket(resetCallback) {
|
|||
btnLoadTask.style.backgroundColor = '#c0392b';
|
||||
btnLoadTask.style.cursor = 'pointer';
|
||||
}
|
||||
document.getElementById('wsStatus').textContent = `狀態:已連線`;
|
||||
document.getElementById('wsStatus').style.color = '#27ae60';
|
||||
// 🌟 UX 升級:剛連上 WS 時,顯示「驗證中...」而不是直接顯示「已連線」
|
||||
document.getElementById('wsStatus').textContent = `狀態:連線與驗證中...`;
|
||||
document.getElementById('wsStatus').style.color = '#f39c12'; // 橘色
|
||||
document.getElementById('btnConnect').style.display = 'none';
|
||||
document.getElementById('btnDisconnect').style.display = 'inline-block';
|
||||
document.getElementById('cmtsHost').disabled = true;
|
||||
|
|
@ -102,6 +103,12 @@ export function connectWebSocket(resetCallback) {
|
|||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
// 🌟 UX 升級:只要收到設備傳來的第一個字元,就代表 SSH 驗證成功,正式轉為綠色「已連線」
|
||||
const statusEl = document.getElementById('wsStatus');
|
||||
if (statusEl.textContent.includes('驗證中')) {
|
||||
statusEl.textContent = `狀態:已連線`;
|
||||
statusEl.style.color = '#27ae60'; // 綠色
|
||||
}
|
||||
term.write(colorizeTerminalStream(event.data));
|
||||
};
|
||||
|
||||
|
|
@ -122,6 +129,11 @@ export function connectWebSocket(resetCallback) {
|
|||
document.getElementById('cmtsPass').disabled = false;
|
||||
term.writeln('\r\n\x1b[31m--- Connection Closed ---\x1b[0m\r\n');
|
||||
|
||||
// 🌟 關鍵修復:攔截後端傳來的 4001 錯誤碼,彈出明確的警告視窗
|
||||
if (event.code === 4001) {
|
||||
alert(`❌ 連線失敗!\n請檢查目標設備 IP、帳號與密碼是否正確。\n\n系統訊息: ${event.reason}`);
|
||||
}
|
||||
|
||||
// 🌟 新增:斷線時,強制觸發配置任務切換,藉此「關閉」SSE 監聽
|
||||
const configTaskSelect = document.getElementById('configTask');
|
||||
if (configTaskSelect) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue