Repository: cmliu/CF-Workers-docker.io Branch: main Commit: 059ff30b89e5 Files: 3 Total size: 27.4 KB Directory structure: gitextract_w56m1j5c/ ├── .github/ │ └── workflows/ │ └── sync.yml ├── README.md └── _worker.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/sync.yml ================================================ name: Upstream Sync permissions: contents: write on: schedule: - cron: "0 0 * * *" # every day workflow_dispatch: jobs: sync_latest_from_upstream: name: Sync latest commits from upstream repo runs-on: ubuntu-latest if: ${{ github.event.repository.fork }} steps: # Step 1: run a standard checkout action - name: Checkout target repo uses: actions/checkout@v3 # Step 2: run the sync action - name: Sync upstream changes id: sync uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 with: upstream_sync_repo: cmliu/CF-Workers-docker.io upstream_sync_branch: main target_sync_branch: main target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set # Set test_mode true to run tests instead of the true action!! test_mode: false - name: Sync check if: failure() run: | echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看项目README.md " echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the project README.md for instructions. " exit 1 ================================================ FILE: README.md ================================================ [**第三方 DockerHub 镜像服务列表**](https://github.com/cmliu/CF-Workers-docker.io?tab=readme-ov-file#%EF%B8%8F-%E7%AC%AC%E4%B8%89%E6%96%B9-dockerhub-%E9%95%9C%E5%83%8F%E6%9C%8D%E5%8A%A1) ![CF-Workers-docker.io](./img.png) # 🐳 CF-Workers-docker.io:Docker仓库镜像代理工具 这个项目是一个基于 Cloudflare Workers 的 Docker 镜像代理工具。它能够中转对 Docker 官方镜像仓库的请求,解决一些访问限制和加速访问的问题。 > [!CAUTION] > **docker.fxxk.dedyn.io 已被GFW污染,需自行部署使用。** > [!WARNING] > 根据 [Cloudflare 协议](https://www.cloudflare.com/zh-cn/terms/) 中,2.2.1 第 (j) use the Services to provide a virtual private network or other similar proxy services. > > 使用本服务可能存在被 Cloudflare 封号的潜在风险,请自行斟酌使用风险。 > > 如果你选择了“根据主机名选择对应的上游地址”方式部署,你可能会: > > 被 Netcraft 扫描到,收到警告邮件 > > 被 Netcraft 同步到 Google Safe Browsing 标记为钓鱼网站 > > 被 Netcraft 投诉到 Cloudflare 标记为钓鱼网站, 无法正常 pull 镜像 > > 收到律师函 ## 🚀 部署方式 - **Workers** 部署:复制 [_worker.js](https://github.com/cmliu/CF-Workers-docker.io/blob/main/_worker.js) 代码,`保存并部署`即可 - **Pages** 部署:`Fork` 后 `连接GitHub` 一键部署即可 ## ⚙️ 如何使用? [视频教程](https://www.youtube.com/watch?v=l2jwq9CagNQ) 例如您的Workers项目域名为:`docker.fxxk.dedyn.io`; ### 1.官方镜像路径前面加域名 ```shell docker pull docker.fxxk.dedyn.io/stilleshan/frpc:latest ``` ```shell docker pull docker.fxxk.dedyn.io/library/nginx:stable-alpine3.19-perl ``` ### 2.一键设置镜像加速 修改文件 `/etc/docker/daemon.json`(如果不存在则创建) ```shell sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://docker.fxxk.dedyn.io"] # 请替换为您自己的Worker自定义域名 } EOF sudo systemctl daemon-reload sudo systemctl restart docker ``` ### 3. 配置常见仓库的镜像加速 #### 3.1 配置 `Containerd` 较简单,它支持任意 `registry` 的 `mirror`,只需要修改配置文件 `/etc/containerd/config.toml`,添加如下的配置: ```yaml [plugins."io.containerd.grpc.v1.cri".registry] [plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://xxxx.xx.com"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.k8s.io"] endpoint = ["https://xxxx.xx.com"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."k8s.gcr.io"] endpoint = ["https://xxxx.xx.com"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."gcr.io"] endpoint = ["https://xxxx.xx.com"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."ghcr.io"] endpoint = ["https://xxxx.xx.com"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."quay.io"] endpoint = ["https://xxxx.xx.com"] ``` `Podman` 同样支持任意 `registry` 的 `mirror`,修改配置文件 `/etc/containers/registries.conf`,添加配置: ```yaml unqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io'] [[registry]] prefix = "docker.io" insecure = true location = "registry-1.docker.io" [[registry.mirror]] location = "xxxx.xx.com" [[registry]] prefix = "registry.k8s.io" insecure = true location = "registry.k8s.io" [[registry.mirror]] location = "xxxx.xx.com" [[registry]] prefix = "k8s.gcr.io" insecure = true location = "k8s.gcr.io" [[registry.mirror]] location = "xxxx.xx.com" [[registry]] prefix = "gcr.io" insecure = true location = "gcr.io" [[registry.mirror]] location = "xxxx.xx.com" [[registry]] prefix = "ghcr.io" insecure = true location = "ghcr.io" [[registry.mirror]] location = "xxxx.xx.com" [[registry]] prefix = "quay.io" insecure = true location = "quay.io" [[registry.mirror]] location = "xxxx.xx.com" ``` #### 3.3 使用 对于以上配置,k8s 在使用的时候,就可以直接 `pull` 外部无法 pull 的镜像了。 ```shell # 手动可以直接pull配置了mirror的仓库 crictl pull registry.k8s.io/kube-proxy:v1.28.4 docker pull nginx:1.21 ``` ## 🔧 变量说明 | 变量名 | 示例 | 必填 | 备注 | |--|--|--|--| | URL302 | `https://t.me/CMLiussss` |❌| 主页302跳转 | | URL | `https://www.baidu.com/` |❌| 主页伪装(设为`nginx`则伪装为nginx默认页面) | | UA | `netcraft` |❌| 支持多元素, 元素之间使用空格或换行作间隔 | # 🛠️ 第三方 DockerHub 镜像服务 **注意:** - 以下内容仅做镜像服务的整理与搜集,未做任何安全性检测和验证。 - 使用前请自行斟酌,并根据实际需求进行必要的安全审查。 - 本列表中的任何服务都不做任何形式的安全承诺或保证。 | DockerHub 镜像仓库 | 镜像加地址 | | ------------------ | ----------- | | [bestcfipas 镜像服务](https://t.me/bestcfipas/4018) | `https://docker.registry.cyou` | | | `https://docker-cf.registry.cyou` | | | `https://registry.lfree.org` | | [zero_free 镜像服务](https://t.me/zero_free/80) | `https://docker.jsdelivr.fyi` | | | `https://docker.aeko.cn` | | [mingyu 镜像服务](https://github.com/ymyuuu/HubP) | `https://hubp.de` | | [Docker 镜像加速站](https://docker.1panel.live) | `https://docker.1panel.live` | | [Hub Proxy](https://hub.rat.dev) | `https://hub.rat.dev` | | [DaoCloud 镜像站](https://github.com/DaoCloud/public-image-mirror) | `https://docker.m.daocloud.io` | # 🙏 鸣谢 ### 💖 赞助支持 - 提供云服务器 - [![digitalvirt.com](https://digitalvirt.com/templates/BlueWhite/img/logo-dark.svg)](https://url.cmliussss.com/dv) ### 🛠 开源代码引用 - [muzihuaner](https://github.com/muzihuaner) - [V2ex网友](https://global.v2ex.com/t/1007922) - [ciiiii](https://github.com/ciiiii/cloudflare-docker-proxy) - [ChatGPT](https://chatgpt.com/) - [白嫖哥](https://t.me/bestcfipas/1900) - [zero_free频道](https://t.me/zero_free/80) - [dongyubin](https://github.com/cmliu/CF-Workers-docker.io/issues/8) - [kiko923](https://github.com/cmliu/CF-Workers-docker.io/issues/5) ================================================ FILE: _worker.js ================================================ // _worker.js // Docker镜像仓库主机地址 let hub_host = 'registry-1.docker.io'; // Docker认证服务器地址 const auth_url = 'https://auth.docker.io'; let 屏蔽爬虫UA = ['netcraft']; // 根据主机名选择对应的上游地址 function routeByHosts(host) { // 定义路由表 const routes = { // 生产环境 "quay": "quay.io", "gcr": "gcr.io", "k8s-gcr": "k8s.gcr.io", "k8s": "registry.k8s.io", "ghcr": "ghcr.io", "cloudsmith": "docker.cloudsmith.io", "nvcr": "nvcr.io", // 测试环境 "test": "registry-1.docker.io", }; if (host in routes) return [routes[host], false]; else return [hub_host, true]; } /** @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} headers 响应头 */ function makeRes(body, status = 200, headers = {}) { headers['access-control-allow-origin'] = '*' // 允许所有来源 return new Response(body, { status, headers }) // 返回新构造的响应 } /** * 构造新的URL对象 * @param {string} urlStr URL字符串 * @param {string} base URL base */ function newUrl(urlStr, base) { try { console.log(`Constructing new URL object with path ${urlStr} and base ${base}`); return new URL(urlStr, base); // 尝试构造新的URL对象 } catch (err) { console.error(err); return null // 构造失败返回null } } async function nginx() { const text = ` Welcome to nginx!

Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

