import aws s3 impl from worker-pastebin
parent
f8a89feedf
commit
fef4f9a2ac
@ -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
|
@ -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
|
@ -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,
|
||||
}
|
Loading…
Reference in New Issue