//环境变量whitelist控制允许访问的Docker仓库,默认为空允许全部,格式为:"xxx/xxx",官方为library
//例: library/,mzzsfy/,abc/bcd 为允许访问官方和mzzsfy所有镜像,外加abc/bcd这个镜像
//环境变量 strictDomain=true 表示仅允许使用已知的域名访问
//环境变量 defaultIndex设置默认主页上游地址,如: https://hub.docker.com
// 在cf部署中,使用泛域名或多触发器可以让同一个脚本可以支持多个镜像仓库,
// 如: k8s.xxx.xxx为访问k8s镜像仓库,docker.xxx.xxx为访问docker主镜像仓库
const customizeRoutes = {
"quay": "quay.io",
"gcr": "gcr.io",
"k8s-gcr": "k8s.gcr.io",
"k8s": "registry.k8s.io",
"ghcr": "ghcr.io",
"cloudsmith": "docker.cloudsmith.io",
"docker": "registry-1.docker.io",
};
// 默认使用的镜像仓库
const defaultHost = 'docker'
/** @type {RequestInit} */
const PREFLIGHT_INIT = {
// 预检请求配置
headers: new Headers({
'access-control-allow-origin': '*', // 允许所有来源
'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', // 允许的HTTP方法
'access-control-max-age': '1728000', // 预检请求的缓存时间
}),
}
/**
* 构造响应
* @param {any} body 响应体
* @param {number} status 响应状态码
* @param {Object<string, string>} headers 响应头
*/
function makeRes(body, status = 200, headers = {}) {
headers['access-control-allow-origin'] = '*' // 允许所有来源
return new Response(body, {status, headers}) // 返回新构造的响应
}
/**
* 构造新的URL对象
* @param {string} urlStr URL字符串
*/
function newUrl(urlStr) {
try {
return new URL(urlStr) // 尝试构造新的URL对象
} catch (err) {
return null // 构造失败返回null
}
}
function token(request, url, env, ctx) {
if (env['whitelist'] && url.searchParams.get('scope')) {
let name = url.searchParams.get('scope').split(':')[1];
if (!name.includes('/')) {
name = 'library/' + name
}
if (!env['whitelist'].split(',').some(e => name.startsWith(e))) {
console.log("非白名单镜像,拒绝访问", name)
return new Response('access denied', {status: 404})
}
}
let paths = url.pathname.split('/');
if (paths[paths.length - 1].includes('.')) {
let oldUrl = url.href;
url.hostname = paths[paths.length - 1];
url.pathname = paths.slice(0, paths.length - 1).join('/')
console.log("修改host", oldUrl, url.href)
}
return fetch(url.href, {
redirect: 'follow',
headers: request.headers,
method: request.method,
body: request.body
})
}
export default {
async fetch(request, env, ctx) {
let url = new URL(request.url); // 解析请求URL
let hostname = url.hostname.split('.')[0];
let route = customizeRoutes[hostname];
if (!route && env['strictDomain'] === 'true') {
return new Response('access denied', {
status: 401, headers: {},
});
}
route = route || customizeRoutes[defaultHost]
const getReqHeader = (key) => request.headers.get(key); // 获取请求头
let workers_url = `https://${url.hostname}`;
const pathname = url.pathname;
//浏览器访问
if (pathname === '/' || getReqHeader('Origin') || getReqHeader('Referer')) {
if (env['defaultIndex']) {
let url1 = new URL(env['defaultIndex']);
if (url.pathname === "/" && url1.pathname !== '/') {
return new Response('', {
status: 302, headers: {
'location': url1.pathname,
}
})
}
url1.pathname = url.pathname
url1.search = url.search
return fetch(url1.href, {
redirect: 'follow',
headers: request.headers,
method: request.method,
body: request.body,
})
}
return new Response('ok', {
status: 200, headers: {},
});
}
if (pathname.includes('/token') && (url.searchParams.has('scope') || url.searchParams.has('service'))) {
return token(request, url, env, ctx)
}
if (env['whitelist']) {
let l = 0
if (pathname.endsWith('/manifests/latest')) {
l = 17
} else if (pathname.endsWith('/blobs/sha256:REDACTED')) {
l = 22
}
let name = pathname.substring(4).substring(0, pathname.length - 4 - l);
if (!name.includes('/')) {
name = 'library/' + name
}
if (!env['whitelist'].split(',').some(e => name.startsWith(e))) {
console.log("非白名单镜像,拒绝访问", name)
return new Response('access denied', {status: 403})
}
}
let body, body2;
if (request.body) {
[body, body2] = request.body.tee()
}
url.hostname = route;
// 发起请求并处理响应
let res = await fetch(url.href, {
method: request.method,
body: body,
headers: request.headers,
})
// 修改 Www-Authenticate 头
let auth = res.headers.get("Www-Authenticate");
if (auth) {
const newHeader = new Headers(res.headers)
let newAuth = auth.replace(/https?:\/\/([a-zA-Z0-9\-.]+)([\w/]+)/, workers_url + "$2/$1");
newHeader.set("Www-Authenticate", newAuth);
console.log("修改Authenticate", auth, "->", newAuth)
if (pathname === '/v2/' && url.searchParams.size === 0) {
let t = await res.text();
console.log("res", t)
return new Response(t, {
status: res.status, headers: newHeader
})
}
return new Response(res.body, {
status: res.status, headers: newHeader
})
}
// 处理重定向
let location = res.headers.get("Location");
if (location) {
console.log('重定向', location);
// 处理预检请求
if (request.method === 'OPTIONS' && request.headers.has('access-control-request-headers')) {
return new Response(null, PREFLIGHT_INIT)
}
const urlObj = newUrl(location)
if (!urlObj) {
let url1 = new URL(url.href)
if (location.startsWith('/')) {
url1.pathname = location
} else {
url1.pathname = url1.pathname + location
}
url = url1
}
return proxy(urlObj, {
method: request.method,
headers: request.headers,
body: body2,
redirect: 'follow',
}, '')
}
return res
}
};
/**
* 代理请求
* @param {URL} urlObj URL对象
* @param {RequestInit} reqInit 请求初始化对象
* @param {string} rawLen 原始长度
*/
async function proxy(urlObj, reqInit, rawLen) {
const res = await fetch(urlObj.href, reqInit)
const resHdrOld = res.headers
const resHdrNew = new Headers(resHdrOld)
// 验证长度
if (rawLen) {
const newLen = resHdrOld.get('content-length') || ''
const badLen = (rawLen !== newLen)
if (badLen) {
return makeRes(res.body, 400, {
'--error': `bad len: ${newLen}, except: ${rawLen}`, 'access-control-expose-headers': '--error',
})
}
}
const status = res.status
resHdrNew.set('access-control-expose-headers', '*')
resHdrNew.set('access-control-allow-origin', '*')
resHdrNew.set('Cache-Control', 'max-age=1500')
// 删除不必要的头
resHdrNew.delete('content-security-policy')
resHdrNew.delete('content-security-policy-report-only')
resHdrNew.delete('clear-site-data')
return new Response(res.body, {
status, headers: resHdrNew
})
}
https://github.com/mzzsfy/Dockerfile/blob/main/cf-worker/docker-image.js