` return text; } async function searchInterface() { const html = ` Docker Hub 镜像搜索

Docker Hub 镜像搜索

快速查找、下载和部署 Docker 容器镜像

基于 Cloudflare Workers / Pages 构建,利用全球边缘网络实现毫秒级响应。

`; return html; } export default { async fetch(request, env, ctx) { const getReqHeader = (key) => request.headers.get(key); // 获取请求头 let url = new URL(request.url); // 解析请求URL const userAgentHeader = request.headers.get('User-Agent'); const userAgent = userAgentHeader ? userAgentHeader.toLowerCase() : "null"; if (env.UA) 屏蔽爬虫UA = 屏蔽爬虫UA.concat(await ADD(env.UA)); const workers_url = `https://${url.hostname}`; // 获取请求参数中的 ns const ns = url.searchParams.get('ns'); const hostname = url.searchParams.get('hubhost') || url.hostname; const hostTop = hostname.split('.')[0]; // 获取主机名的第一部分 let checkHost; // 在这里定义 checkHost 变量 // 如果存在 ns 参数,优先使用它来确定 hub_host if (ns) { if (ns === 'docker.io') { hub_host = 'registry-1.docker.io'; // 设置上游地址为 registry-1.docker.io } else { hub_host = ns; // 直接使用 ns 作为 hub_host } } else { checkHost = routeByHosts(hostTop); hub_host = checkHost[0]; // 获取上游地址 } const fakePage = checkHost ? checkHost[1] : false; // 确保 fakePage 不为 undefined console.log(`域名头部: ${hostTop} 反代地址: ${hub_host} searchInterface: ${fakePage}`); // 更改请求的主机名 url.hostname = hub_host; const hubParams = ['/v1/search', '/v1/repositories']; if (屏蔽爬虫UA.some(fxxk => userAgent.includes(fxxk)) && 屏蔽爬虫UA.length > 0) { // 首页改成一个nginx伪装页 return new Response(await nginx(), { headers: { 'Content-Type': 'text/html; charset=UTF-8', }, }); } else if ((userAgent && userAgent.includes('mozilla')) || hubParams.some(param => url.pathname.includes(param))) { if (url.pathname == '/') { if (env.URL302) { return Response.redirect(env.URL302, 302); } else if (env.URL) { if (env.URL.toLowerCase() == 'nginx') { //首页改成一个nginx伪装页 return new Response(await nginx(), { headers: { 'Content-Type': 'text/html; charset=UTF-8', }, }); } else return fetch(new Request(env.URL, request)); } else { if (fakePage) return new Response(await searchInterface(), { headers: { 'Content-Type': 'text/html; charset=UTF-8', }, }); } } else { // 新增逻辑:/v1/ 路径特殊处理 if (url.pathname.startsWith('/v1/')) { url.hostname = 'index.docker.io'; } else if (fakePage) { url.hostname = 'hub.docker.com'; } if (url.searchParams.get('q')?.includes('library/') && url.searchParams.get('q') != 'library/') { const search = url.searchParams.get('q'); url.searchParams.set('q', search.replace('library/', '')); } const newRequest = new Request(url, request); return fetch(newRequest); } } // 修改包含 %2F 和 %3A 的请求 if (!/%2F/.test(url.search) && /%3A/.test(url.toString())) { let modifiedUrl = url.toString().replace(/%3A(?=.*?&)/, '%3Alibrary%2F'); url = new URL(modifiedUrl); console.log(`handle_url: ${url}`); } // 处理token请求 if (url.pathname.includes('/token')) { let token_parameter = { headers: { 'Host': 'auth.docker.io', 'User-Agent': getReqHeader("User-Agent"), 'Accept': getReqHeader("Accept"), 'Accept-Language': getReqHeader("Accept-Language"), 'Accept-Encoding': getReqHeader("Accept-Encoding"), 'Connection': 'keep-alive', 'Cache-Control': 'max-age=0' } }; let token_url = auth_url + url.pathname + url.search; return fetch(new Request(token_url, request), token_parameter); } // 修改 /v2/ 请求路径 if (hub_host == 'registry-1.docker.io' && /^\/v2\/[^/]+\/[^/]+\/[^/]+$/.test(url.pathname) && !/^\/v2\/library/.test(url.pathname)) { //url.pathname = url.pathname.replace(/\/v2\//, '/v2/library/'); url.pathname = '/v2/library/' + url.pathname.split('/v2/')[1]; console.log(`modified_url: ${url.pathname}`); } // 新增:/v2/、/manifests/、/blobs/、/tags/ 先获取token再请求 if ( url.pathname.startsWith('/v2/') && ( url.pathname.includes('/manifests/') || url.pathname.includes('/blobs/') || url.pathname.includes('/tags/') || url.pathname.endsWith('/tags/list') ) ) { // 提取镜像名 let repo = ''; const v2Match = url.pathname.match(/^\/v2\/(.+?)(?:\/(manifests|blobs|tags)\/)/); if (v2Match) { repo = v2Match[1]; } if (repo) { const tokenUrl = `${auth_url}/token?service=registry.docker.io&scope=repository:${repo}:pull`; const tokenRes = await fetch(tokenUrl, { headers: { 'User-Agent': getReqHeader("User-Agent"), 'Accept': getReqHeader("Accept"), 'Accept-Language': getReqHeader("Accept-Language"), 'Accept-Encoding': getReqHeader("Accept-Encoding"), 'Connection': 'keep-alive', 'Cache-Control': 'max-age=0' } }); const tokenData = await tokenRes.json(); const token = tokenData.token; let parameter = { headers: { 'Host': hub_host, 'User-Agent': getReqHeader("User-Agent"), 'Accept': getReqHeader("Accept"), 'Accept-Language': getReqHeader("Accept-Language"), 'Accept-Encoding': getReqHeader("Accept-Encoding"), 'Connection': 'keep-alive', 'Cache-Control': 'max-age=0', 'Authorization': `Bearer ${token}` }, cacheTtl: 3600 }; if (request.headers.has("X-Amz-Content-Sha256")) { parameter.headers['X-Amz-Content-Sha256'] = getReqHeader("X-Amz-Content-Sha256"); } let original_response = await fetch(new Request(url, request), parameter); let original_response_clone = original_response.clone(); let original_text = original_response_clone.body; let response_headers = original_response.headers; let new_response_headers = new Headers(response_headers); let status = original_response.status; if (new_response_headers.get("Www-Authenticate")) { let auth = new_response_headers.get("Www-Authenticate"); let re = new RegExp(auth_url, 'g'); new_response_headers.set("Www-Authenticate", response_headers.get("Www-Authenticate").replace(re, workers_url)); } if (new_response_headers.get("Location")) { const location = new_response_headers.get("Location"); console.info(`Found redirection location, redirecting to ${location}`); return httpHandler(request, location, hub_host); } let response = new Response(original_text, { status, headers: new_response_headers }); return response; } } // 构造请求参数 let parameter = { headers: { 'Host': hub_host, 'User-Agent': getReqHeader("User-Agent"), 'Accept': getReqHeader("Accept"), 'Accept-Language': getReqHeader("Accept-Language"), 'Accept-Encoding': getReqHeader("Accept-Encoding"), 'Connection': 'keep-alive', 'Cache-Control': 'max-age=0' }, cacheTtl: 3600 // 缓存时间 }; // 添加Authorization头 if (request.headers.has("Authorization")) { parameter.headers.Authorization = getReqHeader("Authorization"); } // 添加可能存在字段X-Amz-Content-Sha256 if (request.headers.has("X-Amz-Content-Sha256")) { parameter.headers['X-Amz-Content-Sha256'] = getReqHeader("X-Amz-Content-Sha256"); } // 发起请求并处理响应 let original_response = await fetch(new Request(url, request), parameter); let original_response_clone = original_response.clone(); let original_text = original_response_clone.body; let response_headers = original_response.headers; let new_response_headers = new Headers(response_headers); let status = original_response.status; // 修改 Www-Authenticate 头 if (new_response_headers.get("Www-Authenticate")) { let auth = new_response_headers.get("Www-Authenticate"); let re = new RegExp(auth_url, 'g'); new_response_headers.set("Www-Authenticate", response_headers.get("Www-Authenticate").replace(re, workers_url)); } // 处理重定向 if (new_response_headers.get("Location")) { const location = new_response_headers.get("Location"); console.info(`Found redirection location, redirecting to ${location}`); return httpHandler(request, location, hub_host); } // 返回修改后的响应 let response = new Response(original_text, { status, headers: new_response_headers }); return response; } }; /** * 处理HTTP请求 * @param {Request} req 请求对象 * @param {string} pathname 请求路径 * @param {string} baseHost 基地址 */ function httpHandler(req, pathname, baseHost) { const reqHdrRaw = req.headers; // 处理预检请求 if (req.method === 'OPTIONS' && reqHdrRaw.has('access-control-request-headers') ) { return new Response(null, PREFLIGHT_INIT); } let rawLen = ''; const reqHdrNew = new Headers(reqHdrRaw); reqHdrNew.delete("Authorization"); // 修复s3错误 const refer = reqHdrNew.get('referer'); let urlStr = pathname; const urlObj = newUrl(urlStr, 'https://' + baseHost); /** @type {RequestInit} */ const reqInit = { method: req.method, headers: reqHdrNew, redirect: 'follow', body: req.body }; return proxy(urlObj, reqInit, rawLen); } /** * 代理请求 * @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 }); } async function ADD(envadd) { var addtext = envadd.replace(/[ |"'\r\n]+/g, ',').replace(/,+/g, ','); // 将空格、双引号、单引号和换行符替换为逗号 if (addtext.charAt(0) == ',') addtext = addtext.slice(1); if (addtext.charAt(addtext.length - 1) == ',') addtext = addtext.slice(0, addtext.length - 1); const add = addtext.split(','); return add; }