File size: 9,810 Bytes
f78edd7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 | 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();
});
|