· 5 years ago · Dec 24, 2020, 10:58 PM
1addEventListener('fetch', event => {
2 event.respondWith(handleRequest(event.request))
3})
4
5async function handleRequest(request) {
6 // split access token into the right parts with the right encoding
7 const accessToken = getCookie(request, "accessToken");
8 const partialToken = accessToken.split('.').slice(0, 2).join('.');
9 let signaturePart = accessToken.split('.')[2];
10
11 const username = JSON.parse(atob(accessToken.split('.')[1])).sub;
12
13 signaturePart = signaturePart.replace(/-/g, "+");
14 signaturePart = signaturePart.replace(/_/g, "/");
15
16 // throw error if the access token is invalid
17 if (!await verify(str2ab(partialToken), str2ab(atob(signaturePart))))
18 return new Response("", {status: 401})
19
20 const upload = await request.arrayBuffer();
21 const uploadSize = upload.byteLength;
22
23 // has properties byteLimit and byteUsage
24 const account = JSON.parse(await ACCOUNT_USAGE.get(username))
25
26 if ( uploadSize > (account.byteLimit - account.byteUsage))
27 return new Response("not enough storage", {status: 400})
28
29 if (!request.headers.get("Object-Name") || !request.headers.get("Content-Sha1"))
30 return new Response("missing headers", {status: 400})
31
32 //make sure the file actually uploads before updating account usage
33 if (await uploadToObjectStorage(upload, request.headers)) {
34 const accountUpdated = {
35 byteLimit: account.byteLimit,
36 byteUsage: account.byteUsage + uploadSize
37 }
38 await ACCOUNT_USAGE.put(username, JSON.stringify(accountUpdated))
39
40 return new Response("uploaded", {status: 200})
41 } else {
42 return new Response("upload failed", {status: 500})
43 }
44}
45
46async function uploadToObjectStorage (file, headers) {
47 //storage parameters are stored in KV
48 const authorizationToken = await STORAGE_ACCESS.get("authorizationToken")
49 const uploadUrl = await STORAGE_ACCESS.get("uploadUrl");
50
51 var uploadHeaders = new Headers();
52 uploadHeaders.append("Authorization", authorizationToken);
53 uploadHeaders.append("X-Bz-File-Name", headers.get("Object-Name"));
54 uploadHeaders.append("Content-Type", "application/octet-stream");
55 uploadHeaders.append("X-Bz-Content-Sha1", headers.get("Content-Sha1"));
56
57 var requestOptions = {
58 method: 'POST',
59 headers: uploadHeaders,
60 body: file
61 };
62
63 const uploadResponse = await fetch(uploadUrl, requestOptions).then(response => {
64 return response;
65 })
66
67 return await uploadResponse.text().then(res => {
68 if (JSON.parse((res)).action == "upload") {
69 return true;
70 } else {
71 return false;
72 }
73 })
74}
75
76/**
77 * Everything below this is used to verify the access token
78 * it uses the builtin Crypto API to verify a JWT
79 */
80
81function getCookie(request, name) {
82 let result = ""
83 const cookieString = request.headers.get("Cookie")
84 if (cookieString) {
85 const cookies = cookieString.split(";")
86 cookies.forEach(cookie => {
87 const cookiePair = cookie.split("=", 2)
88 const cookieName = cookiePair[0].trim()
89 if (cookieName === name) {
90 const cookieVal = cookiePair[1]
91 result = cookieVal
92 }
93 })
94 }
95 return result
96}
97
98async function verify (encoded, signature) {
99 const publicKey = await AUTH.get("publicKey");
100
101 const keyObject = await importPublicKey(publicKey);
102
103 return await crypto.subtle.verify(
104 {
105 name: "ECDSA",
106 hash: {name: "SHA-256"},
107 },
108 keyObject,
109 signature,
110 encoded
111 ).catch((error) => {
112 console.log(error);
113 });
114}
115
116function str2ab(str) {
117 const buf = new ArrayBuffer(str.length);
118 const bufView = new Uint8Array(buf);
119 for (let i = 0, strLen = str.length; i < strLen; i++) {
120 bufView[i] = str.charCodeAt(i);
121 }
122 return buf;
123}
124
125async function importPublicKey(pem) {
126 // fetch the part of the PEM string between header and footer
127 const pemHeader = "-----BEGIN PUBLIC KEY-----";
128 const pemFooter = "-----END PUBLIC KEY-----";
129 const pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length);
130 // base64 decode the string to get the binary data
131 const binaryDerString = atob(pemContents);
132 // convert from a binary string to an ArrayBuffer
133 const binaryDer = str2ab(binaryDerString);
134
135 return await crypto.subtle.importKey(
136 "spki",
137 binaryDer,
138 {
139 name: "ECDSA",
140 hash: {name: "SHA-256"},
141 namedCurve: "P-256"
142 },
143 false,
144 ["verify"]
145 );
146}
147