方案 1(👍): 利用大善人 CF woker 的 镜像加速. https://global.v2ex.com/t/1007922
代码搬运:
'use strict' | |
const hub_host = 'registry-1.docker.io' | |
const auth_url = 'https://auth.docker.io' | |
const workers_url = 'https:// 你的域名 ' | |
/** | |
* static files (404.html, sw.js, conf.js) | |
*/ | |
/** @type {RequestInit} */ | |
const PREFLIGHT_INIT = { | |
status: 204, | |
headers: new Headers({ | |
'access-control-allow-origin': '*', | |
'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', | |
'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}) | |
} | |
/** | |
* @param {string} urlStr | |
*/ | |
function newUrl(urlStr) { | |
try {return new URL(urlStr) | |
} catch (err) {return null} | |
} | |
addEventListener('fetch', e => {const ret = fetchHandler(e) | |
.catch(err => makeRes('cfworker error:\n' + err.stack, 502)) | |
e.respondWith(ret) | |
}) | |
/** | |
* @param {FetchEvent} e | |
*/ | |
async function fetchHandler(e) {const getReqHeader = (key) => e.request.headers.get(key); | |
let url = new URL(e.request.url); | |
if (url.pathname === '/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, e.request), token_parameter) | |
} | |
url.hostname = hub_host; | |
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 | |
}; | |
if (e.request.headers.has("Authorization")) {parameter.headers.Authorization = getReqHeader("Authorization"); | |
} | |
let original_response = await fetch(new Request(url, e.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")) {return httpHandler(e.request, new_response_headers.get("Location")) | |
} | |
let response = new Response(original_text, { | |
status, | |
headers: new_response_headers | |
}) | |
return response; | |
} | |
/** | |
* @param {Request} req | |
* @param {string} pathname | |
*/ | |
function httpHandler(req, pathname) { | |
const reqHdrRaw = req.headers | |
// preflight | |
if (req.method === 'OPTIONS' && | |
reqHdrRaw.has('access-control-request-headers') | |
) {return new Response(null, PREFLIGHT_INIT) | |
} | |
let rawLen = '' | |
const reqHdrNew = new Headers(reqHdrRaw) | |
const refer = reqHdrNew.get('referer') | |
let urlStr = pathname | |
const urlObj = newUrl(urlStr) | |
/** @type {RequestInit} */ | |
const reqInit = { | |
method: req.method, | |
headers: reqHdrNew, | |
redirect: 'follow', | |
body: req.body | |
} | |
return proxy(urlObj, reqInit, rawLen, 0) | |
} | |
/** | |
* | |
* @param {URL} urlObj | |
* @param {RequestInit} reqInit | |
*/ | |
async function proxy(urlObj, reqInit, rawLen) {const res = await fetch(urlObj.href, reqInit) | |
const resHdrOld = res.headers | |
const resHdrNew = new Headers(resHdrOld) | |
// verify | |
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 | |
}) | |
} |
方案 2(服务器自建): 利用 registry 或者 harbor :
下面是 reigistry 方案:
nano registry/docker-compose.yml
#version: '3' #最新版本 docker 不在需要此字段 | |
services: | |
registry: | |
image: registry:2 | |
ports: | |
- "15000:5000" | |
environment: | |
REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io # 上游源 | |
REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR: inmemory # 内存缓存 | |
volumes: | |
- ./data:/var/lib/registry |
如果只是作为镜像, 就把推送功能 ban 掉. 这里推荐用简单的方式 nginx 反代 禁止其他 http method.
# 端口, 域名 都改为自己的 | |
server { | |
listen 80; | |
server_name my-registry-domain.com; | |
location / { | |
# 仅允许 GET 请求 | |
limit_except GET {deny all;} | |
proxy_pass http://localhost:15000; # 假设 Docker Registry 运行在本地的 15000 端口 | |
proxy_set_header Host $host; | |
proxy_set_header X-Real-IP $remote_addr; | |
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
proxy_set_header X-Forwarded-Proto $scheme; | |
} | |
} |
github 加速:https://cdn.jsdelivr.net/gh/hunshcn/gh-proxy@master/index.js
'use strict' | |
/** | |
* static files (404.html, sw.js, conf.js) | |
*/ | |
const ASSET_URL = 'https://hunshcn.github.io/gh-proxy/' | |
// 前缀,如果自定义路由为 example.com/gh/*,将 PREFIX 改为 '/gh/',注意,少一个杠都会错!const PREFIX = '/' | |
// 分支文件使用 jsDelivr 镜像的开关,0 为关闭,默认关闭 | |
const Config = {jsdelivr: 0} | |
const whiteList = [] // 白名单,路径里面有包含字符的才会通过,e.g. ['/username/'] | |
/** @type {ResponseInit} */ | |
const PREFLIGHT_INIT = { | |
status: 204, | |
headers: new Headers({ | |
'access-control-allow-origin': '*', | |
'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', | |
'access-control-max-age': '1728000', | |
}), | |
} | |
const exp1 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:releases|archive)\/.*$/i | |
const exp2 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:blob|raw)\/.*$/i | |
const exp3 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:info|git-).*$/i | |
const exp4 = /^(?:https?:\/\/)?raw\.(?:githubusercontent|github)\.com\/.+?\/.+?\/.+?\/.+$/i | |
const exp5 = /^(?:https?:\/\/)?gist\.(?:githubusercontent|github)\.com\/.+?\/.+?\/.+$/i | |
const exp6 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/tags.*$/i | |
/** | |
* @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}) | |
} | |
/** | |
* @param {string} urlStr | |
*/ | |
function newUrl(urlStr) { | |
try {return new URL(urlStr) | |
} catch (err) {return null} | |
} | |
addEventListener('fetch', e => {const ret = fetchHandler(e) | |
.catch(err => makeRes('cfworker error:\n' + err.stack, 502)) | |
e.respondWith(ret) | |
}) | |
function checkUrl(u) {for (let i of [exp1, exp2, exp3, exp4, exp5, exp6]) {if (u.search(i) === 0) {return true} | |
} | |
return false | |
} | |
/** | |
* @param {FetchEvent} e | |
*/ | |
async function fetchHandler(e) { | |
const req = e.request | |
const urlStr = req.url | |
const urlObj = new URL(urlStr) | |
let path = urlObj.searchParams.get('q') | |
if (path) {return Response.redirect('https://' + urlObj.host + PREFIX + path, 301) | |
} | |
// cfworker 会把路径中的 `//` 合并成 `/` | |
path = urlObj.href.substr(urlObj.origin.length + PREFIX.length).replace(/^https?:\/+/, 'https://') | |
if (path.search(exp1) === 0 || path.search(exp5) === 0 || path.search(exp6) === 0 || path.search(exp3) === 0 || path.search(exp4) === 0) {return httpHandler(req, path) | |
} else if (path.search(exp2) === 0) {if (Config.jsdelivr) {const newUrl = path.replace('/blob/', '@').replace(/^(?:https?:\/\/)?github\.com/, 'https://cdn.jsdelivr.net/gh') | |
return Response.redirect(newUrl, 302) | |
} else {path = path.replace('/blob/', '/raw/') | |
return httpHandler(req, path) | |
} | |
} else if (path.search(exp4) === 0) {const newUrl = path.replace(/(?<=com\/.+?\/.+?)\/(.+?\/)/, '@$1').replace(/^(?:https?:\/\/)?raw\.(?:githubusercontent|github)\.com/, 'https://cdn.jsdelivr.net/gh') | |
return Response.redirect(newUrl, 302) | |
} else {return fetch(ASSET_URL + path) | |
} | |
} | |
/** | |
* @param {Request} req | |
* @param {string} pathname | |
*/ | |
function httpHandler(req, pathname) { | |
const reqHdrRaw = req.headers | |
// preflight | |
if (req.method === 'OPTIONS' && | |
reqHdrRaw.has('access-control-request-headers') | |
) {return new Response(null, PREFLIGHT_INIT) | |
} | |
const reqHdrNew = new Headers(reqHdrRaw) | |
let urlStr = pathname | |
let flag = !Boolean(whiteList.length) | |
for (let i of whiteList) {if (urlStr.includes(i)) { | |
flag = true | |
break | |
} | |
} | |
if (!flag) {return new Response("blocked", {status: 403}) | |
} | |
if (urlStr.search(/^https?:\/\//) !== 0) {urlStr = 'https://' + urlStr} | |
const urlObj = newUrl(urlStr) | |
/** @type {RequestInit} */ | |
const reqInit = { | |
method: req.method, | |
headers: reqHdrNew, | |
redirect: 'manual', | |
body: req.body | |
} | |
return proxy(urlObj, reqInit) | |
} | |
/** | |
* | |
* @param {URL} urlObj | |
* @param {RequestInit} reqInit | |
*/ | |
async function proxy(urlObj, reqInit) {const res = await fetch(urlObj.href, reqInit) | |
const resHdrOld = res.headers | |
const resHdrNew = new Headers(resHdrOld) | |
const status = res.status | |
if (resHdrNew.has('location')) {let _location = resHdrNew.get('location') | |
if (checkUrl(_location)) | |
resHdrNew.set('location', PREFIX + _location) | |
else { | |
reqInit.redirect = 'follow' | |
return proxy(newUrl(_location), reqInit) | |
} | |
} | |
resHdrNew.set('access-control-expose-headers', '*') | |
resHdrNew.set('access-control-allow-origin', '*') | |
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, | |
}) | |
} |
正文完
发表至: *NIX相关
2024-06-08