252 lines
7.1 KiB
TypeScript
252 lines
7.1 KiB
TypeScript
// =====================================================
|
||
// 基於 IP 的連線管理器
|
||
// =====================================================
|
||
|
||
import mysql from 'mysql2/promise';
|
||
import { db } from './database';
|
||
|
||
export class IPBasedConnectionManager {
|
||
private static instance: IPBasedConnectionManager;
|
||
private clientIP: string | null = null;
|
||
private connectionTracker = new Map<string, {
|
||
connectionId: string;
|
||
createdAt: number;
|
||
lastUsed: number;
|
||
userAgent?: string;
|
||
}>();
|
||
|
||
private constructor() {
|
||
this.detectClientIP();
|
||
}
|
||
|
||
public static getInstance(): IPBasedConnectionManager {
|
||
if (!IPBasedConnectionManager.instance) {
|
||
IPBasedConnectionManager.instance = new IPBasedConnectionManager();
|
||
}
|
||
return IPBasedConnectionManager.instance;
|
||
}
|
||
|
||
// 檢測客戶端 IP
|
||
private detectClientIP() {
|
||
// 在瀏覽器環境中,我們無法直接獲取真實 IP
|
||
// 但我們可以通過其他方式來識別連線
|
||
if (typeof window !== 'undefined') {
|
||
// 客戶端:生成一個唯一標識符
|
||
this.clientIP = this.generateClientId();
|
||
console.log('🖥️ 客戶端標識符:', this.clientIP);
|
||
} else {
|
||
// 服務端:嘗試從環境變數或請求中獲取
|
||
this.clientIP = process.env.CLIENT_IP || 'server-side';
|
||
console.log('🖥️ 服務端標識符:', this.clientIP);
|
||
}
|
||
}
|
||
|
||
// 生成客戶端唯一標識符
|
||
private generateClientId(): string {
|
||
const timestamp = Date.now();
|
||
const random = Math.random().toString(36).substring(2);
|
||
return `client_${timestamp}_${random}`;
|
||
}
|
||
|
||
// 設置客戶端 IP(從請求中獲取)
|
||
public setClientIP(ip: string) {
|
||
this.clientIP = ip;
|
||
console.log('🖥️ 設置客戶端 IP:', ip);
|
||
}
|
||
|
||
// 獲取客戶端 IP
|
||
public getClientIP(): string | null {
|
||
return this.clientIP;
|
||
}
|
||
|
||
// 註冊連線(帶 IP 標識)
|
||
public registerConnection(connectionId: string, metadata?: {
|
||
userAgent?: string;
|
||
requestId?: string;
|
||
}) {
|
||
if (!this.clientIP) return;
|
||
|
||
this.connectionTracker.set(connectionId, {
|
||
connectionId,
|
||
createdAt: Date.now(),
|
||
lastUsed: Date.now(),
|
||
userAgent: metadata?.userAgent,
|
||
});
|
||
|
||
console.log(`📝 註冊連線: ${connectionId} (IP: ${this.clientIP})`);
|
||
}
|
||
|
||
// 更新連線使用時間
|
||
public updateConnectionUsage(connectionId: string) {
|
||
const conn = this.connectionTracker.get(connectionId);
|
||
if (conn) {
|
||
conn.lastUsed = Date.now();
|
||
}
|
||
}
|
||
|
||
// 釋放連線
|
||
public releaseConnection(connectionId: string) {
|
||
const conn = this.connectionTracker.get(connectionId);
|
||
if (conn) {
|
||
this.connectionTracker.delete(connectionId);
|
||
console.log(`🗑️ 釋放連線: ${connectionId} (IP: ${this.clientIP})`);
|
||
}
|
||
}
|
||
|
||
// 根據 IP 清理連線
|
||
public async cleanupConnectionsByIP(targetIP?: string): Promise<{
|
||
success: boolean;
|
||
killedCount: number;
|
||
message: string;
|
||
}> {
|
||
const ipToClean = targetIP || this.clientIP;
|
||
|
||
if (!ipToClean) {
|
||
return {
|
||
success: false,
|
||
killedCount: 0,
|
||
message: '無法識別客戶端 IP'
|
||
};
|
||
}
|
||
|
||
console.log(`🧹 開始清理 IP ${ipToClean} 的連線...`);
|
||
|
||
let connection = null;
|
||
let killedCount = 0;
|
||
|
||
try {
|
||
// 建立臨時連線
|
||
connection = await db.getConnection();
|
||
|
||
// 獲取指定 IP 的所有連線(使用更寬鬆的匹配)
|
||
const [connections] = await connection.execute(`
|
||
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
|
||
FROM information_schema.PROCESSLIST
|
||
WHERE USER = ? AND (HOST LIKE ? OR HOST LIKE ? OR HOST LIKE ?)
|
||
ORDER BY TIME DESC
|
||
`, [
|
||
process.env.DB_USER || 'A999',
|
||
`%${ipToClean}%`, // 包含 IP 的 HOST
|
||
`%${ipToClean}.%`, // IP 開頭的 HOST
|
||
`%${ipToClean}-%` // IP 帶連字符的 HOST
|
||
]);
|
||
|
||
console.log(`🔍 找到 ${connections.length} 個來自 ${ipToClean} 的連線`);
|
||
|
||
// 顯示連線詳情
|
||
connections.forEach((conn: any, index: number) => {
|
||
console.log(`${index + 1}. ID: ${conn.ID}, HOST: ${conn.HOST}, 時間: ${conn.TIME}s, 狀態: ${conn.STATE}`);
|
||
});
|
||
|
||
// 殺死指定 IP 的連線(除了當前連線)
|
||
for (const conn of connections) {
|
||
if (conn.ID !== connection.threadId) {
|
||
try {
|
||
await connection.execute(`KILL ${conn.ID}`);
|
||
console.log(`💀 已殺死連線 ${conn.ID} (HOST: ${conn.HOST})`);
|
||
killedCount++;
|
||
} catch (error: any) {
|
||
console.log(`⚠️ 無法殺死連線 ${conn.ID}: ${error.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 清理本地追蹤的連線
|
||
this.connectionTracker.clear();
|
||
|
||
console.log(`✅ 已清理 ${killedCount} 個來自 ${ipToClean} 的連線`);
|
||
|
||
return {
|
||
success: true,
|
||
killedCount,
|
||
message: `已清理 ${killedCount} 個來自 ${ipToClean} 的連線`
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('❌ 清理連線失敗:', error);
|
||
return {
|
||
success: false,
|
||
killedCount: 0,
|
||
message: `清理失敗: ${error instanceof Error ? error.message : '未知錯誤'}`
|
||
};
|
||
} finally {
|
||
if (connection) {
|
||
connection.release();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 獲取指定 IP 的連線狀態
|
||
public async getConnectionsByIP(targetIP?: string): Promise<{
|
||
ip: string;
|
||
connectionCount: number;
|
||
connections: any[];
|
||
}> {
|
||
const ipToCheck = targetIP || this.clientIP;
|
||
|
||
if (!ipToCheck) {
|
||
return {
|
||
ip: 'unknown',
|
||
connectionCount: 0,
|
||
connections: []
|
||
};
|
||
}
|
||
|
||
let connection = null;
|
||
|
||
try {
|
||
connection = await db.getConnection();
|
||
|
||
const [connections] = await connection.execute(`
|
||
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
|
||
FROM information_schema.PROCESSLIST
|
||
WHERE USER = ? AND HOST LIKE ?
|
||
ORDER BY TIME DESC
|
||
`, [process.env.DB_USER || 'A999', `%${ipToCheck}%`]);
|
||
|
||
return {
|
||
ip: ipToCheck,
|
||
connectionCount: connections.length,
|
||
connections: connections
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('❌ 獲取連線狀態失敗:', error);
|
||
return {
|
||
ip: ipToCheck,
|
||
connectionCount: 0,
|
||
connections: []
|
||
};
|
||
} finally {
|
||
if (connection) {
|
||
connection.release();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 清理所有本地追蹤的連線
|
||
public cleanupLocalConnections() {
|
||
const count = this.connectionTracker.size;
|
||
this.connectionTracker.clear();
|
||
console.log(`🧹 已清理 ${count} 個本地追蹤的連線`);
|
||
return count;
|
||
}
|
||
|
||
// 獲取本地連線統計
|
||
public getLocalConnectionStats() {
|
||
return {
|
||
clientIP: this.clientIP,
|
||
trackedConnections: this.connectionTracker.size,
|
||
connections: Array.from(this.connectionTracker.values()).map(conn => ({
|
||
connectionId: conn.connectionId,
|
||
createdAt: new Date(conn.createdAt).toISOString(),
|
||
lastUsed: new Date(conn.lastUsed).toISOString(),
|
||
userAgent: conn.userAgent
|
||
}))
|
||
};
|
||
}
|
||
}
|
||
|
||
// 導出單例實例
|
||
export const ipConnectionManager = IPBasedConnectionManager.getInstance();
|