This repository has been archived by the owner on Jan 12, 2024. It is now read-only.
forked from backblaze-b2-samples/cloudflare-b2-proxy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
178 lines (143 loc) · 5.48 KB
/
index.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
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
//
// Proxy Backblaze S3 compatible API requests, sending notifications to a webhook
//
// Adapted from https://github.com/obezuk/worker-signed-s3-template
//
import { AwsClient } from 'aws4fetch'
// Extract the region from the endpoint
const endpointRegex = /^s3\.([a-zA-Z0-9-]+)\.backblazeb2\.com$/;
const [ , aws_region] = AWS_S3_ENDPOINT.match(endpointRegex);
const aws = new AwsClient({
"accessKeyId": AWS_ACCESS_KEY_ID,
"secretAccessKey": AWS_SECRET_ACCESS_KEY,
"service": "s3",
"region": aws_region,
});
const unsignedError =
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Error>
<Code>AccessDenied</Code>
<Message>Unauthenticated requests are not allowed for this api</Message>
</Error>`;
// Could add more detail regarding the specific error, but this enough for now
const validationError =
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ErrorResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<Error>
<Type>Sender</Type>
<Code>SignatureDoesNotMatch</Code>
<Message>Signature validation failed.</Message>
</Error>
<RequestId>0300D815-9252-41E5-B587-F189759A21BF</RequestId>
</ErrorResponse>`;
addEventListener('fetch', function(event) {
event.respondWith(handleRequest(event))
});
// These headers appear in the request, but are not passed upstream
const UNSIGNABLE_HEADERS = [
'x-forwarded-proto',
'x-real-ip',
]
// Filter out cf-* and any other headers we don't want to include in the signature
function filterHeaders(headers) {
return Array.from(headers.entries())
.filter(pair => !UNSIGNABLE_HEADERS.includes(pair[0]) && !pair[0].startsWith('cf-'));
}
function SignatureMissingException() {}
function SignatureInvalidException() {}
// Verify the signature on the incoming request
async function verifySignature(request) {
const authorization = request.headers.get('Authorization');
if (!authorization) {
throw new SignatureMissingException();
}
// Parse the AWS V4 signature value
const re = /^AWS4-HMAC-SHA256 Credential=([^,]+),\s*SignedHeaders=([^,]+),\s*Signature=(.+)$/;
let [ , credential, signedHeaders, signature] = authorization.match(re);
credential = credential.split('/');
signedHeaders = signedHeaders.split(';');
// Verify that the request was signed with the expected key
if (credential[0] != AWS_ACCESS_KEY_ID) {
throw new SignatureInvalidException();
}
// Use the timestamp from the incoming signature
const datetime = request.headers.get('x-amz-date');
// Extract the headers that we want from the complete set of incoming headers
const headersToSign = signedHeaders
.map(key => ({
name: key,
value: request.headers.get(key)
}))
.reduce((obj, item) => (obj[item.name] = item.value, obj), {});
const signedRequest = await aws.sign(request.url, {
method: request.method,
headers: headersToSign,
body: request.body,
aws: { datetime: datetime }
});
// All we need is the signature component of the Authorization header
const [ , , , generatedSignature] = signedRequest.headers.get('Authorization').match(re);
if (signature !== generatedSignature) {
throw new SignatureInvalidException();
}
}
// Where the magic happens...
async function handleRequest(event) {
const request = event.request;
// Set upstream target hostname.
var url = new URL(request.url);
url.hostname = AWS_S3_ENDPOINT;
// Only handle requests signed by our configured key.
try {
await verifySignature(request);
} catch (e) {
// Signature is missing or bad - deny the request
return new Response(
(e instanceof SignatureMissingException) ?
unsignedError :
validationError,
{
status: 403,
headers: {
'Content-Type': 'application/xml',
'Cache-Control': 'max-age=0, no-cache, no-store'
}
});
}
// Certain headers appear in the incoming request but are
// removed from the outgoing request. If they are in the
// signed headers, B2 can't validate the signature.
const headers = filterHeaders(request.headers);
// Sign the new request
var signedRequest = await aws.sign(url, {
method: request.method,
headers: headers,
body: request.body
});
// Send the signed request to B2 and wait for the upstream response
const response = await fetch(signedRequest);
if (WEBHOOK_URL) {
// Convert content length from a string to an integer
let contentLength = request.headers.get('content-length');
contentLength = contentLength ? parseInt(contentLength) : null;
// This will fire the fetch to the webhook asynchronously so the
// response is not delayed.
event.waitUntil(
fetch(WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
contentLength: contentLength,
contentType: request.headers.get('content-type'),
method: request.method,
signatureTimestamp: request.headers.get('x-amz-date'),
status: response.status,
url: response.url
})
})
);
}
return response;
}