forked from milkey-mouse/git-lfs-s3-proxy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
_worker.js
113 lines (93 loc) · 3.07 KB
/
_worker.js
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
import { AwsClient } from "aws4fetch";
const HOMEPAGE = "https://github.com/milkey-mouse/git-lfs-s3-proxy";
const EXPIRY = 3600;
const MIME = "application/vnd.git-lfs+json";
const METHOD_FOR = {
"upload": "PUT",
"download": "GET",
};
async function sign(s3, bucket, path, method) {
const info = { method };
const signed = await s3.sign(
new Request(`https://${bucket}/${path}?X-Amz-Expires=${EXPIRY}`, info),
{ aws: { signQuery: true } }
);
return signed.url;
}
function parseAuthorization(req) {
const auth = req.headers.get("Authorization");
if (!auth) {
throw new Response(null, { status: 401 });
}
const [scheme, encoded] = auth.split(" ");
if (scheme !== "Basic" || !encoded) {
throw new Response(null, { status: 400 });
}
const buffer = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
const decoded = new TextDecoder().decode(buffer).normalize();
const index = decoded.indexOf(":");
if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {
throw new Response(null, { status: 400 });
}
return { user: decoded.slice(0, index), pass: decoded.slice(index + 1) };
}
async function fetch(req, env) {
const url = new URL(req.url);
if (url.pathname == "/") {
if (req.method === "GET") {
return Response.redirect(HOMEPAGE, 302);
} else {
return new Response(null, { status: 405, headers: { "Allow": "GET" } });
}
}
if (!url.pathname.endsWith("/objects/batch")) {
return new Response(null, { status: 404 });
}
if (req.method !== "POST") {
return new Response(null, { status: 405, headers: { "Allow": "POST" } });
}
// in practice, we'd rather not break out-of-spec clients not setting these
/*if (!req.headers.get("Accept").startsWith(MIME)
|| !req.headers.get("Content-Type").startsWith(MIME)) {
return new Response(null, { status: 406 });
}*/
const { user, pass } = parseAuthorization(req);
let s3Options = { accessKeyId: user, secretAccessKey: pass };
const segments = url.pathname.split("/").slice(1, -2);
let params = {};
let bucketIdx = 0;
for (const segment of segments) {
const sliceIdx = segment.indexOf("=");
if (sliceIdx === -1) {
break;
} else {
const key = decodeURIComponent(segment.slice(0, sliceIdx));
const val = decodeURIComponent(segment.slice(sliceIdx + 1));
s3Options[key] = val;
bucketIdx++;
}
}
const s3 = new AwsClient(s3Options);
const bucket = segments.slice(bucketIdx).join("/");
const expires_in = params.expiry || env.EXPIRY || EXPIRY;
const { objects, operation } = await req.json();
const method = METHOD_FOR[operation];
const response = JSON.stringify({
transfer: "basic",
objects: await Promise.all(objects.map(async ({ oid, size }) => ({
oid, size,
authenticated: true,
actions: {
[operation]: { href: await sign(s3, bucket, oid, method), expires_in },
},
}))),
});
return new Response(response, {
status: 200,
headers: {
"Cache-Control": "no-store",
"Content-Type": "application/vnd.git-lfs+json",
},
});
}
export default { fetch };