forked from mhart/aws4fetch
-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.js
268 lines (227 loc) · 8.51 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
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
/** Skip to the bottom of this file to modify the actual fetch() **/
const encoder = new TextEncoder('utf-8')
let env
try {
env = {
ACCESS_KEY_ID, SECRET_ACCESS_KEY, S3_REGION, S3_BUCKET_NAME
}
} catch (e) {
console.log(`${e}\nVariables not yet defined. Check back when setup is complete!`)
env = {
ACCESS_KEY_ID: '', SECRET_ACCESS_KEY: '', S3_REGION: '', S3_BUCKET_NAME: ''
}
}
class Algo {
static async hmac (key, string, encoding) {
const cryptoKey = await crypto.subtle.importKey(
'raw',
typeof key === 'string' ? encoder.encode(key) : key,
{ name: 'HMAC', hash: { name: 'SHA-256' } },
false,
['sign']
)
const signed = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(string))
return encoding === 'hex' ? this.buf2hex(signed) : signed
}
static async hash (content, encoding) {
const digest = await crypto.subtle.digest('SHA-256', typeof content === 'string' ? encoder.encode(content) : content)
return encoding === 'hex' ? this.buf2hex(digest) : digest
}
static buf2hex (buffer) {
return Array.prototype.map.call(new Uint8Array(buffer), x => ('0' + x.toString(16)).slice(-2)).join('')
}
static encodeRfc3986 (urlEncodedStr) {
return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase())
}
}
class AwsClient {
constructor (sessionToken) {
this.sessionToken = sessionToken
}
static corsResponse (headers = null) {
if (!headers) {
return new Response('', {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type,Authorization',
'Access-Control-Allow-Methods': 'GET,OPTIONS,POST',
'Access-Control-Max-Age': 86400
}
})
}
return new Response('', { headers })
}
static async sign (input, init) {
let hdrs = {}
if (typeof input === 'string') {
console.log('true')
input = new URL(input)
} else if (input instanceof Request) {
hdrs = new Headers(input.headers)
const { method, headers, body } = input
if (init) init = Object.assign({ method, url, headers }, init)
if (['GET', 'HEAD', 'OPTIONS'].includes(input.method) == null && init.body == null && headers.has('Content-Type')) {
init.body = body != null && headers.has('X-Amz-Content-Sha256') ? body : await input.clone().arrayBuffer()
}
input = new URL(input.url)
} else {
return new Response('Runtime error - can only sign URLs or Requests', { status: 400 })
}
if (['us-east', 'us-east-1'].includes(env.S3_REGION)) {
input.hostname = `s3.amazonaws.com`
} else {
input.hostname = `s3-${env.S3_REGION}.amazonaws.com`
}
input.pathname = `/${env.S3_BUCKET_NAME}${input.pathname}`
console.log(input.href)
const signer = new AwsV4Signer(Object.assign({ url: input }, init, AwsClient, init && init.aws))
const signed = Object.assign({}, init, await signer.sign())
delete signed.aws
if (hdrs instanceof Headers) {
for (const [k, v] of hdrs.entries()) {
signed.headers.append(k, v)
}
}
return new Request(signed.url, signed)
}
}
class AwsV4Signer {
constructor ({ method, url, headers, body, sessionToken, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) {
this.method = method || (body ? 'POST' : 'GET')
this.url = new URL(url)
this.headers = new Headers(headers)
this.body = body
this.sessionToken = sessionToken
this.cache = cache || new Map()
this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, '')
this.signQuery = signQuery
this.appendSessionToken = appendSessionToken
this.headers.delete('Host') // Can't be set in insecure env anyway
const params = this.signQuery ? this.url.searchParams : this.headers
if (!this.headers.has('X-Amz-Content-Sha256')) this.headers.set('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD')
params.set('X-Amz-Date', this.datetime)
if (this.sessionToken && !this.appendSessionToken) params.set('X-Amz-Security-Token', this.sessionToken)
// headers are always lowercase in keys()
this.signableHeaders = ['host', ...this.headers.keys()]
.filter(header => allHeaders || !this.unsignableHeaders.includes(header))
.sort()
this.signedHeaders = this.signableHeaders.join(';')
// headers are always trimmed:
// https://fetch.spec.whatwg.org/#concept-header-value-normalize
this.canonicalHeaders = this.signableHeaders
.map(header => header + ':' + (header === 'host' ? this.url.host : this.headers.get(header).replace(/\s+/g, ' ')))
.join('\n')
this.credentialString = [this.datetime.slice(0, 8), env.S3_REGION, 's3', 'aws4_request'].join('/')
if (this.signQuery) {
if (!params.has('X-Amz-Expires')) params.set('X-Amz-Expires', 86400)
params.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256')
params.set('X-Amz-Credential', env.ACCESS_KEY_ID + '/' + this.credentialString)
params.set('X-Amz-SignedHeaders', this.signedHeaders)
}
this.encodedPath = decodeURIComponent(this.url.pathname).replace(/\+/g, ' ')
if (!this.encodedPath) {
this.encodedPath = this.url.pathname
}
if (!singleEncode) {
this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, '/')
}
this.encodedPath = Algo.encodeRfc3986(this.encodedPath)
const seenKeys = new Set()
this.encodedSearch = [...this.url.searchParams]
.filter(([k]) => {
if (!k) return false // no empty keys
if (seenKeys.has(k)) return false // first val only for S3
seenKeys.add(k)
return true
})
.map(pair => pair.map(p => Algo.encodeRfc3986(encodeURIComponent(p))))
.sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0)
.map(pair => pair.join('='))
.join('&')
}
get unsignableHeaders () {
return [
'authorization',
'content-type',
'content-length',
'user-agent',
'presigned-expires',
'expect',
'x-amzn-trace-id',
'x-forwarded-proto',
'range'
]
}
async sign () {
if (this.signQuery) {
this.url.searchParams.set('X-Amz-Signature', await this.signature())
if (this.sessionToken && this.appendSessionToken) {
this.url.searchParams.set('X-Amz-Security-Token', this.sessionToken)
}
} else {
this.headers.set('Authorization', await this.authHeader())
}
return {
method: this.method,
url: this.url,
headers: this.headers,
body: this.body
}
}
async authHeader () {
return [
'AWS4-HMAC-SHA256 Credential=' + env.ACCESS_KEY_ID + '/' + this.credentialString,
'SignedHeaders=' + this.signedHeaders,
'Signature=' + (await this.signature())
].join(', ')
}
async signature () {
const date = this.datetime.slice(0, 8)
const cacheKey = [env.SECRET_ACCESS_KEY, date, env.S3_REGION, 's3'].join()
let kCredentials = this.cache.get(cacheKey)
if (!kCredentials) {
const kDate = await Algo.hmac('AWS4' + env.SECRET_ACCESS_KEY, date)
const kRegion = await Algo.hmac(kDate, env.S3_REGION)
const kService = await Algo.hmac(kRegion, 's3')
kCredentials = await Algo.hmac(kService, 'aws4_request')
this.cache.set(cacheKey, kCredentials)
}
return Algo.hmac(kCredentials, await this.stringToSign(), 'hex')
}
async stringToSign () {
return [
'AWS4-HMAC-SHA256',
this.datetime,
this.credentialString,
await Algo.hash(await this.canonicalString(), 'hex')
].join('\n')
}
async canonicalString () {
return [
this.method,
this.encodedPath,
this.encodedSearch,
this.canonicalHeaders + '\n',
this.signedHeaders,
await this.hexBodyHash()
].join('\n')
}
async hexBodyHash () {
if (this.headers.has('X-Amz-Content-Sha256')) return this.headers.get('X-Amz-Content-Sha256')
return Algo.hash(this.body || '', 'hex')
}
}
/** Modify this portion if you need to customize the request **/
addEventListener('fetch', event => {
event.respondWith(handle(event.request))
event.passThroughOnException()
})
async function handle (request) {
if (request.method === 'OPTIONS') return AwsClient.corsResponse()
/* Sign the request, preserve the headers and the request body */
/* This is the recommended method */
let signedRequest = await AwsClient.sign(request)
let response = await fetch(signedRequest)
if (response.status > 400) response = new Response('Setup not yet complete!')
return response
}