import aws s3 impl from worker-pastebin

This commit is contained in:
Peter Cai 2021-11-24 15:37:47 -05:00
parent f8a89feedf
commit fef4f9a2ac
3 changed files with 238 additions and 0 deletions

118
src/aws/auth.coffee Normal file
View File

@ -0,0 +1,118 @@
import * as crypto from '../crypto'
# Implement AWS signature authentication
class AwsAuth
constructor: (url, conf) ->
@url = new URL url
@date = new Date()
@conf = conf
@region = conf.region
# Setters
setMethod: (method) ->
@method = method.toUpperCase()
@
setQueryStringMap: (qsMap) ->
@qsMap = qsMap
@
setHeaderMap: (headerMap) ->
# headers MUST contain `x-amz-content-sha256: UNSIGNED-PAYLOAD`
@headerMap =
'host': @url.host
for [key, val] in Object.entries headerMap
key = key.toLowerCase()
val = val.trim()
if key == "content-type" || key.startsWith "x-amz-"
@headerMap[key] = val
if !@headerMap['x-amz-content-sha256']
throw "Must contain sha256 header"
@
setRegion: (region) ->
@region = region
@
setService: (service) ->
@service = service # "s3"
@
# Signature calculation: <https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html>
authorizedHeader: (origHeader) ->
origHeader['x-amz-date'] = @timeStampISO8601()
origHeader['authorization'] = await @authorizationHeader()
authorizationHeader: ->
"AWS4-HMAC-SHA256 " +
"Credential=" + @credential() + "," +
"SignedHeaders=" + @signedHeaders() + "," +
"Signature=" + await @calculateSignature()
credential: ->
@conf.accessKeyId + "/" + @scope()
calculateSignature: ->
crypto.hex await crypto.HMAC_SHA256 await @signingKey(), await @stringToSign()
signingKey: ->
accessKey = crypto.utf8Bytes "AWS4" + @conf.secretAccessKey
dateKey = await crypto.HMAC_SHA256 accessKey, @timeStampYYYYMMDD()
dateRegionKey = await crypto.HMAC_SHA256 dateKey, @region
dateRegionServiceKey = await crypto.HMAC_SHA256 dateRegionKey, @service
crypto.HMAC_SHA256 dateRegionServiceKey, "aws4_request"
stringToSign: ->
"AWS4-HMAC-SHA256" + "\n" +
@timeStampISO8601() + "\n" +
@scope() + "\n" +
await crypto.hex await crypto.SHA256 @canonicalRequest()
scope: ->
@timeStampYYYYMMDD() + "/" +
@region + "/" + @service + "/aws4_request"
canonicalRequest: ->
@method + "\n" +
@canonicalURI() + "\n" +
@canonicalQueryString() + "\n" +
@canonicalHeaders() + "\n" +
@signedHeaders() + "\n" +
@headerMap['x-amz-content-sha256']
canonicalURI: ->
# new URL already handles URI encoding for the pathname. No need to repeat here.
@url.pathname
canonicalQueryString: ->
return "" if not @qsMap
[...Object.entries @qsMap].sort()
.map (pair) ->
pair.map (x) -> encodeURIComponent x
.join "="
.join "&"
canonicalHeaders: ->
([...Object.entries @headerMap].sort()
.map (pair) ->
pair.join ':'
.join "\n") + "\n" # There's a trailing "\n"
signedHeaders: ->
[...Object.entries @headerMap].sort()
.map (pair) -> pair[0]
.join ";"
pad: (num) ->
if num < 10
"0" + num
else
num
timeStampISO8601: ->
@timeStampYYYYMMDD() + "T" +
@pad(@date.getUTCHours()) +
@pad(@date.getUTCMinutes()) +
@pad(@date.getUTCSeconds()) + "Z"
timeStampYYYYMMDD: ->
"#{@date.getUTCFullYear()}" +
"#{@pad(@date.getUTCMonth() + 1)}" +
"#{@pad(@date.getUTCDate())}"
export default AwsAuth

88
src/aws/s3.coffee Normal file
View File

@ -0,0 +1,88 @@
import AwsAuth from "./auth"
import parser from "fast-xml-parser"
class S3
constructor: (@conf) ->
@baseURL = @conf.s3.endpoint + "/" + @conf.s3.bucket + "/"
# Wrapper of Fetch API with automatic AWS signature
# Note that query string in url is not supported
# options.method must be specified
request: (url, qsMap, options) ->
# We only support unsigned payload for now
options.headers['x-amz-content-sha256'] = 'UNSIGNED-PAYLOAD'
# Sign the request
auth = new AwsAuth url, @conf.aws
.setMethod options.method
.setQueryStringMap qsMap
.setHeaderMap options.headers
.setService "s3"
# Write needed authorization headers to header object
await auth.authorizedHeader options.headers
fetch url + "?" + auth.canonicalQueryString(), options
# Used for some requests to convert params to snake-cased x-amz-*
camelToSnake: (str) ->
str.replace /([A-Z])/g, "-$1"
.toLowerCase()[1..]
makeHeaders: (params) -> Object.fromEntries do =>
Object.entries params
.map ([key, val]) =>
key = @camelToSnake key
if not (key == "expires" or key.startsWith "content-" or key.startsWith "cache-")
key = 'x-amz-' + key
[key, val]
# See AWS docs for params
# params are passed in query string
# Paging not implemented yet; Need to
listObjects: (params) ->
# Send request using params as query string
resp = await @request @baseURL, params,
method: "GET"
headers:
'x-amz-request-payer': 'requester'
txt = await resp.text()
console.log txt
if not resp.ok
# no error handling yet
throw txt
result = parser.parse txt,
arrayMode: true
console.log result
if not result.ListBucketResult
return null
result.ListBucketResult[0]
# params are in CamelCase, but converted to x-amz-* headers automatically
# Content* headers are exempt from the x-amz-* prefix, as well as Expires
# data can be a buffer or a readable stream
putObject: (key, data, params) ->
# Convert camel-cased params to snake-cased headers
headers = @makeHeaders params
# Send request
resp = await @request @baseURL + key, null,
method: 'PUT'
headers: headers
body: data
txt = await resp.text()
console.log txt
if not resp.ok
# no error handling yet
throw resp.status
else
txt
# params are processed similar to putObject
# returns the full response object (because content-range may be needed)
getObject: (key, params) ->
@request @baseURL + key, null,
method: 'GET'
headers: @makeHeaders params
export default S3

32
src/crypto.coffee Normal file
View File

@ -0,0 +1,32 @@
utf8Bytes = (str) ->
new TextEncoder 'utf-8'
.encode str
fromUtf8Bytes = (bytes) ->
new TextDecoder 'utf-8'
.decode bytes
hex = (buf) -> (Array::map.call new Uint8Array(buf),
(x) => ('00' + x.toString 16).slice(-2)).join ''
fromHex = (str) ->
new Uint8Array str.match(/.{1,2}/g).map (byte) => parseInt byte, 16
HMAC_SHA256_KEY = (buf) ->
crypto.subtle.importKey 'raw', buf,
{ name: 'HMAC', hash: 'SHA-256' }, true, [ 'sign' ]
HMAC_SHA256 = (key, str) ->
cryptoKey = await HMAC_SHA256_KEY key
buf = utf8Bytes str
await crypto.subtle.sign "HMAC", cryptoKey, buf
SHA256 = (str) ->
crypto.subtle.digest "SHA-256", utf8Bytes str
export {
utf8Bytes,
hex,
HMAC_SHA256,
SHA256,
}