[
  {
    "path": ".github/workflows/sync.yml",
    "content": "name: Upstream Sync\n\npermissions:\n  contents: write\n\non:\n  schedule:\n    - cron: \"0 0 * * *\" # every day\n  workflow_dispatch:\n\njobs:\n  sync_latest_from_upstream:\n    name: Sync latest commits from upstream repo\n    runs-on: ubuntu-latest\n    if: ${{ github.event.repository.fork }}\n\n    steps:\n      # Step 1: run a standard checkout action\n      - name: Checkout target repo\n        uses: actions/checkout@v3\n\n      # Step 2: run the sync action\n      - name: Sync upstream changes\n        id: sync\n        uses: aormsby/Fork-Sync-With-Upstream-action@v3.4\n        with:\n          upstream_sync_repo: cmliu/CF-Workers-docker.io\n          upstream_sync_branch: main\n          target_sync_branch: main\n          target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set\n\n          # Set test_mode true to run tests instead of the true action!!\n          test_mode: false\n\n      - name: Sync check\n        if: failure()\n        run: |\n          echo \"[Error] 由于上游仓库的 workflow 文件变更，导致 GitHub 自动暂停了本次自动更新，你需要手动 Sync Fork 一次，详细教程请查看项目README.md \"\n          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. \"\n          exit 1\n"
  },
  {
    "path": "README.md",
    "content": "[**第三方 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)\n\n![CF-Workers-docker.io](./img.png)\n\n# 🐳 CF-Workers-docker.io：Docker仓库镜像代理工具\n\n这个项目是一个基于 Cloudflare Workers 的 Docker 镜像代理工具。它能够中转对 Docker 官方镜像仓库的请求，解决一些访问限制和加速访问的问题。\n\n> [!CAUTION]\n> **docker.fxxk.dedyn.io 已被GFW污染，需自行部署使用。**\n\n> [!WARNING]\n> 根据 [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.\n>\n> 使用本服务可能存在被 Cloudflare 封号的潜在风险，请自行斟酌使用风险。\n>\n> 如果你选择了“根据主机名选择对应的上游地址”方式部署，你可能会:\n> \n> 被 Netcraft 扫描到，收到警告邮件\n>\n> 被 Netcraft 同步到 Google Safe Browsing 标记为钓鱼网站\n>\n> 被 Netcraft 投诉到 Cloudflare 标记为钓鱼网站, 无法正常 pull 镜像\n>\n> 收到律师函\n\n## 🚀 部署方式\n\n- **Workers** 部署：复制 [_worker.js](https://github.com/cmliu/CF-Workers-docker.io/blob/main/_worker.js) 代码，`保存并部署`即可\n- **Pages** 部署：`Fork` 后 `连接GitHub` 一键部署即可\n\n## ⚙️ 如何使用？ [视频教程](https://www.youtube.com/watch?v=l2jwq9CagNQ)\n\n例如您的Workers项目域名为：`docker.fxxk.dedyn.io`；\n\n### 1.官方镜像路径前面加域名\n\n```shell\ndocker pull docker.fxxk.dedyn.io/stilleshan/frpc:latest\n```\n\n```shell\ndocker pull docker.fxxk.dedyn.io/library/nginx:stable-alpine3.19-perl\n```\n\n### 2.一键设置镜像加速\n\n修改文件 `/etc/docker/daemon.json`（如果不存在则创建）\n\n```shell\nsudo mkdir -p /etc/docker\nsudo tee /etc/docker/daemon.json <<-'EOF'\n{\n  \"registry-mirrors\": [\"https://docker.fxxk.dedyn.io\"]  # 请替换为您自己的Worker自定义域名\n}\nEOF\nsudo systemctl daemon-reload\nsudo systemctl restart docker\n```\n\n### 3. 配置常见仓库的镜像加速\n\n#### 3.1 配置\n\n`Containerd` 较简单，它支持任意 `registry` 的 `mirror`，只需要修改配置文件 `/etc/containerd/config.toml`，添加如下的配置：\n\n```yaml\n    [plugins.\"io.containerd.grpc.v1.cri\".registry]\n      [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"docker.io\"]\n          endpoint = [\"https://xxxx.xx.com\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"registry.k8s.io\"]\n          endpoint = [\"https://xxxx.xx.com\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"k8s.gcr.io\"]\n          endpoint = [\"https://xxxx.xx.com\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"gcr.io\"]\n          endpoint = [\"https://xxxx.xx.com\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"ghcr.io\"]\n          endpoint = [\"https://xxxx.xx.com\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"quay.io\"]\n          endpoint = [\"https://xxxx.xx.com\"]\n```\n\n`Podman` 同样支持任意 `registry` 的 `mirror`，修改配置文件 `/etc/containers/registries.conf`，添加配置：\n\n```yaml\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"xxxx.xx.com\"\n\n[[registry]]\nprefix = \"registry.k8s.io\"\ninsecure = true\nlocation = \"registry.k8s.io\"\n\n[[registry.mirror]]\nlocation = \"xxxx.xx.com\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"xxxx.xx.com\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"xxxx.xx.com\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"xxxx.xx.com\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"xxxx.xx.com\"\n\n```\n\n#### 3.3 使用\n\n对于以上配置，k8s 在使用的时候，就可以直接 `pull` 外部无法 pull 的镜像了。\n\n```shell\n# 手动可以直接pull配置了mirror的仓库\ncrictl pull registry.k8s.io/kube-proxy:v1.28.4\ndocker  pull nginx:1.21\n```\n\n## 🔧 变量说明\n\n| 变量名 | 示例 | 必填 | 备注 |\n|--|--|--|--|\n| URL302 | `https://t.me/CMLiussss` |❌| 主页302跳转 |\n| URL | `https://www.baidu.com/` |❌| 主页伪装(设为`nginx`则伪装为nginx默认页面) |\n| UA | `netcraft` |❌| 支持多元素, 元素之间使用空格或换行作间隔 |\n\n# 🛠️ 第三方 DockerHub 镜像服务\n\n**注意:**\n\n- 以下内容仅做镜像服务的整理与搜集，未做任何安全性检测和验证。\n- 使用前请自行斟酌，并根据实际需求进行必要的安全审查。\n- 本列表中的任何服务都不做任何形式的安全承诺或保证。\n\n| DockerHub 镜像仓库 | 镜像加地址 |\n| ------------------ | ----------- |\n| [bestcfipas 镜像服务](https://t.me/bestcfipas/4018) | `https://docker.registry.cyou` |\n|  | `https://docker-cf.registry.cyou` |\n|  | `https://registry.lfree.org` |\n| [zero_free 镜像服务](https://t.me/zero_free/80) | `https://docker.jsdelivr.fyi` |\n|  | `https://docker.aeko.cn` |\n| [mingyu 镜像服务](https://github.com/ymyuuu/HubP) | `https://hubp.de` |\n| [Docker 镜像加速站](https://docker.1panel.live)  | `https://docker.1panel.live` |\n| [Hub Proxy](https://hub.rat.dev) | `https://hub.rat.dev` |\n| [DaoCloud 镜像站](https://github.com/DaoCloud/public-image-mirror) | `https://docker.m.daocloud.io` |\n\n# 🙏 鸣谢\n### 💖 赞助支持 - 提供云服务器\n- [![digitalvirt.com](https://digitalvirt.com/templates/BlueWhite/img/logo-dark.svg)](https://url.cmliussss.com/dv)\n\n### 🛠 开源代码引用\n- [muzihuaner](https://github.com/muzihuaner)\n- [V2ex网友](https://global.v2ex.com/t/1007922)\n- [ciiiii](https://github.com/ciiiii/cloudflare-docker-proxy)\n- [ChatGPT](https://chatgpt.com/)\n- [白嫖哥](https://t.me/bestcfipas/1900)\n- [zero_free频道](https://t.me/zero_free/80)\n- [dongyubin](https://github.com/cmliu/CF-Workers-docker.io/issues/8)\n- [kiko923](https://github.com/cmliu/CF-Workers-docker.io/issues/5)\n"
  },
  {
    "path": "_worker.js",
    "content": "// _worker.js\n\n// Docker镜像仓库主机地址\nlet hub_host = 'registry-1.docker.io';\n// Docker认证服务器地址\nconst auth_url = 'https://auth.docker.io';\n\nlet 屏蔽爬虫UA = ['netcraft'];\n\n// 根据主机名选择对应的上游地址\nfunction routeByHosts(host) {\n\t// 定义路由表\n\tconst routes = {\n\t\t// 生产环境\n\t\t\"quay\": \"quay.io\",\n\t\t\"gcr\": \"gcr.io\",\n\t\t\"k8s-gcr\": \"k8s.gcr.io\",\n\t\t\"k8s\": \"registry.k8s.io\",\n\t\t\"ghcr\": \"ghcr.io\",\n\t\t\"cloudsmith\": \"docker.cloudsmith.io\",\n\t\t\"nvcr\": \"nvcr.io\",\n\n\t\t// 测试环境\n\t\t\"test\": \"registry-1.docker.io\",\n\t};\n\n\tif (host in routes) return [routes[host], false];\n\telse return [hub_host, true];\n}\n\n/** @type {RequestInit} */\nconst PREFLIGHT_INIT = {\n\t// 预检请求配置\n\theaders: new Headers({\n\t\t'access-control-allow-origin': '*', // 允许所有来源\n\t\t'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', // 允许的HTTP方法\n\t\t'access-control-max-age': '1728000', // 预检请求的缓存时间\n\t}),\n}\n\n/**\n * 构造响应\n * @param {any} body 响应体\n * @param {number} status 响应状态码\n * @param {Object<string, string>} headers 响应头\n */\nfunction makeRes(body, status = 200, headers = {}) {\n\theaders['access-control-allow-origin'] = '*' // 允许所有来源\n\treturn new Response(body, { status, headers }) // 返回新构造的响应\n}\n\n/**\n * 构造新的URL对象\n * @param {string} urlStr URL字符串\n * @param {string} base URL base\n */\nfunction newUrl(urlStr, base) {\n\ttry {\n\t\tconsole.log(`Constructing new URL object with path ${urlStr} and base ${base}`);\n\t\treturn new URL(urlStr, base); // 尝试构造新的URL对象\n\t} catch (err) {\n\t\tconsole.error(err);\n\t\treturn null // 构造失败返回null\n\t}\n}\n\nasync function nginx() {\n\tconst text = `\n\t<!DOCTYPE html>\n\t<html>\n\t<head>\n\t<title>Welcome to nginx!</title>\n\t<style>\n\t\tbody {\n\t\t\twidth: 35em;\n\t\t\tmargin: 0 auto;\n\t\t\tfont-family: Tahoma, Verdana, Arial, sans-serif;\n\t\t}\n\t</style>\n\t</head>\n\t<body>\n\t<h1>Welcome to nginx!</h1>\n\t<p>If you see this page, the nginx web server is successfully installed and\n\tworking. Further configuration is required.</p>\n\t\n\t<p>For online documentation and support please refer to\n\t<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\n\tCommercial support is available at\n\t<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\t\n\t<p><em>Thank you for using nginx.</em></p>\n\t</body>\n\t</html>\n\t`\n\treturn text;\n}\n\nasync function searchInterface() {\n\tconst html = `\n\t<!DOCTYPE html>\n\t<html>\n\t<head>\n\t\t<title>Docker Hub 镜像搜索</title>\n\t\t<meta charset=\"UTF-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t\t<style>\n\t\t:root {\n\t\t\t--github-color: rgb(27,86,198);\n\t\t\t--github-bg-color: #ffffff;\n\t\t\t--primary-color: #0066ff;\n\t\t\t--primary-dark: #0052cc;\n\t\t\t--gradient-start: #1a90ff;\n\t\t\t--gradient-end: #003eb3;\n\t\t\t--text-color: #ffffff;\n\t\t\t--shadow-color: rgba(0,0,0,0.1);\n\t\t\t--transition-time: 0.3s;\n\t\t}\n\t\t\n\t\t* {\n\t\t\tbox-sizing: border-box;\n\t\t\tmargin: 0;\n\t\t\tpadding: 0;\n\t\t}\n\n\t\tbody {\n\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tjustify-content: center;\n\t\t\talign-items: center;\n\t\t\tmin-height: 100vh;\n\t\t\tmargin: 0;\n\t\t\tbackground: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);\n\t\t\tpadding: 20px;\n\t\t\tcolor: var(--text-color);\n\t\t\toverflow-x: hidden;\n\t\t}\n\n\t\t.container {\n\t\t\ttext-align: center;\n\t\t\twidth: 100%;\n\t\t\tmax-width: 800px;\n\t\t\tpadding: 20px;\n\t\t\tmargin: 0 auto;\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tjustify-content: center;\n\t\t\tmin-height: 60vh;\n\t\t\tanimation: fadeIn 0.8s ease-out;\n\t\t}\n\n\t\t@keyframes fadeIn {\n\t\t\tfrom { opacity: 0; transform: translateY(20px); }\n\t\t\tto { opacity: 1; transform: translateY(0); }\n\t\t}\n\n\t\t.github-corner {\n\t\t\tposition: fixed;\n\t\t\ttop: 0;\n\t\t\tright: 0;\n\t\t\tz-index: 999;\n\t\t\ttransition: transform var(--transition-time) ease;\n\t\t}\n\t\t\n\t\t.github-corner:hover {\n\t\t\ttransform: scale(1.08);\n\t\t}\n\n\t\t.github-corner svg {\n\t\t\tfill: var(--github-bg-color);\n\t\t\tcolor: var(--github-color);\n\t\t\tposition: absolute;\n\t\t\ttop: 0;\n\t\t\tborder: 0;\n\t\t\tright: 0;\n\t\t\twidth: 80px;\n\t\t\theight: 80px;\n\t\t\tfilter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.2));\n\t\t}\n\n\t\t.logo {\n\t\t\tmargin-bottom: 20px;\n\t\t\ttransition: transform var(--transition-time) ease;\n\t\t\tanimation: float 6s ease-in-out infinite;\n\t\t}\n\t\t\n\t\t@keyframes float {\n\t\t\t0%, 100% { transform: translateY(0); }\n\t\t\t50% { transform: translateY(-10px); }\n\t\t}\n\t\t\n\t\t.logo:hover {\n\t\t\ttransform: scale(1.08) rotate(5deg);\n\t\t}\n\t\t\n\t\t.logo svg {\n\t\t\tfilter: drop-shadow(0 5px 15px rgba(0, 0, 0, 0.2));\n\t\t}\n\t\t\n\t\t.title {\n\t\t\tcolor: var(--text-color);\n\t\t\tfont-size: 2.3em;\n\t\t\tmargin-bottom: 10px;\n\t\t\ttext-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);\n\t\t\tfont-weight: 700;\n\t\t\tletter-spacing: -0.5px;\n\t\t\tanimation: slideInFromTop 0.5s ease-out 0.2s both;\n\t\t}\n\t\t\n\t\t@keyframes slideInFromTop {\n\t\t\tfrom { opacity: 0; transform: translateY(-20px); }\n\t\t\tto { opacity: 1; transform: translateY(0); }\n\t\t}\n\t\t\n\t\t.subtitle {\n\t\t\tcolor: rgba(255, 255, 255, 0.9);\n\t\t\tfont-size: 1.1em;\n\t\t\tmargin-bottom: 25px;\n\t\t\tmax-width: 600px;\n\t\t\tmargin-left: auto;\n\t\t\tmargin-right: auto;\n\t\t\tline-height: 1.4;\n\t\t\tanimation: slideInFromTop 0.5s ease-out 0.4s both;\n\t\t}\n\t\t\n\t\t.search-container {\n\t\t\tdisplay: flex;\n\t\t\talign-items: stretch;\n\t\t\twidth: 100%;\n\t\t\tmax-width: 600px;\n\t\t\tmargin: 0 auto;\n\t\t\theight: 55px;\n\t\t\tposition: relative;\n\t\t\tanimation: slideInFromBottom 0.5s ease-out 0.6s both;\n\t\t\tbox-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);\n\t\t\tborder-radius: 12px;\n\t\t\toverflow: hidden;\n\t\t}\n\t\t\n\t\t@keyframes slideInFromBottom {\n\t\t\tfrom { opacity: 0; transform: translateY(20px); }\n\t\t\tto { opacity: 1; transform: translateY(0); }\n\t\t}\n\t\t\n\t\t#search-input {\n\t\t\tflex: 1;\n\t\t\tpadding: 0 20px;\n\t\t\tfont-size: 16px;\n\t\t\tborder: none;\n\t\t\toutline: none;\n\t\t\ttransition: all var(--transition-time) ease;\n\t\t\theight: 100%;\n\t\t}\n\t\t\n\t\t#search-input:focus {\n\t\t\tpadding-left: 25px;\n\t\t}\n\t\t\n\t\t#search-button {\n\t\t\twidth: 60px;\n\t\t\tbackground-color: var(--primary-color);\n\t\t\tborder: none;\n\t\t\tcursor: pointer;\n\t\t\ttransition: all var(--transition-time) ease;\n\t\t\theight: 100%;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tposition: relative;\n\t\t}\n\t\t\n\t\t#search-button svg {\n\t\t\ttransition: transform 0.3s ease;\n\t\t\tstroke: white;\n\t\t}\n\t\t\n\t\t#search-button:hover {\n\t\t\tbackground-color: var(--primary-dark);\n\t\t}\n\t\t\n\t\t#search-button:hover svg {\n\t\t\ttransform: translateX(2px);\n\t\t}\n\t\t\n\t\t#search-button:active svg {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t\n\t\t.tips {\n\t\t\tcolor: rgba(255, 255, 255, 0.8);\n\t\t\tmargin-top: 20px;\n\t\t\tfont-size: 0.9em;\n\t\t\tanimation: fadeIn 0.5s ease-out 0.8s both;\n\t\t\ttransition: transform var(--transition-time) ease;\n\t\t}\n\t\t\n\t\t.tips:hover {\n\t\t\ttransform: translateY(-2px);\n\t\t}\n\t\t\n\t\t@media (max-width: 768px) {\n\t\t\t.container {\n\t\t\t\tpadding: 20px 15px;\n\t\t\t\tmin-height: 60vh;\n\t\t\t}\n\t\t\t\n\t\t\t.title {\n\t\t\t\tfont-size: 2em;\n\t\t\t}\n\t\t\t\n\t\t\t.subtitle {\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin-bottom: 20px;\n\t\t\t}\n\t\t\t\n\t\t\t.search-container {\n\t\t\t\theight: 50px;\n\t\t\t}\n\t\t}\n\t\t\n\t\t@media (max-width: 480px) {\n\t\t\t.container {\n\t\t\t\tpadding: 15px 10px;\n\t\t\t\tmin-height: 60vh;\n\t\t\t}\n\t\t\t\n\t\t\t.github-corner svg {\n\t\t\t\twidth: 60px;\n\t\t\t\theight: 60px;\n\t\t\t}\n\t\t\t\n\t\t\t.search-container {\n\t\t\t\theight: 45px;\n\t\t\t}\n\t\t\t\n\t\t\t#search-input {\n\t\t\t\tpadding: 0 15px;\n\t\t\t}\n\t\t\t\n\t\t\t#search-button {\n\t\t\t\twidth: 50px;\n\t\t\t}\n\t\t\t\n\t\t\t#search-button svg {\n\t\t\t\twidth: 18px;\n\t\t\t\theight: 18px;\n\t\t\t}\n\t\t\t\n\t\t\t.title {\n\t\t\t\tfont-size: 1.7em;\n\t\t\t\tmargin-bottom: 8px;\n\t\t\t}\n\t\t\t\n\t\t\t.subtitle {\n\t\t\t\tfont-size: 0.95em;\n\t\t\t\tmargin-bottom: 18px;\n\t\t\t}\n\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<a href=\"https://github.com/cmliu/CF-Workers-docker.io\" target=\"_blank\" class=\"github-corner\" aria-label=\"View source on Github\">\n\t\t\t<svg viewBox=\"0 0 250 250\" aria-hidden=\"true\">\n\t\t\t\t<path d=\"M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z\"></path>\n\t\t\t\t<path d=\"M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2\" fill=\"currentColor\" style=\"transform-origin: 130px 106px;\" class=\"octo-arm\"></path>\n\t\t\t\t<path d=\"M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z\" fill=\"currentColor\" class=\"octo-body\"></path>\n\t\t\t</svg>\n\t\t</a>\n\t\t<div class=\"container\">\n\t\t\t<div class=\"logo\">\n\t\t\t\t<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 18\" fill=\"#ffffff\" width=\"110\" height=\"85\">\n\t\t\t\t\t<path d=\"M23.763 6.886c-.065-.053-.673-.512-1.954-.512-.32 0-.659.03-1.01.087-.248-1.703-1.651-2.533-1.716-2.57l-.345-.2-.227.328a4.596 4.596 0 0 0-.611 1.433c-.23.972-.09 1.884.403 2.666-.596.331-1.546.418-1.744.42H.752a.753.753 0 0 0-.75.749c-.007 1.456.233 2.864.692 4.07.545 1.43 1.355 2.483 2.409 3.13 1.181.725 3.104 1.14 5.276 1.14 1.016 0 2.03-.092 2.93-.266 1.417-.273 2.705-.742 3.826-1.391a10.497 10.497 0 0 0 2.61-2.14c1.252-1.42 1.998-3.005 2.553-4.408.075.003.148.005.221.005 1.371 0 2.215-.55 2.68-1.01.505-.5.685-.998.704-1.053L24 7.076l-.237-.19Z\"></path>\n\t\t\t\t\t<path d=\"M2.216 8.075h2.119a.186.186 0 0 0 .185-.186V6a.186.186 0 0 0-.185-.186H2.216A.186.186 0 0 0 2.031 6v1.89c0 .103.083.186.185.186Zm2.92 0h2.118a.185.185 0 0 0 .185-.186V6a.185.185 0 0 0-.185-.186H5.136A.185.185 0 0 0 4.95 6v1.89c0 .103.083.186.186.186Zm2.964 0h2.118a.186.186 0 0 0 .185-.186V6a.186.186 0 0 0-.185-.186H8.1A.185.185 0 0 0 7.914 6v1.89c0 .103.083.186.186.186Zm2.928 0h2.119a.185.185 0 0 0 .185-.186V6a.185.185 0 0 0-.185-.186h-2.119a.186.186 0 0 0-.185.186v1.89c0 .103.083.186.185.186Zm-5.892-2.72h2.118a.185.185 0 0 0 .185-.186V3.28a.186.186 0 0 0-.185-.186H5.136a.186.186 0 0 0-.186.186v1.89c0 .103.083.186.186.186Zm2.964 0h2.118a.186.186 0 0 0 .185-.186V3.28a.186.186 0 0 0-.185-.186H8.1a.186.186 0 0 0-.186.186v1.89c0 .103.083.186.186.186Zm2.928 0h2.119a.185.185 0 0 0 .185-.186V3.28a.186.186 0 0 0-.185-.186h-2.119a.186.186 0 0 0-.185.186v1.89c0 .103.083.186.185.186Zm0-2.72h2.119a.186.186 0 0 0 .185-.186V.56a.185.185 0 0 0-.185-.186h-2.119a.186.186 0 0 0-.185.186v1.89c0 .103.083.186.185.186Zm2.955 5.44h2.118a.185.185 0 0 0 .186-.186V6a.185.185 0 0 0-.186-.186h-2.118a.185.185 0 0 0-.185.186v1.89c0 .103.083.186.185.186Z\"></path>\n\t\t\t\t</svg>\n\t\t\t</div>\n\t\t\t<h1 class=\"title\">Docker Hub 镜像搜索</h1>\n\t\t\t<p class=\"subtitle\">快速查找、下载和部署 Docker 容器镜像</p>\n\t\t\t<div class=\"search-container\">\n\t\t\t\t<input type=\"text\" id=\"search-input\" placeholder=\"输入关键词搜索镜像，如: nginx, mysql, redis...\">\n\t\t\t\t<button id=\"search-button\" title=\"搜索\">\n\t\t\t\t\t<svg width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\">\n\t\t\t\t\t\t<path d=\"M13 5l7 7-7 7M5 5l7 7-7 7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path>\n\t\t\t\t\t</svg>\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<p class=\"tips\">基于 Cloudflare Workers / Pages 构建，利用全球边缘网络实现毫秒级响应。</p>\n\t\t</div>\n\t\t<script>\n\t\tfunction performSearch() {\n\t\t\tconst query = document.getElementById('search-input').value;\n\t\t\tif (query) {\n\t\t\t\twindow.location.href = '/search?q=' + encodeURIComponent(query);\n\t\t\t}\n\t\t}\n\t\n\t\tdocument.getElementById('search-button').addEventListener('click', performSearch);\n\t\tdocument.getElementById('search-input').addEventListener('keypress', function(event) {\n\t\t\tif (event.key === 'Enter') {\n\t\t\t\tperformSearch();\n\t\t\t}\n\t\t});\n\n\t\t// 添加焦点在搜索框\n\t\twindow.addEventListener('load', function() {\n\t\t\tdocument.getElementById('search-input').focus();\n\t\t});\n\t\t</script>\n\t</body>\n\t</html>\n\t`;\n\treturn html;\n}\n\nexport default {\n\tasync fetch(request, env, ctx) {\n\t\tconst getReqHeader = (key) => request.headers.get(key); // 获取请求头\n\n\t\tlet url = new URL(request.url); // 解析请求URL\n\t\tconst userAgentHeader = request.headers.get('User-Agent');\n\t\tconst userAgent = userAgentHeader ? userAgentHeader.toLowerCase() : \"null\";\n\t\tif (env.UA) 屏蔽爬虫UA = 屏蔽爬虫UA.concat(await ADD(env.UA));\n\t\tconst workers_url = `https://${url.hostname}`;\n\n\t\t// 获取请求参数中的 ns\n\t\tconst ns = url.searchParams.get('ns');\n\t\tconst hostname = url.searchParams.get('hubhost') || url.hostname;\n\t\tconst hostTop = hostname.split('.')[0]; // 获取主机名的第一部分\n\n\t\tlet checkHost; // 在这里定义 checkHost 变量\n\t\t// 如果存在 ns 参数，优先使用它来确定 hub_host\n\t\tif (ns) {\n\t\t\tif (ns === 'docker.io') {\n\t\t\t\thub_host = 'registry-1.docker.io'; // 设置上游地址为 registry-1.docker.io\n\t\t\t} else {\n\t\t\t\thub_host = ns; // 直接使用 ns 作为 hub_host\n\t\t\t}\n\t\t} else {\n\t\t\tcheckHost = routeByHosts(hostTop);\n\t\t\thub_host = checkHost[0]; // 获取上游地址\n\t\t}\n\n\t\tconst fakePage = checkHost ? checkHost[1] : false; // 确保 fakePage 不为 undefined\n\t\tconsole.log(`域名头部: ${hostTop} 反代地址: ${hub_host} searchInterface: ${fakePage}`);\n\t\t// 更改请求的主机名\n\t\turl.hostname = hub_host;\n\t\tconst hubParams = ['/v1/search', '/v1/repositories'];\n\t\tif (屏蔽爬虫UA.some(fxxk => userAgent.includes(fxxk)) && 屏蔽爬虫UA.length > 0) {\n\t\t\t// 首页改成一个nginx伪装页\n\t\t\treturn new Response(await nginx(), {\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'text/html; charset=UTF-8',\n\t\t\t\t},\n\t\t\t});\n\t\t} else if ((userAgent && userAgent.includes('mozilla')) || hubParams.some(param => url.pathname.includes(param))) {\n\t\t\tif (url.pathname == '/') {\n\t\t\t\tif (env.URL302) {\n\t\t\t\t\treturn Response.redirect(env.URL302, 302);\n\t\t\t\t} else if (env.URL) {\n\t\t\t\t\tif (env.URL.toLowerCase() == 'nginx') {\n\t\t\t\t\t\t//首页改成一个nginx伪装页\n\t\t\t\t\t\treturn new Response(await nginx(), {\n\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t'Content-Type': 'text/html; charset=UTF-8',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t} else return fetch(new Request(env.URL, request));\n\t\t\t\t} else\t{\n\t\t\t\t\tif (fakePage) return new Response(await searchInterface(), {\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'Content-Type': 'text/html; charset=UTF-8',\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// 新增逻辑：/v1/ 路径特殊处理\n\t\t\t\tif (url.pathname.startsWith('/v1/')) {\n\t\t\t\t\turl.hostname = 'index.docker.io';\n\t\t\t\t} else if (fakePage) {\n\t\t\t\t\turl.hostname = 'hub.docker.com';\n\t\t\t\t}\n\t\t\t\tif (url.searchParams.get('q')?.includes('library/') && url.searchParams.get('q') != 'library/') {\n\t\t\t\t\tconst search = url.searchParams.get('q');\n\t\t\t\t\turl.searchParams.set('q', search.replace('library/', ''));\n\t\t\t\t}\n\t\t\t\tconst newRequest = new Request(url, request);\n\t\t\t\treturn fetch(newRequest);\n\t\t\t}\n\t\t}\n\n\t\t// 修改包含 %2F 和 %3A 的请求\n\t\tif (!/%2F/.test(url.search) && /%3A/.test(url.toString())) {\n\t\t\tlet modifiedUrl = url.toString().replace(/%3A(?=.*?&)/, '%3Alibrary%2F');\n\t\t\turl = new URL(modifiedUrl);\n\t\t\tconsole.log(`handle_url: ${url}`);\n\t\t}\n\n\t\t// 处理token请求\n\t\tif (url.pathname.includes('/token')) {\n\t\t\tlet token_parameter = {\n\t\t\t\theaders: {\n\t\t\t\t\t'Host': 'auth.docker.io',\n\t\t\t\t\t'User-Agent': getReqHeader(\"User-Agent\"),\n\t\t\t\t\t'Accept': getReqHeader(\"Accept\"),\n\t\t\t\t\t'Accept-Language': getReqHeader(\"Accept-Language\"),\n\t\t\t\t\t'Accept-Encoding': getReqHeader(\"Accept-Encoding\"),\n\t\t\t\t\t'Connection': 'keep-alive',\n\t\t\t\t\t'Cache-Control': 'max-age=0'\n\t\t\t\t}\n\t\t\t};\n\t\t\tlet token_url = auth_url + url.pathname + url.search;\n\t\t\treturn fetch(new Request(token_url, request), token_parameter);\n\t\t}\n\n\t\t// 修改 /v2/ 请求路径\n\t\tif (hub_host == 'registry-1.docker.io' && /^\\/v2\\/[^/]+\\/[^/]+\\/[^/]+$/.test(url.pathname) && !/^\\/v2\\/library/.test(url.pathname)) {\n\t\t\t//url.pathname = url.pathname.replace(/\\/v2\\//, '/v2/library/');\n\t\t\turl.pathname = '/v2/library/' + url.pathname.split('/v2/')[1];\n\t\t\tconsole.log(`modified_url: ${url.pathname}`);\n\t\t}\n\n\t\t// 新增：/v2/、/manifests/、/blobs/、/tags/ 先获取token再请求\n\t\tif (\n\t\t\turl.pathname.startsWith('/v2/') &&\n\t\t\t(\n\t\t\t\turl.pathname.includes('/manifests/') ||\n\t\t\t\turl.pathname.includes('/blobs/') ||\n\t\t\t\turl.pathname.includes('/tags/')\n\t\t\t\t|| url.pathname.endsWith('/tags/list')\n\t\t\t)\n\t\t) {\n\t\t\t// 提取镜像名\n\t\t\tlet repo = '';\n\t\t\tconst v2Match = url.pathname.match(/^\\/v2\\/(.+?)(?:\\/(manifests|blobs|tags)\\/)/);\n\t\t\tif (v2Match) {\n\t\t\t\trepo = v2Match[1];\n\t\t\t}\n\t\t\tif (repo) {\n\t\t\t\tconst tokenUrl = `${auth_url}/token?service=registry.docker.io&scope=repository:${repo}:pull`;\n\t\t\t\tconst tokenRes = await fetch(tokenUrl, {\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t'User-Agent': getReqHeader(\"User-Agent\"),\n\t\t\t\t\t\t'Accept': getReqHeader(\"Accept\"),\n\t\t\t\t\t\t'Accept-Language': getReqHeader(\"Accept-Language\"),\n\t\t\t\t\t\t'Accept-Encoding': getReqHeader(\"Accept-Encoding\"),\n\t\t\t\t\t\t'Connection': 'keep-alive',\n\t\t\t\t\t\t'Cache-Control': 'max-age=0'\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tconst tokenData = await tokenRes.json();\n\t\t\t\tconst token = tokenData.token;\n\t\t\t\tlet parameter = {\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t'Host': hub_host,\n\t\t\t\t\t\t'User-Agent': getReqHeader(\"User-Agent\"),\n\t\t\t\t\t\t'Accept': getReqHeader(\"Accept\"),\n\t\t\t\t\t\t'Accept-Language': getReqHeader(\"Accept-Language\"),\n\t\t\t\t\t\t'Accept-Encoding': getReqHeader(\"Accept-Encoding\"),\n\t\t\t\t\t\t'Connection': 'keep-alive',\n\t\t\t\t\t\t'Cache-Control': 'max-age=0',\n\t\t\t\t\t\t'Authorization': `Bearer ${token}`\n\t\t\t\t\t},\n\t\t\t\t\tcacheTtl: 3600\n\t\t\t\t};\n\t\t\t\tif (request.headers.has(\"X-Amz-Content-Sha256\")) {\n\t\t\t\t\tparameter.headers['X-Amz-Content-Sha256'] = getReqHeader(\"X-Amz-Content-Sha256\");\n\t\t\t\t}\n\t\t\t\tlet original_response = await fetch(new Request(url, request), parameter);\n\t\t\t\tlet original_response_clone = original_response.clone();\n\t\t\t\tlet original_text = original_response_clone.body;\n\t\t\t\tlet response_headers = original_response.headers;\n\t\t\t\tlet new_response_headers = new Headers(response_headers);\n\t\t\t\tlet status = original_response.status;\n\t\t\t\tif (new_response_headers.get(\"Www-Authenticate\")) {\n\t\t\t\t\tlet auth = new_response_headers.get(\"Www-Authenticate\");\n\t\t\t\t\tlet re = new RegExp(auth_url, 'g');\n\t\t\t\t\tnew_response_headers.set(\"Www-Authenticate\", response_headers.get(\"Www-Authenticate\").replace(re, workers_url));\n\t\t\t\t}\n\t\t\t\tif (new_response_headers.get(\"Location\")) {\n\t\t\t\t\tconst location = new_response_headers.get(\"Location\");\n\t\t\t\t\tconsole.info(`Found redirection location, redirecting to ${location}`);\n\t\t\t\t\treturn httpHandler(request, location, hub_host);\n\t\t\t\t}\n\t\t\t\tlet response = new Response(original_text, {\n\t\t\t\t\tstatus,\n\t\t\t\t\theaders: new_response_headers\n\t\t\t\t});\n\t\t\t\treturn response;\n\t\t\t}\n\t\t}\n\n\t\t// 构造请求参数\n\t\tlet parameter = {\n\t\t\theaders: {\n\t\t\t\t'Host': hub_host,\n\t\t\t\t'User-Agent': getReqHeader(\"User-Agent\"),\n\t\t\t\t'Accept': getReqHeader(\"Accept\"),\n\t\t\t\t'Accept-Language': getReqHeader(\"Accept-Language\"),\n\t\t\t\t'Accept-Encoding': getReqHeader(\"Accept-Encoding\"),\n\t\t\t\t'Connection': 'keep-alive',\n\t\t\t\t'Cache-Control': 'max-age=0'\n\t\t\t},\n\t\t\tcacheTtl: 3600 // 缓存时间\n\t\t};\n\n\t\t// 添加Authorization头\n\t\tif (request.headers.has(\"Authorization\")) {\n\t\t\tparameter.headers.Authorization = getReqHeader(\"Authorization\");\n\t\t}\n\n\t\t// 添加可能存在字段X-Amz-Content-Sha256\n\t\tif (request.headers.has(\"X-Amz-Content-Sha256\")) {\n\t\t\tparameter.headers['X-Amz-Content-Sha256'] = getReqHeader(\"X-Amz-Content-Sha256\");\n\t\t}\n\n\t\t// 发起请求并处理响应\n\t\tlet original_response = await fetch(new Request(url, request), parameter);\n\t\tlet original_response_clone = original_response.clone();\n\t\tlet original_text = original_response_clone.body;\n\t\tlet response_headers = original_response.headers;\n\t\tlet new_response_headers = new Headers(response_headers);\n\t\tlet status = original_response.status;\n\n\t\t// 修改 Www-Authenticate 头\n\t\tif (new_response_headers.get(\"Www-Authenticate\")) {\n\t\t\tlet auth = new_response_headers.get(\"Www-Authenticate\");\n\t\t\tlet re = new RegExp(auth_url, 'g');\n\t\t\tnew_response_headers.set(\"Www-Authenticate\", response_headers.get(\"Www-Authenticate\").replace(re, workers_url));\n\t\t}\n\n\t\t// 处理重定向\n\t\tif (new_response_headers.get(\"Location\")) {\n\t\t\tconst location = new_response_headers.get(\"Location\");\n\t\t\tconsole.info(`Found redirection location, redirecting to ${location}`);\n\t\t\treturn httpHandler(request, location, hub_host);\n\t\t}\n\n\t\t// 返回修改后的响应\n\t\tlet response = new Response(original_text, {\n\t\t\tstatus,\n\t\t\theaders: new_response_headers\n\t\t});\n\t\treturn response;\n\t}\n};\n\n/**\n * 处理HTTP请求\n * @param {Request} req 请求对象\n * @param {string} pathname 请求路径\n * @param {string} baseHost 基地址\n */\nfunction httpHandler(req, pathname, baseHost) {\n\tconst reqHdrRaw = req.headers;\n\n\t// 处理预检请求\n\tif (req.method === 'OPTIONS' &&\n\t\treqHdrRaw.has('access-control-request-headers')\n\t) {\n\t\treturn new Response(null, PREFLIGHT_INIT);\n\t}\n\n\tlet rawLen = '';\n\n\tconst reqHdrNew = new Headers(reqHdrRaw);\n\n\treqHdrNew.delete(\"Authorization\"); // 修复s3错误\n\n\tconst refer = reqHdrNew.get('referer');\n\n\tlet urlStr = pathname;\n\n\tconst urlObj = newUrl(urlStr, 'https://' + baseHost);\n\n\t/** @type {RequestInit} */\n\tconst reqInit = {\n\t\tmethod: req.method,\n\t\theaders: reqHdrNew,\n\t\tredirect: 'follow',\n\t\tbody: req.body\n\t};\n\treturn proxy(urlObj, reqInit, rawLen);\n}\n\n/**\n * 代理请求\n * @param {URL} urlObj URL对象\n * @param {RequestInit} reqInit 请求初始化对象\n * @param {string} rawLen 原始长度\n */\nasync function proxy(urlObj, reqInit, rawLen) {\n\tconst res = await fetch(urlObj.href, reqInit);\n\tconst resHdrOld = res.headers;\n\tconst resHdrNew = new Headers(resHdrOld);\n\n\t// 验证长度\n\tif (rawLen) {\n\t\tconst newLen = resHdrOld.get('content-length') || '';\n\t\tconst badLen = (rawLen !== newLen);\n\n\t\tif (badLen) {\n\t\t\treturn makeRes(res.body, 400, {\n\t\t\t\t'--error': `bad len: ${newLen}, except: ${rawLen}`,\n\t\t\t\t'access-control-expose-headers': '--error',\n\t\t\t});\n\t\t}\n\t}\n\tconst status = res.status;\n\tresHdrNew.set('access-control-expose-headers', '*');\n\tresHdrNew.set('access-control-allow-origin', '*');\n\tresHdrNew.set('Cache-Control', 'max-age=1500');\n\n\t// 删除不必要的头\n\tresHdrNew.delete('content-security-policy');\n\tresHdrNew.delete('content-security-policy-report-only');\n\tresHdrNew.delete('clear-site-data');\n\n\treturn new Response(res.body, {\n\t\tstatus,\n\t\theaders: resHdrNew\n\t});\n}\n\nasync function ADD(envadd) {\n\tvar addtext = envadd.replace(/[\t |\"'\\r\\n]+/g, ',').replace(/,+/g, ',');\t// 将空格、双引号、单引号和换行符替换为逗号\n\tif (addtext.charAt(0) == ',') addtext = addtext.slice(1);\n\tif (addtext.charAt(addtext.length - 1) == ',') addtext = addtext.slice(0, addtext.length - 1);\n\tconst add = addtext.split(',');\n\treturn add;\n}\n"
  }
]