159 lines
4.6 KiB
JavaScript
159 lines
4.6 KiB
JavaScript
const dockerHub = "https://registry-1.docker.io"; // Docker Hub 的 URL
|
||
|
||
// 定义路由表
|
||
const routes = {
|
||
// 生产环境
|
||
"quay": "https://quay.io",
|
||
"gcr": "https://gcr.io",
|
||
"k8s-gcr": "https://k8s.gcr.io",
|
||
"k8s": "https://registry.k8s.io",
|
||
"ghcr": "https://ghcr.io",
|
||
"cloudsmith": "https://docker.cloudsmith.io",
|
||
|
||
// 测试环境
|
||
"test": dockerHub,
|
||
};
|
||
|
||
// 根据主机名选择对应的上游地址
|
||
function routeByHosts(host) {
|
||
if (host in routes) {
|
||
return routes[host];
|
||
}
|
||
|
||
if (MODE == "debug") {
|
||
return TARGET_UPSTREAM; // 调试模式返回目标上游
|
||
}
|
||
|
||
return ""; // 如果没有匹配到,返回空字符串
|
||
}
|
||
|
||
export default {
|
||
// 处理请求
|
||
async fetch(request, env, ctx) {
|
||
const url = new URL(request.url); // 解析请求 URL
|
||
const hostName = url.hostname.split('.')[0]; // 获取主机名的第一部分
|
||
console.log(hostName);
|
||
const upstream = routeByHosts(hostName); // 获取上游地址
|
||
if (upstream === "") {
|
||
// 如果没有匹配到上游地址,返回 404
|
||
return new Response(
|
||
JSON.stringify({
|
||
routes: routes,
|
||
}),{
|
||
status: 404,
|
||
});
|
||
}
|
||
const isDockerHub = upstream == dockerHub; // 检查是否是 Docker Hub
|
||
const authorization = request.headers.get("Authorization"); // 获取请求头中的授权信息
|
||
if (url.pathname == "/v2/") {
|
||
const newUrl = new URL(upstream + "/v2/");
|
||
const headers = new Headers();
|
||
if (authorization) {
|
||
headers.set("Authorization", authorization); // 如果有授权信息,设置授权头
|
||
}
|
||
// 检查是否需要认证
|
||
const resp = await fetch(newUrl.toString(), {
|
||
method: "GET",
|
||
headers: headers,
|
||
redirect: "follow",
|
||
});
|
||
if (resp.status === 401) {
|
||
if (MODE == "debug") {
|
||
headers.set(
|
||
"Www-Authenticate",
|
||
`Bearer realm="http://${url.host}/v2/auth",service="cloudflare-docker-proxy"`
|
||
);
|
||
} else {
|
||
headers.set(
|
||
"Www-Authenticate",
|
||
`Bearer realm="https://${url.hostname}/v2/auth",service="cloudflare-docker-proxy"`
|
||
);
|
||
}
|
||
return new Response(JSON.stringify({ message: "UNAUTHORIZED" }), {
|
||
status: 401,
|
||
headers: headers,
|
||
});
|
||
} else {
|
||
return resp; // 返回上游响应
|
||
}
|
||
}
|
||
// 获取 token
|
||
if (url.pathname == "/v2/auth") {
|
||
const newUrl = new URL(upstream + "/v2/");
|
||
const resp = await fetch(newUrl.toString(), {
|
||
method: "GET",
|
||
redirect: "follow",
|
||
});
|
||
if (resp.status !== 401) {
|
||
return resp;
|
||
}
|
||
const authenticateStr = resp.headers.get("WWW-Authenticate");
|
||
if (authenticateStr === null) {
|
||
return resp;
|
||
}
|
||
const wwwAuthenticate = parseAuthenticate(authenticateStr);
|
||
let scope = url.searchParams.get("scope");
|
||
// 自动补全 DockerHub 的 library 镜像
|
||
// 例如:repository:busybox:pull => repository:library/busybox:pull
|
||
if (scope && isDockerHub) {
|
||
let scopeParts = scope.split(":");
|
||
if (scopeParts.length == 3 && !scopeParts[1].includes("/")) {
|
||
scopeParts[1] = "library/" + scopeParts[1];
|
||
scope = scopeParts.join(":");
|
||
}
|
||
}
|
||
return await fetchToken(wwwAuthenticate, scope, authorization);
|
||
}
|
||
// 重定向 DockerHub 的 library 镜像
|
||
// 例如:/v2/busybox/manifests/latest => /v2/library/busybox/manifests/latest
|
||
if (isDockerHub) {
|
||
const pathParts = url.pathname.split("/");
|
||
if (pathParts.length == 5) {
|
||
pathParts.splice(2, 0, "library");
|
||
let redirectUrl = new URL(url);
|
||
redirectUrl.pathname = pathParts.join("/");
|
||
return Response.redirect(redirectUrl.toString(), 301);
|
||
}
|
||
}
|
||
// 转发请求
|
||
const newUrl = new URL(upstream + url.pathname);
|
||
const newReq = new Request(newUrl, {
|
||
method: request.method,
|
||
headers: request.headers,
|
||
redirect: "follow",
|
||
});
|
||
return await fetch(newReq);
|
||
}
|
||
};
|
||
|
||
// 解析 Www-Authenticate 头
|
||
function parseAuthenticate(authenticateStr) {
|
||
// 示例:Bearer realm="https://auth.ipv6.docker.com/token",service="registry.docker.io"
|
||
// 匹配 =" 后的字符串,直到 " 结束
|
||
const re = /(?<=\=")(?:\\.|[^"\\])*(?=")/g;
|
||
const matches = authenticateStr.match(re);
|
||
if (matches == null || matches.length < 2) {
|
||
throw new Error(`invalid Www-Authenticate Header: ${authenticateStr}`);
|
||
}
|
||
return {
|
||
realm: matches[0],
|
||
service: matches[1],
|
||
};
|
||
}
|
||
|
||
// 获取 token
|
||
async function fetchToken(wwwAuthenticate, scope, authorization) {
|
||
const url = new URL(wwwAuthenticate.realm);
|
||
if (wwwAuthenticate.service.length) {
|
||
url.searchParams.set("service", wwwAuthenticate.service);
|
||
}
|
||
if (scope) {
|
||
url.searchParams.set("scope", scope);
|
||
}
|
||
const headers = new Headers();
|
||
if (authorization) {
|
||
headers.set("Authorization", authorization);
|
||
}
|
||
return await fetch(url, { method: "GET", headers: headers });
|
||
}
|