import aws s3 impl from worker-pastebin
This commit is contained in:
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