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();
});