Upload 2 files
Browse files- Dockerfile +24 -3
- index.js +314 -0
Dockerfile
CHANGED
|
@@ -1,3 +1,24 @@
|
|
| 1 |
-
FROM
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM docker:latest
|
| 2 |
+
|
| 3 |
+
# 安装 Node.js 和 curl
|
| 4 |
+
RUN apk add --no-cache nodejs npm curl
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
COPY index.js .
|
| 8 |
+
|
| 9 |
+
# 默认环境变量
|
| 10 |
+
ENV TARGET_IMAGE="library/node, library/alpine"
|
| 11 |
+
# 格式: user1:pass1,user2:pass2
|
| 12 |
+
ENV DOCKER_ACCOUNTS=""
|
| 13 |
+
ENV REPORT_URL=""
|
| 14 |
+
# 间隔时间(秒)
|
| 15 |
+
ENV INTERVAL_SECONDS="60"
|
| 16 |
+
# 标记持续时间(秒),默认1小时
|
| 17 |
+
ENV MARK_DURATION_SECONDS="3600"
|
| 18 |
+
|
| 19 |
+
# 暴露端口
|
| 20 |
+
EXPOSE 7860
|
| 21 |
+
|
| 22 |
+
# 覆盖 docker 镜像的默认 ENTRYPOINT
|
| 23 |
+
ENTRYPOINT []
|
| 24 |
+
CMD ["node", "index.js"]
|
index.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const http = require('http');
|
| 2 |
+
const { exec } = require('child_process');
|
| 3 |
+
|
| 4 |
+
// 公网 IP
|
| 5 |
+
let publicIP = 'unknown';
|
| 6 |
+
|
| 7 |
+
// 上报状态
|
| 8 |
+
let lastReportTime = null;
|
| 9 |
+
let lastReportSuccess = false;
|
| 10 |
+
|
| 11 |
+
// 获取环境变量
|
| 12 |
+
const TARGET_IMAGE = process.env.TARGET_IMAGE || '';
|
| 13 |
+
const DOCKER_ACCOUNTS = process.env.DOCKER_ACCOUNTS || ''; // 格式: user1:pass1,user2:pass2
|
| 14 |
+
const REPORT_URL = process.env.REPORT_URL || '';
|
| 15 |
+
const INTERVAL_SECONDS = parseInt(process.env.INTERVAL_SECONDS) || 60; // 间隔时间(秒)
|
| 16 |
+
const MARK_DURATION_SECONDS = parseInt(process.env.MARK_DURATION_SECONDS) || 3600; // 标记持续时间(秒),默认1小时
|
| 17 |
+
|
| 18 |
+
// 解析 Docker 账号列表
|
| 19 |
+
const accounts = DOCKER_ACCOUNTS.split(',')
|
| 20 |
+
.map(acc => {
|
| 21 |
+
const [user, pass] = acc.trim().split(':');
|
| 22 |
+
return user && pass ? { user, pass } : null;
|
| 23 |
+
})
|
| 24 |
+
.filter(acc => acc);
|
| 25 |
+
|
| 26 |
+
// 账号状态管理
|
| 27 |
+
const accountMarkedTime = new Map(); // 记录账号被标记的时间
|
| 28 |
+
let currentAccountIndex = 0;
|
| 29 |
+
let loggedInAccount = null; // 当前已登录的账号
|
| 30 |
+
let anonymousMarkedTime = null; // 匿名用户被标记的时间
|
| 31 |
+
|
| 32 |
+
// 获取当前可用账号(跳过被标记的)
|
| 33 |
+
function getAvailableAccount() {
|
| 34 |
+
if (accounts.length === 0) return null;
|
| 35 |
+
|
| 36 |
+
const now = Date.now();
|
| 37 |
+
const markDuration = MARK_DURATION_SECONDS * 1000;
|
| 38 |
+
|
| 39 |
+
// 清理已过期的标记
|
| 40 |
+
for (const [user, markedTime] of accountMarkedTime) {
|
| 41 |
+
if (now - markedTime >= markDuration) {
|
| 42 |
+
accountMarkedTime.delete(user);
|
| 43 |
+
console.log(`账号 ${user} 标记已移除`);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// 查找可用账号
|
| 48 |
+
for (let i = 0; i < accounts.length; i++) {
|
| 49 |
+
const idx = (currentAccountIndex + i) % accounts.length;
|
| 50 |
+
const acc = accounts[idx];
|
| 51 |
+
if (!accountMarkedTime.has(acc.user)) {
|
| 52 |
+
currentAccountIndex = idx;
|
| 53 |
+
return acc;
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// 所有账号都被标记
|
| 58 |
+
return null;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// 获取离最近标记移除的时间(毫秒)
|
| 62 |
+
function getTimeUntilNextUnmark() {
|
| 63 |
+
if (accountMarkedTime.size === 0) return 0;
|
| 64 |
+
|
| 65 |
+
const now = Date.now();
|
| 66 |
+
const markDuration = MARK_DURATION_SECONDS * 1000;
|
| 67 |
+
let minWait = Infinity;
|
| 68 |
+
|
| 69 |
+
for (const markedTime of accountMarkedTime.values()) {
|
| 70 |
+
const wait = markDuration - (now - markedTime);
|
| 71 |
+
if (wait < minWait && wait > 0) minWait = wait;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
return minWait === Infinity ? 0 : minWait;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// 获取公网 IP
|
| 78 |
+
async function fetchPublicIP() {
|
| 79 |
+
return new Promise((resolve) => {
|
| 80 |
+
exec('curl -s --connect-timeout 5 https://api.ipify.org', (error, stdout, stderr) => {
|
| 81 |
+
if (error || !stdout.trim()) {
|
| 82 |
+
console.error('获取公网 IP 失败:', error?.message || 'empty response');
|
| 83 |
+
resolve('unknown');
|
| 84 |
+
} else {
|
| 85 |
+
resolve(stdout.trim());
|
| 86 |
+
}
|
| 87 |
+
});
|
| 88 |
+
});
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// 定期更新公网 IP
|
| 92 |
+
async function startIPUpdate() {
|
| 93 |
+
publicIP = await fetchPublicIP();
|
| 94 |
+
console.log(`公网 IP: ${publicIP}`);
|
| 95 |
+
|
| 96 |
+
// 每 5 分钟更新一次
|
| 97 |
+
setInterval(async () => {
|
| 98 |
+
publicIP = await fetchPublicIP();
|
| 99 |
+
}, 5 * 60 * 1000);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
// 上报 URL
|
| 103 |
+
async function reportToURL() {
|
| 104 |
+
if (!REPORT_URL) return;
|
| 105 |
+
|
| 106 |
+
return new Promise((resolve) => {
|
| 107 |
+
exec(`curl -s --connect-timeout 10 "${REPORT_URL}"`, (error, stdout, stderr) => {
|
| 108 |
+
lastReportTime = new Date().toISOString();
|
| 109 |
+
if (error) {
|
| 110 |
+
lastReportSuccess = false;
|
| 111 |
+
console.error(`[${lastReportTime}] 上报失败:`, error.message);
|
| 112 |
+
} else {
|
| 113 |
+
lastReportSuccess = true;
|
| 114 |
+
console.log(`[${lastReportTime}] 上报成功`);
|
| 115 |
+
}
|
| 116 |
+
resolve();
|
| 117 |
+
});
|
| 118 |
+
});
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// 定时上报(每 6 小时)
|
| 122 |
+
async function startReportTask() {
|
| 123 |
+
if (!REPORT_URL) {
|
| 124 |
+
console.log('REPORT_URL 未配置,跳过上报');
|
| 125 |
+
return;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// 立即执行一次
|
| 129 |
+
await reportToURL();
|
| 130 |
+
|
| 131 |
+
// 每 6 小时执行一次
|
| 132 |
+
setInterval(async () => {
|
| 133 |
+
await reportToURL();
|
| 134 |
+
}, 6 * 60 * 60 * 1000);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// 解析镜像名称列表
|
| 138 |
+
const images = TARGET_IMAGE.split(',').map(img => img.trim()).filter(img => img);
|
| 139 |
+
|
| 140 |
+
// 当前镜像索引(循环使用)
|
| 141 |
+
let currentImageIndex = 0;
|
| 142 |
+
|
| 143 |
+
console.log(`配置: ${images.length} 个镜像, ${accounts.length} 个账号, 间隔 ${INTERVAL_SECONDS} 秒`);
|
| 144 |
+
|
| 145 |
+
// Docker 登录
|
| 146 |
+
async function dockerLogin(account) {
|
| 147 |
+
return new Promise((resolve) => {
|
| 148 |
+
exec(`echo "${account.pass}" | docker login -u "${account.user}" --password-stdin`, (error, stdout, stderr) => {
|
| 149 |
+
if (error) {
|
| 150 |
+
console.error(`账号 ${account.user} 登录失败:`, stderr.trim());
|
| 151 |
+
resolve(false);
|
| 152 |
+
} else {
|
| 153 |
+
console.log(`账号 ${account.user} 登录成功`);
|
| 154 |
+
resolve(true);
|
| 155 |
+
}
|
| 156 |
+
});
|
| 157 |
+
});
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// 运行一次 docker manifest inspect(只检查一个镜像)
|
| 161 |
+
async function runManifestInspect() {
|
| 162 |
+
if (images.length === 0) {
|
| 163 |
+
console.log('没有配置 TARGET_IMAGE');
|
| 164 |
+
return;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
// 获取当前要检查的镜像
|
| 168 |
+
const image = images[currentImageIndex];
|
| 169 |
+
|
| 170 |
+
// 获取可用账号
|
| 171 |
+
const account = getAvailableAccount();
|
| 172 |
+
|
| 173 |
+
if (accounts.length > 0 && !account) {
|
| 174 |
+
// 所有账号都被标记,等待
|
| 175 |
+
const waitTime = getTimeUntilNextUnmark();
|
| 176 |
+
console.log(`所有账号都被标记,等待 ${Math.round(waitTime / 1000)} 秒后重试`);
|
| 177 |
+
setTimeout(() => runManifestInspect(), waitTime);
|
| 178 |
+
return;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// 检查匿名用户是否被标记
|
| 182 |
+
if (accounts.length === 0 && anonymousMarkedTime) {
|
| 183 |
+
const now = Date.now();
|
| 184 |
+
const markDuration = MARK_DURATION_SECONDS * 1000;
|
| 185 |
+
if (now - anonymousMarkedTime < markDuration) {
|
| 186 |
+
// 匿名用户仍在标记期内,等待
|
| 187 |
+
const waitTime = markDuration - (now - anonymousMarkedTime);
|
| 188 |
+
console.log(`匿名用户已被标记,等待 ${Math.round(waitTime / 1000)} 秒后重试`);
|
| 189 |
+
setTimeout(() => runManifestInspect(), waitTime);
|
| 190 |
+
return;
|
| 191 |
+
} else {
|
| 192 |
+
// 标记已过期,清除
|
| 193 |
+
anonymousMarkedTime = null;
|
| 194 |
+
console.log('匿名用户标记已移除');
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// 如果有账号,检查是否需要登录
|
| 199 |
+
if (account) {
|
| 200 |
+
// 如果当前账号已登录,跳过登录
|
| 201 |
+
if (loggedInAccount?.user !== account.user) {
|
| 202 |
+
const loginSuccess = await dockerLogin(account);
|
| 203 |
+
if (!loginSuccess) {
|
| 204 |
+
// 登录失败,标记账号,尝试下一个
|
| 205 |
+
accountMarkedTime.set(account.user, Date.now());
|
| 206 |
+
console.log(`账号 ${account.user} 登录失败,已标记,${MARK_DURATION_SECONDS} 秒后移除标记`);
|
| 207 |
+
loggedInAccount = null; // 重置登录状态
|
| 208 |
+
currentAccountIndex = (currentAccountIndex + 1) % accounts.length;
|
| 209 |
+
return;
|
| 210 |
+
}
|
| 211 |
+
loggedInAccount = account;
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// 执行 manifest inspect
|
| 216 |
+
await new Promise((resolve) => {
|
| 217 |
+
exec(`docker manifest inspect ${image}`, (error, stdout, stderr) => {
|
| 218 |
+
const timestamp = new Date().toISOString();
|
| 219 |
+
if (error) {
|
| 220 |
+
const errorMsg = stderr.trim();
|
| 221 |
+
console.error(`[${timestamp}] 检查镜像 ${image} 失败:`, errorMsg);
|
| 222 |
+
|
| 223 |
+
// 检查是否是 rate limit 错误
|
| 224 |
+
if (errorMsg.includes('toomanyrequests') || errorMsg.includes('rate limit')) {
|
| 225 |
+
if (account) {
|
| 226 |
+
accountMarkedTime.set(account.user, Date.now());
|
| 227 |
+
console.log(`账号 ${account.user} 触发 rate limit,已标记,${MARK_DURATION_SECONDS} 秒后移除标记`);
|
| 228 |
+
currentAccountIndex = (currentAccountIndex + 1) % accounts.length;
|
| 229 |
+
loggedInAccount = null; // 重置登录状态
|
| 230 |
+
} else {
|
| 231 |
+
// 没有配置账号,标记匿名用户
|
| 232 |
+
anonymousMarkedTime = Date.now();
|
| 233 |
+
console.log(`匿名用户触发 rate limit,已标记,${MARK_DURATION_SECONDS} 秒后移除标记`);
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
} else {
|
| 237 |
+
console.log(`[${timestamp}] 检查镜像 ${image} 成功${account ? ` (账号: ${account.user})` : ' (匿名)'}`);
|
| 238 |
+
}
|
| 239 |
+
resolve();
|
| 240 |
+
});
|
| 241 |
+
});
|
| 242 |
+
|
| 243 |
+
// 更新索引,循环到下一个镜像
|
| 244 |
+
currentImageIndex = (currentImageIndex + 1) % images.length;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
// 后台定时任务
|
| 248 |
+
async function startBackgroundTask() {
|
| 249 |
+
// 立即运行一次
|
| 250 |
+
await runManifestInspect();
|
| 251 |
+
|
| 252 |
+
// 使用递归 setTimeout 代替 setInterval,支持动态等待
|
| 253 |
+
function scheduleNext() {
|
| 254 |
+
setTimeout(async () => {
|
| 255 |
+
await runManifestInspect();
|
| 256 |
+
scheduleNext();
|
| 257 |
+
}, INTERVAL_SECONDS * 1000);
|
| 258 |
+
}
|
| 259 |
+
scheduleNext();
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
// HTTP 服务器
|
| 263 |
+
const server = http.createServer((req, res) => {
|
| 264 |
+
// 隐藏镜像名称:只显示前后一位字母
|
| 265 |
+
const hiddenImages = images.map(img => {
|
| 266 |
+
if (img.length <= 2) return img;
|
| 267 |
+
return img[0] + '...' + img[img.length - 1];
|
| 268 |
+
});
|
| 269 |
+
|
| 270 |
+
// 账号状态
|
| 271 |
+
const accountStatus = accounts.map(acc => ({
|
| 272 |
+
user: acc.user,
|
| 273 |
+
marked: accountMarkedTime.has(acc.user)
|
| 274 |
+
}));
|
| 275 |
+
|
| 276 |
+
// 匿名用户标记状态
|
| 277 |
+
let anonymousMarked = false;
|
| 278 |
+
let anonymousMarkedRemaining = 0;
|
| 279 |
+
if (anonymousMarkedTime) {
|
| 280 |
+
const now = Date.now();
|
| 281 |
+
const elapsed = now - anonymousMarkedTime;
|
| 282 |
+
const markDuration = MARK_DURATION_SECONDS * 1000;
|
| 283 |
+
if (elapsed >= markDuration) {
|
| 284 |
+
anonymousMarkedTime = null; // 标记已过期
|
| 285 |
+
} else {
|
| 286 |
+
anonymousMarked = true;
|
| 287 |
+
anonymousMarkedRemaining = Math.round((markDuration - elapsed) / 1000);
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 292 |
+
res.end(JSON.stringify({
|
| 293 |
+
status: 'running',
|
| 294 |
+
publicIP: publicIP,
|
| 295 |
+
imageCount: images.length,
|
| 296 |
+
images: hiddenImages,
|
| 297 |
+
intervalSeconds: INTERVAL_SECONDS,
|
| 298 |
+
accounts: accountStatus,
|
| 299 |
+
currentAccount: accounts[currentAccountIndex]?.user || null,
|
| 300 |
+
anonymousMarked: anonymousMarked,
|
| 301 |
+
anonymousMarkedRemaining: anonymousMarkedRemaining,
|
| 302 |
+
lastReportTime: lastReportTime,
|
| 303 |
+
lastReportSuccess: lastReportSuccess
|
| 304 |
+
}));
|
| 305 |
+
});
|
| 306 |
+
|
| 307 |
+
// 启动
|
| 308 |
+
const PORT = 7860;
|
| 309 |
+
server.listen(PORT, () => {
|
| 310 |
+
console.log(`HTTP 服务器运行在端口 ${PORT}`);
|
| 311 |
+
startIPUpdate();
|
| 312 |
+
startReportTask();
|
| 313 |
+
startBackgroundTask();
|
| 314 |
+
});
|