diff --git a/build-output/builder-debug.yml b/build-output/builder-debug.yml index 98ad866..8512fd7 100644 --- a/build-output/builder-debug.yml +++ b/build-output/builder-debug.yml @@ -18,4 +18,4 @@ x64: - dist/**/* - index.html nsis: - script: "!include \"C:\\Gitsources\\Ip-Checker\\ip-monitor\\node_modules\\app-builder-lib\\templates\\nsis\\include\\StdUtils.nsh\"\n!addincludedir \"C:\\Gitsources\\Ip-Checker\\ip-monitor\\node_modules\\app-builder-lib\\templates\\nsis\\include\"\n!macro _isUpdated _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"updated\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isUpdated `\"\" isUpdated \"\"`\n\n!macro _isForceRun _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"force-run\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForceRun `\"\" isForceRun \"\"`\n\n!macro _isKeepShortcuts _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"keep-shortcuts\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isKeepShortcuts `\"\" isKeepShortcuts \"\"`\n\n!macro _isNoDesktopShortcut _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"no-desktop-shortcut\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isNoDesktopShortcut `\"\" isNoDesktopShortcut \"\"`\n\n!macro _isDeleteAppData _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"delete-app-data\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isDeleteAppData `\"\" isDeleteAppData \"\"`\n\n!macro _isForAllUsers _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"allusers\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForAllUsers `\"\" isForAllUsers \"\"`\n\n!macro _isForCurrentUser _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"currentuser\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForCurrentUser `\"\" isForCurrentUser \"\"`\n\n!macro addLangs\n !insertmacro MUI_LANGUAGE \"English\"\n !insertmacro MUI_LANGUAGE \"German\"\n !insertmacro MUI_LANGUAGE \"French\"\n !insertmacro MUI_LANGUAGE \"SpanishInternational\"\n !insertmacro MUI_LANGUAGE \"SimpChinese\"\n !insertmacro MUI_LANGUAGE \"TradChinese\"\n !insertmacro MUI_LANGUAGE \"Japanese\"\n !insertmacro MUI_LANGUAGE \"Korean\"\n !insertmacro MUI_LANGUAGE \"Italian\"\n !insertmacro MUI_LANGUAGE \"Dutch\"\n !insertmacro MUI_LANGUAGE \"Danish\"\n !insertmacro MUI_LANGUAGE \"Swedish\"\n !insertmacro MUI_LANGUAGE \"Norwegian\"\n !insertmacro MUI_LANGUAGE \"Finnish\"\n !insertmacro MUI_LANGUAGE \"Russian\"\n !insertmacro MUI_LANGUAGE \"Portuguese\"\n !insertmacro MUI_LANGUAGE \"PortugueseBR\"\n !insertmacro MUI_LANGUAGE \"Polish\"\n !insertmacro MUI_LANGUAGE \"Ukrainian\"\n !insertmacro MUI_LANGUAGE \"Czech\"\n !insertmacro MUI_LANGUAGE \"Slovak\"\n !insertmacro MUI_LANGUAGE \"Hungarian\"\n !insertmacro MUI_LANGUAGE \"Arabic\"\n !insertmacro MUI_LANGUAGE \"Turkish\"\n !insertmacro MUI_LANGUAGE \"Thai\"\n !insertmacro MUI_LANGUAGE \"Vietnamese\"\n!macroend\n\n!include \"C:\\Users\\alper\\AppData\\Local\\Temp\\t-FyvTN1\\0-messages.nsh\"\n!addplugindir /x86-unicode \"C:\\Users\\alper\\AppData\\Local\\electron-builder\\Cache\\nsis\\nsis-resources-3.4.1\\plugins\\x86-unicode\"\n\n!include \"common.nsh\"\n!include \"extractAppPackage.nsh\"\n\n# https://github.com/electron-userland/electron-builder/issues/3972#issuecomment-505171582\nCRCCheck off\nWindowIcon Off\nAutoCloseWindow True\nRequestExecutionLevel ${REQUEST_EXECUTION_LEVEL}\n\nFunction .onInit\n !ifndef SPLASH_IMAGE\n SetSilent silent\n !endif\n\n !insertmacro check64BitAndSetRegView\nFunctionEnd\n\nFunction .onGUIInit\n InitPluginsDir\n\n !ifdef SPLASH_IMAGE\n File /oname=$PLUGINSDIR\\splash.bmp \"${SPLASH_IMAGE}\"\n BgImage::SetBg $PLUGINSDIR\\splash.bmp\n BgImage::Redraw\n !endif\nFunctionEnd\n\nSection\n !ifdef SPLASH_IMAGE\n HideWindow\n !endif\n\n StrCpy $INSTDIR \"$PLUGINSDIR\\app\"\n !ifdef UNPACK_DIR_NAME\n StrCpy $INSTDIR \"$TEMP\\${UNPACK_DIR_NAME}\"\n !endif\n\n RMDir /r $INSTDIR\n SetOutPath $INSTDIR\n\n !ifdef APP_DIR_64\n !ifdef APP_DIR_ARM64\n !ifdef APP_DIR_32\n ${if} ${IsNativeARM64}\n File /r \"${APP_DIR_ARM64}\\*.*\"\n ${elseif} ${RunningX64}\n File /r \"${APP_DIR_64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_32}\\*.*\"\n ${endIf}\n !else\n ${if} ${IsNativeARM64}\n File /r \"${APP_DIR_ARM64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_64}\\*.*\"\n {endIf}\n !endif\n !else\n !ifdef APP_DIR_32\n ${if} ${RunningX64}\n File /r \"${APP_DIR_64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_32}\\*.*\"\n ${endIf}\n !else\n File /r \"${APP_DIR_64}\\*.*\"\n !endif\n !endif\n !else\n !ifdef APP_DIR_32\n File /r \"${APP_DIR_32}\\*.*\"\n !else\n !insertmacro extractEmbeddedAppPackage\n !endif\n !endif\n\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_DIR\", \"$EXEDIR\").r0'\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_FILE\", \"$EXEPATH\").r0'\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_APP_FILENAME\", \"${APP_FILENAME}\").r0'\n ${StdUtils.GetAllParameters} $R0 0\n\n !ifdef SPLASH_IMAGE\n BgImage::Destroy\n !endif\n\n\tExecWait \"$INSTDIR\\${APP_EXECUTABLE_FILENAME} $R0\" $0\n SetErrorLevel $0\n\n SetOutPath $EXEDIR\n\tRMDir /r $INSTDIR\nSectionEnd\n" + script: "!include \"C:\\Gitsources\\Ip-Checker\\ip-monitor\\node_modules\\app-builder-lib\\templates\\nsis\\include\\StdUtils.nsh\"\n!addincludedir \"C:\\Gitsources\\Ip-Checker\\ip-monitor\\node_modules\\app-builder-lib\\templates\\nsis\\include\"\n!macro _isUpdated _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"updated\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isUpdated `\"\" isUpdated \"\"`\n\n!macro _isForceRun _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"force-run\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForceRun `\"\" isForceRun \"\"`\n\n!macro _isKeepShortcuts _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"keep-shortcuts\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isKeepShortcuts `\"\" isKeepShortcuts \"\"`\n\n!macro _isNoDesktopShortcut _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"no-desktop-shortcut\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isNoDesktopShortcut `\"\" isNoDesktopShortcut \"\"`\n\n!macro _isDeleteAppData _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"delete-app-data\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isDeleteAppData `\"\" isDeleteAppData \"\"`\n\n!macro _isForAllUsers _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"allusers\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForAllUsers `\"\" isForAllUsers \"\"`\n\n!macro _isForCurrentUser _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"currentuser\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForCurrentUser `\"\" isForCurrentUser \"\"`\n\n!macro addLangs\n !insertmacro MUI_LANGUAGE \"English\"\n !insertmacro MUI_LANGUAGE \"German\"\n !insertmacro MUI_LANGUAGE \"French\"\n !insertmacro MUI_LANGUAGE \"SpanishInternational\"\n !insertmacro MUI_LANGUAGE \"SimpChinese\"\n !insertmacro MUI_LANGUAGE \"TradChinese\"\n !insertmacro MUI_LANGUAGE \"Japanese\"\n !insertmacro MUI_LANGUAGE \"Korean\"\n !insertmacro MUI_LANGUAGE \"Italian\"\n !insertmacro MUI_LANGUAGE \"Dutch\"\n !insertmacro MUI_LANGUAGE \"Danish\"\n !insertmacro MUI_LANGUAGE \"Swedish\"\n !insertmacro MUI_LANGUAGE \"Norwegian\"\n !insertmacro MUI_LANGUAGE \"Finnish\"\n !insertmacro MUI_LANGUAGE \"Russian\"\n !insertmacro MUI_LANGUAGE \"Portuguese\"\n !insertmacro MUI_LANGUAGE \"PortugueseBR\"\n !insertmacro MUI_LANGUAGE \"Polish\"\n !insertmacro MUI_LANGUAGE \"Ukrainian\"\n !insertmacro MUI_LANGUAGE \"Czech\"\n !insertmacro MUI_LANGUAGE \"Slovak\"\n !insertmacro MUI_LANGUAGE \"Hungarian\"\n !insertmacro MUI_LANGUAGE \"Arabic\"\n !insertmacro MUI_LANGUAGE \"Turkish\"\n !insertmacro MUI_LANGUAGE \"Thai\"\n !insertmacro MUI_LANGUAGE \"Vietnamese\"\n!macroend\n\n!include \"C:\\Users\\alper\\AppData\\Local\\Temp\\t-1gzOG5\\0-messages.nsh\"\n!addplugindir /x86-unicode \"C:\\Users\\alper\\AppData\\Local\\electron-builder\\Cache\\nsis\\nsis-resources-3.4.1\\plugins\\x86-unicode\"\n\n!include \"common.nsh\"\n!include \"extractAppPackage.nsh\"\n\n# https://github.com/electron-userland/electron-builder/issues/3972#issuecomment-505171582\nCRCCheck off\nWindowIcon Off\nAutoCloseWindow True\nRequestExecutionLevel ${REQUEST_EXECUTION_LEVEL}\n\nFunction .onInit\n !ifndef SPLASH_IMAGE\n SetSilent silent\n !endif\n\n !insertmacro check64BitAndSetRegView\nFunctionEnd\n\nFunction .onGUIInit\n InitPluginsDir\n\n !ifdef SPLASH_IMAGE\n File /oname=$PLUGINSDIR\\splash.bmp \"${SPLASH_IMAGE}\"\n BgImage::SetBg $PLUGINSDIR\\splash.bmp\n BgImage::Redraw\n !endif\nFunctionEnd\n\nSection\n !ifdef SPLASH_IMAGE\n HideWindow\n !endif\n\n StrCpy $INSTDIR \"$PLUGINSDIR\\app\"\n !ifdef UNPACK_DIR_NAME\n StrCpy $INSTDIR \"$TEMP\\${UNPACK_DIR_NAME}\"\n !endif\n\n RMDir /r $INSTDIR\n SetOutPath $INSTDIR\n\n !ifdef APP_DIR_64\n !ifdef APP_DIR_ARM64\n !ifdef APP_DIR_32\n ${if} ${IsNativeARM64}\n File /r \"${APP_DIR_ARM64}\\*.*\"\n ${elseif} ${RunningX64}\n File /r \"${APP_DIR_64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_32}\\*.*\"\n ${endIf}\n !else\n ${if} ${IsNativeARM64}\n File /r \"${APP_DIR_ARM64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_64}\\*.*\"\n {endIf}\n !endif\n !else\n !ifdef APP_DIR_32\n ${if} ${RunningX64}\n File /r \"${APP_DIR_64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_32}\\*.*\"\n ${endIf}\n !else\n File /r \"${APP_DIR_64}\\*.*\"\n !endif\n !endif\n !else\n !ifdef APP_DIR_32\n File /r \"${APP_DIR_32}\\*.*\"\n !else\n !insertmacro extractEmbeddedAppPackage\n !endif\n !endif\n\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_DIR\", \"$EXEDIR\").r0'\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_FILE\", \"$EXEPATH\").r0'\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_APP_FILENAME\", \"${APP_FILENAME}\").r0'\n ${StdUtils.GetAllParameters} $R0 0\n\n !ifdef SPLASH_IMAGE\n BgImage::Destroy\n !endif\n\n\tExecWait \"$INSTDIR\\${APP_EXECUTABLE_FILENAME} $R0\" $0\n SetErrorLevel $0\n\n SetOutPath $EXEDIR\n\tRMDir /r $INSTDIR\nSectionEnd\n" diff --git a/build-output/win-unpacked/resources/app.asar b/build-output/win-unpacked/resources/app.asar index 4dea772..411ae19 100644 Binary files a/build-output/win-unpacked/resources/app.asar and b/build-output/win-unpacked/resources/app.asar differ diff --git a/src/main/index.ts b/src/main/index.ts index 527ba74..c5ad51a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -21,7 +21,7 @@ import { NotificationService } from './services/NotificationService' import { PanelService } from './services/PanelService' import { M3u8Parser } from './services/M3u8Parser' import { IPC_CHANNELS } from '../shared/types' -import type { AppSettings, Channel, ErrorLogEntry } from '../shared/types' +import type { AppSettings, Channel, ErrorLogEntry, HeartbeatStats } from '../shared/types' // App quitting flag let isAppQuitting = false @@ -34,6 +34,10 @@ let notificationService: NotificationService let panelService: PanelService let m3u8Parser: M3u8Parser +// Heartbeat timer +let heartbeatInterval: NodeJS.Timeout | null = null +let checksLastMinute = 0 + // Window and tray let mainWindow: BrowserWindow | null = null let tray: Tray | null = null @@ -166,6 +170,9 @@ function initializeServices() { streamChecker = new StreamChecker( settings.monitoring, (result) => { + // Increment checks counter for heartbeat stats + incrementChecksCounter() + // Send result to renderer mainWindow?.webContents.send(IPC_CHANNELS.MONITORING_UPDATE, result) @@ -201,6 +208,67 @@ function initializeServices() { console.log('[App] Portable mode:', isPortableMode()) } +/** + * Start heartbeat timer - sends heartbeat every 60 seconds + * API v1.1: New heartbeat endpoint for monitor liveness + */ +function startHeartbeat(): void { + if (heartbeatInterval) { + clearInterval(heartbeatInterval) + } + + const settings = getSettings() + if (!settings.panel.enabled) { + console.log('[Heartbeat] Panel not enabled, skipping heartbeat') + return + } + + // Send heartbeat every 60 seconds + heartbeatInterval = setInterval(async () => { + if (!panelService.isEnabled()) return + + const channels = getChannels() + const monitoredChannels = channels.filter(c => c.isMonitored) + + const stats: HeartbeatStats = { + active_checks: monitoredChannels.length, + checks_last_minute: checksLastMinute, + cpu_usage_percent: process.cpuUsage().user / 1000000, // Convert to percent + memory_usage_mb: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + } + + const result = await panelService.sendHeartbeat(stats) + if (result.success) { + console.log('[Heartbeat] Sent successfully') + } else { + console.warn('[Heartbeat] Failed:', result.message) + } + + // Reset checks counter + checksLastMinute = 0 + }, 60000) // 60 seconds + + console.log('[Heartbeat] Timer started (60s interval)') +} + +/** + * Stop heartbeat timer + */ +function stopHeartbeat(): void { + if (heartbeatInterval) { + clearInterval(heartbeatInterval) + heartbeatInterval = null + console.log('[Heartbeat] Timer stopped') + } +} + +/** + * Increment checks counter for heartbeat stats + */ +function incrementChecksCounter(): void { + checksLastMinute++ +} + function setupIpcHandlers() { // Window controls ipcMain.on('window:minimize', () => mainWindow?.minimize()) @@ -214,7 +282,7 @@ function setupIpcHandlers() { ipcMain.on('window:close', () => { // Çarpı butonuna basınca uygulamayı tamamen kapat isAppQuitting = true - mainWindow?.close() + app.quit() }) // IP Service @@ -296,6 +364,11 @@ function setupIpcHandlers() { return await panelService.sendBurnVote(vote) }) + // Panel Heartbeat - API v1.1 + ipcMain.handle(IPC_CHANNELS.PANEL_HEARTBEAT, async (_, stats: HeartbeatStats) => { + return await panelService.sendHeartbeat(stats) + }) + // Settings ipcMain.handle(IPC_CHANNELS.GET_SETTINGS, async () => { return getSettings() @@ -337,6 +410,9 @@ app.whenReady().then(() => { createWindow() createTray() setupIpcHandlers() + + // Start heartbeat timer - API v1.1 + startHeartbeat() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { @@ -353,5 +429,6 @@ app.on('window-all-closed', () => { app.on('before-quit', () => { isAppQuitting = true + stopHeartbeat() streamChecker?.stop() }) diff --git a/src/main/preload.ts b/src/main/preload.ts index 8ecdd4d..90fdbf0 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -1,6 +1,6 @@ import { contextBridge, ipcRenderer } from 'electron' import { IPC_CHANNELS } from '../shared/types' -import type { Channel, AppSettings, BurnVote, NotificationType } from '../shared/types' +import type { Channel, AppSettings, BurnVote, NotificationType, HeartbeatStats } from '../shared/types' // Expose protected methods to renderer contextBridge.exposeInMainWorld('electron', { @@ -42,6 +42,7 @@ contextBridge.exposeInMainWorld('electron', { panelRegister: (config: unknown) => ipcRenderer.invoke(IPC_CHANNELS.PANEL_REGISTER, config), panelGetIps: () => ipcRenderer.invoke(IPC_CHANNELS.PANEL_GET_IPS), panelSendVote: (vote: BurnVote) => ipcRenderer.invoke(IPC_CHANNELS.PANEL_SEND_VOTE, vote), + panelHeartbeat: (stats: HeartbeatStats) => ipcRenderer.invoke(IPC_CHANNELS.PANEL_HEARTBEAT, stats), // Settings getSettings: () => ipcRenderer.invoke(IPC_CHANNELS.GET_SETTINGS), diff --git a/src/main/services/PanelService.ts b/src/main/services/PanelService.ts index 09c2417..09b6b03 100644 --- a/src/main/services/PanelService.ts +++ b/src/main/services/PanelService.ts @@ -1,9 +1,28 @@ -import axios, { AxiosInstance } from 'axios' -import type { PanelConfig, PanelIpPool, BurnVote } from '../../shared/types' +import axios, { AxiosInstance, AxiosError } from 'axios' +import type { + PanelConfig, + PanelIpPool, + BurnVote, + HeartbeatStats, + HeartbeatResponse, + PanelIpInfo +} from '../../shared/types' + +// Rate limiting state +interface RateLimitState { + isLimited: boolean + retryAfter: number + lastLimitTime: number +} export class PanelService { private config: PanelConfig private client: AxiosInstance | null = null + private rateLimitState: RateLimitState = { + isLimited: false, + retryAfter: 0, + lastLimitTime: 0 + } constructor(config: PanelConfig) { this.config = config @@ -26,29 +45,50 @@ export class PanelService { timeout: 10000, headers: { 'Content-Type': 'application/json', + // API v1.1: Token is now 64-char hex from admin panel ...(this.config.authToken && { 'Authorization': `Bearer ${this.config.authToken}` }) } }) + + // Add response interceptor for rate limiting + this.client.interceptors.response.use( + response => response, + async (error: AxiosError) => { + if (error.response?.status === 429) { + const retryAfter = parseInt(error.response.headers['retry-after'] || '60', 10) + this.rateLimitState = { + isLimited: true, + retryAfter, + lastLimitTime: Date.now() + } + console.warn(`[PanelService] Rate limited. Retry after ${retryAfter}s`) + } + throw error + } + ) } /** - * Register monitor with panel - * Based on MONITOR_API_GUIDE.md + * @deprecated API v1.1: Token is now created via admin panel + * This method is kept for backwards compatibility but will log a warning */ async register(): Promise<{ success: boolean; authToken?: string; error?: string }> { + console.warn('[PanelService] register() is deprecated in API v1.1. Token should be created via admin panel.') + if (!this.client) { return { success: false, error: 'Panel not configured' } } try { + const os = await import('os') const response = await this.client.post('/api.php?endpoint=servers', { server_id: this.config.monitorId, server_type: 'monitor', - hostname: require('os').hostname(), + hostname: os.hostname(), ip_address: await this.getLocalIp(), - country_code: 'TR' // Could be detected dynamically + country_code: 'TR' }) if (response.data.success) { @@ -66,104 +106,208 @@ export class PanelService { /** * Get IP pool from panel - * Based on MONITOR_API_GUIDE.md - Option A: REST API (Polling) + * API v1.1: Changed endpoint from ip-pool to servers&for_monitor=1 */ async getIpPool(): Promise { if (!this.client || !this.config.enabled) { - return { active: [], honeypot: [] } + return { ips: [], timestamp: Date.now() } + } + + // Check rate limit + if (this.isRateLimited()) { + console.warn('[PanelService] Rate limited, skipping getIpPool') + return { ips: [], timestamp: Date.now() } } try { + // API v1.1: New endpoint const response = await this.client.get('/api.php', { params: { - endpoint: 'ip-pool', - for_monitor: true + endpoint: 'servers', + for_monitor: '1' } }) - if (response.data.success) { - return response.data.ips || { active: [], honeypot: [] } + // API v1.1: New response format + if (response.data.ips) { + return { + ips: response.data.ips as PanelIpInfo[], + timestamp: response.data.timestamp || Date.now() + } } - console.error('Failed to get IP pool:', response.data.message) - return { active: [], honeypot: [] } + // Legacy format support + if (response.data.success && response.data.ips) { + const legacyIps = response.data.ips + return { + ips: [...(legacyIps.active || []), ...(legacyIps.honeypot || [])], + timestamp: response.data.timestamp || Date.now(), + active: legacyIps.active, + honeypot: legacyIps.honeypot + } + } + + console.error('[PanelService] Failed to get IP pool:', response.data.message) + return { ips: [], timestamp: Date.now() } } catch (error) { - console.error('Failed to fetch IP pool:', error) - return { active: [], honeypot: [] } + if ((error as AxiosError).response?.status !== 429) { + console.error('[PanelService] Failed to fetch IP pool:', error) + } + return { ips: [], timestamp: Date.now() } } } /** * Send burn vote - * Based on MONITOR_API_GUIDE.md - Section 2: Burn Bildirimi + * API v1.1: monitor_id removed from payload (extracted from Bearer token) + * API v1.1: reason field removed, evidence format changed */ - async sendBurnVote(vote: BurnVote): Promise<{ success: boolean; error?: string }> { + async sendBurnVote(vote: BurnVote): Promise<{ success: boolean; error?: string; currentVotes?: object }> { if (!this.client || !this.config.enabled) { return { success: false, error: 'Panel not enabled' } } + // Check rate limit + if (this.isRateLimited()) { + console.warn('[PanelService] Rate limited, queuing burn vote') + return { success: false, error: 'Rate limited, try again later' } + } + try { + // API v1.1: New payload format - no monitor_id, no reason const response = await this.client.post('/api.php?endpoint=burn-vote', { ip_address: vote.ip_address, vote: vote.vote, - reason: vote.reason, - evidence: { - ...vote.evidence, - monitor_id: this.config.monitorId, - timestamp: Math.floor(Date.now() / 1000) - } + evidence: vote.evidence }) if (response.data.success) { - console.log('Burn vote submitted:', { + console.log('[PanelService] Burn vote submitted:', { ip: vote.ip_address, vote: vote.vote, current_votes: response.data.current_votes }) - return { success: true } + return { + success: true, + currentVotes: response.data.current_votes + } } return { success: false, error: response.data.message } } catch (error) { + const axiosError = error as AxiosError + + // Handle 403 - wrong server type + if (axiosError.response?.status === 403) { + const errorData = axiosError.response.data as { message?: string } + console.error('[PanelService] Forbidden:', errorData.message) + return { success: false, error: errorData.message || 'Forbidden: Server type not allowed' } + } + + // Retry on rate limit + if (axiosError.response?.status === 429) { + const retryAfter = parseInt(axiosError.response.headers['retry-after'] || '60', 10) + console.warn(`[PanelService] Rate limited on burn vote. Waiting ${retryAfter}s...`) + + // Wait and retry once + await this.sleep(retryAfter * 1000) + return this.sendBurnVote(vote) + } + return { success: false, error: (error as Error).message } } } /** - * Report IP health status + * Send heartbeat to panel + * API v1.1: New endpoint for monitor liveness + */ + async sendHeartbeat(stats: HeartbeatStats): Promise { + if (!this.client || !this.config.enabled) { + return { success: false, message: 'Panel not enabled' } + } + + // Check rate limit + if (this.isRateLimited()) { + return { success: false, message: 'Rate limited' } + } + + try { + const response = await this.client.post( + '/api.php?endpoint=gateway-sync&action=heartbeat', + { data: stats } + ) + + if (response.data.success) { + console.log('[PanelService] Heartbeat sent successfully') + return { success: true, message: response.data.message || 'Heartbeat received' } + } + + return { success: false, message: response.data.message || 'Heartbeat failed' } + } catch (error) { + console.error('[PanelService] Heartbeat failed:', error) + return { success: false, message: (error as Error).message } + } + } + + /** + * Report IP health status - updated for API v1.1 */ async reportHealth( ipAddress: string, isWorking: boolean, checkResult: { - checkType: string - port?: number + checkType: 'http_health' | 'tcp_connect' | 'ssl_verify' | 'dns_lookup' + responseCode?: number latencyMs?: number error?: string } ): Promise { + // API v1.1: New evidence format with checked_at ISO timestamp const vote: BurnVote = { ip_address: ipAddress, vote: isWorking ? 'ok' : 'burn', - reason: isWorking ? 'IP working normally' : checkResult.error || 'Check failed', evidence: { check_type: checkResult.checkType, - port: checkResult.port, - response_time_ms: checkResult.latencyMs, - error_message: checkResult.error, - timestamp: Math.floor(Date.now() / 1000), - monitor_id: this.config.monitorId + response_code: checkResult.responseCode || 0, + latency_ms: checkResult.latencyMs || 0, + error_message: checkResult.error || null, + checked_at: new Date().toISOString() } } await this.sendBurnVote(vote) } + /** + * Check if currently rate limited + */ + private isRateLimited(): boolean { + if (!this.rateLimitState.isLimited) { + return false + } + + const elapsed = (Date.now() - this.rateLimitState.lastLimitTime) / 1000 + if (elapsed >= this.rateLimitState.retryAfter) { + this.rateLimitState.isLimited = false + return false + } + + return true + } + + /** + * Sleep utility for retry logic + */ + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) + } + /** * Get local IP address */ private async getLocalIp(): Promise { - const os = require('os') + const os = await import('os') const interfaces = os.networkInterfaces() for (const name of Object.keys(interfaces)) { @@ -183,5 +327,21 @@ export class PanelService { isEnabled(): boolean { return this.config.enabled && !!this.config.apiUrl && !!this.config.authToken } -} + /** + * Get rate limit status for UI display + */ + getRateLimitStatus(): { isLimited: boolean; secondsRemaining: number } { + if (!this.rateLimitState.isLimited) { + return { isLimited: false, secondsRemaining: 0 } + } + + const elapsed = (Date.now() - this.rateLimitState.lastLimitTime) / 1000 + const remaining = Math.max(0, this.rateLimitState.retryAfter - elapsed) + + return { + isLimited: remaining > 0, + secondsRemaining: Math.ceil(remaining) + } + } +} diff --git a/src/renderer/types/electron.d.ts b/src/renderer/types/electron.d.ts index c2d5c4f..d675572 100644 --- a/src/renderer/types/electron.d.ts +++ b/src/renderer/types/electron.d.ts @@ -30,6 +30,7 @@ export interface ElectronAPI { panelRegister: (config: import('@shared/types').PanelConfig) => Promise<{ success: boolean; authToken?: string }> panelGetIps: () => Promise panelSendVote: (vote: import('@shared/types').BurnVote) => Promise<{ success: boolean }> + panelHeartbeat: (stats: import('@shared/types').HeartbeatStats) => Promise // Settings getSettings: () => Promise diff --git a/src/shared/types.ts b/src/shared/types.ts index e4143cd..0af2a11 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -71,37 +71,62 @@ export interface NotificationSettings { notifyOnConnReset: boolean; } -// Panel API Types (from MONITOR_API_GUIDE) +// Panel API Types (from MONITOR_API_GUIDE v1.1) export interface PanelConfig { apiUrl: string; - authToken: string; + authToken: string; // Now 64-char hex token from admin panel monitorId: string; enabled: boolean; } +// Updated for API v1.1 - new response format from servers endpoint export interface PanelIpInfo { ip_address: string; server_id: string; - check_priority: 'high' | 'normal' | 'low'; + server_type: 'publisher' | 'monitor' | 'gateway'; + status: 'active' | 'inactive' | 'burned'; + active_users?: number; + // Legacy field for backwards compatibility + check_priority?: 'high' | 'normal' | 'low'; } +// Updated response format for API v1.1 export interface PanelIpPool { - active: PanelIpInfo[]; - honeypot: PanelIpInfo[]; + ips: PanelIpInfo[]; + timestamp: number; + // Legacy fields for backwards compatibility + active?: PanelIpInfo[]; + honeypot?: PanelIpInfo[]; } +// Updated for API v1.1 - monitor_id removed (extracted from Bearer token) export interface BurnVote { ip_address: string; vote: 'burn' | 'ok' | 'abstain'; - reason: string; - evidence: { - check_type: string; - port?: number; - response_time_ms?: number; - error_message?: string; - timestamp: number; - monitor_id: string; - }; + evidence: BurnVoteEvidence; +} + +// New evidence format for API v1.1 +export interface BurnVoteEvidence { + check_type: 'http_health' | 'tcp_connect' | 'ssl_verify' | 'dns_lookup'; + response_code: number; + latency_ms: number; + error_message: string | null; + checked_at: string; // ISO 8601 format +} + +// New for API v1.1 - Heartbeat stats +export interface HeartbeatStats { + active_checks: number; + checks_last_minute: number; + cpu_usage_percent?: number; + memory_usage_mb?: number; +} + +// Heartbeat response +export interface HeartbeatResponse { + success: boolean; + message: string; } // App Settings @@ -147,9 +172,10 @@ export const IPC_CHANNELS = { TEST_NOTIFICATION: 'notify:test', // Panel API - PANEL_REGISTER: 'panel:register', + PANEL_REGISTER: 'panel:register', // Deprecated in v1.1 PANEL_GET_IPS: 'panel:get-ips', PANEL_SEND_VOTE: 'panel:send-vote', + PANEL_HEARTBEAT: 'panel:heartbeat', // Settings GET_SETTINGS: 'settings:get',