forked from ciiiii/cloudflare-docker-proxy
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_worker.js
More file actions
222 lines (199 loc) · 6.25 KB
/
Copy path_worker.js
File metadata and controls
222 lines (199 loc) · 6.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
// Cloudflare Pages Functions implementation
// This file should be placed in the functions/ directory
const dockerHub = "https://registry-1.docker.io";
// Environment variables - these should be set in Cloudflare Pages dashboard
// CUSTOM_DOMAIN, MODE, TARGET_UPSTREAM
const getRoutes = (customDomain) => ({
// production
[`docker.${customDomain}`]: dockerHub,
[`quay.${customDomain}`]: "https://quay.io",
[`gcr.${customDomain}`]: "https://gcr.io",
[`k8s-gcr.${customDomain}`]: "https://k8s.gcr.io",
[`k8s.${customDomain}`]: "https://registry.k8s.io",
[`ghcr.${customDomain}`]: "https://ghcr.io",
[`cloudsmith.${customDomain}`]: "https://docker.cloudsmith.io",
[`ecr.${customDomain}`]: "https://public.ecr.aws",
// staging
[`docker-staging.${customDomain}`]: dockerHub,
});
function routeByHosts(host, routes, mode, targetUpstream) {
if (host in routes) {
return routes[host];
}
if (mode === "debug") {
return targetUpstream;
}
return "";
}
async function handleRequest(request, env) {
const customDomain = env.CUSTOM_DOMAIN || "example.com";
const mode = env.MODE || "production";
const targetUpstream = env.TARGET_UPSTREAM || "";
const routes = getRoutes(customDomain);
const url = new URL(request.url);
if (url.pathname === "/") {
return Response.redirect(url.protocol + "//" + url.host + "/v2/", 301);
}
const upstream = routeByHosts(url.hostname, routes, mode, targetUpstream);
if (upstream === "") {
return new Response(
JSON.stringify({
routes: routes,
}),
{
status: 404,
headers: {
"Content-Type": "application/json",
},
}
);
}
const isDockerHub = upstream === dockerHub;
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);
}
// check if need to authenticate
const resp = await fetch(newUrl.toString(), {
method: "GET",
headers: headers,
redirect: "follow",
});
if (resp.status === 401) {
return responseUnauthorized(url, mode);
}
return resp;
}
// get 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");
// autocomplete repo part into scope for DockerHub library images
// Example: repository:busybox:pull => repository:library/busybox:pull
if (scope && isDockerHub) {
const scopeParts = scope.split(":");
if (scopeParts.length === 3 && !scopeParts[1].includes("/")) {
scopeParts[1] = "library/" + scopeParts[1];
scope = scopeParts.join(":");
}
}
return await fetchToken(wwwAuthenticate, scope, authorization);
}
// redirect for DockerHub library images
// Example: /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");
const redirectUrl = new URL(url);
redirectUrl.pathname = pathParts.join("/");
return Response.redirect(redirectUrl, 301);
}
}
// forward requests
const newUrl = new URL(upstream + url.pathname);
const newReq = new Request(newUrl, {
method: request.method,
headers: request.headers,
body: request.body,
// don't follow redirect to dockerhub blob upstream
redirect: isDockerHub ? "manual" : "follow",
});
const resp = await fetch(newReq);
if (resp.status === 401) {
return responseUnauthorized(url, mode);
}
// handle dockerhub blob redirect manually
if (isDockerHub && resp.status === 307) {
const location = new URL(resp.headers.get("Location"));
const redirectResp = await fetch(location.toString(), {
method: "GET",
redirect: "follow",
});
return redirectResp;
}
return resp;
}
function parseAuthenticate(authenticateStr) {
// sample: Bearer realm="https://auth.ipv6.docker.com/token",service="registry.docker.io"
// match strings after =" and before "
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],
};
}
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 });
}
function responseUnauthorized(url, mode) {
const headers = new Headers();
headers.set("Content-Type", "application/json");
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,
});
}
// Cloudflare Pages Functions export
export default {
async fetch(request, env, ctx) {
try {
return await handleRequest(request, env);
} catch (error) {
console.error("Error handling request:", error);
return new Response(
JSON.stringify({
error: "Internal Server Error",
message: error.message
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
},
}
);
}
},
};