docker-image/src/handler.ts
2024-06-11 22:07:33 +08:00

241 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

function getHomePageHtml(): string {
return `
<html>
<head>
<title>CZL Docker镜像服务(仅内部用)</title>
<link rel="shortcut icon" href="https://cdn-r2.czl.net/2023/06/20/649168ec9d6a8.ico">
<style>
@font-face{font-family:'CZL';src:url('https://cdn-r2-cloudflare.czl.net/fonts/CZL/CZL_Sans_SC_Thin.woff2') format('woff2');font-weight:100;font-style:normal;font-display:swap}@font-face{font-family:'CZL';src:url('https://cdn-r2-cloudflare.czl.net/fonts/CZL/CZL_Sans_SC_Black.woff2') format('woff2');font-weight:900;font-style:normal;font-display:swap}@font-face{font-family:'CZL';src:url('https://cdn-r2-cloudflare.czl.net/fonts/CZL/CZL_Sans_SC_Bold.woff2') format('woff2');font-weight:bold;font-style:normal;font-display:swap}@font-face{font-family:'CZL';src:url('https://cdn-r2-cloudflare.czl.net/fonts/CZL/CZL_Sans_SC_Light.woff2') format('woff2');font-weight:300;font-style:normal;font-display:swap}@font-face{font-family:'CZL';src:url('https://cdn-r2-cloudflare.czl.net/fonts/CZL/CZL_Sans_SC_Medium.woff2') format('woff2');font-weight:500;font-style:normal;font-display:swap}@font-face{font-family:'CZL';src:url('https://cdn-r2-cloudflare.czl.net/fonts/CZL/CZL_Sans_SC_Regular.woff2') format('woff2');font-weight:normal;font-style:normal;font-display:swap}
*{
font-family: "CZL", -apple-system,BlinkMacSystemFont,'Helvetica Neue',Helvetica,Segoe UI,Arial,Roboto,'PingFang SC',miui,'Hiragino Sans GB','Microsoft Yahei',sans-serif ;
}
body {
font-family: "CZL", -apple-system,BlinkMacSystemFont,'Helvetica Neue',Helvetica,Segoe UI,Arial,Roboto,'PingFang SC',miui,'Hiragino Sans GB','Microsoft Yahei',sans-serif ;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-image: url('https://random-api.czl.net/pic/all');
background-size: cover;
background-position: center;
color: white;
text-shadow: 1px 1px 2px black;
}
.container {
background-color: rgba(0, 0, 0, 0.5);
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
max-width: 600px;
width: 100%;
}
.step {
margin-bottom: 1em;
}
.step label {
margin-bottom: 0.5em;
display: block;
}
.step .input-group {
display: flex;
align-items: center;
}
.step textarea,
.step input {
flex: 1;
padding: 10px;
border-radius: 5px;
border: none;
margin-right: 10px;
color: black;
}
.button, .icon-button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
min-height: 40px;
}
.button:hover, .icon-button:hover {
background-color: #45a049;
}
.icon-button i {
font-size: 18px;
}
</style>
</head>
<body>
<div class="container">
<h1>快捷命令</h1>
<div class="step" style="color:blue;">
提示支持docker.io, ghcr,quay,k8sgcr,gcr, 非docker.io需加上域名前缀
</div>
<div class="step">
<label>第一步:输入原始镜像地址获取命令.</label>
<div class="input-group">
<input type="text" id="imageInput" placeholder="woodchen/simplemirrorfetch" />
<button class="button" id="generateButton">获取命令</button>
</div>
</div>
<div class="step">
<label>第二步:代理拉取镜像</label>
<div class="input-group">
<textarea id="dockerPullCommand" readonly></textarea>
<button class="icon-button" onclick="copyToClipboard('dockerPullCommand')"><i>📋</i></button>
</div>
</div>
<div class="step">
<label>第三步:重命名镜像</label>
<div class="input-group">
<textarea id="dockerTagCommand" readonly></textarea>
<button class="icon-button" onclick="copyToClipboard('dockerTagCommand')"><i>📋</i></button>
</div>
</div>
<div class="step">
<label>第四步:删除代理镜像</label>
<div class="input-group">
<textarea id="dockerRmiCommand" readonly></textarea>
<button class="icon-button" onclick="copyToClipboard('dockerRmiCommand')"><i>📋</i></button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
document.getElementById('generateButton').addEventListener('click', generateCommands);
});
function generateCommands() {
const imageInput = document.getElementById('imageInput').value;
const source = getSourceFromImage(imageInput);
const imageName = getImageNameFromInput(imageInput);
const dockerPullCommand = \`docker pull \${source}/\${imageName}\`;
const dockerTagCommand = \`docker tag \${source}/\${imageName} \${imageName}\`;
const dockerRmiCommand = \`docker rmi \${source}/\${imageName}\`;
document.getElementById('dockerPullCommand').value = dockerPullCommand;
document.getElementById('dockerTagCommand').value = dockerTagCommand;
document.getElementById('dockerRmiCommand').value = dockerRmiCommand;
}
function getSourceFromImage(imageInput) {
const currentDomain = window.location.hostname;
if (imageInput.startsWith("gcr.io/")) {
return \`\${currentDomain}/gcr\`;
} else if (imageInput.startsWith("k8s.gcr.io/")) {
return \`\${currentDomain}/k8sgcr\`;
} else if (imageInput.startsWith("quay.io/")) {
return \`\${currentDomain}/quay\`;
} else if (imageInput.startsWith("ghcr.io/")) {
return \`\${currentDomain}/ghcr\`;
} else {
return currentDomain;
}
}
function getImageNameFromInput(imageInput) {
return imageInput.replace(/^.+?\\//, '');
}
function copyToClipboard(elementId) {
const copyText = document.getElementById(elementId);
copyText.select();
copyText.setSelectionRange(0, 99999); // For mobile devices
document.execCommand('copy');
}
</script>
</body>
</html>
`;
}
import { TokenProvider } from './token'
import { Backend } from './backend'
const PROXY_HEADER_ALLOW_LIST: string[] = ["accept", "user-agent", "accept-encoding"]
const validActionNames = new Set(["manifests", "blobs", "tags", "referrers"])
const ORG_NAME_BACKEND:{ [key: string]: string; } = {
"gcr": "https://gcr.io",
"k8sgcr": "https://k8s.gcr.io",
"quay": "https://quay.io",
"ghcr": "https://ghcr.io"
}
const DEFAULT_BACKEND_HOST: string = "https://registry-1.docker.io"
export async function handleRequest(request: Request): Promise<Response> {
const url = new URL(request.url)
if (url.pathname === '/') {
return new Response(getHomePageHtml(), {
headers: { 'content-type': 'text/html;charset=UTF-8' },
})
}
return handleRegistryRequest(request)
}
function copyProxyHeaders(inputHeaders: Headers) : Headers {
const headers = new Headers;
for(const pair of inputHeaders.entries()) {
if (PROXY_HEADER_ALLOW_LIST.includes(pair[0].toLowerCase())) {
headers.append(pair[0], pair[1])
}
}
return headers;
}
function orgNameFromPath(pathname: string): string|null {
const splitedPath: string[] = pathname.split("/", 3)
if (splitedPath.length === 3 && splitedPath[0] === "" && splitedPath[1] === "v2") {
return splitedPath[2].toLowerCase()
}
return null
}
function hostByOrgName(orgName: string|null): string {
if (orgName !== null && orgName in ORG_NAME_BACKEND) {
return ORG_NAME_BACKEND[orgName]
}
return DEFAULT_BACKEND_HOST
}
function rewritePath(orgName: string | null, pathname: string): string {
let splitedPath = pathname.split("/");
// /v2/repo/manifests/xxx -> /v2/library/repo/manifests/xxx
// /v2/repo/blobs/xxx -> /v2/library/repo/blobs/xxx
if (orgName === null && splitedPath.length === 5 && validActionNames.has(splitedPath[3])) {
splitedPath = [splitedPath[0], splitedPath[1], "library", splitedPath[2], splitedPath[3], splitedPath[4]]
}
if (orgName === null || !(orgName in ORG_NAME_BACKEND)) {
return pathname
}
const cleanSplitedPath = splitedPath.filter(function(value: string, index: number) {
return value !== orgName || index !== 2;
})
return cleanSplitedPath.join("/")
}
async function handleRegistryRequest(request: Request): Promise<Response> {
const reqURL = new URL(request.url)
const orgName = orgNameFromPath(reqURL.pathname)
const pathname = rewritePath(orgName, reqURL.pathname)
const host = hostByOrgName(orgName)
const tokenProvider = new TokenProvider()
const backend = new Backend(host, tokenProvider)
const headers = copyProxyHeaders(request.headers)
return backend.proxy(pathname, {headers: request.headers})
}