const http = require('http'); const { exec } = require('child_process'); // 公网 IP let publicIP = 'unknown'; // 上报状态 let lastReportTime = null; let lastReportSuccess = false; // 获取环境变量 const TARGET_IMAGE = process.env.TARGET_IMAGE || ''; const DOCKER_ACCOUNTS = process.env.DOCKER_ACCOUNTS || ''; // 格式: user1:pass1,user2:pass2 const REPORT_URL = process.env.REPORT_URL || ''; const INTERVAL_SECONDS = parseInt(process.env.INTERVAL_SECONDS) || 60; // 间隔时间(秒) const MARK_DURATION_SECONDS = parseInt(process.env.MARK_DURATION_SECONDS) || 3600; // 标记持续时间(秒),默认1小时 // 解析 Docker 账号列表 const accounts = DOCKER_ACCOUNTS.split(',') .map(acc => { const [user, pass] = acc.trim().split(':'); return user && pass ? { user, pass } : null; }) .filter(acc => acc); // 账号状态管理 const accountMarkedTime = new Map(); // 记录账号被标记的时间 let currentAccountIndex = 0; let loggedInAccount = null; // 当前已登录的账号 let anonymousMarkedTime = null; // 匿名用户被标记的时间 // 获取当前可用账号(跳过被标记的) function getAvailableAccount() { if (accounts.length === 0) return null; const now = Date.now(); const markDuration = MARK_DURATION_SECONDS * 1000; // 清理已过期的标记 for (const [user, markedTime] of accountMarkedTime) { if (now - markedTime >= markDuration) { accountMarkedTime.delete(user); console.log(`账号 ${user} 标记已移除`); } } // 查找可用账号 for (let i = 0; i < accounts.length; i++) { const idx = (currentAccountIndex + i) % accounts.length; const acc = accounts[idx]; if (!accountMarkedTime.has(acc.user)) { currentAccountIndex = idx; return acc; } } // 所有账号都被标记 return null; } // 获取离最近标记移除的时间(毫秒) function getTimeUntilNextUnmark() { if (accountMarkedTime.size === 0) return 0; const now = Date.now(); const markDuration = MARK_DURATION_SECONDS * 1000; let minWait = Infinity; for (const markedTime of accountMarkedTime.values()) { const wait = markDuration - (now - markedTime); if (wait < minWait && wait > 0) minWait = wait; } return minWait === Infinity ? 0 : minWait; } // 获取公网 IP async function fetchPublicIP() { return new Promise((resolve) => { exec('curl -s --connect-timeout 5 https://api.ipify.org', (error, stdout, stderr) => { if (error || !stdout.trim()) { console.error('获取公网 IP 失败:', error?.message || 'empty response'); resolve('unknown'); } else { resolve(stdout.trim()); } }); }); } // 定期更新公网 IP async function startIPUpdate() { publicIP = await fetchPublicIP(); console.log(`公网 IP: ${publicIP}`); // 每 5 分钟更新一次 setInterval(async () => { publicIP = await fetchPublicIP(); }, 5 * 60 * 1000); } // 上报 URL async function reportToURL() { if (!REPORT_URL) return; return new Promise((resolve) => { exec(`curl -s --connect-timeout 10 "${REPORT_URL}"`, (error, stdout, stderr) => { lastReportTime = new Date().toISOString(); if (error) { lastReportSuccess = false; console.error(`[${lastReportTime}] 上报失败:`, error.message); } else { lastReportSuccess = true; console.log(`[${lastReportTime}] 上报成功`); } resolve(); }); }); } // 定时上报(每 6 小时) async function startReportTask() { if (!REPORT_URL) { console.log('REPORT_URL 未配置,跳过上报'); return; } // 立即执行一次 await reportToURL(); // 每 6 小时执行一次 setInterval(async () => { await reportToURL(); }, 6 * 60 * 60 * 1000); } // 解析镜像名称列表 const images = TARGET_IMAGE.split(',').map(img => img.trim()).filter(img => img); // 当前镜像索引(循环使用) let currentImageIndex = 0; console.log(`配置: ${images.length} 个镜像, ${accounts.length} 个账号, 间隔 ${INTERVAL_SECONDS} 秒`); // Docker 登录 async function dockerLogin(account) { return new Promise((resolve) => { exec(`echo "${account.pass}" | docker login -u "${account.user}" --password-stdin`, (error, stdout, stderr) => { if (error) { console.error(`账号 ${account.user} 登录失败:`, stderr.trim()); resolve(false); } else { console.log(`账号 ${account.user} 登录成功`); resolve(true); } }); }); } // 运行一次 docker manifest inspect(只检查一个镜像) async function runManifestInspect() { if (images.length === 0) { console.log('没有配置 TARGET_IMAGE'); return; } // 获取当前要检查的镜像 const image = images[currentImageIndex]; // 获取可用账号 const account = getAvailableAccount(); if (accounts.length > 0 && !account) { // 所有账号都被标记,等待 const waitTime = getTimeUntilNextUnmark(); console.log(`所有账号都被标记,等待 ${Math.round(waitTime / 1000)} 秒后重试`); setTimeout(() => runManifestInspect(), waitTime); return; } // 检查匿名用户是否被标记 if (accounts.length === 0 && anonymousMarkedTime) { const now = Date.now(); const markDuration = MARK_DURATION_SECONDS * 1000; if (now - anonymousMarkedTime < markDuration) { // 匿名用户仍在标记期内,等待 const waitTime = markDuration - (now - anonymousMarkedTime); console.log(`匿名用户已被标记,等待 ${Math.round(waitTime / 1000)} 秒后重试`); setTimeout(() => runManifestInspect(), waitTime); return; } else { // 标记已过期,清除 anonymousMarkedTime = null; console.log('匿名用户标记已移除'); } } // 如果有账号,检查是否需要登录 if (account) { // 如果当前账号已登录,跳过登录 if (loggedInAccount?.user !== account.user) { const loginSuccess = await dockerLogin(account); if (!loginSuccess) { // 登录失败,标记账号,尝试下一个 accountMarkedTime.set(account.user, Date.now()); console.log(`账号 ${account.user} 登录失败,已标记,${MARK_DURATION_SECONDS} 秒后移除标记`); loggedInAccount = null; // 重置登录状态 currentAccountIndex = (currentAccountIndex + 1) % accounts.length; return; } loggedInAccount = account; } } // 执行 manifest inspect await new Promise((resolve) => { exec(`docker manifest inspect ${image}`, (error, stdout, stderr) => { const timestamp = new Date().toISOString(); if (error) { const errorMsg = stderr.trim(); console.error(`[${timestamp}] 检查镜像 ${image} 失败:`, errorMsg); // 检查是否是 rate limit 错误 if (errorMsg.includes('toomanyrequests') || errorMsg.includes('rate limit')) { if (account) { accountMarkedTime.set(account.user, Date.now()); console.log(`账号 ${account.user} 触发 rate limit,已标记,${MARK_DURATION_SECONDS} 秒后移除标记`); currentAccountIndex = (currentAccountIndex + 1) % accounts.length; loggedInAccount = null; // 重置登录状态 } else { // 没有配置账号,标记匿名用户 anonymousMarkedTime = Date.now(); console.log(`匿名用户触发 rate limit,已标记,${MARK_DURATION_SECONDS} 秒后移除标记`); } } } else { console.log(`[${timestamp}] 检查镜像 ${image} 成功${account ? ` (账号: ${account.user})` : ' (匿名)'}`); } resolve(); }); }); // 更新索引,循环到下一个镜像 currentImageIndex = (currentImageIndex + 1) % images.length; } // 后台定时任务 async function startBackgroundTask() { // 立即运行一次 await runManifestInspect(); // 使用递归 setTimeout 代替 setInterval,支持动态等待 function scheduleNext() { setTimeout(async () => { await runManifestInspect(); scheduleNext(); }, INTERVAL_SECONDS * 1000); } scheduleNext(); } // HTTP 服务器 const server = http.createServer((req, res) => { // 隐藏镜像名称:只显示前后一位字母 const hiddenImages = images.map(img => { if (img.length <= 2) return img; return img[0] + '...' + img[img.length - 1]; }); // 账号状态 const accountStatus = accounts.map(acc => ({ user: acc.user, marked: accountMarkedTime.has(acc.user) })); // 匿名用户标记状态 let anonymousMarked = false; let anonymousMarkedRemaining = 0; if (anonymousMarkedTime) { const now = Date.now(); const elapsed = now - anonymousMarkedTime; const markDuration = MARK_DURATION_SECONDS * 1000; if (elapsed >= markDuration) { anonymousMarkedTime = null; // 标记已过期 } else { anonymousMarked = true; anonymousMarkedRemaining = Math.round((markDuration - elapsed) / 1000); } } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'running', publicIP: publicIP, imageCount: images.length, images: hiddenImages, intervalSeconds: INTERVAL_SECONDS, accounts: accountStatus, currentAccount: accounts[currentAccountIndex]?.user || null, anonymousMarked: anonymousMarked, anonymousMarkedRemaining: anonymousMarkedRemaining, lastReportTime: lastReportTime, lastReportSuccess: lastReportSuccess })); }); // 启动 const PORT = 7860; server.listen(PORT, () => { console.log(`HTTP 服务器运行在端口 ${PORT}`); startIPUpdate(); startReportTask(); startBackgroundTask(); });